@2112-lab/central-plant 0.3.29 → 0.3.31

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.
@@ -4171,6 +4171,7 @@ var TransformControlsManager = /*#__PURE__*/function () {
4171
4171
  raycaster.setFromCamera(mouse, _this4.camera);
4172
4172
  var allIntersects = raycaster.intersectObjects(_this4.scene.children, true);
4173
4173
  var ioDeviceObject = null;
4174
+ var hitMesh = null;
4174
4175
  var _iterator = _createForOfIteratorHelper(allIntersects),
4175
4176
  _step;
4176
4177
  try {
@@ -4185,7 +4186,10 @@ var TransformControlsManager = /*#__PURE__*/function () {
4185
4186
  }
4186
4187
  obj = obj.parent;
4187
4188
  }
4188
- if (ioDeviceObject) break;
4189
+ if (ioDeviceObject) {
4190
+ hitMesh = hit.object;
4191
+ break;
4192
+ }
4189
4193
  }
4190
4194
  } catch (err) {
4191
4195
  _iterator.e(err);
@@ -4200,7 +4204,7 @@ var TransformControlsManager = /*#__PURE__*/function () {
4200
4204
  _this4._ioDragStartY = event.clientY;
4201
4205
  _this4._ioDragMoved = false;
4202
4206
  if (_this4.orbitControls) _this4.orbitControls.enabled = false;
4203
- _this4.callbacks.onIODeviceDrag(ioDeviceObject, 0, true);
4207
+ _this4.callbacks.onIODeviceDrag(ioDeviceObject, 0, true, hitMesh);
4204
4208
  var onMove = function onMove(e) {
4205
4209
  var dx = e.clientX - _this4._ioDragStartX;
4206
4210
  var dy = e.clientY - _this4._ioDragStartY;
@@ -36669,7 +36673,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
36669
36673
  */
36670
36674
  }, {
36671
36675
  key: "startIODeviceDrag",
36672
- value: function startIODeviceDrag(ioDeviceObject) {
36676
+ value: function startIODeviceDrag(ioDeviceObject, hitMesh) {
36673
36677
  var _this$sceneViewer2,
36674
36678
  _this2 = this;
36675
36679
  if (!ioDeviceObject || !this._stateAdapter) return;
@@ -36688,7 +36692,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
36688
36692
  }
36689
36693
  var scopedAttachmentId = this._getScopedAttachmentKey(attachmentId, parentUuid);
36690
36694
  var ioBehavMgr = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.managers) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.ioBehaviorManager;
36691
- var dataPoints = ((ioBehavMgr === null || ioBehavMgr === void 0 ? void 0 : ioBehavMgr.getAnimationDataPoints(parentUuid, attachmentId)) || []).concat((ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [])
36695
+ var dataPoints = ((ioBehavMgr === null || ioBehavMgr === void 0 ? void 0 : ioBehavMgr.getAnimationDataPoints(parentUuid, attachmentId, hitMesh)) || []).concat((ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [])
36692
36696
  // deduplicate by id
36693
36697
  .filter(function (dp, i, arr) {
36694
36698
  return arr.findIndex(function (d) {
@@ -37401,6 +37405,19 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37401
37405
  if (!anims.length) return;
37402
37406
  var key = this._key(parentUuid, attachmentId);
37403
37407
  var entries = [];
37408
+
37409
+ // Capture the device root's world orientation once so each entry can
37410
+ // convert the configured axis from device-local space to world space.
37411
+ var deviceWorldQuat = new THREE__namespace.Quaternion();
37412
+ deviceModelRoot.getWorldQuaternion(deviceWorldQuat);
37413
+
37414
+ // Compute the model's native max dimension so rotAxisOffset values (stored
37415
+ // in dialog-viewer-world units, where the model is normalised to 1 unit)
37416
+ // can be scaled to runtime-world units. viewerMaxDim = 1 / ns_viewer.
37417
+ var _deviceBox = new THREE__namespace.Box3().setFromObject(deviceModelRoot);
37418
+ var _deviceSize = new THREE__namespace.Vector3();
37419
+ _deviceBox.getSize(_deviceSize);
37420
+ var viewerMaxDim = Math.max(_deviceSize.x, _deviceSize.y, _deviceSize.z) || 1;
37404
37421
  var _iterator = _createForOfIteratorHelper(anims),
37405
37422
  _step;
37406
37423
  try {
@@ -37411,11 +37428,23 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37411
37428
  console.warn("[IoBehaviorManager] Could not find mesh for animation \"".concat(anim.name || anim.stateVariable, "\" (uuid: ").concat(anim.meshUuid, ", name: \"").concat(anim.meshName, "\")"));
37412
37429
  continue;
37413
37430
  }
37431
+ var worldPos = new THREE__namespace.Vector3();
37432
+ mesh.getWorldPosition(worldPos);
37433
+ var worldQuat = new THREE__namespace.Quaternion();
37434
+ mesh.getWorldQuaternion(worldQuat);
37435
+ var box = new THREE__namespace.Box3().setFromObject(mesh);
37436
+ var worldCenter = new THREE__namespace.Vector3();
37437
+ if (!box.isEmpty()) box.getCenter(worldCenter);else worldCenter.copy(worldPos);
37414
37438
  entries.push({
37415
37439
  anim: anim,
37416
37440
  mesh: mesh,
37417
37441
  origPos: mesh.position.clone(),
37418
- origRot: mesh.rotation.clone()
37442
+ origRot: mesh.rotation.clone(),
37443
+ origWorldPos: worldPos,
37444
+ origWorldQuat: worldQuat,
37445
+ origWorldCenter: worldCenter,
37446
+ deviceWorldQuat: deviceWorldQuat.clone(),
37447
+ viewerMaxDim: viewerMaxDim
37419
37448
  });
37420
37449
  }
37421
37450
  } catch (err) {
@@ -37445,16 +37474,24 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37445
37474
  }, {
37446
37475
  key: "triggerState",
37447
37476
  value: function triggerState(attachmentId, dataPointId, value, parentUuid) {
37448
- var key = this._key(parentUuid, attachmentId);
37449
- var entries = this._entries.get(key);
37450
- if (!(entries !== null && entries !== void 0 && entries.length)) return;
37451
- var _iterator2 = _createForOfIteratorHelper(entries),
37477
+ var _iterator2 = _createForOfIteratorHelper(this._entries.values()),
37452
37478
  _step2;
37453
37479
  try {
37454
37480
  for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
37455
- var entry = _step2.value;
37456
- if (entry.anim.stateVariable !== dataPointId) continue;
37457
- this._applyAnimation(entry, value);
37481
+ var entries = _step2.value;
37482
+ var _iterator3 = _createForOfIteratorHelper(entries),
37483
+ _step3;
37484
+ try {
37485
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
37486
+ var entry = _step3.value;
37487
+ if (entry.anim.stateVariable !== dataPointId) continue;
37488
+ this._applyAnimation(entry, value);
37489
+ }
37490
+ } catch (err) {
37491
+ _iterator3.e(err);
37492
+ } finally {
37493
+ _iterator3.f();
37494
+ }
37458
37495
  }
37459
37496
  } catch (err) {
37460
37497
  _iterator2.e(err);
@@ -37489,34 +37526,47 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37489
37526
  }, {
37490
37527
  key: "getAnimationDataPoints",
37491
37528
  value: function getAnimationDataPoints(parentUuid, attachmentId) {
37529
+ var _this2 = this;
37530
+ var hitMesh = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
37492
37531
  var key = this._key(parentUuid, attachmentId);
37493
37532
  var entries = this._entries.get(key);
37494
37533
  if (!(entries !== null && entries !== void 0 && entries.length)) return [];
37495
37534
 
37535
+ // When a specific mesh was clicked, filter to only animations whose target
37536
+ // mesh is the clicked mesh or an ancestor of it (e.g. the clicked primitive
37537
+ // is inside a group that is the animation target).
37538
+ var filtered = entries;
37539
+ if (hitMesh) {
37540
+ var matching = entries.filter(function (e) {
37541
+ return _this2._isMeshOrDescendant(hitMesh, e.mesh);
37542
+ });
37543
+ if (matching.length > 0) filtered = matching;
37544
+ }
37545
+
37496
37546
  // Collapse multiple mesh entries that share the same stateVariable
37497
37547
  var seen = new Map(); // stateVariable → anim
37498
- var _iterator3 = _createForOfIteratorHelper(entries),
37499
- _step3;
37548
+ var _iterator4 = _createForOfIteratorHelper(filtered),
37549
+ _step4;
37500
37550
  try {
37501
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
37502
- var anim = _step3.value.anim;
37551
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
37552
+ var anim = _step4.value.anim;
37503
37553
  if (!seen.has(anim.stateVariable)) {
37504
37554
  seen.set(anim.stateVariable, anim);
37505
37555
  }
37506
37556
  }
37507
37557
  } catch (err) {
37508
- _iterator3.e(err);
37558
+ _iterator4.e(err);
37509
37559
  } finally {
37510
- _iterator3.f();
37560
+ _iterator4.f();
37511
37561
  }
37512
37562
  var dps = [];
37513
- var _iterator4 = _createForOfIteratorHelper(seen),
37514
- _step4;
37563
+ var _iterator5 = _createForOfIteratorHelper(seen),
37564
+ _step5;
37515
37565
  try {
37516
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
37517
- var _step4$value = _slicedToArray(_step4.value, 2),
37518
- stateVar = _step4$value[0],
37519
- _anim = _step4$value[1];
37566
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
37567
+ var _step5$value = _slicedToArray(_step5.value, 2),
37568
+ stateVar = _step5$value[0],
37569
+ _anim = _step5$value[1];
37520
37570
  // Normalise stateType from AnimateDevicesDialog variants
37521
37571
  var stateType = void 0;
37522
37572
  var raw = (_anim.stateType || '').toLowerCase();
@@ -37567,9 +37617,9 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37567
37617
  });
37568
37618
  }
37569
37619
  } catch (err) {
37570
- _iterator4.e(err);
37620
+ _iterator5.e(err);
37571
37621
  } finally {
37572
- _iterator4.f();
37622
+ _iterator5.f();
37573
37623
  }
37574
37624
  return dps;
37575
37625
  }
@@ -37604,19 +37654,19 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37604
37654
  key: "unloadForComponent",
37605
37655
  value: function unloadForComponent(parentUuid) {
37606
37656
  var prefix = "".concat(parentUuid, "::");
37607
- var _iterator5 = _createForOfIteratorHelper(this._entries.keys()),
37608
- _step5;
37657
+ var _iterator6 = _createForOfIteratorHelper(this._entries.keys()),
37658
+ _step6;
37609
37659
  try {
37610
- for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
37611
- var key = _step5.value;
37660
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
37661
+ var key = _step6.value;
37612
37662
  if (key.startsWith(prefix)) {
37613
37663
  this._entries.delete(key);
37614
37664
  }
37615
37665
  }
37616
37666
  } catch (err) {
37617
- _iterator5.e(err);
37667
+ _iterator6.e(err);
37618
37668
  } finally {
37619
- _iterator5.f();
37669
+ _iterator6.f();
37620
37670
  }
37621
37671
  }
37622
37672
  }, {
@@ -37635,6 +37685,22 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37635
37685
  return "".concat(parentUuid, "::").concat(attachmentId);
37636
37686
  }
37637
37687
 
37688
+ /**
37689
+ * Returns true if `candidate` is `ancestor` or any descendant of `ancestor`.
37690
+ * @param {THREE.Object3D} candidate
37691
+ * @param {THREE.Object3D} ancestor
37692
+ */
37693
+ }, {
37694
+ key: "_isMeshOrDescendant",
37695
+ value: function _isMeshOrDescendant(candidate, ancestor) {
37696
+ var obj = candidate;
37697
+ while (obj) {
37698
+ if (obj === ancestor) return true;
37699
+ obj = obj.parent;
37700
+ }
37701
+ return false;
37702
+ }
37703
+
37638
37704
  /**
37639
37705
  * Find the mesh inside `root` using UUID first, then name as fallback.
37640
37706
  * GLTFLoader assigns fresh UUIDs on every load, so name is the reliable key.
@@ -37675,27 +37741,32 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37675
37741
  var anim = entry.anim,
37676
37742
  mesh = entry.mesh,
37677
37743
  origPos = entry.origPos,
37678
- origRot = entry.origRot;
37744
+ origRot = entry.origRot,
37745
+ origWorldPos = entry.origWorldPos,
37746
+ origWorldQuat = entry.origWorldQuat,
37747
+ origWorldCenter = entry.origWorldCenter,
37748
+ deviceWorldQuat = entry.deviceWorldQuat,
37749
+ viewerMaxDim = entry.viewerMaxDim;
37679
37750
  var mapping = this._resolveMapping(anim, value);
37680
37751
  if (!mapping) return;
37681
37752
  var types = anim.transformTypes || [];
37682
- var _iterator6 = _createForOfIteratorHelper(types),
37683
- _step6;
37753
+ var _iterator7 = _createForOfIteratorHelper(types),
37754
+ _step7;
37684
37755
  try {
37685
- for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
37686
- var type = _step6.value;
37756
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
37757
+ var type = _step7.value;
37687
37758
  if (type === 'translation') {
37688
37759
  this._applyTranslation(mesh, origPos, mapping.transform);
37689
37760
  } else if (type === 'rotation') {
37690
- this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform);
37761
+ this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim);
37691
37762
  } else if (type === 'color') {
37692
37763
  this._applyColor(mesh, mapping.colorTransform);
37693
37764
  }
37694
37765
  }
37695
37766
  } catch (err) {
37696
- _iterator6.e(err);
37767
+ _iterator7.e(err);
37697
37768
  } finally {
37698
- _iterator6.f();
37769
+ _iterator7.f();
37699
37770
  }
37700
37771
  }
37701
37772
 
@@ -37832,26 +37903,24 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37832
37903
  }
37833
37904
 
37834
37905
  /**
37835
- * Apply a rotation around an arbitrary pivot point in device-local space,
37836
- * optionally also displacing the mesh position to simulate orbital motion.
37837
- *
37838
- * Math (all in device-local space):
37839
- * pivot = rotAxisOffset
37840
- * delta = origPos - pivot
37841
- * newDelta = rotate(delta, angle, axis)
37842
- * newPos = pivot + newDelta
37843
- * newRot[axis] = origRot[axis] + angle
37906
+ * Apply a rotation around an arbitrary pivot point using world-space quaternion
37907
+ * math matching the ReconstructionViewer's setMeshPreviewRotationAxis approach.
37908
+ * This ensures the runtime axis/pivot matches what the user configured in the
37909
+ * animation dialog, regardless of the device's parent transform in the scene.
37844
37910
  *
37845
37911
  * @param {THREE.Object3D} mesh
37846
- * @param {THREE.Vector3} origPos
37847
- * @param {THREE.Euler} origRot
37912
+ * @param {THREE.Vector3} origPos - local position at load time (unused, kept for signature compat)
37913
+ * @param {THREE.Euler} origRot - local rotation at load time (unused)
37848
37914
  * @param {Object} anim
37849
- * @param {number} angleDeg - Degrees
37915
+ * @param {number} angleDeg - Degrees
37916
+ * @param {THREE.Vector3} origWorldPos - world position at load time
37917
+ * @param {THREE.Quaternion} origWorldQuat - world quaternion at load time
37918
+ * @param {THREE.Vector3} origWorldCenter - world bounding-box center at load time
37850
37919
  */
37851
37920
  }, {
37852
37921
  key: "_applyRotation",
37853
- value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg) {
37854
- var _anim$rotAxis, _anim$rotAxisOffset, _off$x, _off$y, _off$z;
37922
+ value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim) {
37923
+ var _anim$rotAxis, _anim$rotAxisOffset;
37855
37924
  var angle = THREE__namespace.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
37856
37925
  var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
37857
37926
  var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
@@ -37860,13 +37929,57 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37860
37929
  z: 0
37861
37930
  };
37862
37931
 
37863
- // Unit vector for the chosen axis
37864
- var axisVec = new THREE__namespace.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
37865
- var pivot = new THREE__namespace.Vector3((_off$x = off.x) !== null && _off$x !== void 0 ? _off$x : 0, (_off$y = off.y) !== null && _off$y !== void 0 ? _off$y : 0, (_off$z = off.z) !== null && _off$z !== void 0 ? _off$z : 0);
37866
- var delta = origPos.clone().sub(pivot);
37867
- delta.applyAxisAngle(axisVec, angle);
37868
- mesh.position.copy(pivot).add(delta);
37869
- mesh.rotation[axis] = origRot[axis] + angle;
37932
+ // Local axis in the device's coordinate system
37933
+ var localAxisVec = new THREE__namespace.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
37934
+ if (origWorldPos && origWorldQuat && origWorldCenter) {
37935
+ // Transform the configured axis from device-local space into world space
37936
+ // so that 'X' means the device's local X, not the world X.
37937
+ var worldAxisVec = localAxisVec.clone().applyQuaternion(deviceWorldQuat || new THREE__namespace.Quaternion());
37938
+
37939
+ // rotAxisOffset is stored in dialog-viewer-world space where the model root
37940
+ // has been rotated Rot_Z(π) by default (ReconstructionViewer's else branch).
37941
+ // Translation uses the same convention and negates X/Y to compensate.
37942
+ // For rotation offset we apply the same compensation mathematically:
37943
+ // offset_runtime = deviceWorldQuat × Rot_Z(π) × offset_dialog × scale
37944
+ // This maps from dialog world space (post Z+180° flip) → model local → world.
37945
+ var scale = viewerMaxDim || 1;
37946
+ var rotZ180 = new THREE__namespace.Quaternion().setFromAxisAngle(new THREE__namespace.Vector3(0, 0, 1), Math.PI);
37947
+ var qCombined = (deviceWorldQuat || new THREE__namespace.Quaternion()).clone().multiply(rotZ180);
37948
+ var offWorld = new THREE__namespace.Vector3((off.x || 0) * scale, (off.y || 0) * scale, (off.z || 0) * scale).applyQuaternion(qCombined);
37949
+ var pivot = origWorldCenter.clone().add(offWorld);
37950
+ var deltaQuat = new THREE__namespace.Quaternion().setFromAxisAngle(worldAxisVec, angle);
37951
+
37952
+ // Rotate world position around pivot
37953
+ var offsetVec = origWorldPos.clone().sub(pivot);
37954
+ offsetVec.applyQuaternion(deltaQuat);
37955
+ var newWorldPos = pivot.clone().add(offsetVec);
37956
+
37957
+ // Compose world quaternion
37958
+ var newWorldQuat = deltaQuat.clone().multiply(origWorldQuat);
37959
+
37960
+ // Convert world position → parent local space
37961
+ if (mesh.parent) {
37962
+ mesh.parent.worldToLocal(newWorldPos);
37963
+ }
37964
+ mesh.position.copy(newWorldPos);
37965
+
37966
+ // Convert quaternion → parent local space
37967
+ if (mesh.parent) {
37968
+ var parentWorldQuat = new THREE__namespace.Quaternion();
37969
+ mesh.parent.getWorldQuaternion(parentWorldQuat);
37970
+ parentWorldQuat.invert();
37971
+ newWorldQuat.premultiply(parentWorldQuat);
37972
+ }
37973
+ mesh.quaternion.copy(newWorldQuat);
37974
+ } else {
37975
+ var _off$x, _off$y, _off$z;
37976
+ // Fallback for entries loaded without world data
37977
+ var _pivot = new THREE__namespace.Vector3((_off$x = off.x) !== null && _off$x !== void 0 ? _off$x : 0, (_off$y = off.y) !== null && _off$y !== void 0 ? _off$y : 0, (_off$z = off.z) !== null && _off$z !== void 0 ? _off$z : 0);
37978
+ var delta = origPos.clone().sub(_pivot);
37979
+ delta.applyAxisAngle(localAxisVec, angle);
37980
+ mesh.position.copy(_pivot).add(delta);
37981
+ mesh.rotation[axis] = origRot[axis] + angle;
37982
+ }
37870
37983
  }
37871
37984
 
37872
37985
  /**
@@ -39550,7 +39663,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
39550
39663
  * Initialize the CentralPlant manager
39551
39664
  *
39552
39665
  * @constructor
39553
- * @version 0.3.29
39666
+ * @version 0.3.31
39554
39667
  * @updated 2025-10-22
39555
39668
  *
39556
39669
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -42790,7 +42903,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
42790
42903
  _this4.componentTooltipManager.toggleIODeviceBinaryState(ioDeviceObject);
42791
42904
  }
42792
42905
  },
42793
- onIODeviceDrag: function onIODeviceDrag(ioDeviceObject, signedDelta, isStart) {
42906
+ onIODeviceDrag: function onIODeviceDrag(ioDeviceObject, signedDelta, isStart, hitMesh) {
42794
42907
  if (isStart) {
42795
42908
  var _ioDeviceObject$userD, _this4$managers$ioBeh, _this4$managers, _this4$managers2;
42796
42909
  // Resolve parentUuid by walking up to the host component.
@@ -42817,7 +42930,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
42817
42930
  }
42818
42931
  if (!_this4.componentTooltipManager) return;
42819
42932
  if (isStart) {
42820
- _this4.componentTooltipManager.startIODeviceDrag(ioDeviceObject);
42933
+ _this4.componentTooltipManager.startIODeviceDrag(ioDeviceObject, hitMesh);
42821
42934
  } else {
42822
42935
  _this4.componentTooltipManager.updateIODeviceDrag(signedDelta);
42823
42936
  }
@@ -35,7 +35,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
35
35
  * Initialize the CentralPlant manager
36
36
  *
37
37
  * @constructor
38
- * @version 0.3.29
38
+ * @version 0.3.31
39
39
  * @updated 2025-10-22
40
40
  *
41
41
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -432,7 +432,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
432
432
  _this4.componentTooltipManager.toggleIODeviceBinaryState(ioDeviceObject);
433
433
  }
434
434
  },
435
- onIODeviceDrag: function onIODeviceDrag(ioDeviceObject, signedDelta, isStart) {
435
+ onIODeviceDrag: function onIODeviceDrag(ioDeviceObject, signedDelta, isStart, hitMesh) {
436
436
  if (isStart) {
437
437
  var _ioDeviceObject$userD, _this4$managers$ioBeh, _this4$managers, _this4$managers2;
438
438
  // Resolve parentUuid by walking up to the host component.
@@ -459,7 +459,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
459
459
  }
460
460
  if (!_this4.componentTooltipManager) return;
461
461
  if (isStart) {
462
- _this4.componentTooltipManager.startIODeviceDrag(ioDeviceObject);
462
+ _this4.componentTooltipManager.startIODeviceDrag(ioDeviceObject, hitMesh);
463
463
  } else {
464
464
  _this4.componentTooltipManager.updateIODeviceDrag(signedDelta);
465
465
  }
@@ -70,6 +70,19 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
70
70
  if (!anims.length) return;
71
71
  var key = this._key(parentUuid, attachmentId);
72
72
  var entries = [];
73
+
74
+ // Capture the device root's world orientation once so each entry can
75
+ // convert the configured axis from device-local space to world space.
76
+ var deviceWorldQuat = new THREE__namespace.Quaternion();
77
+ deviceModelRoot.getWorldQuaternion(deviceWorldQuat);
78
+
79
+ // Compute the model's native max dimension so rotAxisOffset values (stored
80
+ // in dialog-viewer-world units, where the model is normalised to 1 unit)
81
+ // can be scaled to runtime-world units. viewerMaxDim = 1 / ns_viewer.
82
+ var _deviceBox = new THREE__namespace.Box3().setFromObject(deviceModelRoot);
83
+ var _deviceSize = new THREE__namespace.Vector3();
84
+ _deviceBox.getSize(_deviceSize);
85
+ var viewerMaxDim = Math.max(_deviceSize.x, _deviceSize.y, _deviceSize.z) || 1;
73
86
  var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(anims),
74
87
  _step;
75
88
  try {
@@ -80,11 +93,23 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
80
93
  console.warn("[IoBehaviorManager] Could not find mesh for animation \"".concat(anim.name || anim.stateVariable, "\" (uuid: ").concat(anim.meshUuid, ", name: \"").concat(anim.meshName, "\")"));
81
94
  continue;
82
95
  }
96
+ var worldPos = new THREE__namespace.Vector3();
97
+ mesh.getWorldPosition(worldPos);
98
+ var worldQuat = new THREE__namespace.Quaternion();
99
+ mesh.getWorldQuaternion(worldQuat);
100
+ var box = new THREE__namespace.Box3().setFromObject(mesh);
101
+ var worldCenter = new THREE__namespace.Vector3();
102
+ if (!box.isEmpty()) box.getCenter(worldCenter);else worldCenter.copy(worldPos);
83
103
  entries.push({
84
104
  anim: anim,
85
105
  mesh: mesh,
86
106
  origPos: mesh.position.clone(),
87
- origRot: mesh.rotation.clone()
107
+ origRot: mesh.rotation.clone(),
108
+ origWorldPos: worldPos,
109
+ origWorldQuat: worldQuat,
110
+ origWorldCenter: worldCenter,
111
+ deviceWorldQuat: deviceWorldQuat.clone(),
112
+ viewerMaxDim: viewerMaxDim
88
113
  });
89
114
  }
90
115
  } catch (err) {
@@ -114,16 +139,24 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
114
139
  }, {
115
140
  key: "triggerState",
116
141
  value: function triggerState(attachmentId, dataPointId, value, parentUuid) {
117
- var key = this._key(parentUuid, attachmentId);
118
- var entries = this._entries.get(key);
119
- if (!(entries !== null && entries !== void 0 && entries.length)) return;
120
- var _iterator2 = _rollupPluginBabelHelpers.createForOfIteratorHelper(entries),
142
+ var _iterator2 = _rollupPluginBabelHelpers.createForOfIteratorHelper(this._entries.values()),
121
143
  _step2;
122
144
  try {
123
145
  for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
124
- var entry = _step2.value;
125
- if (entry.anim.stateVariable !== dataPointId) continue;
126
- this._applyAnimation(entry, value);
146
+ var entries = _step2.value;
147
+ var _iterator3 = _rollupPluginBabelHelpers.createForOfIteratorHelper(entries),
148
+ _step3;
149
+ try {
150
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
151
+ var entry = _step3.value;
152
+ if (entry.anim.stateVariable !== dataPointId) continue;
153
+ this._applyAnimation(entry, value);
154
+ }
155
+ } catch (err) {
156
+ _iterator3.e(err);
157
+ } finally {
158
+ _iterator3.f();
159
+ }
127
160
  }
128
161
  } catch (err) {
129
162
  _iterator2.e(err);
@@ -158,34 +191,47 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
158
191
  }, {
159
192
  key: "getAnimationDataPoints",
160
193
  value: function getAnimationDataPoints(parentUuid, attachmentId) {
194
+ var _this2 = this;
195
+ var hitMesh = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
161
196
  var key = this._key(parentUuid, attachmentId);
162
197
  var entries = this._entries.get(key);
163
198
  if (!(entries !== null && entries !== void 0 && entries.length)) return [];
164
199
 
200
+ // When a specific mesh was clicked, filter to only animations whose target
201
+ // mesh is the clicked mesh or an ancestor of it (e.g. the clicked primitive
202
+ // is inside a group that is the animation target).
203
+ var filtered = entries;
204
+ if (hitMesh) {
205
+ var matching = entries.filter(function (e) {
206
+ return _this2._isMeshOrDescendant(hitMesh, e.mesh);
207
+ });
208
+ if (matching.length > 0) filtered = matching;
209
+ }
210
+
165
211
  // Collapse multiple mesh entries that share the same stateVariable
166
212
  var seen = new Map(); // stateVariable → anim
167
- var _iterator3 = _rollupPluginBabelHelpers.createForOfIteratorHelper(entries),
168
- _step3;
213
+ var _iterator4 = _rollupPluginBabelHelpers.createForOfIteratorHelper(filtered),
214
+ _step4;
169
215
  try {
170
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
171
- var anim = _step3.value.anim;
216
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
217
+ var anim = _step4.value.anim;
172
218
  if (!seen.has(anim.stateVariable)) {
173
219
  seen.set(anim.stateVariable, anim);
174
220
  }
175
221
  }
176
222
  } catch (err) {
177
- _iterator3.e(err);
223
+ _iterator4.e(err);
178
224
  } finally {
179
- _iterator3.f();
225
+ _iterator4.f();
180
226
  }
181
227
  var dps = [];
182
- var _iterator4 = _rollupPluginBabelHelpers.createForOfIteratorHelper(seen),
183
- _step4;
228
+ var _iterator5 = _rollupPluginBabelHelpers.createForOfIteratorHelper(seen),
229
+ _step5;
184
230
  try {
185
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
186
- var _step4$value = _rollupPluginBabelHelpers.slicedToArray(_step4.value, 2),
187
- stateVar = _step4$value[0],
188
- _anim = _step4$value[1];
231
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
232
+ var _step5$value = _rollupPluginBabelHelpers.slicedToArray(_step5.value, 2),
233
+ stateVar = _step5$value[0],
234
+ _anim = _step5$value[1];
189
235
  // Normalise stateType from AnimateDevicesDialog variants
190
236
  var stateType = void 0;
191
237
  var raw = (_anim.stateType || '').toLowerCase();
@@ -236,9 +282,9 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
236
282
  });
237
283
  }
238
284
  } catch (err) {
239
- _iterator4.e(err);
285
+ _iterator5.e(err);
240
286
  } finally {
241
- _iterator4.f();
287
+ _iterator5.f();
242
288
  }
243
289
  return dps;
244
290
  }
@@ -273,19 +319,19 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
273
319
  key: "unloadForComponent",
274
320
  value: function unloadForComponent(parentUuid) {
275
321
  var prefix = "".concat(parentUuid, "::");
276
- var _iterator5 = _rollupPluginBabelHelpers.createForOfIteratorHelper(this._entries.keys()),
277
- _step5;
322
+ var _iterator6 = _rollupPluginBabelHelpers.createForOfIteratorHelper(this._entries.keys()),
323
+ _step6;
278
324
  try {
279
- for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
280
- var key = _step5.value;
325
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
326
+ var key = _step6.value;
281
327
  if (key.startsWith(prefix)) {
282
328
  this._entries.delete(key);
283
329
  }
284
330
  }
285
331
  } catch (err) {
286
- _iterator5.e(err);
332
+ _iterator6.e(err);
287
333
  } finally {
288
- _iterator5.f();
334
+ _iterator6.f();
289
335
  }
290
336
  }
291
337
  }, {
@@ -304,6 +350,22 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
304
350
  return "".concat(parentUuid, "::").concat(attachmentId);
305
351
  }
306
352
 
353
+ /**
354
+ * Returns true if `candidate` is `ancestor` or any descendant of `ancestor`.
355
+ * @param {THREE.Object3D} candidate
356
+ * @param {THREE.Object3D} ancestor
357
+ */
358
+ }, {
359
+ key: "_isMeshOrDescendant",
360
+ value: function _isMeshOrDescendant(candidate, ancestor) {
361
+ var obj = candidate;
362
+ while (obj) {
363
+ if (obj === ancestor) return true;
364
+ obj = obj.parent;
365
+ }
366
+ return false;
367
+ }
368
+
307
369
  /**
308
370
  * Find the mesh inside `root` using UUID first, then name as fallback.
309
371
  * GLTFLoader assigns fresh UUIDs on every load, so name is the reliable key.
@@ -344,27 +406,32 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
344
406
  var anim = entry.anim,
345
407
  mesh = entry.mesh,
346
408
  origPos = entry.origPos,
347
- origRot = entry.origRot;
409
+ origRot = entry.origRot,
410
+ origWorldPos = entry.origWorldPos,
411
+ origWorldQuat = entry.origWorldQuat,
412
+ origWorldCenter = entry.origWorldCenter,
413
+ deviceWorldQuat = entry.deviceWorldQuat,
414
+ viewerMaxDim = entry.viewerMaxDim;
348
415
  var mapping = this._resolveMapping(anim, value);
349
416
  if (!mapping) return;
350
417
  var types = anim.transformTypes || [];
351
- var _iterator6 = _rollupPluginBabelHelpers.createForOfIteratorHelper(types),
352
- _step6;
418
+ var _iterator7 = _rollupPluginBabelHelpers.createForOfIteratorHelper(types),
419
+ _step7;
353
420
  try {
354
- for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
355
- var type = _step6.value;
421
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
422
+ var type = _step7.value;
356
423
  if (type === 'translation') {
357
424
  this._applyTranslation(mesh, origPos, mapping.transform);
358
425
  } else if (type === 'rotation') {
359
- this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform);
426
+ this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim);
360
427
  } else if (type === 'color') {
361
428
  this._applyColor(mesh, mapping.colorTransform);
362
429
  }
363
430
  }
364
431
  } catch (err) {
365
- _iterator6.e(err);
432
+ _iterator7.e(err);
366
433
  } finally {
367
- _iterator6.f();
434
+ _iterator7.f();
368
435
  }
369
436
  }
370
437
 
@@ -501,26 +568,24 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
501
568
  }
502
569
 
503
570
  /**
504
- * Apply a rotation around an arbitrary pivot point in device-local space,
505
- * optionally also displacing the mesh position to simulate orbital motion.
506
- *
507
- * Math (all in device-local space):
508
- * pivot = rotAxisOffset
509
- * delta = origPos - pivot
510
- * newDelta = rotate(delta, angle, axis)
511
- * newPos = pivot + newDelta
512
- * newRot[axis] = origRot[axis] + angle
571
+ * Apply a rotation around an arbitrary pivot point using world-space quaternion
572
+ * math matching the ReconstructionViewer's setMeshPreviewRotationAxis approach.
573
+ * This ensures the runtime axis/pivot matches what the user configured in the
574
+ * animation dialog, regardless of the device's parent transform in the scene.
513
575
  *
514
576
  * @param {THREE.Object3D} mesh
515
- * @param {THREE.Vector3} origPos
516
- * @param {THREE.Euler} origRot
577
+ * @param {THREE.Vector3} origPos - local position at load time (unused, kept for signature compat)
578
+ * @param {THREE.Euler} origRot - local rotation at load time (unused)
517
579
  * @param {Object} anim
518
- * @param {number} angleDeg - Degrees
580
+ * @param {number} angleDeg - Degrees
581
+ * @param {THREE.Vector3} origWorldPos - world position at load time
582
+ * @param {THREE.Quaternion} origWorldQuat - world quaternion at load time
583
+ * @param {THREE.Vector3} origWorldCenter - world bounding-box center at load time
519
584
  */
520
585
  }, {
521
586
  key: "_applyRotation",
522
- value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg) {
523
- var _anim$rotAxis, _anim$rotAxisOffset, _off$x, _off$y, _off$z;
587
+ value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim) {
588
+ var _anim$rotAxis, _anim$rotAxisOffset;
524
589
  var angle = THREE__namespace.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
525
590
  var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
526
591
  var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
@@ -529,13 +594,57 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
529
594
  z: 0
530
595
  };
531
596
 
532
- // Unit vector for the chosen axis
533
- var axisVec = new THREE__namespace.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
534
- var pivot = new THREE__namespace.Vector3((_off$x = off.x) !== null && _off$x !== void 0 ? _off$x : 0, (_off$y = off.y) !== null && _off$y !== void 0 ? _off$y : 0, (_off$z = off.z) !== null && _off$z !== void 0 ? _off$z : 0);
535
- var delta = origPos.clone().sub(pivot);
536
- delta.applyAxisAngle(axisVec, angle);
537
- mesh.position.copy(pivot).add(delta);
538
- mesh.rotation[axis] = origRot[axis] + angle;
597
+ // Local axis in the device's coordinate system
598
+ var localAxisVec = new THREE__namespace.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
599
+ if (origWorldPos && origWorldQuat && origWorldCenter) {
600
+ // Transform the configured axis from device-local space into world space
601
+ // so that 'X' means the device's local X, not the world X.
602
+ var worldAxisVec = localAxisVec.clone().applyQuaternion(deviceWorldQuat || new THREE__namespace.Quaternion());
603
+
604
+ // rotAxisOffset is stored in dialog-viewer-world space where the model root
605
+ // has been rotated Rot_Z(π) by default (ReconstructionViewer's else branch).
606
+ // Translation uses the same convention and negates X/Y to compensate.
607
+ // For rotation offset we apply the same compensation mathematically:
608
+ // offset_runtime = deviceWorldQuat × Rot_Z(π) × offset_dialog × scale
609
+ // This maps from dialog world space (post Z+180° flip) → model local → world.
610
+ var scale = viewerMaxDim || 1;
611
+ var rotZ180 = new THREE__namespace.Quaternion().setFromAxisAngle(new THREE__namespace.Vector3(0, 0, 1), Math.PI);
612
+ var qCombined = (deviceWorldQuat || new THREE__namespace.Quaternion()).clone().multiply(rotZ180);
613
+ var offWorld = new THREE__namespace.Vector3((off.x || 0) * scale, (off.y || 0) * scale, (off.z || 0) * scale).applyQuaternion(qCombined);
614
+ var pivot = origWorldCenter.clone().add(offWorld);
615
+ var deltaQuat = new THREE__namespace.Quaternion().setFromAxisAngle(worldAxisVec, angle);
616
+
617
+ // Rotate world position around pivot
618
+ var offsetVec = origWorldPos.clone().sub(pivot);
619
+ offsetVec.applyQuaternion(deltaQuat);
620
+ var newWorldPos = pivot.clone().add(offsetVec);
621
+
622
+ // Compose world quaternion
623
+ var newWorldQuat = deltaQuat.clone().multiply(origWorldQuat);
624
+
625
+ // Convert world position → parent local space
626
+ if (mesh.parent) {
627
+ mesh.parent.worldToLocal(newWorldPos);
628
+ }
629
+ mesh.position.copy(newWorldPos);
630
+
631
+ // Convert quaternion → parent local space
632
+ if (mesh.parent) {
633
+ var parentWorldQuat = new THREE__namespace.Quaternion();
634
+ mesh.parent.getWorldQuaternion(parentWorldQuat);
635
+ parentWorldQuat.invert();
636
+ newWorldQuat.premultiply(parentWorldQuat);
637
+ }
638
+ mesh.quaternion.copy(newWorldQuat);
639
+ } else {
640
+ var _off$x, _off$y, _off$z;
641
+ // Fallback for entries loaded without world data
642
+ var _pivot = new THREE__namespace.Vector3((_off$x = off.x) !== null && _off$x !== void 0 ? _off$x : 0, (_off$y = off.y) !== null && _off$y !== void 0 ? _off$y : 0, (_off$z = off.z) !== null && _off$z !== void 0 ? _off$z : 0);
643
+ var delta = origPos.clone().sub(_pivot);
644
+ delta.applyAxisAngle(localAxisVec, angle);
645
+ mesh.position.copy(_pivot).add(delta);
646
+ mesh.rotation[axis] = origRot[axis] + angle;
647
+ }
539
648
  }
540
649
 
541
650
  /**
@@ -469,6 +469,7 @@ var TransformControlsManager = /*#__PURE__*/function () {
469
469
  raycaster.setFromCamera(mouse, _this4.camera);
470
470
  var allIntersects = raycaster.intersectObjects(_this4.scene.children, true);
471
471
  var ioDeviceObject = null;
472
+ var hitMesh = null;
472
473
  var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(allIntersects),
473
474
  _step;
474
475
  try {
@@ -483,7 +484,10 @@ var TransformControlsManager = /*#__PURE__*/function () {
483
484
  }
484
485
  obj = obj.parent;
485
486
  }
486
- if (ioDeviceObject) break;
487
+ if (ioDeviceObject) {
488
+ hitMesh = hit.object;
489
+ break;
490
+ }
487
491
  }
488
492
  } catch (err) {
489
493
  _iterator.e(err);
@@ -498,7 +502,7 @@ var TransformControlsManager = /*#__PURE__*/function () {
498
502
  _this4._ioDragStartY = event.clientY;
499
503
  _this4._ioDragMoved = false;
500
504
  if (_this4.orbitControls) _this4.orbitControls.enabled = false;
501
- _this4.callbacks.onIODeviceDrag(ioDeviceObject, 0, true);
505
+ _this4.callbacks.onIODeviceDrag(ioDeviceObject, 0, true, hitMesh);
502
506
  var onMove = function onMove(e) {
503
507
  var dx = e.clientX - _this4._ioDragStartX;
504
508
  var dy = e.clientY - _this4._ioDragStartY;
@@ -178,7 +178,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
178
178
  */
179
179
  }, {
180
180
  key: "startIODeviceDrag",
181
- value: function startIODeviceDrag(ioDeviceObject) {
181
+ value: function startIODeviceDrag(ioDeviceObject, hitMesh) {
182
182
  var _this$sceneViewer2,
183
183
  _this2 = this;
184
184
  if (!ioDeviceObject || !this._stateAdapter) return;
@@ -197,7 +197,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
197
197
  }
198
198
  var scopedAttachmentId = this._getScopedAttachmentKey(attachmentId, parentUuid);
199
199
  var ioBehavMgr = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.managers) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.ioBehaviorManager;
200
- var dataPoints = ((ioBehavMgr === null || ioBehavMgr === void 0 ? void 0 : ioBehavMgr.getAnimationDataPoints(parentUuid, attachmentId)) || []).concat((ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [])
200
+ var dataPoints = ((ioBehavMgr === null || ioBehavMgr === void 0 ? void 0 : ioBehavMgr.getAnimationDataPoints(parentUuid, attachmentId, hitMesh)) || []).concat((ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [])
201
201
  // deduplicate by id
202
202
  .filter(function (dp, i, arr) {
203
203
  return arr.findIndex(function (d) {
@@ -31,7 +31,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
31
31
  * Initialize the CentralPlant manager
32
32
  *
33
33
  * @constructor
34
- * @version 0.3.29
34
+ * @version 0.3.31
35
35
  * @updated 2025-10-22
36
36
  *
37
37
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -428,7 +428,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
428
428
  _this4.componentTooltipManager.toggleIODeviceBinaryState(ioDeviceObject);
429
429
  }
430
430
  },
431
- onIODeviceDrag: function onIODeviceDrag(ioDeviceObject, signedDelta, isStart) {
431
+ onIODeviceDrag: function onIODeviceDrag(ioDeviceObject, signedDelta, isStart, hitMesh) {
432
432
  if (isStart) {
433
433
  var _ioDeviceObject$userD, _this4$managers$ioBeh, _this4$managers, _this4$managers2;
434
434
  // Resolve parentUuid by walking up to the host component.
@@ -455,7 +455,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
455
455
  }
456
456
  if (!_this4.componentTooltipManager) return;
457
457
  if (isStart) {
458
- _this4.componentTooltipManager.startIODeviceDrag(ioDeviceObject);
458
+ _this4.componentTooltipManager.startIODeviceDrag(ioDeviceObject, hitMesh);
459
459
  } else {
460
460
  _this4.componentTooltipManager.updateIODeviceDrag(signedDelta);
461
461
  }
@@ -46,6 +46,19 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
46
46
  if (!anims.length) return;
47
47
  var key = this._key(parentUuid, attachmentId);
48
48
  var entries = [];
49
+
50
+ // Capture the device root's world orientation once so each entry can
51
+ // convert the configured axis from device-local space to world space.
52
+ var deviceWorldQuat = new THREE.Quaternion();
53
+ deviceModelRoot.getWorldQuaternion(deviceWorldQuat);
54
+
55
+ // Compute the model's native max dimension so rotAxisOffset values (stored
56
+ // in dialog-viewer-world units, where the model is normalised to 1 unit)
57
+ // can be scaled to runtime-world units. viewerMaxDim = 1 / ns_viewer.
58
+ var _deviceBox = new THREE.Box3().setFromObject(deviceModelRoot);
59
+ var _deviceSize = new THREE.Vector3();
60
+ _deviceBox.getSize(_deviceSize);
61
+ var viewerMaxDim = Math.max(_deviceSize.x, _deviceSize.y, _deviceSize.z) || 1;
49
62
  var _iterator = _createForOfIteratorHelper(anims),
50
63
  _step;
51
64
  try {
@@ -56,11 +69,23 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
56
69
  console.warn("[IoBehaviorManager] Could not find mesh for animation \"".concat(anim.name || anim.stateVariable, "\" (uuid: ").concat(anim.meshUuid, ", name: \"").concat(anim.meshName, "\")"));
57
70
  continue;
58
71
  }
72
+ var worldPos = new THREE.Vector3();
73
+ mesh.getWorldPosition(worldPos);
74
+ var worldQuat = new THREE.Quaternion();
75
+ mesh.getWorldQuaternion(worldQuat);
76
+ var box = new THREE.Box3().setFromObject(mesh);
77
+ var worldCenter = new THREE.Vector3();
78
+ if (!box.isEmpty()) box.getCenter(worldCenter);else worldCenter.copy(worldPos);
59
79
  entries.push({
60
80
  anim: anim,
61
81
  mesh: mesh,
62
82
  origPos: mesh.position.clone(),
63
- origRot: mesh.rotation.clone()
83
+ origRot: mesh.rotation.clone(),
84
+ origWorldPos: worldPos,
85
+ origWorldQuat: worldQuat,
86
+ origWorldCenter: worldCenter,
87
+ deviceWorldQuat: deviceWorldQuat.clone(),
88
+ viewerMaxDim: viewerMaxDim
64
89
  });
65
90
  }
66
91
  } catch (err) {
@@ -90,16 +115,24 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
90
115
  }, {
91
116
  key: "triggerState",
92
117
  value: function triggerState(attachmentId, dataPointId, value, parentUuid) {
93
- var key = this._key(parentUuid, attachmentId);
94
- var entries = this._entries.get(key);
95
- if (!(entries !== null && entries !== void 0 && entries.length)) return;
96
- var _iterator2 = _createForOfIteratorHelper(entries),
118
+ var _iterator2 = _createForOfIteratorHelper(this._entries.values()),
97
119
  _step2;
98
120
  try {
99
121
  for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
100
- var entry = _step2.value;
101
- if (entry.anim.stateVariable !== dataPointId) continue;
102
- this._applyAnimation(entry, value);
122
+ var entries = _step2.value;
123
+ var _iterator3 = _createForOfIteratorHelper(entries),
124
+ _step3;
125
+ try {
126
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
127
+ var entry = _step3.value;
128
+ if (entry.anim.stateVariable !== dataPointId) continue;
129
+ this._applyAnimation(entry, value);
130
+ }
131
+ } catch (err) {
132
+ _iterator3.e(err);
133
+ } finally {
134
+ _iterator3.f();
135
+ }
103
136
  }
104
137
  } catch (err) {
105
138
  _iterator2.e(err);
@@ -134,34 +167,47 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
134
167
  }, {
135
168
  key: "getAnimationDataPoints",
136
169
  value: function getAnimationDataPoints(parentUuid, attachmentId) {
170
+ var _this2 = this;
171
+ var hitMesh = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
137
172
  var key = this._key(parentUuid, attachmentId);
138
173
  var entries = this._entries.get(key);
139
174
  if (!(entries !== null && entries !== void 0 && entries.length)) return [];
140
175
 
176
+ // When a specific mesh was clicked, filter to only animations whose target
177
+ // mesh is the clicked mesh or an ancestor of it (e.g. the clicked primitive
178
+ // is inside a group that is the animation target).
179
+ var filtered = entries;
180
+ if (hitMesh) {
181
+ var matching = entries.filter(function (e) {
182
+ return _this2._isMeshOrDescendant(hitMesh, e.mesh);
183
+ });
184
+ if (matching.length > 0) filtered = matching;
185
+ }
186
+
141
187
  // Collapse multiple mesh entries that share the same stateVariable
142
188
  var seen = new Map(); // stateVariable → anim
143
- var _iterator3 = _createForOfIteratorHelper(entries),
144
- _step3;
189
+ var _iterator4 = _createForOfIteratorHelper(filtered),
190
+ _step4;
145
191
  try {
146
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
147
- var anim = _step3.value.anim;
192
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
193
+ var anim = _step4.value.anim;
148
194
  if (!seen.has(anim.stateVariable)) {
149
195
  seen.set(anim.stateVariable, anim);
150
196
  }
151
197
  }
152
198
  } catch (err) {
153
- _iterator3.e(err);
199
+ _iterator4.e(err);
154
200
  } finally {
155
- _iterator3.f();
201
+ _iterator4.f();
156
202
  }
157
203
  var dps = [];
158
- var _iterator4 = _createForOfIteratorHelper(seen),
159
- _step4;
204
+ var _iterator5 = _createForOfIteratorHelper(seen),
205
+ _step5;
160
206
  try {
161
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
162
- var _step4$value = _slicedToArray(_step4.value, 2),
163
- stateVar = _step4$value[0],
164
- _anim = _step4$value[1];
207
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
208
+ var _step5$value = _slicedToArray(_step5.value, 2),
209
+ stateVar = _step5$value[0],
210
+ _anim = _step5$value[1];
165
211
  // Normalise stateType from AnimateDevicesDialog variants
166
212
  var stateType = void 0;
167
213
  var raw = (_anim.stateType || '').toLowerCase();
@@ -212,9 +258,9 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
212
258
  });
213
259
  }
214
260
  } catch (err) {
215
- _iterator4.e(err);
261
+ _iterator5.e(err);
216
262
  } finally {
217
- _iterator4.f();
263
+ _iterator5.f();
218
264
  }
219
265
  return dps;
220
266
  }
@@ -249,19 +295,19 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
249
295
  key: "unloadForComponent",
250
296
  value: function unloadForComponent(parentUuid) {
251
297
  var prefix = "".concat(parentUuid, "::");
252
- var _iterator5 = _createForOfIteratorHelper(this._entries.keys()),
253
- _step5;
298
+ var _iterator6 = _createForOfIteratorHelper(this._entries.keys()),
299
+ _step6;
254
300
  try {
255
- for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
256
- var key = _step5.value;
301
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
302
+ var key = _step6.value;
257
303
  if (key.startsWith(prefix)) {
258
304
  this._entries.delete(key);
259
305
  }
260
306
  }
261
307
  } catch (err) {
262
- _iterator5.e(err);
308
+ _iterator6.e(err);
263
309
  } finally {
264
- _iterator5.f();
310
+ _iterator6.f();
265
311
  }
266
312
  }
267
313
  }, {
@@ -280,6 +326,22 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
280
326
  return "".concat(parentUuid, "::").concat(attachmentId);
281
327
  }
282
328
 
329
+ /**
330
+ * Returns true if `candidate` is `ancestor` or any descendant of `ancestor`.
331
+ * @param {THREE.Object3D} candidate
332
+ * @param {THREE.Object3D} ancestor
333
+ */
334
+ }, {
335
+ key: "_isMeshOrDescendant",
336
+ value: function _isMeshOrDescendant(candidate, ancestor) {
337
+ var obj = candidate;
338
+ while (obj) {
339
+ if (obj === ancestor) return true;
340
+ obj = obj.parent;
341
+ }
342
+ return false;
343
+ }
344
+
283
345
  /**
284
346
  * Find the mesh inside `root` using UUID first, then name as fallback.
285
347
  * GLTFLoader assigns fresh UUIDs on every load, so name is the reliable key.
@@ -320,27 +382,32 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
320
382
  var anim = entry.anim,
321
383
  mesh = entry.mesh,
322
384
  origPos = entry.origPos,
323
- origRot = entry.origRot;
385
+ origRot = entry.origRot,
386
+ origWorldPos = entry.origWorldPos,
387
+ origWorldQuat = entry.origWorldQuat,
388
+ origWorldCenter = entry.origWorldCenter,
389
+ deviceWorldQuat = entry.deviceWorldQuat,
390
+ viewerMaxDim = entry.viewerMaxDim;
324
391
  var mapping = this._resolveMapping(anim, value);
325
392
  if (!mapping) return;
326
393
  var types = anim.transformTypes || [];
327
- var _iterator6 = _createForOfIteratorHelper(types),
328
- _step6;
394
+ var _iterator7 = _createForOfIteratorHelper(types),
395
+ _step7;
329
396
  try {
330
- for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
331
- var type = _step6.value;
397
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
398
+ var type = _step7.value;
332
399
  if (type === 'translation') {
333
400
  this._applyTranslation(mesh, origPos, mapping.transform);
334
401
  } else if (type === 'rotation') {
335
- this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform);
402
+ this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim);
336
403
  } else if (type === 'color') {
337
404
  this._applyColor(mesh, mapping.colorTransform);
338
405
  }
339
406
  }
340
407
  } catch (err) {
341
- _iterator6.e(err);
408
+ _iterator7.e(err);
342
409
  } finally {
343
- _iterator6.f();
410
+ _iterator7.f();
344
411
  }
345
412
  }
346
413
 
@@ -477,26 +544,24 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
477
544
  }
478
545
 
479
546
  /**
480
- * Apply a rotation around an arbitrary pivot point in device-local space,
481
- * optionally also displacing the mesh position to simulate orbital motion.
482
- *
483
- * Math (all in device-local space):
484
- * pivot = rotAxisOffset
485
- * delta = origPos - pivot
486
- * newDelta = rotate(delta, angle, axis)
487
- * newPos = pivot + newDelta
488
- * newRot[axis] = origRot[axis] + angle
547
+ * Apply a rotation around an arbitrary pivot point using world-space quaternion
548
+ * math matching the ReconstructionViewer's setMeshPreviewRotationAxis approach.
549
+ * This ensures the runtime axis/pivot matches what the user configured in the
550
+ * animation dialog, regardless of the device's parent transform in the scene.
489
551
  *
490
552
  * @param {THREE.Object3D} mesh
491
- * @param {THREE.Vector3} origPos
492
- * @param {THREE.Euler} origRot
553
+ * @param {THREE.Vector3} origPos - local position at load time (unused, kept for signature compat)
554
+ * @param {THREE.Euler} origRot - local rotation at load time (unused)
493
555
  * @param {Object} anim
494
- * @param {number} angleDeg - Degrees
556
+ * @param {number} angleDeg - Degrees
557
+ * @param {THREE.Vector3} origWorldPos - world position at load time
558
+ * @param {THREE.Quaternion} origWorldQuat - world quaternion at load time
559
+ * @param {THREE.Vector3} origWorldCenter - world bounding-box center at load time
495
560
  */
496
561
  }, {
497
562
  key: "_applyRotation",
498
- value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg) {
499
- var _anim$rotAxis, _anim$rotAxisOffset, _off$x, _off$y, _off$z;
563
+ value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim) {
564
+ var _anim$rotAxis, _anim$rotAxisOffset;
500
565
  var angle = THREE.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
501
566
  var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
502
567
  var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
@@ -505,13 +570,57 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
505
570
  z: 0
506
571
  };
507
572
 
508
- // Unit vector for the chosen axis
509
- var axisVec = new THREE.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
510
- var pivot = new THREE.Vector3((_off$x = off.x) !== null && _off$x !== void 0 ? _off$x : 0, (_off$y = off.y) !== null && _off$y !== void 0 ? _off$y : 0, (_off$z = off.z) !== null && _off$z !== void 0 ? _off$z : 0);
511
- var delta = origPos.clone().sub(pivot);
512
- delta.applyAxisAngle(axisVec, angle);
513
- mesh.position.copy(pivot).add(delta);
514
- mesh.rotation[axis] = origRot[axis] + angle;
573
+ // Local axis in the device's coordinate system
574
+ var localAxisVec = new THREE.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
575
+ if (origWorldPos && origWorldQuat && origWorldCenter) {
576
+ // Transform the configured axis from device-local space into world space
577
+ // so that 'X' means the device's local X, not the world X.
578
+ var worldAxisVec = localAxisVec.clone().applyQuaternion(deviceWorldQuat || new THREE.Quaternion());
579
+
580
+ // rotAxisOffset is stored in dialog-viewer-world space where the model root
581
+ // has been rotated Rot_Z(π) by default (ReconstructionViewer's else branch).
582
+ // Translation uses the same convention and negates X/Y to compensate.
583
+ // For rotation offset we apply the same compensation mathematically:
584
+ // offset_runtime = deviceWorldQuat × Rot_Z(π) × offset_dialog × scale
585
+ // This maps from dialog world space (post Z+180° flip) → model local → world.
586
+ var scale = viewerMaxDim || 1;
587
+ var rotZ180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI);
588
+ var qCombined = (deviceWorldQuat || new THREE.Quaternion()).clone().multiply(rotZ180);
589
+ var offWorld = new THREE.Vector3((off.x || 0) * scale, (off.y || 0) * scale, (off.z || 0) * scale).applyQuaternion(qCombined);
590
+ var pivot = origWorldCenter.clone().add(offWorld);
591
+ var deltaQuat = new THREE.Quaternion().setFromAxisAngle(worldAxisVec, angle);
592
+
593
+ // Rotate world position around pivot
594
+ var offsetVec = origWorldPos.clone().sub(pivot);
595
+ offsetVec.applyQuaternion(deltaQuat);
596
+ var newWorldPos = pivot.clone().add(offsetVec);
597
+
598
+ // Compose world quaternion
599
+ var newWorldQuat = deltaQuat.clone().multiply(origWorldQuat);
600
+
601
+ // Convert world position → parent local space
602
+ if (mesh.parent) {
603
+ mesh.parent.worldToLocal(newWorldPos);
604
+ }
605
+ mesh.position.copy(newWorldPos);
606
+
607
+ // Convert quaternion → parent local space
608
+ if (mesh.parent) {
609
+ var parentWorldQuat = new THREE.Quaternion();
610
+ mesh.parent.getWorldQuaternion(parentWorldQuat);
611
+ parentWorldQuat.invert();
612
+ newWorldQuat.premultiply(parentWorldQuat);
613
+ }
614
+ mesh.quaternion.copy(newWorldQuat);
615
+ } else {
616
+ var _off$x, _off$y, _off$z;
617
+ // Fallback for entries loaded without world data
618
+ var _pivot = new THREE.Vector3((_off$x = off.x) !== null && _off$x !== void 0 ? _off$x : 0, (_off$y = off.y) !== null && _off$y !== void 0 ? _off$y : 0, (_off$z = off.z) !== null && _off$z !== void 0 ? _off$z : 0);
619
+ var delta = origPos.clone().sub(_pivot);
620
+ delta.applyAxisAngle(localAxisVec, angle);
621
+ mesh.position.copy(_pivot).add(delta);
622
+ mesh.rotation[axis] = origRot[axis] + angle;
623
+ }
515
624
  }
516
625
 
517
626
  /**
@@ -445,6 +445,7 @@ var TransformControlsManager = /*#__PURE__*/function () {
445
445
  raycaster.setFromCamera(mouse, _this4.camera);
446
446
  var allIntersects = raycaster.intersectObjects(_this4.scene.children, true);
447
447
  var ioDeviceObject = null;
448
+ var hitMesh = null;
448
449
  var _iterator = _createForOfIteratorHelper(allIntersects),
449
450
  _step;
450
451
  try {
@@ -459,7 +460,10 @@ var TransformControlsManager = /*#__PURE__*/function () {
459
460
  }
460
461
  obj = obj.parent;
461
462
  }
462
- if (ioDeviceObject) break;
463
+ if (ioDeviceObject) {
464
+ hitMesh = hit.object;
465
+ break;
466
+ }
463
467
  }
464
468
  } catch (err) {
465
469
  _iterator.e(err);
@@ -474,7 +478,7 @@ var TransformControlsManager = /*#__PURE__*/function () {
474
478
  _this4._ioDragStartY = event.clientY;
475
479
  _this4._ioDragMoved = false;
476
480
  if (_this4.orbitControls) _this4.orbitControls.enabled = false;
477
- _this4.callbacks.onIODeviceDrag(ioDeviceObject, 0, true);
481
+ _this4.callbacks.onIODeviceDrag(ioDeviceObject, 0, true, hitMesh);
478
482
  var onMove = function onMove(e) {
479
483
  var dx = e.clientX - _this4._ioDragStartX;
480
484
  var dy = e.clientY - _this4._ioDragStartY;
@@ -154,7 +154,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
154
154
  */
155
155
  }, {
156
156
  key: "startIODeviceDrag",
157
- value: function startIODeviceDrag(ioDeviceObject) {
157
+ value: function startIODeviceDrag(ioDeviceObject, hitMesh) {
158
158
  var _this$sceneViewer2,
159
159
  _this2 = this;
160
160
  if (!ioDeviceObject || !this._stateAdapter) return;
@@ -173,7 +173,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
173
173
  }
174
174
  var scopedAttachmentId = this._getScopedAttachmentKey(attachmentId, parentUuid);
175
175
  var ioBehavMgr = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.managers) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.ioBehaviorManager;
176
- var dataPoints = ((ioBehavMgr === null || ioBehavMgr === void 0 ? void 0 : ioBehavMgr.getAnimationDataPoints(parentUuid, attachmentId)) || []).concat((ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [])
176
+ var dataPoints = ((ioBehavMgr === null || ioBehavMgr === void 0 ? void 0 : ioBehavMgr.getAnimationDataPoints(parentUuid, attachmentId, hitMesh)) || []).concat((ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [])
177
177
  // deduplicate by id
178
178
  .filter(function (dp, i, arr) {
179
179
  return arr.findIndex(function (d) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2112-lab/central-plant",
3
- "version": "0.3.29",
3
+ "version": "0.3.31",
4
4
  "description": "Utility modules for the Central Plant Application",
5
5
  "main": "dist/bundle/index.js",
6
6
  "module": "dist/esm/src/index.js",