@2112-lab/central-plant 0.3.36 → 0.3.38

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.
@@ -11772,7 +11772,8 @@ var SceneExportManager = /*#__PURE__*/function () {
11772
11772
 
11773
11773
  // Copy only essential userData properties (libraryId, objectType, direction, group)
11774
11774
  Object.keys(threeObject.userData).forEach(function (key) {
11775
- if (!excludedKeys.has(key)) {
11775
+ // Skip runtime/internal keys (e.g. _filteredBBoxCache from boundingBoxUtils)
11776
+ if (!excludedKeys.has(key) && !key.startsWith('_')) {
11776
11777
  jsonObject.userData[key] = threeObject.userData[key];
11777
11778
  }
11778
11779
  });
@@ -36542,7 +36543,7 @@ var SceneTooltipsManager = /*#__PURE__*/function (_BaseDisposable) {
36542
36543
  // ---------------------------------------------------------------------------
36543
36544
  // Inline styles (injected once into the document head)
36544
36545
  // ---------------------------------------------------------------------------
36545
- var TOOLTIP_STYLES = "\n.cp-tooltip {\n position: absolute;\n pointer-events: auto;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 13px;\n line-height: 1.4;\n user-select: none;\n min-width: 200px;\n max-width: 280px;\n transform: translate(-50%, -100%);\n margin-top: -12px;\n transition: opacity 0.15s ease;\n}\n\n.cp-tooltip__card {\n background: rgba(28, 32, 40, 0.95);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 10px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);\n overflow: hidden;\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__header {\n padding: 10px 14px;\n font-weight: 600;\n font-size: 14px;\n color: #e8ecf1;\n background: rgba(255, 255, 255, 0.04);\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 I/O Devices section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__io-section {\n position: relative;\n}\n\n.cp-tooltip__io-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 14px;\n color: #a4adba;\n cursor: pointer;\n transition: background 0.12s ease, color 0.12s ease;\n}\n\n.cp-tooltip__io-trigger:hover {\n background: rgba(255, 255, 255, 0.06);\n color: #e8ecf1;\n}\n\n.cp-tooltip__io-trigger-label {\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n font-weight: 500;\n}\n\n.cp-tooltip__io-arrow {\n font-size: 10px;\n transition: transform 0.2s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__io-arrow {\n transform: rotate(90deg);\n}\n\n/* \u2500\u2500 Device list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__device-list {\n max-height: 0;\n overflow: hidden;\n transition: max-height 0.25s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__device-list {\n max-height: 300px;\n}\n\n.cp-tooltip__device-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 14px 6px 20px;\n color: #c1c8d1;\n font-size: 12px;\n border-top: 1px solid rgba(255, 255, 255, 0.04);\n transition: background 0.1s ease;\n}\n\n.cp-tooltip__device-item:hover {\n background: rgba(255, 255, 255, 0.04);\n}\n\n.cp-tooltip__device-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: #4fc3f7;\n flex-shrink: 0;\n}\n\n.cp-tooltip__device-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__no-devices {\n padding: 8px 14px;\n color: #6b7280;\n font-size: 12px;\n font-style: italic;\n}\n\n/* \u2500\u2500 Caret arrow pointing down \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__caret {\n width: 0;\n height: 0;\n border-left: 7px solid transparent;\n border-right: 7px solid transparent;\n border-top: 7px solid rgba(28, 32, 40, 0.95);\n margin: 0 auto;\n position: relative;\n top: -1px;\n}\n\n/* \u2500\u2500 Data point rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__dp-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 4px 12px 4px 30px;\n font-size: 11.5px;\n border-top: 1px solid rgba(255, 255, 255, 0.025);\n min-height: 24px;\n}\n\n.cp-tooltip__dp-name {\n flex: 1;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: #7c8d9e;\n}\n\n/* Read-only state badge */\n.cp-tooltip__dp-badge {\n font-size: 10.5px;\n font-weight: 600;\n padding: 1px 7px;\n border-radius: 8px;\n flex-shrink: 0;\n background: rgba(255, 255, 255, 0.07);\n color: #9aabb8;\n min-width: 30px;\n text-align: center;\n letter-spacing: 0.2px;\n}\n\n.cp-tooltip__dp-badge.active {\n background: rgba(76, 175, 80, 0.28);\n color: #a5d6a7;\n}\n\n/* Binary toggle button */\n.cp-tooltip__dp-toggle {\n font-size: 10.5px;\n font-weight: 600;\n padding: 2px 9px;\n border-radius: 8px;\n flex-shrink: 0;\n border: 1px solid rgba(255, 255, 255, 0.14);\n background: rgba(255, 255, 255, 0.06);\n color: #c1c8d1;\n cursor: pointer;\n min-width: 40px;\n text-align: center;\n transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;\n}\n\n.cp-tooltip__dp-toggle:hover {\n background: rgba(255, 255, 255, 0.12);\n border-color: rgba(255, 255, 255, 0.25);\n}\n\n.cp-tooltip__dp-toggle.on {\n background: rgba(76, 175, 80, 0.28);\n border-color: rgba(76, 175, 80, 0.55);\n color: #a5d6a7;\n}\n\n/* Number / float input */\n.cp-tooltip__dp-input {\n width: 62px;\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 4px;\n color: #e8ecf1;\n font-size: 11px;\n padding: 2px 5px;\n text-align: right;\n outline: none;\n flex-shrink: 0;\n}\n\n.cp-tooltip__dp-input:focus {\n border-color: rgba(79, 195, 247, 0.55);\n}\n\n/* Enum select */\n.cp-tooltip__dp-select {\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 4px;\n color: #e8ecf1;\n font-size: 11px;\n padding: 2px 4px;\n outline: none;\n flex-shrink: 0;\n max-width: 96px;\n cursor: pointer;\n}\n\n.cp-tooltip__dp-select:focus {\n border-color: rgba(79, 195, 247, 0.55);\n}\n\n/* Unit suffix */\n.cp-tooltip__dp-unit {\n font-size: 10px;\n color: #505c68;\n flex-shrink: 0;\n margin-left: -4px;\n}\n";
36546
+ var TOOLTIP_STYLES = "\n.cp-tooltip {\n position: absolute;\n pointer-events: auto;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 13px;\n line-height: 1.4;\n user-select: none;\n min-width: 200px;\n max-width: 280px;\n transform: translate(-50%, -100%);\n margin-top: -12px;\n transition: opacity 0.15s ease;\n color-scheme: light;\n}\n\n.cp-tooltip__card {\n background: rgba(255, 255, 255, 0.97);\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: 10px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n overflow: hidden;\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__header {\n padding: 10px 14px;\n font-weight: 600;\n font-size: 14px;\n color: #2c3e50;\n background: rgba(0, 0, 0, 0.02);\n border-bottom: 1px solid rgba(0, 0, 0, 0.08);\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 I/O Devices section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__io-section {\n position: relative;\n}\n\n.cp-tooltip__io-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 14px;\n color: #666666;\n cursor: pointer;\n transition: background 0.12s ease, color 0.12s ease;\n}\n\n.cp-tooltip__io-trigger:hover {\n background: rgba(0, 0, 0, 0.04);\n color: #333333;\n}\n\n.cp-tooltip__io-trigger-label {\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n font-weight: 500;\n}\n\n.cp-tooltip__io-arrow {\n font-size: 10px;\n transition: transform 0.2s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__io-arrow {\n transform: rotate(90deg);\n}\n\n/* \u2500\u2500 Device list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__device-list {\n max-height: 0;\n overflow: hidden;\n transition: max-height 0.25s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__device-list {\n max-height: 300px;\n}\n\n.cp-tooltip__device-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 14px 6px 20px;\n color: #555555;\n font-size: 12px;\n border-top: 1px solid rgba(0, 0, 0, 0.06);\n transition: background 0.1s ease;\n}\n\n.cp-tooltip__device-item:hover {\n background: rgba(0, 0, 0, 0.03);\n}\n\n.cp-tooltip__device-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: #1976d2;\n flex-shrink: 0;\n}\n\n.cp-tooltip__device-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__no-devices {\n padding: 8px 14px;\n color: #888888;\n font-size: 12px;\n font-style: italic;\n}\n\n/* \u2500\u2500 Caret arrow pointing down \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__caret {\n width: 0;\n height: 0;\n border-left: 7px solid transparent;\n border-right: 7px solid transparent;\n border-top: 7px solid rgba(255, 255, 255, 0.97);\n margin: 0 auto;\n position: relative;\n top: -1px;\n filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.1));\n}\n\n/* \u2500\u2500 Data point rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__dp-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 4px 12px 4px 30px;\n font-size: 11.5px;\n border-top: 1px solid rgba(0, 0, 0, 0.05);\n min-height: 24px;\n}\n\n.cp-tooltip__dp-name {\n flex: 1;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: #666666;\n}\n\n/* Read-only state badge */\n.cp-tooltip__dp-badge {\n font-size: 10.5px;\n font-weight: 600;\n padding: 1px 7px;\n border-radius: 8px;\n flex-shrink: 0;\n background: #f0f2f5;\n color: #666666;\n min-width: 30px;\n text-align: center;\n letter-spacing: 0.2px;\n}\n\n.cp-tooltip__dp-badge.active {\n background: rgba(76, 175, 80, 0.15);\n color: #2e7d32;\n}\n\n/* Binary toggle button */\n.cp-tooltip__dp-toggle {\n font-size: 10.5px;\n font-weight: 600;\n padding: 2px 9px;\n border-radius: 8px;\n flex-shrink: 0;\n border: 1px solid #dddddd;\n background: #f8f9fa;\n color: #555555;\n cursor: pointer;\n min-width: 40px;\n text-align: center;\n transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;\n}\n\n.cp-tooltip__dp-toggle:hover {\n background: #eeeeee;\n border-color: #cccccc;\n}\n\n.cp-tooltip__dp-toggle.on {\n background: rgba(76, 175, 80, 0.15);\n border-color: rgba(76, 175, 80, 0.45);\n color: #2e7d32;\n}\n\n/* Number / float input */\n.cp-tooltip__dp-input {\n width: 62px;\n background: #f8f9fa;\n border: 1px solid #dddddd;\n border-radius: 4px;\n color: #333333;\n font-size: 11px;\n padding: 2px 5px;\n text-align: right;\n outline: none;\n flex-shrink: 0;\n}\n\n.cp-tooltip__dp-input:focus {\n border-color: #1976d2;\n}\n\n/* Enum select */\n.cp-tooltip__dp-select {\n background: #f8f9fa;\n border: 1px solid #dddddd;\n border-radius: 4px;\n color: #333333;\n font-size: 11px;\n padding: 2px 4px;\n outline: none;\n flex-shrink: 0;\n max-width: 96px;\n cursor: pointer;\n}\n\n.cp-tooltip__dp-select:focus {\n border-color: #1976d2;\n}\n\n/* Unit suffix */\n.cp-tooltip__dp-unit {\n font-size: 10px;\n color: #888888;\n flex-shrink: 0;\n margin-left: -4px;\n}\n";
36546
36547
  var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
36547
36548
  /**
36548
36549
  * @param {Object} sceneViewer - The sceneViewer instance
@@ -37474,6 +37475,7 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
37474
37475
  entries.push({
37475
37476
  anim: anim,
37476
37477
  mesh: mesh,
37478
+ deviceModelRoot: deviceModelRoot,
37477
37479
  origPos: mesh.position.clone(),
37478
37480
  origRot: mesh.rotation.clone(),
37479
37481
  origWorldPos: worldPos,
@@ -38224,13 +38226,9 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
38224
38226
  value: function _applyAnimation(entry, value) {
38225
38227
  var anim = entry.anim,
38226
38228
  mesh = entry.mesh,
38227
- origPos = entry.origPos,
38228
- origRot = entry.origRot,
38229
- origWorldPos = entry.origWorldPos,
38230
- origWorldQuat = entry.origWorldQuat,
38231
- origWorldCenter = entry.origWorldCenter,
38232
- deviceWorldQuat = entry.deviceWorldQuat,
38233
- viewerMaxDim = entry.viewerMaxDim;
38229
+ origPos = entry.origPos;
38230
+ entry.origRot;
38231
+ var viewerMaxDim = entry.viewerMaxDim;
38234
38232
  var mapping = this._resolveMapping(anim, value);
38235
38233
  if (!mapping) return;
38236
38234
  var types = anim.transformTypes || [];
@@ -38242,7 +38240,7 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
38242
38240
  if (type === 'translation') {
38243
38241
  this._applyTranslation(mesh, origPos, mapping.transform);
38244
38242
  } else if (type === 'rotation') {
38245
- this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim);
38243
+ this._applyRotation(entry, anim, mapping.rotationTransform, viewerMaxDim);
38246
38244
  } else if (type === 'color') {
38247
38245
  this._applyColor(mesh, mapping.colorTransform);
38248
38246
  }
@@ -38386,25 +38384,71 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
38386
38384
  mesh.position.set(origPos.x - ((_transform$x = transform.x) !== null && _transform$x !== void 0 ? _transform$x : 0), origPos.y - ((_transform$y = transform.y) !== null && _transform$y !== void 0 ? _transform$y : 0), origPos.z + ((_transform$z = transform.z) !== null && _transform$z !== void 0 ? _transform$z : 0));
38387
38385
  }
38388
38386
 
38387
+ /**
38388
+ * Recompute world-space rest-pose transforms for a behavior entry using the
38389
+ * current parent/device world matrix. origPos/origRot are local rest pose and
38390
+ * stay valid after parent moves; world anchors must be derived at apply time.
38391
+ *
38392
+ * @param {{ mesh, origPos, origRot, deviceModelRoot }} entry
38393
+ * @returns {{ origWorldPos: THREE.Vector3, origWorldQuat: THREE.Quaternion, origWorldCenter: THREE.Vector3, deviceWorldQuat: THREE.Quaternion }|null}
38394
+ */
38395
+ }, {
38396
+ key: "_getRestWorldTransforms",
38397
+ value: function _getRestWorldTransforms(entry) {
38398
+ var mesh = entry.mesh,
38399
+ origPos = entry.origPos,
38400
+ origRot = entry.origRot,
38401
+ deviceModelRoot = entry.deviceModelRoot;
38402
+ if (!mesh || !origPos || !origRot) return null;
38403
+ var savedPos = mesh.position.clone();
38404
+ var savedQuat = mesh.quaternion.clone();
38405
+ mesh.position.copy(origPos);
38406
+ mesh.rotation.copy(origRot);
38407
+ mesh.updateMatrixWorld(true);
38408
+ var origWorldPos = new THREE__namespace.Vector3();
38409
+ mesh.getWorldPosition(origWorldPos);
38410
+ var origWorldQuat = new THREE__namespace.Quaternion();
38411
+ mesh.getWorldQuaternion(origWorldQuat);
38412
+ var box = new THREE__namespace.Box3().setFromObject(mesh);
38413
+ var origWorldCenter = new THREE__namespace.Vector3();
38414
+ if (!box.isEmpty()) box.getCenter(origWorldCenter);else origWorldCenter.copy(origWorldPos);
38415
+ var deviceWorldQuat = new THREE__namespace.Quaternion();
38416
+ if (deviceModelRoot) {
38417
+ deviceModelRoot.getWorldQuaternion(deviceWorldQuat);
38418
+ }
38419
+ mesh.position.copy(savedPos);
38420
+ mesh.quaternion.copy(savedQuat);
38421
+ return {
38422
+ origWorldPos: origWorldPos,
38423
+ origWorldQuat: origWorldQuat,
38424
+ origWorldCenter: origWorldCenter,
38425
+ deviceWorldQuat: deviceWorldQuat
38426
+ };
38427
+ }
38428
+
38389
38429
  /**
38390
38430
  * Apply a rotation around an arbitrary pivot point using world-space quaternion
38391
38431
  * math matching the ReconstructionViewer's setMeshPreviewRotationAxis approach.
38392
38432
  * This ensures the runtime axis/pivot matches what the user configured in the
38393
38433
  * animation dialog, regardless of the device's parent transform in the scene.
38394
38434
  *
38395
- * @param {THREE.Object3D} mesh
38396
- * @param {THREE.Vector3} origPos - local position at load time (unused, kept for signature compat)
38397
- * @param {THREE.Euler} origRot - local rotation at load time (unused)
38435
+ * @param {{ mesh, origPos, origRot, deviceModelRoot }} entry
38398
38436
  * @param {Object} anim
38399
38437
  * @param {number} angleDeg - Degrees
38400
- * @param {THREE.Vector3} origWorldPos - world position at load time
38401
- * @param {THREE.Quaternion} origWorldQuat - world quaternion at load time
38402
- * @param {THREE.Vector3} origWorldCenter - world bounding-box center at load time
38438
+ * @param {number} viewerMaxDim
38403
38439
  */
38404
38440
  }, {
38405
38441
  key: "_applyRotation",
38406
- value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim) {
38442
+ value: function _applyRotation(entry, anim, angleDeg, viewerMaxDim) {
38407
38443
  var _anim$rotAxis, _anim$rotAxisOffset;
38444
+ var mesh = entry.mesh,
38445
+ origPos = entry.origPos,
38446
+ origRot = entry.origRot;
38447
+ var restWorld = this._getRestWorldTransforms(entry);
38448
+ var origWorldPos = restWorld === null || restWorld === void 0 ? void 0 : restWorld.origWorldPos;
38449
+ var origWorldQuat = restWorld === null || restWorld === void 0 ? void 0 : restWorld.origWorldQuat;
38450
+ var origWorldCenter = restWorld === null || restWorld === void 0 ? void 0 : restWorld.origWorldCenter;
38451
+ var deviceWorldQuat = restWorld === null || restWorld === void 0 ? void 0 : restWorld.deviceWorldQuat;
38408
38452
  var angle = THREE__namespace.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
38409
38453
  var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
38410
38454
  var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
@@ -40151,7 +40195,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
40151
40195
  * Initialize the CentralPlant manager
40152
40196
  *
40153
40197
  * @constructor
40154
- * @version 0.3.36
40198
+ * @version 0.3.38
40155
40199
  * @updated 2025-10-22
40156
40200
  *
40157
40201
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -35,7 +35,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
35
35
  * Initialize the CentralPlant manager
36
36
  *
37
37
  * @constructor
38
- * @version 0.3.36
38
+ * @version 0.3.38
39
39
  * @updated 2025-10-22
40
40
  *
41
41
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -121,6 +121,7 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
121
121
  entries.push({
122
122
  anim: anim,
123
123
  mesh: mesh,
124
+ deviceModelRoot: deviceModelRoot,
124
125
  origPos: mesh.position.clone(),
125
126
  origRot: mesh.rotation.clone(),
126
127
  origWorldPos: worldPos,
@@ -871,13 +872,9 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
871
872
  value: function _applyAnimation(entry, value) {
872
873
  var anim = entry.anim,
873
874
  mesh = entry.mesh,
874
- origPos = entry.origPos,
875
- origRot = entry.origRot,
876
- origWorldPos = entry.origWorldPos,
877
- origWorldQuat = entry.origWorldQuat,
878
- origWorldCenter = entry.origWorldCenter,
879
- deviceWorldQuat = entry.deviceWorldQuat,
880
- viewerMaxDim = entry.viewerMaxDim;
875
+ origPos = entry.origPos;
876
+ entry.origRot;
877
+ var viewerMaxDim = entry.viewerMaxDim;
881
878
  var mapping = this._resolveMapping(anim, value);
882
879
  if (!mapping) return;
883
880
  var types = anim.transformTypes || [];
@@ -889,7 +886,7 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
889
886
  if (type === 'translation') {
890
887
  this._applyTranslation(mesh, origPos, mapping.transform);
891
888
  } else if (type === 'rotation') {
892
- this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim);
889
+ this._applyRotation(entry, anim, mapping.rotationTransform, viewerMaxDim);
893
890
  } else if (type === 'color') {
894
891
  this._applyColor(mesh, mapping.colorTransform);
895
892
  }
@@ -1033,25 +1030,71 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
1033
1030
  mesh.position.set(origPos.x - ((_transform$x = transform.x) !== null && _transform$x !== void 0 ? _transform$x : 0), origPos.y - ((_transform$y = transform.y) !== null && _transform$y !== void 0 ? _transform$y : 0), origPos.z + ((_transform$z = transform.z) !== null && _transform$z !== void 0 ? _transform$z : 0));
1034
1031
  }
1035
1032
 
1033
+ /**
1034
+ * Recompute world-space rest-pose transforms for a behavior entry using the
1035
+ * current parent/device world matrix. origPos/origRot are local rest pose and
1036
+ * stay valid after parent moves; world anchors must be derived at apply time.
1037
+ *
1038
+ * @param {{ mesh, origPos, origRot, deviceModelRoot }} entry
1039
+ * @returns {{ origWorldPos: THREE.Vector3, origWorldQuat: THREE.Quaternion, origWorldCenter: THREE.Vector3, deviceWorldQuat: THREE.Quaternion }|null}
1040
+ */
1041
+ }, {
1042
+ key: "_getRestWorldTransforms",
1043
+ value: function _getRestWorldTransforms(entry) {
1044
+ var mesh = entry.mesh,
1045
+ origPos = entry.origPos,
1046
+ origRot = entry.origRot,
1047
+ deviceModelRoot = entry.deviceModelRoot;
1048
+ if (!mesh || !origPos || !origRot) return null;
1049
+ var savedPos = mesh.position.clone();
1050
+ var savedQuat = mesh.quaternion.clone();
1051
+ mesh.position.copy(origPos);
1052
+ mesh.rotation.copy(origRot);
1053
+ mesh.updateMatrixWorld(true);
1054
+ var origWorldPos = new THREE__namespace.Vector3();
1055
+ mesh.getWorldPosition(origWorldPos);
1056
+ var origWorldQuat = new THREE__namespace.Quaternion();
1057
+ mesh.getWorldQuaternion(origWorldQuat);
1058
+ var box = new THREE__namespace.Box3().setFromObject(mesh);
1059
+ var origWorldCenter = new THREE__namespace.Vector3();
1060
+ if (!box.isEmpty()) box.getCenter(origWorldCenter);else origWorldCenter.copy(origWorldPos);
1061
+ var deviceWorldQuat = new THREE__namespace.Quaternion();
1062
+ if (deviceModelRoot) {
1063
+ deviceModelRoot.getWorldQuaternion(deviceWorldQuat);
1064
+ }
1065
+ mesh.position.copy(savedPos);
1066
+ mesh.quaternion.copy(savedQuat);
1067
+ return {
1068
+ origWorldPos: origWorldPos,
1069
+ origWorldQuat: origWorldQuat,
1070
+ origWorldCenter: origWorldCenter,
1071
+ deviceWorldQuat: deviceWorldQuat
1072
+ };
1073
+ }
1074
+
1036
1075
  /**
1037
1076
  * Apply a rotation around an arbitrary pivot point using world-space quaternion
1038
1077
  * math matching the ReconstructionViewer's setMeshPreviewRotationAxis approach.
1039
1078
  * This ensures the runtime axis/pivot matches what the user configured in the
1040
1079
  * animation dialog, regardless of the device's parent transform in the scene.
1041
1080
  *
1042
- * @param {THREE.Object3D} mesh
1043
- * @param {THREE.Vector3} origPos - local position at load time (unused, kept for signature compat)
1044
- * @param {THREE.Euler} origRot - local rotation at load time (unused)
1081
+ * @param {{ mesh, origPos, origRot, deviceModelRoot }} entry
1045
1082
  * @param {Object} anim
1046
1083
  * @param {number} angleDeg - Degrees
1047
- * @param {THREE.Vector3} origWorldPos - world position at load time
1048
- * @param {THREE.Quaternion} origWorldQuat - world quaternion at load time
1049
- * @param {THREE.Vector3} origWorldCenter - world bounding-box center at load time
1084
+ * @param {number} viewerMaxDim
1050
1085
  */
1051
1086
  }, {
1052
1087
  key: "_applyRotation",
1053
- value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim) {
1088
+ value: function _applyRotation(entry, anim, angleDeg, viewerMaxDim) {
1054
1089
  var _anim$rotAxis, _anim$rotAxisOffset;
1090
+ var mesh = entry.mesh,
1091
+ origPos = entry.origPos,
1092
+ origRot = entry.origRot;
1093
+ var restWorld = this._getRestWorldTransforms(entry);
1094
+ var origWorldPos = restWorld === null || restWorld === void 0 ? void 0 : restWorld.origWorldPos;
1095
+ var origWorldQuat = restWorld === null || restWorld === void 0 ? void 0 : restWorld.origWorldQuat;
1096
+ var origWorldCenter = restWorld === null || restWorld === void 0 ? void 0 : restWorld.origWorldCenter;
1097
+ var deviceWorldQuat = restWorld === null || restWorld === void 0 ? void 0 : restWorld.deviceWorldQuat;
1055
1098
  var angle = THREE__namespace.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
1056
1099
  var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
1057
1100
  var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
@@ -29,7 +29,7 @@ var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
29
29
  // ---------------------------------------------------------------------------
30
30
  // Inline styles (injected once into the document head)
31
31
  // ---------------------------------------------------------------------------
32
- var TOOLTIP_STYLES = "\n.cp-tooltip {\n position: absolute;\n pointer-events: auto;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 13px;\n line-height: 1.4;\n user-select: none;\n min-width: 200px;\n max-width: 280px;\n transform: translate(-50%, -100%);\n margin-top: -12px;\n transition: opacity 0.15s ease;\n}\n\n.cp-tooltip__card {\n background: rgba(28, 32, 40, 0.95);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 10px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);\n overflow: hidden;\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__header {\n padding: 10px 14px;\n font-weight: 600;\n font-size: 14px;\n color: #e8ecf1;\n background: rgba(255, 255, 255, 0.04);\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 I/O Devices section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__io-section {\n position: relative;\n}\n\n.cp-tooltip__io-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 14px;\n color: #a4adba;\n cursor: pointer;\n transition: background 0.12s ease, color 0.12s ease;\n}\n\n.cp-tooltip__io-trigger:hover {\n background: rgba(255, 255, 255, 0.06);\n color: #e8ecf1;\n}\n\n.cp-tooltip__io-trigger-label {\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n font-weight: 500;\n}\n\n.cp-tooltip__io-arrow {\n font-size: 10px;\n transition: transform 0.2s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__io-arrow {\n transform: rotate(90deg);\n}\n\n/* \u2500\u2500 Device list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__device-list {\n max-height: 0;\n overflow: hidden;\n transition: max-height 0.25s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__device-list {\n max-height: 300px;\n}\n\n.cp-tooltip__device-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 14px 6px 20px;\n color: #c1c8d1;\n font-size: 12px;\n border-top: 1px solid rgba(255, 255, 255, 0.04);\n transition: background 0.1s ease;\n}\n\n.cp-tooltip__device-item:hover {\n background: rgba(255, 255, 255, 0.04);\n}\n\n.cp-tooltip__device-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: #4fc3f7;\n flex-shrink: 0;\n}\n\n.cp-tooltip__device-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__no-devices {\n padding: 8px 14px;\n color: #6b7280;\n font-size: 12px;\n font-style: italic;\n}\n\n/* \u2500\u2500 Caret arrow pointing down \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__caret {\n width: 0;\n height: 0;\n border-left: 7px solid transparent;\n border-right: 7px solid transparent;\n border-top: 7px solid rgba(28, 32, 40, 0.95);\n margin: 0 auto;\n position: relative;\n top: -1px;\n}\n\n/* \u2500\u2500 Data point rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__dp-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 4px 12px 4px 30px;\n font-size: 11.5px;\n border-top: 1px solid rgba(255, 255, 255, 0.025);\n min-height: 24px;\n}\n\n.cp-tooltip__dp-name {\n flex: 1;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: #7c8d9e;\n}\n\n/* Read-only state badge */\n.cp-tooltip__dp-badge {\n font-size: 10.5px;\n font-weight: 600;\n padding: 1px 7px;\n border-radius: 8px;\n flex-shrink: 0;\n background: rgba(255, 255, 255, 0.07);\n color: #9aabb8;\n min-width: 30px;\n text-align: center;\n letter-spacing: 0.2px;\n}\n\n.cp-tooltip__dp-badge.active {\n background: rgba(76, 175, 80, 0.28);\n color: #a5d6a7;\n}\n\n/* Binary toggle button */\n.cp-tooltip__dp-toggle {\n font-size: 10.5px;\n font-weight: 600;\n padding: 2px 9px;\n border-radius: 8px;\n flex-shrink: 0;\n border: 1px solid rgba(255, 255, 255, 0.14);\n background: rgba(255, 255, 255, 0.06);\n color: #c1c8d1;\n cursor: pointer;\n min-width: 40px;\n text-align: center;\n transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;\n}\n\n.cp-tooltip__dp-toggle:hover {\n background: rgba(255, 255, 255, 0.12);\n border-color: rgba(255, 255, 255, 0.25);\n}\n\n.cp-tooltip__dp-toggle.on {\n background: rgba(76, 175, 80, 0.28);\n border-color: rgba(76, 175, 80, 0.55);\n color: #a5d6a7;\n}\n\n/* Number / float input */\n.cp-tooltip__dp-input {\n width: 62px;\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 4px;\n color: #e8ecf1;\n font-size: 11px;\n padding: 2px 5px;\n text-align: right;\n outline: none;\n flex-shrink: 0;\n}\n\n.cp-tooltip__dp-input:focus {\n border-color: rgba(79, 195, 247, 0.55);\n}\n\n/* Enum select */\n.cp-tooltip__dp-select {\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 4px;\n color: #e8ecf1;\n font-size: 11px;\n padding: 2px 4px;\n outline: none;\n flex-shrink: 0;\n max-width: 96px;\n cursor: pointer;\n}\n\n.cp-tooltip__dp-select:focus {\n border-color: rgba(79, 195, 247, 0.55);\n}\n\n/* Unit suffix */\n.cp-tooltip__dp-unit {\n font-size: 10px;\n color: #505c68;\n flex-shrink: 0;\n margin-left: -4px;\n}\n";
32
+ var TOOLTIP_STYLES = "\n.cp-tooltip {\n position: absolute;\n pointer-events: auto;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 13px;\n line-height: 1.4;\n user-select: none;\n min-width: 200px;\n max-width: 280px;\n transform: translate(-50%, -100%);\n margin-top: -12px;\n transition: opacity 0.15s ease;\n color-scheme: light;\n}\n\n.cp-tooltip__card {\n background: rgba(255, 255, 255, 0.97);\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: 10px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n overflow: hidden;\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__header {\n padding: 10px 14px;\n font-weight: 600;\n font-size: 14px;\n color: #2c3e50;\n background: rgba(0, 0, 0, 0.02);\n border-bottom: 1px solid rgba(0, 0, 0, 0.08);\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 I/O Devices section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__io-section {\n position: relative;\n}\n\n.cp-tooltip__io-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 14px;\n color: #666666;\n cursor: pointer;\n transition: background 0.12s ease, color 0.12s ease;\n}\n\n.cp-tooltip__io-trigger:hover {\n background: rgba(0, 0, 0, 0.04);\n color: #333333;\n}\n\n.cp-tooltip__io-trigger-label {\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n font-weight: 500;\n}\n\n.cp-tooltip__io-arrow {\n font-size: 10px;\n transition: transform 0.2s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__io-arrow {\n transform: rotate(90deg);\n}\n\n/* \u2500\u2500 Device list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__device-list {\n max-height: 0;\n overflow: hidden;\n transition: max-height 0.25s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__device-list {\n max-height: 300px;\n}\n\n.cp-tooltip__device-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 14px 6px 20px;\n color: #555555;\n font-size: 12px;\n border-top: 1px solid rgba(0, 0, 0, 0.06);\n transition: background 0.1s ease;\n}\n\n.cp-tooltip__device-item:hover {\n background: rgba(0, 0, 0, 0.03);\n}\n\n.cp-tooltip__device-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: #1976d2;\n flex-shrink: 0;\n}\n\n.cp-tooltip__device-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__no-devices {\n padding: 8px 14px;\n color: #888888;\n font-size: 12px;\n font-style: italic;\n}\n\n/* \u2500\u2500 Caret arrow pointing down \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__caret {\n width: 0;\n height: 0;\n border-left: 7px solid transparent;\n border-right: 7px solid transparent;\n border-top: 7px solid rgba(255, 255, 255, 0.97);\n margin: 0 auto;\n position: relative;\n top: -1px;\n filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.1));\n}\n\n/* \u2500\u2500 Data point rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__dp-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 4px 12px 4px 30px;\n font-size: 11.5px;\n border-top: 1px solid rgba(0, 0, 0, 0.05);\n min-height: 24px;\n}\n\n.cp-tooltip__dp-name {\n flex: 1;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: #666666;\n}\n\n/* Read-only state badge */\n.cp-tooltip__dp-badge {\n font-size: 10.5px;\n font-weight: 600;\n padding: 1px 7px;\n border-radius: 8px;\n flex-shrink: 0;\n background: #f0f2f5;\n color: #666666;\n min-width: 30px;\n text-align: center;\n letter-spacing: 0.2px;\n}\n\n.cp-tooltip__dp-badge.active {\n background: rgba(76, 175, 80, 0.15);\n color: #2e7d32;\n}\n\n/* Binary toggle button */\n.cp-tooltip__dp-toggle {\n font-size: 10.5px;\n font-weight: 600;\n padding: 2px 9px;\n border-radius: 8px;\n flex-shrink: 0;\n border: 1px solid #dddddd;\n background: #f8f9fa;\n color: #555555;\n cursor: pointer;\n min-width: 40px;\n text-align: center;\n transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;\n}\n\n.cp-tooltip__dp-toggle:hover {\n background: #eeeeee;\n border-color: #cccccc;\n}\n\n.cp-tooltip__dp-toggle.on {\n background: rgba(76, 175, 80, 0.15);\n border-color: rgba(76, 175, 80, 0.45);\n color: #2e7d32;\n}\n\n/* Number / float input */\n.cp-tooltip__dp-input {\n width: 62px;\n background: #f8f9fa;\n border: 1px solid #dddddd;\n border-radius: 4px;\n color: #333333;\n font-size: 11px;\n padding: 2px 5px;\n text-align: right;\n outline: none;\n flex-shrink: 0;\n}\n\n.cp-tooltip__dp-input:focus {\n border-color: #1976d2;\n}\n\n/* Enum select */\n.cp-tooltip__dp-select {\n background: #f8f9fa;\n border: 1px solid #dddddd;\n border-radius: 4px;\n color: #333333;\n font-size: 11px;\n padding: 2px 4px;\n outline: none;\n flex-shrink: 0;\n max-width: 96px;\n cursor: pointer;\n}\n\n.cp-tooltip__dp-select:focus {\n border-color: #1976d2;\n}\n\n/* Unit suffix */\n.cp-tooltip__dp-unit {\n font-size: 10px;\n color: #888888;\n flex-shrink: 0;\n margin-left: -4px;\n}\n";
33
33
  var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
34
34
  /**
35
35
  * @param {Object} sceneViewer - The sceneViewer instance
@@ -199,7 +199,8 @@ var SceneExportManager = /*#__PURE__*/function () {
199
199
 
200
200
  // Copy only essential userData properties (libraryId, objectType, direction, group)
201
201
  Object.keys(threeObject.userData).forEach(function (key) {
202
- if (!excludedKeys.has(key)) {
202
+ // Skip runtime/internal keys (e.g. _filteredBBoxCache from boundingBoxUtils)
203
+ if (!excludedKeys.has(key) && !key.startsWith('_')) {
203
204
  jsonObject.userData[key] = threeObject.userData[key];
204
205
  }
205
206
  });
@@ -31,7 +31,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
31
31
  * Initialize the CentralPlant manager
32
32
  *
33
33
  * @constructor
34
- * @version 0.3.36
34
+ * @version 0.3.38
35
35
  * @updated 2025-10-22
36
36
  *
37
37
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -97,6 +97,7 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
97
97
  entries.push({
98
98
  anim: anim,
99
99
  mesh: mesh,
100
+ deviceModelRoot: deviceModelRoot,
100
101
  origPos: mesh.position.clone(),
101
102
  origRot: mesh.rotation.clone(),
102
103
  origWorldPos: worldPos,
@@ -847,13 +848,9 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
847
848
  value: function _applyAnimation(entry, value) {
848
849
  var anim = entry.anim,
849
850
  mesh = entry.mesh,
850
- origPos = entry.origPos,
851
- origRot = entry.origRot,
852
- origWorldPos = entry.origWorldPos,
853
- origWorldQuat = entry.origWorldQuat,
854
- origWorldCenter = entry.origWorldCenter,
855
- deviceWorldQuat = entry.deviceWorldQuat,
856
- viewerMaxDim = entry.viewerMaxDim;
851
+ origPos = entry.origPos;
852
+ entry.origRot;
853
+ var viewerMaxDim = entry.viewerMaxDim;
857
854
  var mapping = this._resolveMapping(anim, value);
858
855
  if (!mapping) return;
859
856
  var types = anim.transformTypes || [];
@@ -865,7 +862,7 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
865
862
  if (type === 'translation') {
866
863
  this._applyTranslation(mesh, origPos, mapping.transform);
867
864
  } else if (type === 'rotation') {
868
- this._applyRotation(mesh, origPos, origRot, anim, mapping.rotationTransform, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim);
865
+ this._applyRotation(entry, anim, mapping.rotationTransform, viewerMaxDim);
869
866
  } else if (type === 'color') {
870
867
  this._applyColor(mesh, mapping.colorTransform);
871
868
  }
@@ -1009,25 +1006,71 @@ var IoBehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
1009
1006
  mesh.position.set(origPos.x - ((_transform$x = transform.x) !== null && _transform$x !== void 0 ? _transform$x : 0), origPos.y - ((_transform$y = transform.y) !== null && _transform$y !== void 0 ? _transform$y : 0), origPos.z + ((_transform$z = transform.z) !== null && _transform$z !== void 0 ? _transform$z : 0));
1010
1007
  }
1011
1008
 
1009
+ /**
1010
+ * Recompute world-space rest-pose transforms for a behavior entry using the
1011
+ * current parent/device world matrix. origPos/origRot are local rest pose and
1012
+ * stay valid after parent moves; world anchors must be derived at apply time.
1013
+ *
1014
+ * @param {{ mesh, origPos, origRot, deviceModelRoot }} entry
1015
+ * @returns {{ origWorldPos: THREE.Vector3, origWorldQuat: THREE.Quaternion, origWorldCenter: THREE.Vector3, deviceWorldQuat: THREE.Quaternion }|null}
1016
+ */
1017
+ }, {
1018
+ key: "_getRestWorldTransforms",
1019
+ value: function _getRestWorldTransforms(entry) {
1020
+ var mesh = entry.mesh,
1021
+ origPos = entry.origPos,
1022
+ origRot = entry.origRot,
1023
+ deviceModelRoot = entry.deviceModelRoot;
1024
+ if (!mesh || !origPos || !origRot) return null;
1025
+ var savedPos = mesh.position.clone();
1026
+ var savedQuat = mesh.quaternion.clone();
1027
+ mesh.position.copy(origPos);
1028
+ mesh.rotation.copy(origRot);
1029
+ mesh.updateMatrixWorld(true);
1030
+ var origWorldPos = new THREE.Vector3();
1031
+ mesh.getWorldPosition(origWorldPos);
1032
+ var origWorldQuat = new THREE.Quaternion();
1033
+ mesh.getWorldQuaternion(origWorldQuat);
1034
+ var box = new THREE.Box3().setFromObject(mesh);
1035
+ var origWorldCenter = new THREE.Vector3();
1036
+ if (!box.isEmpty()) box.getCenter(origWorldCenter);else origWorldCenter.copy(origWorldPos);
1037
+ var deviceWorldQuat = new THREE.Quaternion();
1038
+ if (deviceModelRoot) {
1039
+ deviceModelRoot.getWorldQuaternion(deviceWorldQuat);
1040
+ }
1041
+ mesh.position.copy(savedPos);
1042
+ mesh.quaternion.copy(savedQuat);
1043
+ return {
1044
+ origWorldPos: origWorldPos,
1045
+ origWorldQuat: origWorldQuat,
1046
+ origWorldCenter: origWorldCenter,
1047
+ deviceWorldQuat: deviceWorldQuat
1048
+ };
1049
+ }
1050
+
1012
1051
  /**
1013
1052
  * Apply a rotation around an arbitrary pivot point using world-space quaternion
1014
1053
  * math matching the ReconstructionViewer's setMeshPreviewRotationAxis approach.
1015
1054
  * This ensures the runtime axis/pivot matches what the user configured in the
1016
1055
  * animation dialog, regardless of the device's parent transform in the scene.
1017
1056
  *
1018
- * @param {THREE.Object3D} mesh
1019
- * @param {THREE.Vector3} origPos - local position at load time (unused, kept for signature compat)
1020
- * @param {THREE.Euler} origRot - local rotation at load time (unused)
1057
+ * @param {{ mesh, origPos, origRot, deviceModelRoot }} entry
1021
1058
  * @param {Object} anim
1022
1059
  * @param {number} angleDeg - Degrees
1023
- * @param {THREE.Vector3} origWorldPos - world position at load time
1024
- * @param {THREE.Quaternion} origWorldQuat - world quaternion at load time
1025
- * @param {THREE.Vector3} origWorldCenter - world bounding-box center at load time
1060
+ * @param {number} viewerMaxDim
1026
1061
  */
1027
1062
  }, {
1028
1063
  key: "_applyRotation",
1029
- value: function _applyRotation(mesh, origPos, origRot, anim, angleDeg, origWorldPos, origWorldQuat, origWorldCenter, deviceWorldQuat, viewerMaxDim) {
1064
+ value: function _applyRotation(entry, anim, angleDeg, viewerMaxDim) {
1030
1065
  var _anim$rotAxis, _anim$rotAxisOffset;
1066
+ var mesh = entry.mesh,
1067
+ origPos = entry.origPos,
1068
+ origRot = entry.origRot;
1069
+ var restWorld = this._getRestWorldTransforms(entry);
1070
+ var origWorldPos = restWorld === null || restWorld === void 0 ? void 0 : restWorld.origWorldPos;
1071
+ var origWorldQuat = restWorld === null || restWorld === void 0 ? void 0 : restWorld.origWorldQuat;
1072
+ var origWorldCenter = restWorld === null || restWorld === void 0 ? void 0 : restWorld.origWorldCenter;
1073
+ var deviceWorldQuat = restWorld === null || restWorld === void 0 ? void 0 : restWorld.deviceWorldQuat;
1031
1074
  var angle = THREE.MathUtils.degToRad(typeof angleDeg === 'number' ? angleDeg : 0);
1032
1075
  var axis = ((_anim$rotAxis = anim.rotAxis) !== null && _anim$rotAxis !== void 0 ? _anim$rotAxis : 'x').toLowerCase();
1033
1076
  var off = (_anim$rotAxisOffset = anim.rotAxisOffset) !== null && _anim$rotAxisOffset !== void 0 ? _anim$rotAxisOffset : {
@@ -5,7 +5,7 @@ import { BaseDisposable } from '../../core/baseDisposable.js';
5
5
  // ---------------------------------------------------------------------------
6
6
  // Inline styles (injected once into the document head)
7
7
  // ---------------------------------------------------------------------------
8
- var TOOLTIP_STYLES = "\n.cp-tooltip {\n position: absolute;\n pointer-events: auto;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 13px;\n line-height: 1.4;\n user-select: none;\n min-width: 200px;\n max-width: 280px;\n transform: translate(-50%, -100%);\n margin-top: -12px;\n transition: opacity 0.15s ease;\n}\n\n.cp-tooltip__card {\n background: rgba(28, 32, 40, 0.95);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 10px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);\n overflow: hidden;\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__header {\n padding: 10px 14px;\n font-weight: 600;\n font-size: 14px;\n color: #e8ecf1;\n background: rgba(255, 255, 255, 0.04);\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 I/O Devices section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__io-section {\n position: relative;\n}\n\n.cp-tooltip__io-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 14px;\n color: #a4adba;\n cursor: pointer;\n transition: background 0.12s ease, color 0.12s ease;\n}\n\n.cp-tooltip__io-trigger:hover {\n background: rgba(255, 255, 255, 0.06);\n color: #e8ecf1;\n}\n\n.cp-tooltip__io-trigger-label {\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n font-weight: 500;\n}\n\n.cp-tooltip__io-arrow {\n font-size: 10px;\n transition: transform 0.2s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__io-arrow {\n transform: rotate(90deg);\n}\n\n/* \u2500\u2500 Device list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__device-list {\n max-height: 0;\n overflow: hidden;\n transition: max-height 0.25s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__device-list {\n max-height: 300px;\n}\n\n.cp-tooltip__device-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 14px 6px 20px;\n color: #c1c8d1;\n font-size: 12px;\n border-top: 1px solid rgba(255, 255, 255, 0.04);\n transition: background 0.1s ease;\n}\n\n.cp-tooltip__device-item:hover {\n background: rgba(255, 255, 255, 0.04);\n}\n\n.cp-tooltip__device-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: #4fc3f7;\n flex-shrink: 0;\n}\n\n.cp-tooltip__device-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__no-devices {\n padding: 8px 14px;\n color: #6b7280;\n font-size: 12px;\n font-style: italic;\n}\n\n/* \u2500\u2500 Caret arrow pointing down \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__caret {\n width: 0;\n height: 0;\n border-left: 7px solid transparent;\n border-right: 7px solid transparent;\n border-top: 7px solid rgba(28, 32, 40, 0.95);\n margin: 0 auto;\n position: relative;\n top: -1px;\n}\n\n/* \u2500\u2500 Data point rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__dp-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 4px 12px 4px 30px;\n font-size: 11.5px;\n border-top: 1px solid rgba(255, 255, 255, 0.025);\n min-height: 24px;\n}\n\n.cp-tooltip__dp-name {\n flex: 1;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: #7c8d9e;\n}\n\n/* Read-only state badge */\n.cp-tooltip__dp-badge {\n font-size: 10.5px;\n font-weight: 600;\n padding: 1px 7px;\n border-radius: 8px;\n flex-shrink: 0;\n background: rgba(255, 255, 255, 0.07);\n color: #9aabb8;\n min-width: 30px;\n text-align: center;\n letter-spacing: 0.2px;\n}\n\n.cp-tooltip__dp-badge.active {\n background: rgba(76, 175, 80, 0.28);\n color: #a5d6a7;\n}\n\n/* Binary toggle button */\n.cp-tooltip__dp-toggle {\n font-size: 10.5px;\n font-weight: 600;\n padding: 2px 9px;\n border-radius: 8px;\n flex-shrink: 0;\n border: 1px solid rgba(255, 255, 255, 0.14);\n background: rgba(255, 255, 255, 0.06);\n color: #c1c8d1;\n cursor: pointer;\n min-width: 40px;\n text-align: center;\n transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;\n}\n\n.cp-tooltip__dp-toggle:hover {\n background: rgba(255, 255, 255, 0.12);\n border-color: rgba(255, 255, 255, 0.25);\n}\n\n.cp-tooltip__dp-toggle.on {\n background: rgba(76, 175, 80, 0.28);\n border-color: rgba(76, 175, 80, 0.55);\n color: #a5d6a7;\n}\n\n/* Number / float input */\n.cp-tooltip__dp-input {\n width: 62px;\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 4px;\n color: #e8ecf1;\n font-size: 11px;\n padding: 2px 5px;\n text-align: right;\n outline: none;\n flex-shrink: 0;\n}\n\n.cp-tooltip__dp-input:focus {\n border-color: rgba(79, 195, 247, 0.55);\n}\n\n/* Enum select */\n.cp-tooltip__dp-select {\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 4px;\n color: #e8ecf1;\n font-size: 11px;\n padding: 2px 4px;\n outline: none;\n flex-shrink: 0;\n max-width: 96px;\n cursor: pointer;\n}\n\n.cp-tooltip__dp-select:focus {\n border-color: rgba(79, 195, 247, 0.55);\n}\n\n/* Unit suffix */\n.cp-tooltip__dp-unit {\n font-size: 10px;\n color: #505c68;\n flex-shrink: 0;\n margin-left: -4px;\n}\n";
8
+ var TOOLTIP_STYLES = "\n.cp-tooltip {\n position: absolute;\n pointer-events: auto;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 13px;\n line-height: 1.4;\n user-select: none;\n min-width: 200px;\n max-width: 280px;\n transform: translate(-50%, -100%);\n margin-top: -12px;\n transition: opacity 0.15s ease;\n color-scheme: light;\n}\n\n.cp-tooltip__card {\n background: rgba(255, 255, 255, 0.97);\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: 10px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n overflow: hidden;\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__header {\n padding: 10px 14px;\n font-weight: 600;\n font-size: 14px;\n color: #2c3e50;\n background: rgba(0, 0, 0, 0.02);\n border-bottom: 1px solid rgba(0, 0, 0, 0.08);\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 I/O Devices section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__io-section {\n position: relative;\n}\n\n.cp-tooltip__io-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 14px;\n color: #666666;\n cursor: pointer;\n transition: background 0.12s ease, color 0.12s ease;\n}\n\n.cp-tooltip__io-trigger:hover {\n background: rgba(0, 0, 0, 0.04);\n color: #333333;\n}\n\n.cp-tooltip__io-trigger-label {\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n font-weight: 500;\n}\n\n.cp-tooltip__io-arrow {\n font-size: 10px;\n transition: transform 0.2s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__io-arrow {\n transform: rotate(90deg);\n}\n\n/* \u2500\u2500 Device list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__device-list {\n max-height: 0;\n overflow: hidden;\n transition: max-height 0.25s ease;\n}\n\n.cp-tooltip__io-section.expanded .cp-tooltip__device-list {\n max-height: 300px;\n}\n\n.cp-tooltip__device-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 14px 6px 20px;\n color: #555555;\n font-size: 12px;\n border-top: 1px solid rgba(0, 0, 0, 0.06);\n transition: background 0.1s ease;\n}\n\n.cp-tooltip__device-item:hover {\n background: rgba(0, 0, 0, 0.03);\n}\n\n.cp-tooltip__device-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: #1976d2;\n flex-shrink: 0;\n}\n\n.cp-tooltip__device-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__no-devices {\n padding: 8px 14px;\n color: #888888;\n font-size: 12px;\n font-style: italic;\n}\n\n/* \u2500\u2500 Caret arrow pointing down \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__caret {\n width: 0;\n height: 0;\n border-left: 7px solid transparent;\n border-right: 7px solid transparent;\n border-top: 7px solid rgba(255, 255, 255, 0.97);\n margin: 0 auto;\n position: relative;\n top: -1px;\n filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.1));\n}\n\n/* \u2500\u2500 Data point rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cp-tooltip__dp-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 4px 12px 4px 30px;\n font-size: 11.5px;\n border-top: 1px solid rgba(0, 0, 0, 0.05);\n min-height: 24px;\n}\n\n.cp-tooltip__dp-name {\n flex: 1;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: #666666;\n}\n\n/* Read-only state badge */\n.cp-tooltip__dp-badge {\n font-size: 10.5px;\n font-weight: 600;\n padding: 1px 7px;\n border-radius: 8px;\n flex-shrink: 0;\n background: #f0f2f5;\n color: #666666;\n min-width: 30px;\n text-align: center;\n letter-spacing: 0.2px;\n}\n\n.cp-tooltip__dp-badge.active {\n background: rgba(76, 175, 80, 0.15);\n color: #2e7d32;\n}\n\n/* Binary toggle button */\n.cp-tooltip__dp-toggle {\n font-size: 10.5px;\n font-weight: 600;\n padding: 2px 9px;\n border-radius: 8px;\n flex-shrink: 0;\n border: 1px solid #dddddd;\n background: #f8f9fa;\n color: #555555;\n cursor: pointer;\n min-width: 40px;\n text-align: center;\n transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;\n}\n\n.cp-tooltip__dp-toggle:hover {\n background: #eeeeee;\n border-color: #cccccc;\n}\n\n.cp-tooltip__dp-toggle.on {\n background: rgba(76, 175, 80, 0.15);\n border-color: rgba(76, 175, 80, 0.45);\n color: #2e7d32;\n}\n\n/* Number / float input */\n.cp-tooltip__dp-input {\n width: 62px;\n background: #f8f9fa;\n border: 1px solid #dddddd;\n border-radius: 4px;\n color: #333333;\n font-size: 11px;\n padding: 2px 5px;\n text-align: right;\n outline: none;\n flex-shrink: 0;\n}\n\n.cp-tooltip__dp-input:focus {\n border-color: #1976d2;\n}\n\n/* Enum select */\n.cp-tooltip__dp-select {\n background: #f8f9fa;\n border: 1px solid #dddddd;\n border-radius: 4px;\n color: #333333;\n font-size: 11px;\n padding: 2px 4px;\n outline: none;\n flex-shrink: 0;\n max-width: 96px;\n cursor: pointer;\n}\n\n.cp-tooltip__dp-select:focus {\n border-color: #1976d2;\n}\n\n/* Unit suffix */\n.cp-tooltip__dp-unit {\n font-size: 10px;\n color: #888888;\n flex-shrink: 0;\n margin-left: -4px;\n}\n";
9
9
  var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
10
10
  /**
11
11
  * @param {Object} sceneViewer - The sceneViewer instance
@@ -177,7 +177,8 @@ var SceneExportManager = /*#__PURE__*/function () {
177
177
 
178
178
  // Copy only essential userData properties (libraryId, objectType, direction, group)
179
179
  Object.keys(threeObject.userData).forEach(function (key) {
180
- if (!excludedKeys.has(key)) {
180
+ // Skip runtime/internal keys (e.g. _filteredBBoxCache from boundingBoxUtils)
181
+ if (!excludedKeys.has(key) && !key.startsWith('_')) {
181
182
  jsonObject.userData[key] = threeObject.userData[key];
182
183
  }
183
184
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2112-lab/central-plant",
3
- "version": "0.3.36",
3
+ "version": "0.3.38",
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",