@2112-lab/central-plant 0.1.77 → 0.1.78

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.
@@ -3403,6 +3403,331 @@ function getObjectsByType(scene, typePredicate) {
3403
3403
  return results;
3404
3404
  }
3405
3405
 
3406
+ /**
3407
+ * Creates a wireframe box helper (LineSegments) from a Box3, visually identical
3408
+ * to THREE.BoxHelper but driven by an explicit Box3 instead of setFromObject().
3409
+ *
3410
+ * @param {THREE.Box3} box3 - The bounding box to visualize
3411
+ * @param {number} color - Line color (hex)
3412
+ * @returns {THREE.LineSegments} A wireframe box matching BoxHelper's visual style
3413
+ * @private
3414
+ */
3415
+ function _createBoxHelperFromBox3(box3, color) {
3416
+ var indices = new Uint16Array([0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7]);
3417
+ var positions = new Float32Array(8 * 3);
3418
+ var geometry = new THREE__namespace.BufferGeometry();
3419
+ geometry.setIndex(new THREE__namespace.BufferAttribute(indices, 1));
3420
+ geometry.setAttribute('position', new THREE__namespace.BufferAttribute(positions, 3));
3421
+ var helper = new THREE__namespace.LineSegments(geometry, new THREE__namespace.LineBasicMaterial({
3422
+ color: color,
3423
+ toneMapped: false
3424
+ }));
3425
+ helper.matrixAutoUpdate = false;
3426
+
3427
+ // Populate positions from box3
3428
+ _updateBoxHelperPositions(helper, box3);
3429
+ return helper;
3430
+ }
3431
+
3432
+ /**
3433
+ * Updates a box helper's geometry positions from a Box3.
3434
+ * Matches the vertex layout used by THREE.BoxHelper.
3435
+ *
3436
+ * @param {THREE.LineSegments} helper - The wireframe helper to update
3437
+ * @param {THREE.Box3} box3 - The bounding box
3438
+ * @private
3439
+ */
3440
+ function _updateBoxHelperPositions(helper, box3) {
3441
+ if (box3.isEmpty()) return;
3442
+ var min = box3.min;
3443
+ var max = box3.max;
3444
+ var position = helper.geometry.attributes.position;
3445
+ var array = position.array;
3446
+
3447
+ // Same vertex layout as THREE.BoxHelper
3448
+ array[0] = max.x;
3449
+ array[1] = max.y;
3450
+ array[2] = max.z;
3451
+ array[3] = min.x;
3452
+ array[4] = max.y;
3453
+ array[5] = max.z;
3454
+ array[6] = min.x;
3455
+ array[7] = min.y;
3456
+ array[8] = max.z;
3457
+ array[9] = max.x;
3458
+ array[10] = min.y;
3459
+ array[11] = max.z;
3460
+ array[12] = max.x;
3461
+ array[13] = max.y;
3462
+ array[14] = min.z;
3463
+ array[15] = min.x;
3464
+ array[16] = max.y;
3465
+ array[17] = min.z;
3466
+ array[18] = min.x;
3467
+ array[19] = min.y;
3468
+ array[20] = min.z;
3469
+ array[21] = max.x;
3470
+ array[22] = min.y;
3471
+ array[23] = min.z;
3472
+ position.needsUpdate = true;
3473
+ helper.geometry.computeBoundingSphere();
3474
+ }
3475
+
3476
+ /**
3477
+ * Computes a bounding box for a Three.js object, excluding descendant meshes
3478
+ * that belong to subtrees rooted at objects with excluded objectTypes.
3479
+ *
3480
+ * This mirrors what THREE.Box3.expandByObject() does internally, but with a
3481
+ * filter predicate that skips meshes whose ancestry (up to the root object)
3482
+ * includes any excluded objectType.
3483
+ *
3484
+ * @param {THREE.Object3D} object - The root object to compute bbox for
3485
+ * @param {string[]} excludeTypes - userData.objectType values to exclude (e.g., ['io-device', 'connector'])
3486
+ * @returns {THREE.Box3} The filtered bounding box in world space
3487
+ *
3488
+ * @example
3489
+ * // Compute bbox for pump body only, excluding io-devices and connectors
3490
+ * const pumpBodyBBox = computeFilteredBoundingBox(pumpModel, ['io-device', 'connector'])
3491
+ */
3492
+ function computeFilteredBoundingBox(object) {
3493
+ var excludeTypes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
3494
+ var box = new THREE__namespace.Box3();
3495
+ var tempBox = new THREE__namespace.Box3();
3496
+ var hasGeometry = false;
3497
+
3498
+ // Build a Set for O(1) lookups
3499
+ var excludeSet = new Set(excludeTypes);
3500
+ object.updateWorldMatrix(false, true);
3501
+ object.traverse(function (child) {
3502
+ // Only process nodes with geometry (Mesh, SkinnedMesh, etc.)
3503
+ if (!child.geometry) return;
3504
+
3505
+ // Walk up the ancestry from child to root object, checking for excluded types.
3506
+ // If any ancestor (excluding the root object itself) has an excluded objectType,
3507
+ // this mesh belongs to an excluded subtree — skip it.
3508
+ var ancestor = child;
3509
+ while (ancestor && ancestor !== object) {
3510
+ var _ancestor$userData;
3511
+ if ((_ancestor$userData = ancestor.userData) !== null && _ancestor$userData !== void 0 && _ancestor$userData.objectType && excludeSet.has(ancestor.userData.objectType)) {
3512
+ return; // Skip — this mesh belongs to an excluded subtree
3513
+ }
3514
+ ancestor = ancestor.parent;
3515
+ }
3516
+
3517
+ // Include this mesh's geometry in the bounding box
3518
+ child.geometry.computeBoundingBox();
3519
+ if (child.geometry.boundingBox) {
3520
+ tempBox.copy(child.geometry.boundingBox);
3521
+ tempBox.applyMatrix4(child.matrixWorld);
3522
+ if (!hasGeometry) {
3523
+ box.copy(tempBox);
3524
+ hasGeometry = true;
3525
+ } else {
3526
+ box.union(tempBox);
3527
+ }
3528
+ }
3529
+ });
3530
+ if (!hasGeometry) {
3531
+ // Fallback: return a zero-size box at the object's world position
3532
+ var position = new THREE__namespace.Vector3();
3533
+ object.getWorldPosition(position);
3534
+ box.setFromCenterAndSize(position, new THREE__namespace.Vector3(0, 0, 0));
3535
+ console.warn("[boundingBoxUtils] computeFilteredBoundingBox: No geometry found for ".concat(object.uuid, ", returning empty box"));
3536
+ }
3537
+ return box;
3538
+ }
3539
+
3540
+ /**
3541
+ * Computes individual world-space bounding boxes for each io-device child
3542
+ * of a component. Uses standard THREE.Box3.setFromObject() on each io-device
3543
+ * since io-devices don't have their own sub-devices that need filtering.
3544
+ *
3545
+ * @param {THREE.Object3D} componentObject - The component's Three.js object
3546
+ * @returns {Array<{uuid: string, userData: Object, worldBoundingBox: {min: number[], max: number[]}}>}
3547
+ * Array of io-device bounding box descriptors ready for injection into scene data
3548
+ *
3549
+ * @example
3550
+ * const ioDeviceBBoxes = computeIODeviceBoundingBoxes(pumpModel)
3551
+ * // Returns: [{ uuid: 'signal-light-1', userData: {...}, worldBoundingBox: { min: [...], max: [...] } }]
3552
+ */
3553
+ function computeIODeviceBoundingBoxes(componentObject) {
3554
+ var results = [];
3555
+ var _iterator = _createForOfIteratorHelper(componentObject.children),
3556
+ _step;
3557
+ try {
3558
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
3559
+ var _child$userData;
3560
+ var child = _step.value;
3561
+ if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) !== 'io-device') continue;
3562
+ var bbox = new THREE__namespace.Box3().setFromObject(child);
3563
+ if (!bbox.isEmpty()) {
3564
+ results.push({
3565
+ uuid: child.uuid,
3566
+ userData: {
3567
+ objectType: 'io-device',
3568
+ deviceId: child.userData.deviceId || null,
3569
+ attachmentId: child.userData.attachmentId || null,
3570
+ parentComponentId: child.userData.parentComponentId || componentObject.uuid
3571
+ },
3572
+ worldBoundingBox: {
3573
+ min: [bbox.min.x, bbox.min.y, bbox.min.z],
3574
+ max: [bbox.max.x, bbox.max.y, bbox.max.z]
3575
+ }
3576
+ });
3577
+ }
3578
+ }
3579
+ } catch (err) {
3580
+ _iterator.e(err);
3581
+ } finally {
3582
+ _iterator.f();
3583
+ }
3584
+ return results;
3585
+ }
3586
+
3587
+ /**
3588
+ * Creates bounding box helpers for a selected object. For smart components
3589
+ * (components with io-device children), this produces:
3590
+ * - One filtered helper for the component body (excluding io-devices and connectors)
3591
+ * - One helper per io-device child
3592
+ *
3593
+ * For non-smart components and other objects, produces a single standard BoxHelper.
3594
+ *
3595
+ * Each helper is tagged with metadata in userData for update tracking:
3596
+ * - `isBoundingBox: true`
3597
+ * - `sourceObjectUuid: string` — the object this helper represents
3598
+ * - `isFiltered: boolean` — whether filtered computation was used
3599
+ * - `excludeTypes: string[]` — types excluded (for recomputation)
3600
+ *
3601
+ * @param {THREE.Object3D} object - The selected scene object
3602
+ * @param {number} color - Line color (hex), default green
3603
+ * @returns {THREE.LineSegments[]} Array of box helpers to add to the scene
3604
+ *
3605
+ * @example
3606
+ * const helpers = createSelectionBoxHelpers(pumpModel, 0x00ff00)
3607
+ * helpers.forEach(h => scene.add(h))
3608
+ */
3609
+ function createSelectionBoxHelpers(object) {
3610
+ var _object$children;
3611
+ var color = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0x00ff00;
3612
+ var helpers = [];
3613
+ var excludeTypes = ['io-device', 'connector'];
3614
+
3615
+ // Check if this object has io-device children (smart component)
3616
+ var hasIODevices = (_object$children = object.children) === null || _object$children === void 0 ? void 0 : _object$children.some(function (child) {
3617
+ var _child$userData2;
3618
+ return ((_child$userData2 = child.userData) === null || _child$userData2 === void 0 ? void 0 : _child$userData2.objectType) === 'io-device';
3619
+ });
3620
+ if (hasIODevices) {
3621
+ // 1. Create filtered helper for the component body
3622
+ var filteredBox = computeFilteredBoundingBox(object, excludeTypes);
3623
+ var componentHelper = _createBoxHelperFromBox3(filteredBox, color);
3624
+ componentHelper.isHelper = true;
3625
+ componentHelper.userData = {
3626
+ isBoundingBox: true,
3627
+ sourceObjectUuid: object.uuid,
3628
+ isFiltered: true,
3629
+ excludeTypes: excludeTypes
3630
+ };
3631
+ helpers.push(componentHelper);
3632
+
3633
+ // 2. Create individual helpers for each io-device
3634
+ var _iterator2 = _createForOfIteratorHelper(object.children),
3635
+ _step2;
3636
+ try {
3637
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
3638
+ var _child$userData3;
3639
+ var child = _step2.value;
3640
+ if (((_child$userData3 = child.userData) === null || _child$userData3 === void 0 ? void 0 : _child$userData3.objectType) !== 'io-device') continue;
3641
+ var deviceBox = new THREE__namespace.Box3().setFromObject(child);
3642
+ if (deviceBox.isEmpty()) continue;
3643
+ var deviceHelper = _createBoxHelperFromBox3(deviceBox, color);
3644
+ deviceHelper.isHelper = true;
3645
+ deviceHelper.userData = {
3646
+ isBoundingBox: true,
3647
+ sourceObjectUuid: child.uuid,
3648
+ isFiltered: false,
3649
+ isIODevice: true,
3650
+ parentComponentUuid: object.uuid
3651
+ };
3652
+ helpers.push(deviceHelper);
3653
+ }
3654
+ } catch (err) {
3655
+ _iterator2.e(err);
3656
+ } finally {
3657
+ _iterator2.f();
3658
+ }
3659
+ } else {
3660
+ // Standard BoxHelper for non-smart objects
3661
+ var boxHelper = new THREE__namespace.BoxHelper(object, color);
3662
+ boxHelper.isHelper = true;
3663
+ boxHelper.userData = {
3664
+ isBoundingBox: true,
3665
+ sourceObjectUuid: object.uuid,
3666
+ isFiltered: false
3667
+ };
3668
+ helpers.push(boxHelper);
3669
+ }
3670
+ return helpers;
3671
+ }
3672
+
3673
+ /**
3674
+ * Updates a set of bounding box helpers to reflect current object transforms.
3675
+ * Handles both standard BoxHelpers and filtered/io-device helpers.
3676
+ *
3677
+ * @param {THREE.LineSegments[]} helpers - Array of box helpers
3678
+ * @param {THREE.Object3D[]} selectedObjects - The selected scene objects
3679
+ * @param {THREE.Scene} scene - The scene (for finding objects by uuid)
3680
+ */
3681
+ function updateSelectionBoxHelpers(helpers, selectedObjects, scene) {
3682
+ var _iterator3 = _createForOfIteratorHelper(helpers),
3683
+ _step3;
3684
+ try {
3685
+ var _loop = function _loop() {
3686
+ var helper = _step3.value;
3687
+ var _helper$userData = helper.userData,
3688
+ sourceObjectUuid = _helper$userData.sourceObjectUuid,
3689
+ isFiltered = _helper$userData.isFiltered,
3690
+ excludeTypes = _helper$userData.excludeTypes,
3691
+ isIODevice = _helper$userData.isIODevice,
3692
+ parentComponentUuid = _helper$userData.parentComponentUuid;
3693
+ var sourceObject;
3694
+ if (isIODevice && parentComponentUuid) {
3695
+ var _parent$children;
3696
+ // Find the parent component first, then the io-device child
3697
+ var parent = scene.getObjectByProperty('uuid', parentComponentUuid);
3698
+ sourceObject = parent === null || parent === void 0 || (_parent$children = parent.children) === null || _parent$children === void 0 ? void 0 : _parent$children.find(function (c) {
3699
+ return c.uuid === sourceObjectUuid;
3700
+ });
3701
+ } else {
3702
+ sourceObject = selectedObjects.find(function (obj) {
3703
+ return obj.uuid === sourceObjectUuid;
3704
+ }) || scene.getObjectByProperty('uuid', sourceObjectUuid);
3705
+ }
3706
+ if (!sourceObject) return 1; // continue
3707
+ sourceObject.updateMatrixWorld(true);
3708
+ if (isFiltered && excludeTypes) {
3709
+ // Recompute filtered bbox
3710
+ var box = computeFilteredBoundingBox(sourceObject, excludeTypes);
3711
+ _updateBoxHelperPositions(helper, box);
3712
+ } else if (isIODevice) {
3713
+ // Recompute io-device bbox
3714
+ var _box = new THREE__namespace.Box3().setFromObject(sourceObject);
3715
+ _updateBoxHelperPositions(helper, _box);
3716
+ } else if (helper.update) {
3717
+ // Standard BoxHelper — use built-in update
3718
+ helper.update();
3719
+ }
3720
+ };
3721
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
3722
+ if (_loop()) continue;
3723
+ }
3724
+ } catch (err) {
3725
+ _iterator3.e(err);
3726
+ } finally {
3727
+ _iterator3.f();
3728
+ }
3729
+ }
3730
+
3406
3731
  var TransformControlsManager = /*#__PURE__*/function () {
3407
3732
  function TransformControlsManager(scene, camera, renderer) {
3408
3733
  var orbitControls = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
@@ -4318,23 +4643,16 @@ var TransformControlsManager = /*#__PURE__*/function () {
4318
4643
  return;
4319
4644
  }
4320
4645
  try {
4321
- // Create individual bounding boxes for each selected object
4646
+ // Create bounding box helpers for each selected object
4647
+ // Smart components get filtered helpers (component body + individual io-device boxes)
4322
4648
  this.selectedObjects.forEach(function (obj) {
4323
- var boundingBoxHelper = new THREE__namespace.BoxHelper(obj, _this5.config.boundingBoxColor);
4324
-
4325
- // Mark it as a helper to avoid selection
4326
- boundingBoxHelper.isHelper = true;
4327
- boundingBoxHelper.userData = {
4328
- isBoundingBox: true
4329
- };
4330
-
4331
- // Add to scene
4332
- _this5.scene.add(boundingBoxHelper);
4333
-
4334
- // Store in array for later cleanup
4335
- _this5.boundingBoxHelpers.push(boundingBoxHelper);
4649
+ var helpers = createSelectionBoxHelpers(obj, _this5.config.boundingBoxColor);
4650
+ helpers.forEach(function (helper) {
4651
+ _this5.scene.add(helper);
4652
+ _this5.boundingBoxHelpers.push(helper);
4653
+ });
4336
4654
  });
4337
- console.log("\uD83D\uDCE6 Bounding boxes created for ".concat(this.selectedObjects.length, " object(s)"));
4655
+ console.log("\uD83D\uDCE6 Bounding boxes created for ".concat(this.selectedObjects.length, " object(s) (").concat(this.boundingBoxHelpers.length, " helpers)"));
4338
4656
  } catch (error) {
4339
4657
  console.warn('⚠️ Failed to create bounding boxes:', error);
4340
4658
  }
@@ -4465,21 +4783,19 @@ var TransformControlsManager = /*#__PURE__*/function () {
4465
4783
  // Update bounding boxes for all selected objects
4466
4784
  if (this.selectedObjects.length > 0 && this.boundingBoxHelpers.length > 0) {
4467
4785
  try {
4468
- // Update each bounding box helper
4469
- this.boundingBoxHelpers.forEach(function (helper, index) {
4470
- var obj = _this6.selectedObjects[index];
4471
- if (obj) {
4472
- // Force object matrix update to ensure correct bounding box
4473
- obj.updateMatrixWorld(true);
4474
-
4475
- // Update bounding box
4476
- helper.update();
4477
-
4478
- // Also update the cached bounding box if it exists
4479
- if (_this6.boundingBoxCache.has(obj)) {
4480
- var updatedBoundingBox = new THREE__namespace.Box3().setFromObject(obj);
4481
- _this6.boundingBoxCache.set(obj, updatedBoundingBox);
4482
- }
4786
+ // Ensure all selected objects have up-to-date matrices
4787
+ this.selectedObjects.forEach(function (obj) {
4788
+ return obj.updateMatrixWorld(true);
4789
+ });
4790
+
4791
+ // Use the centralized update function which handles filtered, io-device, and standard helpers
4792
+ updateSelectionBoxHelpers(this.boundingBoxHelpers, this.selectedObjects, this.scene);
4793
+
4794
+ // Also update the cached bounding box if it exists
4795
+ this.selectedObjects.forEach(function (obj) {
4796
+ if (_this6.boundingBoxCache.has(obj)) {
4797
+ var updatedBoundingBox = new THREE__namespace.Box3().setFromObject(obj);
4798
+ _this6.boundingBoxCache.set(obj, updatedBoundingBox);
4483
4799
  }
4484
4800
  });
4485
4801
  } catch (error) {
@@ -27401,20 +27717,63 @@ var PathfindingManager = /*#__PURE__*/function (_BaseDisposable) {
27401
27717
  // Find the actual component object in the scene
27402
27718
  var componentObject = _this3.sceneViewer.scene.getObjectByProperty('uuid', child.uuid);
27403
27719
  if (componentObject) {
27404
- // Compute world bounding box
27405
- var _worldBBox = new THREE__namespace.Box3().setFromObject(componentObject);
27406
- console.log("\uD83D\uDD04 Updated worldBoundingBox for component ".concat(child.uuid, ": min=[").concat(_worldBBox.min.x.toFixed(2), ", ").concat(_worldBBox.min.y.toFixed(2), ", ").concat(_worldBBox.min.z.toFixed(2), "], max=[").concat(_worldBBox.max.x.toFixed(2), ", ").concat(_worldBBox.max.y.toFixed(2), ", ").concat(_worldBBox.max.z.toFixed(2), "]"));
27720
+ // Compute FILTERED bounding box — excludes io-device and connector subtrees
27721
+ // so the component body bbox is tight-fitting and doesn't envelop attached devices
27722
+ var filteredBBox = computeFilteredBoundingBox(componentObject, ['io-device', 'connector']);
27723
+ console.log("\uD83D\uDD04 Updated worldBoundingBox for component ".concat(child.uuid, " (filtered): min=[").concat(filteredBBox.min.x.toFixed(2), ", ").concat(filteredBBox.min.y.toFixed(2), ", ").concat(filteredBBox.min.z.toFixed(2), "], max=[").concat(filteredBBox.max.x.toFixed(2), ", ").concat(filteredBBox.max.y.toFixed(2), ", ").concat(filteredBBox.max.z.toFixed(2), "]"));
27407
27724
 
27408
- // Return enriched component data with worldBoundingBox in userData
27409
- // Note: pathfinder expects arrays [x, y, z] format for min/max
27410
- return _objectSpread2(_objectSpread2({}, child), {}, {
27725
+ // Build the enriched component entry
27726
+ var enrichedChild = _objectSpread2(_objectSpread2({}, child), {}, {
27411
27727
  userData: _objectSpread2(_objectSpread2({}, child.userData), {}, {
27412
27728
  worldBoundingBox: {
27413
- min: [_worldBBox.min.x, _worldBBox.min.y, _worldBBox.min.z],
27414
- max: [_worldBBox.max.x, _worldBBox.max.y, _worldBBox.max.z]
27729
+ min: [filteredBBox.min.x, filteredBBox.min.y, filteredBBox.min.z],
27730
+ max: [filteredBBox.max.x, filteredBBox.max.y, filteredBBox.max.z]
27415
27731
  }
27416
27732
  })
27417
27733
  });
27734
+
27735
+ // Compute separate bounding boxes for each attached io-device
27736
+ // These are injected as children so the pathfinder treats each as an independent obstacle
27737
+ var ioDeviceBBoxes = computeIODeviceBoundingBoxes(componentObject);
27738
+ if (ioDeviceBBoxes.length > 0) {
27739
+ // Ensure children array exists (may already contain connectors)
27740
+ if (!enrichedChild.children) {
27741
+ enrichedChild.children = [];
27742
+ }
27743
+
27744
+ // Inject io-device entries with their own worldBoundingBox
27745
+ ioDeviceBBoxes.forEach(function (deviceBBox) {
27746
+ // Check if this io-device already exists in scene data children
27747
+ var existingIndex = enrichedChild.children.findIndex(function (c) {
27748
+ return c.uuid === deviceBBox.uuid;
27749
+ });
27750
+ if (existingIndex >= 0) {
27751
+ // Update existing entry with bounding box
27752
+ enrichedChild.children[existingIndex] = _objectSpread2(_objectSpread2({}, enrichedChild.children[existingIndex]), {}, {
27753
+ userData: _objectSpread2(_objectSpread2({}, enrichedChild.children[existingIndex].userData), {}, {
27754
+ objectType: 'io-device',
27755
+ worldBoundingBox: deviceBBox.worldBoundingBox
27756
+ })
27757
+ });
27758
+ } else {
27759
+ // Create new entry for the io-device
27760
+ enrichedChild.children.push({
27761
+ uuid: deviceBBox.uuid,
27762
+ userData: _objectSpread2(_objectSpread2({}, deviceBBox.userData), {}, {
27763
+ worldBoundingBox: deviceBBox.worldBoundingBox
27764
+ }),
27765
+ children: []
27766
+ });
27767
+ }
27768
+ console.log("\uD83D\uDCE6 Injected io-device bbox for ".concat(deviceBBox.uuid, ": min=[").concat(deviceBBox.worldBoundingBox.min.map(function (v) {
27769
+ return v.toFixed(2);
27770
+ }).join(', '), "], max=[").concat(deviceBBox.worldBoundingBox.max.map(function (v) {
27771
+ return v.toFixed(2);
27772
+ }).join(', '), "]"));
27773
+ });
27774
+ console.log("\uD83D\uDCE6 Injected ".concat(ioDeviceBBoxes.length, " io-device bounding box(es) for component ").concat(child.uuid));
27775
+ }
27776
+ return enrichedChild;
27418
27777
  } else {
27419
27778
  console.warn("\u26A0\uFE0F Could not find component object in scene: ".concat(child.uuid));
27420
27779
  }
@@ -29477,6 +29836,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
29477
29836
 
29478
29837
  /**
29479
29838
  * Helper function to compute world bounding boxes
29839
+ * For components: uses filtered bbox (excludes io-device and connector subtrees)
29840
+ * For io-devices: computes separate bounding boxes and injects them as children
29480
29841
  */
29481
29842
  }, {
29482
29843
  key: "computeWorldBoundingBoxes",
@@ -29515,12 +29876,46 @@ var SceneOperationsManager = /*#__PURE__*/function () {
29515
29876
  };
29516
29877
  jsonObject = _findJsonObject(data.scene.children);
29517
29878
  if (jsonObject) {
29518
- // Compute world bounding box
29519
- var boundingBox = new THREE__namespace.Box3().setFromObject(object);
29520
-
29521
29879
  // Store in JSON userData for pathfinder (skip for gateways - they're just routing points)
29522
29880
  if (!jsonObject.userData) jsonObject.userData = {};
29523
- if (jsonObject.userData.objectType !== 'gateway') {
29881
+ if (jsonObject.userData.objectType === 'component') {
29882
+ // For components: compute filtered bounding box (excludes io-device and connector subtrees)
29883
+ var filteredBBox = computeFilteredBoundingBox(object, ['io-device', 'connector']);
29884
+ jsonObject.userData.worldBoundingBox = {
29885
+ min: filteredBBox.min.toArray(),
29886
+ max: filteredBBox.max.toArray()
29887
+ };
29888
+ console.log("Added filtered world bounding box for component:", jsonObject.userData.worldBoundingBox);
29889
+
29890
+ // Compute and inject separate io-device bounding boxes as children
29891
+ var ioDeviceBBoxes = computeIODeviceBoundingBoxes(object);
29892
+ if (ioDeviceBBoxes.length > 0) {
29893
+ if (!jsonObject.children) jsonObject.children = [];
29894
+ ioDeviceBBoxes.forEach(function (deviceBBox) {
29895
+ var existingIndex = jsonObject.children.findIndex(function (c) {
29896
+ return c.uuid === deviceBBox.uuid;
29897
+ });
29898
+ if (existingIndex >= 0) {
29899
+ // Update existing entry
29900
+ if (!jsonObject.children[existingIndex].userData) jsonObject.children[existingIndex].userData = {};
29901
+ jsonObject.children[existingIndex].userData.objectType = 'io-device';
29902
+ jsonObject.children[existingIndex].userData.worldBoundingBox = deviceBBox.worldBoundingBox;
29903
+ } else {
29904
+ // Create new entry
29905
+ jsonObject.children.push({
29906
+ uuid: deviceBBox.uuid,
29907
+ userData: _objectSpread2(_objectSpread2({}, deviceBBox.userData), {}, {
29908
+ worldBoundingBox: deviceBBox.worldBoundingBox
29909
+ }),
29910
+ children: []
29911
+ });
29912
+ }
29913
+ });
29914
+ console.log("\uD83D\uDCE6 Injected ".concat(ioDeviceBBoxes.length, " io-device bbox(es) for component ").concat(jsonObject.uuid));
29915
+ }
29916
+ } else if (jsonObject.userData.objectType !== 'gateway') {
29917
+ // For non-component, non-gateway objects: standard bounding box
29918
+ var boundingBox = new THREE__namespace.Box3().setFromObject(object);
29524
29919
  jsonObject.userData.worldBoundingBox = {
29525
29920
  min: boundingBox.min.toArray(),
29526
29921
  max: boundingBox.max.toArray()
@@ -35051,7 +35446,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
35051
35446
  * Initialize the CentralPlant manager
35052
35447
  *
35053
35448
  * @constructor
35054
- * @version 0.1.77
35449
+ * @version 0.1.78
35055
35450
  * @updated 2025-10-22
35056
35451
  *
35057
35452
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -19,7 +19,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
19
19
  * Initialize the CentralPlant manager
20
20
  *
21
21
  * @constructor
22
- * @version 0.1.77
22
+ * @version 0.1.78
23
23
  * @updated 2025-10-22
24
24
  *
25
25
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -6,6 +6,7 @@ var _rollupPluginBabelHelpers = require('../../../_virtual/_rollupPluginBabelHel
6
6
  var THREE = require('three');
7
7
  var transformControls = require('./transformControls.js');
8
8
  var objectTypes = require('../../utils/objectTypes.js');
9
+ var boundingBoxUtils = require('../../utils/boundingBoxUtils.js');
9
10
 
10
11
  function _interopNamespace(e) {
11
12
  if (e && e.__esModule) return e;
@@ -942,23 +943,16 @@ var TransformControlsManager = /*#__PURE__*/function () {
942
943
  return;
943
944
  }
944
945
  try {
945
- // Create individual bounding boxes for each selected object
946
+ // Create bounding box helpers for each selected object
947
+ // Smart components get filtered helpers (component body + individual io-device boxes)
946
948
  this.selectedObjects.forEach(function (obj) {
947
- var boundingBoxHelper = new THREE__namespace.BoxHelper(obj, _this5.config.boundingBoxColor);
948
-
949
- // Mark it as a helper to avoid selection
950
- boundingBoxHelper.isHelper = true;
951
- boundingBoxHelper.userData = {
952
- isBoundingBox: true
953
- };
954
-
955
- // Add to scene
956
- _this5.scene.add(boundingBoxHelper);
957
-
958
- // Store in array for later cleanup
959
- _this5.boundingBoxHelpers.push(boundingBoxHelper);
949
+ var helpers = boundingBoxUtils.createSelectionBoxHelpers(obj, _this5.config.boundingBoxColor);
950
+ helpers.forEach(function (helper) {
951
+ _this5.scene.add(helper);
952
+ _this5.boundingBoxHelpers.push(helper);
953
+ });
960
954
  });
961
- console.log("\uD83D\uDCE6 Bounding boxes created for ".concat(this.selectedObjects.length, " object(s)"));
955
+ console.log("\uD83D\uDCE6 Bounding boxes created for ".concat(this.selectedObjects.length, " object(s) (").concat(this.boundingBoxHelpers.length, " helpers)"));
962
956
  } catch (error) {
963
957
  console.warn('⚠️ Failed to create bounding boxes:', error);
964
958
  }
@@ -1089,21 +1083,19 @@ var TransformControlsManager = /*#__PURE__*/function () {
1089
1083
  // Update bounding boxes for all selected objects
1090
1084
  if (this.selectedObjects.length > 0 && this.boundingBoxHelpers.length > 0) {
1091
1085
  try {
1092
- // Update each bounding box helper
1093
- this.boundingBoxHelpers.forEach(function (helper, index) {
1094
- var obj = _this6.selectedObjects[index];
1095
- if (obj) {
1096
- // Force object matrix update to ensure correct bounding box
1097
- obj.updateMatrixWorld(true);
1098
-
1099
- // Update bounding box
1100
- helper.update();
1101
-
1102
- // Also update the cached bounding box if it exists
1103
- if (_this6.boundingBoxCache.has(obj)) {
1104
- var updatedBoundingBox = new THREE__namespace.Box3().setFromObject(obj);
1105
- _this6.boundingBoxCache.set(obj, updatedBoundingBox);
1106
- }
1086
+ // Ensure all selected objects have up-to-date matrices
1087
+ this.selectedObjects.forEach(function (obj) {
1088
+ return obj.updateMatrixWorld(true);
1089
+ });
1090
+
1091
+ // Use the centralized update function which handles filtered, io-device, and standard helpers
1092
+ boundingBoxUtils.updateSelectionBoxHelpers(this.boundingBoxHelpers, this.selectedObjects, this.scene);
1093
+
1094
+ // Also update the cached bounding box if it exists
1095
+ this.selectedObjects.forEach(function (obj) {
1096
+ if (_this6.boundingBoxCache.has(obj)) {
1097
+ var updatedBoundingBox = new THREE__namespace.Box3().setFromObject(obj);
1098
+ _this6.boundingBoxCache.set(obj, updatedBoundingBox);
1107
1099
  }
1108
1100
  });
1109
1101
  } catch (error) {