@2112-lab/central-plant 0.3.26 → 0.3.28

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.
Files changed (28) hide show
  1. package/dist/bundle/index.js +922 -974
  2. package/dist/cjs/src/core/centralPlant.js +8 -115
  3. package/dist/cjs/src/core/centralPlantInternals.js +23 -17
  4. package/dist/cjs/src/core/sceneViewer.js +55 -2
  5. package/dist/cjs/src/index.js +0 -2
  6. package/dist/cjs/src/managers/behaviors/IoAnimationManager.js +24 -1
  7. package/dist/cjs/src/managers/behaviors/IoOutlineManager.js +258 -0
  8. package/dist/cjs/src/managers/controls/transformControlsManager.js +319 -43
  9. package/dist/cjs/src/managers/scene/animationManager.js +9 -2
  10. package/dist/cjs/src/managers/scene/componentTooltipManager.js +190 -34
  11. package/dist/cjs/src/managers/scene/modelManager.js +15 -1
  12. package/dist/cjs/src/managers/scene/sceneExportManager.js +3 -29
  13. package/dist/cjs/src/managers/scene/sceneOperationsManager.js +12 -289
  14. package/dist/cjs/src/utils/boundingBoxUtils.js +38 -40
  15. package/dist/esm/src/core/centralPlant.js +8 -115
  16. package/dist/esm/src/core/centralPlantInternals.js +23 -17
  17. package/dist/esm/src/core/sceneViewer.js +55 -2
  18. package/dist/esm/src/index.js +0 -1
  19. package/dist/esm/src/managers/behaviors/IoAnimationManager.js +24 -1
  20. package/dist/esm/src/managers/behaviors/IoOutlineManager.js +234 -0
  21. package/dist/esm/src/managers/controls/transformControlsManager.js +319 -43
  22. package/dist/esm/src/managers/scene/animationManager.js +9 -2
  23. package/dist/esm/src/managers/scene/componentTooltipManager.js +191 -35
  24. package/dist/esm/src/managers/scene/modelManager.js +16 -2
  25. package/dist/esm/src/managers/scene/sceneExportManager.js +4 -30
  26. package/dist/esm/src/managers/scene/sceneOperationsManager.js +12 -289
  27. package/dist/esm/src/utils/boundingBoxUtils.js +39 -42
  28. package/package.json +1 -1
@@ -3611,6 +3611,34 @@ function computeIODeviceBoundingBoxes(componentObject) {
3611
3611
  * const helpers = createSelectionBoxHelpers(pumpModel, 0x00ff00)
3612
3612
  * helpers.forEach(h => scene.add(h))
3613
3613
  */
3614
+ /**
3615
+ * Returns a filtered bounding box for `object`, using a cache stored on
3616
+ * `object.userData._filteredBBoxCache`. The cache key is the serialised
3617
+ * world-matrix elements string; if the object has moved the cache is
3618
+ * automatically invalidated and recomputed.
3619
+ *
3620
+ * This avoids the expensive full-traverse on every selection event for
3621
+ * large smart components with many child meshes.
3622
+ *
3623
+ * @param {THREE.Object3D} object
3624
+ * @param {string[]} excludeTypes
3625
+ * @returns {THREE.Box3}
3626
+ */
3627
+ function computeFilteredBoundingBoxCached(object, excludeTypes) {
3628
+ object.updateMatrixWorld(true);
3629
+ var matrixKey = object.matrixWorld.elements.join(',');
3630
+ var cache = object.userData._filteredBBoxCache;
3631
+ if (cache && cache.matrixKey === matrixKey) {
3632
+ return new THREE__namespace.Box3(_construct(THREE__namespace.Vector3, _toConsumableArray(cache.min)), _construct(THREE__namespace.Vector3, _toConsumableArray(cache.max)));
3633
+ }
3634
+ var box = computeFilteredBoundingBox(object, excludeTypes);
3635
+ object.userData._filteredBBoxCache = {
3636
+ matrixKey: matrixKey,
3637
+ min: box.min.toArray(),
3638
+ max: box.max.toArray()
3639
+ };
3640
+ return box;
3641
+ }
3614
3642
  function createSelectionBoxHelpers(object) {
3615
3643
  var _object$children;
3616
3644
  var color = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0x00ff00;
@@ -3624,7 +3652,7 @@ function createSelectionBoxHelpers(object) {
3624
3652
  });
3625
3653
  if (hasIODevices) {
3626
3654
  // 1. Create filtered helper for the component body
3627
- var filteredBox = computeFilteredBoundingBox(object, excludeTypes);
3655
+ var filteredBox = computeFilteredBoundingBoxCached(object, excludeTypes);
3628
3656
  var componentHelper = _createBoxHelperFromBox3(filteredBox, color);
3629
3657
  componentHelper.isHelper = true;
3630
3658
  componentHelper.userData = {
@@ -3634,33 +3662,6 @@ function createSelectionBoxHelpers(object) {
3634
3662
  excludeTypes: excludeTypes
3635
3663
  };
3636
3664
  helpers.push(componentHelper);
3637
-
3638
- // 2. Create individual helpers for each io-device
3639
- var _iterator2 = _createForOfIteratorHelper(object.children),
3640
- _step2;
3641
- try {
3642
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
3643
- var _child$userData3;
3644
- var child = _step2.value;
3645
- if (((_child$userData3 = child.userData) === null || _child$userData3 === void 0 ? void 0 : _child$userData3.objectType) !== 'io-device') continue;
3646
- var deviceBox = new THREE__namespace.Box3().setFromObject(child);
3647
- if (deviceBox.isEmpty()) continue;
3648
- var deviceHelper = _createBoxHelperFromBox3(deviceBox, color);
3649
- deviceHelper.isHelper = true;
3650
- deviceHelper.userData = {
3651
- isBoundingBox: true,
3652
- sourceObjectUuid: child.uuid,
3653
- isFiltered: false,
3654
- isIODevice: true,
3655
- parentComponentUuid: object.uuid
3656
- };
3657
- helpers.push(deviceHelper);
3658
- }
3659
- } catch (err) {
3660
- _iterator2.e(err);
3661
- } finally {
3662
- _iterator2.f();
3663
- }
3664
3665
  } else {
3665
3666
  // Standard BoxHelper for non-smart objects
3666
3667
  var boxHelper = new THREE__namespace.BoxHelper(object, color);
@@ -3684,11 +3685,11 @@ function createSelectionBoxHelpers(object) {
3684
3685
  * @param {THREE.Scene} scene - The scene (for finding objects by uuid)
3685
3686
  */
3686
3687
  function updateSelectionBoxHelpers(helpers, selectedObjects, scene) {
3687
- var _iterator3 = _createForOfIteratorHelper(helpers),
3688
- _step3;
3688
+ var _iterator2 = _createForOfIteratorHelper(helpers),
3689
+ _step2;
3689
3690
  try {
3690
3691
  var _loop = function _loop() {
3691
- var helper = _step3.value;
3692
+ var helper = _step2.value;
3692
3693
  var _helper$userData = helper.userData,
3693
3694
  sourceObjectUuid = _helper$userData.sourceObjectUuid,
3694
3695
  isFiltered = _helper$userData.isFiltered,
@@ -3711,25 +3712,21 @@ function updateSelectionBoxHelpers(helpers, selectedObjects, scene) {
3711
3712
  if (!sourceObject) return 1; // continue
3712
3713
  sourceObject.updateMatrixWorld(true);
3713
3714
  if (isFiltered && excludeTypes) {
3714
- // Recompute filtered bbox
3715
- var box = computeFilteredBoundingBox(sourceObject, excludeTypes);
3715
+ // Recompute filtered bbox (uses cache when the object hasn't moved)
3716
+ var box = computeFilteredBoundingBoxCached(sourceObject, excludeTypes);
3716
3717
  _updateBoxHelperPositions(helper, box);
3717
- } else if (isIODevice) {
3718
- // Recompute io-device bbox
3719
- var _box = new THREE__namespace.Box3().setFromObject(sourceObject);
3720
- _updateBoxHelperPositions(helper, _box);
3721
3718
  } else if (helper.update) {
3722
3719
  // Standard BoxHelper — use built-in update
3723
3720
  helper.update();
3724
3721
  }
3725
3722
  };
3726
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
3723
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
3727
3724
  if (_loop()) continue;
3728
3725
  }
3729
3726
  } catch (err) {
3730
- _iterator3.e(err);
3727
+ _iterator2.e(err);
3731
3728
  } finally {
3732
- _iterator3.f();
3729
+ _iterator2.f();
3733
3730
  }
3734
3731
  }
3735
3732
 
@@ -3823,8 +3820,17 @@ var TransformControlsManager = /*#__PURE__*/function () {
3823
3820
  onModeChange: null,
3824
3821
  onObjectRemoved: null,
3825
3822
  onSelectionChanged: null,
3826
- onIODeviceClick: null
3823
+ onIODeviceClick: null,
3824
+ onIODeviceDrag: null,
3825
+ onIODeviceDragEnd: null
3827
3826
  };
3827
+
3828
+ // IO device drag tracking state
3829
+ this._ioDragMesh = null;
3830
+ this._ioDragStartX = 0;
3831
+ this._ioDragStartY = 0;
3832
+ this._ioDragMoved = false;
3833
+ this._suppressNextClick = false;
3828
3834
  this.init();
3829
3835
  }
3830
3836
 
@@ -3904,6 +3910,12 @@ var TransformControlsManager = /*#__PURE__*/function () {
3904
3910
  if (callbacks.onIODeviceClick) {
3905
3911
  this.callbacks.onIODeviceClick = callbacks.onIODeviceClick;
3906
3912
  }
3913
+ if (callbacks.onIODeviceDrag) {
3914
+ this.callbacks.onIODeviceDrag = callbacks.onIODeviceDrag;
3915
+ }
3916
+ if (callbacks.onIODeviceDragEnd) {
3917
+ this.callbacks.onIODeviceDragEnd = callbacks.onIODeviceDragEnd;
3918
+ }
3907
3919
  console.log('🔗 Transform controls callbacks registered');
3908
3920
  }
3909
3921
  /**
@@ -4110,6 +4122,11 @@ var TransformControlsManager = /*#__PURE__*/function () {
4110
4122
  value: function setupKeyboardControls() {
4111
4123
  var _this3 = this;
4112
4124
  this.eventHandlers.keydown = function (event) {
4125
+ // Restore perspective camera when pressing Escape from ortho face view
4126
+ if (event.code === 'Escape') {
4127
+ _this3._restorePerspectiveCamera();
4128
+ }
4129
+
4113
4130
  // Only handle keys when transform controls are active
4114
4131
  if (!_this3.transformControls.enabled || _this3.transformState.isTransforming) {
4115
4132
  return;
@@ -4144,9 +4161,82 @@ var TransformControlsManager = /*#__PURE__*/function () {
4144
4161
  var raycaster = new THREE__namespace.Raycaster();
4145
4162
  var mouse = new THREE__namespace.Vector2();
4146
4163
 
4147
- // Click handler: first click selects (bounding box), second click on same object shows tooltip
4164
+ // ── IO device drag ────────────────────────────────────────────────────
4165
+ // Detect pointerdown on an IO device mesh and convert a drag gesture into
4166
+ // state changes. Up/right = positive direction, down/left = negative.
4167
+ this.eventHandlers.pointerdown = function (event) {
4168
+ if (_this4.transformState.isTransforming) return;
4169
+ if (!_this4.callbacks.onIODeviceDrag) return;
4170
+ _this4._calculateMousePosition(event, mouse);
4171
+ raycaster.setFromCamera(mouse, _this4.camera);
4172
+ var allIntersects = raycaster.intersectObjects(_this4.scene.children, true);
4173
+ var ioDeviceObject = null;
4174
+ var _iterator = _createForOfIteratorHelper(allIntersects),
4175
+ _step;
4176
+ try {
4177
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
4178
+ var hit = _step.value;
4179
+ var obj = hit.object;
4180
+ while (obj) {
4181
+ var _obj$userData;
4182
+ if (((_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType) === 'io-device') {
4183
+ ioDeviceObject = obj;
4184
+ break;
4185
+ }
4186
+ obj = obj.parent;
4187
+ }
4188
+ if (ioDeviceObject) break;
4189
+ }
4190
+ } catch (err) {
4191
+ _iterator.e(err);
4192
+ } finally {
4193
+ _iterator.f();
4194
+ }
4195
+ if (!ioDeviceObject) return;
4196
+
4197
+ // Begin session
4198
+ _this4._ioDragMesh = ioDeviceObject;
4199
+ _this4._ioDragStartX = event.clientX;
4200
+ _this4._ioDragStartY = event.clientY;
4201
+ _this4._ioDragMoved = false;
4202
+ if (_this4.orbitControls) _this4.orbitControls.enabled = false;
4203
+ _this4.callbacks.onIODeviceDrag(ioDeviceObject, 0, true);
4204
+ var onMove = function onMove(e) {
4205
+ var dx = e.clientX - _this4._ioDragStartX;
4206
+ var dy = e.clientY - _this4._ioDragStartY;
4207
+ if (Math.abs(dx) > 4 || Math.abs(dy) > 4) _this4._ioDragMoved = true;
4208
+ // Up (−screenY) and right (+screenX) are positive
4209
+ var signedDelta = _this4._ioDragStartY - e.clientY + (e.clientX - _this4._ioDragStartX);
4210
+ _this4.callbacks.onIODeviceDrag(_this4._ioDragMesh, signedDelta, false);
4211
+ };
4212
+ var _onUp = function onUp() {
4213
+ window.removeEventListener('pointermove', onMove);
4214
+ window.removeEventListener('pointerup', _onUp);
4215
+ if (_this4.orbitControls) _this4.orbitControls.enabled = true;
4216
+ if (_this4._ioDragMoved) {
4217
+ // Suppress the click event that will fire after this pointerup
4218
+ _this4._suppressNextClick = true;
4219
+ }
4220
+ if (_this4.callbacks.onIODeviceDragEnd) {
4221
+ _this4.callbacks.onIODeviceDragEnd(_this4._ioDragMesh);
4222
+ }
4223
+ _this4._ioDragMesh = null;
4224
+ };
4225
+ window.addEventListener('pointermove', onMove);
4226
+ window.addEventListener('pointerup', _onUp);
4227
+ };
4228
+ this.renderer.domElement.addEventListener('pointerdown', this.eventHandlers.pointerdown);
4229
+
4230
+ // Click handler: left-click selects the object (bounding box + transform controls).
4231
+ // Right-click on a component shows the tooltip.
4148
4232
  this.eventHandlers.click = function (event) {
4149
4233
  var _targetObject$userDat;
4234
+ // Suppress click that follows an IO device drag
4235
+ if (_this4._suppressNextClick) {
4236
+ _this4._suppressNextClick = false;
4237
+ return;
4238
+ }
4239
+
4150
4240
  // Skip if currently transforming
4151
4241
  if (_this4.transformState.isTransforming) {
4152
4242
  return;
@@ -4159,15 +4249,15 @@ var TransformControlsManager = /*#__PURE__*/function () {
4159
4249
  // Check for direct io-device mesh click (before bounding box selection)
4160
4250
  if (_this4.callbacks.onIODeviceClick) {
4161
4251
  var allIntersects = raycaster.intersectObjects(_this4.scene.children, true);
4162
- var _iterator = _createForOfIteratorHelper(allIntersects),
4163
- _step;
4252
+ var _iterator2 = _createForOfIteratorHelper(allIntersects),
4253
+ _step2;
4164
4254
  try {
4165
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
4166
- var hit = _step.value;
4255
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
4256
+ var hit = _step2.value;
4167
4257
  var obj = hit.object;
4168
4258
  while (obj) {
4169
- var _obj$userData;
4170
- if (((_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType) === 'io-device') {
4259
+ var _obj$userData2;
4260
+ if (((_obj$userData2 = obj.userData) === null || _obj$userData2 === void 0 ? void 0 : _obj$userData2.objectType) === 'io-device') {
4171
4261
  _this4.callbacks.onIODeviceClick(obj);
4172
4262
  return;
4173
4263
  }
@@ -4175,9 +4265,9 @@ var TransformControlsManager = /*#__PURE__*/function () {
4175
4265
  }
4176
4266
  }
4177
4267
  } catch (err) {
4178
- _iterator.e(err);
4268
+ _iterator2.e(err);
4179
4269
  } finally {
4180
- _iterator.f();
4270
+ _iterator2.f();
4181
4271
  }
4182
4272
  }
4183
4273
 
@@ -4212,23 +4302,194 @@ var TransformControlsManager = /*#__PURE__*/function () {
4212
4302
  return;
4213
4303
  }
4214
4304
 
4215
- // Check if the clicked object is already selected
4216
- var isAlreadySelected = _this4.selectedObjects.length === 1 && _this4.selectedObjects[0] === targetObject;
4217
- if (isAlreadySelected) {
4218
- // Second click on already-selected object: Full selection including tooltips
4219
- _this4.selectObject(targetObject);
4220
- } else {
4221
- // First click on a new object: Transform controls and bounding box only
4222
- // Always deselect first (this clears tooltips via the callback)
4305
+ // Left-click: select for transform only (bounding box, no tooltip)
4306
+ if (!_this4.selectedObjects.includes(targetObject)) {
4223
4307
  _this4.deselectObject();
4224
-
4225
- // Then select the new object if there is one (this shows transform controls and bounding box)
4226
- if (targetObject) {
4227
- _this4.selectObjectForTransformOnly(targetObject);
4228
- }
4229
4308
  }
4309
+ _this4.selectObjectForTransformOnly(targetObject);
4230
4310
  };
4231
4311
  this.renderer.domElement.addEventListener('click', this.eventHandlers.click);
4312
+
4313
+ // Right-click handler: show tooltip for the clicked component.
4314
+ this.eventHandlers.contextmenu = function (event) {
4315
+ var _targetObject$userDat2;
4316
+ event.preventDefault();
4317
+ if (_this4.transformState.isTransforming) return;
4318
+ _this4._calculateMousePosition(event, mouse);
4319
+ raycaster.setFromCamera(mouse, _this4.camera);
4320
+ var targetObject = _this4._findTargetObject(raycaster, objectFilter);
4321
+ if (!targetObject) return;
4322
+ var objectType = (_targetObject$userDat2 = targetObject.userData) === null || _targetObject$userDat2 === void 0 ? void 0 : _targetObject$userDat2.objectType;
4323
+ if (objectType !== 'component' && objectType !== 'gateway' && objectType !== 'segment') return;
4324
+
4325
+ // Ensure the object is selected, then show the full tooltip.
4326
+ if (!_this4.selectedObjects.includes(targetObject)) {
4327
+ _this4.deselectObject();
4328
+ _this4.selectObjectForTransformOnly(targetObject);
4329
+ }
4330
+ _this4.selectObject(targetObject);
4331
+ };
4332
+ this.renderer.domElement.addEventListener('contextmenu', this.eventHandlers.contextmenu);
4333
+
4334
+ // Double-click handler: switch to orthographic camera fixed to the clicked face.
4335
+ // Press Escape to restore the perspective camera.
4336
+ this.eventHandlers.dblclick = function (event) {
4337
+ if (_this4.transformState.isTransforming) return;
4338
+ var sv = _this4.sceneViewer;
4339
+ if (!sv) return;
4340
+ _this4._calculateMousePosition(event, mouse);
4341
+ raycaster.setFromCamera(_this4.camera, _this4.camera); // ensure raycaster uses current camera
4342
+ raycaster.setFromCamera(mouse, _this4.camera);
4343
+
4344
+ // Raycast directly against scene meshes to obtain the face normal
4345
+ var allHits = raycaster.intersectObjects(_this4.scene.children, true).filter(function (h) {
4346
+ var _h$object$userData, _h$object$userData2;
4347
+ return !((_h$object$userData = h.object.userData) !== null && _h$object$userData !== void 0 && _h$object$userData.isTransformControls) && !((_h$object$userData2 = h.object.userData) !== null && _h$object$userData2 !== void 0 && _h$object$userData2.isBoundingBox);
4348
+ });
4349
+ if (!allHits.length || !allHits[0].face) return;
4350
+ var hit = allHits[0];
4351
+
4352
+ // Walk up to find the component root
4353
+ var componentObj = hit.object;
4354
+ while (componentObj) {
4355
+ var _componentObj$userDat;
4356
+ if (((_componentObj$userDat = componentObj.userData) === null || _componentObj$userDat === void 0 ? void 0 : _componentObj$userDat.objectType) === 'component') break;
4357
+ componentObj = componentObj.parent;
4358
+ }
4359
+ if (!componentObj) return;
4360
+
4361
+ // Transform face normal from local object space to world space
4362
+ var normalMatrix = new THREE__namespace.Matrix3().getNormalMatrix(hit.object.matrixWorld);
4363
+ var worldNormal = hit.face.normal.clone().applyMatrix3(normalMatrix).normalize();
4364
+
4365
+ // Snap to the closest cardinal axis (Z-up: ±X, ±Y, ±Z)
4366
+ var cardinals = [new THREE__namespace.Vector3(1, 0, 0), new THREE__namespace.Vector3(-1, 0, 0), new THREE__namespace.Vector3(0, 1, 0), new THREE__namespace.Vector3(0, -1, 0), new THREE__namespace.Vector3(0, 0, 1), new THREE__namespace.Vector3(0, 0, -1)];
4367
+ var faceDir = cardinals.reduce(function (best, c) {
4368
+ return c.dot(worldNormal) > best.dot(worldNormal) ? c : best;
4369
+ });
4370
+
4371
+ // Bounding box of the component
4372
+ var bbox = new THREE__namespace.Box3().setFromObject(componentObj);
4373
+ var center = bbox.getCenter(new THREE__namespace.Vector3());
4374
+ var size = bbox.getSize(new THREE__namespace.Vector3());
4375
+
4376
+ // Ortho half-extents: axes perpendicular to the face direction
4377
+ var isXFace = Math.abs(faceDir.x) > 0.5;
4378
+ var isYFace = Math.abs(faceDir.y) > 0.5;
4379
+ var halfW, halfH;
4380
+ if (isXFace) {
4381
+ halfW = size.y / 2;
4382
+ halfH = size.z / 2;
4383
+ } else if (isYFace) {
4384
+ halfW = size.x / 2;
4385
+ halfH = size.z / 2;
4386
+ } else {
4387
+ halfW = size.x / 2;
4388
+ halfH = size.y / 2;
4389
+ }
4390
+ var padding = 1.6;
4391
+ var domEl = sv.renderer.domElement;
4392
+ var aspect = domEl.clientWidth / (domEl.clientHeight || 1);
4393
+
4394
+ // Frustum must satisfy both:
4395
+ // frustumHalfW >= halfW * padding (object fits horizontally)
4396
+ // frustumHalfH >= halfH * padding (object fits vertically)
4397
+ // frustumHalfW / frustumHalfH == aspect (no squishing)
4398
+ var minHalfH = halfH * padding;
4399
+ var minHalfW = halfW * padding;
4400
+ var frustumHalfH = Math.max(minHalfH, minHalfW / aspect);
4401
+ var frustumHalfW = frustumHalfH * aspect;
4402
+ var isZFace = Math.abs(faceDir.z) > 0.5;
4403
+ var upVec = isZFace ? new THREE__namespace.Vector3(0, 1, 0) : new THREE__namespace.Vector3(0, 0, 1);
4404
+
4405
+ // Store the perspective camera snapshot on first entry
4406
+ if (!sv._perspCameraSnapshot) {
4407
+ var _sv$controls$target$c, _sv$controls, _sv$controls2, _sv$controls3, _sv$controls4;
4408
+ sv._perspCameraSnapshot = {
4409
+ camera: sv.camera,
4410
+ controlsTarget: (_sv$controls$target$c = (_sv$controls = sv.controls) === null || _sv$controls === void 0 || (_sv$controls = _sv$controls.target) === null || _sv$controls === void 0 ? void 0 : _sv$controls.clone()) !== null && _sv$controls$target$c !== void 0 ? _sv$controls$target$c : new THREE__namespace.Vector3(),
4411
+ controlsMinDist: (_sv$controls2 = sv.controls) === null || _sv$controls2 === void 0 ? void 0 : _sv$controls2.minDistance,
4412
+ controlsMaxDist: (_sv$controls3 = sv.controls) === null || _sv$controls3 === void 0 ? void 0 : _sv$controls3.maxDistance,
4413
+ controlsMaxPolar: (_sv$controls4 = sv.controls) === null || _sv$controls4 === void 0 ? void 0 : _sv$controls4.maxPolarAngle
4414
+ };
4415
+ }
4416
+
4417
+ // Build the orthographic camera
4418
+ var ortho = new THREE__namespace.OrthographicCamera(-frustumHalfW, frustumHalfW, frustumHalfH, -frustumHalfH, 0.01, 1000);
4419
+ ortho.up.copy(upVec);
4420
+ var dist = Math.max(size.x, size.y, size.z) * 3;
4421
+ ortho.position.copy(center).addScaledVector(faceDir, dist);
4422
+ ortho.lookAt(center);
4423
+ // Store half-extents for resize updates (pre-aspect values so resize can recompute)
4424
+ ortho.userData._orthoHalfExtents = {
4425
+ halfW: halfW * padding,
4426
+ halfH: halfH * padding
4427
+ };
4428
+ ortho.updateProjectionMatrix();
4429
+ sv.camera = ortho;
4430
+ _this4.camera = ortho;
4431
+ if (sv.controls) {
4432
+ sv.controls.object = ortho;
4433
+ sv.controls.target.copy(center);
4434
+ sv.controls.minDistance = 0.1;
4435
+ sv.controls.maxDistance = 500;
4436
+ sv.controls.maxPolarAngle = Math.PI;
4437
+ sv.controls.update();
4438
+
4439
+ // Exit ortho mode when the camera actually moves (orbit/pan/zoom).
4440
+ // Using 'change' instead of 'start' so a simple click (e.g. io-device
4441
+ // toggle) never triggers the exit — only real camera movement does.
4442
+ // Skip the first 'change' that fires from controls.update() below.
4443
+ var _skipChanges = 1;
4444
+ var _onOrbitChange = function onOrbitChange() {
4445
+ if (_skipChanges-- > 0) return;
4446
+ sv.controls.removeEventListener('change', _onOrbitChange);
4447
+ _this4._orthoOrbitStartListener = null;
4448
+ _this4._restorePerspectiveCamera();
4449
+ };
4450
+ sv.controls.addEventListener('change', _onOrbitChange);
4451
+ _this4._orthoOrbitStartListener = {
4452
+ controls: sv.controls,
4453
+ fn: _onOrbitChange
4454
+ };
4455
+ }
4456
+ console.log('📐 Switched to orthographic face view:', faceDir);
4457
+ };
4458
+ this.renderer.domElement.addEventListener('dblclick', this.eventHandlers.dblclick);
4459
+ }
4460
+
4461
+ /**
4462
+ * Restore the perspective camera after an orthographic face view.
4463
+ * Safe to call when not in ortho mode (no-op).
4464
+ * @private
4465
+ */
4466
+ }, {
4467
+ key: "_restorePerspectiveCamera",
4468
+ value: function _restorePerspectiveCamera() {
4469
+ var sv = this.sceneViewer;
4470
+ if (!(sv !== null && sv !== void 0 && sv._perspCameraSnapshot)) return;
4471
+
4472
+ // Clean up any pending orbit-change listener
4473
+ if (this._orthoOrbitStartListener) {
4474
+ var _this$_orthoOrbitStar = this._orthoOrbitStartListener,
4475
+ controls = _this$_orthoOrbitStar.controls,
4476
+ fn = _this$_orthoOrbitStar.fn;
4477
+ controls.removeEventListener('change', fn);
4478
+ this._orthoOrbitStartListener = null;
4479
+ }
4480
+ var snap = sv._perspCameraSnapshot;
4481
+ sv.camera = snap.camera;
4482
+ this.camera = snap.camera;
4483
+ if (sv.controls) {
4484
+ sv.controls.object = snap.camera;
4485
+ sv.controls.target.copy(snap.controlsTarget);
4486
+ if (snap.controlsMinDist != null) sv.controls.minDistance = snap.controlsMinDist;
4487
+ if (snap.controlsMaxDist != null) sv.controls.maxDistance = snap.controlsMaxDist;
4488
+ if (snap.controlsMaxPolar != null) sv.controls.maxPolarAngle = snap.controlsMaxPolar;
4489
+ sv.controls.update();
4490
+ }
4491
+ sv._perspCameraSnapshot = null;
4492
+ console.log('📐 Restored perspective camera from ortho face view');
4232
4493
  }
4233
4494
 
4234
4495
  /**
@@ -4378,11 +4639,11 @@ var TransformControlsManager = /*#__PURE__*/function () {
4378
4639
  var isNewSegmentHorizontal = this._isSegmentHorizontal(segment);
4379
4640
 
4380
4641
  // Check if all existing segments have the same orientation
4381
- var _iterator2 = _createForOfIteratorHelper(existingSegments),
4382
- _step2;
4642
+ var _iterator3 = _createForOfIteratorHelper(existingSegments),
4643
+ _step3;
4383
4644
  try {
4384
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
4385
- var existingSegment = _step2.value;
4645
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
4646
+ var existingSegment = _step3.value;
4386
4647
  var isExistingHorizontal = this._isSegmentHorizontal(existingSegment);
4387
4648
 
4388
4649
  // Disallow mixing horizontal and vertical
@@ -4391,9 +4652,9 @@ var TransformControlsManager = /*#__PURE__*/function () {
4391
4652
  }
4392
4653
  }
4393
4654
  } catch (err) {
4394
- _iterator2.e(err);
4655
+ _iterator3.e(err);
4395
4656
  } finally {
4396
- _iterator2.f();
4657
+ _iterator3.f();
4397
4658
  }
4398
4659
  return true;
4399
4660
  }
@@ -4845,12 +5106,12 @@ var TransformControlsManager = /*#__PURE__*/function () {
4845
5106
  value: function _applyDeltaToBoundingBoxHelpers() {
4846
5107
  if (!this._dragStartGroupPosition || this.boundingBoxHelpers.length === 0) return;
4847
5108
  var delta = this.multiSelectionGroup.position.clone().sub(this._dragStartGroupPosition);
4848
- var _iterator3 = _createForOfIteratorHelper(this.boundingBoxHelpers),
4849
- _step3;
5109
+ var _iterator4 = _createForOfIteratorHelper(this.boundingBoxHelpers),
5110
+ _step4;
4850
5111
  try {
4851
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
5112
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
4852
5113
  var _helper$geometry2;
4853
- var helper = _step3.value;
5114
+ var helper = _step4.value;
4854
5115
  var startPositions = helper.userData._dragStartPositions;
4855
5116
  var posAttr = (_helper$geometry2 = helper.geometry) === null || _helper$geometry2 === void 0 || (_helper$geometry2 = _helper$geometry2.attributes) === null || _helper$geometry2 === void 0 ? void 0 : _helper$geometry2.position;
4856
5117
  if (!startPositions || !posAttr) continue;
@@ -4864,9 +5125,9 @@ var TransformControlsManager = /*#__PURE__*/function () {
4864
5125
  posAttr.needsUpdate = true;
4865
5126
  }
4866
5127
  } catch (err) {
4867
- _iterator3.e(err);
5128
+ _iterator4.e(err);
4868
5129
  } finally {
4869
- _iterator3.f();
5130
+ _iterator4.f();
4870
5131
  }
4871
5132
  }
4872
5133
 
@@ -5244,11 +5505,11 @@ var TransformControlsManager = /*#__PURE__*/function () {
5244
5505
  var objectFilter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
5245
5506
  var objectsWithBounds = this.getSelectableObjectsWithBounds(objectFilter);
5246
5507
  var intersections = [];
5247
- var _iterator4 = _createForOfIteratorHelper(objectsWithBounds),
5248
- _step4;
5508
+ var _iterator5 = _createForOfIteratorHelper(objectsWithBounds),
5509
+ _step5;
5249
5510
  try {
5250
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
5251
- var item = _step4.value;
5511
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
5512
+ var item = _step5.value;
5252
5513
  var object = item.object,
5253
5514
  boundingBox = item.boundingBox;
5254
5515
 
@@ -5268,9 +5529,9 @@ var TransformControlsManager = /*#__PURE__*/function () {
5268
5529
 
5269
5530
  // Sort by distance (closest first)
5270
5531
  } catch (err) {
5271
- _iterator4.e(err);
5532
+ _iterator5.e(err);
5272
5533
  } finally {
5273
- _iterator4.f();
5534
+ _iterator5.f();
5274
5535
  }
5275
5536
  intersections.sort(function (a, b) {
5276
5537
  return a.distance - b.distance;
@@ -5656,8 +5917,8 @@ var TransformControlsManager = /*#__PURE__*/function () {
5656
5917
  key: "_updateSegmentReference",
5657
5918
  value: function _updateSegmentReference(oldSegment, newSegment, index) {
5658
5919
  var selectedIndex = this.selectedObjects.findIndex(function (obj) {
5659
- var _obj$userData2;
5660
- return obj.uuid === oldSegment.uuid || ((_obj$userData2 = obj.userData) === null || _obj$userData2 === void 0 ? void 0 : _obj$userData2.originalUuid) === oldSegment.uuid;
5920
+ var _obj$userData3;
5921
+ return obj.uuid === oldSegment.uuid || ((_obj$userData3 = obj.userData) === null || _obj$userData3 === void 0 ? void 0 : _obj$userData3.originalUuid) === oldSegment.uuid;
5661
5922
  });
5662
5923
  if (selectedIndex !== -1 && newSegment) {
5663
5924
  // Clear bounding box cache
@@ -6044,9 +6305,21 @@ var TransformControlsManager = /*#__PURE__*/function () {
6044
6305
  if (this.eventHandlers.keydown) {
6045
6306
  window.removeEventListener('keydown', this.eventHandlers.keydown);
6046
6307
  }
6308
+ if (this.eventHandlers.pointerdown) {
6309
+ this.renderer.domElement.removeEventListener('pointerdown', this.eventHandlers.pointerdown);
6310
+ }
6047
6311
  if (this.eventHandlers.click) {
6048
6312
  this.renderer.domElement.removeEventListener('click', this.eventHandlers.click);
6049
6313
  }
6314
+ if (this.eventHandlers.contextmenu) {
6315
+ this.renderer.domElement.removeEventListener('contextmenu', this.eventHandlers.contextmenu);
6316
+ }
6317
+ if (this.eventHandlers.dblclick) {
6318
+ this.renderer.domElement.removeEventListener('dblclick', this.eventHandlers.dblclick);
6319
+ }
6320
+
6321
+ // Restore perspective camera if disposed while in ortho face view
6322
+ this._restorePerspectiveCamera();
6050
6323
 
6051
6324
  // Remove sceneViewer event listener
6052
6325
  if (this._objectTransformedListener && this.sceneViewer && typeof this.sceneViewer.off === 'function') {
@@ -11555,40 +11828,14 @@ var SceneExportManager = /*#__PURE__*/function () {
11555
11828
  }
11556
11829
  });
11557
11830
 
11558
- // Helper function to extract behaviors from current scene data
11559
- var extractBehaviors = function extractBehaviors() {
11560
- var _this$sceneViewer, _this$sceneViewer2;
11561
- // Only export behaviors that are NOT re-derivable from a component's
11562
- // defaultBehaviors[]. All component/device default behaviors are
11563
- // reconstructed at load time by Step B of _processBehaviors() using the
11564
- // component dictionary, so writing compact behaviorRef entries for them
11565
- // would be redundant. The scene JSON behaviors[] array is reserved for
11566
- // any future scene-level overrides that cannot be derived from the asset.
11567
- if ((_this$sceneViewer = _this.sceneViewer) !== null && _this$sceneViewer !== void 0 && (_this$sceneViewer = _this$sceneViewer.managers) !== null && _this$sceneViewer !== void 0 && _this$sceneViewer.behaviorManager) {
11568
- return _this.sceneViewer.managers.behaviorManager.getBehaviors().filter(function (b) {
11569
- return !b._isDefaultBehavior;
11570
- });
11571
- }
11572
- // Fallback when BehaviorManager is not available: exclude any entry that
11573
- // was already a behaviorRef (it was derivable from the component asset)
11574
- // and exclude the legacy _isDefaultBehavior marker if present.
11575
- return (((_this$sceneViewer2 = _this.sceneViewer) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.currentSceneData) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.behaviors) || []).filter(function (b) {
11576
- return !b.behaviorRef && !b._isDefaultBehavior;
11577
- });
11578
- };
11579
-
11580
11831
  // Build the complete export data structure (matching central-plant-input.json format)
11581
- var behaviors = extractBehaviors();
11582
- var exportData = _objectSpread2(_objectSpread2({
11832
+ var exportData = {
11583
11833
  version: '2.3',
11584
- connections: extractConnections()
11585
- }, behaviors.length > 0 ? {
11586
- behaviors: behaviors
11587
- } : {}), {}, {
11834
+ connections: extractConnections(),
11588
11835
  scene: {
11589
11836
  children: sceneChildren
11590
11837
  }
11591
- });
11838
+ };
11592
11839
  console.log('✅ Scene export completed:', exportData);
11593
11840
  console.log("\uD83D\uDCCA Exported ".concat(sceneChildren.length, " components and ").concat(exportData.connections.length, " connections"));
11594
11841
  return exportData;
@@ -29545,407 +29792,6 @@ var PathFlowManager = /*#__PURE__*/function (_BaseDisposable) {
29545
29792
  }]);
29546
29793
  }(BaseDisposable);
29547
29794
 
29548
- var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
29549
- function BehaviorManager(sceneViewer) {
29550
- var _this;
29551
- _classCallCheck(this, BehaviorManager);
29552
- _this = _callSuper(this, BehaviorManager);
29553
- _this.sceneViewer = sceneViewer;
29554
-
29555
- /** @type {Array<Object>} Raw behavior definitions from the scene JSON */
29556
- _this._behaviors = [];
29557
-
29558
- /** @type {Map<string, THREE.Object3D>} Cache: attachmentId -> resolved Three.js object */
29559
- _this._resolvedOutputs = new Map();
29560
- return _this;
29561
- }
29562
-
29563
- // ─────────────────────────────────────────────────────────────────────────
29564
- // PUBLIC API
29565
- // ─────────────────────────────────────────────────────────────────────────
29566
-
29567
- /**
29568
- * Load and store behavior definitions, then resolve output objects.
29569
- * Should be called AFTER GLB models have been loaded so all meshes exist.
29570
- * @param {Array<Object>} behaviorsArray - Array of behavior definition objects
29571
- */
29572
- _inherits(BehaviorManager, _BaseDisposable);
29573
- return _createClass(BehaviorManager, [{
29574
- key: "loadBehaviors",
29575
- value: function loadBehaviors(behaviorsArray) {
29576
- if (!Array.isArray(behaviorsArray)) {
29577
- console.warn('⚠️ BehaviorManager.loadBehaviors(): expected an array, got', _typeof(behaviorsArray));
29578
- return;
29579
- }
29580
- this._behaviors = behaviorsArray;
29581
- this._resolvedOutputs.clear();
29582
- this._resolveOutputObjects();
29583
- console.log("\u2705 BehaviorManager: loaded ".concat(this._behaviors.length, " behavior(s)"));
29584
- }
29585
-
29586
- /**
29587
- * Returns all stored behavior definitions.
29588
- * @returns {Array<Object>}
29589
- */
29590
- }, {
29591
- key: "getBehaviors",
29592
- value: function getBehaviors() {
29593
- return _toConsumableArray(this._behaviors);
29594
- }
29595
-
29596
- /**
29597
- * Add a single behavior definition at runtime and resolve its output object.
29598
- * @param {Object} behaviorDef
29599
- */
29600
- }, {
29601
- key: "addBehavior",
29602
- value: function addBehavior(behaviorDef) {
29603
- if (!behaviorDef || !behaviorDef.id) {
29604
- console.warn('⚠️ BehaviorManager.addBehavior(): missing id', behaviorDef);
29605
- return false;
29606
- }
29607
- if (this._behaviors.find(function (b) {
29608
- return b.id === behaviorDef.id;
29609
- })) {
29610
- console.warn("\u26A0\uFE0F BehaviorManager.addBehavior(): id \"".concat(behaviorDef.id, "\" already exists"));
29611
- return false;
29612
- }
29613
- this._behaviors.push(behaviorDef);
29614
- this._resolveOutputForBehavior(behaviorDef);
29615
- console.log("\u2705 BehaviorManager: added behavior \"".concat(behaviorDef.id, "\""));
29616
- return true;
29617
- }
29618
-
29619
- /**
29620
- * Remove a behavior definition by id.
29621
- * @param {string} behaviorId
29622
- * @returns {boolean}
29623
- */
29624
- }, {
29625
- key: "removeBehavior",
29626
- value: function removeBehavior(behaviorId) {
29627
- var idx = this._behaviors.findIndex(function (b) {
29628
- return b.id === behaviorId;
29629
- });
29630
- if (idx === -1) {
29631
- console.warn("\u26A0\uFE0F BehaviorManager.removeBehavior(): id \"".concat(behaviorId, "\" not found"));
29632
- return false;
29633
- }
29634
- this._behaviors.splice(idx, 1);
29635
- // We keep the resolved cache entry — it is harmless if no behaviors reference it
29636
- console.log("\u2705 BehaviorManager: removed behavior \"".concat(behaviorId, "\""));
29637
- return true;
29638
- }
29639
-
29640
- /**
29641
- * Simulate an IO device state value arriving and evaluate all matching behaviors.
29642
- * Any behavior whose input.attachment + input.state matches will have its
29643
- * conditions tested and actions applied.
29644
- *
29645
- * @param {string} attachmentId - The attachment ID of the input io-device
29646
- * @param {string} stateId - The state ID on that device
29647
- * @param {*} value - Current value of the state
29648
- * @param {string} [parentUuid] - UUID of the parent component instance (used to
29649
- * disambiguate clones that share the same attachmentId)
29650
- */
29651
- }, {
29652
- key: "triggerState",
29653
- value: function triggerState(attachmentId, stateId, value, parentUuid) {
29654
- var _this2 = this;
29655
- var matching = this._behaviors.filter(function (b) {
29656
- var _b$input, _b$input2, _b$input3;
29657
- if (((_b$input = b.input) === null || _b$input === void 0 ? void 0 : _b$input.attachment) !== attachmentId) return false;
29658
- if (((_b$input2 = b.input) === null || _b$input2 === void 0 ? void 0 : _b$input2.state) !== stateId) return false;
29659
- // If both the behavior and the caller carry a component UUID, they must match.
29660
- // If either side has no component context, fall back to attachment-only matching.
29661
- if ((_b$input3 = b.input) !== null && _b$input3 !== void 0 && _b$input3.component && parentUuid && b.input.component !== parentUuid) return false;
29662
- return true;
29663
- });
29664
- if (matching.length === 0) {
29665
- console.log("\u2139\uFE0F BehaviorManager.triggerState(): no behaviors for \"".concat(attachmentId, "/").concat(stateId, "\""));
29666
- return;
29667
- }
29668
- matching.forEach(function (behavior) {
29669
- _this2._evaluateBehavior(behavior, value);
29670
- });
29671
- }
29672
-
29673
- // ─────────────────────────────────────────────────────────────────────────
29674
- // PRIVATE HELPERS
29675
- // ─────────────────────────────────────────────────────────────────────────
29676
-
29677
- /**
29678
- * Walk all behaviors and resolve each output object once.
29679
- */
29680
- }, {
29681
- key: "_resolveOutputObjects",
29682
- value: function _resolveOutputObjects() {
29683
- var _this3 = this;
29684
- this._behaviors.forEach(function (b) {
29685
- return _this3._resolveOutputForBehavior(b);
29686
- });
29687
- }
29688
-
29689
- /**
29690
- * Resolve a single behavior's output object and cache it by behavior id.
29691
- * Looks for an object in the scene whose userData.attachmentId matches
29692
- * behavior.output.attachment. If behavior.output.child is also set, it
29693
- * then searches within that object for a descendant whose name matches.
29694
- *
29695
- * @param {Object} behavior
29696
- */
29697
- }, {
29698
- key: "_resolveOutputForBehavior",
29699
- value: function _resolveOutputForBehavior(behavior) {
29700
- var _this$sceneViewer, _behavior$output, _behavior$output2, _behavior$output3;
29701
- var cacheKey = this._outputCacheKey(behavior);
29702
- if (this._resolvedOutputs.has(cacheKey)) return; // already resolved
29703
-
29704
- var scene = (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 ? void 0 : _this$sceneViewer.scene;
29705
- if (!scene) {
29706
- console.warn('⚠️ BehaviorManager: scene not available for output resolution');
29707
- return;
29708
- }
29709
- var attachmentId = (_behavior$output = behavior.output) === null || _behavior$output === void 0 ? void 0 : _behavior$output.attachment;
29710
- var childName = (_behavior$output2 = behavior.output) === null || _behavior$output2 === void 0 ? void 0 : _behavior$output2.child;
29711
- var componentUuid = (_behavior$output3 = behavior.output) === null || _behavior$output3 === void 0 ? void 0 : _behavior$output3.component;
29712
- if (!attachmentId) {
29713
- console.warn("\u26A0\uFE0F BehaviorManager: behavior \"".concat(behavior.id, "\" has no output.attachment"));
29714
- return;
29715
- }
29716
-
29717
- // Find the attachment root in the scene, scoped to the correct component
29718
- // instance when a componentUuid is specified (prevents cross-clone bleed).
29719
- var attachmentRoot = null;
29720
- scene.traverse(function (obj) {
29721
- var _obj$userData, _obj$parent;
29722
- if (attachmentRoot) return;
29723
- if (((_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.attachmentId) !== attachmentId) return;
29724
- // If a component UUID is specified, the io-device's parent must match it
29725
- if (componentUuid && ((_obj$parent = obj.parent) === null || _obj$parent === void 0 ? void 0 : _obj$parent.uuid) !== componentUuid) return;
29726
- attachmentRoot = obj;
29727
- });
29728
- if (!attachmentRoot) {
29729
- console.warn("\u26A0\uFE0F BehaviorManager: could not find attachment \"".concat(attachmentId, "\" in scene"));
29730
- return;
29731
- }
29732
- var targetObject = attachmentRoot;
29733
-
29734
- // If a specific child mesh name is specified, drill into it
29735
- if (childName) {
29736
- var foundChild = null;
29737
- attachmentRoot.traverse(function (obj) {
29738
- if (!foundChild && obj.name === childName) {
29739
- foundChild = obj;
29740
- }
29741
- });
29742
- if (!foundChild) {
29743
- console.warn("\u26A0\uFE0F BehaviorManager: child \"".concat(childName, "\" not found under attachment \"").concat(attachmentId, "\""));
29744
- return;
29745
- }
29746
- targetObject = foundChild;
29747
- } else {
29748
- // No child specified: if the root has no material (e.g. it's a Group),
29749
- // fall through to the first Mesh descendant so that material property
29750
- // actions (e.g. material.emissiveIntensity) work without needing an
29751
- // explicit child selection.
29752
- if (!attachmentRoot.isMesh) {
29753
- var firstMesh = null;
29754
- attachmentRoot.traverse(function (obj) {
29755
- if (!firstMesh && obj.isMesh) firstMesh = obj;
29756
- });
29757
- if (firstMesh) {
29758
- targetObject = firstMesh;
29759
- console.log("\uD83D\uDD17 BehaviorManager: no child specified \u2014 auto-resolved to first mesh \"".concat(firstMesh.name || firstMesh.uuid, "\" under \"").concat(attachmentId, "\""));
29760
- }
29761
- }
29762
- }
29763
- this._resolvedOutputs.set(cacheKey, targetObject);
29764
- console.log("\uD83D\uDD17 BehaviorManager: resolved output for \"".concat(behavior.id, "\" \u2192 ").concat(targetObject.name || targetObject.uuid));
29765
- }
29766
-
29767
- /**
29768
- * Build a stable cache key for a behavior's output.
29769
- * @param {Object} behavior
29770
- * @returns {string}
29771
- */
29772
- }, {
29773
- key: "_outputCacheKey",
29774
- value: function _outputCacheKey(behavior) {
29775
- var _behavior$output4, _behavior$output5;
29776
- return "".concat(behavior.id, "::").concat((_behavior$output4 = behavior.output) === null || _behavior$output4 === void 0 ? void 0 : _behavior$output4.attachment, "::").concat(((_behavior$output5 = behavior.output) === null || _behavior$output5 === void 0 ? void 0 : _behavior$output5.child) || '');
29777
- }
29778
-
29779
- /**
29780
- * Evaluate a behavior's conditions against the current data-point value
29781
- * and apply matching actions.
29782
- *
29783
- * @param {Object} behavior
29784
- * @param {*} value - Current data-point value
29785
- */
29786
- }, {
29787
- key: "_evaluateBehavior",
29788
- value: function _evaluateBehavior(behavior, value) {
29789
- var _this4 = this;
29790
- if (!Array.isArray(behavior.conditions) || behavior.conditions.length === 0) return;
29791
- var cacheKey = this._outputCacheKey(behavior);
29792
- var targetObject = this._resolvedOutputs.get(cacheKey);
29793
- if (!targetObject) {
29794
- // Attempt late resolution (model may have loaded after behavior was added)
29795
- this._resolveOutputForBehavior(behavior);
29796
- var resolved = this._resolvedOutputs.get(cacheKey);
29797
- if (!resolved) {
29798
- console.warn("\u26A0\uFE0F BehaviorManager: no resolved output for behavior \"".concat(behavior.id, "\" \u2014 skipping"));
29799
- return;
29800
- }
29801
- }
29802
- var output = this._resolvedOutputs.get(cacheKey);
29803
- behavior.conditions.forEach(function (condition) {
29804
- if (_this4._testCondition(condition.when, value)) {
29805
- if (Array.isArray(condition.actions)) {
29806
- condition.actions.forEach(function (action) {
29807
- _this4._applyAction(output, action.set, action.value, action.relative === true);
29808
- });
29809
- }
29810
- }
29811
- });
29812
- }
29813
-
29814
- /**
29815
- * Safely test a condition expression against a value.
29816
- * Supports a fixed set of patterns — no arbitrary eval().
29817
- *
29818
- * Supported patterns:
29819
- * "state.value === true" → value === true
29820
- * "state.value === false" → value === false
29821
- * "state.value === <num>" → value === num
29822
- * "state.value > <num>" → value > num
29823
- * "state.value < <num>" → value < num
29824
- * "state.value >= <num>" → value >= num
29825
- * "state.value <= <num>" → value <= num
29826
- *
29827
- * @param {string} whenExpr
29828
- * @param {*} value
29829
- * @returns {boolean}
29830
- */
29831
- }, {
29832
- key: "_testCondition",
29833
- value: function _testCondition(whenExpr, value) {
29834
- if (typeof whenExpr !== 'string') return false;
29835
- var expr = whenExpr.trim();
29836
-
29837
- // Strip the "state.value " prefix if present (also accept legacy "dataPoint.value" prefix)
29838
- var stripped = expr.replace(/^(state|dataPoint)\.value\s*/, '');
29839
-
29840
- // Parse operator and RHS
29841
- var match = stripped.match(/^(===|!==|>=|<=|>|<)\s*(.+)$/);
29842
- if (!match) {
29843
- console.warn("\u26A0\uFE0F BehaviorManager: unrecognised condition expression: \"".concat(whenExpr, "\""));
29844
- return false;
29845
- }
29846
- var operator = match[1];
29847
- var rawRhs = match[2].trim();
29848
-
29849
- // Parse RHS to the appropriate JS type
29850
- var rhs;
29851
- if (rawRhs === 'true') rhs = true;else if (rawRhs === 'false') rhs = false;else if (rawRhs === 'null') rhs = null;else if (!isNaN(rawRhs)) rhs = parseFloat(rawRhs);else rhs = rawRhs; // treat as string
29852
-
29853
- switch (operator) {
29854
- case '===':
29855
- // Treat 0/1 as false/true when the RHS is a boolean (binary toggle)
29856
- if (typeof rhs === 'boolean') return Boolean(value) === rhs;
29857
- return value === rhs;
29858
- case '!==':
29859
- if (typeof rhs === 'boolean') return Boolean(value) !== rhs;
29860
- return value !== rhs;
29861
- case '>':
29862
- return value > rhs;
29863
- case '<':
29864
- return value < rhs;
29865
- case '>=':
29866
- return value >= rhs;
29867
- case '<=':
29868
- return value <= rhs;
29869
- default:
29870
- return false;
29871
- }
29872
- }
29873
-
29874
- /**
29875
- * Apply a single action to a Three.js object by navigating a dot-notation path.
29876
- * Example: set = "material.emissiveIntensity", value = 1.0
29877
- *
29878
- * @param {THREE.Object3D} object
29879
- * @param {string} propertyPath - Dot-notation path relative to the object
29880
- * @param {*} value
29881
- */
29882
- }, {
29883
- key: "_applyAction",
29884
- value: function _applyAction(object, propertyPath, value) {
29885
- var relative = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
29886
- if (!object || typeof propertyPath !== 'string') return;
29887
- var parts = propertyPath.split('.');
29888
-
29889
- // Clone shared material onto this mesh before mutating any material property.
29890
- // Three.js reuses the same Material instance across all meshes loaded from the
29891
- // same GLB, so without cloning a color/emissive change would bleed to every
29892
- // other component instance that shares the model.
29893
- if (parts[0] === 'material' && object.isMesh && object.material && !object.userData._behaviorMaterialCloned) {
29894
- object.material = object.material.clone();
29895
- object.userData._behaviorMaterialCloned = true;
29896
- }
29897
- var target = object;
29898
- for (var i = 0; i < parts.length - 1; i++) {
29899
- if (target == null) {
29900
- console.warn("\u26A0\uFE0F BehaviorManager._applyAction(): path \"".concat(propertyPath, "\" broken at \"").concat(parts[i], "\""));
29901
- return;
29902
- }
29903
- target = target[parts[i]];
29904
- }
29905
- var lastKey = parts[parts.length - 1];
29906
- if (target == null || !(lastKey in target)) {
29907
- console.warn("\u26A0\uFE0F BehaviorManager._applyAction(): property \"".concat(propertyPath, "\" not found on"), object);
29908
- return;
29909
- }
29910
-
29911
- // If relative, capture the mesh's original value on first execution and offset from it.
29912
- if (relative) {
29913
- var baselineKey = "_baseline_".concat(propertyPath.replace(/\./g, '_'));
29914
- if (!(baselineKey in object.userData)) {
29915
- object.userData[baselineKey] = target[lastKey];
29916
- }
29917
- value = object.userData[baselineKey] + parseFloat(value);
29918
- }
29919
-
29920
- // THREE.Color objects must be mutated via .set() rather than replaced
29921
- var existing = target[lastKey];
29922
- if (existing && existing.isColor && typeof value === 'string') {
29923
- existing.set(value);
29924
- } else {
29925
- // Coerce numeric strings to numbers for non-color properties
29926
- target[lastKey] = typeof value === 'string' && value !== '' && !isNaN(value) ? parseFloat(value) : value;
29927
- }
29928
- console.log("\u2705 BehaviorManager: applied \"".concat(propertyPath, "\" = ").concat(value, " on \"").concat(object.name || object.uuid, "\""));
29929
-
29930
- // If we touched a material property, mark it needs update
29931
- if (parts[0] === 'material' && target.needsUpdate !== undefined) {
29932
- target.needsUpdate = true;
29933
- }
29934
- }
29935
-
29936
- // ─────────────────────────────────────────────────────────────────────────
29937
- // DISPOSAL
29938
- // ─────────────────────────────────────────────────────────────────────────
29939
- }, {
29940
- key: "dispose",
29941
- value: function dispose() {
29942
- this._behaviors = [];
29943
- this._resolvedOutputs.clear();
29944
- _superPropGet(BehaviorManager, "dispose", this, 3)([]);
29945
- }
29946
- }]);
29947
- }(BaseDisposable);
29948
-
29949
29795
  /**
29950
29796
  * IO Device Utilities
29951
29797
  * Shared utility functions for attaching IO devices to smart components.
@@ -31463,7 +31309,7 @@ var ModelManager = /*#__PURE__*/function () {
31463
31309
  key: "loadLibraryModel",
31464
31310
  value: function () {
31465
31311
  var _loadLibraryModel = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(targetMesh, jsonEntry, componentData) {
31466
- var component, _jsonEntry$userData, _jsonEntry$userData2, _jsonEntry$userData3, originalProps, connectorChildren, gltfScene, libraryModel, _this$sceneViewer, ioAnimMgr, _loop, _i, _Object$entries, _jsonEntry$userData4, _t;
31312
+ var component, _jsonEntry$userData, _jsonEntry$userData2, _jsonEntry$userData3, originalProps, connectorChildren, gltfScene, libraryModel, _this$sceneViewer, ioAnimMgr, _loop, _i, _Object$entries, warmFn, _jsonEntry$userData4, _t;
31467
31313
  return _regenerator().w(function (_context2) {
31468
31314
  while (1) switch (_context2.n) {
31469
31315
  case 0:
@@ -31560,6 +31406,20 @@ var ModelManager = /*#__PURE__*/function () {
31560
31406
  case 8:
31561
31407
  // Replace mesh in scene
31562
31408
  this._replaceMeshInScene(targetMesh, libraryModel, originalProps.parent, component);
31409
+
31410
+ // Pre-warm the filtered bounding-box cache for smart components so the
31411
+ // first selection is instant. Deferred to idle time so it does not
31412
+ // block the current frame.
31413
+ if (componentData.isSmart) {
31414
+ warmFn = function warmFn() {
31415
+ return computeFilteredBoundingBoxCached(libraryModel, ['io-device', 'connector']);
31416
+ };
31417
+ if (typeof requestIdleCallback !== 'undefined') {
31418
+ requestIdleCallback(warmFn);
31419
+ } else {
31420
+ setTimeout(warmFn, 0);
31421
+ }
31422
+ }
31563
31423
  console.log("\uD83C\uDF89 ".concat((_jsonEntry$userData3 = jsonEntry.userData) === null || _jsonEntry$userData3 === void 0 ? void 0 : _jsonEntry$userData3.libraryId, " GLB model successfully rendered in scene"));
31564
31424
  return _context2.a(2, libraryModel);
31565
31425
  case 9:
@@ -33478,13 +33338,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
33478
33338
  phaseStart = performance.now();
33479
33339
  this._finalizeScene(data, crosscubeTextureSet, isImported);
33480
33340
  timers.phase5_finalize = performance.now() - phaseStart;
33481
-
33482
- // Phase 6: Load behaviors (after GLB models are present so output objects can be resolved)
33483
- phaseStart = performance.now();
33484
- this._processBehaviors(data);
33485
- timers.phase6_behaviors = performance.now() - phaseStart;
33486
33341
  totalTime = performance.now() - totalStart;
33487
- console.log("\u23F1\uFE0F Scene Loading Performance:\n Phase 1 (Prepare) : ".concat(timers.phase1_prepare.toFixed(0), "ms\n Phase 2 (Create Objects): ").concat(timers.phase2_createObjects.toFixed(0), "ms\n Phase 3 (GLB Models) : ").concat(timers.phase3_glbModels.toFixed(0), "ms\n Phase 4 (Pathfinding) : ").concat(timers.phase4_pathfinding.toFixed(0), "ms\n Phase 5 (Finalize) : ").concat(timers.phase5_finalize.toFixed(0), "ms\n Phase 6 (Behaviors) : ").concat(timers.phase6_behaviors.toFixed(0), "ms\n \u2500\u2500\u2500\u2500\u2500\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 Total : ").concat(totalTime.toFixed(0), "ms"));
33342
+ console.log("\u23F1\uFE0F Scene Loading Performance:\n Phase 1 (Prepare) : ".concat(timers.phase1_prepare.toFixed(0), "ms\n Phase 2 (Create Objects): ").concat(timers.phase2_createObjects.toFixed(0), "ms\n Phase 3 (GLB Models) : ").concat(timers.phase3_glbModels.toFixed(0), "ms\n Phase 4 (Pathfinding) : ").concat(timers.phase4_pathfinding.toFixed(0), "ms\n Phase 5 (Finalize) : ").concat(timers.phase5_finalize.toFixed(0), "ms\n \u2500\u2500\u2500\u2500\u2500\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 Total : ").concat(totalTime.toFixed(0), "ms"));
33488
33343
  console.log('✅ Scene loaded successfully');
33489
33344
 
33490
33345
  // Notify UI components (e.g. SceneHierarchy) that the scene is fully loaded
@@ -33842,278 +33697,6 @@ var SceneOperationsManager = /*#__PURE__*/function () {
33842
33697
  }
33843
33698
  }
33844
33699
 
33845
- /**
33846
- * Process behaviors from the scene data, expand any defaultBehaviors defined
33847
- * on component dictionary entries, resolve behaviorRef entries against device
33848
- * assets, inject per-instance component UUIDs, and hand the flat resolved
33849
- * array to BehaviorManager.
33850
- *
33851
- * Resolution cascade:
33852
- * 1. Explicit behaviors in data.behaviors — passed through as-is (behaviorRef
33853
- * entries are resolved against the component/device dictionaries).
33854
- * 2. defaultBehaviors[] on each placed smart component's dictionary entry —
33855
- * expanded per instance with the instance UUID injected. Compact
33856
- * behaviorRef-derived entries are tagged _isDefaultBehavior:true (the
33857
- * exporter can re-derive them); full L2 behavior objects are tagged false
33858
- * so they are exported verbatim and survive round-trips.
33859
- *
33860
- * @param {Object} data - Scene JSON data
33861
- */
33862
- }, {
33863
- key: "_processBehaviors",
33864
- value: function _processBehaviors(data) {
33865
- var _this$sceneViewer2,
33866
- _this$sceneViewer$cen2,
33867
- _this5 = this,
33868
- _data$scene3;
33869
- var behaviorManager = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.managers) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.behaviorManager;
33870
- if (!behaviorManager) {
33871
- console.warn('⚠️ _processBehaviors: BehaviorManager not available');
33872
- return;
33873
- }
33874
-
33875
- // Obtain the component dictionary (extended = includes S3/smart components)
33876
- var componentDictionary = ((_this$sceneViewer$cen2 = this.sceneViewer.centralPlant) === null || _this$sceneViewer$cen2 === void 0 || (_this$sceneViewer$cen2 = _this$sceneViewer$cen2.managers) === null || _this$sceneViewer$cen2 === void 0 || (_this$sceneViewer$cen2 = _this$sceneViewer$cen2.componentDataManager) === null || _this$sceneViewer$cen2 === void 0 ? void 0 : _this$sceneViewer$cen2.componentDictionary) || {};
33877
-
33878
- // ── Step A: Resolve any behaviorRef entries from data.behaviors ─────────
33879
- var explicitBehaviors = [];
33880
- if (Array.isArray(data === null || data === void 0 ? void 0 : data.behaviors)) {
33881
- data.behaviors.forEach(function (entry) {
33882
- if (entry.behaviorRef) {
33883
- var resolved = _this5._resolveBehaviorRef(entry, componentDictionary);
33884
- if (resolved) explicitBehaviors.push(resolved);
33885
- } else {
33886
- explicitBehaviors.push(entry);
33887
- }
33888
- });
33889
- }
33890
-
33891
- // Build a set of explicit behavior ids for deduplication
33892
- var explicitIds = new Set(explicitBehaviors.map(function (b) {
33893
- return b.id;
33894
- }));
33895
-
33896
- // Build a set of (component::attachment::state) tuples covered by explicit
33897
- // behaviors. If an expanded default behavior would produce the SAME input
33898
- // tuple, the explicit behavior is treated as the intentional override and
33899
- // the default expansion is skipped. This prevents a component's built-in
33900
- // switch→LED wiring from doubling-up when the user has deliberately authored
33901
- // cross-component behaviors that re-wire the same switch.
33902
- var explicitInputTuples = new Set(explicitBehaviors.filter(function (b) {
33903
- var _b$input, _b$input2, _b$input3;
33904
- return ((_b$input = b.input) === null || _b$input === void 0 ? void 0 : _b$input.component) && ((_b$input2 = b.input) === null || _b$input2 === void 0 ? void 0 : _b$input2.attachment) && ((_b$input3 = b.input) === null || _b$input3 === void 0 ? void 0 : _b$input3.state);
33905
- }).map(function (b) {
33906
- return "".concat(b.input.component, "::").concat(b.input.attachment, "::").concat(b.input.state);
33907
- }));
33908
-
33909
- // Build a set of instanceUuids already covered by behaviorRef entries in
33910
- // data.behaviors (written by the exporter for smart component defaults).
33911
- // Step B skips these instances to avoid loading the same behaviors twice —
33912
- // once via Step A (resolved from data.behaviors refs) and once via Step B
33913
- // (auto-expanded from the component dictionary).
33914
- var coveredInstances = new Set();
33915
- if (Array.isArray(data === null || data === void 0 ? void 0 : data.behaviors)) {
33916
- data.behaviors.forEach(function (entry) {
33917
- if (entry.behaviorRef && entry.component) {
33918
- coveredInstances.add(entry.component);
33919
- }
33920
- });
33921
- }
33922
-
33923
- // ── Step B: Walk placed components and expand their defaultBehaviors ─────
33924
- var instanceBehaviors = [];
33925
- if (Array.isArray(data === null || data === void 0 || (_data$scene3 = data.scene) === null || _data$scene3 === void 0 ? void 0 : _data$scene3.children)) {
33926
- data.scene.children.forEach(function (child) {
33927
- var _child$userData0, _compData$defaultBeha;
33928
- var libraryId = (_child$userData0 = child.userData) === null || _child$userData0 === void 0 ? void 0 : _child$userData0.libraryId;
33929
- if (!libraryId) return;
33930
- var instanceUuid = child.uuid;
33931
- // Skip instances whose defaults were already resolved by Step A
33932
- // (they have explicit behaviorRef entries in data.behaviors).
33933
- if (coveredInstances.has(instanceUuid)) return;
33934
- var compData = componentDictionary[libraryId];
33935
- if (!(compData !== null && compData !== void 0 && (_compData$defaultBeha = compData.defaultBehaviors) !== null && _compData$defaultBeha !== void 0 && _compData$defaultBeha.length)) return;
33936
- compData.defaultBehaviors.forEach(function (template) {
33937
- var _expanded$input, _expanded$input2, _expanded$input3;
33938
- var expanded = _this5._expandDefaultBehavior(template, instanceUuid, componentDictionary);
33939
- if (!expanded) return;
33940
- // Skip if an explicit scene behavior already covers this id
33941
- if (explicitIds.has(expanded.id)) return;
33942
- // Skip if an explicit scene behavior already covers the same
33943
- // (component, attachment, state) input tuple. This prevents a
33944
- // component's built-in default wiring (e.g. switch → own LED) from
33945
- // double-firing when the user has authored a cross-component override
33946
- // that rewires the same switch to a different target.
33947
- if ((_expanded$input = expanded.input) !== null && _expanded$input !== void 0 && _expanded$input.component && (_expanded$input2 = expanded.input) !== null && _expanded$input2 !== void 0 && _expanded$input2.attachment && (_expanded$input3 = expanded.input) !== null && _expanded$input3 !== void 0 && _expanded$input3.state) {
33948
- var tuple = "".concat(expanded.input.component, "::").concat(expanded.input.attachment, "::").concat(expanded.input.state);
33949
- if (explicitInputTuples.has(tuple)) return;
33950
- }
33951
- // All component defaultBehaviors are re-derivable from the component
33952
- // asset at export time (via compact behaviorRef), so mark them all.
33953
- expanded._isDefaultBehavior = true;
33954
- instanceBehaviors.push(expanded);
33955
- });
33956
- });
33957
- }
33958
- var allBehaviors = [].concat(explicitBehaviors, instanceBehaviors);
33959
- if (allBehaviors.length === 0) return;
33960
- behaviorManager.loadBehaviors(allBehaviors);
33961
- console.log("\u2705 _processBehaviors: loaded ".concat(explicitBehaviors.length, " explicit + ").concat(instanceBehaviors.length, " default-expanded behavior(s)"));
33962
- }
33963
-
33964
- /**
33965
- * Register the defaultBehaviors of a single newly placed component instance
33966
- * into the BehaviorManager. Called from addComponent() (drag-drop), where
33967
- * _processBehaviors() (scene-load path) is not invoked.
33968
- *
33969
- * @param {Object} componentData - Entry from the component dictionary
33970
- * @param {string} instanceUuid - UUID of the placed component (componentModel.uuid)
33971
- */
33972
- }, {
33973
- key: "registerBehaviorsForComponentInstance",
33974
- value: function registerBehaviorsForComponentInstance(componentData, instanceUuid) {
33975
- var _this$sceneViewer3,
33976
- _componentData$defaul,
33977
- _this$sceneViewer$cen3,
33978
- _this6 = this;
33979
- var behaviorManager = (_this$sceneViewer3 = this.sceneViewer) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.managers) === null || _this$sceneViewer3 === void 0 ? void 0 : _this$sceneViewer3.behaviorManager;
33980
- if (!behaviorManager) return;
33981
- if (!(componentData !== null && componentData !== void 0 && (_componentData$defaul = componentData.defaultBehaviors) !== null && _componentData$defaul !== void 0 && _componentData$defaul.length)) return;
33982
- var componentDictionary = ((_this$sceneViewer$cen3 = this.sceneViewer.centralPlant) === null || _this$sceneViewer$cen3 === void 0 || (_this$sceneViewer$cen3 = _this$sceneViewer$cen3.managers) === null || _this$sceneViewer$cen3 === void 0 || (_this$sceneViewer$cen3 = _this$sceneViewer$cen3.componentDataManager) === null || _this$sceneViewer$cen3 === void 0 ? void 0 : _this$sceneViewer$cen3.componentDictionary) || {};
33983
- var registered = 0;
33984
- componentData.defaultBehaviors.forEach(function (template) {
33985
- var expanded = _this6._expandDefaultBehavior(template, instanceUuid, componentDictionary);
33986
- if (!expanded) return;
33987
- expanded._isDefaultBehavior = true;
33988
- behaviorManager.addBehavior(expanded);
33989
- registered++;
33990
- });
33991
- if (registered > 0) {
33992
- console.log("\u2705 registerBehaviorsForComponentInstance: registered ".concat(registered, " behavior(s) for instance ").concat(instanceUuid));
33993
- }
33994
- }
33995
-
33996
- /**
33997
- * Resolve a scene-level or component-level behaviorRef entry into a full
33998
- * behavior definition by looking up the referenced template in the device
33999
- * asset's defaultBehaviors[] and substituting 'self' with the real
34000
- * attachmentId.
34001
- *
34002
- * Ref shape: { behaviorRef, deviceId, attachment, component? }
34003
- *
34004
- * @param {Object} ref
34005
- * @param {Object} componentDictionary
34006
- * @returns {Object|null} Resolved behavior or null if not found
34007
- */
34008
- }, {
34009
- key: "_resolveBehaviorRef",
34010
- value: function _resolveBehaviorRef(ref, componentDictionary) {
34011
- var _resolved$input, _resolved$output;
34012
- if (!ref.behaviorRef) {
34013
- console.warn('⚠️ _resolveBehaviorRef: missing behaviorRef', ref);
34014
- return null;
34015
- }
34016
-
34017
- // ── Component-level L2 ref: { behaviorRef, libraryId, component } ──────
34018
- // The full behavior template lives on the smart component's defaultBehaviors[].
34019
- // No 'self' substitution needed — attachment IDs are already real.
34020
- if (ref.libraryId) {
34021
- var compData = componentDictionary[ref.libraryId];
34022
- if (!compData) {
34023
- console.warn("\u26A0\uFE0F _resolveBehaviorRef: component \"".concat(ref.libraryId, "\" not in dictionary"));
34024
- return null;
34025
- }
34026
- var _template = (compData.defaultBehaviors || []).find(function (b) {
34027
- return b.id === ref.behaviorRef;
34028
- });
34029
- if (!_template) {
34030
- console.warn("\u26A0\uFE0F _resolveBehaviorRef: behavior \"".concat(ref.behaviorRef, "\" not found on component \"").concat(ref.libraryId, "\""));
34031
- return null;
34032
- }
34033
- var _resolved = JSON.parse(JSON.stringify(_template));
34034
- _resolved.id = "".concat(_resolved.id, "::").concat(ref.component);
34035
- if (ref.component) {
34036
- _resolved.input = _objectSpread2(_objectSpread2({}, _resolved.input), {}, {
34037
- component: ref.component
34038
- });
34039
- _resolved.output = _objectSpread2(_objectSpread2({}, _resolved.output), {}, {
34040
- component: ref.component
34041
- });
34042
- }
34043
- return _resolved;
34044
- }
34045
-
34046
- // ── Device-level L1 ref: { behaviorRef, deviceId, attachment, component } ─
34047
- if (!ref.deviceId || !ref.attachment) {
34048
- console.warn('⚠️ _resolveBehaviorRef: incomplete ref', ref);
34049
- return null;
34050
- }
34051
- var deviceData = componentDictionary[ref.deviceId];
34052
- if (!deviceData) {
34053
- console.warn("\u26A0\uFE0F _resolveBehaviorRef: device \"".concat(ref.deviceId, "\" not in dictionary"));
34054
- return null;
34055
- }
34056
- var template = (deviceData.defaultBehaviors || []).find(function (b) {
34057
- return b.id === ref.behaviorRef;
34058
- });
34059
- if (!template) {
34060
- console.warn("\u26A0\uFE0F _resolveBehaviorRef: behavior \"".concat(ref.behaviorRef, "\" not found on device \"").concat(ref.deviceId, "\""));
34061
- return null;
34062
- }
34063
- // Deep clone and substitute 'self' -> real attachmentId
34064
- var resolved = JSON.parse(JSON.stringify(template));
34065
- resolved.id = "".concat(resolved.id, "::").concat(ref.attachment);
34066
- if (((_resolved$input = resolved.input) === null || _resolved$input === void 0 ? void 0 : _resolved$input.attachment) === 'self') resolved.input.attachment = ref.attachment;
34067
- if (((_resolved$output = resolved.output) === null || _resolved$output === void 0 ? void 0 : _resolved$output.attachment) === 'self') resolved.output.attachment = ref.attachment;
34068
- // Inject component UUID guard if the ref carries one
34069
- if (ref.component) {
34070
- resolved.input = _objectSpread2(_objectSpread2({}, resolved.input), {}, {
34071
- component: ref.component
34072
- });
34073
- resolved.output = _objectSpread2(_objectSpread2({}, resolved.output), {}, {
34074
- component: ref.component
34075
- });
34076
- }
34077
- return resolved;
34078
- }
34079
-
34080
- /**
34081
- * Expand a single defaultBehaviors[] template entry for a specific component
34082
- * instance placed in the scene.
34083
- *
34084
- * If the entry is a behaviorRef, it is first resolved against the device
34085
- * library; otherwise it is treated as a full behavior definition.
34086
- * In both cases the instanceUuid is injected as input.component and
34087
- * output.component, and the behavior id is suffixed with ::instanceUuid.
34088
- *
34089
- * @param {Object} template - Entry from compData.defaultBehaviors[]
34090
- * @param {string} instanceUuid - UUID of the placed component instance
34091
- * @param {Object} componentDictionary
34092
- * @returns {Object|null}
34093
- */
34094
- }, {
34095
- key: "_expandDefaultBehavior",
34096
- value: function _expandDefaultBehavior(template, instanceUuid, componentDictionary) {
34097
- var base;
34098
- if (template.behaviorRef) {
34099
- // Resolve the device ref first (substitutes 'self' -> attachment)
34100
- base = this._resolveBehaviorRef(template, componentDictionary);
34101
- if (!base) return null;
34102
- } else {
34103
- base = JSON.parse(JSON.stringify(template));
34104
- }
34105
- // Suffix the id so each instance gets a unique behavior id
34106
- base.id = "".concat(base.id, "::").concat(instanceUuid);
34107
- // Inject the instance UUID as the component scope guard
34108
- base.input = _objectSpread2(_objectSpread2({}, base.input), {}, {
34109
- component: instanceUuid
34110
- });
34111
- base.output = _objectSpread2(_objectSpread2({}, base.output), {}, {
34112
- component: instanceUuid
34113
- });
34114
- return base;
34115
- }
34116
-
34117
33700
  /**
34118
33701
  * Save original world matrices for direction calculations
34119
33702
  */
@@ -34182,7 +33765,7 @@ var SceneOperationsManager = /*#__PURE__*/function () {
34182
33765
  key: "loadSceneFromData",
34183
33766
  value: (function () {
34184
33767
  var _loadSceneFromData = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9(data) {
34185
- var _data$scene4, _data$scene5, _data$scene6, _data$scene7;
33768
+ var _data$scene3, _data$scene4, _data$scene5, _data$scene6;
34186
33769
  return _regenerator().w(function (_context9) {
34187
33770
  while (1) switch (_context9.n) {
34188
33771
  case 0:
@@ -34192,10 +33775,10 @@ var SceneOperationsManager = /*#__PURE__*/function () {
34192
33775
  dataType: _typeof(data),
34193
33776
  hasScene: !!(data !== null && data !== void 0 && data.scene),
34194
33777
  sceneType: _typeof(data === null || data === void 0 ? void 0 : data.scene),
34195
- hasChildren: !!(data !== null && data !== void 0 && (_data$scene4 = data.scene) !== null && _data$scene4 !== void 0 && _data$scene4.children),
34196
- childrenType: data !== null && data !== void 0 && (_data$scene5 = data.scene) !== null && _data$scene5 !== void 0 && _data$scene5.children ? _typeof(data.scene.children) : 'undefined',
34197
- isArray: Array.isArray(data === null || data === void 0 || (_data$scene6 = data.scene) === null || _data$scene6 === void 0 ? void 0 : _data$scene6.children),
34198
- childrenLength: data === null || data === void 0 || (_data$scene7 = data.scene) === null || _data$scene7 === void 0 || (_data$scene7 = _data$scene7.children) === null || _data$scene7 === void 0 ? void 0 : _data$scene7.length,
33778
+ hasChildren: !!(data !== null && data !== void 0 && (_data$scene3 = data.scene) !== null && _data$scene3 !== void 0 && _data$scene3.children),
33779
+ childrenType: data !== null && data !== void 0 && (_data$scene4 = data.scene) !== null && _data$scene4 !== void 0 && _data$scene4.children ? _typeof(data.scene.children) : 'undefined',
33780
+ isArray: Array.isArray(data === null || data === void 0 || (_data$scene5 = data.scene) === null || _data$scene5 === void 0 ? void 0 : _data$scene5.children),
33781
+ childrenLength: data === null || data === void 0 || (_data$scene6 = data.scene) === null || _data$scene6 === void 0 || (_data$scene6 = _data$scene6.children) === null || _data$scene6 === void 0 ? void 0 : _data$scene6.length,
34199
33782
  dataKeys: data ? Object.keys(data) : [],
34200
33783
  sceneKeys: data !== null && data !== void 0 && data.scene ? Object.keys(data.scene) : []
34201
33784
  });
@@ -34326,8 +33909,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
34326
33909
  // Process children (connectors, etc.) if they exist
34327
33910
  if (componentModel.children && componentModel.children.length > 0) {
34328
33911
  componentModel.children.forEach(function (child) {
34329
- var _child$userData1, _child$userData10;
34330
- var childType = ((_child$userData1 = child.userData) === null || _child$userData1 === void 0 ? void 0 : _child$userData1.objectType) || ((_child$userData10 = child.userData) === null || _child$userData10 === void 0 ? void 0 : _child$userData10.objectType);
33912
+ var _child$userData0, _child$userData1;
33913
+ var childType = ((_child$userData0 = child.userData) === null || _child$userData0 === void 0 ? void 0 : _child$userData0.objectType) || ((_child$userData1 = child.userData) === null || _child$userData1 === void 0 ? void 0 : _child$userData1.objectType);
34331
33914
  if (childType === 'connector') {
34332
33915
  var _child$geometry;
34333
33916
  var childBoundingBox = new THREE__namespace.Box3().setFromObject(child);
@@ -34412,8 +33995,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
34412
33995
  if (segment.children && segment.children.length > 0) {
34413
33996
  var childrenToRemove = _toConsumableArray(segment.children);
34414
33997
  childrenToRemove.forEach(function (child) {
34415
- var _child$userData11;
34416
- if ((_child$userData11 = child.userData) !== null && _child$userData11 !== void 0 && _child$userData11.isPipeElbow) {
33998
+ var _child$userData10;
33999
+ if ((_child$userData10 = child.userData) !== null && _child$userData10 !== void 0 && _child$userData10.isPipeElbow) {
34417
34000
  console.log("\uD83D\uDDD1\uFE0F Removing elbow child from segment before manualization: ".concat(child.uuid));
34418
34001
  segment.remove(child);
34419
34002
  if (child.geometry) child.geometry.dispose();
@@ -34564,7 +34147,7 @@ var SceneOperationsManager = /*#__PURE__*/function () {
34564
34147
  value: function _convertConnectedGatewaysToManual(connectors, currentSceneData) {
34565
34148
  var _connectors$,
34566
34149
  _segment$userData2,
34567
- _this7 = this;
34150
+ _this5 = this;
34568
34151
  console.log('🔍 Checking for connected gateways to convert to manual...');
34569
34152
  var sceneViewer = this.sceneViewer;
34570
34153
  var convertedGateways = [];
@@ -34601,7 +34184,7 @@ var SceneOperationsManager = /*#__PURE__*/function () {
34601
34184
  console.log("\uD83D\uDD27 Found computed gateway at endpoint: ".concat(endpointObject.uuid, " - converting to manual"));
34602
34185
 
34603
34186
  // Convert gateway to manual (declared) using manualizeGateway for consistency
34604
- _this7.manualizeGateway(endpointObject, currentSceneData);
34187
+ _this5.manualizeGateway(endpointObject, currentSceneData);
34605
34188
  convertedGateways.push(endpointObject);
34606
34189
  } else if (((_endpointObject$userD5 = endpointObject.userData) === null || _endpointObject$userD5 === void 0 ? void 0 : _endpointObject$userD5.objectType) === 'gateway' && ((_endpointObject$userD6 = endpointObject.userData) === null || _endpointObject$userD6 === void 0 ? void 0 : _endpointObject$userD6.isDeclared) === true) {
34607
34190
  console.log("\u2139\uFE0F Gateway ".concat(endpointObject.uuid, " is already declared (manual), skipping conversion"));
@@ -34791,11 +34374,18 @@ var AnimationManager = /*#__PURE__*/function (_BaseDisposable) {
34791
34374
  sceneViewer.performanceMonitorManager.beginFrame();
34792
34375
  }
34793
34376
  try {
34377
+ var _sceneViewer$managers;
34794
34378
  // Update controls
34795
34379
  sceneViewer.controls.update();
34796
34380
 
34797
- // Render the scene
34798
- sceneViewer.renderer.render(sceneViewer.scene, sceneViewer.camera);
34381
+ // Render the scene — route through the outline manager when active so
34382
+ // the mask pass and screen-space composite run after the main render.
34383
+ var ioOutline = (_sceneViewer$managers = sceneViewer.managers) === null || _sceneViewer$managers === void 0 ? void 0 : _sceneViewer$managers.ioOutlineManager;
34384
+ if (ioOutline !== null && ioOutline !== void 0 && ioOutline.isActive) {
34385
+ ioOutline.render();
34386
+ } else {
34387
+ sceneViewer.renderer.render(sceneViewer.scene, sceneViewer.camera);
34388
+ }
34799
34389
  } catch (renderError) {
34800
34390
  // Catch WebGL or rendering errors to prevent the animation loop from
34801
34391
  // producing a permanent white screen. Log once and continue so that
@@ -37030,7 +36620,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37030
36620
  }, {
37031
36621
  key: "toggleIODeviceBinaryState",
37032
36622
  value: function toggleIODeviceBinaryState(ioDeviceObject) {
37033
- var _ref, _this$sceneViewer, _this$sceneViewer2;
36623
+ var _ref, _this$sceneViewer;
37034
36624
  if (!ioDeviceObject || !this._stateAdapter) return;
37035
36625
  var ud = ioDeviceObject.userData;
37036
36626
  var attachmentId = ud === null || ud === void 0 ? void 0 : ud.attachmentId;
@@ -37064,11 +36654,172 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37064
36654
  var currentVal = (_ref = storedVal !== null && storedVal !== void 0 ? storedVal : binaryState.defaultValue) !== null && _ref !== void 0 ? _ref : false;
37065
36655
  var newVal = !Boolean(currentVal);
37066
36656
  this._stateAdapter.setState(scopedAttachmentId, dpId, newVal);
37067
- (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.managers) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.behaviorManager) === null || _this$sceneViewer === void 0 || _this$sceneViewer.triggerState(attachmentId, dpId, newVal, parentUuid);
37068
- (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.managers) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.ioAnimationManager) === null || _this$sceneViewer2 === void 0 || _this$sceneViewer2.triggerState(attachmentId, dpId, newVal, parentUuid);
36657
+ (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.managers) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.ioAnimationManager) === null || _this$sceneViewer === void 0 || _this$sceneViewer.triggerState(attachmentId, dpId, newVal, parentUuid);
37069
36658
  console.log("\uD83D\uDD04 [IODevice] Toggled ".concat(scopedAttachmentId, ".").concat(dpId, ": ").concat(currentVal, " \u2192 ").concat(newVal));
37070
36659
  }
37071
36660
 
36661
+ // ── IO device drag-to-state ─────────────────────────────────────────────
36662
+
36663
+ /**
36664
+ * Begin tracking a drag gesture on an IO device mesh.
36665
+ * Records the initial state of each animation data point so that
36666
+ * `updateIODeviceDrag` can compute relative offsets from it.
36667
+ *
36668
+ * @param {THREE.Object3D} ioDeviceObject
36669
+ */
36670
+ }, {
36671
+ key: "startIODeviceDrag",
36672
+ value: function startIODeviceDrag(ioDeviceObject) {
36673
+ var _this$sceneViewer2,
36674
+ _this2 = this;
36675
+ if (!ioDeviceObject || !this._stateAdapter) return;
36676
+ var ud = ioDeviceObject.userData;
36677
+ var attachmentId = ud === null || ud === void 0 ? void 0 : ud.attachmentId;
36678
+ if (!attachmentId) return;
36679
+ var parentUuid = null;
36680
+ var obj = ioDeviceObject.parent;
36681
+ while (obj) {
36682
+ var _obj$userData2;
36683
+ if (((_obj$userData2 = obj.userData) === null || _obj$userData2 === void 0 ? void 0 : _obj$userData2.objectType) === 'component') {
36684
+ parentUuid = obj.uuid;
36685
+ break;
36686
+ }
36687
+ obj = obj.parent;
36688
+ }
36689
+ var scopedAttachmentId = this._getScopedAttachmentKey(attachmentId, parentUuid);
36690
+ var ioAnimMgr = (_this$sceneViewer2 = this.sceneViewer) === null || _this$sceneViewer2 === void 0 || (_this$sceneViewer2 = _this$sceneViewer2.managers) === null || _this$sceneViewer2 === void 0 ? void 0 : _this$sceneViewer2.ioAnimationManager;
36691
+ var dataPoints = ((ioAnimMgr === null || ioAnimMgr === void 0 ? void 0 : ioAnimMgr.getAnimationDataPoints(parentUuid, attachmentId)) || []).concat((ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [])
36692
+ // deduplicate by id
36693
+ .filter(function (dp, i, arr) {
36694
+ return arr.findIndex(function (d) {
36695
+ return d.id === dp.id;
36696
+ }) === i;
36697
+ });
36698
+ var dpSessions = [];
36699
+ var _iterator = _createForOfIteratorHelper(dataPoints),
36700
+ _step;
36701
+ try {
36702
+ var _loop = function _loop() {
36703
+ var _this2$_stateAdapter$;
36704
+ var dp = _step.value;
36705
+ var stateType = (dp.stateType || '').toLowerCase();
36706
+ if (stateType !== 'binary' && stateType !== 'boolean' && stateType !== 'enum') return 1; // continue
36707
+ var curVal = (_this2$_stateAdapter$ = _this2._stateAdapter.getState(scopedAttachmentId, dp.id)) !== null && _this2$_stateAdapter$ !== void 0 ? _this2$_stateAdapter$ : dp.defaultValue;
36708
+ if (stateType === 'binary' || stateType === 'boolean') {
36709
+ dpSessions.push({
36710
+ dp: dp,
36711
+ scopedAttachmentId: scopedAttachmentId,
36712
+ attachmentId: attachmentId,
36713
+ parentUuid: parentUuid,
36714
+ stateType: 'binary',
36715
+ lastApplied: curVal
36716
+ });
36717
+ } else {
36718
+ var _dp$stateConfig;
36719
+ var opts = ((_dp$stateConfig = dp.stateConfig) === null || _dp$stateConfig === void 0 ? void 0 : _dp$stateConfig.options) || [];
36720
+ var curIdx = opts.findIndex(function (o) {
36721
+ return String(o) === String(curVal);
36722
+ });
36723
+ dpSessions.push({
36724
+ dp: dp,
36725
+ scopedAttachmentId: scopedAttachmentId,
36726
+ attachmentId: attachmentId,
36727
+ parentUuid: parentUuid,
36728
+ stateType: 'enum',
36729
+ opts: opts,
36730
+ startIdx: curIdx >= 0 ? curIdx : 0,
36731
+ lastAppliedIdx: curIdx >= 0 ? curIdx : 0
36732
+ });
36733
+ }
36734
+ };
36735
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
36736
+ if (_loop()) continue;
36737
+ }
36738
+ } catch (err) {
36739
+ _iterator.e(err);
36740
+ } finally {
36741
+ _iterator.f();
36742
+ }
36743
+ this._ioDragSession = dpSessions.length ? {
36744
+ dpSessions: dpSessions
36745
+ } : null;
36746
+ }
36747
+
36748
+ /**
36749
+ * Update animated mesh state while a drag is in progress.
36750
+ * Called continuously during pointermove.
36751
+ *
36752
+ * Sign convention: up/right = positive `signedDelta`.
36753
+ * - Binary: > +20 px → true/on state, < −20 px → false/off state.
36754
+ * - Enum: each ±30 px step advances/retreats one option in the list.
36755
+ *
36756
+ * @param {number} signedDelta - Cumulative signed pixel displacement since drag start
36757
+ */
36758
+ }, {
36759
+ key: "updateIODeviceDrag",
36760
+ value: function updateIODeviceDrag(signedDelta) {
36761
+ var session = this._ioDragSession;
36762
+ if (!session) return;
36763
+ var BINARY_THRESHOLD = 20;
36764
+ var ENUM_STEP_PX = 30;
36765
+ var _iterator2 = _createForOfIteratorHelper(session.dpSessions),
36766
+ _step2;
36767
+ try {
36768
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
36769
+ var dps = _step2.value;
36770
+ if (dps.stateType === 'binary') {
36771
+ var newVal = void 0;
36772
+ if (signedDelta > BINARY_THRESHOLD) {
36773
+ newVal = true;
36774
+ } else if (signedDelta < -BINARY_THRESHOLD) {
36775
+ newVal = false;
36776
+ } else {
36777
+ continue; // dead zone
36778
+ }
36779
+ if (newVal === dps.lastApplied) continue;
36780
+ dps.lastApplied = newVal;
36781
+ this._applyDpState(dps, newVal);
36782
+ } else if (dps.stateType === 'enum') {
36783
+ var steps = Math.round(signedDelta / ENUM_STEP_PX);
36784
+ var newIdx = Math.max(0, Math.min(dps.opts.length - 1, dps.startIdx + steps));
36785
+ if (newIdx === dps.lastAppliedIdx) continue;
36786
+ dps.lastAppliedIdx = newIdx;
36787
+ this._applyDpState(dps, dps.opts[newIdx]);
36788
+ }
36789
+ }
36790
+ } catch (err) {
36791
+ _iterator2.e(err);
36792
+ } finally {
36793
+ _iterator2.f();
36794
+ }
36795
+ }
36796
+
36797
+ /**
36798
+ * Clean up drag session state on pointerup.
36799
+ */
36800
+ }, {
36801
+ key: "endIODeviceDrag",
36802
+ value: function endIODeviceDrag() {
36803
+ this._ioDragSession = null;
36804
+ }
36805
+
36806
+ /**
36807
+ * Apply a new value to a data point, updating Vuex state and firing behavior/animation triggers.
36808
+ * @private
36809
+ */
36810
+ }, {
36811
+ key: "_applyDpState",
36812
+ value: function _applyDpState(_ref2, newVal) {
36813
+ var _this$_stateAdapter, _this$sceneViewer3;
36814
+ var scopedAttachmentId = _ref2.scopedAttachmentId,
36815
+ attachmentId = _ref2.attachmentId,
36816
+ parentUuid = _ref2.parentUuid,
36817
+ dp = _ref2.dp;
36818
+ var dpId = dp.id;
36819
+ (_this$_stateAdapter = this._stateAdapter) === null || _this$_stateAdapter === void 0 || _this$_stateAdapter.setState(scopedAttachmentId, dpId, newVal);
36820
+ (_this$sceneViewer3 = this.sceneViewer) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.managers) === null || _this$sceneViewer3 === void 0 || (_this$sceneViewer3 = _this$sceneViewer3.ioAnimationManager) === null || _this$sceneViewer3 === void 0 || _this$sceneViewer3.triggerState(attachmentId, dpId, newVal, parentUuid);
36821
+ }
36822
+
37072
36823
  /**
37073
36824
  * Should be called when an object is selected or deselected.
37074
36825
  * @param {THREE.Object3D|null} object
@@ -37182,18 +36933,18 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37182
36933
  }, {
37183
36934
  key: "_getIODevices",
37184
36935
  value: function _getIODevices(object) {
37185
- var _this2 = this;
36936
+ var _this3 = this;
37186
36937
  var devices = [];
37187
36938
  var parentUuid = object.uuid; // The component's own UUID
37188
36939
  object.traverse(function (child) {
37189
36940
  var _child$userData;
37190
36941
  if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'io-device') {
37191
- var _this2$sceneViewer$ma, _this2$sceneViewer;
36942
+ var _this3$sceneViewer$ma, _this3$sceneViewer;
37192
36943
  var attachmentId = child.userData.attachmentId || '';
37193
36944
 
37194
36945
  // Use only data points from the animate window (animationConfig).
37195
36946
  // The static ioConfig.states[] snapshot on userData is intentionally ignored.
37196
- var dataPoints = (_this2$sceneViewer$ma = (_this2$sceneViewer = _this2.sceneViewer) === null || _this2$sceneViewer === void 0 || (_this2$sceneViewer = _this2$sceneViewer.managers) === null || _this2$sceneViewer === void 0 || (_this2$sceneViewer = _this2$sceneViewer.ioAnimationManager) === null || _this2$sceneViewer === void 0 ? void 0 : _this2$sceneViewer.getAnimationDataPoints(parentUuid, attachmentId)) !== null && _this2$sceneViewer$ma !== void 0 ? _this2$sceneViewer$ma : [];
36947
+ var dataPoints = (_this3$sceneViewer$ma = (_this3$sceneViewer = _this3.sceneViewer) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.managers) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.ioAnimationManager) === null || _this3$sceneViewer === void 0 ? void 0 : _this3$sceneViewer.getAnimationDataPoints(parentUuid, attachmentId)) !== null && _this3$sceneViewer$ma !== void 0 ? _this3$sceneViewer$ma : [];
37197
36948
 
37198
36949
  // When data points come from animationConfig they already carry direction:'input'.
37199
36950
  // Pass null so _buildDataPointRow uses the per-dp direction instead of the
@@ -37203,7 +36954,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37203
36954
  label: child.userData.attachmentLabel || child.name || child.userData.deviceId || 'Unknown Device',
37204
36955
  deviceId: child.userData.deviceId || '',
37205
36956
  attachmentId: attachmentId,
37206
- scopedAttachmentId: _this2._getScopedAttachmentKey(attachmentId, parentUuid),
36957
+ scopedAttachmentId: _this3._getScopedAttachmentKey(attachmentId, parentUuid),
37207
36958
  dataPoints: dataPoints,
37208
36959
  direction: deviceDirection
37209
36960
  });
@@ -37219,7 +36970,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37219
36970
  }, {
37220
36971
  key: "_buildTooltip",
37221
36972
  value: function _buildTooltip(object) {
37222
- var _this3 = this;
36973
+ var _this4 = this;
37223
36974
  // Remove any existing tooltip first
37224
36975
  this.hide();
37225
36976
  // Re-assign selected object since hide() clears it
@@ -37290,7 +37041,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37290
37041
  // Use scopedAttachmentId to ensure state is isolated per component instance
37291
37042
  if (device.scopedAttachmentId && device.dataPoints.length > 0) {
37292
37043
  device.dataPoints.forEach(function (dp) {
37293
- var row = _this3._buildDataPointRow(device.scopedAttachmentId, dp, device.direction, device.attachmentId);
37044
+ var row = _this4._buildDataPointRow(device.scopedAttachmentId, dp, device.direction, device.attachmentId);
37294
37045
  list.appendChild(row);
37295
37046
  });
37296
37047
  }
@@ -37301,11 +37052,11 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37301
37052
  // Hover expand/collapse
37302
37053
  trigger.addEventListener('mouseenter', function () {
37303
37054
  ioSection.classList.add('expanded');
37304
- _this3._ioExpanded = true;
37055
+ _this4._ioExpanded = true;
37305
37056
  });
37306
37057
  ioSection.addEventListener('mouseleave', function () {
37307
37058
  ioSection.classList.remove('expanded');
37308
- _this3._ioExpanded = false;
37059
+ _this4._ioExpanded = false;
37309
37060
  });
37310
37061
  card.appendChild(ioSection);
37311
37062
  } else {
@@ -37349,11 +37100,11 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37349
37100
  }, {
37350
37101
  key: "_positionTooltip",
37351
37102
  value: function _positionTooltip() {
37352
- var _this$sceneViewer3, _this$sceneViewer4;
37103
+ var _this$sceneViewer4, _this$sceneViewer5;
37353
37104
  if (!this.tooltipEl || !this.selectedObject) return;
37354
37105
  var container = this._getContainer();
37355
- var camera = (_this$sceneViewer3 = this.sceneViewer) === null || _this$sceneViewer3 === void 0 ? void 0 : _this$sceneViewer3.camera;
37356
- var renderer = (_this$sceneViewer4 = this.sceneViewer) === null || _this$sceneViewer4 === void 0 ? void 0 : _this$sceneViewer4.renderer;
37106
+ var camera = (_this$sceneViewer4 = this.sceneViewer) === null || _this$sceneViewer4 === void 0 ? void 0 : _this$sceneViewer4.camera;
37107
+ var renderer = (_this$sceneViewer5 = this.sceneViewer) === null || _this$sceneViewer5 === void 0 ? void 0 : _this$sceneViewer5.renderer;
37357
37108
  if (!container || !camera || !renderer) return;
37358
37109
 
37359
37110
  // Compute bounding box to position above the component
@@ -37390,8 +37141,8 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37390
37141
  }, {
37391
37142
  key: "_getContainer",
37392
37143
  value: function _getContainer() {
37393
- var _this$sceneViewer5;
37394
- return ((_this$sceneViewer5 = this.sceneViewer) === null || _this$sceneViewer5 === void 0 || (_this$sceneViewer5 = _this$sceneViewer5.renderer) === null || _this$sceneViewer5 === void 0 || (_this$sceneViewer5 = _this$sceneViewer5.domElement) === null || _this$sceneViewer5 === void 0 ? void 0 : _this$sceneViewer5.parentElement) || null;
37144
+ var _this$sceneViewer6;
37145
+ return ((_this$sceneViewer6 = this.sceneViewer) === null || _this$sceneViewer6 === void 0 || (_this$sceneViewer6 = _this$sceneViewer6.renderer) === null || _this$sceneViewer6 === void 0 || (_this$sceneViewer6 = _this$sceneViewer6.domElement) === null || _this$sceneViewer6 === void 0 ? void 0 : _this$sceneViewer6.parentElement) || null;
37395
37146
  }
37396
37147
 
37397
37148
  // -----------------------------------------------------------------------
@@ -37413,10 +37164,10 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37413
37164
  }, {
37414
37165
  key: "_buildDataPointRow",
37415
37166
  value: function _buildDataPointRow(scopedAttachmentId, dp, deviceDirection, originalAttachmentId) {
37416
- var _ref2,
37167
+ var _ref3,
37417
37168
  _this$_stateAdapter$g,
37418
- _this$_stateAdapter,
37419
- _this4 = this;
37169
+ _this$_stateAdapter2,
37170
+ _this5 = this;
37420
37171
  var row = document.createElement('div');
37421
37172
  row.className = 'cp-tooltip__dp-row';
37422
37173
  var nameEl = document.createElement('span');
@@ -37428,18 +37179,13 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37428
37179
  // Device-level direction takes precedence; fall back to per-dp direction
37429
37180
  var resolvedDirection = deviceDirection || dp.direction || 'output';
37430
37181
  var isInput = resolvedDirection === 'input' || resolvedDirection === 'bidirectional';
37431
- var currentVal = (_ref2 = (_this$_stateAdapter$g = (_this$_stateAdapter = this._stateAdapter) === null || _this$_stateAdapter === void 0 ? void 0 : _this$_stateAdapter.getState(scopedAttachmentId, dpId)) !== null && _this$_stateAdapter$g !== void 0 ? _this$_stateAdapter$g : dp.defaultValue) !== null && _ref2 !== void 0 ? _ref2 : null;
37182
+ var currentVal = (_ref3 = (_this$_stateAdapter$g = (_this$_stateAdapter2 = this._stateAdapter) === null || _this$_stateAdapter2 === void 0 ? void 0 : _this$_stateAdapter2.getState(scopedAttachmentId, dpId)) !== null && _this$_stateAdapter$g !== void 0 ? _this$_stateAdapter$g : dp.defaultValue) !== null && _ref3 !== void 0 ? _ref3 : null;
37432
37183
  if (isInput) {
37433
37184
  var ctrl = this._buildInputControl(dp, currentVal, function (newVal) {
37434
- var _this4$_stateAdapter, _this4$selectedObject, _this4$sceneViewer, _this4$sceneViewer2;
37435
- (_this4$_stateAdapter = _this4._stateAdapter) === null || _this4$_stateAdapter === void 0 || _this4$_stateAdapter.setState(scopedAttachmentId, dpId, newVal);
37436
- // Also fire BehaviorManager so any wired behaviors react immediately.
37437
- // Pass the parent component UUID so behaviors scoped to a specific instance
37438
- // don't bleed across clones that share the same attachmentId.
37439
- // Use originalAttachmentId for behavior triggering as behaviors are keyed by original ID
37440
- var parentUuid = ((_this4$selectedObject = _this4.selectedObject) === null || _this4$selectedObject === void 0 ? void 0 : _this4$selectedObject.uuid) || null;
37441
- (_this4$sceneViewer = _this4.sceneViewer) === null || _this4$sceneViewer === void 0 || (_this4$sceneViewer = _this4$sceneViewer.managers) === null || _this4$sceneViewer === void 0 || (_this4$sceneViewer = _this4$sceneViewer.behaviorManager) === null || _this4$sceneViewer === void 0 || _this4$sceneViewer.triggerState(originalAttachmentId || scopedAttachmentId, dpId, newVal, parentUuid);
37442
- (_this4$sceneViewer2 = _this4.sceneViewer) === null || _this4$sceneViewer2 === void 0 || (_this4$sceneViewer2 = _this4$sceneViewer2.managers) === null || _this4$sceneViewer2 === void 0 || (_this4$sceneViewer2 = _this4$sceneViewer2.ioAnimationManager) === null || _this4$sceneViewer2 === void 0 || _this4$sceneViewer2.triggerState(originalAttachmentId || scopedAttachmentId, dpId, newVal, parentUuid);
37185
+ var _this5$_stateAdapter, _this5$selectedObject, _this5$sceneViewer;
37186
+ (_this5$_stateAdapter = _this5._stateAdapter) === null || _this5$_stateAdapter === void 0 || _this5$_stateAdapter.setState(scopedAttachmentId, dpId, newVal);
37187
+ var parentUuid = ((_this5$selectedObject = _this5.selectedObject) === null || _this5$selectedObject === void 0 ? void 0 : _this5$selectedObject.uuid) || null;
37188
+ (_this5$sceneViewer = _this5.sceneViewer) === null || _this5$sceneViewer === void 0 || (_this5$sceneViewer = _this5$sceneViewer.managers) === null || _this5$sceneViewer === void 0 || (_this5$sceneViewer = _this5$sceneViewer.ioAnimationManager) === null || _this5$sceneViewer === void 0 || _this5$sceneViewer.triggerState(originalAttachmentId || scopedAttachmentId, dpId, newVal, parentUuid);
37443
37189
  });
37444
37190
  row.appendChild(ctrl);
37445
37191
  this._stateElements.set(key, {
@@ -37448,9 +37194,9 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37448
37194
  isInput: true
37449
37195
  });
37450
37196
  } else {
37451
- var _dp$stateConfig;
37197
+ var _dp$stateConfig2;
37452
37198
  // unit suffix (optional, shown between name and badge)
37453
- var unit = (_dp$stateConfig = dp.stateConfig) === null || _dp$stateConfig === void 0 ? void 0 : _dp$stateConfig.unit;
37199
+ var unit = (_dp$stateConfig2 = dp.stateConfig) === null || _dp$stateConfig2 === void 0 ? void 0 : _dp$stateConfig2.unit;
37454
37200
  if (unit) {
37455
37201
  var unitEl = document.createElement('span');
37456
37202
  unitEl.className = 'cp-tooltip__dp-unit';
@@ -37595,7 +37341,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37595
37341
  }, {
37596
37342
  key: "_refreshStateDisplays",
37597
37343
  value: function _refreshStateDisplays() {
37598
- var _this5 = this;
37344
+ var _this6 = this;
37599
37345
  if (!this._stateAdapter || !this._stateElements.size) return;
37600
37346
  this._stateElements.forEach(function (entry, key) {
37601
37347
  if (entry.isInput) return; // interactive controls are user-driven; don't overwrite
@@ -37604,8 +37350,8 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
37604
37350
  if (sepIdx === -1) return;
37605
37351
  var scopedAttachmentId = key.slice(0, sepIdx);
37606
37352
  var dataPointId = key.slice(sepIdx + 2);
37607
- var val = _this5._stateAdapter.getState(scopedAttachmentId, dataPointId);
37608
- _this5._applyBadgeValue(entry.el, val, entry.dp);
37353
+ var val = _this6._stateAdapter.getState(scopedAttachmentId, dataPointId);
37354
+ _this6._applyBadgeValue(entry.el, val, entry.dp);
37609
37355
  });
37610
37356
  }
37611
37357
  }]);
@@ -37828,6 +37574,26 @@ var IoAnimationManager = /*#__PURE__*/function (_BaseDisposable) {
37828
37574
  return dps;
37829
37575
  }
37830
37576
 
37577
+ /**
37578
+ * Return the Three.js mesh objects that are animated for a given attachment.
37579
+ * Used by IoOutlineManager to include animated meshes in the outline.
37580
+ *
37581
+ * @param {string} parentUuid
37582
+ * @param {string} attachmentId
37583
+ * @returns {THREE.Object3D[]}
37584
+ */
37585
+ }, {
37586
+ key: "getAnimatedMeshes",
37587
+ value: function getAnimatedMeshes(parentUuid, attachmentId) {
37588
+ var key = this._key(parentUuid, attachmentId);
37589
+ var entries = this._entries.get(key);
37590
+ if (!(entries !== null && entries !== void 0 && entries.length)) return [];
37591
+ // Deduplicate — multiple animations can target the same mesh
37592
+ return _toConsumableArray(new Set(entries.map(function (e) {
37593
+ return e.mesh;
37594
+ })));
37595
+ }
37596
+
37831
37597
  /**
37832
37598
  * Remove all animation entries associated with a given host component.
37833
37599
  * Call when a component is removed from the scene.
@@ -38059,7 +37825,10 @@ var IoAnimationManager = /*#__PURE__*/function (_BaseDisposable) {
38059
37825
  value: function _applyTranslation(mesh, origPos, transform) {
38060
37826
  var _transform$x, _transform$y, _transform$z;
38061
37827
  if (!transform) return;
38062
- 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));
37828
+ // X and Y are negated to match the sign convention used in the AnimateDevicesDialog
37829
+ // preview (_syncViewerTransform negates x and y before calling setMeshPreviewOffset).
37830
+ // Z is added directly (no negation) — matching the dialog's z handling.
37831
+ 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));
38063
37832
  }
38064
37833
 
38065
37834
  /**
@@ -38130,6 +37899,235 @@ var IoAnimationManager = /*#__PURE__*/function (_BaseDisposable) {
38130
37899
  }]);
38131
37900
  }(BaseDisposable);
38132
37901
 
37902
+ /**
37903
+ * Three.js layer reserved exclusively for the outline mask pass.
37904
+ * Layer 20 is high enough to be well clear of typical app layer usage.
37905
+ */
37906
+ var OUTLINE_LAYER = 20;
37907
+
37908
+ // ── Shaders ──────────────────────────────────────────────────────────────────
37909
+
37910
+ var VERT_SHADER = /* glsl */"\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n";
37911
+
37912
+ /**
37913
+ * Screen-space outline shader.
37914
+ *
37915
+ * Samples in a circular kernel to find the nearest filled (silhouette) pixel.
37916
+ * Applies a smooth alpha falloff at the outer edge using smoothstep so the
37917
+ * outline fades cleanly rather than cutting off hard.
37918
+ *
37919
+ * RADIUS — maximum distance in pixels from the silhouette edge to draw.
37920
+ * SAMPLES — kernel half-extent; must be an integer ≥ ceil(RADIUS).
37921
+ */
37922
+ var FRAG_SHADER = /* glsl */"\nuniform sampler2D tMask;\nuniform vec2 uInvSize;\nvarying vec2 vUv;\n\nconst float RADIUS = 3.0; // total outline width in pixels\nconst int SAMPLES = 4; // kernel half-extent (\u2265 ceil(RADIUS))\n\nvoid main() {\n float center = texture2D(tMask, vUv).r;\n\n // Pixels inside the silhouette: the main render already drew them.\n if (center > 0.5) discard;\n\n // Find the closest filled neighbour within the circular kernel.\n float minDist = 99.0;\n for (int x = -SAMPLES; x <= SAMPLES; x++) {\n for (int y = -SAMPLES; y <= SAMPLES; y++) {\n float dist = length(vec2(float(x), float(y)));\n if (dist > RADIUS + 1.0) continue; // skip corners outside circle\n vec2 offset = vec2(float(x), float(y)) * uInvSize;\n if (texture2D(tMask, vUv + offset).r > 0.5) {\n minDist = min(minDist, dist);\n }\n }\n }\n\n if (minDist > RADIUS + 0.5) discard;\n\n // Smooth alpha: full opacity near the edge, fades to 0 at RADIUS pixels out.\n float alpha = 1.0 - smoothstep(RADIUS - 1.0, RADIUS + 0.5, minDist);\n gl_FragColor = vec4(1.0, 1.0, 1.0, alpha);\n}\n";
37923
+ var IoOutlineManager = /*#__PURE__*/function (_BaseDisposable) {
37924
+ function IoOutlineManager(sceneViewer) {
37925
+ var _this;
37926
+ _classCallCheck(this, IoOutlineManager);
37927
+ _this = _callSuper(this, IoOutlineManager);
37928
+ _this.sceneViewer = sceneViewer;
37929
+ _this._maskTarget = null;
37930
+ _this._maskMat = null;
37931
+ _this._overlayScene = null;
37932
+ _this._overlayCamera = null;
37933
+ _this._overlayMat = null;
37934
+
37935
+ /**
37936
+ * Meshes that have been moved onto OUTLINE_LAYER, stored with their
37937
+ * original layers.mask so they can be restored on clearance.
37938
+ * @type {{ mesh: THREE.Mesh, originalMask: number }[]}
37939
+ */
37940
+ _this._layeredMeshes = [];
37941
+
37942
+ /** @type {boolean} Whether an outline is currently active. */
37943
+ _this.isActive = false;
37944
+ return _this;
37945
+ }
37946
+
37947
+ // ─────────────────────────────────────────────────────────────────────────
37948
+ // PUBLIC API
37949
+ // ─────────────────────────────────────────────────────────────────────────
37950
+
37951
+ /**
37952
+ * Set the objects to outline. Pass an empty array (or call with no args)
37953
+ * to remove the outline.
37954
+ * @param {THREE.Object3D[]} objects
37955
+ */
37956
+ _inherits(IoOutlineManager, _BaseDisposable);
37957
+ return _createClass(IoOutlineManager, [{
37958
+ key: "setTargets",
37959
+ value: function setTargets() {
37960
+ var _this2 = this;
37961
+ var objects = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
37962
+ this._clearLayeredMeshes();
37963
+ if (!objects.length) {
37964
+ this.isActive = false;
37965
+ return;
37966
+ }
37967
+ objects.forEach(function (obj) {
37968
+ obj.traverse(function (child) {
37969
+ if (!child.isMesh) return;
37970
+ var originalMask = child.layers.mask;
37971
+ child.layers.enable(OUTLINE_LAYER);
37972
+ _this2._layeredMeshes.push({
37973
+ mesh: child,
37974
+ originalMask: originalMask
37975
+ });
37976
+ });
37977
+ });
37978
+ this.isActive = this._layeredMeshes.length > 0;
37979
+ }
37980
+
37981
+ /**
37982
+ * Render one frame: main scene → mask pass → outline composite.
37983
+ * Must be called instead of renderer.render() when isActive is true.
37984
+ */
37985
+ }, {
37986
+ key: "render",
37987
+ value: function render() {
37988
+ var sv = this.sceneViewer;
37989
+ var renderer = sv.renderer,
37990
+ scene = sv.scene,
37991
+ camera = sv.camera;
37992
+ if (!renderer || !scene || !camera) return;
37993
+
37994
+ // ── Step 1: Normal scene render — sky, transparency, all unaffected ──
37995
+ renderer.render(scene, camera);
37996
+ if (!this.isActive) return;
37997
+ this._ensureResources();
37998
+ if (!this._maskTarget) return;
37999
+
38000
+ // Stash renderer / scene / camera state we are about to mutate
38001
+ var prevBg = scene.background;
38002
+ var prevOverride = scene.overrideMaterial;
38003
+ var prevLayerMask = camera.layers.mask;
38004
+ var prevAutoClear = renderer.autoClear;
38005
+ var prevClearClr = renderer.getClearColor(new THREE__namespace.Color());
38006
+ var prevClearA = renderer.getClearAlpha();
38007
+
38008
+ // ── Step 2: Render the device silhouette into the private mask target ──
38009
+ scene.background = null; // transparent clear
38010
+ scene.overrideMaterial = this._maskMat; // flat white for all geometry
38011
+ camera.layers.set(OUTLINE_LAYER); // only see the device meshes
38012
+ renderer.setClearColor(0x000000, 0);
38013
+ renderer.autoClear = true;
38014
+ renderer.setRenderTarget(this._maskTarget);
38015
+ renderer.render(scene, camera);
38016
+ renderer.setRenderTarget(null);
38017
+
38018
+ // Restore mutated state
38019
+ scene.background = prevBg;
38020
+ scene.overrideMaterial = prevOverride;
38021
+ camera.layers.mask = prevLayerMask;
38022
+ renderer.setClearColor(prevClearClr, prevClearA);
38023
+
38024
+ // ── Step 3: Composite the outline on top without clearing the framebuffer
38025
+ renderer.autoClear = false;
38026
+ renderer.render(this._overlayScene, this._overlayCamera);
38027
+ renderer.autoClear = prevAutoClear;
38028
+ }
38029
+
38030
+ /**
38031
+ * Call when the canvas is resized to keep the mask target and shader in sync.
38032
+ * @param {number} width
38033
+ * @param {number} height
38034
+ */
38035
+ }, {
38036
+ key: "setSize",
38037
+ value: function setSize(width, height) {
38038
+ if (this._maskTarget) this._maskTarget.setSize(width, height);
38039
+ if (this._overlayMat) {
38040
+ this._overlayMat.uniforms.uInvSize.value.set(1 / width, 1 / height);
38041
+ }
38042
+ }
38043
+ }, {
38044
+ key: "dispose",
38045
+ value: function dispose() {
38046
+ this._clearLayeredMeshes();
38047
+ if (this._maskTarget) {
38048
+ this._maskTarget.dispose();
38049
+ this._maskTarget = null;
38050
+ }
38051
+ if (this._maskMat) {
38052
+ this._maskMat.dispose();
38053
+ this._maskMat = null;
38054
+ }
38055
+ if (this._overlayMat) {
38056
+ this._overlayMat.dispose();
38057
+ this._overlayMat = null;
38058
+ }
38059
+ _superPropGet(IoOutlineManager, "dispose", this, 3)([]);
38060
+ }
38061
+
38062
+ // ─────────────────────────────────────────────────────────────────────────
38063
+ // PRIVATE
38064
+ // ─────────────────────────────────────────────────────────────────────────
38065
+
38066
+ /** Lazy-initialise GPU resources on first use. */
38067
+ }, {
38068
+ key: "_ensureResources",
38069
+ value: function _ensureResources() {
38070
+ if (this._maskTarget) return;
38071
+ var renderer = this.sceneViewer.renderer;
38072
+ if (!renderer) return;
38073
+ var size = renderer.getSize(new THREE__namespace.Vector2());
38074
+ var w = size.x;
38075
+ var h = size.y;
38076
+
38077
+ // Private render target — receives the flat white device silhouette
38078
+ this._maskTarget = new THREE__namespace.WebGLRenderTarget(w, h);
38079
+
38080
+ // Flat white material used during the mask pass
38081
+ this._maskMat = new THREE__namespace.MeshBasicMaterial({
38082
+ color: 0xffffff
38083
+ });
38084
+
38085
+ // Screen-space overlay: reads the mask and draws only the outline pixels
38086
+ this._overlayMat = new THREE__namespace.ShaderMaterial({
38087
+ uniforms: {
38088
+ tMask: {
38089
+ value: this._maskTarget.texture
38090
+ },
38091
+ uInvSize: {
38092
+ value: new THREE__namespace.Vector2(1 / w, 1 / h)
38093
+ }
38094
+ },
38095
+ vertexShader: VERT_SHADER,
38096
+ fragmentShader: FRAG_SHADER,
38097
+ transparent: true,
38098
+ depthTest: false,
38099
+ depthWrite: false
38100
+ });
38101
+ var quad = new THREE__namespace.Mesh(new THREE__namespace.PlaneGeometry(2, 2), this._overlayMat);
38102
+ quad.frustumCulled = false;
38103
+ this._overlayScene = new THREE__namespace.Scene();
38104
+ this._overlayScene.add(quad);
38105
+ this._overlayCamera = new THREE__namespace.OrthographicCamera(-1, 1, 1, -1, 0, 1);
38106
+ }
38107
+
38108
+ /** Remove OUTLINE_LAYER from all tracked meshes and clear the list. */
38109
+ }, {
38110
+ key: "_clearLayeredMeshes",
38111
+ value: function _clearLayeredMeshes() {
38112
+ var _iterator = _createForOfIteratorHelper(this._layeredMeshes),
38113
+ _step;
38114
+ try {
38115
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
38116
+ var _step$value = _step.value,
38117
+ mesh = _step$value.mesh,
38118
+ originalMask = _step$value.originalMask;
38119
+ mesh.layers.mask = originalMask;
38120
+ }
38121
+ } catch (err) {
38122
+ _iterator.e(err);
38123
+ } finally {
38124
+ _iterator.f();
38125
+ }
38126
+ this._layeredMeshes = [];
38127
+ }
38128
+ }]);
38129
+ }(BaseDisposable);
38130
+
38133
38131
  // ─────────────────────────────────────────────────────────────────────────────
38134
38132
  // Flow-direction helpers (module-level)
38135
38133
  // ─────────────────────────────────────────────────────────────────────────────
@@ -38245,13 +38243,13 @@ var CentralPlantInternals = /*#__PURE__*/function () {
38245
38243
  this.centralPlant.managers.keyboardControlsManager = new KeyboardControlsManager(this.centralPlant.sceneViewer);
38246
38244
  this.centralPlant.managers.pathfindingManager = new PathfindingManager(this.centralPlant.sceneViewer);
38247
38245
  this.centralPlant.managers.pathFlowManager = new PathFlowManager(this.centralPlant.sceneViewer);
38248
- this.centralPlant.managers.behaviorManager = new BehaviorManager(this.centralPlant.sceneViewer);
38249
38246
  this.centralPlant.managers.sceneOperationsManager = new SceneOperationsManager(this.centralPlant.sceneViewer);
38250
38247
  this.centralPlant.managers.animationManager = new AnimationManager(this.centralPlant.sceneViewer);
38251
38248
  this.centralPlant.managers.cameraControlsManager = new CameraControlsManager(this.centralPlant.sceneViewer);
38252
38249
  this.centralPlant.managers.componentDragManager = new ComponentDragManager(this.centralPlant.sceneViewer);
38253
38250
  this.centralPlant.managers.viewport2DManager = new Viewport2DManager(this.centralPlant.sceneViewer);
38254
38251
  this.centralPlant.managers.ioAnimationManager = new IoAnimationManager(this.centralPlant.sceneViewer);
38252
+ this.centralPlant.managers.ioOutlineManager = new IoOutlineManager(this.centralPlant.sceneViewer);
38255
38253
 
38256
38254
  // All managers are now stored in the managers collection and will be attached via attachToComponent()
38257
38255
  }
@@ -39022,7 +39020,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
39022
39020
  return false;
39023
39021
  }
39024
39022
  try {
39025
- var _componentData$childr, _componentData$childr2, _this$centralPlant$sc7, _componentData$childr3, _componentData$defaul;
39023
+ var _componentData$childr, _componentData$childr2, _this$centralPlant$sc7, _componentData$childr3;
39026
39024
  // Generate a unique component ID if not provided
39027
39025
  var componentId = options.customId || this.generateUniqueComponentId(libraryId);
39028
39026
 
@@ -39254,20 +39252,25 @@ var CentralPlantInternals = /*#__PURE__*/function () {
39254
39252
  }
39255
39253
  }
39256
39254
 
39257
- // Register default behaviors for smart components so the BehaviorManager
39258
- // responds to tooltip-driven state changes immediately after drop.
39259
- // (The scene-load path uses _processBehaviors instead, which runs on loadSceneData.)
39260
- if ((_componentData$defaul = componentData.defaultBehaviors) !== null && _componentData$defaul !== void 0 && _componentData$defaul.length) {
39261
- var _this$centralPlant$sc9, _som$registerBehavior;
39262
- var som = (_this$centralPlant$sc9 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc9 === void 0 ? void 0 : _this$centralPlant$sc9.sceneOperationsManager;
39263
- som === null || som === void 0 || (_som$registerBehavior = som.registerBehaviorsForComponentInstance) === null || _som$registerBehavior === void 0 || _som$registerBehavior.call(som, componentData, componentId);
39264
- }
39265
-
39266
39255
  // Notify the component manager about the new component
39267
39256
  if (componentManager.registerComponent) {
39268
39257
  componentManager.registerComponent(componentModel);
39269
39258
  }
39270
39259
 
39260
+ // Pre-warm the filtered bounding-box cache for smart components so the
39261
+ // first selection is instant. Deferred to idle time so it does not
39262
+ // block the current frame.
39263
+ if (componentData.isSmart) {
39264
+ var warmFn = function warmFn() {
39265
+ return computeFilteredBoundingBoxCached(componentModel, ['io-device', 'connector']);
39266
+ };
39267
+ if (typeof requestIdleCallback !== 'undefined') {
39268
+ requestIdleCallback(warmFn);
39269
+ } else {
39270
+ setTimeout(warmFn, 0);
39271
+ }
39272
+ }
39273
+
39271
39274
  // EMIT COMPONENT ADDED EVENT
39272
39275
  // This allows UI components (like SceneHierarchy) to update reactively
39273
39276
  if (this.centralPlant.sceneViewer.emit) {
@@ -39321,18 +39324,18 @@ var CentralPlantInternals = /*#__PURE__*/function () {
39321
39324
  }, {
39322
39325
  key: "deleteComponent",
39323
39326
  value: function deleteComponent(componentId) {
39324
- var _this$centralPlant$sc0;
39327
+ var _this$centralPlant$sc9;
39325
39328
  // Check if component manager is available
39326
- var componentManager = (_this$centralPlant$sc0 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc0 === void 0 ? void 0 : _this$centralPlant$sc0.componentManager;
39329
+ var componentManager = (_this$centralPlant$sc9 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc9 === void 0 ? void 0 : _this$centralPlant$sc9.componentManager;
39327
39330
  if (!componentManager) {
39328
39331
  console.error('❌ deleteComponent(): Component manager not available');
39329
39332
  return false;
39330
39333
  }
39331
39334
  try {
39332
- var _this$centralPlant$sc1, _this$centralPlant$sc10, _sceneData$scene2, _sceneData$scene3;
39335
+ var _this$centralPlant$sc0, _this$centralPlant$sc1, _sceneData$scene2, _sceneData$scene3;
39333
39336
  console.log("\uD83D\uDDD1\uFE0F deleteComponent(): Deleting component ".concat(componentId));
39334
- var threeScene = (_this$centralPlant$sc1 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc1 === void 0 ? void 0 : _this$centralPlant$sc1.scene;
39335
- var sceneData = (_this$centralPlant$sc10 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc10 === void 0 ? void 0 : _this$centralPlant$sc10.currentSceneData;
39337
+ var threeScene = (_this$centralPlant$sc0 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc0 === void 0 ? void 0 : _this$centralPlant$sc0.scene;
39338
+ var sceneData = (_this$centralPlant$sc1 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc1 === void 0 ? void 0 : _this$centralPlant$sc1.currentSceneData;
39336
39339
 
39337
39340
  // Step 1: Resolve the actual Three.js UUID from componentId.
39338
39341
  // The UI emits object.name (e.g. "Pump (PUMP-1)") as the selection ID, but
@@ -39547,7 +39550,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
39547
39550
  * Initialize the CentralPlant manager
39548
39551
  *
39549
39552
  * @constructor
39550
- * @version 0.3.26
39553
+ * @version 0.3.28
39551
39554
  * @updated 2025-10-22
39552
39555
  *
39553
39556
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -40581,107 +40584,6 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
40581
40584
  };
40582
40585
  }
40583
40586
 
40584
- // ─────────────────────────────────────────────────────────────────────────
40585
- // BEHAVIORS API
40586
- // ─────────────────────────────────────────────────────────────────────────
40587
-
40588
- /**
40589
- * Get all behavior definitions currently stored in the BehaviorManager.
40590
- * @returns {Array<Object>} Array of behavior definition objects, or empty array.
40591
- * @example
40592
- * const behaviors = centralPlant.getBehaviors()
40593
- * behaviors.forEach(b => console.log(b.id, b.input, b.output))
40594
- */
40595
- }, {
40596
- key: "getBehaviors",
40597
- value: function getBehaviors() {
40598
- var _this$sceneViewer4;
40599
- var bm = (_this$sceneViewer4 = this.sceneViewer) === null || _this$sceneViewer4 === void 0 || (_this$sceneViewer4 = _this$sceneViewer4.managers) === null || _this$sceneViewer4 === void 0 ? void 0 : _this$sceneViewer4.behaviorManager;
40600
- if (!bm) {
40601
- console.warn('⚠️ getBehaviors(): BehaviorManager not available');
40602
- return [];
40603
- }
40604
- return bm.getBehaviors();
40605
- }
40606
-
40607
- /**
40608
- * Add a behavior definition at runtime.
40609
- * @param {Object} behaviorDef - Full behavior definition object (must have unique `id`)
40610
- * @returns {boolean} True if added successfully, false otherwise
40611
- * @example
40612
- * centralPlant.addBehavior({
40613
- * id: 'my-behavior',
40614
- * input: { attachment: 'device-01', dataPoint: 'boolean-status-01' },
40615
- * output: { attachment: 'light-01', child: 'indicator-mesh-01' },
40616
- * conditions: [{ when: 'dataPoint.value === true', actions: [{ set: 'material.emissiveIntensity', value: 1.0 }] }]
40617
- * })
40618
- */
40619
- }, {
40620
- key: "addBehavior",
40621
- value: function addBehavior(behaviorDef) {
40622
- var _this$sceneViewer5, _this$sceneViewer6;
40623
- var bm = (_this$sceneViewer5 = this.sceneViewer) === null || _this$sceneViewer5 === void 0 || (_this$sceneViewer5 = _this$sceneViewer5.managers) === null || _this$sceneViewer5 === void 0 ? void 0 : _this$sceneViewer5.behaviorManager;
40624
- if (!bm) {
40625
- console.warn('⚠️ addBehavior(): BehaviorManager not available');
40626
- return false;
40627
- }
40628
- // Also persist into currentSceneData so export includes it
40629
- if ((_this$sceneViewer6 = this.sceneViewer) !== null && _this$sceneViewer6 !== void 0 && _this$sceneViewer6.currentSceneData) {
40630
- if (!Array.isArray(this.sceneViewer.currentSceneData.behaviors)) {
40631
- this.sceneViewer.currentSceneData.behaviors = [];
40632
- }
40633
- this.sceneViewer.currentSceneData.behaviors.push(behaviorDef);
40634
- }
40635
- return bm.addBehavior(behaviorDef);
40636
- }
40637
-
40638
- /**
40639
- * Remove a behavior definition by id.
40640
- * @param {string} behaviorId
40641
- * @returns {boolean} True if removed, false if not found
40642
- * @example
40643
- * centralPlant.removeBehavior('my-behavior')
40644
- */
40645
- }, {
40646
- key: "removeBehavior",
40647
- value: function removeBehavior(behaviorId) {
40648
- var _this$sceneViewer7, _this$sceneViewer8;
40649
- var bm = (_this$sceneViewer7 = this.sceneViewer) === null || _this$sceneViewer7 === void 0 || (_this$sceneViewer7 = _this$sceneViewer7.managers) === null || _this$sceneViewer7 === void 0 ? void 0 : _this$sceneViewer7.behaviorManager;
40650
- if (!bm) {
40651
- console.warn('⚠️ removeBehavior(): BehaviorManager not available');
40652
- return false;
40653
- }
40654
- // Also remove from currentSceneData
40655
- if ((_this$sceneViewer8 = this.sceneViewer) !== null && _this$sceneViewer8 !== void 0 && (_this$sceneViewer8 = _this$sceneViewer8.currentSceneData) !== null && _this$sceneViewer8 !== void 0 && _this$sceneViewer8.behaviors) {
40656
- var idx = this.sceneViewer.currentSceneData.behaviors.findIndex(function (b) {
40657
- return b.id === behaviorId;
40658
- });
40659
- if (idx !== -1) this.sceneViewer.currentSceneData.behaviors.splice(idx, 1);
40660
- }
40661
- return bm.removeBehavior(behaviorId);
40662
- }
40663
-
40664
- /**
40665
- * Simulate an IO device state value arriving and trigger any matching behaviors.
40666
- * Useful for live testing in the UI or for integration with real data feeds.
40667
- * @param {string} attachmentId - The attachment id of the input io-device
40668
- * @param {string} stateId - The state id on that device
40669
- * @param {*} value - The new state value
40670
- * @example
40671
- * centralPlant.triggerState('pump-push-button-01', 'power', true)
40672
- */
40673
- }, {
40674
- key: "triggerState",
40675
- value: function triggerState(attachmentId, stateId, value, parentUuid) {
40676
- var _this$sceneViewer9;
40677
- var bm = (_this$sceneViewer9 = this.sceneViewer) === null || _this$sceneViewer9 === void 0 || (_this$sceneViewer9 = _this$sceneViewer9.managers) === null || _this$sceneViewer9 === void 0 ? void 0 : _this$sceneViewer9.behaviorManager;
40678
- if (!bm) {
40679
- console.warn('⚠️ triggerState(): BehaviorManager not available');
40680
- return;
40681
- }
40682
- bm.triggerState(attachmentId, stateId, value, parentUuid);
40683
- }
40684
-
40685
40587
  /**
40686
40588
  * Set the state of an I/O device instance in the Three.js scene.
40687
40589
  *
@@ -40714,15 +40616,9 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
40714
40616
  }, {
40715
40617
  key: "setIoDeviceState",
40716
40618
  value: function setIoDeviceState(attachmentId, stateId, value, parentUuid) {
40717
- var _this$sceneViewer0, _this$sceneViewer1, _this$sceneViewer10;
40718
- var bm = (_this$sceneViewer0 = this.sceneViewer) === null || _this$sceneViewer0 === void 0 || (_this$sceneViewer0 = _this$sceneViewer0.managers) === null || _this$sceneViewer0 === void 0 ? void 0 : _this$sceneViewer0.behaviorManager;
40719
- if (!bm) {
40720
- console.warn('⚠️ setIoDeviceState(): BehaviorManager not available');
40721
- return false;
40722
- }
40723
-
40619
+ var _this$sceneViewer4, _this$sceneViewer5, _this$sceneViewer6;
40724
40620
  // 1. Persist via state adapter if one has been configured
40725
- var stateAdapter = (_this$sceneViewer1 = this.sceneViewer) === null || _this$sceneViewer1 === void 0 || (_this$sceneViewer1 = _this$sceneViewer1.managers) === null || _this$sceneViewer1 === void 0 || (_this$sceneViewer1 = _this$sceneViewer1.componentTooltipManager) === null || _this$sceneViewer1 === void 0 ? void 0 : _this$sceneViewer1._stateAdapter;
40621
+ var stateAdapter = (_this$sceneViewer4 = this.sceneViewer) === null || _this$sceneViewer4 === void 0 || (_this$sceneViewer4 = _this$sceneViewer4.managers) === null || _this$sceneViewer4 === void 0 || (_this$sceneViewer4 = _this$sceneViewer4.componentTooltipManager) === null || _this$sceneViewer4 === void 0 ? void 0 : _this$sceneViewer4._stateAdapter;
40726
40622
  if (stateAdapter !== null && stateAdapter !== void 0 && stateAdapter.setState) {
40727
40623
  var scopedKey = parentUuid ? "".concat(parentUuid, "::").concat(attachmentId) : attachmentId;
40728
40624
  try {
@@ -40732,11 +40628,11 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
40732
40628
  }
40733
40629
  }
40734
40630
 
40735
- // 2. Apply Three.js behavior changes
40736
- bm.triggerState(attachmentId, stateId, value, parentUuid);
40631
+ // 2. Apply io-animation changes
40632
+ (_this$sceneViewer5 = this.sceneViewer) === null || _this$sceneViewer5 === void 0 || (_this$sceneViewer5 = _this$sceneViewer5.managers) === null || _this$sceneViewer5 === void 0 || (_this$sceneViewer5 = _this$sceneViewer5.ioAnimationManager) === null || _this$sceneViewer5 === void 0 || _this$sceneViewer5.triggerState(attachmentId, stateId, value, parentUuid);
40737
40633
 
40738
40634
  // 3. Emit event for host apps that don't use the state adapter (e.g. cp3d-viewer)
40739
- (_this$sceneViewer10 = this.sceneViewer) === null || _this$sceneViewer10 === void 0 || _this$sceneViewer10.emit('io-device-state-changed', {
40635
+ (_this$sceneViewer6 = this.sceneViewer) === null || _this$sceneViewer6 === void 0 || _this$sceneViewer6.emit('io-device-state-changed', {
40740
40636
  attachmentId: attachmentId,
40741
40637
  stateId: stateId,
40742
40638
  value: value,
@@ -40758,8 +40654,8 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
40758
40654
  }, {
40759
40655
  key: "getSceneAttachments",
40760
40656
  value: function getSceneAttachments() {
40761
- var _this$sceneViewer11;
40762
- var scene = (_this$sceneViewer11 = this.sceneViewer) === null || _this$sceneViewer11 === void 0 ? void 0 : _this$sceneViewer11.scene;
40657
+ var _this$sceneViewer7;
40658
+ var scene = (_this$sceneViewer7 = this.sceneViewer) === null || _this$sceneViewer7 === void 0 ? void 0 : _this$sceneViewer7.scene;
40763
40659
  if (!scene) return [];
40764
40660
  var results = [];
40765
40661
  scene.traverse(function (obj) {
@@ -42564,7 +42460,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
42564
42460
  this.centralPlant.attachToComponent();
42565
42461
 
42566
42462
  // Sync our managers tracking object after attachment
42567
- managerKeys = ['threeJSResourceManager', 'performanceMonitorManager', 'settingsManager', 'sceneExportManager', 'componentManager', 'sceneInitializationManager', 'environmentManager', 'keyboardControlsManager', 'pathfindingManager', 'pathFlowManager', 'behaviorManager', 'ioAnimationManager', 'sceneOperationsManager', 'animationManager', 'cameraControlsManager', 'componentDragManager', 'tooltipsManager', 'componentTooltipManager']; // Populate our managers tracking object
42463
+ managerKeys = ['threeJSResourceManager', 'performanceMonitorManager', 'settingsManager', 'sceneExportManager', 'componentManager', 'sceneInitializationManager', 'environmentManager', 'keyboardControlsManager', 'pathfindingManager', 'pathFlowManager', 'ioAnimationManager', 'ioOutlineManager', 'sceneOperationsManager', 'animationManager', 'cameraControlsManager', 'componentDragManager', 'tooltipsManager', 'componentTooltipManager']; // Populate our managers tracking object
42568
42464
  managerKeys.forEach(function (key) {
42569
42465
  if (_this2[key]) {
42570
42466
  _this2.managers[key] = _this2[key];
@@ -42752,7 +42648,21 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
42752
42648
  }
42753
42649
 
42754
42650
  // Update camera aspect ratio
42755
- this.camera.aspect = width / height;
42651
+ if (this.camera.isPerspectiveCamera) {
42652
+ this.camera.aspect = width / height;
42653
+ } else if (this.camera.isOrthographicCamera) {
42654
+ var _this$camera$userData;
42655
+ var aspect = width / height;
42656
+ var extents = (_this$camera$userData = this.camera.userData) === null || _this$camera$userData === void 0 ? void 0 : _this$camera$userData._orthoHalfExtents;
42657
+ if (extents) {
42658
+ var frustumHalfH = Math.max(extents.halfH, extents.halfW / aspect);
42659
+ var frustumHalfW = frustumHalfH * aspect;
42660
+ this.camera.left = -frustumHalfW;
42661
+ this.camera.right = frustumHalfW;
42662
+ this.camera.top = frustumHalfH;
42663
+ this.camera.bottom = -frustumHalfH;
42664
+ }
42665
+ }
42756
42666
  this.camera.updateProjectionMatrix();
42757
42667
 
42758
42668
  // Update renderer size (updateStyle=true to sync canvas CSS)
@@ -42879,6 +42789,45 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
42879
42789
  if (_this4.componentTooltipManager) {
42880
42790
  _this4.componentTooltipManager.toggleIODeviceBinaryState(ioDeviceObject);
42881
42791
  }
42792
+ },
42793
+ onIODeviceDrag: function onIODeviceDrag(ioDeviceObject, signedDelta, isStart) {
42794
+ if (isStart) {
42795
+ var _ioDeviceObject$userD, _this4$managers$ioAni, _this4$managers, _this4$managers2;
42796
+ // Resolve parentUuid by walking up to the host component.
42797
+ // Use userData.originalUuid (the custom componentId) because that
42798
+ // is what IoAnimationManager uses as the map key — NOT obj.uuid.
42799
+ var parentUuid = null;
42800
+ var obj = ioDeviceObject.parent;
42801
+ while (obj) {
42802
+ var _obj$userData;
42803
+ if (((_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType) === 'component') {
42804
+ parentUuid = obj.userData.originalUuid || obj.uuid;
42805
+ break;
42806
+ }
42807
+ obj = obj.parent;
42808
+ }
42809
+ var attachmentId = (_ioDeviceObject$userD = ioDeviceObject.userData) === null || _ioDeviceObject$userD === void 0 ? void 0 : _ioDeviceObject$userD.attachmentId;
42810
+ // When animated meshes are available, outline ONLY them so their
42811
+ // silhouette is isolated and the outline ring is visible around
42812
+ // them specifically (not swallowed by the larger device body).
42813
+ // Fall back to the whole device group when none are registered.
42814
+ var animatedMeshes = attachmentId && parentUuid ? (_this4$managers$ioAni = (_this4$managers = _this4.managers) === null || _this4$managers === void 0 || (_this4$managers = _this4$managers.ioAnimationManager) === null || _this4$managers === void 0 ? void 0 : _this4$managers.getAnimatedMeshes(parentUuid, attachmentId)) !== null && _this4$managers$ioAni !== void 0 ? _this4$managers$ioAni : [] : [];
42815
+ var targets = animatedMeshes.length > 0 ? animatedMeshes : [ioDeviceObject];
42816
+ (_this4$managers2 = _this4.managers) === null || _this4$managers2 === void 0 || (_this4$managers2 = _this4$managers2.ioOutlineManager) === null || _this4$managers2 === void 0 || _this4$managers2.setTargets(targets);
42817
+ }
42818
+ if (!_this4.componentTooltipManager) return;
42819
+ if (isStart) {
42820
+ _this4.componentTooltipManager.startIODeviceDrag(ioDeviceObject);
42821
+ } else {
42822
+ _this4.componentTooltipManager.updateIODeviceDrag(signedDelta);
42823
+ }
42824
+ },
42825
+ onIODeviceDragEnd: function onIODeviceDragEnd(ioDeviceObject) {
42826
+ var _this4$managers3;
42827
+ (_this4$managers3 = _this4.managers) === null || _this4$managers3 === void 0 || (_this4$managers3 = _this4$managers3.ioOutlineManager) === null || _this4$managers3 === void 0 || _this4$managers3.setTargets([]);
42828
+ if (_this4.componentTooltipManager) {
42829
+ _this4.componentTooltipManager.endIODeviceDrag();
42830
+ }
42882
42831
  }
42883
42832
  });
42884
42833
 
@@ -48229,7 +48178,6 @@ var rendering3D = /*#__PURE__*/Object.freeze({
48229
48178
  });
48230
48179
 
48231
48180
  exports.AnimationManager = AnimationManager;
48232
- exports.BehaviorManager = BehaviorManager;
48233
48181
  exports.CACHE_EXPIRY = CACHE_EXPIRY;
48234
48182
  exports.CACHE_NAME_PREFIX = CACHE_NAME_PREFIX;
48235
48183
  exports.CacheManager = CacheManager;