@2112-lab/central-plant 0.3.29 → 0.3.30

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) {
@@ -37489,13 +37518,26 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37489
37518
  }, {
37490
37519
  key: "getAnimationDataPoints",
37491
37520
  value: function getAnimationDataPoints(parentUuid, attachmentId) {
37521
+ var _this2 = this;
37522
+ var hitMesh = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
37492
37523
  var key = this._key(parentUuid, attachmentId);
37493
37524
  var entries = this._entries.get(key);
37494
37525
  if (!(entries !== null && entries !== void 0 && entries.length)) return [];
37495
37526
 
37527
+ // When a specific mesh was clicked, filter to only animations whose target
37528
+ // mesh is the clicked mesh or an ancestor of it (e.g. the clicked primitive
37529
+ // is inside a group that is the animation target).
37530
+ var filtered = entries;
37531
+ if (hitMesh) {
37532
+ var matching = entries.filter(function (e) {
37533
+ return _this2._isMeshOrDescendant(hitMesh, e.mesh);
37534
+ });
37535
+ if (matching.length > 0) filtered = matching;
37536
+ }
37537
+
37496
37538
  // Collapse multiple mesh entries that share the same stateVariable
37497
37539
  var seen = new Map(); // stateVariable → anim
37498
- var _iterator3 = _createForOfIteratorHelper(entries),
37540
+ var _iterator3 = _createForOfIteratorHelper(filtered),
37499
37541
  _step3;
37500
37542
  try {
37501
37543
  for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
@@ -37635,6 +37677,22 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37635
37677
  return "".concat(parentUuid, "::").concat(attachmentId);
37636
37678
  }
37637
37679
 
37680
+ /**
37681
+ * Returns true if `candidate` is `ancestor` or any descendant of `ancestor`.
37682
+ * @param {THREE.Object3D} candidate
37683
+ * @param {THREE.Object3D} ancestor
37684
+ */
37685
+ }, {
37686
+ key: "_isMeshOrDescendant",
37687
+ value: function _isMeshOrDescendant(candidate, ancestor) {
37688
+ var obj = candidate;
37689
+ while (obj) {
37690
+ if (obj === ancestor) return true;
37691
+ obj = obj.parent;
37692
+ }
37693
+ return false;
37694
+ }
37695
+
37638
37696
  /**
37639
37697
  * Find the mesh inside `root` using UUID first, then name as fallback.
37640
37698
  * GLTFLoader assigns fresh UUIDs on every load, so name is the reliable key.
@@ -37675,7 +37733,12 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37675
37733
  var anim = entry.anim,
37676
37734
  mesh = entry.mesh,
37677
37735
  origPos = entry.origPos,
37678
- origRot = entry.origRot;
37736
+ origRot = entry.origRot,
37737
+ origWorldPos = entry.origWorldPos,
37738
+ origWorldQuat = entry.origWorldQuat,
37739
+ origWorldCenter = entry.origWorldCenter,
37740
+ deviceWorldQuat = entry.deviceWorldQuat,
37741
+ viewerMaxDim = entry.viewerMaxDim;
37679
37742
  var mapping = this._resolveMapping(anim, value);
37680
37743
  if (!mapping) return;
37681
37744
  var types = anim.transformTypes || [];
@@ -37687,7 +37750,7 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37687
37750
  if (type === 'translation') {
37688
37751
  this._applyTranslation(mesh, origPos, mapping.transform);
37689
37752
  } else if (type === 'rotation') {
37690
- this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform);
37753
+ this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim);
37691
37754
  } else if (type === 'color') {
37692
37755
  this._applyColor(mesh, mapping.colorTransform);
37693
37756
  }
@@ -37832,26 +37895,24 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37832
37895
  }
37833
37896
 
37834
37897
  /**
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
37898
+ * Apply a rotation around an arbitrary pivot point using world-space quaternion
37899
+ * math matching the ReconstructionViewer's setMeshPreviewRotationAxis approach.
37900
+ * This ensures the runtime axis/pivot matches what the user configured in the
37901
+ * animation dialog, regardless of the device's parent transform in the scene.
37844
37902
  *
37845
37903
  * @param {THREE.Object3D} mesh
37846
- * @param {THREE.Vector3} origPos
37847
- * @param {THREE.Euler} origRot
37904
+ * @param {THREE.Vector3} origPos - local position at load time (unused, kept for signature compat)
37905
+ * @param {THREE.Euler} origRot - local rotation at load time (unused)
37848
37906
  * @param {Object} anim
37849
- * @param {number} angleDeg - Degrees
37907
+ * @param {number} angleDeg - Degrees
37908
+ * @param {THREE.Vector3} origWorldPos - world position at load time
37909
+ * @param {THREE.Quaternion} origWorldQuat - world quaternion at load time
37910
+ * @param {THREE.Vector3} origWorldCenter - world bounding-box center at load time
37850
37911
  */
37851
37912
  }, {
37852
37913
  key: "_applyRotation",
37853
- value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg) {
37854
- var _anim$rotAxis, _anim$rotAxisOffset, _off$x, _off$y, _off$z;
37914
+ value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim) {
37915
+ var _anim$rotAxis, _anim$rotAxisOffset;
37855
37916
  var angle = THREE__namespace.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
37856
37917
  var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
37857
37918
  var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
@@ -37860,13 +37921,57 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37860
37921
  z: 0
37861
37922
  };
37862
37923
 
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;
37924
+ // Local axis in the device's coordinate system
37925
+ var localAxisVec = new THREE__namespace.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
37926
+ if (origWorldPos && origWorldQuat && origWorldCenter) {
37927
+ // Transform the configured axis from device-local space into world space
37928
+ // so that 'X' means the device's local X, not the world X.
37929
+ var worldAxisVec = localAxisVec.clone().applyQuaternion(deviceWorldQuat || new THREE__namespace.Quaternion());
37930
+
37931
+ // rotAxisOffset is stored in dialog-viewer-world space where the model root
37932
+ // has been rotated Rot_Z(π) by default (ReconstructionViewer's else branch).
37933
+ // Translation uses the same convention and negates X/Y to compensate.
37934
+ // For rotation offset we apply the same compensation mathematically:
37935
+ // offset_runtime = deviceWorldQuat × Rot_Z(π) × offset_dialog × scale
37936
+ // This maps from dialog world space (post Z+180° flip) → model local → world.
37937
+ var scale = viewerMaxDim || 1;
37938
+ var rotZ180 = new THREE__namespace.Quaternion().setFromAxisAngle(new THREE__namespace.Vector3(0, 0, 1), Math.PI);
37939
+ var qCombined = (deviceWorldQuat || new THREE__namespace.Quaternion()).clone().multiply(rotZ180);
37940
+ var offWorld = new THREE__namespace.Vector3((off.x || 0) * scale, (off.y || 0) * scale, (off.z || 0) * scale).applyQuaternion(qCombined);
37941
+ var pivot = origWorldCenter.clone().add(offWorld);
37942
+ var deltaQuat = new THREE__namespace.Quaternion().setFromAxisAngle(worldAxisVec, angle);
37943
+
37944
+ // Rotate world position around pivot
37945
+ var offsetVec = origWorldPos.clone().sub(pivot);
37946
+ offsetVec.applyQuaternion(deltaQuat);
37947
+ var newWorldPos = pivot.clone().add(offsetVec);
37948
+
37949
+ // Compose world quaternion
37950
+ var newWorldQuat = deltaQuat.clone().multiply(origWorldQuat);
37951
+
37952
+ // Convert world position → parent local space
37953
+ if (mesh.parent) {
37954
+ mesh.parent.worldToLocal(newWorldPos);
37955
+ }
37956
+ mesh.position.copy(newWorldPos);
37957
+
37958
+ // Convert quaternion → parent local space
37959
+ if (mesh.parent) {
37960
+ var parentWorldQuat = new THREE__namespace.Quaternion();
37961
+ mesh.parent.getWorldQuaternion(parentWorldQuat);
37962
+ parentWorldQuat.invert();
37963
+ newWorldQuat.premultiply(parentWorldQuat);
37964
+ }
37965
+ mesh.quaternion.copy(newWorldQuat);
37966
+ } else {
37967
+ var _off$x, _off$y, _off$z;
37968
+ // Fallback for entries loaded without world data
37969
+ 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);
37970
+ var delta = origPos.clone().sub(_pivot);
37971
+ delta.applyAxisAngle(localAxisVec, angle);
37972
+ mesh.position.copy(_pivot).add(delta);
37973
+ mesh.rotation[axis] = origRot[axis] + angle;
37974
+ }
37870
37975
  }
37871
37976
 
37872
37977
  /**
@@ -39550,7 +39655,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
39550
39655
  * Initialize the CentralPlant manager
39551
39656
  *
39552
39657
  * @constructor
39553
- * @version 0.3.29
39658
+ * @version 0.3.30
39554
39659
  * @updated 2025-10-22
39555
39660
  *
39556
39661
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -42790,7 +42895,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
42790
42895
  _this4.componentTooltipManager.toggleIODeviceBinaryState(ioDeviceObject);
42791
42896
  }
42792
42897
  },
42793
- onIODeviceDrag: function onIODeviceDrag(ioDeviceObject, signedDelta, isStart) {
42898
+ onIODeviceDrag: function onIODeviceDrag(ioDeviceObject, signedDelta, isStart, hitMesh) {
42794
42899
  if (isStart) {
42795
42900
  var _ioDeviceObject$userD, _this4$managers$ioBeh, _this4$managers, _this4$managers2;
42796
42901
  // Resolve parentUuid by walking up to the host component.
@@ -42817,7 +42922,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
42817
42922
  }
42818
42923
  if (!_this4.componentTooltipManager) return;
42819
42924
  if (isStart) {
42820
- _this4.componentTooltipManager.startIODeviceDrag(ioDeviceObject);
42925
+ _this4.componentTooltipManager.startIODeviceDrag(ioDeviceObject, hitMesh);
42821
42926
  } else {
42822
42927
  _this4.componentTooltipManager.updateIODeviceDrag(signedDelta);
42823
42928
  }
@@ -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.30
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) {
@@ -158,13 +183,26 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
158
183
  }, {
159
184
  key: "getAnimationDataPoints",
160
185
  value: function getAnimationDataPoints(parentUuid, attachmentId) {
186
+ var _this2 = this;
187
+ var hitMesh = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
161
188
  var key = this._key(parentUuid, attachmentId);
162
189
  var entries = this._entries.get(key);
163
190
  if (!(entries !== null && entries !== void 0 && entries.length)) return [];
164
191
 
192
+ // When a specific mesh was clicked, filter to only animations whose target
193
+ // mesh is the clicked mesh or an ancestor of it (e.g. the clicked primitive
194
+ // is inside a group that is the animation target).
195
+ var filtered = entries;
196
+ if (hitMesh) {
197
+ var matching = entries.filter(function (e) {
198
+ return _this2._isMeshOrDescendant(hitMesh, e.mesh);
199
+ });
200
+ if (matching.length > 0) filtered = matching;
201
+ }
202
+
165
203
  // Collapse multiple mesh entries that share the same stateVariable
166
204
  var seen = new Map(); // stateVariable → anim
167
- var _iterator3 = _rollupPluginBabelHelpers.createForOfIteratorHelper(entries),
205
+ var _iterator3 = _rollupPluginBabelHelpers.createForOfIteratorHelper(filtered),
168
206
  _step3;
169
207
  try {
170
208
  for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
@@ -304,6 +342,22 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
304
342
  return "".concat(parentUuid, "::").concat(attachmentId);
305
343
  }
306
344
 
345
+ /**
346
+ * Returns true if `candidate` is `ancestor` or any descendant of `ancestor`.
347
+ * @param {THREE.Object3D} candidate
348
+ * @param {THREE.Object3D} ancestor
349
+ */
350
+ }, {
351
+ key: "_isMeshOrDescendant",
352
+ value: function _isMeshOrDescendant(candidate, ancestor) {
353
+ var obj = candidate;
354
+ while (obj) {
355
+ if (obj === ancestor) return true;
356
+ obj = obj.parent;
357
+ }
358
+ return false;
359
+ }
360
+
307
361
  /**
308
362
  * Find the mesh inside `root` using UUID first, then name as fallback.
309
363
  * GLTFLoader assigns fresh UUIDs on every load, so name is the reliable key.
@@ -344,7 +398,12 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
344
398
  var anim = entry.anim,
345
399
  mesh = entry.mesh,
346
400
  origPos = entry.origPos,
347
- origRot = entry.origRot;
401
+ origRot = entry.origRot,
402
+ origWorldPos = entry.origWorldPos,
403
+ origWorldQuat = entry.origWorldQuat,
404
+ origWorldCenter = entry.origWorldCenter,
405
+ deviceWorldQuat = entry.deviceWorldQuat,
406
+ viewerMaxDim = entry.viewerMaxDim;
348
407
  var mapping = this._resolveMapping(anim, value);
349
408
  if (!mapping) return;
350
409
  var types = anim.transformTypes || [];
@@ -356,7 +415,7 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
356
415
  if (type === 'translation') {
357
416
  this._applyTranslation(mesh, origPos, mapping.transform);
358
417
  } else if (type === 'rotation') {
359
- this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform);
418
+ this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim);
360
419
  } else if (type === 'color') {
361
420
  this._applyColor(mesh, mapping.colorTransform);
362
421
  }
@@ -501,26 +560,24 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
501
560
  }
502
561
 
503
562
  /**
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
563
+ * Apply a rotation around an arbitrary pivot point using world-space quaternion
564
+ * math matching the ReconstructionViewer's setMeshPreviewRotationAxis approach.
565
+ * This ensures the runtime axis/pivot matches what the user configured in the
566
+ * animation dialog, regardless of the device's parent transform in the scene.
513
567
  *
514
568
  * @param {THREE.Object3D} mesh
515
- * @param {THREE.Vector3} origPos
516
- * @param {THREE.Euler} origRot
569
+ * @param {THREE.Vector3} origPos - local position at load time (unused, kept for signature compat)
570
+ * @param {THREE.Euler} origRot - local rotation at load time (unused)
517
571
  * @param {Object} anim
518
- * @param {number} angleDeg - Degrees
572
+ * @param {number} angleDeg - Degrees
573
+ * @param {THREE.Vector3} origWorldPos - world position at load time
574
+ * @param {THREE.Quaternion} origWorldQuat - world quaternion at load time
575
+ * @param {THREE.Vector3} origWorldCenter - world bounding-box center at load time
519
576
  */
520
577
  }, {
521
578
  key: "_applyRotation",
522
- value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg) {
523
- var _anim$rotAxis, _anim$rotAxisOffset, _off$x, _off$y, _off$z;
579
+ value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim) {
580
+ var _anim$rotAxis, _anim$rotAxisOffset;
524
581
  var angle = THREE__namespace.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
525
582
  var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
526
583
  var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
@@ -529,13 +586,57 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
529
586
  z: 0
530
587
  };
531
588
 
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;
589
+ // Local axis in the device's coordinate system
590
+ var localAxisVec = new THREE__namespace.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
591
+ if (origWorldPos && origWorldQuat && origWorldCenter) {
592
+ // Transform the configured axis from device-local space into world space
593
+ // so that 'X' means the device's local X, not the world X.
594
+ var worldAxisVec = localAxisVec.clone().applyQuaternion(deviceWorldQuat || new THREE__namespace.Quaternion());
595
+
596
+ // rotAxisOffset is stored in dialog-viewer-world space where the model root
597
+ // has been rotated Rot_Z(π) by default (ReconstructionViewer's else branch).
598
+ // Translation uses the same convention and negates X/Y to compensate.
599
+ // For rotation offset we apply the same compensation mathematically:
600
+ // offset_runtime = deviceWorldQuat × Rot_Z(π) × offset_dialog × scale
601
+ // This maps from dialog world space (post Z+180° flip) → model local → world.
602
+ var scale = viewerMaxDim || 1;
603
+ var rotZ180 = new THREE__namespace.Quaternion().setFromAxisAngle(new THREE__namespace.Vector3(0, 0, 1), Math.PI);
604
+ var qCombined = (deviceWorldQuat || new THREE__namespace.Quaternion()).clone().multiply(rotZ180);
605
+ var offWorld = new THREE__namespace.Vector3((off.x || 0) * scale, (off.y || 0) * scale, (off.z || 0) * scale).applyQuaternion(qCombined);
606
+ var pivot = origWorldCenter.clone().add(offWorld);
607
+ var deltaQuat = new THREE__namespace.Quaternion().setFromAxisAngle(worldAxisVec, angle);
608
+
609
+ // Rotate world position around pivot
610
+ var offsetVec = origWorldPos.clone().sub(pivot);
611
+ offsetVec.applyQuaternion(deltaQuat);
612
+ var newWorldPos = pivot.clone().add(offsetVec);
613
+
614
+ // Compose world quaternion
615
+ var newWorldQuat = deltaQuat.clone().multiply(origWorldQuat);
616
+
617
+ // Convert world position → parent local space
618
+ if (mesh.parent) {
619
+ mesh.parent.worldToLocal(newWorldPos);
620
+ }
621
+ mesh.position.copy(newWorldPos);
622
+
623
+ // Convert quaternion → parent local space
624
+ if (mesh.parent) {
625
+ var parentWorldQuat = new THREE__namespace.Quaternion();
626
+ mesh.parent.getWorldQuaternion(parentWorldQuat);
627
+ parentWorldQuat.invert();
628
+ newWorldQuat.premultiply(parentWorldQuat);
629
+ }
630
+ mesh.quaternion.copy(newWorldQuat);
631
+ } else {
632
+ var _off$x, _off$y, _off$z;
633
+ // Fallback for entries loaded without world data
634
+ 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);
635
+ var delta = origPos.clone().sub(_pivot);
636
+ delta.applyAxisAngle(localAxisVec, angle);
637
+ mesh.position.copy(_pivot).add(delta);
638
+ mesh.rotation[axis] = origRot[axis] + angle;
639
+ }
539
640
  }
540
641
 
541
642
  /**
@@ -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.30
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) {
@@ -134,13 +159,26 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
134
159
  }, {
135
160
  key: "getAnimationDataPoints",
136
161
  value: function getAnimationDataPoints(parentUuid, attachmentId) {
162
+ var _this2 = this;
163
+ var hitMesh = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
137
164
  var key = this._key(parentUuid, attachmentId);
138
165
  var entries = this._entries.get(key);
139
166
  if (!(entries !== null && entries !== void 0 && entries.length)) return [];
140
167
 
168
+ // When a specific mesh was clicked, filter to only animations whose target
169
+ // mesh is the clicked mesh or an ancestor of it (e.g. the clicked primitive
170
+ // is inside a group that is the animation target).
171
+ var filtered = entries;
172
+ if (hitMesh) {
173
+ var matching = entries.filter(function (e) {
174
+ return _this2._isMeshOrDescendant(hitMesh, e.mesh);
175
+ });
176
+ if (matching.length > 0) filtered = matching;
177
+ }
178
+
141
179
  // Collapse multiple mesh entries that share the same stateVariable
142
180
  var seen = new Map(); // stateVariable → anim
143
- var _iterator3 = _createForOfIteratorHelper(entries),
181
+ var _iterator3 = _createForOfIteratorHelper(filtered),
144
182
  _step3;
145
183
  try {
146
184
  for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
@@ -280,6 +318,22 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
280
318
  return "".concat(parentUuid, "::").concat(attachmentId);
281
319
  }
282
320
 
321
+ /**
322
+ * Returns true if `candidate` is `ancestor` or any descendant of `ancestor`.
323
+ * @param {THREE.Object3D} candidate
324
+ * @param {THREE.Object3D} ancestor
325
+ */
326
+ }, {
327
+ key: "_isMeshOrDescendant",
328
+ value: function _isMeshOrDescendant(candidate, ancestor) {
329
+ var obj = candidate;
330
+ while (obj) {
331
+ if (obj === ancestor) return true;
332
+ obj = obj.parent;
333
+ }
334
+ return false;
335
+ }
336
+
283
337
  /**
284
338
  * Find the mesh inside `root` using UUID first, then name as fallback.
285
339
  * GLTFLoader assigns fresh UUIDs on every load, so name is the reliable key.
@@ -320,7 +374,12 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
320
374
  var anim = entry.anim,
321
375
  mesh = entry.mesh,
322
376
  origPos = entry.origPos,
323
- origRot = entry.origRot;
377
+ origRot = entry.origRot,
378
+ origWorldPos = entry.origWorldPos,
379
+ origWorldQuat = entry.origWorldQuat,
380
+ origWorldCenter = entry.origWorldCenter,
381
+ deviceWorldQuat = entry.deviceWorldQuat,
382
+ viewerMaxDim = entry.viewerMaxDim;
324
383
  var mapping = this._resolveMapping(anim, value);
325
384
  if (!mapping) return;
326
385
  var types = anim.transformTypes || [];
@@ -332,7 +391,7 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
332
391
  if (type === 'translation') {
333
392
  this._applyTranslation(mesh, origPos, mapping.transform);
334
393
  } else if (type === 'rotation') {
335
- this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform);
394
+ this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim);
336
395
  } else if (type === 'color') {
337
396
  this._applyColor(mesh, mapping.colorTransform);
338
397
  }
@@ -477,26 +536,24 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
477
536
  }
478
537
 
479
538
  /**
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
539
+ * Apply a rotation around an arbitrary pivot point using world-space quaternion
540
+ * math matching the ReconstructionViewer's setMeshPreviewRotationAxis approach.
541
+ * This ensures the runtime axis/pivot matches what the user configured in the
542
+ * animation dialog, regardless of the device's parent transform in the scene.
489
543
  *
490
544
  * @param {THREE.Object3D} mesh
491
- * @param {THREE.Vector3} origPos
492
- * @param {THREE.Euler} origRot
545
+ * @param {THREE.Vector3} origPos - local position at load time (unused, kept for signature compat)
546
+ * @param {THREE.Euler} origRot - local rotation at load time (unused)
493
547
  * @param {Object} anim
494
- * @param {number} angleDeg - Degrees
548
+ * @param {number} angleDeg - Degrees
549
+ * @param {THREE.Vector3} origWorldPos - world position at load time
550
+ * @param {THREE.Quaternion} origWorldQuat - world quaternion at load time
551
+ * @param {THREE.Vector3} origWorldCenter - world bounding-box center at load time
495
552
  */
496
553
  }, {
497
554
  key: "_applyRotation",
498
- value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg) {
499
- var _anim$rotAxis, _anim$rotAxisOffset, _off$x, _off$y, _off$z;
555
+ value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim) {
556
+ var _anim$rotAxis, _anim$rotAxisOffset;
500
557
  var angle = THREE.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
501
558
  var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
502
559
  var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
@@ -505,13 +562,57 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
505
562
  z: 0
506
563
  };
507
564
 
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;
565
+ // Local axis in the device's coordinate system
566
+ var localAxisVec = new THREE.Vector3(axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0);
567
+ if (origWorldPos && origWorldQuat && origWorldCenter) {
568
+ // Transform the configured axis from device-local space into world space
569
+ // so that 'X' means the device's local X, not the world X.
570
+ var worldAxisVec = localAxisVec.clone().applyQuaternion(deviceWorldQuat || new THREE.Quaternion());
571
+
572
+ // rotAxisOffset is stored in dialog-viewer-world space where the model root
573
+ // has been rotated Rot_Z(π) by default (ReconstructionViewer's else branch).
574
+ // Translation uses the same convention and negates X/Y to compensate.
575
+ // For rotation offset we apply the same compensation mathematically:
576
+ // offset_runtime = deviceWorldQuat × Rot_Z(π) × offset_dialog × scale
577
+ // This maps from dialog world space (post Z+180° flip) → model local → world.
578
+ var scale = viewerMaxDim || 1;
579
+ var rotZ180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI);
580
+ var qCombined = (deviceWorldQuat || new THREE.Quaternion()).clone().multiply(rotZ180);
581
+ var offWorld = new THREE.Vector3((off.x || 0) * scale, (off.y || 0) * scale, (off.z || 0) * scale).applyQuaternion(qCombined);
582
+ var pivot = origWorldCenter.clone().add(offWorld);
583
+ var deltaQuat = new THREE.Quaternion().setFromAxisAngle(worldAxisVec, angle);
584
+
585
+ // Rotate world position around pivot
586
+ var offsetVec = origWorldPos.clone().sub(pivot);
587
+ offsetVec.applyQuaternion(deltaQuat);
588
+ var newWorldPos = pivot.clone().add(offsetVec);
589
+
590
+ // Compose world quaternion
591
+ var newWorldQuat = deltaQuat.clone().multiply(origWorldQuat);
592
+
593
+ // Convert world position → parent local space
594
+ if (mesh.parent) {
595
+ mesh.parent.worldToLocal(newWorldPos);
596
+ }
597
+ mesh.position.copy(newWorldPos);
598
+
599
+ // Convert quaternion → parent local space
600
+ if (mesh.parent) {
601
+ var parentWorldQuat = new THREE.Quaternion();
602
+ mesh.parent.getWorldQuaternion(parentWorldQuat);
603
+ parentWorldQuat.invert();
604
+ newWorldQuat.premultiply(parentWorldQuat);
605
+ }
606
+ mesh.quaternion.copy(newWorldQuat);
607
+ } else {
608
+ var _off$x, _off$y, _off$z;
609
+ // Fallback for entries loaded without world data
610
+ 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);
611
+ var delta = origPos.clone().sub(_pivot);
612
+ delta.applyAxisAngle(localAxisVec, angle);
613
+ mesh.position.copy(_pivot).add(delta);
614
+ mesh.rotation[axis] = origRot[axis] + angle;
615
+ }
515
616
  }
516
617
 
517
618
  /**
@@ -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.30",
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",