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