@lovelace_lol/embody 0.0.11 → 0.0.13

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,7 +1,7 @@
1
1
  import * as THREE3 from 'three';
2
2
  import { NoBlending, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NormalBlending, Vector3, Box3, Timer, Matrix4, AnimationMixer, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopOnce, LoopRepeat, LoopPingPong, PropertyBinding, Euler, Quaternion, AnimationUtils, InterpolateDiscrete, InterpolateLinear, 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
3
  import { CC4_HUMANOID_SKELETON_TEMPLATE, HUMANOID_SKELETON_TEMPLATES, DEFAULT_HAIR_COLOR_APPEARANCE, initEmbodyCore, requireInitializedEmbodyCore } from '@lovelace_lol/embody/wasm';
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, VISEME_KEYS } from '@lovelace_lol/embody/wasm';
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
5
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
6
6
  import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
7
7
  import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
@@ -6582,9 +6582,6 @@ var resolveContinuumDisplayLabel = (profile, negativeAuId, positiveAuId) => requ
6582
6582
  function inspectModel(model, meshes, profile) {
6583
6583
  return new ThreeModelInspector().inspectModel(model, { meshes, profile });
6584
6584
  }
6585
- function getBodyControls(profile, model) {
6586
- return request2("profile.getBodyControls", { profile, ...model ? { model: inspectModel(model, void 0, profile).descriptor } : {} });
6587
- }
6588
6585
  function extractModelData(model, meshes, animations = []) {
6589
6586
  const inspection = inspectModel(model, meshes);
6590
6587
  return JSON.parse(requireInitializedEmbodyCore().extract_model_data_json(json(inspection.descriptor), json(serializeAnimationClips(model, animations, inspection))));
@@ -7436,12 +7433,10 @@ var Embody = class _Embody {
7436
7433
  return parse(this.core.build_clip(source.name, json2(source.input), json2(buildOptions)));
7437
7434
  case "typed":
7438
7435
  return parse(this.core.build_typed_clip(source.name, json2(source.input), json2(buildOptions)));
7439
- case "semantic":
7440
- return parse(this.core.build_semantic_clip(source.name, json2(source.input), json2(buildOptions)));
7441
7436
  case "compiled":
7442
7437
  return source.clip;
7443
7438
  case "native":
7444
- throw new Error("Native animation sources do not require semantic compilation.");
7439
+ throw new Error("Native animation sources do not require AU compilation.");
7445
7440
  }
7446
7441
  }
7447
7442
  restValues(track) {
@@ -7880,7 +7875,8 @@ var Embody = class _Embody {
7880
7875
  return this.handle(source, options, play);
7881
7876
  }
7882
7877
  buildClip(name, curves, options = {}) {
7883
- return options.semanticAnimation ? this.semanticAnimationToClip(name, options.semanticAnimation, { ...options, faceCurves: curves }) : this.build({ name, kind: "curves", input: copy(curves), options: copy(options) }, options, false);
7878
+ if (options.semanticAnimation) throw new Error("Decode legacy named animation tracks to numeric AU curves before building a clip.");
7879
+ return this.build({ name, kind: "curves", input: copy(curves), options: copy(options) }, options, false);
7884
7880
  }
7885
7881
  snippetToClip(name, curves, options = {}) {
7886
7882
  return this.buildClip(name, curves, options);
@@ -7891,9 +7887,6 @@ var Embody = class _Embody {
7891
7887
  typedSnippetToClip(name, channels, options = {}) {
7892
7888
  return this.buildTypedClip(name, channels, options);
7893
7889
  }
7894
- semanticAnimationToClip(name, animation, options = {}) {
7895
- return this.build({ name, kind: "semantic", input: copy(animation), options: copy(options) }, options, false);
7896
- }
7897
7890
  playSnippet(snippet, curvesOrOptions = {}, options = {}) {
7898
7891
  return typeof snippet === "string" ? this.buildClip(snippet, curvesOrOptions, options).play() : this.buildClip(snippet.name, snippet.curves, curvesOrOptions).play();
7899
7892
  }
@@ -8103,26 +8096,8 @@ var Embody = class _Embody {
8103
8096
  getAUBalance(id) {
8104
8097
  return this.core.get_au_balance(id);
8105
8098
  }
8106
- getBodyControls() {
8107
- return parse(this.core.get_body_controls_json());
8108
- }
8109
- getSemanticPoseCatalog() {
8110
- return parse(this.core.get_semantic_pose_catalog_json());
8111
- }
8112
- captureSemanticPose() {
8113
- return parse(this.core.capture_semantic_pose_json());
8114
- }
8115
- validateSemanticPose(pose) {
8116
- return parse(this.core.validate_semantic_pose_json(json2(pose)));
8117
- }
8118
- applySemanticPose(pose) {
8119
- this.mutateLive(() => this.core.apply_semantic_pose_json(json2(pose)));
8120
- }
8121
- validateSemanticAnimation(animation, options = {}) {
8122
- this.core.validate_semantic_animation_json(json2(animation), json2(options));
8123
- }
8124
- resetBodyControls() {
8125
- this.mutateLive(() => this.core.reset_body_controls());
8099
+ getAUDescriptors() {
8100
+ return parse(this.core.get_au_descriptors_json());
8126
8101
  }
8127
8102
  setContinuum(negative, positive, value, balance = NaN) {
8128
8103
  this.mutateLive(() => this.core.set_continuum(negative, positive, value, balance));
@@ -8402,6 +8377,262 @@ async function createRustEmbodyHost(model, config = {}) {
8402
8377
  throw error;
8403
8378
  }
8404
8379
  }
8380
+
8381
+ // three/sceneInkBackgroundStyle.ts
8382
+ var SCENE_INK_BASE_GRADIENT = "radial-gradient(ellipse at 50% 42%, #46535c 0%, #35414a 42%, #29333b 76%, #252e35 100%)";
8383
+ var DEFAULT_PALETTE = ["#7cb8b2", "#627888", "#8b989f"];
8384
+ function createSceneInkBackgroundCss(palette = DEFAULT_PALETTE) {
8385
+ const [a, b, c] = palette;
8386
+ return [
8387
+ `radial-gradient(ellipse 46% 62% at calc(20% + var(--scene-ink-shift-x, 0%)) 30%, color-mix(in srgb, ${a} 24%, transparent), transparent 78%)`,
8388
+ `radial-gradient(ellipse 32% 44% at 34% calc(52% + var(--scene-ink-shift-y, 0%)), color-mix(in srgb, ${a} 12%, transparent), transparent 82%)`,
8389
+ `radial-gradient(ellipse 48% 60% at calc(82% - var(--scene-ink-shift-x, 0%)) 62%, color-mix(in srgb, ${b} 22%, transparent), transparent 80%)`,
8390
+ `radial-gradient(ellipse 28% 38% at 68% calc(26% - var(--scene-ink-shift-y, 0%)), color-mix(in srgb, ${b} 10%, transparent), transparent 82%)`,
8391
+ `radial-gradient(ellipse 50% 38% at 38% calc(78% + var(--scene-ink-shift-y, 0%)), color-mix(in srgb, ${c} 18%, transparent), transparent 82%)`,
8392
+ SCENE_INK_BASE_GRADIENT
8393
+ ].join(", ");
8394
+ }
8395
+ createSceneInkBackgroundCss();
8396
+
8397
+ // three/sceneInkBackground.ts
8398
+ var toCssColor = (tone) => `#${new THREE3.Color(tone).getHexString()}`;
8399
+ function createSceneInkBackground(onBackgroundChange) {
8400
+ let currentBackground = "";
8401
+ let disposed = false;
8402
+ const applyBackground = (palette) => {
8403
+ if (disposed) return;
8404
+ const colors = palette ? [toCssColor(palette[0]), toCssColor(palette[1]), toCssColor(palette[2])] : void 0;
8405
+ const background = createSceneInkBackgroundCss(colors);
8406
+ if (background === currentBackground) return;
8407
+ currentBackground = background;
8408
+ onBackgroundChange?.(background);
8409
+ };
8410
+ applyBackground();
8411
+ return {
8412
+ setPalette: applyBackground,
8413
+ dispose: () => {
8414
+ if (disposed) return;
8415
+ disposed = true;
8416
+ }
8417
+ };
8418
+ }
8419
+ var paletteCache = /* @__PURE__ */ new Map();
8420
+ var MAX_TEXTURE_SAMPLE_SIZE = 10;
8421
+ var MIN_SAMPLE_SATURATION = 0.04;
8422
+ var MIN_CANDIDATE_DISTANCE = 0.12;
8423
+ function resolveCharacterMaterialPalette(model, cacheKey) {
8424
+ const cached = getCachedPalette(cacheKey);
8425
+ if (cached.materialPalette !== void 0) {
8426
+ return cached.materialPalette;
8427
+ }
8428
+ const samples = collectPaletteSamples(model, false);
8429
+ const palette = buildPalette(samples);
8430
+ cached.materialPalette = palette;
8431
+ return palette;
8432
+ }
8433
+ function resolveCharacterTexturePalette(model, cacheKey) {
8434
+ const cached = getCachedPalette(cacheKey);
8435
+ if (cached.texturePalette !== void 0) {
8436
+ return Promise.resolve(cached.texturePalette);
8437
+ }
8438
+ if (cached.texturePalettePromise) {
8439
+ return cached.texturePalettePromise;
8440
+ }
8441
+ cached.texturePalettePromise = schedulePaletteExtraction(() => {
8442
+ const samples = collectPaletteSamples(model, true);
8443
+ const palette = buildPalette(samples);
8444
+ cached.texturePalette = palette;
8445
+ return palette;
8446
+ });
8447
+ return cached.texturePalettePromise;
8448
+ }
8449
+ function getCachedPalette(cacheKey) {
8450
+ const cached = paletteCache.get(cacheKey) ?? {};
8451
+ paletteCache.set(cacheKey, cached);
8452
+ return cached;
8453
+ }
8454
+ function schedulePaletteExtraction(extractPalette) {
8455
+ return new Promise((resolve) => {
8456
+ const run = () => resolve(extractPalette());
8457
+ if (typeof window !== "undefined" && "requestIdleCallback" in window) {
8458
+ window.requestIdleCallback(run, { timeout: 700 });
8459
+ return;
8460
+ }
8461
+ globalThis.setTimeout(run, 0);
8462
+ });
8463
+ }
8464
+ function collectPaletteSamples(model, includeTextures) {
8465
+ const samples = [];
8466
+ const seenMaterials = /* @__PURE__ */ new Set();
8467
+ const seenTextures = /* @__PURE__ */ new Set();
8468
+ model.traverse((object) => {
8469
+ const mesh = object;
8470
+ if (!mesh.isMesh || !mesh.material) {
8471
+ return;
8472
+ }
8473
+ const materials2 = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
8474
+ for (const material of materials2) {
8475
+ if (!material || seenMaterials.has(material.uuid)) {
8476
+ continue;
8477
+ }
8478
+ seenMaterials.add(material.uuid);
8479
+ collectMaterialSamples(material, samples, seenTextures, includeTextures);
8480
+ }
8481
+ });
8482
+ return samples;
8483
+ }
8484
+ function collectMaterialSamples(material, samples, seenTextures, includeTextures) {
8485
+ pushColorSample(samples, material.color, 1.25);
8486
+ pushColorSample(samples, material.emissive, 0.8);
8487
+ if (!includeTextures) {
8488
+ return;
8489
+ }
8490
+ if (material.map && !seenTextures.has(material.map.uuid)) {
8491
+ seenTextures.add(material.map.uuid);
8492
+ samples.push(...sampleTexture(material.map));
8493
+ }
8494
+ if (material.emissiveMap && !seenTextures.has(material.emissiveMap.uuid)) {
8495
+ seenTextures.add(material.emissiveMap.uuid);
8496
+ samples.push(...sampleTexture(material.emissiveMap).map(({ color, score }) => ({
8497
+ color,
8498
+ score: score * 0.65
8499
+ })));
8500
+ }
8501
+ }
8502
+ function pushColorSample(samples, color, score) {
8503
+ if (!color || !isUsableColor(color)) {
8504
+ return;
8505
+ }
8506
+ samples.push({
8507
+ color: color.clone(),
8508
+ score
8509
+ });
8510
+ }
8511
+ function sampleTexture(texture) {
8512
+ if (typeof document === "undefined") {
8513
+ return [];
8514
+ }
8515
+ const image = texture.image;
8516
+ const imageSize = getImageSize(image);
8517
+ if (!image || !imageSize) {
8518
+ return [];
8519
+ }
8520
+ const canvas = document.createElement("canvas");
8521
+ const width = Math.min(MAX_TEXTURE_SAMPLE_SIZE, imageSize.width);
8522
+ const height = Math.min(MAX_TEXTURE_SAMPLE_SIZE, imageSize.height);
8523
+ canvas.width = width;
8524
+ canvas.height = height;
8525
+ const context = canvas.getContext("2d", { willReadFrequently: true });
8526
+ if (!context) {
8527
+ return [];
8528
+ }
8529
+ try {
8530
+ context.drawImage(image, 0, 0, width, height);
8531
+ const data = context.getImageData(0, 0, width, height).data;
8532
+ const samples = [];
8533
+ for (let index = 0; index < data.length; index += 4) {
8534
+ const alpha = data[index + 3] / 255;
8535
+ if (alpha < 0.18) {
8536
+ continue;
8537
+ }
8538
+ const color = new THREE3.Color(
8539
+ data[index] / 255,
8540
+ data[index + 1] / 255,
8541
+ data[index + 2] / 255
8542
+ );
8543
+ if (!isUsableColor(color)) {
8544
+ continue;
8545
+ }
8546
+ const hsl = { h: 0, s: 0, l: 0 };
8547
+ color.getHSL(hsl);
8548
+ samples.push({
8549
+ color,
8550
+ score: alpha * (0.4 + hsl.s * 1.3)
8551
+ });
8552
+ }
8553
+ return samples;
8554
+ } catch {
8555
+ return [];
8556
+ }
8557
+ }
8558
+ function getImageSize(image) {
8559
+ if (!image || !("width" in image) || !("height" in image)) {
8560
+ return null;
8561
+ }
8562
+ const width = Number(image.width);
8563
+ const height = Number(image.height);
8564
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
8565
+ return null;
8566
+ }
8567
+ return { width, height };
8568
+ }
8569
+ function isUsableColor(color) {
8570
+ const hsl = { h: 0, s: 0, l: 0 };
8571
+ color.getHSL(hsl);
8572
+ return hsl.l > 0.05 && hsl.l < 0.94 && hsl.s >= MIN_SAMPLE_SATURATION;
8573
+ }
8574
+ function buildPalette(samples) {
8575
+ if (samples.length === 0) {
8576
+ return null;
8577
+ }
8578
+ const buckets = /* @__PURE__ */ new Map();
8579
+ for (const sample of samples) {
8580
+ const hsl = { h: 0, s: 0, l: 0 };
8581
+ sample.color.getHSL(hsl);
8582
+ const bucketId = Math.floor(hsl.h * 18);
8583
+ const bucket = buckets.get(bucketId) ?? { color: new THREE3.Color(0, 0, 0), score: 0 };
8584
+ bucket.color.r += sample.color.r * sample.score;
8585
+ bucket.color.g += sample.color.g * sample.score;
8586
+ bucket.color.b += sample.color.b * sample.score;
8587
+ bucket.score += sample.score;
8588
+ buckets.set(bucketId, bucket);
8589
+ }
8590
+ const candidates = Array.from(buckets.values()).filter((bucket) => bucket.score > 0).map((bucket) => ({
8591
+ color: massageBackgroundTone(bucket.color.multiplyScalar(1 / bucket.score)),
8592
+ score: bucket.score
8593
+ })).sort((a, b) => b.score - a.score);
8594
+ const selected = [];
8595
+ for (const candidate of candidates) {
8596
+ const distinctEnough = selected.every((color) => colorDistance(color, candidate.color) >= MIN_CANDIDATE_DISTANCE);
8597
+ if (!distinctEnough) {
8598
+ continue;
8599
+ }
8600
+ selected.push(candidate.color);
8601
+ if (selected.length === 3) {
8602
+ break;
8603
+ }
8604
+ }
8605
+ if (selected.length === 0) {
8606
+ return null;
8607
+ }
8608
+ while (selected.length < 3) {
8609
+ selected.push(createCompanionTone(selected[0], selected.length));
8610
+ }
8611
+ return [selected[0], selected[1], selected[2]];
8612
+ }
8613
+ function massageBackgroundTone(color) {
8614
+ const hsl = { h: 0, s: 0, l: 0 };
8615
+ color.getHSL(hsl);
8616
+ const saturation = THREE3.MathUtils.clamp(Math.max(hsl.s * 1.35, 0.46), 0.46, 0.82);
8617
+ const lightness = THREE3.MathUtils.clamp(hsl.l + 0.08, 0.42, 0.66);
8618
+ return new THREE3.Color().setHSL(hsl.h, saturation, lightness);
8619
+ }
8620
+ function createCompanionTone(source, index) {
8621
+ const hsl = { h: 0, s: 0, l: 0 };
8622
+ source.getHSL(hsl);
8623
+ const hueOffset = index === 1 ? 0.075 : -0.065;
8624
+ return new THREE3.Color().setHSL(
8625
+ THREE3.MathUtils.euclideanModulo(hsl.h + hueOffset, 1),
8626
+ THREE3.MathUtils.clamp(hsl.s * (index === 1 ? 0.92 : 0.82), 0.38, 0.76),
8627
+ THREE3.MathUtils.clamp(hsl.l + (index === 1 ? 0.04 : -0.03), 0.38, 0.68)
8628
+ );
8629
+ }
8630
+ function colorDistance(a, b) {
8631
+ const dr = a.r - b.r;
8632
+ const dg = a.g - b.g;
8633
+ const db = a.b - b.b;
8634
+ return Math.sqrt(dr * dr + dg * dg + db * db);
8635
+ }
8405
8636
  var draco = null;
8406
8637
  var dracoPath = "";
8407
8638
  var gltfLoader = null;
@@ -8476,6 +8707,154 @@ function parseCharacterModel(data, resourcePath = "", options = {}) {
8476
8707
  const loader = getGltfLoader(options.dracoDecoderPath);
8477
8708
  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));
8478
8709
  }
8710
+
8711
+ // three/modelMorphLayout.ts
8712
+ var THREE_RENDERED_MORPH_SEMANTICS = ["position", "normal", "color"];
8713
+ function resolveMorphTargetCount(mesh) {
8714
+ const morphAttributes = mesh.geometry.morphAttributes;
8715
+ const attributeCount = THREE_RENDERED_MORPH_SEMANTICS.reduce(
8716
+ (count, semantic) => Math.max(count, morphAttributes[semantic]?.length ?? 0),
8717
+ 0
8718
+ );
8719
+ const influenceCount = mesh.morphTargetInfluences?.length ?? 0;
8720
+ const dictionaryCount = Object.values(mesh.morphTargetDictionary ?? {}).reduce((count, index) => {
8721
+ if (!Number.isInteger(index) || index < 0) {
8722
+ throw new Error(
8723
+ `Mesh "${mesh.name || mesh.uuid}" has an invalid morph target dictionary index: ${index}.`
8724
+ );
8725
+ }
8726
+ return Math.max(count, index + 1);
8727
+ }, 0);
8728
+ return Math.max(attributeCount, influenceCount, dictionaryCount);
8729
+ }
8730
+ function createNeutralMorphAttribute(mesh, semantic, existingAttributes, targetIndex) {
8731
+ const geometry = mesh.geometry;
8732
+ const baseAttribute = geometry.getAttribute(semantic);
8733
+ const template = baseAttribute ?? existingAttributes.find(Boolean);
8734
+ if (!template) {
8735
+ throw new Error(
8736
+ `Mesh "${mesh.name || mesh.uuid}" cannot repair morph ${semantic}[${targetIndex}] because it has no base or existing attribute to copy.`
8737
+ );
8738
+ }
8739
+ const neutral = template.clone();
8740
+ if (geometry.morphTargetsRelative || !baseAttribute) {
8741
+ for (let vertexIndex = 0; vertexIndex < neutral.count; vertexIndex += 1) {
8742
+ for (let component = 0; component < neutral.itemSize; component += 1) {
8743
+ neutral.setComponent(vertexIndex, component, 0);
8744
+ }
8745
+ }
8746
+ }
8747
+ const targetName = Object.entries(mesh.morphTargetDictionary ?? {}).find(([, index]) => index === targetIndex)?.[0];
8748
+ neutral.name = targetName ?? "";
8749
+ return neutral;
8750
+ }
8751
+ function normalizeRenderableCharacterMorphAttributes(model) {
8752
+ const repairs = [];
8753
+ model.traverse((object) => {
8754
+ const mesh = object;
8755
+ if (!mesh.isMesh || !mesh.geometry) return;
8756
+ const morphAttributes = mesh.geometry.morphAttributes;
8757
+ const removedEmptyChannels = [];
8758
+ for (const semantic of THREE_RENDERED_MORPH_SEMANTICS) {
8759
+ if (semantic !== "position" && morphAttributes[semantic]?.length === 0) {
8760
+ delete morphAttributes[semantic];
8761
+ removedEmptyChannels.push(semantic);
8762
+ }
8763
+ }
8764
+ const targetCount = resolveMorphTargetCount(mesh);
8765
+ if (targetCount === 0) {
8766
+ if (morphAttributes.position?.length === 0) {
8767
+ delete morphAttributes.position;
8768
+ removedEmptyChannels.push("position");
8769
+ }
8770
+ if (removedEmptyChannels.length > 0) {
8771
+ repairs.push({
8772
+ meshName: mesh.name || mesh.uuid,
8773
+ paddedChannels: {},
8774
+ removedEmptyChannels,
8775
+ paddedInfluences: 0
8776
+ });
8777
+ }
8778
+ return;
8779
+ }
8780
+ const positionCount = mesh.geometry.getAttribute("position")?.count;
8781
+ if (!positionCount) {
8782
+ throw new Error(
8783
+ `Mesh "${mesh.name || mesh.uuid}" has morph targets but no base position attribute.`
8784
+ );
8785
+ }
8786
+ const paddedChannels = {};
8787
+ for (const semantic of THREE_RENDERED_MORPH_SEMANTICS) {
8788
+ let attributes = morphAttributes[semantic];
8789
+ if (!attributes) {
8790
+ if (semantic !== "position") continue;
8791
+ attributes = [];
8792
+ morphAttributes.position = attributes;
8793
+ }
8794
+ const baseAttribute = mesh.geometry.getAttribute(semantic);
8795
+ const referenceAttribute = baseAttribute ?? attributes.find(Boolean);
8796
+ const expectedItemSize = referenceAttribute?.itemSize;
8797
+ for (let targetIndex = 0; targetIndex < targetCount; targetIndex += 1) {
8798
+ const attribute = attributes[targetIndex];
8799
+ if (!attribute) {
8800
+ attributes[targetIndex] = createNeutralMorphAttribute(
8801
+ mesh,
8802
+ semantic,
8803
+ attributes,
8804
+ targetIndex
8805
+ );
8806
+ paddedChannels[semantic] = (paddedChannels[semantic] ?? 0) + 1;
8807
+ continue;
8808
+ }
8809
+ if (attribute.count !== positionCount || attribute.itemSize !== expectedItemSize) {
8810
+ throw new Error(
8811
+ `Mesh "${mesh.name || mesh.uuid}" has an invalid morph ${semantic}[${targetIndex}] (count=${attribute.count}, itemSize=${attribute.itemSize}; expected count=${positionCount}, itemSize=${expectedItemSize}).`
8812
+ );
8813
+ }
8814
+ }
8815
+ }
8816
+ const influences = mesh.morphTargetInfluences ?? [];
8817
+ const paddedInfluences = targetCount - influences.length;
8818
+ while (influences.length < targetCount) {
8819
+ influences.push(0);
8820
+ }
8821
+ mesh.morphTargetInfluences = influences;
8822
+ if (removedEmptyChannels.length > 0 || Object.keys(paddedChannels).length > 0 || paddedInfluences > 0) {
8823
+ repairs.push({
8824
+ meshName: mesh.name || mesh.uuid,
8825
+ paddedChannels,
8826
+ removedEmptyChannels,
8827
+ paddedInfluences
8828
+ });
8829
+ }
8830
+ });
8831
+ return repairs;
8832
+ }
8833
+ function assertRenderableCharacterMorphAttributes(model) {
8834
+ model.traverse((object) => {
8835
+ const mesh = object;
8836
+ if (!mesh.isMesh) return;
8837
+ const morphAttributes = mesh.geometry?.morphAttributes;
8838
+ const primary = morphAttributes?.position ?? morphAttributes?.normal ?? morphAttributes?.color;
8839
+ const targetCount = primary?.length ?? 0;
8840
+ if (targetCount === 0) return;
8841
+ const positionCount = mesh.geometry.getAttribute("position")?.count;
8842
+ const invalidSemantics = THREE_RENDERED_MORPH_SEMANTICS.filter((semantic) => {
8843
+ const attributes = morphAttributes[semantic];
8844
+ if (semantic === "position" && !attributes) return true;
8845
+ if (!attributes) return false;
8846
+ const expectedItemSize = mesh.geometry.getAttribute(semantic)?.itemSize ?? attributes.find(Boolean)?.itemSize;
8847
+ return attributes.length !== targetCount || Array.from({ length: targetCount }, (_, index) => attributes[index]).some((attribute) => !attribute || attribute.count !== positionCount || attribute.itemSize !== expectedItemSize);
8848
+ });
8849
+ const influenceCount = mesh.morphTargetInfluences?.length ?? 0;
8850
+ if (invalidSemantics.length > 0 || influenceCount !== targetCount) {
8851
+ const channelCounts = THREE_RENDERED_MORPH_SEMANTICS.map((semantic) => `${semantic}=${morphAttributes[semantic]?.length ?? 0}`).join(", ");
8852
+ throw new Error(
8853
+ `Mesh "${mesh.name || mesh.uuid}" has an invalid Three.js morph layout (${channelCounts}, influences=${influenceCount}, expected=${targetCount}).`
8854
+ );
8855
+ }
8856
+ });
8857
+ }
8479
8858
  var DEFAULT_CHARACTER_LIGHTING_PRESETS = {
8480
8859
  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 } },
8481
8860
  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 } },
@@ -8709,6 +9088,7 @@ function constructDefaultCharacterScene(container, options, renderer, disposeRen
8709
9088
  lifetime.add(disposeRenderer);
8710
9089
  try {
8711
9090
  const sceneType = CHARACTER_SCENE_TYPES[options.type] ?? CHARACTER_SCENE_TYPES.studio;
9091
+ let currentSceneType = sceneType.id;
8712
9092
  const width = Math.max(1, container.clientWidth || globalThis.innerWidth || 1);
8713
9093
  const height = Math.max(1, container.clientHeight || globalThis.innerHeight || 1);
8714
9094
  const ratio = () => Math.min(globalThis.devicePixelRatio || 1, options.pixelRatioCap ?? 1.5);
@@ -8724,12 +9104,32 @@ function constructDefaultCharacterScene(container, options, renderer, disposeRen
8724
9104
  const preset = DEFAULT_CHARACTER_LIGHTING_PRESETS[options.lightingPreset ?? sceneType.lightingPreset]?.settings ?? DEFAULT_CHARACTER_LIGHTING_SETTINGS;
8725
9105
  const lighting = createLighting(scene, { ...preset, ...options.lighting });
8726
9106
  lifetime.add(() => lighting.dispose());
8727
- const shadowPlane = options.shadowPlane ?? sceneType.shadowPlane ? createShadowPlane(scene, { opacity: lighting.getSettings().shadowOpacity }) : null;
8728
- if (shadowPlane) {
8729
- lifetime.add(() => shadowPlane.material.dispose());
8730
- lifetime.add(() => shadowPlane.geometry.dispose());
8731
- lifetime.add(() => scene.remove(shadowPlane));
8732
- }
9107
+ let shadowPlane = null;
9108
+ const ensureShadowPlane = () => {
9109
+ if (shadowPlane) return shadowPlane;
9110
+ const plane = createShadowPlane(scene, { opacity: lighting.getSettings().shadowOpacity });
9111
+ lifetime.add(() => plane.material.dispose());
9112
+ lifetime.add(() => plane.geometry.dispose());
9113
+ lifetime.add(() => scene.remove(plane));
9114
+ shadowPlane = plane;
9115
+ return plane;
9116
+ };
9117
+ if (options.shadowPlane ?? sceneType.shadowPlane) ensureShadowPlane();
9118
+ const setSceneSettings = (value) => {
9119
+ if (lifetime.isDisposed()) return;
9120
+ const type = CHARACTER_SCENE_TYPES[value.type ?? DEFAULT_CHARACTER_SCENE_TYPE_ID] ?? CHARACTER_SCENE_TYPES[DEFAULT_CHARACTER_SCENE_TYPE_ID];
9121
+ currentSceneType = type.id;
9122
+ const background2 = value.background === void 0 ? type.background : value.background;
9123
+ scene.background = background2 == null ? null : new Color(background2);
9124
+ camera.fov = value.cameraFov ?? 45;
9125
+ camera.updateProjectionMatrix();
9126
+ const preset2 = DEFAULT_CHARACTER_LIGHTING_PRESETS[value.lightingPreset ?? type.lightingPreset] ?? DEFAULT_CHARACTER_LIGHTING_PRESETS[DEFAULT_CHARACTER_LIGHTING_PRESET_ID];
9127
+ lighting.setSettings({ ...preset2.settings, ...value.lighting });
9128
+ renderer.shadowMap.enabled = value.shadows ?? true;
9129
+ const showPlane = value.shadowPlane ?? type.shadowPlane;
9130
+ if (showPlane) ensureShadowPlane();
9131
+ if (shadowPlane) shadowPlane.visible = showPlane;
9132
+ };
8733
9133
  const resize = () => {
8734
9134
  if (lifetime.isDisposed()) return;
8735
9135
  const w = Math.max(1, container.clientWidth || globalThis.innerWidth || 1);
@@ -8739,7 +9139,11 @@ function constructDefaultCharacterScene(container, options, renderer, disposeRen
8739
9139
  renderer.setPixelRatio(ratio());
8740
9140
  renderer.setSize(w, h, false);
8741
9141
  };
8742
- const handle = { container, scene, renderer, camera, lighting, shadowPlane, sceneType: sceneType.id, ownsScene: true, resize, dispose: lifetime.dispose };
9142
+ const handle = { container, scene, renderer, camera, lighting, get shadowPlane() {
9143
+ return shadowPlane;
9144
+ }, get sceneType() {
9145
+ return currentSceneType;
9146
+ }, ownsScene: true, resize, setSceneSettings, dispose: lifetime.dispose };
8743
9147
  const attach = () => {
8744
9148
  lifetime.add(() => {
8745
9149
  if (renderer.domElement.parentElement === container) container.removeChild(renderer.domElement);
@@ -8872,6 +9276,7 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
8872
9276
  const lifetime = createDisposer();
8873
9277
  try {
8874
9278
  const sceneType = CHARACTER_SCENE_TYPES[options.type] ?? CHARACTER_SCENE_TYPES.studio;
9279
+ let currentSceneType = sceneType.id;
8875
9280
  const scene = new Scene();
8876
9281
  const background = options.background === void 0 ? sceneType.background : options.background;
8877
9282
  scene.background = background == null ? null : new Color(background);
@@ -8879,12 +9284,18 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
8879
9284
  const preset = DEFAULT_CHARACTER_LIGHTING_PRESETS[options.lightingPreset ?? sceneType.lightingPreset]?.settings ?? DEFAULT_CHARACTER_LIGHTING_SETTINGS;
8880
9285
  const lighting = createMutableCharacterLighting(scene, { ...preset, ...options.lighting });
8881
9286
  lifetime.add(lighting.controller.dispose);
8882
- const shadowPlane = options.shadowPlane ?? sceneType.shadowPlane ? createShadowPlane(scene, { opacity: lighting.controller.getSettings().shadowOpacity }) : null;
8883
- if (shadowPlane) {
8884
- lifetime.add(() => shadowPlane.material.dispose());
8885
- lifetime.add(() => shadowPlane.geometry.dispose());
8886
- lifetime.add(() => scene.remove(shadowPlane));
8887
- }
9287
+ let shadowsEnabled = options.shadows ?? true;
9288
+ let shadowPlane = null;
9289
+ const ensureShadowPlane = () => {
9290
+ if (shadowPlane) return shadowPlane;
9291
+ const plane = createShadowPlane(scene, { opacity: lighting.controller.getSettings().shadowOpacity });
9292
+ lifetime.add(() => plane.material.dispose());
9293
+ lifetime.add(() => plane.geometry.dispose());
9294
+ lifetime.add(() => scene.remove(plane));
9295
+ shadowPlane = plane;
9296
+ return plane;
9297
+ };
9298
+ if (options.shadowPlane ?? sceneType.shadowPlane) ensureShadowPlane();
8888
9299
  let settings = { preference: rendererPreference(options.rendering?.preference ?? options.renderer ?? "auto") };
8889
9300
  let active = null;
8890
9301
  let candidateInUse = null;
@@ -8989,7 +9400,7 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
8989
9400
  candidateInUse = candidate;
8990
9401
  const renderer = candidate.renderer;
8991
9402
  sizeRenderer(renderer, true);
8992
- renderer.shadowMap.enabled = options.shadows ?? true;
9403
+ renderer.shadowMap.enabled = shadowsEnabled;
8993
9404
  renderer.shadowMap.type = candidate.backend === "webgl" ? PCFShadowMap : PCFSoftShadowMap;
8994
9405
  Object.assign(renderer.domElement.style, { display: "block", width: "100%", height: "100%" });
8995
9406
  if (leaseCount) await leaseBarrier;
@@ -9196,14 +9607,34 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
9196
9607
  else cleanup();
9197
9608
  return disposal;
9198
9609
  };
9610
+ const setSceneSettings = (value) => {
9611
+ if (disposed) return;
9612
+ shadowsEnabled = value.shadows ?? true;
9613
+ const type = CHARACTER_SCENE_TYPES[value.type ?? DEFAULT_CHARACTER_SCENE_TYPE_ID] ?? CHARACTER_SCENE_TYPES[DEFAULT_CHARACTER_SCENE_TYPE_ID];
9614
+ currentSceneType = type.id;
9615
+ const background2 = value.background === void 0 ? type.background : value.background;
9616
+ scene.background = background2 == null ? null : new Color(background2);
9617
+ camera.fov = value.cameraFov ?? 45;
9618
+ camera.updateProjectionMatrix();
9619
+ const preset2 = DEFAULT_CHARACTER_LIGHTING_PRESETS[value.lightingPreset ?? type.lightingPreset] ?? DEFAULT_CHARACTER_LIGHTING_PRESETS[DEFAULT_CHARACTER_LIGHTING_PRESET_ID];
9620
+ lighting.controller.setSettings({ ...preset2.settings, ...value.lighting });
9621
+ if (active) active.renderer.shadowMap.enabled = value.shadows ?? true;
9622
+ const showPlane = value.shadowPlane ?? type.shadowPlane;
9623
+ if (showPlane) ensureShadowPlane();
9624
+ if (shadowPlane) shadowPlane.visible = showPlane;
9625
+ };
9199
9626
  const ready = setSettings(settings);
9200
9627
  return {
9201
9628
  container,
9202
9629
  scene,
9203
9630
  camera,
9204
9631
  lighting: lighting.controller,
9205
- shadowPlane,
9206
- sceneType: sceneType.id,
9632
+ get shadowPlane() {
9633
+ return shadowPlane;
9634
+ },
9635
+ get sceneType() {
9636
+ return currentSceneType;
9637
+ },
9207
9638
  ownsScene: true,
9208
9639
  rendering,
9209
9640
  ready,
@@ -9214,6 +9645,7 @@ function createDefaultCharacterSceneRuntime(container, options = {}) {
9214
9645
  return active?.backend ?? null;
9215
9646
  },
9216
9647
  resize,
9648
+ setSceneSettings,
9217
9649
  dispose
9218
9650
  };
9219
9651
  } catch (failure) {
@@ -9229,7 +9661,9 @@ async function createDefaultCharacterSceneAsync(container, options = {}) {
9229
9661
  const handle = snapshot2.backend === "webgl" ? { ...runtime, renderer: snapshot2.renderer, backend: "webgl" } : { ...runtime, renderer: snapshot2.renderer, backend: "webgpu" };
9230
9662
  Object.defineProperties(handle, {
9231
9663
  renderer: { get: () => runtime.renderer },
9232
- backend: { get: () => runtime.backend }
9664
+ backend: { get: () => runtime.backend },
9665
+ shadowPlane: { get: () => runtime.shadowPlane },
9666
+ sceneType: { get: () => runtime.sceneType }
9233
9667
  });
9234
9668
  return handle;
9235
9669
  } catch (failure) {
@@ -9249,24 +9683,94 @@ async function createCharacterHost(options) {
9249
9683
  let state;
9250
9684
  let engine;
9251
9685
  let controller;
9686
+ let host;
9252
9687
  let disposed = false;
9253
9688
  let disposal;
9254
9689
  let switching = Promise.resolve();
9690
+ let activeChange;
9691
+ let request3 = 0;
9255
9692
  let mountedCanvas;
9256
- const aborted = () => new DOMException("Character host creation was aborted", "AbortError");
9257
- const checkCreation = () => {
9258
- if (options.signal?.aborted) throw aborted();
9693
+ const previousBackground = options.container.style?.background ?? "";
9694
+ let backgroundRevision = 0;
9695
+ const applyCharacterBackground = (value, character) => {
9696
+ const revision = ++backgroundRevision;
9697
+ const settings = sceneSettings(character);
9698
+ const usePalette = !options.external && settings.background === void 0 && (settings.type === void 0 || settings.type === "studio");
9699
+ if (options.container.style) options.container.style.background = previousBackground;
9700
+ if (!usePalette) return;
9701
+ const background = createSceneInkBackground((css) => {
9702
+ if (!disposed && revision === backgroundRevision && options.container.style) options.container.style.background = css;
9703
+ });
9704
+ const key = character.modelUrl;
9705
+ const materialPalette = resolveCharacterMaterialPalette(value.model, key);
9706
+ background.setPalette(materialPalette);
9707
+ void resolveCharacterTexturePalette(value.model, key).then((palette) => {
9708
+ background.setPalette(palette ?? materialPalette);
9709
+ background.dispose();
9710
+ });
9711
+ };
9712
+ const visibility = /* @__PURE__ */ new WeakMap();
9713
+ const lifetime = new AbortController();
9714
+ const initial = new AbortController();
9715
+ activeChange = initial;
9716
+ const aborted = () => new DOMException("Character host request was cancelled", "AbortError");
9717
+ const cancel = () => {
9718
+ lifetime.abort();
9719
+ activeChange?.abort();
9720
+ };
9721
+ const cancelCreation = () => initial.abort();
9722
+ options.signal?.addEventListener("abort", cancelCreation, { once: true });
9723
+ if (options.signal?.aborted) cancelCreation();
9724
+ const check = (signal) => {
9725
+ if (disposed || lifetime.signal.aborted || signal.aborted) throw aborted();
9259
9726
  };
9260
9727
  const releaseModel = async (value) => {
9261
9728
  const release = () => disposeCharacterModel(state?.scene ?? null, value.model);
9262
9729
  if (state?.rendering) await state.rendering.releaseSceneResources(release);
9263
9730
  else release();
9264
9731
  };
9732
+ const acquireModel = (character, loader, signal) => (options.loadModel ?? loadCharacterModel)(character.modelUrl, { ...loader, signal });
9733
+ const sceneSettings = (character) => {
9734
+ const { background, ...value } = { ...options.scene, ...character.scene };
9735
+ return { ...value, ...background === void 0 ? {} : {
9736
+ background: background && typeof background === "object" ? background.getHex() : background
9737
+ } };
9738
+ };
9739
+ const acquireRenderer = async (signal) => {
9740
+ const rendering = state?.rendering;
9741
+ if (!rendering) return void 0;
9742
+ try {
9743
+ await rendering.setSettings(rendering.getSettings());
9744
+ } catch (error) {
9745
+ if (!rendering.getSnapshot().renderer) throw error;
9746
+ }
9747
+ check(signal);
9748
+ return rendering.acquireRenderer();
9749
+ };
9265
9750
  const makeEngine = (value, character) => {
9266
- const next = new Embody({ ...options.embody, presetType: character.presetType ?? options.embody?.presetType, profile: character.profile ?? options.embody?.profile });
9751
+ let profile = character.profile ?? options.embody?.profile;
9752
+ let annotationConfig = character.annotationConfig;
9753
+ if (profile) {
9754
+ const bones = [];
9755
+ value.model.traverse((object) => {
9756
+ if (object.isBone && object.name) bones.push({ name: object.name });
9757
+ });
9758
+ const validation = validateMappings(value.meshes, { bones }, profile, { suggestCorrections: true, useResolvedNames: true, minConfidence: 0.8 });
9759
+ if (validation.suggestedConfig && validation.corrections?.some((correction) => correction.applied)) {
9760
+ profile = { ...profile, ...validation.suggestedConfig };
9761
+ if (annotationConfig) annotationConfig = { ...annotationConfig, ...validation.suggestedConfig };
9762
+ }
9763
+ }
9764
+ const resolvedCharacter = { ...character, profile, annotationConfig };
9765
+ const next = new Embody({ ...options.embody, presetType: character.presetType ?? options.embody?.presetType, profile });
9267
9766
  try {
9268
- next.onReady(value);
9269
- return next;
9767
+ next.onReady({
9768
+ model: value.model,
9769
+ meshes: value.meshes,
9770
+ ...options.registerEmbeddedAnimations === false ? {} : { animations: value.animations }
9771
+ });
9772
+ next.stop();
9773
+ return { next, character: resolvedCharacter };
9270
9774
  } catch (error) {
9271
9775
  try {
9272
9776
  next.dispose();
@@ -9278,6 +9782,10 @@ async function createCharacterHost(options) {
9278
9782
  const dispose = () => {
9279
9783
  if (disposal) return disposal;
9280
9784
  disposed = true;
9785
+ backgroundRevision += 1;
9786
+ if (options.container.style) options.container.style.background = previousBackground;
9787
+ cancel();
9788
+ options.signal?.removeEventListener("abort", cancelCreation);
9281
9789
  disposal = (async () => {
9282
9790
  let failure;
9283
9791
  const attempt = async (release) => {
@@ -9293,24 +9801,57 @@ async function createCharacterHost(options) {
9293
9801
  await attempt(() => engine?.dispose());
9294
9802
  const ownedScene = !options.external && state && "dispose" in state ? state : void 0;
9295
9803
  if (ownedScene) await attempt(() => ownedScene.dispose());
9296
- const lastModel = loaded;
9297
- if (lastModel) await attempt(() => releaseModel(lastModel));
9804
+ if (loaded) await attempt(() => releaseModel(loaded));
9298
9805
  mountedCanvas?.remove();
9299
9806
  if (failure !== void 0) throw failure;
9300
9807
  })();
9301
9808
  return disposal;
9302
9809
  };
9810
+ const prepare2 = async (value, next, character, signal) => {
9811
+ check(signal);
9812
+ state?.setSceneSettings?.(sceneSettings(character));
9813
+ visibility.set(value.model, value.model.visible);
9814
+ value.model.visible = false;
9815
+ value.model.userData.embodyCharacterModel = true;
9816
+ await options.prepareCharacter?.({ ...value, engine: next, character, signal });
9817
+ check(signal);
9818
+ normalizeRenderableCharacterMorphAttributes(value.model);
9819
+ assertRenderableCharacterMorphAttributes(value.model);
9820
+ if (options.prepareCharacter) next.bindModel();
9821
+ value.model.updateMatrixWorld(true);
9822
+ state.scene.add(value.model);
9823
+ if (controller) await controller.prepareModelForRender(value.model, { signal });
9824
+ check(signal);
9825
+ };
9826
+ const frameCharacter = async (value, character) => {
9827
+ if (!controller) return;
9828
+ controller.setModel(value.model);
9829
+ if (character.annotationConfig) controller.prepareRegionsForReveal(character.annotationConfig);
9830
+ else if (options.autoFrame !== false) await controller.focusFullBody(0);
9831
+ };
9832
+ const ready = async (value, next, character, signal) => {
9833
+ await frameCharacter(value, character);
9834
+ check(signal);
9835
+ await options.onCharacterReady?.({
9836
+ ...value,
9837
+ engine: next,
9838
+ character,
9839
+ signal,
9840
+ scene: state.scene,
9841
+ camera: state.camera,
9842
+ cameraController: controller ?? null,
9843
+ rendering: state.rendering ?? null
9844
+ });
9845
+ check(signal);
9846
+ };
9303
9847
  try {
9304
- checkCreation();
9305
- loaded = await loadCharacterModel(options.character.modelUrl, options.loader);
9306
- checkCreation();
9307
- state = options.external ?? await createDefaultCharacterSceneAsync(options.container, { ...options.scene, signal: options.signal });
9308
- checkCreation();
9848
+ check(initial.signal);
9849
+ loaded = await acquireModel(options.character, options.loader, initial.signal);
9850
+ check(initial.signal);
9851
+ state = options.external ?? await createDefaultCharacterSceneAsync(options.container, { ...options.scene, ...options.character.scene, signal: initial.signal });
9852
+ check(initial.signal);
9309
9853
  await initEmbodyCore();
9310
- checkCreation();
9311
- applyCharacterModelTransform(loaded.model, options.character);
9312
- state.scene.add(loaded.model);
9313
- engine = makeEngine(loaded, options.character);
9854
+ check(initial.signal);
9314
9855
  if (options.external?.mountCanvas && !state.renderer.domElement.parentElement) {
9315
9856
  mountedCanvas = state.renderer.domElement;
9316
9857
  options.container.appendChild(mountedCanvas);
@@ -9318,25 +9859,36 @@ async function createCharacterHost(options) {
9318
9859
  if (options.renderLoop ?? !options.external) {
9319
9860
  if (!(state.camera instanceof PerspectiveCamera)) throw new Error("Character host rendering requires a PerspectiveCamera");
9320
9861
  controller = new DPthreeCameraController({
9862
+ ...options.camera,
9321
9863
  scene: state.scene,
9322
9864
  camera: state.camera,
9323
9865
  domElement: state.renderer.domElement,
9324
9866
  ...state.rendering ? { rendering: state.rendering } : { renderer: state.renderer },
9325
- showDOMControls: false
9867
+ showDOMControls: options.camera?.showDOMControls ?? false
9326
9868
  });
9327
- controller.setModel(loaded.model);
9328
- await controller.prepareModelForRender(loaded.model, { signal: options.signal });
9329
- if (options.autoFrame !== false) await controller.focusFullBody(0);
9330
9869
  }
9331
- checkCreation();
9332
- if (options.autoStart === false) engine.stop();
9333
- else engine.start();
9870
+ applyCharacterModelTransform(loaded.model, options.character);
9871
+ loaded.model.updateMatrixWorld(true);
9872
+ const first = makeEngine(loaded, options.character);
9873
+ engine = first.next;
9874
+ let currentCharacter = first.character;
9875
+ const initialLease = await acquireRenderer(initial.signal);
9876
+ try {
9877
+ await prepare2(loaded, engine, first.character, initial.signal);
9878
+ await ready(loaded, engine, first.character, initial.signal);
9879
+ loaded.model.visible = visibility.get(loaded.model) ?? true;
9880
+ applyCharacterBackground(loaded, first.character);
9881
+ } finally {
9882
+ initialLease?.release();
9883
+ }
9884
+ if (options.autoStart !== false) engine.start();
9334
9885
  const sceneState = state;
9335
9886
  const rendering = state.rendering ?? null;
9336
- const host = {
9887
+ host = {
9337
9888
  engine,
9338
9889
  scene: state.scene,
9339
9890
  camera: state.camera,
9891
+ cameraController: controller ?? null,
9340
9892
  get renderer() {
9341
9893
  return rendering ? rendering.getSnapshot().renderer : sceneState.renderer;
9342
9894
  },
@@ -9347,68 +9899,92 @@ async function createCharacterHost(options) {
9347
9899
  model: loaded.model,
9348
9900
  meshes: loaded.meshes,
9349
9901
  animations: loaded.animations,
9902
+ gltf: loaded.gltf,
9350
9903
  lighting: "lighting" in state ? state.lighting : null,
9351
9904
  ownsScene: !options.external,
9352
9905
  container: options.container,
9353
9906
  resize() {
9354
9907
  if ("resize" in sceneState) sceneState.resize();
9355
9908
  },
9356
- setCharacter(character, loader = options.loader) {
9357
- if (disposed) return Promise.reject(new Error("Character host is disposed"));
9909
+ setCharacter(character, loader = options.loader, signal) {
9910
+ if (disposed || lifetime.signal.aborted) return Promise.reject(aborted());
9911
+ if (!character.modelUrl) return Promise.reject(new Error("setCharacter requires character.modelUrl"));
9912
+ activeChange?.abort();
9913
+ const operation = new AbortController();
9914
+ const abort = () => operation.abort();
9915
+ signal?.addEventListener("abort", abort, { once: true });
9916
+ if (signal?.aborted) abort();
9917
+ activeChange = operation;
9918
+ const generation = ++request3;
9358
9919
  const change = switching.catch(() => {
9359
9920
  }).then(async () => {
9360
- if (disposed) throw new Error("Character host is disposed");
9361
- const next = await loadCharacterModel(character.modelUrl, loader);
9921
+ check(operation.signal);
9922
+ if (generation !== request3) throw aborted();
9923
+ const next = await acquireModel(character, loader, operation.signal);
9362
9924
  let nextEngine;
9925
+ let lease = void 0;
9926
+ let cameraChanged = false;
9927
+ let previous;
9363
9928
  try {
9364
- if (disposed) throw new Error("Character host is disposed");
9929
+ check(operation.signal);
9930
+ lease = await acquireRenderer(operation.signal);
9365
9931
  applyCharacterModelTransform(next.model, character);
9366
- nextEngine = makeEngine(next, character);
9367
- nextEngine.stop();
9368
- sceneState.scene.add(next.model);
9369
- if (controller) await controller.prepareModelForRender(next.model);
9370
- if (disposed) throw new Error("Character host is disposed");
9371
- } catch (error) {
9372
- try {
9373
- nextEngine?.dispose();
9374
- } catch {
9375
- }
9376
- try {
9377
- await releaseModel(next);
9378
- } catch {
9379
- }
9380
- throw error;
9381
- }
9382
- const previous = loaded;
9383
- try {
9932
+ next.model.updateMatrixWorld(true);
9933
+ const prepared = makeEngine(next, character);
9934
+ nextEngine = prepared.next;
9935
+ await prepare2(next, nextEngine, prepared.character, operation.signal);
9936
+ cameraChanged = true;
9937
+ await ready(next, nextEngine, prepared.character, operation.signal);
9938
+ check(operation.signal);
9939
+ previous = loaded;
9384
9940
  engine.dispose();
9941
+ engine = nextEngine;
9942
+ loaded = next;
9943
+ currentCharacter = prepared.character;
9944
+ applyCharacterBackground(next, currentCharacter);
9945
+ host.engine = engine;
9946
+ host.model = next.model;
9947
+ host.meshes = next.meshes;
9948
+ host.animations = next.animations;
9949
+ host.gltf = next.gltf;
9950
+ previous.model.visible = false;
9951
+ next.model.visible = visibility.get(next.model) ?? true;
9952
+ if (options.autoStart !== false) engine.start();
9385
9953
  } catch (error) {
9386
- try {
9387
- nextEngine?.dispose();
9388
- } catch {
9389
- }
9390
- try {
9391
- await releaseModel(next);
9392
- } catch {
9954
+ if (loaded !== next) {
9955
+ try {
9956
+ state?.setSceneSettings?.(sceneSettings(currentCharacter));
9957
+ } catch {
9958
+ }
9959
+ if (cameraChanged && !disposed && controller && loaded) {
9960
+ try {
9961
+ await frameCharacter(loaded, currentCharacter);
9962
+ } catch {
9963
+ }
9964
+ }
9965
+ try {
9966
+ nextEngine?.dispose();
9967
+ } catch {
9968
+ }
9969
+ lease?.release();
9970
+ lease = void 0;
9971
+ try {
9972
+ await releaseModel(next);
9973
+ } catch {
9974
+ }
9393
9975
  }
9394
9976
  throw error;
9977
+ } finally {
9978
+ lease?.release();
9395
9979
  }
9396
- engine = nextEngine;
9397
- loaded = next;
9398
- host.engine = engine;
9399
- host.model = next.model;
9400
- host.meshes = next.meshes;
9401
- host.animations = next.animations;
9402
- controller?.setModel(next.model);
9403
- if (options.autoStart !== false) engine.start();
9404
- await releaseModel(previous);
9405
- if (controller && options.autoFrame !== false) await controller.focusFullBody(0);
9980
+ if (previous) await releaseModel(previous);
9406
9981
  });
9407
- switching = change;
9408
- return change;
9982
+ switching = change.finally(() => signal?.removeEventListener("abort", abort));
9983
+ return switching;
9409
9984
  },
9410
9985
  dispose
9411
9986
  };
9987
+ options.signal?.removeEventListener("abort", cancelCreation);
9412
9988
  return host;
9413
9989
  } catch (error) {
9414
9990
  try {
@@ -9418,6 +9994,1072 @@ async function createCharacterHost(options) {
9418
9994
  throw error;
9419
9995
  }
9420
9996
  }
9997
+ var COLORS = {
9998
+ mapped: 4889258,
9999
+ unmapped: 5925498,
10000
+ highlighted: 13934656,
10001
+ selected: 6534125
10002
+ };
10003
+ var OPACITY = {
10004
+ mapped: 0.58,
10005
+ unmapped: 0.38,
10006
+ highlighted: 0.7,
10007
+ selected: 0.8,
10008
+ context: 0.46,
10009
+ muted: 0.16
10010
+ };
10011
+ var AXIS_GIZMO_COLORS = {
10012
+ pitch: 16743004,
10013
+ yaw: 6410389,
10014
+ roll: 6924543
10015
+ };
10016
+ var AXIS_GIZMO_IDLE_OPACITY = 0.72;
10017
+ var AXIS_GIZMO_MUTED_OPACITY = 0.26;
10018
+ var AXIS_GIZMO_ACTIVE_OPACITY = 0.96;
10019
+ var AXIS_GIZMO_RADIUS_FACTOR = 0.48;
10020
+ var AXIS_GIZMO_MIN_RADIUS = 0.025;
10021
+ var AXIS_GIZMO_MAX_RADIUS = 0.14;
10022
+ var AXIS_GIZMO_SCREEN_RADIUS_PX = 54;
10023
+ var AXIS_GIZMO_ACTIVE_SCALE = 1.05;
10024
+ var AXIS_GIZMO_HIT_TUBE = 0.15;
10025
+ var VISIBILITY_FADE_MS = 180;
10026
+ var VISIBILITY_EPSILON = 0.01;
10027
+ var LEAF_BONE_LENGTH = 0.02;
10028
+ var WIDTH_RATIO = 0.1;
10029
+ var MID_POSITION = 0.2;
10030
+ var ROOT_HELPER_NAME_PATTERN = /armature|bone.?root|rootjoint|(?:^|[_\W])root(?:$|[_\W])/i;
10031
+ var HIP_NAME_PATTERN = /(?:^|[_\W])hip(?:$|[_\W]|\d)/i;
10032
+ var PELVIS_NAME_PATTERN = /(?:^|[_\W])pelvis(?:$|[_\W]|\d)/i;
10033
+ var POSITION_EPSILON = 1e-4;
10034
+ var DEFAULT_AXIS_CHANNELS = {
10035
+ pitch: "rx",
10036
+ yaw: "ry",
10037
+ roll: "rz"
10038
+ };
10039
+ function createOctahedralBoneGeometry() {
10040
+ const w = WIDTH_RATIO;
10041
+ const positions = new Float32Array([
10042
+ // Face 0: root → mid1 → mid2
10043
+ 0,
10044
+ 0,
10045
+ 0,
10046
+ w,
10047
+ w,
10048
+ MID_POSITION,
10049
+ -w,
10050
+ w,
10051
+ MID_POSITION,
10052
+ // Face 1: root → mid2 → mid3
10053
+ 0,
10054
+ 0,
10055
+ 0,
10056
+ -w,
10057
+ w,
10058
+ MID_POSITION,
10059
+ -w,
10060
+ -w,
10061
+ MID_POSITION,
10062
+ // Face 2: root → mid3 → mid4
10063
+ 0,
10064
+ 0,
10065
+ 0,
10066
+ -w,
10067
+ -w,
10068
+ MID_POSITION,
10069
+ w,
10070
+ -w,
10071
+ MID_POSITION,
10072
+ // Face 3: root → mid4 → mid1
10073
+ 0,
10074
+ 0,
10075
+ 0,
10076
+ w,
10077
+ -w,
10078
+ MID_POSITION,
10079
+ w,
10080
+ w,
10081
+ MID_POSITION,
10082
+ // Face 4: tip → mid2 → mid1
10083
+ 0,
10084
+ 0,
10085
+ 1,
10086
+ -w,
10087
+ w,
10088
+ MID_POSITION,
10089
+ w,
10090
+ w,
10091
+ MID_POSITION,
10092
+ // Face 5: tip → mid3 → mid2
10093
+ 0,
10094
+ 0,
10095
+ 1,
10096
+ -w,
10097
+ -w,
10098
+ MID_POSITION,
10099
+ -w,
10100
+ w,
10101
+ MID_POSITION,
10102
+ // Face 6: tip → mid4 → mid3
10103
+ 0,
10104
+ 0,
10105
+ 1,
10106
+ w,
10107
+ -w,
10108
+ MID_POSITION,
10109
+ -w,
10110
+ -w,
10111
+ MID_POSITION,
10112
+ // Face 7: tip → mid1 → mid4
10113
+ 0,
10114
+ 0,
10115
+ 1,
10116
+ w,
10117
+ w,
10118
+ MID_POSITION,
10119
+ w,
10120
+ -w,
10121
+ MID_POSITION
10122
+ ]);
10123
+ const geometry = new THREE3.BufferGeometry();
10124
+ geometry.setAttribute("position", new THREE3.BufferAttribute(positions, 3));
10125
+ geometry.computeVertexNormals();
10126
+ return geometry;
10127
+ }
10128
+ function collectBones(root) {
10129
+ const bones = /* @__PURE__ */ new Set();
10130
+ root.traverse((obj) => {
10131
+ if (obj.isBone) {
10132
+ bones.add(obj);
10133
+ }
10134
+ if (obj.isSkinnedMesh) {
10135
+ const skinned = obj;
10136
+ skinned.skeleton?.bones?.forEach((bone) => bones.add(bone));
10137
+ }
10138
+ });
10139
+ return Array.from(bones);
10140
+ }
10141
+ function isBone2(obj) {
10142
+ return !!obj && obj.isBone;
10143
+ }
10144
+ function countBoneSubtree(bone) {
10145
+ let count = 1;
10146
+ for (const child of bone.children) {
10147
+ if (isBone2(child)) {
10148
+ count += countBoneSubtree(child);
10149
+ }
10150
+ }
10151
+ return count;
10152
+ }
10153
+ function resolveTopLevelHelperRoot(root) {
10154
+ const topLevelBones = collectBones(root).filter((bone) => !isBone2(bone.parent));
10155
+ if (topLevelBones.length !== 1) return null;
10156
+ let helperRoot = topLevelBones[0];
10157
+ while (ROOT_HELPER_NAME_PATTERN.test(helperRoot.name)) {
10158
+ const boneChildren = helperRoot.children.filter(isBone2);
10159
+ if (boneChildren.length !== 1) break;
10160
+ const onlyChild = boneChildren[0];
10161
+ if (!ROOT_HELPER_NAME_PATTERN.test(onlyChild.name)) break;
10162
+ helperRoot = onlyChild;
10163
+ }
10164
+ return ROOT_HELPER_NAME_PATTERN.test(helperRoot.name) ? helperRoot : null;
10165
+ }
10166
+ function resolveBodyDisplayRoot(root) {
10167
+ const helperRoot = resolveTopLevelHelperRoot(root);
10168
+ if (!helperRoot) return null;
10169
+ const boneChildren = helperRoot.children.filter(isBone2);
10170
+ if (boneChildren.length === 0) return null;
10171
+ if (boneChildren.length === 1) return boneChildren[0];
10172
+ const subtreeSizes = boneChildren.map((child) => ({ child, size: countBoneSubtree(child) })).sort((left, right) => right.size - left.size);
10173
+ const [largest, secondLargest] = subtreeSizes;
10174
+ if (!largest || largest.size < 3) return null;
10175
+ if (!secondLargest || secondLargest.size <= 2 || largest.size >= secondLargest.size * 3) {
10176
+ return largest.child;
10177
+ }
10178
+ return null;
10179
+ }
10180
+ function collectBoneChildren(bone) {
10181
+ return bone.children.filter(isBone2);
10182
+ }
10183
+ function hasZeroLengthConnection(parentBone, childBone) {
10184
+ parentBone.getWorldPosition(_parentPos);
10185
+ childBone.getWorldPosition(_childPos);
10186
+ return _parentPos.distanceToSquared(_childPos) <= POSITION_EPSILON * POSITION_EPSILON;
10187
+ }
10188
+ function hasRootHelperAncestor(bone) {
10189
+ let current = bone.parent;
10190
+ while (isBone2(current)) {
10191
+ if (ROOT_HELPER_NAME_PATTERN.test(current.name)) return true;
10192
+ current = current.parent;
10193
+ }
10194
+ return false;
10195
+ }
10196
+ function shouldSkipRootHelperConnection(parentBone, boneChildren) {
10197
+ if (boneChildren.length !== 1) return false;
10198
+ const childBone = boneChildren[0];
10199
+ if (ROOT_HELPER_NAME_PATTERN.test(parentBone.name)) {
10200
+ if (!isBone2(parentBone.parent)) return true;
10201
+ return collectBoneChildren(childBone).some((grandChild) => hasZeroLengthConnection(childBone, grandChild));
10202
+ }
10203
+ return hasRootHelperAncestor(parentBone) && HIP_NAME_PATTERN.test(parentBone.name) && PELVIS_NAME_PATTERN.test(childBone.name) && hasZeroLengthConnection(parentBone, childBone);
10204
+ }
10205
+ function setRayFromPointer(clientX, clientY, camera, domElement) {
10206
+ const rect = domElement.getBoundingClientRect();
10207
+ if (rect.width <= 0 || rect.height <= 0) return false;
10208
+ _mouse.x = (clientX - rect.left) / rect.width * 2 - 1;
10209
+ _mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1;
10210
+ _raycaster.setFromCamera(_mouse, camera);
10211
+ return true;
10212
+ }
10213
+ var _parentPos = new THREE3.Vector3();
10214
+ var _childPos = new THREE3.Vector3();
10215
+ var _direction = new THREE3.Vector3();
10216
+ var _zAxis = new THREE3.Vector3(0, 0, 1);
10217
+ var _quaternion = new THREE3.Quaternion();
10218
+ var _bonePos = new THREE3.Vector3();
10219
+ var _boneQuat = new THREE3.Quaternion();
10220
+ var _boneEuler = new THREE3.Euler();
10221
+ var _mouse = new THREE3.Vector2();
10222
+ var _raycaster = new THREE3.Raycaster();
10223
+ var _cameraSpacePosition = new THREE3.Vector3();
10224
+ var _planeNormal = new THREE3.Vector3();
10225
+ var _dragPlane = new THREE3.Plane();
10226
+ var _planeHit = new THREE3.Vector3();
10227
+ function resolveSkeletonOverlayStartBoneName(root) {
10228
+ return resolveBodyDisplayRoot(root)?.name ?? null;
10229
+ }
10230
+ var SkeletonOverlay = class {
10231
+ constructor(scene, mappedBoneNames, semanticKeyMap, options = {}) {
10232
+ __publicField(this, "scene");
10233
+ __publicField(this, "group");
10234
+ __publicField(this, "visualizations", []);
10235
+ __publicField(this, "boneTree", []);
10236
+ __publicField(this, "mappedBoneNames");
10237
+ __publicField(this, "semanticKeyMap");
10238
+ // boneName → semanticKey
10239
+ __publicField(this, "boneLookup", /* @__PURE__ */ new Map());
10240
+ // Shared geometry and materials
10241
+ __publicField(this, "sharedGeometry");
10242
+ __publicField(this, "mappedMaterial");
10243
+ __publicField(this, "unmappedMaterial");
10244
+ __publicField(this, "highlightMaterial");
10245
+ __publicField(this, "selectedMaterial");
10246
+ __publicField(this, "contextMaterial");
10247
+ __publicField(this, "mutedMaterial");
10248
+ __publicField(this, "axisRingGeometry");
10249
+ __publicField(this, "axisRingHitGeometry");
10250
+ __publicField(this, "axisCenterGeometry");
10251
+ __publicField(this, "pitchRingMaterial");
10252
+ __publicField(this, "yawRingMaterial");
10253
+ __publicField(this, "rollRingMaterial");
10254
+ __publicField(this, "axisHitMaterial");
10255
+ __publicField(this, "axisCenterMaterial");
10256
+ __publicField(this, "axisGizmoGroup");
10257
+ __publicField(this, "axisGizmoMeshes");
10258
+ __publicField(this, "axisGizmoHitMeshes");
10259
+ // Sentinel mesh for per-frame updates (stored for disposal)
10260
+ __publicField(this, "sentinelMesh", null);
10261
+ // State
10262
+ __publicField(this, "highlightedBone", null);
10263
+ __publicField(this, "selectedBone", null);
10264
+ __publicField(this, "activeAxis", null);
10265
+ __publicField(this, "opacityScale");
10266
+ __publicField(this, "focusSelectedBone");
10267
+ __publicField(this, "visibilityAlpha", 0);
10268
+ __publicField(this, "visibilityTarget", 0);
10269
+ __publicField(this, "visibilityLastFrameAt", 0);
10270
+ this.scene = scene;
10271
+ this.mappedBoneNames = mappedBoneNames;
10272
+ this.semanticKeyMap = semanticKeyMap;
10273
+ this.opacityScale = THREE3.MathUtils.clamp(options.opacityScale ?? 1, 0, 1);
10274
+ this.focusSelectedBone = options.focusSelectedBone ?? false;
10275
+ this.group = new THREE3.Group();
10276
+ this.group.name = "SkeletonOverlay";
10277
+ this.group.renderOrder = 999;
10278
+ this.group.visible = false;
10279
+ this.sharedGeometry = createOctahedralBoneGeometry();
10280
+ const materialOpts = {
10281
+ transparent: true,
10282
+ depthTest: false,
10283
+ depthWrite: false,
10284
+ side: THREE3.DoubleSide
10285
+ };
10286
+ this.mappedMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, color: COLORS.mapped, opacity: OPACITY.mapped });
10287
+ this.unmappedMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, color: COLORS.unmapped, opacity: OPACITY.unmapped });
10288
+ this.highlightMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, color: COLORS.highlighted, opacity: OPACITY.highlighted });
10289
+ this.selectedMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, color: COLORS.selected, opacity: OPACITY.selected });
10290
+ this.contextMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, color: COLORS.selected, opacity: OPACITY.context });
10291
+ this.mutedMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, color: COLORS.unmapped, opacity: OPACITY.muted });
10292
+ this.mappedMaterial.forceSinglePass = true;
10293
+ this.unmappedMaterial.forceSinglePass = true;
10294
+ this.highlightMaterial.forceSinglePass = true;
10295
+ this.selectedMaterial.forceSinglePass = true;
10296
+ this.contextMaterial.forceSinglePass = true;
10297
+ this.mutedMaterial.forceSinglePass = true;
10298
+ this.axisRingGeometry = new THREE3.TorusGeometry(1, 0.032, 10, 120);
10299
+ this.axisRingHitGeometry = new THREE3.TorusGeometry(1, AXIS_GIZMO_HIT_TUBE, 8, 64);
10300
+ this.axisCenterGeometry = new THREE3.SphereGeometry(0.082, 12, 8);
10301
+ this.pitchRingMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, color: AXIS_GIZMO_COLORS.pitch, opacity: AXIS_GIZMO_IDLE_OPACITY });
10302
+ this.yawRingMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, color: AXIS_GIZMO_COLORS.yaw, opacity: AXIS_GIZMO_IDLE_OPACITY });
10303
+ this.rollRingMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, color: AXIS_GIZMO_COLORS.roll, opacity: AXIS_GIZMO_IDLE_OPACITY });
10304
+ this.axisHitMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, opacity: 0 });
10305
+ this.axisHitMaterial.colorWrite = false;
10306
+ this.axisCenterMaterial = new THREE3.MeshBasicMaterial({ ...materialOpts, color: 16119802, opacity: 0.92 });
10307
+ this.axisGizmoGroup = new THREE3.Group();
10308
+ this.axisGizmoGroup.name = "SelectedBoneAxisGizmo";
10309
+ this.axisGizmoGroup.visible = false;
10310
+ this.axisGizmoGroup.renderOrder = 1e3;
10311
+ const pitchRing = new THREE3.Mesh(this.axisRingGeometry, this.pitchRingMaterial);
10312
+ pitchRing.renderOrder = 1e3;
10313
+ pitchRing.userData.axis = "pitch";
10314
+ const pitchHit = new THREE3.Mesh(this.axisRingHitGeometry, this.axisHitMaterial);
10315
+ pitchHit.userData.axis = "pitch";
10316
+ const yawRing = new THREE3.Mesh(this.axisRingGeometry, this.yawRingMaterial);
10317
+ yawRing.renderOrder = 1e3;
10318
+ yawRing.userData.axis = "yaw";
10319
+ const yawHit = new THREE3.Mesh(this.axisRingHitGeometry, this.axisHitMaterial);
10320
+ yawHit.userData.axis = "yaw";
10321
+ const rollRing = new THREE3.Mesh(this.axisRingGeometry, this.rollRingMaterial);
10322
+ rollRing.renderOrder = 1e3;
10323
+ rollRing.userData.axis = "roll";
10324
+ const rollHit = new THREE3.Mesh(this.axisRingHitGeometry, this.axisHitMaterial);
10325
+ rollHit.rotation.copy(rollRing.rotation);
10326
+ rollHit.userData.axis = "roll";
10327
+ const center = new THREE3.Mesh(this.axisCenterGeometry, this.axisCenterMaterial);
10328
+ center.renderOrder = 1001;
10329
+ this.axisGizmoMeshes = {
10330
+ pitch: pitchRing,
10331
+ yaw: yawRing,
10332
+ roll: rollRing
10333
+ };
10334
+ this.axisGizmoHitMeshes = {
10335
+ pitch: pitchHit,
10336
+ yaw: yawHit,
10337
+ roll: rollHit
10338
+ };
10339
+ Object.values(this.axisGizmoMeshes).forEach((mesh) => {
10340
+ mesh.frustumCulled = false;
10341
+ this.axisGizmoGroup.add(mesh);
10342
+ });
10343
+ Object.values(this.axisGizmoHitMeshes).forEach((mesh) => {
10344
+ mesh.frustumCulled = false;
10345
+ this.axisGizmoGroup.add(mesh);
10346
+ });
10347
+ center.frustumCulled = false;
10348
+ this.axisGizmoGroup.add(center);
10349
+ this.syncAxisGizmoChannels();
10350
+ this.syncVisibilityOpacity();
10351
+ this.updateAxisGizmoAppearance();
10352
+ }
10353
+ /**
10354
+ * Build the skeleton visualization from a scene root.
10355
+ * Traverses to find all THREE.Bone instances and creates octahedral meshes.
10356
+ */
10357
+ loadSkeleton(root) {
10358
+ this.clear();
10359
+ root.updateWorldMatrix(true, true);
10360
+ const allBones = collectSkeletonBones(root);
10361
+ const includedBones = new Set(allBones);
10362
+ this.boneLookup = new Map(allBones.map((bone) => [bone.name, bone]));
10363
+ if (allBones.length === 0) return;
10364
+ this.boneTree = allBones.map((bone) => {
10365
+ const isMapped = this.mappedBoneNames.has(bone.name);
10366
+ const semanticKey = this.semanticKeyMap.get(bone.name) ?? null;
10367
+ const boneChildren = bone.children.filter((c) => isBone2(c) && includedBones.has(c)).map((c) => c.name);
10368
+ const parentName = bone.parent && isBone2(bone.parent) && includedBones.has(bone.parent) ? bone.parent.name : null;
10369
+ let depth = 0;
10370
+ let current = bone.parent;
10371
+ while (current && isBone2(current) && includedBones.has(current)) {
10372
+ depth++;
10373
+ current = current.parent;
10374
+ }
10375
+ return {
10376
+ name: bone.name,
10377
+ depth,
10378
+ parent: parentName,
10379
+ children: boneChildren,
10380
+ isMapped,
10381
+ semanticKey,
10382
+ boneObject: bone
10383
+ };
10384
+ });
10385
+ for (const bone of allBones) {
10386
+ const boneChildren = bone.children.filter(
10387
+ (c) => isBone2(c) && includedBones.has(c)
10388
+ );
10389
+ if (boneChildren.length > 0) {
10390
+ if (shouldSkipRootHelperConnection(bone, boneChildren)) {
10391
+ continue;
10392
+ }
10393
+ for (const child of boneChildren) {
10394
+ this.createBoneMesh(bone, child);
10395
+ }
10396
+ } else {
10397
+ this.createBoneMesh(bone, null);
10398
+ }
10399
+ }
10400
+ this.group.add(this.axisGizmoGroup);
10401
+ const sentinelGeo = new THREE3.SphereGeometry(1e-6, 4, 2);
10402
+ const sentinelMat = new THREE3.MeshBasicMaterial({
10403
+ transparent: true,
10404
+ opacity: 0,
10405
+ depthWrite: false,
10406
+ depthTest: false
10407
+ });
10408
+ sentinelMat.colorWrite = false;
10409
+ const sentinel = new THREE3.Mesh(sentinelGeo, sentinelMat);
10410
+ sentinel.visible = true;
10411
+ sentinel.frustumCulled = false;
10412
+ sentinel.onBeforeRender = (renderer, _scene, camera) => this.update(camera, renderer.domElement);
10413
+ this.sentinelMesh = sentinel;
10414
+ this.group.add(sentinel);
10415
+ this.scene.add(this.group);
10416
+ this.update();
10417
+ }
10418
+ createBoneMesh(parentBone, childBone) {
10419
+ const isMapped = this.mappedBoneNames.has(parentBone.name);
10420
+ const material = isMapped ? this.mappedMaterial : this.unmappedMaterial;
10421
+ const mesh = new THREE3.Mesh(this.sharedGeometry, material);
10422
+ mesh.name = `bone_vis_${parentBone.name}${childBone ? `_${childBone.name}` : "_leaf"}`;
10423
+ mesh.frustumCulled = false;
10424
+ mesh.renderOrder = 999;
10425
+ mesh.userData.boneName = parentBone.name;
10426
+ this.visualizations.push({
10427
+ mesh,
10428
+ parentBone,
10429
+ childBone,
10430
+ boneName: parentBone.name,
10431
+ isMapped
10432
+ });
10433
+ this.group.add(mesh);
10434
+ }
10435
+ /** Called each frame via onBeforeRender. Updates all bone mesh transforms. */
10436
+ update(camera, domElement) {
10437
+ this.advanceVisibility();
10438
+ for (const vis of this.visualizations) {
10439
+ vis.parentBone.getWorldPosition(_parentPos);
10440
+ if (vis.childBone) {
10441
+ vis.childBone.getWorldPosition(_childPos);
10442
+ } else {
10443
+ _childPos.set(0, LEAF_BONE_LENGTH, 0);
10444
+ vis.parentBone.localToWorld(_childPos);
10445
+ }
10446
+ _direction.subVectors(_childPos, _parentPos);
10447
+ const length = _direction.length();
10448
+ if (length < 1e-4) {
10449
+ vis.mesh.visible = false;
10450
+ continue;
10451
+ }
10452
+ vis.mesh.visible = this.visibilityAlpha > VISIBILITY_EPSILON;
10453
+ _direction.normalize();
10454
+ _quaternion.setFromUnitVectors(_zAxis, _direction);
10455
+ vis.mesh.position.copy(_parentPos);
10456
+ vis.mesh.quaternion.copy(_quaternion);
10457
+ vis.mesh.scale.set(length, length, length);
10458
+ }
10459
+ this.updateAxisGizmo(camera, domElement);
10460
+ }
10461
+ syncVisibilityOpacity() {
10462
+ const alpha = this.visibilityAlpha * this.opacityScale;
10463
+ this.mappedMaterial.opacity = OPACITY.mapped * alpha;
10464
+ this.unmappedMaterial.opacity = OPACITY.unmapped * alpha;
10465
+ this.highlightMaterial.opacity = OPACITY.highlighted * alpha;
10466
+ this.selectedMaterial.opacity = OPACITY.selected * alpha;
10467
+ this.contextMaterial.opacity = OPACITY.context * alpha;
10468
+ this.mutedMaterial.opacity = OPACITY.muted * alpha;
10469
+ }
10470
+ syncGroupVisibility() {
10471
+ this.group.visible = this.visualizations.length > 0 && (this.visibilityTarget > 0 || this.visibilityAlpha > VISIBILITY_EPSILON || this.selectedBone != null);
10472
+ }
10473
+ advanceVisibility() {
10474
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
10475
+ const deltaMs = this.visibilityLastFrameAt > 0 ? now - this.visibilityLastFrameAt : 16;
10476
+ this.visibilityLastFrameAt = now;
10477
+ if (Math.abs(this.visibilityAlpha - this.visibilityTarget) > VISIBILITY_EPSILON) {
10478
+ const step = deltaMs / VISIBILITY_FADE_MS;
10479
+ if (this.visibilityTarget > this.visibilityAlpha) {
10480
+ this.visibilityAlpha = Math.min(this.visibilityTarget, this.visibilityAlpha + step);
10481
+ } else {
10482
+ this.visibilityAlpha = Math.max(this.visibilityTarget, this.visibilityAlpha - step);
10483
+ }
10484
+ } else {
10485
+ this.visibilityAlpha = this.visibilityTarget;
10486
+ }
10487
+ if (this.visibilityAlpha <= VISIBILITY_EPSILON && this.visibilityTarget === 0) {
10488
+ this.visibilityAlpha = 0;
10489
+ }
10490
+ this.syncVisibilityOpacity();
10491
+ this.syncGroupVisibility();
10492
+ }
10493
+ resolveSelectedBoneObject() {
10494
+ if (!this.selectedBone) return null;
10495
+ return this.boneLookup.get(this.selectedBone) ?? null;
10496
+ }
10497
+ resolveBoneRelativeAxisGizmoRadius(bone) {
10498
+ bone.getWorldPosition(_parentPos);
10499
+ let referenceLength = 0;
10500
+ for (const child of bone.children) {
10501
+ if (!isBone2(child)) continue;
10502
+ child.getWorldPosition(_childPos);
10503
+ const nextDistance = _parentPos.distanceTo(_childPos);
10504
+ if (nextDistance <= 1e-4) continue;
10505
+ referenceLength = referenceLength === 0 ? nextDistance : Math.min(referenceLength, nextDistance);
10506
+ }
10507
+ if (referenceLength <= 1e-4 && isBone2(bone.parent)) {
10508
+ bone.parent.getWorldPosition(_childPos);
10509
+ referenceLength = _parentPos.distanceTo(_childPos);
10510
+ }
10511
+ if (referenceLength <= 1e-4) {
10512
+ referenceLength = LEAF_BONE_LENGTH;
10513
+ }
10514
+ return THREE3.MathUtils.clamp(
10515
+ referenceLength * AXIS_GIZMO_RADIUS_FACTOR,
10516
+ AXIS_GIZMO_MIN_RADIUS,
10517
+ AXIS_GIZMO_MAX_RADIUS
10518
+ );
10519
+ }
10520
+ resolveScreenSpaceAxisGizmoRadius(origin, camera, domElement) {
10521
+ if (!camera || !domElement) return null;
10522
+ const rect = domElement.getBoundingClientRect();
10523
+ const viewportHeight = domElement.clientHeight || rect.height;
10524
+ if (viewportHeight <= 0) return null;
10525
+ if (camera.isPerspectiveCamera) {
10526
+ const perspectiveCamera = camera;
10527
+ _cameraSpacePosition.copy(origin).applyMatrix4(perspectiveCamera.matrixWorldInverse);
10528
+ const depth = -_cameraSpacePosition.z;
10529
+ if (!Number.isFinite(depth) || depth <= perspectiveCamera.near) return null;
10530
+ const visibleWorldHeight = 2 * depth * Math.tan(THREE3.MathUtils.degToRad(perspectiveCamera.getEffectiveFOV()) / 2);
10531
+ return visibleWorldHeight * (AXIS_GIZMO_SCREEN_RADIUS_PX / viewportHeight);
10532
+ }
10533
+ if (camera.isOrthographicCamera) {
10534
+ const orthographicCamera = camera;
10535
+ const visibleWorldHeight = (orthographicCamera.top - orthographicCamera.bottom) / orthographicCamera.zoom;
10536
+ return visibleWorldHeight * (AXIS_GIZMO_SCREEN_RADIUS_PX / viewportHeight);
10537
+ }
10538
+ return null;
10539
+ }
10540
+ resolveAxisGizmoRadius(bone, origin, camera, domElement) {
10541
+ return this.resolveScreenSpaceAxisGizmoRadius(origin, camera, domElement) ?? this.resolveBoneRelativeAxisGizmoRadius(bone);
10542
+ }
10543
+ updateAxisGizmoAppearance() {
10544
+ const hasSelection = !!this.selectedBone;
10545
+ this.axisGizmoGroup.visible = hasSelection && this.group.visible;
10546
+ const meshes = [
10547
+ ["pitch", this.axisGizmoMeshes.pitch, this.axisGizmoHitMeshes.pitch],
10548
+ ["yaw", this.axisGizmoMeshes.yaw, this.axisGizmoHitMeshes.yaw],
10549
+ ["roll", this.axisGizmoMeshes.roll, this.axisGizmoHitMeshes.roll]
10550
+ ];
10551
+ meshes.forEach(([axis, mesh, hitMesh]) => {
10552
+ const material = mesh.material;
10553
+ const isActive = this.activeAxis === axis;
10554
+ const baseOpacity = this.activeAxis == null ? AXIS_GIZMO_IDLE_OPACITY : isActive ? AXIS_GIZMO_ACTIVE_OPACITY : AXIS_GIZMO_MUTED_OPACITY;
10555
+ material.opacity = hasSelection ? baseOpacity : 0;
10556
+ const scale = isActive ? AXIS_GIZMO_ACTIVE_SCALE : 1;
10557
+ mesh.scale.setScalar(scale);
10558
+ hitMesh.scale.setScalar(scale);
10559
+ });
10560
+ this.axisCenterMaterial.opacity = hasSelection ? 0.92 : 0;
10561
+ }
10562
+ updateAxisGizmo(camera, domElement) {
10563
+ const selectedBone = this.resolveSelectedBoneObject();
10564
+ if (!selectedBone || !this.group.visible) {
10565
+ this.axisGizmoGroup.visible = false;
10566
+ return;
10567
+ }
10568
+ selectedBone.getWorldPosition(_bonePos);
10569
+ selectedBone.getWorldQuaternion(_boneQuat);
10570
+ const radius = this.resolveAxisGizmoRadius(selectedBone, _bonePos, camera, domElement);
10571
+ this.axisGizmoGroup.visible = true;
10572
+ this.axisGizmoGroup.position.copy(_bonePos);
10573
+ this.axisGizmoGroup.quaternion.copy(_boneQuat);
10574
+ this.axisGizmoGroup.scale.setScalar(radius);
10575
+ this.updateAxisGizmoAppearance();
10576
+ this.axisGizmoGroup.updateWorldMatrix(true, true);
10577
+ }
10578
+ applyAxisRingChannel(mesh, hitMesh, axis) {
10579
+ const channel = DEFAULT_AXIS_CHANNELS[axis];
10580
+ mesh.rotation.set(0, 0, 0);
10581
+ hitMesh.rotation.set(0, 0, 0);
10582
+ if (channel === "rx") {
10583
+ mesh.rotation.y = Math.PI / 2;
10584
+ hitMesh.rotation.y = Math.PI / 2;
10585
+ return;
10586
+ }
10587
+ if (channel === "ry") {
10588
+ mesh.rotation.x = Math.PI / 2;
10589
+ hitMesh.rotation.x = Math.PI / 2;
10590
+ }
10591
+ }
10592
+ syncAxisGizmoChannels() {
10593
+ Object.keys(this.axisGizmoMeshes).forEach((axis) => {
10594
+ this.applyAxisRingChannel(
10595
+ this.axisGizmoMeshes[axis],
10596
+ this.axisGizmoHitMeshes[axis],
10597
+ axis
10598
+ );
10599
+ });
10600
+ }
10601
+ getSelectedBonePointerData() {
10602
+ const bone = this.resolveSelectedBoneObject();
10603
+ if (!bone || !this.selectedBone) return null;
10604
+ bone.getWorldPosition(_bonePos);
10605
+ bone.getWorldQuaternion(_boneQuat);
10606
+ return {
10607
+ boneName: this.selectedBone,
10608
+ origin: _bonePos.clone(),
10609
+ quaternion: _boneQuat.clone()
10610
+ };
10611
+ }
10612
+ getAxisPlaneNormal(axis, quaternion) {
10613
+ const channel = DEFAULT_AXIS_CHANNELS[axis];
10614
+ if (channel === "rx") {
10615
+ return _planeNormal.set(1, 0, 0).applyQuaternion(quaternion).normalize();
10616
+ }
10617
+ if (channel === "ry") {
10618
+ return _planeNormal.set(0, 1, 0).applyQuaternion(quaternion).normalize();
10619
+ }
10620
+ return _planeNormal.set(0, 0, 1).applyQuaternion(quaternion).normalize();
10621
+ }
10622
+ // ── Public API ─────────────────────────────────────────────────────────
10623
+ getBoneTree() {
10624
+ return this.boneTree;
10625
+ }
10626
+ setVisible(visible) {
10627
+ this.visibilityTarget = visible ? 1 : 0;
10628
+ this.visibilityLastFrameAt = 0;
10629
+ this.syncGroupVisibility();
10630
+ this.advanceVisibility();
10631
+ this.updateAxisGizmoAppearance();
10632
+ }
10633
+ isVisible() {
10634
+ return this.visibilityTarget > 0;
10635
+ }
10636
+ setHighlighted(boneName) {
10637
+ this.highlightedBone = boneName;
10638
+ this.refreshBoneMaterials();
10639
+ }
10640
+ setSelected(boneName) {
10641
+ this.selectedBone = boneName;
10642
+ this.refreshBoneMaterials();
10643
+ this.syncGroupVisibility();
10644
+ this.updateAxisGizmo();
10645
+ }
10646
+ setActiveAxis(axis) {
10647
+ this.activeAxis = axis;
10648
+ this.updateAxisGizmoAppearance();
10649
+ }
10650
+ /** Get world position and rotation for a bone by name */
10651
+ getBoneTransform(boneName) {
10652
+ const node = this.boneTree.find((b) => b.name === boneName);
10653
+ if (!node) return null;
10654
+ const bone = node.boneObject;
10655
+ bone.getWorldPosition(_bonePos);
10656
+ bone.getWorldQuaternion(_boneQuat);
10657
+ _boneEuler.setFromQuaternion(_boneQuat);
10658
+ return {
10659
+ position: [_bonePos.x, _bonePos.y, _bonePos.z],
10660
+ rotation: [
10661
+ THREE3.MathUtils.radToDeg(_boneEuler.x),
10662
+ THREE3.MathUtils.radToDeg(_boneEuler.y),
10663
+ THREE3.MathUtils.radToDeg(_boneEuler.z)
10664
+ ]
10665
+ };
10666
+ }
10667
+ pickBone(clientX, clientY, camera, domElement) {
10668
+ if (!this.group.visible || this.visibilityTarget <= 0 || this.visibilityAlpha <= 0.35) {
10669
+ return null;
10670
+ }
10671
+ if (!setRayFromPointer(clientX, clientY, camera, domElement)) {
10672
+ return null;
10673
+ }
10674
+ const hits = _raycaster.intersectObjects(
10675
+ this.visualizations.map((visualization) => visualization.mesh),
10676
+ false
10677
+ );
10678
+ const hit = hits[0];
10679
+ if (!hit) return null;
10680
+ return typeof hit.object.userData.boneName === "string" ? hit.object.userData.boneName : null;
10681
+ }
10682
+ pickAxisGizmo(clientX, clientY, camera, domElement) {
10683
+ if (!this.axisGizmoGroup.visible || !this.selectedBone) return null;
10684
+ this.updateAxisGizmo(camera, domElement);
10685
+ if (!this.axisGizmoGroup.visible) return null;
10686
+ if (!setRayFromPointer(clientX, clientY, camera, domElement)) {
10687
+ return null;
10688
+ }
10689
+ const hits = _raycaster.intersectObjects(Object.values(this.axisGizmoHitMeshes), false);
10690
+ const hit = hits[0];
10691
+ if (!hit) return null;
10692
+ const axis = hit.object.userData.axis;
10693
+ if (axis !== "pitch" && axis !== "yaw" && axis !== "roll") {
10694
+ return null;
10695
+ }
10696
+ const base = this.getSelectedBonePointerData();
10697
+ if (!base) return null;
10698
+ return {
10699
+ ...base,
10700
+ axis,
10701
+ channel: DEFAULT_AXIS_CHANNELS[axis],
10702
+ point: hit.point.clone()
10703
+ };
10704
+ }
10705
+ projectPointerToAxisPlane(clientX, clientY, camera, domElement, axis) {
10706
+ if (!this.axisGizmoGroup.visible || !this.selectedBone) {
10707
+ return null;
10708
+ }
10709
+ this.updateAxisGizmo(camera, domElement);
10710
+ if (!this.axisGizmoGroup.visible) return null;
10711
+ const base = this.getSelectedBonePointerData();
10712
+ if (!base) return null;
10713
+ if (!setRayFromPointer(clientX, clientY, camera, domElement)) {
10714
+ return null;
10715
+ }
10716
+ const normal = this.getAxisPlaneNormal(axis, base.quaternion);
10717
+ _dragPlane.setFromNormalAndCoplanarPoint(normal, base.origin);
10718
+ const hit = _raycaster.ray.intersectPlane(_dragPlane, _planeHit);
10719
+ if (!hit) return null;
10720
+ return {
10721
+ ...base,
10722
+ axis,
10723
+ channel: DEFAULT_AXIS_CHANNELS[axis],
10724
+ point: hit.clone()
10725
+ };
10726
+ }
10727
+ isSelectedContextBone(boneName) {
10728
+ if (!this.selectedBone || boneName === this.selectedBone) return false;
10729
+ const selectedBone = this.boneLookup.get(this.selectedBone);
10730
+ const bone = this.boneLookup.get(boneName);
10731
+ if (!selectedBone || !bone) return false;
10732
+ return bone.parent === selectedBone || selectedBone.parent === bone;
10733
+ }
10734
+ resolveBoneMaterial(boneName, isMapped) {
10735
+ if (boneName === this.selectedBone) {
10736
+ return this.selectedMaterial;
10737
+ }
10738
+ if (boneName === this.highlightedBone) {
10739
+ return this.highlightMaterial;
10740
+ }
10741
+ if (this.focusSelectedBone && this.selectedBone) {
10742
+ return this.isSelectedContextBone(boneName) ? this.contextMaterial : this.mutedMaterial;
10743
+ }
10744
+ return isMapped ? this.mappedMaterial : this.unmappedMaterial;
10745
+ }
10746
+ refreshBoneMaterials() {
10747
+ for (const vis of this.visualizations) {
10748
+ vis.mesh.material = this.resolveBoneMaterial(vis.boneName, vis.isMapped);
10749
+ }
10750
+ }
10751
+ clear() {
10752
+ if (this.sentinelMesh) {
10753
+ this.sentinelMesh.geometry.dispose();
10754
+ this.sentinelMesh.material.dispose();
10755
+ this.sentinelMesh = null;
10756
+ }
10757
+ while (this.group.children.length > 0) {
10758
+ this.group.remove(this.group.children[0]);
10759
+ }
10760
+ this.visualizations = [];
10761
+ this.boneTree = [];
10762
+ this.boneLookup.clear();
10763
+ this.axisGizmoGroup.visible = false;
10764
+ this.group.visible = false;
10765
+ }
10766
+ dispose() {
10767
+ this.scene.remove(this.group);
10768
+ this.clear();
10769
+ this.sharedGeometry.dispose();
10770
+ this.mappedMaterial.dispose();
10771
+ this.unmappedMaterial.dispose();
10772
+ this.highlightMaterial.dispose();
10773
+ this.selectedMaterial.dispose();
10774
+ this.contextMaterial.dispose();
10775
+ this.mutedMaterial.dispose();
10776
+ this.axisRingGeometry.dispose();
10777
+ this.axisRingHitGeometry.dispose();
10778
+ this.axisCenterGeometry.dispose();
10779
+ this.pitchRingMaterial.dispose();
10780
+ this.yawRingMaterial.dispose();
10781
+ this.rollRingMaterial.dispose();
10782
+ this.axisHitMaterial.dispose();
10783
+ this.axisCenterMaterial.dispose();
10784
+ }
10785
+ };
10786
+ function collectSkeletonBones(root) {
10787
+ const startBone = resolveBodyDisplayRoot(root);
10788
+ return collectBones(startBone ?? root);
10789
+ }
10790
+ function findModelRoot(scene) {
10791
+ const roots = {};
10792
+ scene.traverse((obj) => {
10793
+ if (!roots.tagged && (obj.userData?.embodyCharacterModel === true || obj.userData?.loomlargeCharacterModel === true)) {
10794
+ roots.tagged = obj;
10795
+ }
10796
+ if (!roots.bone && obj.isBone) {
10797
+ roots.bone = obj;
10798
+ }
10799
+ if (!roots.skinned && obj.isSkinnedMesh) {
10800
+ roots.skinned = obj;
10801
+ }
10802
+ });
10803
+ if (roots.tagged) return roots.tagged;
10804
+ const anchor = roots.bone ?? roots.skinned;
10805
+ if (!anchor) return null;
10806
+ let current = anchor;
10807
+ while (current && current.parent && current.parent !== scene) {
10808
+ current = current.parent;
10809
+ }
10810
+ return current;
10811
+ }
10812
+ function buildMappedBoneInfo(profile, actualBoneNames = []) {
10813
+ const mappedNames = /* @__PURE__ */ new Set();
10814
+ const semanticKeyMap = /* @__PURE__ */ new Map();
10815
+ const semanticKeysMap = /* @__PURE__ */ new Map();
10816
+ const actualNames = Array.from(new Set(actualBoneNames));
10817
+ const boneNodes = profile.boneNodes || {};
10818
+ const prefix = profile.bonePrefix || "";
10819
+ const suffix = profile.boneSuffix || "";
10820
+ const addSemanticKey = (boneName, semanticKey) => {
10821
+ mappedNames.add(boneName);
10822
+ if (!semanticKeyMap.has(boneName)) {
10823
+ semanticKeyMap.set(boneName, semanticKey);
10824
+ }
10825
+ const existing = semanticKeysMap.get(boneName) ?? [];
10826
+ if (!existing.includes(semanticKey)) {
10827
+ semanticKeysMap.set(boneName, [...existing, semanticKey]);
10828
+ }
10829
+ };
10830
+ for (const [semanticKey, nodeName] of Object.entries(boneNodes)) {
10831
+ const fullName = prefix + nodeName + suffix;
10832
+ addSemanticKey(fullName, semanticKey);
10833
+ if (fullName !== nodeName) {
10834
+ addSemanticKey(nodeName, semanticKey);
10835
+ }
10836
+ actualNames.forEach((actualName) => {
10837
+ if (fuzzyNameMatch(actualName, fullName, profile.suffixPattern) || fuzzyNameMatch(actualName, nodeName, profile.suffixPattern)) {
10838
+ addSemanticKey(actualName, semanticKey);
10839
+ }
10840
+ });
10841
+ }
10842
+ return { mappedNames, semanticKeyMap, semanticKeysMap };
10843
+ }
10844
+ var DEFAULT_THRESHOLD = 1e-4;
10845
+ var DEFAULT_MAX_POINTS = 12e3;
10846
+ var COLOR_LOW = { r: 1, g: 0.78, b: 0.28 };
10847
+ var COLOR_HIGH = { r: 1, g: 0.36, b: 0.16 };
10848
+ function getMorphPositionAttribute2(mesh, morphName) {
10849
+ const morphIndex = mesh.morphTargetDictionary?.[morphName];
10850
+ if (morphIndex == null) return null;
10851
+ const morphPositions = mesh.geometry.morphAttributes.position;
10852
+ return morphPositions?.[morphIndex] ?? null;
10853
+ }
10854
+ function getDeltaMagnitudeAtIndex(mesh, morphAttribute, index) {
10855
+ const geometry = mesh.geometry;
10856
+ const relative = geometry.morphTargetsRelative;
10857
+ const basePositions = geometry.getAttribute("position");
10858
+ if (!basePositions) return 0;
10859
+ const dx = morphAttribute.getX(index) - (relative ? 0 : basePositions.getX(index));
10860
+ const dy = morphAttribute.getY(index) - (relative ? 0 : basePositions.getY(index));
10861
+ const dz = morphAttribute.getZ(index) - (relative ? 0 : basePositions.getZ(index));
10862
+ return Math.hypot(dx, dy, dz);
10863
+ }
10864
+ function resolveMorphPreviewMeshes(root, morphName, meshNames) {
10865
+ const nameFilter = meshNames?.length ? new Set(meshNames) : null;
10866
+ const meshes = [];
10867
+ root.traverse((obj) => {
10868
+ const mesh = obj;
10869
+ if (!mesh.isMesh) return;
10870
+ if (nameFilter && !nameFilter.has(mesh.name)) return;
10871
+ if (mesh.morphTargetDictionary?.[morphName] == null) return;
10872
+ meshes.push(mesh);
10873
+ });
10874
+ return meshes;
10875
+ }
10876
+ function buildMorphVertexPreviewSample(mesh, morphName, options = {}) {
10877
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD;
10878
+ const maxPoints = options.maxPoints ?? DEFAULT_MAX_POINTS;
10879
+ const morphAttribute = getMorphPositionAttribute2(mesh, morphName);
10880
+ if (!morphAttribute) return null;
10881
+ const candidateIndices = [];
10882
+ const magnitudes = [];
10883
+ for (let index = 0; index < morphAttribute.count; index += 1) {
10884
+ const magnitude = getDeltaMagnitudeAtIndex(mesh, morphAttribute, index);
10885
+ if (magnitude <= threshold) continue;
10886
+ candidateIndices.push(index);
10887
+ magnitudes.push(magnitude);
10888
+ }
10889
+ if (candidateIndices.length === 0) return null;
10890
+ const stride = candidateIndices.length > maxPoints ? Math.ceil(candidateIndices.length / maxPoints) : 1;
10891
+ const sampleCount = Math.ceil(candidateIndices.length / stride);
10892
+ const indices = new Uint32Array(sampleCount);
10893
+ const intensities = new Float32Array(sampleCount);
10894
+ let maxMagnitude = 0;
10895
+ let cursor = 0;
10896
+ for (let i = 0; i < candidateIndices.length; i += stride) {
10897
+ const magnitude = magnitudes[i] ?? 0;
10898
+ indices[cursor] = candidateIndices[i] ?? 0;
10899
+ intensities[cursor] = magnitude;
10900
+ maxMagnitude = Math.max(maxMagnitude, magnitude);
10901
+ cursor += 1;
10902
+ }
10903
+ const normalizer = maxMagnitude > 0 ? 1 / maxMagnitude : 1;
10904
+ for (let i = 0; i < intensities.length; i += 1) {
10905
+ intensities[i] *= normalizer;
10906
+ }
10907
+ return { indices, intensities };
10908
+ }
10909
+ function writePointColor(colors, offset, intensity2) {
10910
+ colors[offset] = COLOR_LOW.r + (COLOR_HIGH.r - COLOR_LOW.r) * intensity2;
10911
+ colors[offset + 1] = COLOR_LOW.g + (COLOR_HIGH.g - COLOR_LOW.g) * intensity2;
10912
+ colors[offset + 2] = COLOR_LOW.b + (COLOR_HIGH.b - COLOR_LOW.b) * intensity2;
10913
+ }
10914
+ function buildMorphVertexPreviewRenderData(meshes, morphName, cache, options = {}) {
10915
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD;
10916
+ const maxPoints = options.maxPoints ?? DEFAULT_MAX_POINTS;
10917
+ const sampledMeshes = [];
10918
+ let pointCount = 0;
10919
+ for (const mesh of meshes) {
10920
+ const cacheKey = `${mesh.uuid}:${morphName}:${threshold}:${maxPoints}`;
10921
+ let sample = cache.get(cacheKey) ?? null;
10922
+ if (!sample) {
10923
+ sample = buildMorphVertexPreviewSample(mesh, morphName, { threshold, maxPoints });
10924
+ if (sample) cache.set(cacheKey, sample);
10925
+ }
10926
+ if (!sample) continue;
10927
+ sampledMeshes.push({ mesh, sample });
10928
+ pointCount += sample.indices.length;
10929
+ }
10930
+ if (pointCount === 0) return null;
10931
+ const positions = new Float32Array(pointCount * 3);
10932
+ const colors = new Float32Array(pointCount * 3);
10933
+ const vertex = new THREE3.Vector3();
10934
+ let cursor = 0;
10935
+ for (const { mesh, sample } of sampledMeshes) {
10936
+ mesh.updateWorldMatrix(true, false);
10937
+ const skinnedMesh = mesh;
10938
+ if (skinnedMesh.isSkinnedMesh && skinnedMesh.skeleton) {
10939
+ skinnedMesh.skeleton.update();
10940
+ }
10941
+ for (let i = 0; i < sample.indices.length; i += 1) {
10942
+ mesh.getVertexPosition(sample.indices[i] ?? 0, vertex);
10943
+ vertex.applyMatrix4(mesh.matrixWorld);
10944
+ const offset = cursor * 3;
10945
+ positions[offset] = vertex.x;
10946
+ positions[offset + 1] = vertex.y;
10947
+ positions[offset + 2] = vertex.z;
10948
+ writePointColor(colors, offset, sample.intensities[i] ?? 0);
10949
+ cursor += 1;
10950
+ }
10951
+ }
10952
+ return {
10953
+ positions,
10954
+ colors,
10955
+ pointCount,
10956
+ meshCount: sampledMeshes.length
10957
+ };
10958
+ }
10959
+ var previewRenderBindings = /* @__PURE__ */ new WeakMap();
10960
+ function bindPreviewRender(scene, update) {
10961
+ let binding = previewRenderBindings.get(scene);
10962
+ if (!binding) {
10963
+ const previous = scene.onBeforeRender;
10964
+ const callbacks = /* @__PURE__ */ new Set();
10965
+ const render = function(...args) {
10966
+ previous.apply(scene, args);
10967
+ callbacks.forEach((callback) => callback());
10968
+ };
10969
+ binding = { callbacks, previous, render };
10970
+ scene.onBeforeRender = render;
10971
+ previewRenderBindings.set(scene, binding);
10972
+ }
10973
+ binding.callbacks.add(update);
10974
+ const current = binding;
10975
+ return () => {
10976
+ current.callbacks.delete(update);
10977
+ if (current.callbacks.size > 0) return;
10978
+ if (scene.onBeforeRender === current.render) scene.onBeforeRender = current.previous;
10979
+ if (previewRenderBindings.get(scene) === current) previewRenderBindings.delete(scene);
10980
+ };
10981
+ }
10982
+ var MorphVertexPreview = class {
10983
+ constructor(scene, options = {}) {
10984
+ this.scene = scene;
10985
+ this.options = options;
10986
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
10987
+ __publicField(this, "points", null);
10988
+ __publicField(this, "active", null);
10989
+ __publicField(this, "disposed", false);
10990
+ __publicField(this, "unbindRender", null);
10991
+ }
10992
+ show(morphName, meshNames) {
10993
+ if (this.disposed) return;
10994
+ if (meshNames?.length === 0) {
10995
+ this.hide();
10996
+ return;
10997
+ }
10998
+ this.active = { morphName, meshNames: meshNames?.slice() };
10999
+ this.unbindRender ?? (this.unbindRender = bindPreviewRender(this.scene, () => this.update()));
11000
+ this.update();
11001
+ }
11002
+ hide() {
11003
+ this.active = null;
11004
+ this.unbindRender?.();
11005
+ this.unbindRender = null;
11006
+ if (this.points) this.points.visible = false;
11007
+ }
11008
+ dispose() {
11009
+ if (this.disposed) return;
11010
+ this.disposed = true;
11011
+ this.hide();
11012
+ this.cache.clear();
11013
+ if (!this.points) return;
11014
+ this.points.removeFromParent();
11015
+ this.points.geometry.dispose();
11016
+ this.points.material.dispose();
11017
+ this.points = null;
11018
+ }
11019
+ createPoints() {
11020
+ const points = new THREE3.Points(new THREE3.BufferGeometry(), new THREE3.PointsMaterial({
11021
+ size: 0.012,
11022
+ sizeAttenuation: true,
11023
+ transparent: true,
11024
+ opacity: 0.9,
11025
+ depthWrite: false,
11026
+ vertexColors: true,
11027
+ toneMapped: false
11028
+ }));
11029
+ points.name = "MorphVertexPreviewPoints";
11030
+ points.renderOrder = 500;
11031
+ points.frustumCulled = false;
11032
+ points.visible = false;
11033
+ this.scene.add(points);
11034
+ this.points = points;
11035
+ return points;
11036
+ }
11037
+ update() {
11038
+ if (this.disposed || !this.active) return;
11039
+ const { morphName, meshNames } = this.active;
11040
+ const meshes = resolveMorphPreviewMeshes(this.scene, morphName, meshNames);
11041
+ const data = buildMorphVertexPreviewRenderData(meshes, morphName, this.cache, this.options);
11042
+ if (!data) {
11043
+ if (this.points) this.points.visible = false;
11044
+ return;
11045
+ }
11046
+ const points = this.points ?? this.createPoints();
11047
+ this.updateAttribute(points.geometry, "position", data.positions, data.pointCount);
11048
+ this.updateAttribute(points.geometry, "color", data.colors, data.pointCount);
11049
+ points.geometry.computeBoundingSphere();
11050
+ points.material.size = data.pointCount > 6e3 ? 9e-3 : 0.012;
11051
+ points.visible = true;
11052
+ }
11053
+ updateAttribute(geometry, name, values, pointCount) {
11054
+ const attribute = geometry.getAttribute(name);
11055
+ if (!attribute || attribute.count !== pointCount) {
11056
+ geometry.setAttribute(name, new THREE3.BufferAttribute(values, 3));
11057
+ return;
11058
+ }
11059
+ attribute.array.set(values);
11060
+ attribute.needsUpdate = true;
11061
+ }
11062
+ };
9421
11063
 
9422
11064
  // three/index.ts
9423
11065
  var THREE_BLENDING_MODES = {
@@ -9429,7 +11071,7 @@ var THREE_BLENDING_MODES = {
9429
11071
  };
9430
11072
  var BLENDING_MODES = THREE_BLENDING_MODES;
9431
11073
  var MORPH_ATTRIBUTE_SEMANTICS = ["position", "normal", "tangent", "color"];
9432
- var createNeutralMorphAttribute = (geometry, semantic, name) => {
11074
+ var createNeutralMorphAttribute2 = (geometry, semantic, name) => {
9433
11075
  const base = geometry.getAttribute(semantic);
9434
11076
  if (!base) throw new Error(`Cannot create ${semantic} morph data without a base attribute`);
9435
11077
  const values = new Float32Array(base.count * base.itemSize);
@@ -9940,7 +11582,7 @@ var ThreeFrameApplier = class {
9940
11582
  sourceIsRelative
9941
11583
  );
9942
11584
  } else if (!attributes[targetIndex] || targetIndex === index) {
9943
- attributes[targetIndex] = createNeutralMorphAttribute(
11585
+ attributes[targetIndex] = createNeutralMorphAttribute2(
9944
11586
  geometry,
9945
11587
  semantic,
9946
11588
  targetIndex === index ? target.name : `morph_${targetIndex}`
@@ -9983,6 +11625,6 @@ var collectMorphMeshes = (root) => {
9983
11625
  return meshes;
9984
11626
  };
9985
11627
 
9986
- export { BLENDING_MODES, CHARACTER_SCENE_TYPES, CHARACTER_SCENE_TYPE_IDS, DPthreeCameraController 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, DPthree, DPthree3DMarkers, DPthreeCameraController, DPthreeHTMLMarkers, DPthreeMarkers, Embody, DPthree3DMarkers as Markers3D, DPthreeHTMLMarkers as MarkersHTML, RUNTIME_ANNOTATION_REGION_PREFIX, RustAnnotationCameraCore, THREE_BLENDING_MODES, ThreeFrameApplier, ThreeModelInspector, ThreeNativePlaybackConnector, analyzeModel, applyAUBoneBindingUpdate, applyBilateralAxisBindingUpdate, applyBoneAxisBindingUpdate, applyCharacterModelTransform, bindModelReferencePose, boneResolutionProfile, buildBoneAuOptions, buildRegionDisplayOptions, captureModelReferencePose, classifyAuAsJointControl, collectMorphMeshes, 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, findNodeKeyForBone, formatAxisDirectionLabel, formatAxisLabel, fuzzyNameMatch, getAUBoneBindingState, getAxisFromChannel, getBilateralAxisBindingState, getBodyControls, 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, resolveRegionCameraAngle, resolveRegionVisibilityCameraAngle, resolveVisemeProfile, serializeAnimationClips, setAUMorphTargets, setHumanoidRoleBinding, shouldProjectMarkerAnchorToSurface, shouldUseFaceCenterForMarkerAnchor, stripConfiguredBoneAffixes, supportsRustEmbodyPreset, toWorldDirection, validateHumanoidCharacterization, validateMappings };
11628
+ export { BLENDING_MODES, CHARACTER_SCENE_TYPES, CHARACTER_SCENE_TYPE_IDS, DPthreeCameraController 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, DPthree, DPthree3DMarkers, DPthreeCameraController, DPthreeHTMLMarkers, DPthreeMarkers, Embody, DPthree3DMarkers as Markers3D, DPthreeHTMLMarkers as MarkersHTML, MorphVertexPreview, RUNTIME_ANNOTATION_REGION_PREFIX, RustAnnotationCameraCore, SkeletonOverlay, THREE_BLENDING_MODES, 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 };
9987
11629
  //# sourceMappingURL=three.js.map
9988
11630
  //# sourceMappingURL=three.js.map