@lovelace_lol/embody 0.0.14 → 0.0.16

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
@@ -1,10 +1,10 @@
1
- import * as THREE10 from 'three';
2
- import { NoBlending, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NormalBlending, Timer, Quaternion, Vector3, Matrix4, AnimationMixer, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopOnce, LoopRepeat, LoopPingPong, PropertyBinding, Euler, AnimationUtils, InterpolateDiscrete, InterpolateLinear, Box3, MathUtils, Mesh, PlaneGeometry, ShadowMaterial, HemisphereLight, DirectionalLight, SRGBColorSpace, ACESFilmicToneMapping, WebGLRenderer, Scene, Color, PerspectiveCamera, PCFShadowMap, PCFSoftShadowMap, NumberKeyframeTrack, QuaternionKeyframeTrack, VectorKeyframeTrack, AnimationClip, PMREMGenerator, FloatType, Float32BufferAttribute, BufferAttribute, WebGPUCoordinateSystem, WebGLCoordinateSystem } from 'three';
3
- import { CC4_HUMANOID_SKELETON_TEMPLATE, HUMANOID_SKELETON_TEMPLATES, DEFAULT_HAIR_COLOR_APPEARANCE, requireInitializedEmbodyCore, initEmbodyCore } from '@lovelace_lol/embody/wasm';
1
+ import * as THREE11 from 'three';
2
+ import { Matrix4, NoBlending, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NormalBlending, Timer, Quaternion, Vector3, NormalAnimationBlendMode, InterpolateLinear, InterpolateDiscrete, QuaternionKeyframeTrack, VectorKeyframeTrack, AnimationClip, LoadingManager, AnimationMixer, AdditiveAnimationBlendMode, LoopOnce, LoopRepeat, LoopPingPong, PropertyBinding, Euler, AnimationUtils, Box3, MathUtils, Mesh, PlaneGeometry, ShadowMaterial, HemisphereLight, DirectionalLight, SRGBColorSpace, ACESFilmicToneMapping, WebGLRenderer, Scene, Color, PerspectiveCamera, PCFShadowMap, PCFSoftShadowMap, NumberKeyframeTrack, MeshBasicMaterial, TextureLoader, Texture, PMREMGenerator, Group, FloatType, Float32BufferAttribute, BufferAttribute, EquirectangularReflectionMapping, CanvasTexture, WebGPUCoordinateSystem, WebGLCoordinateSystem } from 'three';
3
+ import { CC4_HUMANOID_SKELETON_TEMPLATE, HUMANOID_SKELETON_TEMPLATES, DEFAULT_HAIR_COLOR_APPEARANCE, initEmbodyCore, requireInitializedEmbodyCore } from '@lovelace_lol/embody/wasm';
4
4
  export * from '@lovelace_lol/embody/wasm';
5
5
  export { CC4_HUMANOID_SKELETON_TEMPLATE, DEFAULT_AXIS_TO_CHANNEL, DEFAULT_BONE_MAX_DEGREES, DEFAULT_HAIR_COLOR_APPEARANCE, HAIR_COLOR_PRESETS, HUMANOID_SKELETON_TEMPLATES, JOINT_CONTROL_SECTION, LEGACY_BODY_CONTROL_AUS, VISEME_KEYS, getAUDescriptors } from '@lovelace_lol/embody/wasm';
6
- import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
7
6
  import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
7
+ import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
8
8
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
9
9
  import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
10
10
 
@@ -33,46 +33,72 @@ function annotationProfileInput(profile) {
33
33
  }
34
34
  return result;
35
35
  }
36
- function getRuntimeAnnotationSide(region) {
37
- return annotationQuery("side", { region });
38
- }
39
- function getRuntimeAUMeshSideOffset(region, sideSign, modelSize) {
40
- const value = annotationQuery("meshSideOffset", { region, sideSign, size: modelSize });
41
- return value ? new THREE10.Vector3(value.x, value.y, value.z) : null;
36
+ function readAttribute(attribute) {
37
+ const values = new Float32Array(attribute.count * attribute.itemSize);
38
+ for (let index = 0; index < attribute.count; index++) for (let axis = 0; axis < attribute.itemSize; axis++) values[index * attribute.itemSize + axis] = attribute.getComponent(index, axis);
39
+ return values;
42
40
  }
43
- function getRuntimeAUMorphAnchorPoint(region, mesh) {
44
- const core = requireInitializedEmbodyCore();
45
- const names = annotationQuery("morphNames", { region });
46
- const positions = mesh.geometry.getAttribute("position");
47
- if (!positions) return null;
41
+ function readMorphGeometry(mesh, names) {
42
+ const position = mesh.geometry.getAttribute("position");
43
+ if (!position || position.itemSize !== 3) return null;
48
44
  const attributes = names.flatMap((name) => {
49
45
  const index = mesh.morphTargetDictionary?.[name];
50
46
  const attribute = index == null ? void 0 : mesh.geometry.morphAttributes.position?.[index];
51
47
  return attribute ? [attribute] : [];
52
48
  });
53
- const base = readAttribute(positions);
49
+ if (!attributes.length || attributes.some((attribute) => attribute.count !== position.count || attribute.itemSize !== 3)) return null;
50
+ const base = readAttribute(position);
54
51
  const morphs = new Float32Array(attributes.length * base.length);
55
52
  attributes.forEach((attribute, index) => morphs.set(readAttribute(attribute), index * base.length));
56
- const candidates = core.annotation_morph_candidates(base, morphs, mesh.geometry.morphTargetsRelative);
57
- mesh.updateWorldMatrix(true, false);
53
+ return { base, morphs, relative: mesh.geometry.morphTargetsRelative };
54
+ }
55
+ function updateAnnotationWorldMatrices(model) {
56
+ model.parent?.updateWorldMatrix(true, false);
57
+ model.updateMatrixWorld(true);
58
+ }
59
+ function sampleMorphVertices(mesh, indices, samples, vertex = new THREE11.Vector3()) {
58
60
  const skinned = mesh;
59
- if (skinned.isSkinnedMesh) skinned.skeleton.update();
60
- const samples = new Float32Array(candidates.length / 2 * 4);
61
- const vertex = new THREE10.Vector3();
62
- for (let offset = 0; offset < candidates.length; offset += 2) {
63
- mesh.getVertexPosition(candidates[offset], vertex).applyMatrix4(mesh.matrixWorld);
64
- samples.set([vertex.x, vertex.y, vertex.z, candidates[offset + 1]], offset * 2);
61
+ if (skinned.isSkinnedMesh) {
62
+ for (const bone of skinned.skeleton.bones) bone.updateWorldMatrix(true, false);
63
+ skinned.skeleton.update();
64
+ }
65
+ for (let offset = 0; offset < indices.length; offset++) {
66
+ mesh.getVertexPosition(indices[offset], vertex).applyMatrix4(mesh.matrixWorld);
67
+ samples[offset * 3] = vertex.x;
68
+ samples[offset * 3 + 1] = vertex.y;
69
+ samples[offset * 3 + 2] = vertex.z;
65
70
  }
66
- const value = core.annotation_morph_center(JSON.stringify(region), samples);
67
- return value.length === 3 ? new THREE10.Vector3().fromArray(value) : null;
68
71
  }
72
+ var MorphGeometryObservation = class {
73
+ constructor() {
74
+ __publicField(this, "revision", 0);
75
+ __publicField(this, "namesJson", "[]");
76
+ __publicField(this, "signature", []);
77
+ }
78
+ observe(mesh) {
79
+ const dictionary = Object.entries(mesh.morphTargetDictionary ?? {}).sort(([a], [b]) => a.localeCompare(b));
80
+ const attributes = [mesh.geometry.getAttribute("position"), ...mesh.geometry.morphAttributes.position ?? []];
81
+ const signature = [mesh.geometry, mesh.geometry.morphTargetsRelative, ...dictionary.flat()];
82
+ for (const attribute of attributes) {
83
+ signature.push(attribute);
84
+ if (attribute) signature.push(
85
+ attribute.array,
86
+ attribute.count,
87
+ attribute.itemSize,
88
+ attribute.normalized,
89
+ "data" in attribute ? attribute.data.version : attribute.version,
90
+ "data" in attribute ? attribute.data.stride : 0,
91
+ "offset" in attribute ? attribute.offset : 0
92
+ );
93
+ }
94
+ if (signature.length === this.signature.length && signature.every((value, index) => value === this.signature[index])) return;
95
+ this.signature = signature;
96
+ this.revision++;
97
+ this.namesJson = JSON.stringify(dictionary.map(([name]) => name));
98
+ }
99
+ };
69
100
 
70
101
  // three/annotations/modelObservation.ts
71
- function readAttribute(attribute) {
72
- const values = new Float32Array(attribute.count * attribute.itemSize);
73
- for (let index = 0; index < attribute.count; index++) for (let axis = 0; axis < attribute.itemSize; axis++) values[index * attribute.itemSize + axis] = attribute.getComponent(index, axis);
74
- return values;
75
- }
76
102
  var bounds = (box) => box.isEmpty() ? null : { min: box.min.toArray(), max: box.max.toArray() };
77
103
  var AnnotationModelObserver = class {
78
104
  constructor(model) {
@@ -80,6 +106,10 @@ var AnnotationModelObserver = class {
80
106
  __publicField(this, "objects", /* @__PURE__ */ new Map());
81
107
  __publicField(this, "meshes", []);
82
108
  __publicField(this, "ids", /* @__PURE__ */ new Map());
109
+ __publicField(this, "morphGeneration", null);
110
+ __publicField(this, "morphObservations", []);
111
+ __publicField(this, "morphGeometry", /* @__PURE__ */ new Map());
112
+ __publicField(this, "vertex", new THREE11.Vector3());
83
113
  model.traverse((object) => {
84
114
  const id = this.objects.size + 1;
85
115
  this.ids.set(object, id);
@@ -90,13 +120,16 @@ var AnnotationModelObserver = class {
90
120
  id(object) {
91
121
  return this.ids.get(object);
92
122
  }
123
+ updateWorldMatrices() {
124
+ updateAnnotationWorldMatrices(this.model);
125
+ }
93
126
  capture() {
94
- this.model.updateWorldMatrix(true, true);
95
- const modelBox = new THREE10.Box3().setFromObject(this.model);
96
- if (modelBox.isEmpty()) modelBox.setFromCenterAndSize(this.model.getWorldPosition(new THREE10.Vector3()), new THREE10.Vector3());
127
+ this.updateWorldMatrices();
128
+ const modelBox = new THREE11.Box3().setFromObject(this.model);
129
+ if (modelBox.isEmpty()) modelBox.setFromCenterAndSize(this.model.getWorldPosition(new THREE11.Vector3()), new THREE11.Vector3());
97
130
  return {
98
131
  bounds: bounds(modelBox),
99
- quaternion: this.model.getWorldQuaternion(new THREE10.Quaternion()).toArray(),
132
+ quaternion: this.model.getWorldQuaternion(new THREE11.Quaternion()).toArray(),
100
133
  inverse: this.model.matrixWorld.clone().invert().toArray(),
101
134
  objects: [...this.objects].map(([id, object]) => {
102
135
  const mesh = object;
@@ -106,8 +139,8 @@ var AnnotationModelObserver = class {
106
139
  name: object.name,
107
140
  kind: mesh.isMesh ? "Mesh" : bone ? "Bone" : object.type,
108
141
  parent: object.parent ? this.ids.get(object.parent) ?? null : null,
109
- position: object.getWorldPosition(new THREE10.Vector3()).toArray(),
110
- bounds: bone ? null : bounds(new THREE10.Box3().setFromObject(object)),
142
+ position: object.getWorldPosition(new THREE11.Vector3()).toArray(),
143
+ bounds: bone ? null : bounds(new THREE11.Box3().setFromObject(object)),
111
144
  matrix: object.matrixWorld.toArray(),
112
145
  inverse: object.matrixWorld.clone().invert().toArray(),
113
146
  morphNames: Object.keys(mesh.morphTargetDictionary ?? {})
@@ -121,7 +154,7 @@ var AnnotationModelObserver = class {
121
154
  transforms(runtime) {
122
155
  const ids = runtime.tracked_objects();
123
156
  if (!ids.length) return new Float32Array();
124
- this.model.updateWorldMatrix(true, true);
157
+ this.updateWorldMatrices();
125
158
  const values = new Float32Array(ids.length * 17);
126
159
  ids.forEach((id, index) => {
127
160
  const object = this.objects.get(id);
@@ -132,16 +165,51 @@ var AnnotationModelObserver = class {
132
165
  });
133
166
  return values;
134
167
  }
135
- morphAnchors(runtime) {
168
+ prepareMorphAnchors(runtime) {
169
+ const generation = runtime.generation();
170
+ if (!runtime.is_current(generation)) return;
171
+ if (this.morphGeneration !== generation) this.morphObservations = [];
172
+ this.morphGeneration = generation;
173
+ const changed = /* @__PURE__ */ new Set();
174
+ for (const mesh of this.meshes) {
175
+ const id = this.ids.get(mesh);
176
+ let observation = this.morphGeometry.get(id);
177
+ if (!observation) {
178
+ observation = new MorphGeometryObservation();
179
+ this.morphGeometry.set(id, observation);
180
+ }
181
+ observation.observe(mesh);
182
+ if (runtime.observe_morph_geometry(generation, id, observation.revision, observation.namesJson)) changed.add(id);
183
+ }
184
+ if (!changed.size) return;
185
+ this.morphObservations = this.morphObservations.filter((observation) => !changed.has(observation.objectId));
136
186
  const requests = JSON.parse(runtime.command("morphRequests", "{}", performance.now()));
137
- const anchors = {};
138
187
  for (const request3 of requests) {
188
+ if (!changed.has(request3.objectId)) continue;
139
189
  const mesh = this.objects.get(request3.objectId);
140
190
  if (!mesh?.isMesh) continue;
141
- const point2 = getRuntimeAUMorphAnchorPoint(request3.region, mesh);
142
- if (point2) anchors[request3.region.name] = { x: point2.x, y: point2.y, z: point2.z };
191
+ const geometry = readMorphGeometry(mesh, request3.morphNames);
192
+ if (!geometry) continue;
193
+ const plan = runtime.prepare_morph_anchor(generation, request3.objectId, request3.region.name, geometry.base, geometry.morphs, geometry.relative);
194
+ if (plan.length < 2) continue;
195
+ const indices = plan.slice(1);
196
+ this.morphObservations.push({ planId: plan[0], objectId: request3.objectId, mesh, indices, samples: new Float32Array(indices.length * 3) });
197
+ }
198
+ }
199
+ /** Transport only: Rust owns the vertex selection, weights and live anchor. */
200
+ observeMorphAnchors(runtime) {
201
+ this.prepareMorphAnchors(runtime);
202
+ if (this.morphGeneration === null || !runtime.is_current(this.morphGeneration)) {
203
+ this.morphObservations = [];
204
+ this.morphGeneration = null;
205
+ return;
206
+ }
207
+ if (!this.morphObservations.length) return;
208
+ this.updateWorldMatrices();
209
+ for (const { planId, mesh, indices, samples } of this.morphObservations) {
210
+ sampleMorphVertices(mesh, indices, samples, this.vertex);
211
+ runtime.observe_morph_anchor(this.morphGeneration, planId, samples);
143
212
  }
144
- return anchors;
145
213
  }
146
214
  /** Rust selects influenced vertices; Three evaluates the current native pose. */
147
215
  observeFocus(runtime, request3) {
@@ -162,7 +230,7 @@ var AnnotationModelObserver = class {
162
230
  if (index < 0 || !skinIndices || !skinWeights) continue;
163
231
  mesh.skeleton.update();
164
232
  const selected = core.annotation_bone_vertices(readAttribute(skinIndices), readAttribute(skinWeights), new Uint32Array([index]));
165
- const vertex = new THREE10.Vector3();
233
+ const vertex = new THREE11.Vector3();
166
234
  for (const selectedIndex of selected) {
167
235
  mesh.getVertexPosition(selectedIndex, vertex).applyMatrix4(mesh.matrixWorld);
168
236
  points.push(vertex.x, vertex.y, vertex.z);
@@ -192,9 +260,9 @@ function bindSceneRendering(controller, scene, camera, binding) {
192
260
  if (owner.scene !== scene || owner.camera !== camera) throw new Error("Rendering and camera controllers must share their scene and camera.");
193
261
  if (owner.binding) throw new Error("Character scene rendering already has a camera controller.");
194
262
  owner.binding = binding;
195
- const snapshot2 = controller.getSnapshot();
196
- if (snapshot2.renderer) void binding.commit(snapshot2.renderer).catch(binding.onError);
197
- if (snapshot2.status !== "ready" && snapshot2.status !== "error") void binding.suspend().catch(binding.onError);
263
+ const snapshot3 = controller.getSnapshot();
264
+ if (snapshot3.renderer) void binding.commit(snapshot3.renderer).catch(binding.onError);
265
+ if (snapshot3.status !== "ready" && snapshot3.status !== "error") void binding.suspend().catch(binding.onError);
198
266
  return () => {
199
267
  if (owner.binding === binding) {
200
268
  owner.drain = binding.suspend();
@@ -236,8 +304,8 @@ function prepareModelSkinning(model) {
236
304
  };
237
305
  }
238
306
  var bindings = /* @__PURE__ */ new WeakMap();
239
- function bindPlaybackRendering(model, mixer, afterFrame) {
240
- const binding = { mixer, enabled: true, afterFrame };
307
+ function bindPlaybackRendering(model, mixer, afterFrame, afterSample) {
308
+ const binding = { mixer, enabled: true, disposed: false, afterFrame, afterSample };
241
309
  const entries = bindings.get(model) ?? /* @__PURE__ */ new Set();
242
310
  entries.add(binding);
243
311
  bindings.set(model, entries);
@@ -246,6 +314,7 @@ function bindPlaybackRendering(model, mixer, afterFrame) {
246
314
  binding.enabled = enabled;
247
315
  },
248
316
  dispose() {
317
+ binding.disposed = true;
249
318
  entries.delete(binding);
250
319
  if (!entries.size) bindings.delete(model);
251
320
  }
@@ -262,8 +331,16 @@ function renderScenePlayback(scene, timer) {
262
331
  }
263
332
  }
264
333
  });
265
- for (const mixer of mixers) mixer.update(timer.getDelta());
266
- for (const binding of active) binding.afterFrame();
334
+ const deltaSeconds = timer.getDelta();
335
+ const elapsed = /* @__PURE__ */ new Map();
336
+ for (const mixer of mixers) {
337
+ elapsed.set(mixer, Math.abs(deltaSeconds * mixer.timeScale));
338
+ mixer.update(deltaSeconds);
339
+ }
340
+ for (const binding of active) if (binding.enabled && !binding.disposed) {
341
+ binding.afterSample?.(elapsed.get(binding.mixer));
342
+ }
343
+ for (const binding of active) if (binding.enabled && !binding.disposed) binding.afterFrame(deltaSeconds);
267
344
  }
268
345
  function createScenePlaybackFrame(scene, document2) {
269
346
  const timer = new Timer();
@@ -349,12 +426,12 @@ function compileModelForRender(renderer, scene, camera, model) {
349
426
  });
350
427
  }
351
428
  function drawModelForRender(renderer, scene, camera, model) {
352
- const viewport = renderer.getViewport(new THREE10.Vector4());
353
- const scissor = renderer.getScissor(new THREE10.Vector4());
429
+ const viewport = renderer.getViewport(new THREE11.Vector4());
430
+ const scissor = renderer.getScissor(new THREE11.Vector4());
354
431
  const scissorTest = renderer.getScissorTest();
355
432
  const restoreRenderTarget = captureRenderTarget(renderer);
356
433
  const autoClear = renderer.autoClear;
357
- const clearColor = renderer.getClearColor(Object.assign(new THREE10.Color(), { a: 1 }));
434
+ const clearColor = renderer.getClearColor(Object.assign(new THREE11.Color(), { a: 1 }));
358
435
  const clearAlpha = renderer.getClearAlpha();
359
436
  const native = "isWebGPURenderer" in renderer ? renderer : null;
360
437
  const renderObject = native?.getRenderObjectFunction();
@@ -778,14 +855,14 @@ var CameraDOMControls = class {
778
855
  this.wrapper.remove();
779
856
  }
780
857
  };
781
- var ThreeAnnotationControls = class extends THREE10.EventDispatcher {
858
+ var ThreeAnnotationControls = class extends THREE11.EventDispatcher {
782
859
  constructor(domElement, runtime, advance, captureInput = true) {
783
860
  super();
784
861
  this.domElement = domElement;
785
862
  this.runtime = runtime;
786
863
  this.advance = advance;
787
864
  this.captureInput = captureInput;
788
- __publicField(this, "target", new THREE10.Vector3());
865
+ __publicField(this, "target", new THREE11.Vector3());
789
866
  __publicField(this, "disposed", false);
790
867
  __publicField(this, "previousTouchAction", "");
791
868
  __publicField(this, "connected", false);
@@ -928,12 +1005,12 @@ var ThreeAnnotationRenderer = class {
928
1005
  this.domElement = domElement;
929
1006
  this.onPick = onPick;
930
1007
  this.onHover = onHover;
931
- __publicField(this, "group", new THREE10.Group());
1008
+ __publicField(this, "group", new THREE11.Group());
932
1009
  __publicField(this, "objects", /* @__PURE__ */ new Map());
933
1010
  __publicField(this, "overlay");
934
- __publicField(this, "raycaster", new THREE10.Raycaster());
935
- __publicField(this, "pointer", new THREE10.Vector2());
936
- __publicField(this, "viewProjection", new THREE10.Matrix4());
1011
+ __publicField(this, "raycaster", new THREE11.Raycaster());
1012
+ __publicField(this, "pointer", new THREE11.Vector2());
1013
+ __publicField(this, "viewProjection", new THREE11.Matrix4());
937
1014
  __publicField(this, "overlayParent", null);
938
1015
  __publicField(this, "releaseParentPosition", null);
939
1016
  __publicField(this, "overlayResizeObserver");
@@ -991,28 +1068,28 @@ var ThreeAnnotationRenderer = class {
991
1068
  this.attachOverlay();
992
1069
  element.addEventListener("click", this.click);
993
1070
  }
994
- sync(snapshot2) {
995
- if (this.revision === snapshot2.descriptorRevision && this.style === snapshot2.style) return;
996
- if (this.style !== snapshot2.style) this.clear();
997
- this.style = snapshot2.style;
998
- const ids = new Set(snapshot2.descriptors.map((descriptor) => descriptor.id));
1071
+ sync(snapshot3) {
1072
+ if (this.revision === snapshot3.descriptorRevision && this.style === snapshot3.style) return;
1073
+ if (this.style !== snapshot3.style) this.clear();
1074
+ this.style = snapshot3.style;
1075
+ const ids = new Set(snapshot3.descriptors.map((descriptor) => descriptor.id));
999
1076
  for (const id of this.objects.keys()) if (!ids.has(id)) this.remove(id);
1000
- for (const descriptor of snapshot2.descriptors) {
1077
+ for (const descriptor of snapshot3.descriptors) {
1001
1078
  const signature = JSON.stringify(descriptor);
1002
1079
  if (this.objects.get(descriptor.id)?.descriptor === signature) continue;
1003
1080
  this.remove(descriptor.id);
1004
1081
  this.objects.set(descriptor.id, this.create(descriptor, signature));
1005
1082
  }
1006
- this.revision = snapshot2.descriptorRevision;
1083
+ this.revision = snapshot3.descriptorRevision;
1007
1084
  }
1008
1085
  create(descriptor, signature) {
1009
1086
  const { style } = descriptor;
1010
- const sphere = new THREE10.Mesh(new THREE10.SphereGeometry(descriptor.radius, 12, 12), new THREE10.MeshBasicMaterial({ color: style.markerColor, transparent: true }));
1087
+ const sphere = new THREE11.Mesh(new THREE11.SphereGeometry(descriptor.radius, 12, 12), new THREE11.MeshBasicMaterial({ color: style.markerColor, transparent: true }));
1011
1088
  const dashed = style.line.style === "dashed" || style.line.style === "dotted";
1012
- const material = dashed ? new THREE10.LineDashedMaterial({ color: style.lineColor, transparent: true, dashSize: style.line.style === "dotted" ? 5e-3 : 0.02, gapSize: 0.01, linewidth: style.line.thickness }) : new THREE10.LineBasicMaterial({ color: style.lineColor, transparent: true, linewidth: style.line.thickness });
1013
- const geometry = new THREE10.BufferGeometry();
1014
- geometry.setAttribute("position", new THREE10.BufferAttribute(new Float32Array(51), 3));
1015
- const line = new THREE10.Line(geometry, material);
1089
+ const material = dashed ? new THREE11.LineDashedMaterial({ color: style.lineColor, transparent: true, dashSize: style.line.style === "dotted" ? 5e-3 : 0.02, gapSize: 0.01, linewidth: style.line.thickness }) : new THREE11.LineBasicMaterial({ color: style.lineColor, transparent: true, linewidth: style.line.thickness });
1090
+ const geometry = new THREE11.BufferGeometry();
1091
+ geometry.setAttribute("position", new THREE11.BufferAttribute(new Float32Array(51), 3));
1092
+ const line = new THREE11.Line(geometry, material);
1016
1093
  const canvas = document.createElement("canvas");
1017
1094
  const context = canvas.getContext("2d");
1018
1095
  if (!context) {
@@ -1033,9 +1110,9 @@ var ThreeAnnotationRenderer = class {
1033
1110
  context.fillRect(0, 0, canvas.width, canvas.height);
1034
1111
  context.fillStyle = style.labelColor;
1035
1112
  context.fillText(descriptor.label, canvas.width / 2, canvas.height / 2);
1036
- const texture = new THREE10.CanvasTexture(canvas);
1037
- const label = new THREE10.Sprite(new THREE10.SpriteMaterial({ map: texture, transparent: true, depthTest: true, depthWrite: false, sizeAttenuation: false }));
1038
- const arrow = style.line.arrowHead ? new THREE10.Mesh(new THREE10.ConeGeometry(descriptor.arrowRadius, descriptor.arrowLength, 8), new THREE10.MeshBasicMaterial({ color: style.lineColor, transparent: true })) : null;
1113
+ const texture = new THREE11.CanvasTexture(canvas);
1114
+ const label = new THREE11.Sprite(new THREE11.SpriteMaterial({ map: texture, transparent: true, depthTest: true, depthWrite: false, sizeAttenuation: false }));
1115
+ const arrow = style.line.arrowHead ? new THREE11.Mesh(new THREE11.ConeGeometry(descriptor.arrowRadius, descriptor.arrowLength, 8), new THREE11.MeshBasicMaterial({ color: style.lineColor, transparent: true })) : null;
1039
1116
  let element = null;
1040
1117
  if (this.style === "html") {
1041
1118
  element = document.createElement("div");
@@ -1070,7 +1147,7 @@ var ThreeAnnotationRenderer = class {
1070
1147
  const rays = this.runtime.occlusion_queries();
1071
1148
  const hits = new Float32Array(rays.length / 8 * 2);
1072
1149
  for (let offset = 0; offset < rays.length; offset += 8) {
1073
- this.raycaster.set(new THREE10.Vector3().fromArray(rays, offset + 1), new THREE10.Vector3().fromArray(rays, offset + 4));
1150
+ this.raycaster.set(new THREE11.Vector3().fromArray(rays, offset + 1), new THREE11.Vector3().fromArray(rays, offset + 4));
1074
1151
  this.raycaster.near = 0;
1075
1152
  this.raycaster.far = rays[offset + 7];
1076
1153
  const intersection = this.raycaster.intersectObjects(this.meshes, false)[0];
@@ -1097,7 +1174,7 @@ var ThreeAnnotationRenderer = class {
1097
1174
  positions.array.set(values.subarray(offset + 19, offset + 70));
1098
1175
  positions.needsUpdate = true;
1099
1176
  objects.line.geometry.computeBoundingSphere();
1100
- if (objects.line.material instanceof THREE10.LineDashedMaterial) objects.line.computeLineDistances();
1177
+ if (objects.line.material instanceof THREE11.LineDashedMaterial) objects.line.computeLineDistances();
1101
1178
  objects.label.position.fromArray(values, offset + 5);
1102
1179
  objects.label.scale.set(values[offset + 9], values[offset + 10], 1);
1103
1180
  objects.label.material.opacity = values[offset + 11];
@@ -1220,7 +1297,7 @@ var ThreeAnnotationController = class _ThreeAnnotationController {
1220
1297
  __publicField(this, "markerStateListeners", /* @__PURE__ */ new Set());
1221
1298
  __publicField(this, "regionListeners", /* @__PURE__ */ new Set());
1222
1299
  __publicField(this, "renderer", null);
1223
- __publicField(this, "playbackTimer", new THREE10.Timer());
1300
+ __publicField(this, "playbackTimer", new THREE11.Timer());
1224
1301
  __publicField(this, "renderFrame");
1225
1302
  __publicField(this, "resizeRenderer");
1226
1303
  __publicField(this, "onRenderError");
@@ -1248,7 +1325,7 @@ var ThreeAnnotationController = class _ThreeAnnotationController {
1248
1325
  }, nativeOptions.captureInput);
1249
1326
  this.state = this.readState();
1250
1327
  if (this.manageCamera) this.applyCameraState(this.state.camera);
1251
- else this.controls.target.copy(this.camera.getWorldDirection(new THREE10.Vector3()).add(this.camera.position));
1328
+ else this.controls.target.copy(this.camera.getWorldDirection(new THREE11.Vector3()).add(this.camera.position));
1252
1329
  this.markerRenderer = new ThreeAnnotationRenderer(this.runtime, this.scene, this.camera, this.domElement, (ids) => this.pick(ids), (id) => {
1253
1330
  if (!this.disposed) {
1254
1331
  this.call("hover", { id });
@@ -1451,11 +1528,12 @@ var ThreeAnnotationController = class _ThreeAnnotationController {
1451
1528
  const observer = this.observer;
1452
1529
  const generation = this.runtime.generation();
1453
1530
  observer.observe(this.runtime);
1454
- const queries = JSON.parse(this.runtime.plan_markers(JSON.stringify(observer.morphAnchors(this.runtime))));
1531
+ observer.observeMorphAnchors(this.runtime);
1532
+ const queries = JSON.parse(this.runtime.plan_markers("{}"));
1455
1533
  for (const initial of queries) {
1456
1534
  let query = initial;
1457
1535
  while (query && this.isCurrent(generation)) {
1458
- const raycaster = new THREE10.Raycaster(new THREE10.Vector3().fromArray(query.origin), new THREE10.Vector3().fromArray(query.direction), 0, query.far);
1536
+ const raycaster = new THREE11.Raycaster(new THREE11.Vector3().fromArray(query.origin), new THREE11.Vector3().fromArray(query.direction), 0, query.far);
1459
1537
  const hits = sliceSurfaceQueries ? await intersectMeshesInSlices(raycaster, observer.meshes, { isCurrent: () => this.isCurrent(generation), yieldToPaint: waitForCharacterLoadPaint }) : raycaster.intersectObjects(observer.meshes, false);
1460
1538
  if (!hits || !this.isCurrent(generation)) return;
1461
1539
  query = JSON.parse(this.runtime.surface_result(query.generation, query.id, query.phase, new Float32Array(hits.flatMap((hit) => hit.point.toArray()))));
@@ -1566,6 +1644,7 @@ var ThreeAnnotationController = class _ThreeAnnotationController {
1566
1644
  if (this.disposed) return;
1567
1645
  this.cancelModelPreparation();
1568
1646
  this.call("clear");
1647
+ this.observer?.observeMorphAnchors(this.runtime);
1569
1648
  this.hostConfig = null;
1570
1649
  this.configSnapshot = null;
1571
1650
  this.configSnapshotHost = null;
@@ -1679,7 +1758,10 @@ var ThreeAnnotationController = class _ThreeAnnotationController {
1679
1758
  this.controls.target.fromArray(pose, 3);
1680
1759
  this.camera.lookAt(this.controls.target);
1681
1760
  }
1682
- if (this.observer) this.runtime.observe_transforms(this.observer.transforms(this.runtime));
1761
+ if (this.observer) {
1762
+ this.runtime.observe_transforms(this.observer.transforms(this.runtime));
1763
+ this.observer.observeMorphAnchors(this.runtime);
1764
+ }
1683
1765
  this.sync();
1684
1766
  this.markerRenderer.update(now);
1685
1767
  this.settleFlights();
@@ -1937,7 +2019,7 @@ var ThreeAnnotationMarkers = class {
1937
2019
  }
1938
2020
  getMarkerPosition(name) {
1939
2021
  const p = this.controller.getMarkerPosition(name);
1940
- return p ? new THREE10.Vector3(p.x, p.y, p.z) : null;
2022
+ return p ? new THREE11.Vector3(p.x, p.y, p.z) : null;
1941
2023
  }
1942
2024
  updateLineStyle(name, line) {
1943
2025
  this.controller.setRegionLineStyle(name, line);
@@ -2112,11 +2194,35 @@ function shouldUseFaceCenterForMarkerAnchor(resolved) {
2112
2194
  function shouldProjectMarkerAnchorToSurface(resolved, legacyDefault) {
2113
2195
  return annotationQuery("projectAnchor", { resolved, legacyDefault });
2114
2196
  }
2197
+ function getRuntimeAnnotationSide(region) {
2198
+ return annotationQuery("side", { region });
2199
+ }
2200
+ function getRuntimeAUMeshSideOffset(region, sideSign, modelSize) {
2201
+ const value = annotationQuery("meshSideOffset", { region, sideSign, size: modelSize });
2202
+ return value ? new THREE11.Vector3(value.x, value.y, value.z) : null;
2203
+ }
2204
+ function getRuntimeAUMorphAnchorPoint(region, mesh) {
2205
+ const core = requireInitializedEmbodyCore();
2206
+ const names = annotationQuery("morphNames", { region });
2207
+ const geometry = readMorphGeometry(mesh, names);
2208
+ if (!geometry) return null;
2209
+ const candidates = core.annotation_morph_anchor_candidates(JSON.stringify(region), geometry.base, geometry.morphs, geometry.relative);
2210
+ updateAnnotationWorldMatrices(mesh);
2211
+ const positions = new Float32Array(candidates.length / 2 * 3);
2212
+ sampleMorphVertices(mesh, candidates.filter((_value, index) => index % 2 === 0), positions);
2213
+ const samples = new Float32Array(candidates.length / 2 * 4);
2214
+ for (let index = 0; index < candidates.length / 2; index++) {
2215
+ samples.set(positions.subarray(index * 3, index * 3 + 3), index * 4);
2216
+ samples[index * 4 + 3] = candidates[index * 2 + 1];
2217
+ }
2218
+ const value = core.annotation_weighted_anchor(samples);
2219
+ return value.length === 3 ? new THREE11.Vector3().fromArray(value) : null;
2220
+ }
2115
2221
  var AnnotationLifecycleBridge = class {
2116
- constructor(mode, duration, apply) {
2222
+ constructor(mode, duration, apply2) {
2117
2223
  this.mode = mode;
2118
2224
  this.duration = duration;
2119
- this.apply = apply;
2225
+ this.apply = apply2;
2120
2226
  __publicField(this, "readiness", Promise.resolve());
2121
2227
  __publicField(this, "initializing", null);
2122
2228
  __publicField(this, "core", null);
@@ -2258,96 +2364,6 @@ function boneResolutionProfile(profile) {
2258
2364
  suffixPattern: profile.suffixPattern
2259
2365
  };
2260
2366
  }
2261
- var json = (value) => JSON.stringify(value ?? {});
2262
- function request2(op, payload = {}) {
2263
- return JSON.parse(requireInitializedEmbodyCore().embody_request(json({ op, payload })));
2264
- }
2265
- var getPreset = (id) => request2("preset.get", { id });
2266
- var extendCharacterConfigWithPreset = (config) => request2("profile.extendConfig", { config });
2267
- var extractProfileOverrides = (config) => request2("profile.extractOverrides", { config });
2268
- var mergeCharacterRegionsByName = (base, override) => request2("profile.mergeRegions", { base, override });
2269
- var getMeshNamesForAUProfile = (profile, auId) => request2("profile.getMeshNamesForAU", { profile, auId });
2270
- var getHumanoidSpecification = () => request2("humanoid.getSpecification");
2271
- var setAUMorphTargets = (profile, auId, side, targets) => request2("profile.setAUMorphTargets", { profile, auId, side, targets });
2272
- var setHumanoidRoleBinding = (profile, role, boneName) => request2("profile.setHumanoidRoleBinding", { profile, role, boneName });
2273
- var resolveHumanoidCharacterization = (profile) => request2("profile.resolveHumanoidCharacterization", { profile });
2274
- var hasLeftRightMorphs = (profile, auId) => request2("profile.hasLeftRightMorphs", { profile, auId });
2275
- var isMixedAU = (profile, auId) => request2("profile.isMixedAU", { profile, auId });
2276
- var resolveVisemeProfile = (profile) => request2("profile.resolveVisemes", { profile });
2277
- var isMaxDegreesOnlyAxisBindingUpdate = (update) => request2("bone.isMaxDegreesOnlyUpdate", { update });
2278
- var findNodeKeyForBone = (boneName, profile) => profile ? request2("bone.findNodeKey", { profile, boneName }) : null;
2279
- var ensureBoneNodeKey = (profile, boneName) => request2("bone.ensureNodeKey", { profile, boneName });
2280
- var ensureBilateralBoneNodeKeys = (profile, leftBoneName, rightBoneName) => request2("bone.ensureBilateralNodeKeys", { profile, leftBoneName, rightBoneName });
2281
- var resolveBoneNameForNodeKey = (profile, nodeKey) => request2("bone.resolveBoneName", { profile, nodeKey });
2282
- var stripConfiguredBoneAffixes = (profile, boneName) => request2("bone.stripAffixes", { profile: profile ?? {}, boneName });
2283
- var inferChiralBoneNamePair = (boneNames, selectedBoneName) => request2("bone.inferChiralPair", { boneNames, selectedBoneName });
2284
- var formatAxisLabel = (axis) => request2("bone.formatAxisLabel", { axis });
2285
- var formatAxisDirectionLabel = (axis, direction) => request2("bone.formatDirectionLabel", { axis, direction });
2286
- var getAxisFromChannel = (channel) => channel ? request2("bone.getAxisFromChannel", { channel }) : null;
2287
- var getBoneAxisBindingState = (profile, boneName, axis) => request2("bone.getAxisState", { profile, boneName, axis });
2288
- var resolveBoneAxisChannel = (profile, boneName, axis) => request2("bone.resolveAxisChannel", { profile, boneName, axis });
2289
- var getBilateralAxisBindingState = (profile, boneName, axis) => request2("bone.getBilateralAxisState", { profile, boneName, axis });
2290
- var getAUBoneBindingState = (profile, auId) => request2("bone.getAUBindingState", { profile, auId });
2291
- var inferEyeControlScope = (profile, binding) => request2("bone.inferEyeScope", { profile, binding });
2292
- var inferEyeControlFamily = (binding) => request2("bone.inferEyeFamily", { binding });
2293
- var applyBoneAxisBindingUpdate = (profile, boneName, axis, update) => request2("bone.applyAxisUpdate", { profile, boneName, axis, update });
2294
- var applyBilateralAxisBindingUpdate = (profile, boneName, axis, scope, update) => request2("bone.applyBilateralAxisUpdate", { profile, boneName, axis, scope, update });
2295
- var applyAUBoneBindingUpdate = (profile, auId, update) => request2("bone.applyAUBindingUpdate", { profile, auId, update });
2296
- var createBoneAxisAu = (profile, boneName, axis, direction, options = {}) => request2("bone.createAxisAU", { profile, boneName, axis, direction, name: options.name });
2297
- var createBilateralBoneAxisAu = (profile, boneName, axis, scope, direction, options = {}) => request2("bone.createBilateralAxisAU", { profile, boneName, axis, scope, direction, name: options.name });
2298
- var buildBoneAuOptions = (profile) => request2("bone.buildAUOptions", { profile });
2299
- var isJointControlAuInfo = (info) => request2("bone.isJointControlInfo", { info });
2300
- var classifyAuAsJointControl = (profile, auId, options = {}) => request2("bone.classifyJointControl", { profile, auId, boneName: options.boneName, nodeKey: options.nodeKey });
2301
- var resolveContinuumDisplayLabel = (profile, negativeAuId, positiveAuId) => request2("bone.resolveContinuumLabel", { profile, negativeAuId, positiveAuId }) ?? `AU ${negativeAuId} <-> AU ${positiveAuId}`;
2302
- function inspectModel(model, meshes, profile) {
2303
- return new ThreeModelInspector().inspectModel(model, { meshes, profile });
2304
- }
2305
- function extractModelData(model, meshes, animations = []) {
2306
- const inspection = inspectModel(model, meshes);
2307
- return JSON.parse(requireInitializedEmbodyCore().extract_model_data_json(json(inspection.descriptor), json(serializeAnimationClips(model, animations, inspection))));
2308
- }
2309
- async function analyzeModel(options) {
2310
- const source = options.source;
2311
- const model = source.type === "gltf" ? source.gltf.scene ?? source.gltf.scenes[0] : source.model;
2312
- const animations = source.type === "gltf" ? source.gltf.animations : source.animations ?? [];
2313
- const inspection = inspectModel(model, source.type === "runtime" ? source.meshes : void 0);
2314
- return JSON.parse(requireInitializedEmbodyCore().analyze_model_descriptor(json(inspection.descriptor), json(serializeAnimationClips(model, animations, inspection)), options.preset ? json(options.preset) : "", json({ suggestCorrections: Boolean(options.suggestCorrections) })));
2315
- }
2316
- function validateMappings(meshes, skeleton, profile, options = {}) {
2317
- let nextMorphId = 1;
2318
- const morphTargets = [];
2319
- const descriptorMeshes = (meshes ?? []).map((value, index) => {
2320
- const mesh = value;
2321
- const id = index + 1;
2322
- const morphTargetIds = Object.entries(mesh.morphTargetDictionary ?? mesh.geometry?.morphTargetDictionary ?? {}).map(([name, hostIndex]) => {
2323
- const morphId = nextMorphId++;
2324
- morphTargets.push({ id: morphId, meshId: id, name, hostIndex });
2325
- return morphId;
2326
- });
2327
- return { id, name: mesh.name ?? "", morphTargetIds, visible: true };
2328
- });
2329
- const bones = (skeleton?.bones ?? []).map((bone, index) => ({ id: index + 1, name: typeof bone === "string" ? bone : bone.name ?? "" }));
2330
- return JSON.parse(requireInitializedEmbodyCore().validate_profile_model(json(profile), json({ meshes: descriptorMeshes, morphTargets, bones, objects: [] }), json(options)));
2331
- }
2332
- function validateHumanoidCharacterization(model, profile) {
2333
- return JSON.parse(requireInitializedEmbodyCore().validate_humanoid_characterization(json(profile), json(inspectModel(model, void 0, profile).descriptor)));
2334
- }
2335
- function normalizeHairColorAppearance(value, fallback = DEFAULT_HAIR_COLOR_APPEARANCE) {
2336
- return JSON.parse(requireInitializedEmbodyCore().normalize_hair_color_appearance_json(json(value), json(fallback)));
2337
- }
2338
- var getHumanoidSkeletonTemplate = (id) => HUMANOID_SKELETON_TEMPLATES.find((template) => template.id === id) ?? null;
2339
- function getHumanoidSkeletonTemplateBoneNames(template = CC4_HUMANOID_SKELETON_TEMPLATE) {
2340
- return JSON.parse(requireInitializedEmbodyCore().humanoid_skeleton_template_bone_names(json(template)));
2341
- }
2342
- function createValidationSkeletonFromHumanoidTemplate(template = CC4_HUMANOID_SKELETON_TEMPLATE) {
2343
- return JSON.parse(requireInitializedEmbodyCore().create_validation_skeleton_json(json(template)));
2344
- }
2345
- function computeHumanoidSkeletonTemplateRestBounds(template) {
2346
- return Array.from(requireInitializedEmbodyCore().compute_humanoid_skeleton_template_rest_bounds(json(template)));
2347
- }
2348
- function extractHumanoidSkeletonTemplateFromModel(model, options) {
2349
- return JSON.parse(requireInitializedEmbodyCore().extract_humanoid_skeleton_template_json(json(inspectModel(model).descriptor), json(options)));
2350
- }
2351
2367
  var isBone = (object) => !!(object.isBone || object.type === "Bone");
2352
2368
  var finite = (values, description) => {
2353
2369
  if (!values.every(Number.isFinite)) {
@@ -2472,6 +2488,286 @@ function bindModelReferencePose(model, referencePose) {
2472
2488
  if (index !== referencePose.nodes.length) throw new Error("Incompatible reference pose hierarchy: extra captured nodes");
2473
2489
  return bindings2;
2474
2490
  }
2491
+
2492
+ // three/vrm-animation.ts
2493
+ var MAX_BYTES = 64 * 1024 * 1024;
2494
+ var identity = new Matrix4();
2495
+ var isBone2 = (node) => node.isBone || node.type === "Bone";
2496
+ function decomposeReference(matrix, name) {
2497
+ const position = new Vector3();
2498
+ const rotation = new Quaternion();
2499
+ const scale = new Vector3();
2500
+ if (!matrix.elements.every(Number.isFinite) || Math.abs(matrix.determinant()) < 1e-20) {
2501
+ throw new Error(`Invalid reference matrix for ${name}`);
2502
+ }
2503
+ matrix.decompose(position, rotation, scale);
2504
+ const reconstructed = new Matrix4().compose(position, rotation, scale);
2505
+ if (matrix.elements.some((v, i) => Math.abs(v - reconstructed.elements[i]) > 1e-5 * Math.max(1, Math.abs(v)))) {
2506
+ throw new Error(`Reference ${name} contains shear, which VRMA retargeting does not support`);
2507
+ }
2508
+ return { translation: position.toArray(), rotation: rotation.toArray(), scale: scale.toArray() };
2509
+ }
2510
+ function objectsInModel(model) {
2511
+ const all = [];
2512
+ model.traverse((node) => all.push(node));
2513
+ const needed = /* @__PURE__ */ new Set([model]);
2514
+ for (const bone of all.filter(isBone2)) {
2515
+ for (let node = bone; node && node !== model; node = node.parent) needed.add(node);
2516
+ }
2517
+ const objects = all.filter((node) => needed.has(node));
2518
+ if (objects.length > 4096) throw new Error("Animation rig exceeds 4096 nodes");
2519
+ const ids = new Map(objects.map((node, i) => [node, String(i)]));
2520
+ return { objects, ids };
2521
+ }
2522
+ function inspectReference(model, referencePose, metersPerUnit, includeRoot = false) {
2523
+ const bindings2 = bindModelReferencePose(model, referencePose);
2524
+ const { objects, ids } = objectsInModel(model);
2525
+ const nodes = objects.map((node) => ({
2526
+ id: ids.get(node),
2527
+ name: node.name,
2528
+ parent: node === model ? null : ids.get(node.parent),
2529
+ // Placement of the model in a scene is not part of its humanoid coordinate system.
2530
+ ...decomposeReference(node === model && !includeRoot ? identity : new Matrix4().fromArray(bindings2.get(node).localMatrix), node.name)
2531
+ }));
2532
+ return { rig: { metersPerUnit, nodes, humanoidBones: {} }, objects, ids };
2533
+ }
2534
+ function inspectSkinBind(model, metersPerUnit) {
2535
+ if (model.parent) throw new Error("Use an explicit referencePose for a parented source model");
2536
+ const initial = captureModelReferencePose(model);
2537
+ const initialBindings = bindModelReferencePose(model, initial);
2538
+ const { objects, ids } = objectsInModel(model);
2539
+ const world = /* @__PURE__ */ new Map();
2540
+ const all = [];
2541
+ model.traverse((node) => all.push(node));
2542
+ for (const node of all) {
2543
+ const mesh = node;
2544
+ if (!mesh.isSkinnedMesh) continue;
2545
+ const { bones, boneInverses } = mesh.skeleton;
2546
+ if (bones.length !== boneInverses.length) throw new Error("Invalid skin bind inverse count");
2547
+ bones.forEach((bone, index) => {
2548
+ const inverse = boneInverses[index];
2549
+ if (Math.abs(inverse.determinant()) < 1e-20) throw new Error(`Singular bind matrix for ${bone.name}`);
2550
+ const bind = inverse.clone().invert();
2551
+ const prior = world.get(bone);
2552
+ if (prior && prior.elements.some((value, i) => Math.abs(value - bind.elements[i]) > 1e-4)) {
2553
+ throw new Error(`Conflicting skin bind matrices for ${bone.name}`);
2554
+ }
2555
+ world.set(bone, bind);
2556
+ });
2557
+ }
2558
+ const nodes = objects.map((node) => {
2559
+ if (isBone2(node) && !world.has(node)) {
2560
+ throw new Error(`No skin bind reference for ${node.name}; supply an explicit authored T-pose reference`);
2561
+ }
2562
+ const nodeWorld = world.get(node) ?? new Matrix4().fromArray(initialBindings.get(node).worldMatrix);
2563
+ const parentWorld = node.parent ? world.get(node.parent) ?? new Matrix4().fromArray(initialBindings.get(node.parent).worldMatrix) : identity;
2564
+ const local = parentWorld.clone().invert().multiply(nodeWorld);
2565
+ return { id: ids.get(node), name: node.name, parent: node === model ? null : ids.get(node.parent), ...decomposeReference(local, node.name) };
2566
+ });
2567
+ return { rig: { metersPerUnit, nodes, humanoidBones: {} }, objects, ids };
2568
+ }
2569
+ function uniqueBone(objects, name) {
2570
+ const matches = objects.filter((node) => isBone2(node) && (node.name === name || node.uuid === name));
2571
+ if (matches.length !== 1) throw new Error(`Animation bone ${name} must resolve uniquely (found ${matches.length})`);
2572
+ return matches[0];
2573
+ }
2574
+ function convertMixamoToVrmAnimation(source, clip, options) {
2575
+ const core = requireInitializedEmbodyCore();
2576
+ if (clip.blendMode !== NormalAnimationBlendMode) {
2577
+ throw new Error("VRMA conversion requires absolute local animation keys; additive clips are unsupported");
2578
+ }
2579
+ const { rig, objects, ids } = options.referencePose ? inspectReference(source, options.referencePose, options.metersPerUnit, true) : inspectSkinBind(source, options.metersPerUnit);
2580
+ const tracks = clip.tracks.map((track) => {
2581
+ const match = /^(.*)\.(quaternion|position|scale)$/.exec(track.name);
2582
+ if (!match) throw new Error(`Unsupported Mixamo track ${track.name}`);
2583
+ const bone = uniqueBone(objects, match[1]);
2584
+ const interpolation = track.getInterpolation();
2585
+ if (interpolation !== InterpolateLinear && interpolation !== InterpolateDiscrete) {
2586
+ throw new Error(`Unsupported interpolation on ${track.name}`);
2587
+ }
2588
+ return {
2589
+ node: ids.get(bone),
2590
+ path: match[2] === "quaternion" ? "rotation" : match[2] === "position" ? "translation" : "scale",
2591
+ interpolation: interpolation === InterpolateDiscrete ? "STEP" : "LINEAR",
2592
+ times: Array.from(track.times),
2593
+ values: Array.from(track.values)
2594
+ };
2595
+ });
2596
+ const normalized = core.normalize_mixamo_animation(JSON.stringify({ version: 1, name: clip.name, durationSeconds: clip.duration, rig, tracks }));
2597
+ return core.encode_vrma_animation(normalized);
2598
+ }
2599
+ function inspectVrmAnimation(bytes) {
2600
+ if (bytes.byteLength > MAX_BYTES) throw new Error("VRMA exceeds 64 MiB");
2601
+ return JSON.parse(requireInitializedEmbodyCore().decode_vrma_animation(bytes));
2602
+ }
2603
+ function createAnimationClipFromVrmAnimation(bytes, target, options) {
2604
+ const core = requireInitializedEmbodyCore();
2605
+ const document2 = inspectVrmAnimation(bytes);
2606
+ const { rig, objects, ids } = inspectReference(target, options.referencePose, options.metersPerUnit);
2607
+ let humanoidBones = options.humanoidBones;
2608
+ if (!humanoidBones) {
2609
+ if (!options.profile) throw new Error("VRMA target requires a characterized profile or explicit humanoidBones");
2610
+ const inspection = new ThreeModelInspector().inspectModel(target, { referencePose: options.referencePose });
2611
+ const result = JSON.parse(core.validate_humanoid_characterization(JSON.stringify(options.profile), JSON.stringify(inspection.descriptor)));
2612
+ if (!result.valid) throw new Error(`Cannot characterize VRMA target: ${result.errors.join("; ")}`);
2613
+ humanoidBones = Object.fromEntries(Object.entries(result.roles).map(([role, binding]) => [role, binding.boneName]));
2614
+ }
2615
+ for (const [role, name] of Object.entries(humanoidBones)) {
2616
+ if (role !== "leftEye" && role !== "rightEye") rig.humanoidBones[role] = ids.get(uniqueBone(objects, name));
2617
+ }
2618
+ const retargeted = JSON.parse(core.retarget_vrma_animation(JSON.stringify(document2), JSON.stringify(rig)));
2619
+ const byId = new Map(objects.map((node) => [ids.get(node), node]));
2620
+ const tracks = retargeted.tracks.map((track) => {
2621
+ const node = byId.get(track.node);
2622
+ if (!node) throw new Error(`Retargeted node ${track.node} is not in the target model`);
2623
+ const interpolation = track.interpolation === "STEP" ? InterpolateDiscrete : InterpolateLinear;
2624
+ return track.path === "rotation" ? new QuaternionKeyframeTrack(`${node.uuid}.quaternion`, track.times, track.values, interpolation) : new VectorKeyframeTrack(`${node.uuid}.position`, track.times, track.values, interpolation);
2625
+ });
2626
+ return new AnimationClip(options.name ?? document2.name, retargeted.durationSeconds, tracks);
2627
+ }
2628
+ function convertMixamoFbxToVrmAnimations(bytes, options) {
2629
+ requireInitializedEmbodyCore();
2630
+ if (!bytes.byteLength || bytes.byteLength > MAX_BYTES) throw new Error("FBX must be between 1 byte and 64 MiB");
2631
+ const textures = /* @__PURE__ */ new Set();
2632
+ class AnimationTextureLoader extends TextureLoader {
2633
+ load(url) {
2634
+ if (url.startsWith("blob:")) URL.revokeObjectURL(url);
2635
+ const texture = new Texture();
2636
+ textures.add(texture);
2637
+ return texture;
2638
+ }
2639
+ }
2640
+ const manager = new LoadingManager();
2641
+ manager.addHandler(/.*/, new AnimationTextureLoader(manager));
2642
+ manager.setURLModifier(() => {
2643
+ throw new Error("External FBX resources are not allowed for animation conversion");
2644
+ });
2645
+ let source;
2646
+ try {
2647
+ source = new FBXLoader(manager).parse(bytes, "");
2648
+ if (!source.animations.length) throw new Error("FBX has no animation clips");
2649
+ let hasSkin = false;
2650
+ source.traverse((node) => {
2651
+ if (node.isSkinnedMesh) hasSkin = true;
2652
+ });
2653
+ const referencePose = hasSkin ? void 0 : captureModelReferencePose(source);
2654
+ return source.animations.map((clip) => ({ name: clip.name, bytes: convertMixamoToVrmAnimation(source, clip, { ...options, referencePose }) }));
2655
+ } finally {
2656
+ const materials2 = /* @__PURE__ */ new Set();
2657
+ source?.traverse((node) => {
2658
+ const mesh = node;
2659
+ mesh.geometry?.dispose();
2660
+ if (mesh.material) for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) materials2.add(material);
2661
+ node.skeleton?.dispose();
2662
+ });
2663
+ for (const material of materials2) material.dispose();
2664
+ for (const texture of textures) texture.dispose();
2665
+ }
2666
+ }
2667
+ var json = (value) => JSON.stringify(value ?? {});
2668
+ function request2(op, payload = {}) {
2669
+ return JSON.parse(requireInitializedEmbodyCore().embody_request(json({ op, payload })));
2670
+ }
2671
+ var getPreset = (id) => request2("preset.get", { id });
2672
+ var extendCharacterConfigWithPreset = (config) => request2("profile.extendConfig", { config });
2673
+ var extractProfileOverrides = (config) => request2("profile.extractOverrides", { config });
2674
+ var mergeCharacterRegionsByName = (base, override) => request2("profile.mergeRegions", { base, override });
2675
+ var getMeshNamesForAUProfile = (profile, auId) => request2("profile.getMeshNamesForAU", { profile, auId });
2676
+ var resolveMorphMeshSelections = (profile, categories, availableNames) => request2("profile.resolveMorphMeshSelections", { profile, categories, availableNames });
2677
+ var getHumanoidSpecification = () => request2("humanoid.getSpecification");
2678
+ var setAUMorphTargets = (profile, auId, side, targets) => request2("profile.setAUMorphTargets", { profile, auId, side, targets });
2679
+ var setHumanoidRoleBinding = (profile, role, boneName) => request2("profile.setHumanoidRoleBinding", { profile, role, boneName });
2680
+ var resolveHumanoidCharacterization = (profile) => request2("profile.resolveHumanoidCharacterization", { profile });
2681
+ var hasLeftRightMorphs = (profile, auId) => request2("profile.hasLeftRightMorphs", { profile, auId });
2682
+ var isMixedAU = (profile, auId) => request2("profile.isMixedAU", { profile, auId });
2683
+ var resolveVisemeProfile = (profile) => request2("profile.resolveVisemes", { profile });
2684
+ var isMaxDegreesOnlyAxisBindingUpdate = (update) => request2("bone.isMaxDegreesOnlyUpdate", { update });
2685
+ var findNodeKeyForBone = (boneName, profile) => profile ? request2("bone.findNodeKey", { profile, boneName }) : null;
2686
+ var ensureBoneNodeKey = (profile, boneName) => request2("bone.ensureNodeKey", { profile, boneName });
2687
+ var ensureBilateralBoneNodeKeys = (profile, leftBoneName, rightBoneName) => request2("bone.ensureBilateralNodeKeys", { profile, leftBoneName, rightBoneName });
2688
+ var resolveBoneNameForNodeKey = (profile, nodeKey) => request2("bone.resolveBoneName", { profile, nodeKey });
2689
+ var stripConfiguredBoneAffixes = (profile, boneName) => request2("bone.stripAffixes", { profile: profile ?? {}, boneName });
2690
+ var inferChiralBoneNamePair = (boneNames, selectedBoneName) => request2("bone.inferChiralPair", { boneNames, selectedBoneName });
2691
+ var formatAxisLabel = (axis) => request2("bone.formatAxisLabel", { axis });
2692
+ var formatAxisDirectionLabel = (axis, direction) => request2("bone.formatDirectionLabel", { axis, direction });
2693
+ var getAxisFromChannel = (channel) => channel ? request2("bone.getAxisFromChannel", { channel }) : null;
2694
+ var getBoneAxisBindingState = (profile, boneName, axis) => request2("bone.getAxisState", { profile, boneName, axis });
2695
+ var resolveBoneAxisChannel = (profile, boneName, axis) => request2("bone.resolveAxisChannel", { profile, boneName, axis });
2696
+ var getBilateralAxisBindingState = (profile, boneName, axis) => request2("bone.getBilateralAxisState", { profile, boneName, axis });
2697
+ var getAUBoneBindingState = (profile, auId) => request2("bone.getAUBindingState", { profile, auId });
2698
+ var getAUBoneBindingStates = (profile, auIds) => request2("bone.getAUBindingStates", {
2699
+ profile: {
2700
+ auToBones: profile.auToBones,
2701
+ boneNodes: profile.boneNodes,
2702
+ bonePrefix: profile.bonePrefix,
2703
+ boneSuffix: profile.boneSuffix,
2704
+ suffixPattern: profile.suffixPattern,
2705
+ compositeRotations: profile.compositeRotations,
2706
+ continuumPairs: profile.continuumPairs,
2707
+ humanoidCharacterization: profile.humanoidCharacterization
2708
+ },
2709
+ auIds
2710
+ });
2711
+ var inferEyeControlScope = (profile, binding) => request2("bone.inferEyeScope", { profile, binding });
2712
+ var inferEyeControlFamily = (binding) => request2("bone.inferEyeFamily", { binding });
2713
+ var applyBoneAxisBindingUpdate = (profile, boneName, axis, update) => request2("bone.applyAxisUpdate", { profile, boneName, axis, update });
2714
+ var applyBilateralAxisBindingUpdate = (profile, boneName, axis, scope, update) => request2("bone.applyBilateralAxisUpdate", { profile, boneName, axis, scope, update });
2715
+ var applyAUBoneBindingUpdate = (profile, auId, update) => request2("bone.applyAUBindingUpdate", { profile, auId, update });
2716
+ var createBoneAxisAu = (profile, boneName, axis, direction, options = {}) => request2("bone.createAxisAU", { profile, boneName, axis, direction, name: options.name });
2717
+ var createBilateralBoneAxisAu = (profile, boneName, axis, scope, direction, options = {}) => request2("bone.createBilateralAxisAU", { profile, boneName, axis, scope, direction, name: options.name });
2718
+ var buildBoneAuOptions = (profile) => request2("bone.buildAUOptions", { profile });
2719
+ var isJointControlAuInfo = (info) => request2("bone.isJointControlInfo", { info });
2720
+ var classifyAuAsJointControl = (profile, auId, options = {}) => request2("bone.classifyJointControl", { profile, auId, boneName: options.boneName, nodeKey: options.nodeKey });
2721
+ var resolveContinuumDisplayLabel = (profile, negativeAuId, positiveAuId) => request2("bone.resolveContinuumLabel", { profile, negativeAuId, positiveAuId }) ?? `AU ${negativeAuId} <-> AU ${positiveAuId}`;
2722
+ function inspectModel(model, meshes, profile) {
2723
+ return new ThreeModelInspector().inspectModel(model, { meshes, profile });
2724
+ }
2725
+ function extractModelData(model, meshes, animations = []) {
2726
+ const inspection = inspectModel(model, meshes);
2727
+ return JSON.parse(requireInitializedEmbodyCore().extract_model_data_json(json(inspection.descriptor), json(serializeAnimationClips(model, animations, inspection))));
2728
+ }
2729
+ async function analyzeModel(options) {
2730
+ const source = options.source;
2731
+ const model = source.type === "gltf" ? source.gltf.scene ?? source.gltf.scenes[0] : source.model;
2732
+ const animations = source.type === "gltf" ? source.gltf.animations : source.animations ?? [];
2733
+ const inspection = inspectModel(model, source.type === "runtime" ? source.meshes : void 0);
2734
+ return JSON.parse(requireInitializedEmbodyCore().analyze_model_descriptor(json(inspection.descriptor), json(serializeAnimationClips(model, animations, inspection)), options.preset ? json(options.preset) : "", json({ suggestCorrections: Boolean(options.suggestCorrections) })));
2735
+ }
2736
+ function validateMappings(meshes, skeleton, profile, options = {}) {
2737
+ let nextMorphId = 1;
2738
+ const morphTargets = [];
2739
+ const descriptorMeshes = (meshes ?? []).map((value, index) => {
2740
+ const mesh = value;
2741
+ const id = index + 1;
2742
+ const morphTargetIds = Object.entries(mesh.morphTargetDictionary ?? mesh.geometry?.morphTargetDictionary ?? {}).map(([name, hostIndex]) => {
2743
+ const morphId = nextMorphId++;
2744
+ morphTargets.push({ id: morphId, meshId: id, name, hostIndex });
2745
+ return morphId;
2746
+ });
2747
+ return { id, name: mesh.name ?? "", morphTargetIds, visible: true };
2748
+ });
2749
+ const bones = (skeleton?.bones ?? []).map((bone, index) => ({ id: index + 1, name: typeof bone === "string" ? bone : bone.name ?? "" }));
2750
+ return JSON.parse(requireInitializedEmbodyCore().validate_profile_model(json(profile), json({ meshes: descriptorMeshes, morphTargets, bones, objects: [] }), json(options)));
2751
+ }
2752
+ function validateHumanoidCharacterization(model, profile) {
2753
+ return JSON.parse(requireInitializedEmbodyCore().validate_humanoid_characterization(json(profile), json(inspectModel(model, void 0, profile).descriptor)));
2754
+ }
2755
+ function normalizeHairColorAppearance(value, fallback = DEFAULT_HAIR_COLOR_APPEARANCE) {
2756
+ return JSON.parse(requireInitializedEmbodyCore().normalize_hair_color_appearance_json(json(value), json(fallback)));
2757
+ }
2758
+ var getHumanoidSkeletonTemplate = (id) => HUMANOID_SKELETON_TEMPLATES.find((template) => template.id === id) ?? null;
2759
+ function getHumanoidSkeletonTemplateBoneNames(template = CC4_HUMANOID_SKELETON_TEMPLATE) {
2760
+ return JSON.parse(requireInitializedEmbodyCore().humanoid_skeleton_template_bone_names(json(template)));
2761
+ }
2762
+ function createValidationSkeletonFromHumanoidTemplate(template = CC4_HUMANOID_SKELETON_TEMPLATE) {
2763
+ return JSON.parse(requireInitializedEmbodyCore().create_validation_skeleton_json(json(template)));
2764
+ }
2765
+ function computeHumanoidSkeletonTemplateRestBounds(template) {
2766
+ return Array.from(requireInitializedEmbodyCore().compute_humanoid_skeleton_template_rest_bounds(json(template)));
2767
+ }
2768
+ function extractHumanoidSkeletonTemplateFromModel(model, options) {
2769
+ return JSON.parse(requireInitializedEmbodyCore().extract_humanoid_skeleton_template_json(json(inspectModel(model).descriptor), json(options)));
2770
+ }
2475
2771
  function withComponentTracks(clip) {
2476
2772
  let expanded = false;
2477
2773
  const tracks = clip.tracks.flatMap((track) => {
@@ -2484,16 +2780,18 @@ function withComponentTracks(clip) {
2484
2780
  if (!Number.isInteger(width) || width < 1 || euler && width !== 3) throw new Error(`Invalid component track ${JSON.stringify(track.name)}.`);
2485
2781
  expanded = true;
2486
2782
  return Array.from({ length: width }, (_, component) => {
2487
- const scalar = track.clone();
2488
- scalar.name = `${track.name}[${euler ? ["x", "y", "z"][component] : component}]`;
2489
- scalar.values = track.values.slice(0, track.times.length * parts);
2490
- for (let sample = 0; sample < scalar.values.length; sample += 1) scalar.values[sample] = track.values[sample * width + component];
2783
+ const values = track.values.slice(0, track.times.length * parts);
2784
+ for (let sample = 0; sample < values.length; sample += 1) values[sample] = track.values[sample * width + component];
2785
+ const Track = track.constructor;
2786
+ const scalar = new Track(`${track.name}[${euler ? ["x", "y", "z"][component] : component}]`, track.times.slice(), values);
2787
+ scalar.createInterpolant = track.createInterpolant;
2491
2788
  return scalar;
2492
2789
  });
2493
2790
  });
2494
2791
  if (!expanded) return clip;
2495
- const indexed = clip.clone();
2496
- indexed.tracks = tracks;
2792
+ const Clip = clip.constructor;
2793
+ const indexed = new Clip(clip.name, clip.duration, tracks, clip.blendMode);
2794
+ indexed.userData = JSON.parse(JSON.stringify(clip.userData));
2497
2795
  return indexed;
2498
2796
  }
2499
2797
  function concreteTrackName(target, inspection) {
@@ -2565,6 +2863,23 @@ var ThreeNativePlaybackConnector = class {
2565
2863
  __publicField(this, "disposed", false);
2566
2864
  __publicField(this, "onError");
2567
2865
  __publicField(this, "onRelease");
2866
+ __publicField(this, "afterSample");
2867
+ __publicField(this, "afterFrame", (deltaSeconds) => {
2868
+ this.flushEvents();
2869
+ if (!this.disposed) this.binding.onFrame?.(deltaSeconds);
2870
+ });
2871
+ __publicField(this, "sampleResponse", (deltaSeconds) => {
2872
+ if (this.disposed || !this.renderingEnabled) return;
2873
+ try {
2874
+ this.afterSample?.(deltaSeconds);
2875
+ } catch (error) {
2876
+ const failure = asError(error);
2877
+ queueMicrotask(() => {
2878
+ if (this.onError) this.onError(failure);
2879
+ else throw failure;
2880
+ });
2881
+ }
2882
+ });
2568
2883
  __publicField(this, "finished", (event) => {
2569
2884
  const handle = this.actionHandles.get(event.action);
2570
2885
  if (!handle || this.disposed) return;
@@ -2587,8 +2902,14 @@ var ThreeNativePlaybackConnector = class {
2587
2902
  this.binding = { ...binding, mixer: binding.mixer ?? new AnimationMixer(binding.model) };
2588
2903
  this.onError = binding.onError;
2589
2904
  this.onRelease = binding.onRelease;
2905
+ this.afterSample = binding.afterSample;
2590
2906
  this.binding.mixer.addEventListener("finished", this.finished);
2591
- this.rendering = bindPlaybackRendering(binding.model, this.binding.mixer, () => this.flushEvents());
2907
+ this.rendering = bindPlaybackRendering(
2908
+ binding.model,
2909
+ this.binding.mixer,
2910
+ this.afterFrame,
2911
+ this.sampleResponse
2912
+ );
2592
2913
  }
2593
2914
  /** Prepare bindings first; old actions are released before the new binding is exposed. */
2594
2915
  rebind(modelKey, profileKey, binding = this.binding) {
@@ -2599,7 +2920,12 @@ var ThreeNativePlaybackConnector = class {
2599
2920
  this.rendering.dispose();
2600
2921
  this.binding = { ...binding, mixer: binding.mixer ?? new AnimationMixer(binding.model) };
2601
2922
  this.binding.mixer.addEventListener("finished", this.finished);
2602
- this.rendering = bindPlaybackRendering(binding.model, this.binding.mixer, () => this.flushEvents());
2923
+ this.rendering = bindPlaybackRendering(
2924
+ binding.model,
2925
+ this.binding.mixer,
2926
+ this.afterFrame,
2927
+ this.sampleResponse
2928
+ );
2603
2929
  this.rendering.setEnabled(this.renderingEnabled);
2604
2930
  });
2605
2931
  }
@@ -2933,7 +3259,15 @@ var intensity = (options, fallback = 1) => {
2933
3259
  const value = options.intensityScale ?? options.intensity ?? options.weight;
2934
3260
  return finite2(value) ? Math.max(0, Math.min(2, value)) : fallback;
2935
3261
  };
3262
+ var weightRatio = (outgoing, options) => outgoing.getEffectiveWeight() / (finite2(options.mixerWeight) && options.mixerWeight > 0 ? options.mixerWeight : 1);
3263
+ var weighted = (sample, neutral, ratio, quaternion) => {
3264
+ if (ratio === 1) return sample;
3265
+ if (quaternion) return new Quaternion().fromArray(neutral ?? [0, 0, 0, 1]).slerp(new Quaternion().fromArray(sample), ratio).toArray();
3266
+ return sample.map((value, axis) => (neutral?.[axis] ?? 0) + (value - (neutral?.[axis] ?? 0)) * ratio);
3267
+ };
2936
3268
  var presetId = (value) => value?.trim().toLowerCase() || "cc4";
3269
+ var HAIR_RESPONSE = "__embody_hair_response";
3270
+ var HAIR_IMPULSE_CLIP = "__embody_hair_impulse";
2937
3271
  function supportsRustEmbodyPreset(value) {
2938
3272
  return requireInitializedEmbodyCore().has_preset(presetId(value));
2939
3273
  }
@@ -2957,10 +3291,12 @@ var Embody = class _Embody {
2957
3291
  __publicField(this, "profileMode");
2958
3292
  __publicField(this, "preset");
2959
3293
  __publicField(this, "sources", /* @__PURE__ */ new Map());
3294
+ __publicField(this, "vrmAnimations", /* @__PURE__ */ new Map());
2960
3295
  __publicField(this, "playbacks", /* @__PURE__ */ new Map());
2961
3296
  __publicField(this, "current", /* @__PURE__ */ new Map());
2962
3297
  __publicField(this, "playDepth", 0);
2963
3298
  __publicField(this, "pendingEvents", []);
3299
+ __publicField(this, "pendingReleases", []);
2964
3300
  __publicField(this, "bindingGeneration", 0);
2965
3301
  __publicField(this, "disposed", false);
2966
3302
  __publicField(this, "rendering", true);
@@ -2968,6 +3304,7 @@ var Embody = class _Embody {
2968
3304
  __publicField(this, "nextLiveTarget", 0);
2969
3305
  __publicField(this, "liveTargets", /* @__PURE__ */ new Map());
2970
3306
  __publicField(this, "hairObjects", /* @__PURE__ */ new Map());
3307
+ __publicField(this, "hairHeadObservation");
2971
3308
  __publicField(this, "hairConfig", {});
2972
3309
  __publicField(this, "hairAppearance", {
2973
3310
  hairColor: copy(DEFAULT_HAIR_COLOR_APPEARANCE),
@@ -3017,6 +3354,7 @@ var Embody = class _Embody {
3017
3354
  this.assertAlive();
3018
3355
  const model = options.model;
3019
3356
  if (!model?.isObject3D) throw new Error("onReady requires a Three Object3D model.");
3357
+ this.applier.clearMeshHighlights();
3020
3358
  const reference = this.references.get(model) ?? options.referencePose ?? (!this.boundModel ? this.config.referencePose : void 0) ?? captureModelReferencePose(model);
3021
3359
  const inspection = this.inspector.inspectModel(model, { meshes: options.meshes, profile: this.profile, referencePose: reference });
3022
3360
  this.bind(model, reference, inspection, this.profileSource, this.profile);
@@ -3056,7 +3394,10 @@ var Embody = class _Embody {
3056
3394
  }
3057
3395
  this.core.free();
3058
3396
  this.core = core;
3059
- if (replacingModel) this.sources.clear();
3397
+ if (replacingModel) {
3398
+ this.sources.clear();
3399
+ this.vrmAnimations.clear();
3400
+ }
3060
3401
  this.boundModel = model;
3061
3402
  this.inspection = inspection;
3062
3403
  this.referenceBindings = referenceBindings;
@@ -3077,10 +3418,17 @@ var Embody = class _Embody {
3077
3418
  this.connector = new ThreeNativePlaybackConnector(session, {
3078
3419
  model,
3079
3420
  inspection,
3421
+ onFrame: (deltaSeconds) => {
3422
+ if (this.disposed) return;
3423
+ this.applier.applyMeshHighlightFrame(parse(this.core.advance_mesh_highlight(deltaSeconds)));
3424
+ },
3080
3425
  onRelease: (id) => {
3081
3426
  const playback = this.playbacks.get(id);
3082
- if (playback?.terminal) this.forget(playback);
3083
- }
3427
+ if (!playback?.terminal) return;
3428
+ if (this.playDepth) this.pendingReleases.push(playback);
3429
+ else this.forget(playback);
3430
+ },
3431
+ afterSample: (mixerDeltaSeconds) => this.sampleHairResponse(mixerDeltaSeconds, true)
3084
3432
  });
3085
3433
  } catch (error) {
3086
3434
  session.free();
@@ -3091,17 +3439,20 @@ var Embody = class _Embody {
3091
3439
  this.connector.setTimeScale(this.timeScale);
3092
3440
  this.connector.subscribe((event) => this.onPlaybackEvent(event));
3093
3441
  this.hairConfig = parse(this.core.get_hair_config_json());
3442
+ this.bindHairHead(model, inspection);
3094
3443
  this.registerHairObjects(inspection.allMeshes.filter((mesh) => ["hair", "eyebrow"].includes(this.meshCategory(mesh))));
3095
3444
  if (preserveLiveState) this.commitLive();
3096
3445
  }
3097
3446
  bindModel(meshes) {
3098
3447
  const model = this.requireModel();
3448
+ this.applier.clearMeshHighlights();
3099
3449
  const reference = this.references.get(model);
3100
3450
  const inspection = this.inspector.inspectModel(model, { meshes, profile: this.profile, referencePose: reference });
3101
3451
  this.bind(model, reference, inspection, this.profileSource, this.profile);
3102
3452
  }
3103
3453
  extendReferencePose() {
3104
3454
  const model = this.requireModel();
3455
+ this.applier.clearMeshHighlights();
3105
3456
  const reference = extendModelReferencePose(model, this.references.get(model));
3106
3457
  const inspection = this.inspector.inspectModel(model, { profile: this.profile, referencePose: reference });
3107
3458
  this.bind(model, reference, inspection, this.profileSource, this.profile);
@@ -3116,6 +3467,7 @@ var Embody = class _Embody {
3116
3467
  this.profile = profile;
3117
3468
  return;
3118
3469
  }
3470
+ this.applier.clearMeshHighlights();
3119
3471
  const reference = this.references.get(this.boundModel);
3120
3472
  const inspection = this.inspector.inspectModel(this.boundModel, { profile, referencePose: reference });
3121
3473
  this.bind(this.boundModel, reference, inspection, source, profile, false);
@@ -3140,12 +3492,16 @@ var Embody = class _Embody {
3140
3492
  const playback = this.playbacks.get(event.handle);
3141
3493
  if (!playback) return;
3142
3494
  playback.terminal = true;
3143
- if (!playback.connector.getAction(playback.id)) {
3144
- this.playbacks.delete(playback.id);
3145
- if (this.current.get(playback.source.name) === playback) this.current.delete(playback.source.name);
3146
- }
3495
+ if (!playback.connector.getAction(playback.id)) this.forget(playback);
3147
3496
  playback.notify({ ...event, actionId: event.handle, clipName: playback.source.name });
3148
3497
  }
3498
+ flushPendingPlaybackEvents() {
3499
+ while (!this.playDepth && this.pendingReleases.length) {
3500
+ const playback = this.pendingReleases.shift();
3501
+ if (this.playbacks.get(playback.id) === playback) this.forget(playback);
3502
+ }
3503
+ while (!this.playDepth && this.pendingEvents.length) this.onPlaybackEvent(this.pendingEvents.shift());
3504
+ }
3149
3505
  compile(source, options) {
3150
3506
  const buildOptions = { ..."options" in source ? source.options : {}, ...options, intensityScale: intensity(options) };
3151
3507
  switch (source.kind) {
@@ -3215,7 +3571,10 @@ var Embody = class _Embody {
3215
3571
  const candidates = outgoing?.getClip().tracks;
3216
3572
  const oldTrack = candidates?.[index]?.name === track.name ? candidates[index] : candidates?.find((candidate) => candidate.name === track.name);
3217
3573
  if (oldTrack && outgoing) {
3218
- previous = Array.from(oldTrack.createInterpolant().evaluate(outgoing.time));
3574
+ const sample = Array.from(oldTrack.createInterpolant().evaluate(outgoing.time));
3575
+ const quaternion = track.ValueTypeName === "quaternion";
3576
+ const ratio = weightRatio(outgoing, options);
3577
+ previous = outgoing.blendMode === AdditiveAnimationBlendMode ? weighted(sample, void 0, ratio, quaternion) : weighted(sample, rest, ratio, quaternion);
3219
3578
  if (outgoing.blendMode === AdditiveAnimationBlendMode && rest) {
3220
3579
  previous = track.ValueTypeName === "quaternion" ? new Quaternion().fromArray(rest).multiply(new Quaternion().fromArray(previous)).toArray() : previous.map((value, axis) => value + rest[axis]);
3221
3580
  }
@@ -3351,7 +3710,7 @@ var Embody = class _Embody {
3351
3710
  validateEasing(options) {
3352
3711
  if (options.easing !== void 0 && !["linear", "easeIn", "easeOut", "easeInOut", "easeInOutCubic"].includes(options.easing)) throw new Error(`Unknown playback easing: ${options.easing}`);
3353
3712
  }
3354
- handle(source, options, autoPlay) {
3713
+ handle(source, options, autoPlay, owner = "caller") {
3355
3714
  options = { ..."options" in source ? source.options : {}, ...options };
3356
3715
  const listeners = /* @__PURE__ */ new Set();
3357
3716
  const generation = this.bindingGeneration;
@@ -3379,8 +3738,10 @@ var Embody = class _Embody {
3379
3738
  const connector = this.requireConnector();
3380
3739
  const inheritedStarts = /* @__PURE__ */ new Map();
3381
3740
  const clip = this.materialize(source, options, inheritedStarts);
3382
- const hairMotion = this.authoredHeadMotion(clip);
3383
3741
  const previous = this.current.get(source.name);
3742
+ const outgoing = previous ? connector.getAction(previous.id) : void 0;
3743
+ const predecessorTime = outgoing ? outgoing.time : Number.NaN;
3744
+ const predecessorWeightRatio = outgoing ? weightRatio(outgoing, options) : 1;
3384
3745
  const settings = this.nativeOptions(options, clip);
3385
3746
  if (previous && connector.getAction(previous.id)) settings.replace = previous.id;
3386
3747
  if (current) newPromise();
@@ -3392,12 +3753,30 @@ var Embody = class _Embody {
3392
3753
  id = connector.playNativeClip(clip, settings);
3393
3754
  } catch (error) {
3394
3755
  this.playDepth -= 1;
3395
- while (!this.playDepth && this.pendingEvents.length) this.onPlaybackEvent(this.pendingEvents.shift());
3756
+ this.flushPendingPlaybackEvents();
3757
+ throw error;
3758
+ }
3759
+ try {
3760
+ if (source.kind === "curves") {
3761
+ this.core.record_semantic_slot(
3762
+ source.name,
3763
+ json2(source.input),
3764
+ intensity(options),
3765
+ options.easing ?? "linear",
3766
+ predecessorTime,
3767
+ predecessorWeightRatio
3768
+ );
3769
+ } else this.core.release_semantic_slot(source.name);
3770
+ } catch (error) {
3771
+ connector.control(id, { type: "stop" });
3772
+ this.playDepth -= 1;
3773
+ this.flushPendingPlaybackEvents();
3396
3774
  throw error;
3397
3775
  }
3398
3776
  const playback = {
3399
3777
  id,
3400
3778
  source,
3779
+ owner,
3401
3780
  options: { ...options, intensityScale: intensity(options) },
3402
3781
  connector,
3403
3782
  terminal: false,
@@ -3439,7 +3818,7 @@ var Embody = class _Embody {
3439
3818
  if (this.connector !== connector || !connector.getAction(id)) return result;
3440
3819
  ensure();
3441
3820
  const seconds = finite2(playback.options.fadeOutSec) ? playback.options.fadeOutSec : 0;
3442
- if (seconds > 0 && !playback.terminal) connector.fadeOut(id, seconds);
3821
+ if (seconds > 0) connector.fadeOut(id, seconds);
3443
3822
  else control({ type: "stop" });
3444
3823
  return result;
3445
3824
  };
@@ -3473,11 +3852,7 @@ var Embody = class _Embody {
3473
3852
  if (finite2(fade) && fade > 0) connector.fadeIn(id, fade);
3474
3853
  } finally {
3475
3854
  this.playDepth -= 1;
3476
- while (!this.playDepth && this.pendingEvents.length) this.onPlaybackEvent(this.pendingEvents.shift());
3477
- }
3478
- if (hairMotion && connector.getAction(id)) {
3479
- this.playHairImpulse(hairMotion.horizontal, hairMotion.vertical);
3480
- this.setHairGravity(hairMotion.pitch, clip.duration);
3855
+ this.flushPendingPlaybackEvents();
3481
3856
  }
3482
3857
  return result;
3483
3858
  };
@@ -3515,7 +3890,10 @@ var Embody = class _Embody {
3515
3890
  }
3516
3891
  forget(playback) {
3517
3892
  this.playbacks.delete(playback.id);
3518
- if (this.current.get(playback.source.name) === playback) this.current.delete(playback.source.name);
3893
+ if (this.current.get(playback.source.name) === playback) {
3894
+ this.current.delete(playback.source.name);
3895
+ this.core.release_semantic_slot(playback.source.name);
3896
+ }
3519
3897
  }
3520
3898
  updatePlayback(playback, patch) {
3521
3899
  this.validateEasing(patch);
@@ -3531,6 +3909,7 @@ var Embody = class _Embody {
3531
3909
  if (["weight", "intensity", "intensityScale", "blendMode", "easing", "balance", "balanceMap", "mixWeights"].some((key) => key in patch)) {
3532
3910
  const clip = this.materialize(playback.source, options, playback.inheritedStarts);
3533
3911
  connector.replaceClip(playback.id, clip);
3912
+ if (playback.source.kind === "curves") this.core.restyle_semantic_slot(playback.source.name, intensity(options), options.easing ?? "linear");
3534
3913
  if ("blendMode" in patch) connector.control(playback.id, { type: "setBlendMode", blendMode: clip.blendMode === AdditiveAnimationBlendMode ? "additive" : "normal" });
3535
3914
  }
3536
3915
  if ("mixerWeight" in patch) connector.control(playback.id, { type: "setWeight", weight: patch.mixerWeight });
@@ -3556,6 +3935,38 @@ var Embody = class _Embody {
3556
3935
  playback.options = options;
3557
3936
  if (!connector.getAction(playback.id)) this.forget(playback);
3558
3937
  }
3938
+ supportsVrmAnimation() {
3939
+ return true;
3940
+ }
3941
+ convertMixamoFbxToVrmAnimations(bytes, options) {
3942
+ this.requireModel();
3943
+ return convertMixamoFbxToVrmAnimations(bytes, options);
3944
+ }
3945
+ getVrmAnimationInfo(bytes) {
3946
+ this.requireModel();
3947
+ const document2 = inspectVrmAnimation(bytes);
3948
+ return { name: document2.name, durationSeconds: document2.durationSeconds, trackCount: document2.tracks.length };
3949
+ }
3950
+ /** Registers a source; Animation owns the later playback request. */
3951
+ registerVrmAnimation(name, bytes, options) {
3952
+ if (typeof name !== "string" || !name.trim()) throw new Error("VRM Animation requires a nonempty clip name.");
3953
+ const model = this.requireModel();
3954
+ const clip = createAnimationClipFromVrmAnimation(bytes, model, {
3955
+ name,
3956
+ metersPerUnit: options.metersPerUnit,
3957
+ referencePose: this.references.get(model),
3958
+ profile: this.profile
3959
+ });
3960
+ this.registerAnimationClips([clip]);
3961
+ this.vrmAnimations.set(name, { model, clip });
3962
+ }
3963
+ /** Stale editor cleanup must not remove a later same-name native source. */
3964
+ releaseVrmAnimation(name) {
3965
+ const registered = this.vrmAnimations.get(name);
3966
+ const source = this.sources.get(name);
3967
+ if (registered && !this.disposed && registered.model === this.boundModel && source?.kind === "native" && source.clip === registered.clip) this.removeAnimationClip(name);
3968
+ this.vrmAnimations.delete(name);
3969
+ }
3559
3970
  registerAnimationClips(clips) {
3560
3971
  const model = this.requireModel();
3561
3972
  for (const clip of clips) {
@@ -3567,12 +3978,13 @@ var Embody = class _Embody {
3567
3978
  }
3568
3979
  loadAnimationClips(clips) {
3569
3980
  this.stopAllAnimations();
3981
+ this.vrmAnimations.clear();
3570
3982
  for (const source of [...this.sources.values()]) if (source.kind === "native") {
3571
3983
  this.sources.delete(source.name);
3572
3984
  this.core.remove_animation_clip(source.name);
3573
3985
  }
3574
3986
  this.registerAnimationClips(clips ?? []);
3575
- this.restartHair();
3987
+ this.sampleHairResponse(0);
3576
3988
  }
3577
3989
  removeAnimationClip(name) {
3578
3990
  this.stopAnimation(name);
@@ -3587,12 +3999,12 @@ var Embody = class _Embody {
3587
3999
  if (!source) throw new Error(`Unknown animation clip: ${name}`);
3588
4000
  return this.handle(source, options, true);
3589
4001
  }
3590
- build(source, options, play) {
4002
+ build(source, options, play, owner = "caller") {
3591
4003
  this.requireModel();
3592
4004
  this.validateEasing(options);
3593
4005
  this.compile(source, options);
3594
4006
  this.sources.set(source.name, source);
3595
- return this.handle(source, options, play);
4007
+ return this.handle(source, options, play, owner);
3596
4008
  }
3597
4009
  buildClip(name, curves, options = {}) {
3598
4010
  if (options.semanticAnimation) throw new Error("Decode legacy named animation tracks to numeric AU curves before building a clip.");
@@ -3625,7 +4037,7 @@ var Embody = class _Embody {
3625
4037
  this.core.cleanup_snippet(name);
3626
4038
  }
3627
4039
  crossfadeTo(name, seconds, options = {}) {
3628
- const outgoing = [...this.current.values()].filter((playback) => !playback.source.name.startsWith("__embody_"));
4040
+ const outgoing = [...this.current.values()].filter((playback) => playback.owner === "caller");
3629
4041
  const handle = this.playAnimation(name, { ...options, fadeInSec: seconds });
3630
4042
  for (const playback of outgoing) if (playback.connector.getAction(playback.id)) playback.connector.fadeOut(playback.id, seconds);
3631
4043
  return handle;
@@ -3636,28 +4048,35 @@ var Embody = class _Embody {
3636
4048
  if (playback.connector.getAction(playback.id)) playback.connector.control(playback.id, { type: "stop" });
3637
4049
  this.forget(playback);
3638
4050
  }
4051
+ /** Stop every caller playback. Live setter targets and hair actions keep running. */
3639
4052
  stopAllAnimations() {
3640
4053
  for (const playback of [...this.playbacks.values()]) {
3641
- if (playback.source.kind === "compiled") continue;
4054
+ if (playback.owner !== "caller") continue;
3642
4055
  if (playback.connector.getAction(playback.id)) playback.connector.control(playback.id, { type: "stop" });
3643
4056
  this.forget(playback);
3644
4057
  }
3645
4058
  }
3646
4059
  pauseAnimation(name) {
4060
+ if (name === HAIR_RESPONSE) this.core.set_hair_response_paused(true);
3647
4061
  const p = this.current.get(name);
3648
4062
  if (p) p.connector.control(p.id, { type: "pause" });
3649
4063
  }
3650
4064
  resumeAnimation(name) {
4065
+ if (name === HAIR_RESPONSE) {
4066
+ this.core.set_hair_response_paused(false);
4067
+ this.sampleHairResponse(0);
4068
+ }
3651
4069
  const p = this.current.get(name);
3652
4070
  if (!p) return;
3653
4071
  if (p.terminal) p.replay({ ...p.options, time: p.connector.getAction(p.id)?.time ?? 0 });
3654
4072
  else p.connector.control(p.id, { type: "resume" });
3655
4073
  }
4074
+ // Hair actions follow the hair configuration and Embody.stop()/start(), not caller pause commands.
3656
4075
  pauseAllAnimations() {
3657
- for (const p of this.playbacks.values()) if (!p.terminal) p.connector.control(p.id, { type: "pause" });
4076
+ for (const p of this.playbacks.values()) if (!p.terminal && p.owner !== "hair") p.connector.control(p.id, { type: "pause" });
3658
4077
  }
3659
4078
  resumeAllAnimations() {
3660
- for (const name of [...this.current.keys()]) this.resumeAnimation(name);
4079
+ for (const [name, p] of [...this.current]) if (p.owner !== "hair") this.resumeAnimation(name);
3661
4080
  }
3662
4081
  updateClipParams(name, options) {
3663
4082
  const p = this.current.get(name);
@@ -3758,7 +4177,8 @@ var Embody = class _Embody {
3758
4177
  const handle = this.handle(
3759
4178
  { name: releaseClip.name, kind: "compiled", clip: releaseClip },
3760
4179
  { loopMode: "once", finalPose: "release", mixerWeight: action.getEffectiveWeight() },
3761
- true
4180
+ true,
4181
+ "live"
3762
4182
  );
3763
4183
  connector.fadeOut(handle.actionId, duration);
3764
4184
  const record = { track: released, handle, releasing: true };
@@ -3776,11 +4196,17 @@ var Embody = class _Embody {
3776
4196
  const outgoing = previous?.handle.actionId && connector.getAction(previous.handle.actionId);
3777
4197
  let nativeTrack = track;
3778
4198
  if (duration > 0 && outgoing) {
3779
- const start = Array.from(outgoing.getClip().tracks[0].createInterpolant().evaluate(outgoing.time));
4199
+ const outgoingTrack = outgoing.getClip().tracks[0];
4200
+ const start = weighted(
4201
+ Array.from(outgoingTrack.createInterpolant().evaluate(outgoing.time)),
4202
+ this.restValues(outgoingTrack),
4203
+ outgoing.getEffectiveWeight(),
4204
+ outgoingTrack.ValueTypeName === "quaternion"
4205
+ );
3780
4206
  nativeTrack = { ...track, values: [...start, ...endpoint(track)], inheritStart: false };
3781
4207
  }
3782
4208
  const targetClip = { ...clip, name, tracks: [nativeTrack] };
3783
- const handle = this.handle({ name, kind: "compiled", clip: targetClip }, { loopMode: "once", finalPose: "hold" }, true);
4209
+ const handle = this.handle({ name, kind: "compiled", clip: targetClip }, { loopMode: "once", finalPose: "hold" }, true, "live");
3784
4210
  this.liveTargets.set(key, { track, handle, releasing: false });
3785
4211
  handles.push(handle);
3786
4212
  }
@@ -3825,6 +4251,49 @@ var Embody = class _Embody {
3825
4251
  getContinuum(negative, positive) {
3826
4252
  return this.core.get_continuum(negative, positive);
3827
4253
  }
4254
+ /**
4255
+ * One clip slot's own semantic AU contribution right now (`{auId: value}`),
4256
+ * evaluated in Rust at the native action's time and effective weight.
4257
+ * Null when the slot is not playing or was not an AU-curve clip.
4258
+ */
4259
+ readAUContribution(name) {
4260
+ const playback = this.current.get(name);
4261
+ const action = playback?.connector.getAction(playback.id);
4262
+ return playback && action ? parse(this.core.read_semantic_slot(name, action.time, action.getEffectiveWeight())) : null;
4263
+ }
4264
+ /**
4265
+ * On-demand AU state: live control values, active AU-curve clip
4266
+ * contributions and their sum. Baked/native and typed clips cannot be
4267
+ * inverted to AU values and are listed as `unreadable`, never estimated.
4268
+ */
4269
+ readAUState() {
4270
+ const active = [];
4271
+ const unreadable = [];
4272
+ for (const [name, playback] of this.current) {
4273
+ const action = playback.connector.getAction(playback.id);
4274
+ if (!action) continue;
4275
+ if (playback.source.kind === "curves") active.push({ name, time: action.time, weight: action.getEffectiveWeight() });
4276
+ else if (playback.source.kind !== "compiled") unreadable.push(name);
4277
+ }
4278
+ return { ...parse(this.core.read_semantic_state(json2(active))), unreadable };
4279
+ }
4280
+ /**
4281
+ * Distance from a slot's current contribution to AU destinations, measured
4282
+ * along profile continuum pairs, and whether it reverses unfinished travel.
4283
+ * A slot that is not playing is at zero. Callers choose any timing policy.
4284
+ */
4285
+ measureAUGoal(name, destinations) {
4286
+ const playback = this.current.get(name);
4287
+ const action = playback?.connector.getAction(playback.id);
4288
+ const unfinished = !!playback && !!action && !playback.terminal && action.time < action.getClip().duration;
4289
+ return parse(this.core.measure_au_goal(
4290
+ name,
4291
+ action?.time ?? Number.NaN,
4292
+ action?.getEffectiveWeight() ?? Number.NaN,
4293
+ json2(destinations),
4294
+ unfinished
4295
+ ));
4296
+ }
3828
4297
  transitionAU(id, value, durationMs = 200, balance = this.core.get_au_balance(id)) {
3829
4298
  return this.transition(this.mutateLive(() => this.core.set_au_signed(id, value, balance), durationMs));
3830
4299
  }
@@ -3924,7 +4393,9 @@ var Embody = class _Embody {
3924
4393
  this.meshProfile(name).visible = visible;
3925
4394
  }
3926
4395
  highlightMesh(name, color, value) {
3927
- this.applier.highlightMesh(this.requireModel(), name, color, value);
4396
+ this.requireModel();
4397
+ this.core.request_mesh_highlight(name ?? void 0, color, value);
4398
+ this.applier.applyMeshHighlightFrame(parse(this.core.evaluate_mesh_highlight_frame()));
3928
4399
  }
3929
4400
  getMeshMaterialConfig(name) {
3930
4401
  return this.applier.getMeshMaterialConfig(this.requireModel(), name);
@@ -3942,7 +4413,9 @@ var Embody = class _Embody {
3942
4413
  const material = mesh && (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material);
3943
4414
  if (material) this.hairAppearance[`${category}Color`] = { ...this.hairAppearance[`${category}Color`], baseColor: material.color ? `#${material.color.getHexString()}` : "#000000", emissive: material.emissive ? `#${material.emissive.getHexString()}` : "#000000", emissiveIntensity: material.emissiveIntensity ?? 0 };
3944
4415
  }
3945
- this.restartHair();
4416
+ for (const [name, playback] of [...this.current]) if (playback.owner === "hair") this.stopAnimation(name);
4417
+ this.core.reset_hair_response();
4418
+ this.sampleHairResponse(0);
3946
4419
  return [...this.hairObjects.values()].map((mesh) => ({ name: mesh.name, isMesh: true, isEyebrow: this.meshCategory(mesh) === "eyebrow" }));
3947
4420
  }
3948
4421
  getRegisteredHairObjects() {
@@ -3982,12 +4455,69 @@ var Embody = class _Embody {
3982
4455
  setHairOutline(value) {
3983
4456
  Object.assign(this.hairAppearance, { showOutline: value.show }, value.color ? { outlineColor: value.color } : {}, value.opacity !== void 0 ? { outlineOpacity: value.opacity } : {});
3984
4457
  }
3985
- restartHair() {
3986
- for (const name of [...this.current.keys()]) if (name.startsWith("__embody_hair")) this.stopAnimation(name);
3987
- if (!this.hairConfig.enabled || !this.hairObjects.size || !this.connector) return;
3988
- if (this.hairConfig.idleSwayAmount > 0 || this.hairConfig.windStrength > 0) {
3989
- const curves = parse(this.wasm.build_hair_idle_curves(json2(this.hairConfig), this.hairConfig.idleClipDuration));
3990
- if (Object.keys(curves).length) this.playSnippet("__embody_hair_idle", curves, { loopMode: "repeat", meshNames: [...this.hairObjects.keys()], source: "hair", blendMode: "additive" });
4458
+ hairClipOptions(options) {
4459
+ return { ...options, meshNames: [...this.hairObjects.keys()], source: "hair", blendMode: "additive" };
4460
+ }
4461
+ bindHairHead(model, inspection) {
4462
+ this.hairHeadObservation = void 0;
4463
+ const binding = parse(this.core.get_hair_head_binding_json());
4464
+ const head = binding && inspection.boneBindings.get(binding.boneId);
4465
+ if (!binding || !head) return;
4466
+ const chain = [];
4467
+ for (let node2 = head; node2 && node2 !== model; node2 = node2.parent) chain.push(node2);
4468
+ if (!chain.length || chain[chain.length - 1].parent !== model) return;
4469
+ const reference = new Quaternion();
4470
+ const node = new Quaternion();
4471
+ for (let index = chain.length - 1; index >= 0; index -= 1) {
4472
+ const rotation = this.referenceBindings.get(chain[index])?.transform.rotation;
4473
+ reference.multiply(rotation ? node.set(rotation.x, rotation.y, rotation.z, rotation.w) : chain[index].quaternion);
4474
+ }
4475
+ this.hairHeadObservation = {
4476
+ boneName: binding.boneName,
4477
+ chain,
4478
+ reference: binding.reference,
4479
+ modelToReference: new Quaternion().fromArray(binding.reference).multiply(reference.normalize().invert()),
4480
+ rotation: new Quaternion()
4481
+ };
4482
+ }
4483
+ sampleHairResponse(deltaSeconds, observeHead = false) {
4484
+ if (this.disposed || !this.connector || !this.inspection) return;
4485
+ if (!this.hairConfig.enabled || !this.hairObjects.size) {
4486
+ this.stopAnimation(HAIR_RESPONSE);
4487
+ return;
4488
+ }
4489
+ const previous = this.current.get(HAIR_RESPONSE);
4490
+ const action = previous?.connector.getAction(previous.id);
4491
+ const head = this.hairHeadObservation;
4492
+ let pose = null;
4493
+ if (observeHead && head) {
4494
+ const rotation = head.rotation.copy(head.modelToReference);
4495
+ for (let index = head.chain.length - 1; index >= 0; index -= 1) rotation.multiply(head.chain[index].quaternion);
4496
+ pose = { boneName: head.boneName, reference: head.reference, current: rotation.normalize().toArray() };
4497
+ }
4498
+ const ir = parse(this.core.build_hair_response_clip(
4499
+ HAIR_RESPONSE,
4500
+ JSON.stringify(pose),
4501
+ action?.paused ? 0 : deltaSeconds,
4502
+ json2({ meshNames: [...this.hairObjects.keys()] })
4503
+ ));
4504
+ if (!ir.tracks.length) {
4505
+ this.stopAnimation(HAIR_RESPONSE);
4506
+ return;
4507
+ }
4508
+ const clip = createAnimationClipFromClipIR(ir, this.inspection);
4509
+ clip.blendMode = AdditiveAnimationBlendMode;
4510
+ const oldTracks = action?.getClip().tracks;
4511
+ const sameBindings = oldTracks && oldTracks.length === clip.tracks.length && clip.tracks.every((track, index) => {
4512
+ const old = oldTracks[index];
4513
+ return old.name === track.name && old.ValueTypeName === track.ValueTypeName && old.getValueSize() === track.getValueSize();
4514
+ });
4515
+ if (previous && action && sameBindings) {
4516
+ this.connector.replaceClip(previous.id, clip);
4517
+ previous.source = { name: HAIR_RESPONSE, kind: "native", clip };
4518
+ } else {
4519
+ const handle = this.handle({ name: HAIR_RESPONSE, kind: "native", clip }, { loopMode: "repeat", blendMode: "additive" }, true, "hair");
4520
+ if (action?.paused || this.core.is_hair_response_paused()) handle.pause();
3991
4521
  }
3992
4522
  }
3993
4523
  setHairPhysicsEnabled(value) {
@@ -3997,8 +4527,12 @@ var Embody = class _Embody {
3997
4527
  return !!this.hairConfig.enabled;
3998
4528
  }
3999
4529
  setHairPhysicsConfig(value) {
4530
+ this.assertAlive();
4000
4531
  this.hairConfig = parse(this.core.set_hair_config_json(json2(value)));
4001
- this.restartHair();
4532
+ if (!this.hairConfig.enabled) {
4533
+ for (const [name, playback] of [...this.current]) if (playback.owner === "hair") this.stopAnimation(name);
4534
+ }
4535
+ this.sampleHairResponse(0);
4002
4536
  }
4003
4537
  getHairPhysicsConfig() {
4004
4538
  return copy(this.hairConfig);
@@ -4014,41 +4548,12 @@ var Embody = class _Embody {
4014
4548
  if (state.position) mesh.position.set(state.position.x, state.position.y, state.position.z);
4015
4549
  if (state.color) this.applier.applyHairAppearance([mesh], state.color);
4016
4550
  }
4017
- authoredHeadMotion(clip) {
4018
- if (!this.hairConfig.enabled || clip.name.startsWith("__embody_hair") || !this.hairObjects.size) return void 0;
4019
- const tracks = [];
4020
- for (const track of clip.tracks) {
4021
- if (track.ValueTypeName !== "quaternion") continue;
4022
- const parsed = PropertyBinding.parseTrackName(track.name);
4023
- let object = PropertyBinding.findNode(this.requireModel(), parsed.nodeName);
4024
- if (parsed.objectName === "bones" && object) {
4025
- const skeleton = object.skeleton;
4026
- object = skeleton?.bones.find((bone) => bone.name === String(parsed.objectIndex)) ?? skeleton?.bones[Number(parsed.objectIndex)];
4027
- }
4028
- if (!object) continue;
4029
- const cubic = track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true;
4030
- const stride = track.getValueSize();
4031
- const reference = this.restValues(track);
4032
- if (!reference) continue;
4033
- const samples = [];
4034
- for (let index = 0; index < track.times.length; index += 1) {
4035
- const offset = index * stride + (cubic ? 4 : 0);
4036
- samples.push(Array.from(track.values.subarray(offset, offset + 4)));
4037
- }
4038
- tracks.push({ boneName: object.name, current: object.quaternion.toArray(), reference, samples });
4039
- }
4040
- return parse(this.core.resolve_hair_motion_json(json2(tracks), clip.blendMode === AdditiveAnimationBlendMode)) ?? void 0;
4041
- }
4042
- setHairGravity(pitch, durationSeconds) {
4043
- const clip = parse(this.core.build_hair_gravity_clip("__embody_hair_gravity", pitch, durationSeconds, json2({ meshNames: [...this.hairObjects.keys()], source: "hair" })));
4044
- if (!clip.tracks.length) return;
4045
- this.handle({ name: clip.name, kind: "compiled", clip }, { loopMode: "once", finalPose: "hold", blendMode: "additive" }, true);
4046
- }
4047
4551
  playHairImpulse(horizontal, vertical) {
4048
4552
  if (!this.hairConfig.enabled || !this.hairObjects.size) return void 0;
4049
4553
  const curves = parse(this.wasm.build_hair_impulse_curves(json2(this.hairConfig), this.hairConfig.impulseClipDuration, horizontal, vertical));
4050
4554
  if (!Object.keys(curves).length) return void 0;
4051
- return this.playSnippet("__embody_hair_impulse", curves, { loopMode: "once", finalPose: "release", meshNames: [...this.hairObjects.keys()], source: "hair", blendMode: "additive" });
4555
+ const options = this.hairClipOptions({ loopMode: "once", finalPose: "release" });
4556
+ return this.build({ name: HAIR_IMPULSE_CLIP, kind: "curves", input: curves, options: copy(options) }, options, true, "hair");
4052
4557
  }
4053
4558
  start() {
4054
4559
  this.assertAlive();
@@ -4069,11 +4574,14 @@ var Embody = class _Embody {
4069
4574
  dispose() {
4070
4575
  if (this.disposed) return;
4071
4576
  this.connector?.dispose();
4577
+ this.applier.clearMeshHighlights();
4072
4578
  this.core.free();
4073
4579
  this.playbacks.clear();
4074
4580
  this.current.clear();
4075
4581
  this.sources.clear();
4582
+ this.vrmAnimations.clear();
4076
4583
  this.hairObjects.clear();
4584
+ this.hairHeadObservation = void 0;
4077
4585
  this.meshes.length = 0;
4078
4586
  this.meshByName.clear();
4079
4587
  this.bones = {};
@@ -4115,7 +4623,7 @@ function createSceneInkBackgroundCss(palette = DEFAULT_PALETTE) {
4115
4623
  createSceneInkBackgroundCss();
4116
4624
 
4117
4625
  // three/sceneInkBackground.ts
4118
- var toCssColor = (tone) => `#${new THREE10.Color(tone).getHexString()}`;
4626
+ var toCssColor = (tone) => `#${new THREE11.Color(tone).getHexString()}`;
4119
4627
  function createSceneInkBackground(onBackgroundChange) {
4120
4628
  let currentBackground = "";
4121
4629
  let disposed = false;
@@ -4255,7 +4763,7 @@ function sampleTexture(texture) {
4255
4763
  if (alpha < 0.18) {
4256
4764
  continue;
4257
4765
  }
4258
- const color = new THREE10.Color(
4766
+ const color = new THREE11.Color(
4259
4767
  data[index] / 255,
4260
4768
  data[index + 1] / 255,
4261
4769
  data[index + 2] / 255
@@ -4300,7 +4808,7 @@ function buildPalette(samples) {
4300
4808
  const hsl = { h: 0, s: 0, l: 0 };
4301
4809
  sample.color.getHSL(hsl);
4302
4810
  const bucketId = Math.floor(hsl.h * 18);
4303
- const bucket = buckets.get(bucketId) ?? { color: new THREE10.Color(0, 0, 0), score: 0 };
4811
+ const bucket = buckets.get(bucketId) ?? { color: new THREE11.Color(0, 0, 0), score: 0 };
4304
4812
  bucket.color.r += sample.color.r * sample.score;
4305
4813
  bucket.color.g += sample.color.g * sample.score;
4306
4814
  bucket.color.b += sample.color.b * sample.score;
@@ -4333,18 +4841,18 @@ function buildPalette(samples) {
4333
4841
  function massageBackgroundTone(color) {
4334
4842
  const hsl = { h: 0, s: 0, l: 0 };
4335
4843
  color.getHSL(hsl);
4336
- const saturation = THREE10.MathUtils.clamp(Math.max(hsl.s * 1.35, 0.46), 0.46, 0.82);
4337
- const lightness = THREE10.MathUtils.clamp(hsl.l + 0.08, 0.42, 0.66);
4338
- return new THREE10.Color().setHSL(hsl.h, saturation, lightness);
4844
+ const saturation = THREE11.MathUtils.clamp(Math.max(hsl.s * 1.35, 0.46), 0.46, 0.82);
4845
+ const lightness = THREE11.MathUtils.clamp(hsl.l + 0.08, 0.42, 0.66);
4846
+ return new THREE11.Color().setHSL(hsl.h, saturation, lightness);
4339
4847
  }
4340
4848
  function createCompanionTone(source, index) {
4341
4849
  const hsl = { h: 0, s: 0, l: 0 };
4342
4850
  source.getHSL(hsl);
4343
4851
  const hueOffset = index === 1 ? 0.075 : -0.065;
4344
- return new THREE10.Color().setHSL(
4345
- THREE10.MathUtils.euclideanModulo(hsl.h + hueOffset, 1),
4346
- THREE10.MathUtils.clamp(hsl.s * (index === 1 ? 0.92 : 0.82), 0.38, 0.76),
4347
- THREE10.MathUtils.clamp(hsl.l + (index === 1 ? 0.04 : -0.03), 0.38, 0.68)
4852
+ return new THREE11.Color().setHSL(
4853
+ THREE11.MathUtils.euclideanModulo(hsl.h + hueOffset, 1),
4854
+ THREE11.MathUtils.clamp(hsl.s * (index === 1 ? 0.92 : 0.82), 0.38, 0.76),
4855
+ THREE11.MathUtils.clamp(hsl.l + (index === 1 ? 0.04 : -0.03), 0.38, 0.68)
4348
4856
  );
4349
4857
  }
4350
4858
  function colorDistance(a, b) {
@@ -4383,6 +4891,11 @@ var prepare = (model, castShadows = true) => {
4383
4891
  });
4384
4892
  return meshes;
4385
4893
  };
4894
+ var prepareCharacterModelResult = (model, animations, gltf, options) => ({ model, meshes: prepare(model, options.castShadows), animations, gltf });
4895
+ var bufferFormat = (data) => {
4896
+ const header = new TextDecoder().decode(new Uint8Array(data, 0, Math.min(data.byteLength, 4096)));
4897
+ return header.startsWith("Kaydara FBX Binary \0") || /^\s*FBXHeaderExtension\s*:/m.test(header) ? "fbx" : "gltf";
4898
+ };
4386
4899
  var progress = (listener) => {
4387
4900
  let last = -1;
4388
4901
  return (event) => {
@@ -4415,8 +4928,8 @@ function disposeCharacterModel(scene, model) {
4415
4928
  });
4416
4929
  }
4417
4930
  function loadCharacterModel(url, options = {}) {
4418
- const done = (model, animations, gltf) => ({ model, meshes: prepare(model, options.castShadows), animations, gltf });
4419
- if (/\.fbx(?:\?|$)/i.test(url)) {
4931
+ const done = (model, animations, gltf) => prepareCharacterModelResult(model, animations, gltf, options);
4932
+ if ((options.format ?? (/\.fbx(?:[?#]|$)/i.test(url) ? "fbx" : "gltf")) === "fbx") {
4420
4933
  fbxLoader ?? (fbxLoader = new FBXLoader());
4421
4934
  return new Promise((resolve, reject) => fbxLoader.load(url, (model) => resolve(done(model, model.animations ?? [], null)), progress(options.onProgress), reject));
4422
4935
  }
@@ -4424,8 +4937,15 @@ function loadCharacterModel(url, options = {}) {
4424
4937
  return new Promise((resolve, reject) => loader.load(url, (gltf) => resolve(done(gltf.scene, gltf.animations ?? [], gltf)), progress(options.onProgress), reject));
4425
4938
  }
4426
4939
  function parseCharacterModel(data, resourcePath = "", options = {}) {
4940
+ if ((options.format ?? bufferFormat(data)) === "fbx") {
4941
+ return Promise.resolve().then(() => {
4942
+ fbxLoader ?? (fbxLoader = new FBXLoader());
4943
+ const model = fbxLoader.parse(data, resourcePath);
4944
+ return prepareCharacterModelResult(model, model.animations ?? [], null, options);
4945
+ });
4946
+ }
4427
4947
  const loader = getGltfLoader(options.dracoDecoderPath);
4428
- return new Promise((resolve, reject) => loader.parse(data, resourcePath, (gltf) => resolve({ model: gltf.scene, meshes: prepare(gltf.scene, options.castShadows), animations: gltf.animations ?? [], gltf }), reject));
4948
+ return new Promise((resolve, reject) => loader.parse(data, resourcePath, (gltf) => resolve(prepareCharacterModelResult(gltf.scene, gltf.animations ?? [], gltf, options)), reject));
4429
4949
  }
4430
4950
 
4431
4951
  // three/modelMorphLayout.ts
@@ -4575,6 +5095,63 @@ function assertRenderableCharacterMorphAttributes(model) {
4575
5095
  }
4576
5096
  });
4577
5097
  }
5098
+ var owners2 = /* @__PURE__ */ new WeakMap();
5099
+ var snapshot = (scene) => ({ value: scene.background, intensity: scene.backgroundIntensity, rotation: scene.backgroundRotation.clone() });
5100
+ var apply = (scene, value) => {
5101
+ scene.background = value.value;
5102
+ scene.backgroundIntensity = value.intensity;
5103
+ scene.backgroundRotation.copy(value.rotation);
5104
+ };
5105
+ var ownerFor = (scene) => {
5106
+ let owner = owners2.get(scene);
5107
+ if (!owner) {
5108
+ owner = { base: snapshot(scene), override: null, disposed: false };
5109
+ owners2.set(scene, owner);
5110
+ }
5111
+ return owner;
5112
+ };
5113
+ function setSceneBaseBackground(scene, value) {
5114
+ const owner = ownerFor(scene);
5115
+ owner.base = { value, intensity: 1, rotation: new Euler() };
5116
+ if (!owner.override?.value) apply(scene, owner.base);
5117
+ }
5118
+ function acquireSceneBackground(scene, onSceneDispose) {
5119
+ const owner = ownerFor(scene);
5120
+ if (owner.disposed) throw new Error("The scenery scene has been disposed.");
5121
+ if (owner.override) throw new Error("The scene already has a scenery renderer.");
5122
+ owner.base = snapshot(scene);
5123
+ const override = { value: null, onSceneDispose };
5124
+ owner.override = override;
5125
+ let disposed = false;
5126
+ return {
5127
+ set(value, intensity2, rotationDegrees) {
5128
+ if (disposed || owner.disposed) throw new Error("The scenery background has been disposed.");
5129
+ override.value = { value, intensity: intensity2, rotation: new Euler(0, rotationDegrees * Math.PI / 180, 0) };
5130
+ apply(scene, override.value);
5131
+ },
5132
+ clear() {
5133
+ if (disposed) return;
5134
+ override.value = null;
5135
+ apply(scene, owner.base);
5136
+ },
5137
+ dispose() {
5138
+ if (disposed) return;
5139
+ disposed = true;
5140
+ if (owner.override === override) {
5141
+ owner.override = null;
5142
+ apply(scene, owner.base);
5143
+ }
5144
+ }
5145
+ };
5146
+ }
5147
+ function disposeSceneBackground(scene) {
5148
+ const owner = ownerFor(scene);
5149
+ if (owner.disposed) return;
5150
+ owner.disposed = true;
5151
+ owner.override?.onSceneDispose();
5152
+ }
5153
+
5154
+ // three/scene.ts
4578
5155
  var DEFAULT_CHARACTER_LIGHTING_PRESETS = {
4579
5156
  cleanStudio: { id: "cleanStudio", label: "Soft Studio", settings: { envMapEnabled: true, environmentIntensity: 0.24, environmentBlur: 0.04, exposure: 1.08, ambientIntensity: 0.32, keyIntensity: 0.52, fillIntensity: 0.18, rimIntensity: 0.08, shadowOpacity: 0.22 } },
4580
5157
  softFill: { id: "softFill", label: "Soft Fill", settings: { envMapEnabled: true, environmentIntensity: 0.3, environmentBlur: 0.04, exposure: 1.1, ambientIntensity: 0.38, keyIntensity: 0.44, fillIntensity: 0.24, rimIntensity: 0.1, shadowOpacity: 0.18 } },
@@ -4819,7 +5396,8 @@ function constructDefaultCharacterScene(container, options, renderer, disposeRen
4819
5396
  Object.assign(renderer.domElement.style, { display: "block", width: "100%", height: "100%" });
4820
5397
  const scene = new Scene();
4821
5398
  const background = options.background === void 0 ? sceneType.background : options.background;
4822
- scene.background = background == null ? null : new Color(background);
5399
+ setSceneBaseBackground(scene, background == null ? null : new Color(background));
5400
+ lifetime.add(() => disposeSceneBackground(scene));
4823
5401
  const camera = new PerspectiveCamera(options.cameraFov ?? 45, width / height, 0.1, 1e3);
4824
5402
  const preset = DEFAULT_CHARACTER_LIGHTING_PRESETS[options.lightingPreset ?? sceneType.lightingPreset]?.settings ?? DEFAULT_CHARACTER_LIGHTING_SETTINGS;
4825
5403
  const lighting = createLighting(scene, { ...preset, ...options.lighting });
@@ -4840,7 +5418,7 @@ function constructDefaultCharacterScene(container, options, renderer, disposeRen
4840
5418
  const type = CHARACTER_SCENE_TYPES[value.type ?? DEFAULT_CHARACTER_SCENE_TYPE_ID] ?? CHARACTER_SCENE_TYPES[DEFAULT_CHARACTER_SCENE_TYPE_ID];
4841
5419
  currentSceneType = type.id;
4842
5420
  const background2 = value.background === void 0 ? type.background : value.background;
4843
- scene.background = background2 == null ? null : new Color(background2);
5421
+ setSceneBaseBackground(scene, background2 == null ? null : new Color(background2));
4844
5422
  camera.fov = value.cameraFov ?? 45;
4845
5423
  camera.updateProjectionMatrix();
4846
5424
  const preset2 = DEFAULT_CHARACTER_LIGHTING_PRESETS[value.lightingPreset ?? type.lightingPreset] ?? DEFAULT_CHARACTER_LIGHTING_PRESETS[DEFAULT_CHARACTER_LIGHTING_PRESET_ID];
@@ -4975,17 +5553,17 @@ function prepareCameraCoordinates(scene, camera, backend) {
4975
5553
  }
4976
5554
  });
4977
5555
  const previous = Array.from(cameras, (current) => [current, current.coordinateSystem]);
4978
- const apply = (current, coordinates2) => {
5556
+ const apply2 = (current, coordinates2) => {
4979
5557
  current.coordinateSystem = coordinates2;
4980
5558
  current.updateProjectionMatrix();
4981
5559
  };
4982
5560
  const coordinates = backend === "webgpu" ? WebGPUCoordinateSystem : WebGLCoordinateSystem;
4983
- cameras.forEach((current) => apply(current, coordinates));
5561
+ cameras.forEach((current) => apply2(current, coordinates));
4984
5562
  shadows.forEach((shadow) => {
4985
5563
  shadow.needsUpdate = true;
4986
5564
  });
4987
5565
  return () => {
4988
- previous.forEach(([current, original]) => apply(current, original));
5566
+ previous.forEach(([current, original]) => apply2(current, original));
4989
5567
  shadows.forEach((shadow) => {
4990
5568
  shadow.needsUpdate = true;
4991
5569
  });
@@ -4999,7 +5577,8 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
4999
5577
  let currentSceneType = sceneType.id;
5000
5578
  const scene = new Scene();
5001
5579
  const background = options.background === void 0 ? sceneType.background : options.background;
5002
- scene.background = background == null ? null : new Color(background);
5580
+ setSceneBaseBackground(scene, background == null ? null : new Color(background));
5581
+ lifetime.add(() => disposeSceneBackground(scene));
5003
5582
  const camera = new PerspectiveCamera(options.cameraFov ?? 45, 1, 0.1, 1e3);
5004
5583
  const preset = DEFAULT_CHARACTER_LIGHTING_PRESETS[options.lightingPreset ?? sceneType.lightingPreset]?.settings ?? DEFAULT_CHARACTER_LIGHTING_SETTINGS;
5005
5584
  const lighting = createMutableCharacterLighting(scene, { ...preset, ...options.lighting });
@@ -5039,10 +5618,10 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
5039
5618
  return { ...common, backend: null, renderer: null };
5040
5619
  };
5041
5620
  const notify = () => {
5042
- const snapshot2 = getSnapshot();
5621
+ const snapshot3 = getSnapshot();
5043
5622
  for (const listener of listeners) {
5044
5623
  try {
5045
- listener(snapshot2);
5624
+ listener(snapshot3);
5046
5625
  } catch (failure) {
5047
5626
  console.error("Character rendering subscriber failed.", failure);
5048
5627
  }
@@ -5169,9 +5748,9 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
5169
5748
  rollback(() => oldRenderer?.dispose());
5170
5749
  status = "ready";
5171
5750
  error = null;
5172
- const snapshot2 = getSnapshot();
5751
+ const snapshot3 = getSnapshot();
5173
5752
  notify();
5174
- return snapshot2;
5753
+ return snapshot3;
5175
5754
  } catch (failure) {
5176
5755
  if (suspended) {
5177
5756
  try {
@@ -5250,6 +5829,17 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
5250
5829
  }
5251
5830
  } };
5252
5831
  },
5832
+ acquireRendererAsync: async () => {
5833
+ for (; ; ) {
5834
+ if (disposed) throw renderingAborted();
5835
+ const settling = pending;
5836
+ if (settling) {
5837
+ await settling.catch(() => void 0);
5838
+ continue;
5839
+ }
5840
+ return rendering.acquireRenderer();
5841
+ }
5842
+ },
5253
5843
  releaseSceneResources: (release) => {
5254
5844
  if (disposal) return disposal.then(release, release);
5255
5845
  const previous = pending;
@@ -5333,7 +5923,7 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
5333
5923
  const type = CHARACTER_SCENE_TYPES[value.type ?? DEFAULT_CHARACTER_SCENE_TYPE_ID] ?? CHARACTER_SCENE_TYPES[DEFAULT_CHARACTER_SCENE_TYPE_ID];
5334
5924
  currentSceneType = type.id;
5335
5925
  const background2 = value.background === void 0 ? type.background : value.background;
5336
- scene.background = background2 == null ? null : new Color(background2);
5926
+ setSceneBaseBackground(scene, background2 == null ? null : new Color(background2));
5337
5927
  camera.fov = value.cameraFov ?? 45;
5338
5928
  camera.updateProjectionMatrix();
5339
5929
  const preset2 = DEFAULT_CHARACTER_LIGHTING_PRESETS[value.lightingPreset ?? type.lightingPreset] ?? DEFAULT_CHARACTER_LIGHTING_PRESETS[DEFAULT_CHARACTER_LIGHTING_PRESET_ID];
@@ -5376,9 +5966,9 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
5376
5966
  async function createDefaultCharacterSceneAsync(container, options = {}) {
5377
5967
  const runtime = createDefaultCharacterSceneRuntime(container, options);
5378
5968
  try {
5379
- const snapshot2 = await runtime.ready;
5380
- if (!snapshot2.renderer) throw new Error("Character scene renderer was not initialized.");
5381
- const handle = snapshot2.backend === "webgl" ? { ...runtime, renderer: snapshot2.renderer, backend: "webgl" } : { ...runtime, renderer: snapshot2.renderer, backend: "webgpu" };
5969
+ const snapshot3 = await runtime.ready;
5970
+ if (!snapshot3.renderer) throw new Error("Character scene renderer was not initialized.");
5971
+ const handle = snapshot3.backend === "webgl" ? { ...runtime, renderer: snapshot3.renderer, backend: "webgl" } : { ...runtime, renderer: snapshot3.renderer, backend: "webgpu" };
5382
5972
  Object.defineProperties(handle, {
5383
5973
  renderer: { get: () => runtime.renderer },
5384
5974
  backend: { get: () => runtime.backend },
@@ -5465,7 +6055,14 @@ async function createCharacterHost(options) {
5465
6055
  if (!rendering.getSnapshot().renderer) throw error;
5466
6056
  }
5467
6057
  check(signal);
5468
- return rendering.acquireRenderer();
6058
+ const lease = await rendering.acquireRendererAsync();
6059
+ try {
6060
+ check(signal);
6061
+ return lease;
6062
+ } catch (error) {
6063
+ lease.release();
6064
+ throw error;
6065
+ }
5469
6066
  };
5470
6067
  const makeEngine = (value, character) => {
5471
6068
  let profile = character.profile ?? options.embody?.profile;
@@ -5714,6 +6311,302 @@ async function createCharacterHost(options) {
5714
6311
  throw error;
5715
6312
  }
5716
6313
  }
6314
+ var finite4 = (value) => typeof value === "number" && Number.isFinite(value);
6315
+ var identity2 = (value) => typeof value === "string" && value.trim().length > 0;
6316
+ var messageOf = (error) => error instanceof Error ? error.message : String(error);
6317
+ function copyLayout(layout) {
6318
+ if (layout === null) return { skybox: null, instances: [] };
6319
+ if (!layout || !Array.isArray(layout.instances)) throw new Error("Scenery requires an instances array.");
6320
+ const ids = /* @__PURE__ */ new Set();
6321
+ const instances = layout.instances.map((instance) => {
6322
+ if (!instance || !identity2(instance.instanceId) || !identity2(instance.assetId)) throw new Error("Each scenery placement requires an instanceId and assetId.");
6323
+ if (ids.has(instance.instanceId)) throw new Error(`Duplicate scenery instanceId: ${instance.instanceId}`);
6324
+ ids.add(instance.instanceId);
6325
+ const position = instance.position;
6326
+ if (!position || !finite4(position.x) || !finite4(position.y) || !finite4(position.z)) throw new Error(`Scenery placement ${instance.instanceId} requires finite X/Y/Z coordinates.`);
6327
+ return { instanceId: instance.instanceId, assetId: instance.assetId, position: { x: position.x, y: position.y, z: position.z } };
6328
+ });
6329
+ const skybox = layout.skybox;
6330
+ if (skybox !== null) {
6331
+ if (!skybox || !["skybox", "backdrop", "color"].includes(skybox.kind)) throw new Error("Scenery background kind must be skybox, backdrop or color.");
6332
+ if (skybox.assetId !== null && !identity2(skybox.assetId)) throw new Error("The scenery background assetId must be a nonempty string or null.");
6333
+ if (skybox.kind === "color" && skybox.assetId !== null) throw new Error("A solid scenery color cannot use an image asset.");
6334
+ if (typeof skybox.color !== "string" || !/^#[\da-f]{6}$/i.test(skybox.color)) throw new Error("Scenery background color must use six-digit #rrggbb.");
6335
+ if (!finite4(skybox.intensity) || skybox.intensity < 0 || !finite4(skybox.rotation)) throw new Error("Scenery background intensity must be nonnegative and rotation must be finite degrees.");
6336
+ }
6337
+ return { instances, skybox: skybox === null ? null : { kind: skybox.kind, assetId: skybox.assetId, color: skybox.color, intensity: skybox.intensity, rotation: skybox.rotation } };
6338
+ }
6339
+ function disposeRoots(roots) {
6340
+ const geometries = /* @__PURE__ */ new Set(), materials2 = /* @__PURE__ */ new Set(), textures = /* @__PURE__ */ new Set();
6341
+ const skeletons = /* @__PURE__ */ new Set();
6342
+ for (const root of roots) root.traverse((object) => {
6343
+ if (object.geometry) geometries.add(object.geometry);
6344
+ if (object.skeleton) skeletons.add(object.skeleton);
6345
+ for (const material of object.material ? Array.isArray(object.material) ? object.material : [object.material] : []) {
6346
+ materials2.add(material);
6347
+ for (const value of Object.values(material)) if (value?.isTexture) textures.add(value);
6348
+ }
6349
+ });
6350
+ const images = /* @__PURE__ */ new Set();
6351
+ for (const texture of textures) {
6352
+ for (const image of Array.isArray(texture.image) ? texture.image : [texture.image]) if (image) images.add(image);
6353
+ texture.dispose();
6354
+ }
6355
+ for (const geometry of geometries) geometry.dispose();
6356
+ for (const material of materials2) material.dispose();
6357
+ for (const skeleton of skeletons) skeleton.dispose();
6358
+ for (const image of images) image.close?.();
6359
+ }
6360
+ function display(value, skybox) {
6361
+ const texture = "isTexture" in value ? value : null;
6362
+ if (texture) {
6363
+ texture.colorSpace = SRGBColorSpace;
6364
+ if (skybox.kind === "skybox") texture.mapping = EquirectangularReflectionMapping;
6365
+ }
6366
+ let disposed = false;
6367
+ return {
6368
+ value,
6369
+ texture,
6370
+ intensity: skybox.intensity,
6371
+ rotation: skybox.kind === "skybox" ? skybox.rotation : 0,
6372
+ dispose() {
6373
+ if (disposed) return;
6374
+ disposed = true;
6375
+ texture?.dispose();
6376
+ }
6377
+ };
6378
+ }
6379
+ function generatedSky(skybox) {
6380
+ const canvas = document.createElement("canvas");
6381
+ canvas.width = 1024;
6382
+ canvas.height = 512;
6383
+ const context = canvas.getContext("2d");
6384
+ if (!context) throw new Error("The browser could not create the scenery skybox.");
6385
+ const gradient = context.createLinearGradient(0, 0, 0, 512);
6386
+ gradient.addColorStop(0, "#10172c");
6387
+ gradient.addColorStop(0.48, skybox.color);
6388
+ gradient.addColorStop(0.52, "#b6aaa0");
6389
+ gradient.addColorStop(1, "#151c25");
6390
+ context.fillStyle = gradient;
6391
+ context.fillRect(0, 0, 1024, 512);
6392
+ return display(new CanvasTexture(canvas), skybox);
6393
+ }
6394
+ async function imageDisplay(asset, skybox) {
6395
+ if (!["image/png", "image/jpeg", "image/webp"].includes(asset.contentType)) throw new Error("Scenery backgrounds require a PNG, JPEG or WebP image.");
6396
+ const url = URL.createObjectURL(new Blob([asset.bytes], { type: asset.contentType }));
6397
+ let pending;
6398
+ try {
6399
+ const texture = await new Promise((resolve, reject) => {
6400
+ pending = new TextureLoader().load(url, resolve, void 0, reject);
6401
+ });
6402
+ return display(texture, skybox);
6403
+ } catch (error) {
6404
+ pending?.dispose();
6405
+ throw new Error(`Scenery background image could not decode: ${messageOf(error)}`);
6406
+ } finally {
6407
+ URL.revokeObjectURL(url);
6408
+ }
6409
+ }
6410
+ async function parseModel(asset) {
6411
+ if (asset.contentType !== "model/gltf-binary" || asset.bytes.byteLength < 20) throw new Error("Scenery models require a self-contained GLB 2.0 file.");
6412
+ const header = new DataView(asset.bytes);
6413
+ if (header.getUint32(0, true) !== 1179937895 || header.getUint32(4, true) !== 2 || header.getUint32(8, true) !== asset.bytes.byteLength) throw new Error("Scenery models require a valid GLB 2.0 container.");
6414
+ let offset = 12, document2;
6415
+ while (offset < asset.bytes.byteLength) {
6416
+ if (offset + 8 > asset.bytes.byteLength) throw new Error("The scenery GLB has a truncated chunk header.");
6417
+ const size = header.getUint32(offset, true), kind = header.getUint32(offset + 4, true);
6418
+ if (size % 4 || size > asset.bytes.byteLength - offset - 8) throw new Error("The scenery GLB has an invalid chunk length.");
6419
+ if (offset === 12 && kind !== 1313821514) throw new Error("The scenery GLB must begin with its JSON chunk.");
6420
+ if (kind === 1313821514) {
6421
+ if (document2) throw new Error("The scenery GLB contains duplicate JSON chunks.");
6422
+ try {
6423
+ document2 = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(asset.bytes, offset + 8, size)));
6424
+ } catch (error) {
6425
+ throw new Error(`The scenery GLB JSON is invalid: ${messageOf(error)}`);
6426
+ }
6427
+ if (!document2 || typeof document2 !== "object" || Array.isArray(document2)) throw new Error("The scenery GLB JSON must be an object.");
6428
+ }
6429
+ offset += 8 + size;
6430
+ }
6431
+ for (const resources of [document2?.buffers, document2?.images]) {
6432
+ if (resources !== void 0 && !Array.isArray(resources)) throw new Error("The scenery GLB buffers and images must be arrays.");
6433
+ for (const resource of resources ?? []) {
6434
+ if (resource?.uri !== void 0 && (typeof resource.uri !== "string" || !resource.uri.startsWith("data:"))) {
6435
+ throw new Error("Scenery GLB files must embed their geometry and textures; external URLs are not allowed.");
6436
+ }
6437
+ }
6438
+ }
6439
+ const manager = new LoadingManager();
6440
+ manager.setURLModifier((url) => {
6441
+ if (url.startsWith("data:") || url.startsWith("blob:")) return url;
6442
+ throw new Error("Scenery GLB files must embed their geometry and textures; external URLs are not allowed.");
6443
+ });
6444
+ const gltf = await new GLTFLoader(manager).parseAsync(asset.bytes, "");
6445
+ if (!gltf.scene?.isObject3D) throw new Error("The scenery GLB has no scene to display.");
6446
+ return [gltf.scene, ...gltf.scenes.filter((root) => root !== gltf.scene)];
6447
+ }
6448
+ function createSceneSceneryRenderer(options) {
6449
+ const { scene, rendering, loadAsset } = options;
6450
+ if (!scene?.isScene || typeof loadAsset !== "function") throw new Error("Scenery requires a Three scene and an asset loader.");
6451
+ let disposed = false, disposal = null;
6452
+ let backgroundKey = null, backgroundGeneration = 0;
6453
+ let backgroundLoad = null, currentDisplay = null;
6454
+ const models = /* @__PURE__ */ new Map(), tasks = /* @__PURE__ */ new Set();
6455
+ const report = (message) => {
6456
+ try {
6457
+ options.onError?.(message);
6458
+ } catch (error) {
6459
+ console.error("Scenery error subscriber failed.", error);
6460
+ }
6461
+ };
6462
+ const track = (task) => {
6463
+ tasks.add(task);
6464
+ void task.then(() => tasks.delete(task), (error) => {
6465
+ tasks.delete(task);
6466
+ report(messageOf(error));
6467
+ });
6468
+ return task;
6469
+ };
6470
+ const release = (callback) => {
6471
+ try {
6472
+ return rendering ? rendering.releaseSceneResources(callback) : (callback(), Promise.resolve());
6473
+ } catch (error) {
6474
+ return Promise.reject(error);
6475
+ }
6476
+ };
6477
+ const background = acquireSceneBackground(scene, () => {
6478
+ void dispose().catch((error) => report(messageOf(error)));
6479
+ });
6480
+ const retire = (model) => {
6481
+ if (model.retired) return Promise.resolve();
6482
+ model.retired = true;
6483
+ model.controller.abort();
6484
+ return release(() => {
6485
+ model.wrapper.removeFromParent();
6486
+ model.attached = false;
6487
+ disposeRoots(model.roots);
6488
+ model.roots = [];
6489
+ });
6490
+ };
6491
+ const mount = (instance) => {
6492
+ const wrapper = new Group();
6493
+ wrapper.name = `Scenery ${instance.instanceId}`;
6494
+ wrapper.position.set(instance.position.x, instance.position.y, instance.position.z);
6495
+ const model = { assetId: instance.assetId, wrapper, controller: new AbortController(), roots: [], attached: false, retired: false };
6496
+ models.set(instance.instanceId, model);
6497
+ track((async () => {
6498
+ let roots = [];
6499
+ try {
6500
+ const asset = await loadAsset(model.assetId, model.controller.signal);
6501
+ if (model.controller.signal.aborted) return;
6502
+ roots = await parseModel(asset);
6503
+ if (model.controller.signal.aborted || disposed) {
6504
+ disposeRoots(roots);
6505
+ roots = [];
6506
+ return;
6507
+ }
6508
+ await release(() => {
6509
+ if (model.controller.signal.aborted || disposed) {
6510
+ disposeRoots(roots);
6511
+ roots = [];
6512
+ return;
6513
+ }
6514
+ model.roots = roots;
6515
+ roots = [];
6516
+ wrapper.add(model.roots[0]);
6517
+ scene.add(wrapper);
6518
+ model.attached = true;
6519
+ });
6520
+ } catch (error) {
6521
+ if (roots.length) disposeRoots(roots);
6522
+ if (!model.controller.signal.aborted && !disposed) {
6523
+ if (models.get(instance.instanceId) === model) models.delete(instance.instanceId);
6524
+ await retire(model);
6525
+ report(`Scenery placement ${instance.instanceId} could not load: ${messageOf(error)}`);
6526
+ }
6527
+ }
6528
+ })());
6529
+ };
6530
+ const updateBackground = (skybox) => {
6531
+ const key = JSON.stringify(skybox);
6532
+ if (backgroundKey === key) return;
6533
+ backgroundKey = key;
6534
+ const generation = ++backgroundGeneration;
6535
+ backgroundLoad?.abort();
6536
+ const controller = new AbortController();
6537
+ backgroundLoad = controller;
6538
+ track((async () => {
6539
+ let next = null;
6540
+ try {
6541
+ if (skybox) {
6542
+ if (skybox.kind === "color") next = display(new Color(skybox.color), skybox);
6543
+ else if (!skybox.assetId) next = generatedSky(skybox);
6544
+ else {
6545
+ const asset = await loadAsset(skybox.assetId, controller.signal);
6546
+ if (controller.signal.aborted) return;
6547
+ next = await imageDisplay(asset, skybox);
6548
+ }
6549
+ }
6550
+ await release(() => {
6551
+ if (controller.signal.aborted || generation !== backgroundGeneration || disposed) {
6552
+ next?.dispose();
6553
+ next = null;
6554
+ return;
6555
+ }
6556
+ if (next) background.set(next.value, next.intensity, next.rotation);
6557
+ else background.clear();
6558
+ const previous = currentDisplay;
6559
+ currentDisplay = next;
6560
+ next = null;
6561
+ previous?.dispose();
6562
+ });
6563
+ } catch (error) {
6564
+ next?.dispose();
6565
+ if (!controller.signal.aborted && generation === backgroundGeneration && !disposed) {
6566
+ backgroundKey = null;
6567
+ report(`Scenery background could not load: ${messageOf(error)}`);
6568
+ }
6569
+ }
6570
+ })());
6571
+ };
6572
+ function dispose() {
6573
+ if (disposal) return disposal;
6574
+ disposed = true;
6575
+ backgroundGeneration++;
6576
+ backgroundLoad?.abort();
6577
+ background.dispose();
6578
+ const releases = [...models.values()].map(retire);
6579
+ models.clear();
6580
+ releases.push(release(() => {
6581
+ currentDisplay?.dispose();
6582
+ currentDisplay = null;
6583
+ }));
6584
+ disposal = (async () => {
6585
+ const results = await Promise.allSettled([...tasks, ...releases]);
6586
+ const failed = results.find((result) => result.status === "rejected");
6587
+ if (failed) throw failed.reason;
6588
+ })();
6589
+ return disposal;
6590
+ }
6591
+ return {
6592
+ update(value) {
6593
+ if (disposed) throw new Error("The scenery renderer has been disposed.");
6594
+ const layout = copyLayout(value);
6595
+ const next = new Map(layout.instances.map((instance) => [instance.instanceId, instance]));
6596
+ for (const [id, model] of models) if (next.get(id)?.assetId !== model.assetId) {
6597
+ models.delete(id);
6598
+ track(retire(model));
6599
+ }
6600
+ for (const instance of layout.instances) {
6601
+ const current = models.get(instance.instanceId);
6602
+ if (current) current.wrapper.position.set(instance.position.x, instance.position.y, instance.position.z);
6603
+ else mount(instance);
6604
+ }
6605
+ updateBackground(layout.skybox);
6606
+ },
6607
+ dispose
6608
+ };
6609
+ }
5717
6610
  var COLORS = {
5718
6611
  mapped: 4889258,
5719
6612
  unmapped: 5925498,
@@ -5840,8 +6733,8 @@ function createOctahedralBoneGeometry() {
5840
6733
  -w,
5841
6734
  MID_POSITION
5842
6735
  ]);
5843
- const geometry = new THREE10.BufferGeometry();
5844
- geometry.setAttribute("position", new THREE10.BufferAttribute(positions, 3));
6736
+ const geometry = new THREE11.BufferGeometry();
6737
+ geometry.setAttribute("position", new THREE11.BufferAttribute(positions, 3));
5845
6738
  geometry.computeVertexNormals();
5846
6739
  return geometry;
5847
6740
  }
@@ -5858,24 +6751,24 @@ function collectBones(root) {
5858
6751
  });
5859
6752
  return Array.from(bones);
5860
6753
  }
5861
- function isBone2(obj) {
6754
+ function isBone3(obj) {
5862
6755
  return !!obj && obj.isBone;
5863
6756
  }
5864
6757
  function countBoneSubtree(bone) {
5865
6758
  let count = 1;
5866
6759
  for (const child of bone.children) {
5867
- if (isBone2(child)) {
6760
+ if (isBone3(child)) {
5868
6761
  count += countBoneSubtree(child);
5869
6762
  }
5870
6763
  }
5871
6764
  return count;
5872
6765
  }
5873
6766
  function resolveTopLevelHelperRoot(root) {
5874
- const topLevelBones = collectBones(root).filter((bone) => !isBone2(bone.parent));
6767
+ const topLevelBones = collectBones(root).filter((bone) => !isBone3(bone.parent));
5875
6768
  if (topLevelBones.length !== 1) return null;
5876
6769
  let helperRoot = topLevelBones[0];
5877
6770
  while (ROOT_HELPER_NAME_PATTERN.test(helperRoot.name)) {
5878
- const boneChildren = helperRoot.children.filter(isBone2);
6771
+ const boneChildren = helperRoot.children.filter(isBone3);
5879
6772
  if (boneChildren.length !== 1) break;
5880
6773
  const onlyChild = boneChildren[0];
5881
6774
  if (!ROOT_HELPER_NAME_PATTERN.test(onlyChild.name)) break;
@@ -5886,7 +6779,7 @@ function resolveTopLevelHelperRoot(root) {
5886
6779
  function resolveBodyDisplayRoot(root) {
5887
6780
  const helperRoot = resolveTopLevelHelperRoot(root);
5888
6781
  if (!helperRoot) return null;
5889
- const boneChildren = helperRoot.children.filter(isBone2);
6782
+ const boneChildren = helperRoot.children.filter(isBone3);
5890
6783
  if (boneChildren.length === 0) return null;
5891
6784
  if (boneChildren.length === 1) return boneChildren[0];
5892
6785
  const subtreeSizes = boneChildren.map((child) => ({ child, size: countBoneSubtree(child) })).sort((left, right) => right.size - left.size);
@@ -5898,7 +6791,7 @@ function resolveBodyDisplayRoot(root) {
5898
6791
  return null;
5899
6792
  }
5900
6793
  function collectBoneChildren(bone) {
5901
- return bone.children.filter(isBone2);
6794
+ return bone.children.filter(isBone3);
5902
6795
  }
5903
6796
  function hasZeroLengthConnection(parentBone, childBone) {
5904
6797
  parentBone.getWorldPosition(_parentPos);
@@ -5907,7 +6800,7 @@ function hasZeroLengthConnection(parentBone, childBone) {
5907
6800
  }
5908
6801
  function hasRootHelperAncestor(bone) {
5909
6802
  let current = bone.parent;
5910
- while (isBone2(current)) {
6803
+ while (isBone3(current)) {
5911
6804
  if (ROOT_HELPER_NAME_PATTERN.test(current.name)) return true;
5912
6805
  current = current.parent;
5913
6806
  }
@@ -5917,7 +6810,7 @@ function shouldSkipRootHelperConnection(parentBone, boneChildren) {
5917
6810
  if (boneChildren.length !== 1) return false;
5918
6811
  const childBone = boneChildren[0];
5919
6812
  if (ROOT_HELPER_NAME_PATTERN.test(parentBone.name)) {
5920
- if (!isBone2(parentBone.parent)) return true;
6813
+ if (!isBone3(parentBone.parent)) return true;
5921
6814
  return collectBoneChildren(childBone).some((grandChild) => hasZeroLengthConnection(childBone, grandChild));
5922
6815
  }
5923
6816
  return hasRootHelperAncestor(parentBone) && HIP_NAME_PATTERN.test(parentBone.name) && PELVIS_NAME_PATTERN.test(childBone.name) && hasZeroLengthConnection(parentBone, childBone);
@@ -5930,20 +6823,20 @@ function setRayFromPointer(clientX, clientY, camera, domElement) {
5930
6823
  _raycaster.setFromCamera(_mouse, camera);
5931
6824
  return true;
5932
6825
  }
5933
- var _parentPos = new THREE10.Vector3();
5934
- var _childPos = new THREE10.Vector3();
5935
- var _direction = new THREE10.Vector3();
5936
- var _zAxis = new THREE10.Vector3(0, 0, 1);
5937
- var _quaternion = new THREE10.Quaternion();
5938
- var _bonePos = new THREE10.Vector3();
5939
- var _boneQuat = new THREE10.Quaternion();
5940
- var _boneEuler = new THREE10.Euler();
5941
- var _mouse = new THREE10.Vector2();
5942
- var _raycaster = new THREE10.Raycaster();
5943
- var _cameraSpacePosition = new THREE10.Vector3();
5944
- var _planeNormal = new THREE10.Vector3();
5945
- var _dragPlane = new THREE10.Plane();
5946
- var _planeHit = new THREE10.Vector3();
6826
+ var _parentPos = new THREE11.Vector3();
6827
+ var _childPos = new THREE11.Vector3();
6828
+ var _direction = new THREE11.Vector3();
6829
+ var _zAxis = new THREE11.Vector3(0, 0, 1);
6830
+ var _quaternion = new THREE11.Quaternion();
6831
+ var _bonePos = new THREE11.Vector3();
6832
+ var _boneQuat = new THREE11.Quaternion();
6833
+ var _boneEuler = new THREE11.Euler();
6834
+ var _mouse = new THREE11.Vector2();
6835
+ var _raycaster = new THREE11.Raycaster();
6836
+ var _cameraSpacePosition = new THREE11.Vector3();
6837
+ var _planeNormal = new THREE11.Vector3();
6838
+ var _dragPlane = new THREE11.Plane();
6839
+ var _planeHit = new THREE11.Vector3();
5947
6840
  function resolveSkeletonOverlayStartBoneName(root) {
5948
6841
  return resolveBodyDisplayRoot(root)?.name ?? null;
5949
6842
  }
@@ -5990,9 +6883,9 @@ var SkeletonOverlay = class {
5990
6883
  this.scene = scene;
5991
6884
  this.mappedBoneNames = mappedBoneNames;
5992
6885
  this.semanticKeyMap = semanticKeyMap;
5993
- this.opacityScale = THREE10.MathUtils.clamp(options.opacityScale ?? 1, 0, 1);
6886
+ this.opacityScale = THREE11.MathUtils.clamp(options.opacityScale ?? 1, 0, 1);
5994
6887
  this.focusSelectedBone = options.focusSelectedBone ?? false;
5995
- this.group = new THREE10.Group();
6888
+ this.group = new THREE11.Group();
5996
6889
  this.group.name = "SkeletonOverlay";
5997
6890
  this.group.renderOrder = 999;
5998
6891
  this.group.visible = false;
@@ -6001,50 +6894,50 @@ var SkeletonOverlay = class {
6001
6894
  transparent: true,
6002
6895
  depthTest: false,
6003
6896
  depthWrite: false,
6004
- side: THREE10.DoubleSide
6897
+ side: THREE11.DoubleSide
6005
6898
  };
6006
- this.mappedMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, color: COLORS.mapped, opacity: OPACITY.mapped });
6007
- this.unmappedMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, color: COLORS.unmapped, opacity: OPACITY.unmapped });
6008
- this.highlightMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, color: COLORS.highlighted, opacity: OPACITY.highlighted });
6009
- this.selectedMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, color: COLORS.selected, opacity: OPACITY.selected });
6010
- this.contextMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, color: COLORS.selected, opacity: OPACITY.context });
6011
- this.mutedMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, color: COLORS.unmapped, opacity: OPACITY.muted });
6899
+ this.mappedMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, color: COLORS.mapped, opacity: OPACITY.mapped });
6900
+ this.unmappedMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, color: COLORS.unmapped, opacity: OPACITY.unmapped });
6901
+ this.highlightMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, color: COLORS.highlighted, opacity: OPACITY.highlighted });
6902
+ this.selectedMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, color: COLORS.selected, opacity: OPACITY.selected });
6903
+ this.contextMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, color: COLORS.selected, opacity: OPACITY.context });
6904
+ this.mutedMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, color: COLORS.unmapped, opacity: OPACITY.muted });
6012
6905
  this.mappedMaterial.forceSinglePass = true;
6013
6906
  this.unmappedMaterial.forceSinglePass = true;
6014
6907
  this.highlightMaterial.forceSinglePass = true;
6015
6908
  this.selectedMaterial.forceSinglePass = true;
6016
6909
  this.contextMaterial.forceSinglePass = true;
6017
6910
  this.mutedMaterial.forceSinglePass = true;
6018
- this.axisRingGeometry = new THREE10.TorusGeometry(1, 0.032, 10, 120);
6019
- this.axisRingHitGeometry = new THREE10.TorusGeometry(1, AXIS_GIZMO_HIT_TUBE, 8, 64);
6020
- this.axisCenterGeometry = new THREE10.SphereGeometry(0.082, 12, 8);
6021
- this.pitchRingMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, color: AXIS_GIZMO_COLORS.pitch, opacity: AXIS_GIZMO_IDLE_OPACITY });
6022
- this.yawRingMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, color: AXIS_GIZMO_COLORS.yaw, opacity: AXIS_GIZMO_IDLE_OPACITY });
6023
- this.rollRingMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, color: AXIS_GIZMO_COLORS.roll, opacity: AXIS_GIZMO_IDLE_OPACITY });
6024
- this.axisHitMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, opacity: 0 });
6911
+ this.axisRingGeometry = new THREE11.TorusGeometry(1, 0.032, 10, 120);
6912
+ this.axisRingHitGeometry = new THREE11.TorusGeometry(1, AXIS_GIZMO_HIT_TUBE, 8, 64);
6913
+ this.axisCenterGeometry = new THREE11.SphereGeometry(0.082, 12, 8);
6914
+ this.pitchRingMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, color: AXIS_GIZMO_COLORS.pitch, opacity: AXIS_GIZMO_IDLE_OPACITY });
6915
+ this.yawRingMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, color: AXIS_GIZMO_COLORS.yaw, opacity: AXIS_GIZMO_IDLE_OPACITY });
6916
+ this.rollRingMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, color: AXIS_GIZMO_COLORS.roll, opacity: AXIS_GIZMO_IDLE_OPACITY });
6917
+ this.axisHitMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, opacity: 0 });
6025
6918
  this.axisHitMaterial.colorWrite = false;
6026
- this.axisCenterMaterial = new THREE10.MeshBasicMaterial({ ...materialOpts, color: 16119802, opacity: 0.92 });
6027
- this.axisGizmoGroup = new THREE10.Group();
6919
+ this.axisCenterMaterial = new THREE11.MeshBasicMaterial({ ...materialOpts, color: 16119802, opacity: 0.92 });
6920
+ this.axisGizmoGroup = new THREE11.Group();
6028
6921
  this.axisGizmoGroup.name = "SelectedBoneAxisGizmo";
6029
6922
  this.axisGizmoGroup.visible = false;
6030
6923
  this.axisGizmoGroup.renderOrder = 1e3;
6031
- const pitchRing = new THREE10.Mesh(this.axisRingGeometry, this.pitchRingMaterial);
6924
+ const pitchRing = new THREE11.Mesh(this.axisRingGeometry, this.pitchRingMaterial);
6032
6925
  pitchRing.renderOrder = 1e3;
6033
6926
  pitchRing.userData.axis = "pitch";
6034
- const pitchHit = new THREE10.Mesh(this.axisRingHitGeometry, this.axisHitMaterial);
6927
+ const pitchHit = new THREE11.Mesh(this.axisRingHitGeometry, this.axisHitMaterial);
6035
6928
  pitchHit.userData.axis = "pitch";
6036
- const yawRing = new THREE10.Mesh(this.axisRingGeometry, this.yawRingMaterial);
6929
+ const yawRing = new THREE11.Mesh(this.axisRingGeometry, this.yawRingMaterial);
6037
6930
  yawRing.renderOrder = 1e3;
6038
6931
  yawRing.userData.axis = "yaw";
6039
- const yawHit = new THREE10.Mesh(this.axisRingHitGeometry, this.axisHitMaterial);
6932
+ const yawHit = new THREE11.Mesh(this.axisRingHitGeometry, this.axisHitMaterial);
6040
6933
  yawHit.userData.axis = "yaw";
6041
- const rollRing = new THREE10.Mesh(this.axisRingGeometry, this.rollRingMaterial);
6934
+ const rollRing = new THREE11.Mesh(this.axisRingGeometry, this.rollRingMaterial);
6042
6935
  rollRing.renderOrder = 1e3;
6043
6936
  rollRing.userData.axis = "roll";
6044
- const rollHit = new THREE10.Mesh(this.axisRingHitGeometry, this.axisHitMaterial);
6937
+ const rollHit = new THREE11.Mesh(this.axisRingHitGeometry, this.axisHitMaterial);
6045
6938
  rollHit.rotation.copy(rollRing.rotation);
6046
6939
  rollHit.userData.axis = "roll";
6047
- const center = new THREE10.Mesh(this.axisCenterGeometry, this.axisCenterMaterial);
6940
+ const center = new THREE11.Mesh(this.axisCenterGeometry, this.axisCenterMaterial);
6048
6941
  center.renderOrder = 1001;
6049
6942
  this.axisGizmoMeshes = {
6050
6943
  pitch: pitchRing,
@@ -6084,11 +6977,11 @@ var SkeletonOverlay = class {
6084
6977
  this.boneTree = allBones.map((bone) => {
6085
6978
  const isMapped = this.mappedBoneNames.has(bone.name);
6086
6979
  const semanticKey = this.semanticKeyMap.get(bone.name) ?? null;
6087
- const boneChildren = bone.children.filter((c) => isBone2(c) && includedBones.has(c)).map((c) => c.name);
6088
- const parentName = bone.parent && isBone2(bone.parent) && includedBones.has(bone.parent) ? bone.parent.name : null;
6980
+ const boneChildren = bone.children.filter((c) => isBone3(c) && includedBones.has(c)).map((c) => c.name);
6981
+ const parentName = bone.parent && isBone3(bone.parent) && includedBones.has(bone.parent) ? bone.parent.name : null;
6089
6982
  let depth = 0;
6090
6983
  let current = bone.parent;
6091
- while (current && isBone2(current) && includedBones.has(current)) {
6984
+ while (current && isBone3(current) && includedBones.has(current)) {
6092
6985
  depth++;
6093
6986
  current = current.parent;
6094
6987
  }
@@ -6104,7 +6997,7 @@ var SkeletonOverlay = class {
6104
6997
  });
6105
6998
  for (const bone of allBones) {
6106
6999
  const boneChildren = bone.children.filter(
6107
- (c) => isBone2(c) && includedBones.has(c)
7000
+ (c) => isBone3(c) && includedBones.has(c)
6108
7001
  );
6109
7002
  if (boneChildren.length > 0) {
6110
7003
  if (shouldSkipRootHelperConnection(bone, boneChildren)) {
@@ -6118,15 +7011,15 @@ var SkeletonOverlay = class {
6118
7011
  }
6119
7012
  }
6120
7013
  this.group.add(this.axisGizmoGroup);
6121
- const sentinelGeo = new THREE10.SphereGeometry(1e-6, 4, 2);
6122
- const sentinelMat = new THREE10.MeshBasicMaterial({
7014
+ const sentinelGeo = new THREE11.SphereGeometry(1e-6, 4, 2);
7015
+ const sentinelMat = new THREE11.MeshBasicMaterial({
6123
7016
  transparent: true,
6124
7017
  opacity: 0,
6125
7018
  depthWrite: false,
6126
7019
  depthTest: false
6127
7020
  });
6128
7021
  sentinelMat.colorWrite = false;
6129
- const sentinel = new THREE10.Mesh(sentinelGeo, sentinelMat);
7022
+ const sentinel = new THREE11.Mesh(sentinelGeo, sentinelMat);
6130
7023
  sentinel.visible = true;
6131
7024
  sentinel.frustumCulled = false;
6132
7025
  sentinel.onBeforeRender = (renderer, _scene, camera) => this.update(camera, renderer.domElement);
@@ -6138,7 +7031,7 @@ var SkeletonOverlay = class {
6138
7031
  createBoneMesh(parentBone, childBone) {
6139
7032
  const isMapped = this.mappedBoneNames.has(parentBone.name);
6140
7033
  const material = isMapped ? this.mappedMaterial : this.unmappedMaterial;
6141
- const mesh = new THREE10.Mesh(this.sharedGeometry, material);
7034
+ const mesh = new THREE11.Mesh(this.sharedGeometry, material);
6142
7035
  mesh.name = `bone_vis_${parentBone.name}${childBone ? `_${childBone.name}` : "_leaf"}`;
6143
7036
  mesh.frustumCulled = false;
6144
7037
  mesh.renderOrder = 999;
@@ -6218,20 +7111,20 @@ var SkeletonOverlay = class {
6218
7111
  bone.getWorldPosition(_parentPos);
6219
7112
  let referenceLength = 0;
6220
7113
  for (const child of bone.children) {
6221
- if (!isBone2(child)) continue;
7114
+ if (!isBone3(child)) continue;
6222
7115
  child.getWorldPosition(_childPos);
6223
7116
  const nextDistance = _parentPos.distanceTo(_childPos);
6224
7117
  if (nextDistance <= 1e-4) continue;
6225
7118
  referenceLength = referenceLength === 0 ? nextDistance : Math.min(referenceLength, nextDistance);
6226
7119
  }
6227
- if (referenceLength <= 1e-4 && isBone2(bone.parent)) {
7120
+ if (referenceLength <= 1e-4 && isBone3(bone.parent)) {
6228
7121
  bone.parent.getWorldPosition(_childPos);
6229
7122
  referenceLength = _parentPos.distanceTo(_childPos);
6230
7123
  }
6231
7124
  if (referenceLength <= 1e-4) {
6232
7125
  referenceLength = LEAF_BONE_LENGTH;
6233
7126
  }
6234
- return THREE10.MathUtils.clamp(
7127
+ return THREE11.MathUtils.clamp(
6235
7128
  referenceLength * AXIS_GIZMO_RADIUS_FACTOR,
6236
7129
  AXIS_GIZMO_MIN_RADIUS,
6237
7130
  AXIS_GIZMO_MAX_RADIUS
@@ -6247,7 +7140,7 @@ var SkeletonOverlay = class {
6247
7140
  _cameraSpacePosition.copy(origin).applyMatrix4(perspectiveCamera.matrixWorldInverse);
6248
7141
  const depth = -_cameraSpacePosition.z;
6249
7142
  if (!Number.isFinite(depth) || depth <= perspectiveCamera.near) return null;
6250
- const visibleWorldHeight = 2 * depth * Math.tan(THREE10.MathUtils.degToRad(perspectiveCamera.getEffectiveFOV()) / 2);
7143
+ const visibleWorldHeight = 2 * depth * Math.tan(THREE11.MathUtils.degToRad(perspectiveCamera.getEffectiveFOV()) / 2);
6251
7144
  return visibleWorldHeight * (AXIS_GIZMO_SCREEN_RADIUS_PX / viewportHeight);
6252
7145
  }
6253
7146
  if (camera.isOrthographicCamera) {
@@ -6378,9 +7271,9 @@ var SkeletonOverlay = class {
6378
7271
  return {
6379
7272
  position: [_bonePos.x, _bonePos.y, _bonePos.z],
6380
7273
  rotation: [
6381
- THREE10.MathUtils.radToDeg(_boneEuler.x),
6382
- THREE10.MathUtils.radToDeg(_boneEuler.y),
6383
- THREE10.MathUtils.radToDeg(_boneEuler.z)
7274
+ THREE11.MathUtils.radToDeg(_boneEuler.x),
7275
+ THREE11.MathUtils.radToDeg(_boneEuler.y),
7276
+ THREE11.MathUtils.radToDeg(_boneEuler.z)
6384
7277
  ]
6385
7278
  };
6386
7279
  }
@@ -6582,7 +7475,7 @@ function getDeltaMagnitudeAtIndex(mesh, morphAttribute, index) {
6582
7475
  return Math.hypot(dx, dy, dz);
6583
7476
  }
6584
7477
  function resolveMorphPreviewMeshes(root, morphName, meshNames) {
6585
- const nameFilter = meshNames?.length ? new Set(meshNames) : null;
7478
+ const nameFilter = meshNames ? new Set(meshNames) : null;
6586
7479
  const meshes = [];
6587
7480
  root.traverse((obj) => {
6588
7481
  const mesh = obj;
@@ -6631,7 +7524,7 @@ function writePointColor(colors, offset, intensity2) {
6631
7524
  colors[offset + 1] = COLOR_LOW.g + (COLOR_HIGH.g - COLOR_LOW.g) * intensity2;
6632
7525
  colors[offset + 2] = COLOR_LOW.b + (COLOR_HIGH.b - COLOR_LOW.b) * intensity2;
6633
7526
  }
6634
- function buildMorphVertexPreviewRenderData(meshes, morphName, cache, options = {}) {
7527
+ function buildMorphVertexPreviewRenderData(meshes, morphName, cache, options = {}, previous = null) {
6635
7528
  const threshold = options.threshold ?? DEFAULT_THRESHOLD;
6636
7529
  const maxPoints = options.maxPoints ?? DEFAULT_MAX_POINTS;
6637
7530
  const sampledMeshes = [];
@@ -6648,9 +7541,9 @@ function buildMorphVertexPreviewRenderData(meshes, morphName, cache, options = {
6648
7541
  pointCount += sample.indices.length;
6649
7542
  }
6650
7543
  if (pointCount === 0) return null;
6651
- const positions = new Float32Array(pointCount * 3);
6652
- const colors = new Float32Array(pointCount * 3);
6653
- const vertex = new THREE10.Vector3();
7544
+ const positions = previous?.positions.length === pointCount * 3 ? previous.positions : new Float32Array(pointCount * 3);
7545
+ const colors = previous?.colors.length === pointCount * 3 ? previous.colors : new Float32Array(pointCount * 3);
7546
+ const vertex = new THREE11.Vector3();
6654
7547
  let cursor = 0;
6655
7548
  for (const { mesh, sample } of sampledMeshes) {
6656
7549
  mesh.updateWorldMatrix(true, false);
@@ -6706,6 +7599,7 @@ var MorphVertexPreview = class {
6706
7599
  __publicField(this, "cache", /* @__PURE__ */ new Map());
6707
7600
  __publicField(this, "points", null);
6708
7601
  __publicField(this, "active", null);
7602
+ __publicField(this, "renderData", null);
6709
7603
  __publicField(this, "disposed", false);
6710
7604
  __publicField(this, "unbindRender", null);
6711
7605
  }
@@ -6715,12 +7609,18 @@ var MorphVertexPreview = class {
6715
7609
  this.hide();
6716
7610
  return;
6717
7611
  }
7612
+ if (this.active?.morphName !== morphName || this.active?.meshNames?.length !== meshNames?.length || meshNames?.some((name, index) => name !== this.active?.meshNames?.[index])) {
7613
+ this.cache.clear();
7614
+ this.renderData = null;
7615
+ }
6718
7616
  this.active = { morphName, meshNames: meshNames?.slice() };
6719
7617
  this.unbindRender ?? (this.unbindRender = bindPreviewRender(this.scene, () => this.update()));
6720
7618
  this.update();
6721
7619
  }
6722
7620
  hide() {
6723
7621
  this.active = null;
7622
+ this.cache.clear();
7623
+ this.renderData = null;
6724
7624
  this.unbindRender?.();
6725
7625
  this.unbindRender = null;
6726
7626
  if (this.points) this.points.visible = false;
@@ -6737,7 +7637,7 @@ var MorphVertexPreview = class {
6737
7637
  this.points = null;
6738
7638
  }
6739
7639
  createPoints() {
6740
- const points = new THREE10.Points(new THREE10.BufferGeometry(), new THREE10.PointsMaterial({
7640
+ const points = new THREE11.Points(new THREE11.BufferGeometry(), new THREE11.PointsMaterial({
6741
7641
  size: 0.012,
6742
7642
  sizeAttenuation: true,
6743
7643
  transparent: true,
@@ -6758,26 +7658,31 @@ var MorphVertexPreview = class {
6758
7658
  if (this.disposed || !this.active) return;
6759
7659
  const { morphName, meshNames } = this.active;
6760
7660
  const meshes = resolveMorphPreviewMeshes(this.scene, morphName, meshNames);
6761
- const data = buildMorphVertexPreviewRenderData(meshes, morphName, this.cache, this.options);
7661
+ const data = buildMorphVertexPreviewRenderData(meshes, morphName, this.cache, this.options, this.renderData);
7662
+ this.renderData = data;
6762
7663
  if (!data) {
6763
7664
  if (this.points) this.points.visible = false;
6764
7665
  return;
6765
7666
  }
6766
7667
  const points = this.points ?? this.createPoints();
6767
- this.updateAttribute(points.geometry, "position", data.positions, data.pointCount);
6768
- this.updateAttribute(points.geometry, "color", data.colors, data.pointCount);
7668
+ this.updateGeometry(points.geometry, data);
6769
7669
  points.geometry.computeBoundingSphere();
6770
7670
  points.material.size = data.pointCount > 6e3 ? 9e-3 : 0.012;
6771
7671
  points.visible = true;
6772
7672
  }
6773
- updateAttribute(geometry, name, values, pointCount) {
6774
- const attribute = geometry.getAttribute(name);
6775
- if (!attribute || attribute.count !== pointCount) {
6776
- geometry.setAttribute(name, new THREE10.BufferAttribute(values, 3));
7673
+ updateGeometry(geometry, data) {
7674
+ const position = geometry.getAttribute("position");
7675
+ const color = geometry.getAttribute("color");
7676
+ if (!position || !color || position.count !== data.pointCount || color.count !== data.pointCount) {
7677
+ if (position || color) geometry.dispose();
7678
+ geometry.setAttribute("position", new THREE11.BufferAttribute(data.positions, 3));
7679
+ geometry.setAttribute("color", new THREE11.BufferAttribute(data.colors, 3));
6777
7680
  return;
6778
7681
  }
6779
- attribute.array.set(values);
6780
- attribute.needsUpdate = true;
7682
+ if (position.array !== data.positions) position.array.set(data.positions);
7683
+ if (color.array !== data.colors) color.array.set(data.colors);
7684
+ position.needsUpdate = true;
7685
+ color.needsUpdate = true;
6781
7686
  }
6782
7687
  };
6783
7688
 
@@ -6831,7 +7736,7 @@ var transform = (obj) => ({
6831
7736
  rotation: { x: obj.quaternion.x, y: obj.quaternion.y, z: obj.quaternion.z, w: obj.quaternion.w },
6832
7737
  scale: { x: obj.scale.x, y: obj.scale.y, z: obj.scale.z }
6833
7738
  });
6834
- var snapshot = (obj, reference) => {
7739
+ var snapshot2 = (obj, reference) => {
6835
7740
  if (!reference) {
6836
7741
  return {
6837
7742
  obj,
@@ -6881,6 +7786,7 @@ var ThreeModelInspector = class {
6881
7786
  const allMeshes = [];
6882
7787
  const boneObjects = [];
6883
7788
  model.traverse((obj) => {
7789
+ if (highlightOverlays.has(obj)) return;
6884
7790
  objects.push(obj);
6885
7791
  if (obj.isMesh) allMeshes.push(obj);
6886
7792
  if (obj.isBone || obj.type === "Bone") boneObjects.push(obj);
@@ -6916,7 +7822,7 @@ var ThreeModelInspector = class {
6916
7822
  }
6917
7823
  const bones = {};
6918
7824
  for (const bone of boneObjects) {
6919
- if (bone.name) bones[bone.name] = snapshot(bone, references?.get(bone));
7825
+ if (bone.name) bones[bone.name] = snapshot2(bone, references?.get(bone));
6920
7826
  }
6921
7827
  const profile = options.profile;
6922
7828
  if (profile?.boneNodes) {
@@ -6925,7 +7831,7 @@ var ThreeModelInspector = class {
6925
7831
  const suffix = profile.boneSuffix ?? "";
6926
7832
  const name = `${base.startsWith(prefix) ? "" : prefix}${base}${base.endsWith(suffix) ? "" : suffix}`;
6927
7833
  const object = model.getObjectByName(name) ?? model.getObjectByName(base);
6928
- if (object) bones[key] = bones[object.name] ?? snapshot(object, references?.get(object));
7834
+ if (object) bones[key] = bones[object.name] ?? snapshot2(object, references?.get(object));
6929
7835
  }
6930
7836
  }
6931
7837
  return {
@@ -7033,6 +7939,16 @@ function serializeAnimationClips(model, clips, inspection) {
7033
7939
  const meshIds = new Map(Array.from(inspection.meshBindings, ([id, mesh]) => [mesh, id]));
7034
7940
  const boneIds = new Map(Array.from(inspection.boneBindings, ([id, bone]) => [bone, id]));
7035
7941
  const objectIds = new Map(Array.from(inspection.objectBindings, ([id, object]) => [object, id]));
7942
+ const morphIds = /* @__PURE__ */ new Map();
7943
+ for (const [id, { mesh, index }] of inspection.morphBindings) {
7944
+ let indices = morphIds.get(mesh);
7945
+ if (!indices) {
7946
+ indices = /* @__PURE__ */ new Map();
7947
+ morphIds.set(mesh, indices);
7948
+ }
7949
+ if (!indices.has(index)) indices.set(index, id);
7950
+ }
7951
+ const destinations = /* @__PURE__ */ new Map();
7036
7952
  return clips.map((clip) => {
7037
7953
  const tracks = [];
7038
7954
  const channelKinds = /* @__PURE__ */ new Map();
@@ -7044,7 +7960,11 @@ function serializeAnimationClips(model, clips, inspection) {
7044
7960
  return id;
7045
7961
  };
7046
7962
  withComponentTracks(clip).tracks.forEach((track, index) => {
7047
- const found = findTrackTarget(model, track);
7963
+ let found = destinations.get(track.name);
7964
+ if (found === void 0) {
7965
+ found = findTrackTarget(model, track);
7966
+ destinations.set(track.name, found);
7967
+ }
7048
7968
  if (!found?.object) return;
7049
7969
  const { parsed, object } = found;
7050
7970
  let target = null;
@@ -7053,8 +7973,9 @@ function serializeAnimationClips(model, clips, inspection) {
7053
7973
  const mesh = object;
7054
7974
  const requested = parsed.propertyIndex;
7055
7975
  const indexValue = typeof requested === "number" ? requested : Number.isFinite(Number(requested)) ? Number(requested) : mesh.morphTargetDictionary?.[String(requested)];
7056
- const binding = Array.from(inspection.morphBindings).find(([, entry]) => entry.mesh === mesh && entry.index === indexValue);
7057
- if (binding) target = { kind: "morphTarget", meshId: meshIds.get(mesh), morphTargetId: binding[0] };
7976
+ if (indexValue === void 0) return;
7977
+ const morphTargetId = morphIds.get(mesh)?.get(indexValue);
7978
+ if (morphTargetId !== void 0) target = { kind: "morphTarget", meshId: meshIds.get(mesh), morphTargetId };
7058
7979
  } else if (property === "visible" && object.isMesh) {
7059
7980
  target = { kind: "meshVisibility", meshId: meshIds.get(object) };
7060
7981
  } else {
@@ -7068,7 +7989,7 @@ function serializeAnimationClips(model, clips, inspection) {
7068
7989
  const kind = target.kind === "morphTarget" ? "face" : target.kind === "boneTransform" ? "body" : "scene";
7069
7990
  const cubic = !!track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline;
7070
7991
  const size = track.getValueSize() / (cubic ? 3 : 1);
7071
- if (!Array.from(track.values).every(Number.isFinite)) {
7992
+ if (!track.values.every(Number.isFinite)) {
7072
7993
  throw new Error(`Animation track ${JSON.stringify(track.name)} contains non-finite samples.`);
7073
7994
  }
7074
7995
  const values = cubic ? Array.from(track.times).flatMap((_, key) => Array.from(track.values.slice((key * 3 + 1) * size, (key * 3 + 2) * size))) : Array.from(track.values);
@@ -7092,15 +8013,17 @@ function serializeAnimationClips(model, clips, inspection) {
7092
8013
  });
7093
8014
  }
7094
8015
  var materials = (value) => value ? Array.isArray(value) ? value : [value] : [];
8016
+ var highlightOverlays = /* @__PURE__ */ new WeakSet();
7095
8017
  var ThreeFrameApplier = class {
7096
8018
  constructor() {
7097
8019
  __publicField(this, "meshes", /* @__PURE__ */ new Map());
7098
8020
  __publicField(this, "morphs", /* @__PURE__ */ new Map());
7099
8021
  __publicField(this, "bones", /* @__PURE__ */ new Map());
7100
8022
  __publicField(this, "objects", /* @__PURE__ */ new Map());
7101
- __publicField(this, "originalEmissive", /* @__PURE__ */ new Map());
8023
+ __publicField(this, "highlighted", /* @__PURE__ */ new Map());
7102
8024
  }
7103
8025
  setBindings(inspection) {
8026
+ this.clearMeshHighlights();
7104
8027
  this.meshes = new Map(inspection.meshBindings);
7105
8028
  this.morphs = new Map(inspection.morphBindings);
7106
8029
  this.bones = new Map(inspection.boneBindings);
@@ -7146,7 +8069,7 @@ var ThreeFrameApplier = class {
7146
8069
  const config = configs[object.name];
7147
8070
  if (!config) return;
7148
8071
  if (typeof config.visible === "boolean") object.visible = config.visible;
7149
- if (config.material) this.applyMaterial(object, config.material);
8072
+ if (config.material) this.applyHighlightedMeshMaterial(object, config.material);
7150
8073
  });
7151
8074
  }
7152
8075
  getMeshMaterialConfig(root, name) {
@@ -7154,38 +8077,97 @@ var ThreeFrameApplier = class {
7154
8077
  this.visitMesh(root, name, (mesh) => {
7155
8078
  const material = materials(mesh.material)[0];
7156
8079
  if (!material) return;
7157
- const blending = Object.entries(THREE_BLENDING_MODES).find(([, value]) => value === material.blending)?.[0] ?? "Normal";
8080
+ const blending = Object.keys(THREE_BLENDING_MODES).find((key) => THREE_BLENDING_MODES[key] === material.blending) ?? "Normal";
7158
8081
  result = { renderOrder: mesh.renderOrder, transparent: material.transparent, opacity: material.opacity, depthWrite: material.depthWrite, depthTest: material.depthTest, blending };
7159
8082
  });
7160
8083
  return result;
7161
8084
  }
7162
8085
  setMeshMaterialConfig(root, name, config) {
7163
- this.visitMesh(root, name, (mesh) => this.applyMaterial(mesh, config));
8086
+ this.visitMesh(root, name, (mesh) => this.applyHighlightedMeshMaterial(mesh, config));
8087
+ }
8088
+ applyHighlightedMeshMaterial(mesh, config) {
8089
+ this.applyMaterial(mesh, config);
8090
+ const original = this.highlighted.get(mesh)?.original;
8091
+ if (!original) return;
8092
+ const highlighted = mesh.material;
8093
+ try {
8094
+ mesh.material = original;
8095
+ this.applyMaterial(mesh, config);
8096
+ } finally {
8097
+ mesh.material = highlighted;
8098
+ }
7164
8099
  }
7165
8100
  setMeshVisible(root, name, visible) {
7166
8101
  this.visitMesh(root, name, (mesh) => {
7167
8102
  mesh.visible = visible;
7168
8103
  });
7169
8104
  }
7170
- highlightMesh(root, name, color = 65535, intensity2 = 0.5) {
7171
- root.traverse((object) => {
7172
- if (!object.isMesh) return;
7173
- for (const material of materials(object.material)) {
8105
+ /** Apply a complete Rust snapshot. Only native resources and ID bindings live here. */
8106
+ applyMeshHighlightFrame(frame) {
8107
+ if (frame.version !== 1) throw new Error(`Unsupported mesh highlight frame version: ${frame.version}`);
8108
+ const selected = new Set(frame.highlights.map((write) => this.meshes.get(write.meshId)));
8109
+ for (const mesh of this.highlighted.keys()) {
8110
+ if (!selected.has(mesh)) this.clearMeshHighlight(mesh);
8111
+ }
8112
+ for (const write of frame.highlights) {
8113
+ if (write.edgeMode !== "triangles") throw new Error(`Unsupported highlight edge mode: ${write.edgeMode}`);
8114
+ const mesh = this.meshes.get(write.meshId);
8115
+ if (!mesh) continue;
8116
+ let state = this.highlighted.get(mesh);
8117
+ if (!state) {
8118
+ const original = mesh.material;
8119
+ const copies = materials(original).map((material) => {
8120
+ const copy2 = material.clone();
8121
+ copy2.polygonOffset = true;
8122
+ copy2.polygonOffsetFactor = 1;
8123
+ copy2.polygonOffsetUnits = 1;
8124
+ return copy2;
8125
+ });
8126
+ mesh.material = Array.isArray(original) ? copies : copies[0];
8127
+ const overlay = mesh.clone(false);
8128
+ overlay.name = "";
8129
+ overlay.userData = {};
8130
+ overlay.position.set(0, 0, 0);
8131
+ overlay.quaternion.identity();
8132
+ overlay.scale.set(1, 1, 1);
8133
+ overlay.matrix.identity();
8134
+ overlay.matrixAutoUpdate = true;
8135
+ overlay.visible = true;
8136
+ overlay.castShadow = overlay.receiveShadow = false;
8137
+ overlay.raycast = () => {
8138
+ };
8139
+ overlay.material = new MeshBasicMaterial({ wireframe: write.edgeMode === "triangles", transparent: true, depthWrite: false, toneMapped: false });
8140
+ overlay.onBeforeRender = () => {
8141
+ overlay.geometry = mesh.geometry;
8142
+ overlay.morphTargetInfluences = mesh.morphTargetInfluences;
8143
+ };
8144
+ highlightOverlays.add(overlay);
8145
+ mesh.add(overlay);
8146
+ state = { original, overlay };
8147
+ this.highlighted.set(mesh, state);
8148
+ }
8149
+ for (const material of materials(mesh.material)) {
7174
8150
  if (!material.emissive) continue;
7175
- const key = `${object.uuid}:${material.uuid}`;
7176
- if (name === object.name) {
7177
- if (!this.originalEmissive.has(key)) this.originalEmissive.set(key, { color: material.emissive.getHex(), intensity: material.emissiveIntensity ?? 0 });
7178
- material.emissive.setHex(color);
7179
- material.emissiveIntensity = intensity2;
7180
- } else {
7181
- const original = this.originalEmissive.get(key);
7182
- if (original) {
7183
- material.emissive.setHex(original.color);
7184
- material.emissiveIntensity = original.intensity;
7185
- }
7186
- }
8151
+ material.emissive.setRGB(...write.fillColor);
8152
+ material.emissiveIntensity = write.fillIntensity;
7187
8153
  }
7188
- });
8154
+ const edges = state.overlay.material;
8155
+ edges.color.setRGB(...write.edgeColor);
8156
+ edges.opacity = write.edgeOpacity;
8157
+ edges.side = materials(mesh.material)[0]?.side ?? edges.side;
8158
+ state.overlay.renderOrder = mesh.renderOrder + 1;
8159
+ }
8160
+ }
8161
+ clearMeshHighlights() {
8162
+ for (const mesh of this.highlighted.keys()) this.clearMeshHighlight(mesh);
8163
+ }
8164
+ clearMeshHighlight(mesh) {
8165
+ const state = this.highlighted.get(mesh);
8166
+ state.overlay.removeFromParent();
8167
+ state.overlay.material.dispose();
8168
+ for (const material of materials(mesh.material)) material.dispose();
8169
+ mesh.material = state.original;
8170
+ this.highlighted.delete(mesh);
7189
8171
  }
7190
8172
  applyHairAppearance(meshes, appearance) {
7191
8173
  for (const mesh of meshes) {
@@ -7322,13 +8304,19 @@ var ThreeFrameApplier = class {
7322
8304
  applyMaterial(mesh, config) {
7323
8305
  if (typeof config.renderOrder === "number") mesh.renderOrder = config.renderOrder;
7324
8306
  for (const material of materials(mesh.material)) {
7325
- if (typeof config.opacity === "number") material.opacity = config.opacity;
7326
- if (typeof config.transparent === "boolean") material.transparent = config.transparent;
7327
- else if (typeof config.opacity === "number" && config.opacity < 1) material.transparent = true;
7328
- if (typeof config.depthWrite === "boolean") material.depthWrite = config.depthWrite;
7329
- if (typeof config.depthTest === "boolean") material.depthTest = config.depthTest;
7330
- if (config.blending) material.blending = THREE_BLENDING_MODES[config.blending];
7331
- material.needsUpdate = true;
8307
+ let changed = false;
8308
+ const assign = (key, value) => {
8309
+ if (material[key] === value) return;
8310
+ material[key] = value;
8311
+ changed = true;
8312
+ };
8313
+ if (typeof config.opacity === "number") assign("opacity", config.opacity);
8314
+ if (typeof config.transparent === "boolean") assign("transparent", config.transparent);
8315
+ else if (typeof config.opacity === "number" && config.opacity < 1) assign("transparent", true);
8316
+ if (typeof config.depthWrite === "boolean") assign("depthWrite", config.depthWrite);
8317
+ if (typeof config.depthTest === "boolean") assign("depthTest", config.depthTest);
8318
+ if (config.blending) assign("blending", THREE_BLENDING_MODES[config.blending]);
8319
+ if (changed) material.needsUpdate = true;
7332
8320
  }
7333
8321
  }
7334
8322
  visitMesh(root, name, visit) {
@@ -7340,11 +8328,11 @@ var ThreeFrameApplier = class {
7340
8328
  var collectMorphMeshes = (root) => {
7341
8329
  const meshes = [];
7342
8330
  root.traverse((object) => {
7343
- if (object.isMesh && morphEntries(object).length > 0) meshes.push(object);
8331
+ if (object.isMesh && !highlightOverlays.has(object) && morphEntries(object).length > 0) meshes.push(object);
7344
8332
  });
7345
8333
  return meshes;
7346
8334
  };
7347
8335
 
7348
- export { BLENDING_MODES, CHARACTER_SCENE_TYPES, CHARACTER_SCENE_TYPE_IDS, ThreeAnnotationController as CameraController, CameraDOMControls, CameraDOMControls as Controls, DEFAULT_CHARACTER_LIGHTING_PRESETS, DEFAULT_CHARACTER_LIGHTING_PRESET_ID, DEFAULT_CHARACTER_LIGHTING_PRESET_IDS, DEFAULT_CHARACTER_LIGHTING_SETTINGS, DEFAULT_CHARACTER_SCENE_TYPE_ID, DEFAULT_MARKER_AUTO_HIDE_MS, DEFAULT_PREVIEW_AUTO_CLEAR_MS, Embody, HtmlAnnotationMarkers, ThreeAnnotationMarkers as Markers3D, HtmlAnnotationMarkers as MarkersHTML, MorphVertexPreview, HtmlAnnotationMarkers as OverlayAnnotationMarkers, RUNTIME_ANNOTATION_REGION_PREFIX, RustAnnotationCameraCore, SkeletonOverlay, THREE_BLENDING_MODES, ThreeAnnotationController, ThreeAnnotationMarkers, ThreeAnnotations, ThreeFrameApplier, ThreeModelInspector, ThreeNativePlaybackConnector, analyzeModel, applyAUBoneBindingUpdate, applyBilateralAxisBindingUpdate, applyBoneAxisBindingUpdate, applyCharacterModelTransform, bindModelReferencePose, boneResolutionProfile, buildBoneAuOptions, buildMappedBoneInfo, buildMorphVertexPreviewRenderData, buildMorphVertexPreviewSample, buildRegionDisplayOptions, captureModelReferencePose, classifyAuAsJointControl, collectMorphMeshes, collectSkeletonBones, computeHumanoidSkeletonTemplateRestBounds, createAnimationClipFromClipIR, createBilateralBoneAxisAu, createBoneAxisAu, createCharacterHost, createDefaultCharacterLighting, createDefaultCharacterScene, createDefaultCharacterSceneAsync, createDefaultCharacterSceneRuntime, createMarkerLifecycleRevealOptions, createMarkerVisibilityLifecycle, createRuntimeAnnotationPreviewLifecycle, createRustAnnotationCameraCore, createRustEmbodyHost, createRustEmbodyHostUnbound, createScenePlaybackFrame, createShadowPlane, createValidationSkeletonFromHumanoidTemplate, detectAnnotationLaterality, disposeCharacterModel, ensureBilateralBoneNodeKeys, ensureBoneNodeKey, extendCharacterConfigWithPreset, extendModelReferencePose, extractHumanoidSkeletonTemplateFromModel, extractModelData, extractProfileOverrides, filterCameraViewRegions, findModelRoot, findNodeKeyForBone, formatAxisDirectionLabel, formatAxisLabel, fuzzyNameMatch, getAUBoneBindingState, getAxisFromChannel, getBilateralAxisBindingState, getBoneAxisBindingState, getDefaultAnnotationLaterality, getHumanoidSkeletonTemplate, getHumanoidSkeletonTemplateBoneNames, getHumanoidSpecification, getMeshNamesForAUProfile, getModelLocalOrbitAngle, getPreset, getRuntimeAUMeshSideOffset, getRuntimeAUMorphAnchorPoint, getRuntimeAnnotationSide, getSemanticHorizontalSign, getSemanticHorizontalSignForSide, getWorldDirectionForCameraAngle, hasLeftRightMorphs, inferChiralBoneNamePair, inferEyeControlFamily, inferEyeControlScope, inferRuntimeAnnotationPreviewSide, isJointControlAuInfo, isMaxDegreesOnlyAxisBindingUpdate, isMixedAU, isRuntimeAnnotationRegionName, loadCharacterModel, mergeCharacterRegionsByName, normalizeDefaultCharacterLightingSettings, normalizeHairColorAppearance, parseCharacterModel, passesMarkerCameraAngleGate, pickContinuumAnnotationAuId, renderScenePlayback, resolveAnnotationCharacterConfig, resolveBoneAxisChannel, resolveBoneNameForNodeKey, resolveBoneNames, resolveContinuumDisplayLabel, resolveFaceCenter, resolveHumanoidCharacterization, resolveMarkerAnchorRegion, resolveMorphPreviewMeshes, resolveRegionCameraAngle, resolveRegionVisibilityCameraAngle, resolveSkeletonOverlayStartBoneName, resolveVisemeProfile, serializeAnimationClips, setAUMorphTargets, setHumanoidRoleBinding, shouldProjectMarkerAnchorToSurface, shouldUseFaceCenterForMarkerAnchor, stripConfiguredBoneAffixes, supportsRustEmbodyPreset, toWorldDirection, validateHumanoidCharacterization, validateMappings };
8336
+ export { BLENDING_MODES, CHARACTER_SCENE_TYPES, CHARACTER_SCENE_TYPE_IDS, ThreeAnnotationController as CameraController, CameraDOMControls, CameraDOMControls as Controls, DEFAULT_CHARACTER_LIGHTING_PRESETS, DEFAULT_CHARACTER_LIGHTING_PRESET_ID, DEFAULT_CHARACTER_LIGHTING_PRESET_IDS, DEFAULT_CHARACTER_LIGHTING_SETTINGS, DEFAULT_CHARACTER_SCENE_TYPE_ID, DEFAULT_MARKER_AUTO_HIDE_MS, DEFAULT_PREVIEW_AUTO_CLEAR_MS, Embody, HtmlAnnotationMarkers, ThreeAnnotationMarkers as Markers3D, HtmlAnnotationMarkers as MarkersHTML, MorphVertexPreview, HtmlAnnotationMarkers as OverlayAnnotationMarkers, RUNTIME_ANNOTATION_REGION_PREFIX, RustAnnotationCameraCore, SkeletonOverlay, THREE_BLENDING_MODES, ThreeAnnotationController, ThreeAnnotationMarkers, ThreeAnnotations, ThreeFrameApplier, ThreeModelInspector, ThreeNativePlaybackConnector, analyzeModel, applyAUBoneBindingUpdate, applyBilateralAxisBindingUpdate, applyBoneAxisBindingUpdate, applyCharacterModelTransform, bindModelReferencePose, boneResolutionProfile, buildBoneAuOptions, buildMappedBoneInfo, buildMorphVertexPreviewRenderData, buildMorphVertexPreviewSample, buildRegionDisplayOptions, captureModelReferencePose, classifyAuAsJointControl, collectMorphMeshes, collectSkeletonBones, computeHumanoidSkeletonTemplateRestBounds, convertMixamoFbxToVrmAnimations, convertMixamoToVrmAnimation, createAnimationClipFromClipIR, createAnimationClipFromVrmAnimation, createBilateralBoneAxisAu, createBoneAxisAu, createCharacterHost, createDefaultCharacterLighting, createDefaultCharacterScene, createDefaultCharacterSceneAsync, createDefaultCharacterSceneRuntime, createMarkerLifecycleRevealOptions, createMarkerVisibilityLifecycle, createRuntimeAnnotationPreviewLifecycle, createRustAnnotationCameraCore, createRustEmbodyHost, createRustEmbodyHostUnbound, createScenePlaybackFrame, createSceneSceneryRenderer, createShadowPlane, createValidationSkeletonFromHumanoidTemplate, detectAnnotationLaterality, disposeCharacterModel, ensureBilateralBoneNodeKeys, ensureBoneNodeKey, extendCharacterConfigWithPreset, extendModelReferencePose, extractHumanoidSkeletonTemplateFromModel, extractModelData, extractProfileOverrides, filterCameraViewRegions, findModelRoot, findNodeKeyForBone, formatAxisDirectionLabel, formatAxisLabel, fuzzyNameMatch, getAUBoneBindingState, getAUBoneBindingStates, getAxisFromChannel, getBilateralAxisBindingState, getBoneAxisBindingState, getDefaultAnnotationLaterality, getHumanoidSkeletonTemplate, getHumanoidSkeletonTemplateBoneNames, getHumanoidSpecification, getMeshNamesForAUProfile, getModelLocalOrbitAngle, getPreset, getRuntimeAUMeshSideOffset, getRuntimeAUMorphAnchorPoint, getRuntimeAnnotationSide, getSemanticHorizontalSign, getSemanticHorizontalSignForSide, getWorldDirectionForCameraAngle, hasLeftRightMorphs, inferChiralBoneNamePair, inferEyeControlFamily, inferEyeControlScope, inferRuntimeAnnotationPreviewSide, inspectVrmAnimation, isJointControlAuInfo, isMaxDegreesOnlyAxisBindingUpdate, isMixedAU, isRuntimeAnnotationRegionName, loadCharacterModel, mergeCharacterRegionsByName, normalizeDefaultCharacterLightingSettings, normalizeHairColorAppearance, parseCharacterModel, passesMarkerCameraAngleGate, pickContinuumAnnotationAuId, renderScenePlayback, resolveAnnotationCharacterConfig, resolveBoneAxisChannel, resolveBoneNameForNodeKey, resolveBoneNames, resolveContinuumDisplayLabel, resolveFaceCenter, resolveHumanoidCharacterization, resolveMarkerAnchorRegion, resolveMorphMeshSelections, resolveMorphPreviewMeshes, resolveRegionCameraAngle, resolveRegionVisibilityCameraAngle, resolveSkeletonOverlayStartBoneName, resolveVisemeProfile, serializeAnimationClips, setAUMorphTargets, setHumanoidRoleBinding, shouldProjectMarkerAnchorToSurface, shouldUseFaceCenterForMarkerAnchor, stripConfiguredBoneAffixes, supportsRustEmbodyPreset, toWorldDirection, validateHumanoidCharacterization, validateMappings };
7349
8337
  //# sourceMappingURL=index.js.map
7350
8338
  //# sourceMappingURL=index.js.map