@lovelace_lol/embody 0.0.9 → 0.0.11

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.
@@ -72,6 +72,7 @@ export declare class DPthree3DMarkers {
72
72
  private zoomThreshold;
73
73
  private autoCollapseThreshold;
74
74
  private lastCameraDistance;
75
+ private markerLayoutDirty;
75
76
  private visibilityAnimationActive;
76
77
  private readonly visibilityAnimationScheduler;
77
78
  private visibilityAnimationStates;
@@ -247,7 +248,7 @@ export declare class DPthree3DMarkers {
247
248
  private refreshLaterality;
248
249
  /**
249
250
  * Apply zoom-based scaling to all markers, lines, and labels.
250
- * Only called when crossing the zoom threshold (not every frame).
251
+ * Runs on zoom threshold crossings and when marker geometry changes.
251
252
  */
252
253
  private applyZoomScale;
253
254
  setVisible(visible: boolean): void;
@@ -1,5 +1,10 @@
1
1
  import type { AnimationClip } from 'three';
2
2
  import type { ClipIRTrack, ThreeModelInspection } from './index';
3
+ /**
4
+ * Packed morphs must share PropertyMixers with indexed compiled tracks. Euler
5
+ * components avoid Euler.fromArray's incompatible mixer-buffer offset/order.
6
+ */
7
+ export declare function withComponentTracks(clip: AnimationClip): AnimationClip;
3
8
  /** One PropertyMixer cache key for every concrete renderer destination. */
4
9
  export declare function concreteTrackName(target: ClipIRTrack['target'] | undefined, inspection: ThreeModelInspection): string | undefined;
5
10
  /** Retain authored samples and interpolants; normalize only cloned binding names. */
package/dist/three.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as THREE3 from 'three';
2
- import { NoBlending, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NormalBlending, Vector3, Box3, Timer, Matrix4, AnimationMixer, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopOnce, LoopRepeat, LoopPingPong, PropertyBinding, Quaternion, AnimationUtils, InterpolateDiscrete, InterpolateLinear, MathUtils, Mesh, PlaneGeometry, ShadowMaterial, HemisphereLight, DirectionalLight, SRGBColorSpace, ACESFilmicToneMapping, WebGLRenderer, Scene, Color, PerspectiveCamera, PCFShadowMap, PCFSoftShadowMap, NumberKeyframeTrack, QuaternionKeyframeTrack, VectorKeyframeTrack, AnimationClip, PMREMGenerator, FloatType, Float32BufferAttribute, Euler, BufferAttribute, WebGPUCoordinateSystem, WebGLCoordinateSystem } from 'three';
2
+ import { NoBlending, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NormalBlending, Vector3, Box3, Timer, Matrix4, AnimationMixer, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopOnce, LoopRepeat, LoopPingPong, PropertyBinding, Euler, Quaternion, AnimationUtils, InterpolateDiscrete, InterpolateLinear, MathUtils, Mesh, PlaneGeometry, ShadowMaterial, HemisphereLight, DirectionalLight, SRGBColorSpace, ACESFilmicToneMapping, WebGLRenderer, Scene, Color, PerspectiveCamera, PCFShadowMap, PCFSoftShadowMap, NumberKeyframeTrack, QuaternionKeyframeTrack, VectorKeyframeTrack, AnimationClip, PMREMGenerator, FloatType, Float32BufferAttribute, BufferAttribute, WebGPUCoordinateSystem, WebGLCoordinateSystem } from 'three';
3
3
  import { CC4_HUMANOID_SKELETON_TEMPLATE, HUMANOID_SKELETON_TEMPLATES, DEFAULT_HAIR_COLOR_APPEARANCE, initEmbodyCore, requireInitializedEmbodyCore } from '@lovelace_lol/embody/wasm';
4
4
  export { CC4_HUMANOID_SKELETON_TEMPLATE, DEFAULT_AXIS_TO_CHANNEL, DEFAULT_BONE_MAX_DEGREES, DEFAULT_HAIR_COLOR_APPEARANCE, HAIR_COLOR_PRESETS, HUMANOID_SKELETON_TEMPLATES, JOINT_CONTROL_SECTION, VISEME_KEYS } from '@lovelace_lol/embody/wasm';
5
5
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
@@ -1206,6 +1206,7 @@ var DPthree3DMarkers = class {
1206
1206
  // Calculated from model size - collapse expanded regions when zoomed out
1207
1207
  __publicField(this, "lastCameraDistance", -1);
1208
1208
  // Cache to avoid unnecessary checks
1209
+ __publicField(this, "markerLayoutDirty", false);
1209
1210
  __publicField(this, "visibilityAnimationActive", false);
1210
1211
  __publicField(this, "visibilityAnimationScheduler", new AnimationFrameTaskScheduler());
1211
1212
  __publicField(this, "visibilityAnimationStates", []);
@@ -1369,6 +1370,8 @@ var DPthree3DMarkers = class {
1369
1370
  return;
1370
1371
  }
1371
1372
  this.separateOverlappingMarkers(annotationCameraCore);
1373
+ this.markerLayoutDirty = true;
1374
+ this.update();
1372
1375
  console.log(`[3DMarkers] loadRegions complete. Created markers for:`, Array.from(this.markerMeshes.keys()));
1373
1376
  console.log(`[3DMarkers] lineEndpoints:`, Array.from(this.lineEndpoints.keys()));
1374
1377
  console.log(`[3DMarkers] resolvedStyles:`, Array.from(this.resolvedStyles.keys()));
@@ -1840,6 +1843,12 @@ var DPthree3DMarkers = class {
1840
1843
  this.markerTargetVisibility.set(region.name, !startHidden && !region.isFallback);
1841
1844
  this.markerAngleGateVisibility.delete(region.name);
1842
1845
  this.markerViewportVisibility.set(region.name, true);
1846
+ this.markerLayoutDirty = true;
1847
+ if (this.visibilityAnimationActive) {
1848
+ this.setMarkerObjectsVisible(region.name, false, false);
1849
+ } else {
1850
+ this.update();
1851
+ }
1843
1852
  }
1844
1853
  /**
1845
1854
  * Embody Rust owns endpoint layout; the Three adapter applies the returned positions
@@ -2455,7 +2464,7 @@ var DPthree3DMarkers = class {
2455
2464
  const dy = this.camera.position.y - this.modelCenter.y;
2456
2465
  const dz = this.camera.position.z - this.modelCenter.z;
2457
2466
  const cameraDistance = Math.sqrt(dx * dx + dy * dy + dz * dz);
2458
- const distanceChanged = this.lastCameraDistance < 0 || Math.abs(cameraDistance - this.lastCameraDistance) > this.lastCameraDistance * 0.02;
2467
+ const distanceChanged = this.markerLayoutDirty || this.lastCameraDistance < 0 || Math.abs(cameraDistance - this.lastCameraDistance) > this.lastCameraDistance * 0.02;
2459
2468
  if (distanceChanged) {
2460
2469
  const previousDistance = this.lastCameraDistance;
2461
2470
  this.lastCameraDistance = cameraDistance;
@@ -2466,22 +2475,27 @@ var DPthree3DMarkers = class {
2466
2475
  sphere.scale.setScalar(clampedScale);
2467
2476
  }
2468
2477
  const shouldBeZoomedIn = cameraDistance < this.zoomThreshold;
2469
- if (shouldBeZoomedIn !== this.isZoomedIn) {
2478
+ const zoomChanged = shouldBeZoomedIn !== this.isZoomedIn;
2479
+ if (this.markerLayoutDirty || zoomChanged) {
2470
2480
  this.isZoomedIn = shouldBeZoomedIn;
2471
- this.applyZoomScale(shouldBeZoomedIn ? ZOOMED_IN_LINE_SCALE : 1);
2481
+ this.applyZoomScale(
2482
+ shouldBeZoomedIn ? ZOOMED_IN_LINE_SCALE : 1,
2483
+ zoomChanged || shouldBeZoomedIn
2484
+ );
2472
2485
  }
2473
2486
  const crossedAutoCollapseThreshold = previousDistance >= 0 && previousDistance <= this.autoCollapseThreshold && cameraDistance > this.autoCollapseThreshold;
2474
2487
  if (crossedAutoCollapseThreshold) {
2475
2488
  this.collapseAllRegions();
2476
2489
  }
2477
2490
  }
2478
- const cameraMoved = !this.hasCameraAngleUpdateSnapshot || this.lastCameraAngleUpdatePosition.distanceToSquared(this.camera.position) > 1e-4 || 1 - Math.abs(this.lastCameraAngleUpdateQuaternion.dot(this.camera.quaternion)) > 1e-5;
2491
+ const cameraMoved = this.markerLayoutDirty || !this.hasCameraAngleUpdateSnapshot || this.lastCameraAngleUpdatePosition.distanceToSquared(this.camera.position) > 1e-4 || 1 - Math.abs(this.lastCameraAngleUpdateQuaternion.dot(this.camera.quaternion)) > 1e-5;
2479
2492
  if (!cameraMoved) {
2480
2493
  return;
2481
2494
  }
2482
2495
  this.lastCameraAngleUpdatePosition.copy(this.camera.position);
2483
2496
  this.lastCameraAngleUpdateQuaternion.copy(this.camera.quaternion);
2484
2497
  this.hasCameraAngleUpdateSnapshot = true;
2498
+ this.markerLayoutDirty = false;
2485
2499
  const cameraAngle = getModelLocalOrbitAngle(this.model, this.modelCenter, this.camera.position);
2486
2500
  for (const [name, sphere] of this.markerMeshes) {
2487
2501
  const markerAngle = sphere.userData.cameraAngle;
@@ -2519,14 +2533,16 @@ var DPthree3DMarkers = class {
2519
2533
  }
2520
2534
  /**
2521
2535
  * Apply zoom-based scaling to all markers, lines, and labels.
2522
- * Only called when crossing the zoom threshold (not every frame).
2536
+ * Runs on zoom threshold crossings and when marker geometry changes.
2523
2537
  */
2524
- applyZoomScale(scaleFactor) {
2538
+ applyZoomScale(scaleFactor, scaleSpheres = true) {
2525
2539
  const isZoomed = scaleFactor < 1;
2526
2540
  const sphereScaleFactor = isZoomed ? ZOOMED_IN_SPHERE_SCALE : 1;
2527
2541
  const labelScaleFactor = isZoomed ? ZOOMED_IN_LABEL_SCALE : 1;
2528
- for (const sphere of this.markerMeshes.values()) {
2529
- sphere.scale.setScalar(sphereScaleFactor);
2542
+ if (scaleSpheres) {
2543
+ for (const sphere of this.markerMeshes.values()) {
2544
+ sphere.scale.setScalar(sphereScaleFactor);
2545
+ }
2530
2546
  }
2531
2547
  for (const line of this.lineMeshes.values()) {
2532
2548
  const material = line.material;
@@ -2630,6 +2646,10 @@ var DPthree3DMarkers = class {
2630
2646
  object.visible = true;
2631
2647
  this.setObjectOpacity(object, opacity * itemOpacityFactor);
2632
2648
  }
2649
+ const name = state.label?.object.userData.annotation;
2650
+ if (typeof name === "string" && !this.updateMarkerLineLayout(name)) {
2651
+ this.setMarkerObjectsVisible(name, false, false);
2652
+ }
2633
2653
  }
2634
2654
  }
2635
2655
  restoreVisibilityAnimationStates(states) {
@@ -2815,6 +2835,8 @@ var DPthree3DMarkers = class {
2815
2835
  this.stopVisibilityAnimation(true);
2816
2836
  if (visible) {
2817
2837
  this.markerGroup.visible = true;
2838
+ this.markerLayoutDirty = true;
2839
+ this.update();
2818
2840
  }
2819
2841
  this.visibilityAnimationStates = this.captureVisibilityAnimationStates();
2820
2842
  if (this.visibilityAnimationStates.length === 0) {
@@ -2877,6 +2899,7 @@ var DPthree3DMarkers = class {
2877
2899
  this.markerAngleGateVisibility.clear();
2878
2900
  this.markerViewportVisibility.clear();
2879
2901
  this.soloedMarker = null;
2902
+ this.markerLayoutDirty = false;
2880
2903
  this.hasCameraAngleUpdateSnapshot = false;
2881
2904
  }
2882
2905
  // ============ SOLO MODE ============
@@ -6732,8 +6755,30 @@ function bindModelReferencePose(model, referencePose) {
6732
6755
  if (index !== referencePose.nodes.length) throw new Error("Incompatible reference pose hierarchy: extra captured nodes");
6733
6756
  return bindings2;
6734
6757
  }
6735
-
6736
- // three/clipBindings.ts
6758
+ function withComponentTracks(clip) {
6759
+ let expanded = false;
6760
+ const tracks = clip.tracks.flatMap((track) => {
6761
+ const parsed = PropertyBinding.parseTrackName(track.name);
6762
+ const euler = parsed.propertyName === "rotation";
6763
+ if (!euler && !["morphTargetInfluences", "weights"].includes(parsed.propertyName) || parsed.propertyIndex !== void 0) return [track];
6764
+ const cubic = track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true;
6765
+ const parts = cubic ? 3 : 1;
6766
+ const width = track.getValueSize() / parts;
6767
+ if (!Number.isInteger(width) || width < 1 || euler && width !== 3) throw new Error(`Invalid component track ${JSON.stringify(track.name)}.`);
6768
+ expanded = true;
6769
+ return Array.from({ length: width }, (_, component) => {
6770
+ const scalar = track.clone();
6771
+ scalar.name = `${track.name}[${euler ? ["x", "y", "z"][component] : component}]`;
6772
+ scalar.values = track.values.slice(0, track.times.length * parts);
6773
+ for (let sample = 0; sample < scalar.values.length; sample += 1) scalar.values[sample] = track.values[sample * width + component];
6774
+ return scalar;
6775
+ });
6776
+ });
6777
+ if (!expanded) return clip;
6778
+ const indexed = clip.clone();
6779
+ indexed.tracks = tracks;
6780
+ return indexed;
6781
+ }
6737
6782
  function concreteTrackName(target, inspection) {
6738
6783
  if (!target) return void 0;
6739
6784
  if (target.kind === "morphTarget") {
@@ -6745,6 +6790,7 @@ function concreteTrackName(target, inspection) {
6745
6790
  const property = target.property ?? "rotation";
6746
6791
  if (!object) return void 0;
6747
6792
  if (property === "rotation" || property === "quaternion") return `${object.uuid}.quaternion`;
6793
+ if (property === "eulerX" || property === "eulerY" || property === "eulerZ") return `${object.uuid}.rotation[${property.slice(-1).toLowerCase()}]`;
6748
6794
  if (property === "position" || property === "scale") return `${object.uuid}.${property}`;
6749
6795
  }
6750
6796
  if (target.kind === "meshVisibility") {
@@ -6855,6 +6901,7 @@ var ThreeNativePlaybackConnector = class {
6855
6901
  */
6856
6902
  playNativeClip(clip, options = {}) {
6857
6903
  this.beforeOperation();
6904
+ clip = withComponentTracks(clip);
6858
6905
  const [ir] = serializeAnimationClips(this.binding.model, [clip], this.binding.inspection);
6859
6906
  if (ir.tracks.length !== clip.tracks.length) {
6860
6907
  throw new Error("Every native playback track must serialize against the bound model.");
@@ -6902,6 +6949,7 @@ var ThreeNativePlaybackConnector = class {
6902
6949
  /** Edit authored samples, retaining the native action and its loop/fade progress. */
6903
6950
  replaceClip(handle, clip) {
6904
6951
  this.beforeOperation();
6952
+ clip = withComponentTracks(clip);
6905
6953
  const resource = this.resources.get(handle);
6906
6954
  if (!resource) throw new Error("Unknown, foreign or stale playback handle.");
6907
6955
  const [ir] = serializeAnimationClips(this.binding.model, [clip], this.binding.inspection);
@@ -7410,10 +7458,17 @@ var Embody = class _Embody {
7410
7458
  switch (parsed.propertyName) {
7411
7459
  case "quaternion":
7412
7460
  return Object.values(reference.transform.rotation);
7461
+ case "rotation": {
7462
+ const rotation = reference.transform.rotation;
7463
+ const euler = reference.rotationEuler ?? new Euler().setFromQuaternion(new Quaternion(rotation.x, rotation.y, rotation.z, rotation.w), reference.rotationOrder);
7464
+ const axis = parsed.propertyIndex;
7465
+ return axis === "x" || axis === "y" || axis === "z" ? [euler[axis]] : [euler.x, euler.y, euler.z];
7466
+ }
7413
7467
  case "position":
7414
7468
  return Object.values(reference.transform.position);
7415
7469
  case "scale":
7416
7470
  return Object.values(reference.transform.scale);
7471
+ case "weights":
7417
7472
  case "morphTargetInfluences": {
7418
7473
  const mesh = object;
7419
7474
  const index = typeof parsed.propertyIndex === "string" && !/^\d+$/.test(parsed.propertyIndex) ? mesh.morphTargetDictionary?.[parsed.propertyIndex] : Number(parsed.propertyIndex);
@@ -7430,7 +7485,7 @@ var Embody = class _Embody {
7430
7485
  if (!nativeSource) {
7431
7486
  ir = this.compile(source, options);
7432
7487
  }
7433
- const clip = nativeSource ? source.clip.clone() : createAnimationClipFromClipIR(ir, this.inspection);
7488
+ const clip = nativeSource ? withComponentTracks(source.clip).clone() : createAnimationClipFromClipIR(ir, this.inspection);
7434
7489
  if (ir) {
7435
7490
  const predecessor = this.current.get(source.name);
7436
7491
  const outgoing = predecessor?.connector.getAction(predecessor.id);
@@ -9589,6 +9644,10 @@ function createAnimationClipFromClipIR(clip, inspection) {
9589
9644
  if (property === "rotation" || property === "quaternion") {
9590
9645
  if (track.inheritStart) object.quaternion.toArray(values, 0);
9591
9646
  tracks.push(new QuaternionKeyframeTrack(trackName, times, values, interpolation));
9647
+ } else if (property === "eulerX" || property === "eulerY" || property === "eulerZ") {
9648
+ const axis = property.slice(-1).toLowerCase();
9649
+ if (track.inheritStart) values[0] = object.rotation[axis];
9650
+ tracks.push(new NumberKeyframeTrack(trackName, times, values, interpolation));
9592
9651
  } else if (property === "position") {
9593
9652
  if (track.inheritStart) object.position.toArray(values, 0);
9594
9653
  tracks.push(new VectorKeyframeTrack(trackName, times, values, interpolation));
@@ -9622,7 +9681,7 @@ function serializeAnimationClips(model, clips, inspection) {
9622
9681
  channelKinds.set(kind, id);
9623
9682
  return id;
9624
9683
  };
9625
- clip.tracks.forEach((track, index) => {
9684
+ withComponentTracks(clip).tracks.forEach((track, index) => {
9626
9685
  const found = findTrackTarget(model, track);
9627
9686
  if (!found?.object) return;
9628
9687
  const { parsed, object } = found;
@@ -9637,7 +9696,7 @@ function serializeAnimationClips(model, clips, inspection) {
9637
9696
  } else if (property === "visible" && object.isMesh) {
9638
9697
  target = { kind: "meshVisibility", meshId: meshIds.get(object) };
9639
9698
  } else {
9640
- const transformProperty = property === "quaternion" || property === "rotation" ? "rotation" : property === "position" ? "position" : property === "scale" ? "scale" : null;
9699
+ const transformProperty = property === "rotation" && ["x", "y", "z"].includes(String(parsed.propertyIndex)) ? `euler${String(parsed.propertyIndex).toUpperCase()}` : property === "quaternion" ? "rotation" : property === "position" ? "position" : property === "scale" ? "scale" : null;
9641
9700
  if (transformProperty) {
9642
9701
  const boneId = boneIds.get(object);
9643
9702
  target = boneId ? { kind: "boneTransform", boneId, property: transformProperty } : { kind: "objectTransform", objectId: objectIds.get(object), property: transformProperty };