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