@2112-lab/central-plant 0.1.76 → 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()
@@ -30938,7 +31333,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
30938
31333
  console.log("\uD83D\uDD0D ModelPreloader available:", !!modelPreloader);
30939
31334
  console.log("\uD83D\uDD0D ComponentDictionary available:", !!(modelPreloader !== null && modelPreloader !== void 0 && modelPreloader.componentDictionary));
30940
31335
  if (!(modelPreloader && modelPreloader.componentDictionary)) {
30941
- _context2.n = 13;
31336
+ _context2.n = 14;
30942
31337
  break;
30943
31338
  }
30944
31339
  console.log("\uD83D\uDCDA Available dictionary keys:", Object.keys(modelPreloader.componentDictionary));
@@ -30954,7 +31349,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
30954
31349
  });
30955
31350
  }
30956
31351
  if (!(componentData && componentData.modelKey)) {
30957
- _context2.n = 11;
31352
+ _context2.n = 12;
30958
31353
  break;
30959
31354
  }
30960
31355
  // Try to get cached model first
@@ -30994,7 +31389,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
30994
31389
  console.warn("\u26A0\uFE0F Failed to preload model ".concat(componentData.modelKey, ":"), _t2);
30995
31390
  case 8:
30996
31391
  if (!cachedModel) {
30997
- _context2.n = 9;
31392
+ _context2.n = 10;
30998
31393
  break;
30999
31394
  }
31000
31395
  this.dragData.previewObject = cachedModel.clone();
@@ -31005,6 +31400,14 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31005
31400
  // Store original colors BEFORE making transparent
31006
31401
  this._storeOriginalColors(this.dragData.previewObject);
31007
31402
 
31403
+ // For smart components, load and attach IO device models to the preview
31404
+ if (!(componentData.isSmart && componentData.attachedDevices)) {
31405
+ _context2.n = 9;
31406
+ break;
31407
+ }
31408
+ _context2.n = 9;
31409
+ return this._attachIODeviceModelsToPreview(this.dragData.previewObject, componentData, modelPreloader);
31410
+ case 9:
31008
31411
  // Make the preview semi-transparent
31009
31412
  this._setPreviewTransparency(this.dragData.previewObject, 0.5);
31010
31413
  this.dragData.previewObject.userData = {
@@ -31018,19 +31421,19 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31018
31421
  this.sceneViewer.scene.add(this.dragData.previewObject);
31019
31422
  console.log("\u2705 Created ".concat(componentData.isS3Component ? 'S3' : 'static', " GLB preview object for: ").concat(componentId));
31020
31423
  return _context2.a(2);
31021
- case 9:
31022
- console.warn("\u26A0\uFE0F Failed to load model for ".concat(componentId, ", will use fallback"));
31023
31424
  case 10:
31024
- _context2.n = 12;
31025
- break;
31425
+ console.warn("\u26A0\uFE0F Failed to load model for ".concat(componentId, ", will use fallback"));
31026
31426
  case 11:
31027
- console.warn("\u26A0\uFE0F No modelKey found for component ".concat(componentId));
31028
- case 12:
31029
- _context2.n = 14;
31427
+ _context2.n = 13;
31030
31428
  break;
31429
+ case 12:
31430
+ console.warn("\u26A0\uFE0F No modelKey found for component ".concat(componentId));
31031
31431
  case 13:
31032
- console.warn("\u26A0\uFE0F ModelPreloader or component dictionary not available");
31432
+ _context2.n = 15;
31433
+ break;
31033
31434
  case 14:
31435
+ console.warn("\u26A0\uFE0F ModelPreloader or component dictionary not available");
31436
+ case 15:
31034
31437
  // Fallback: Create a simple preview mesh if model not available
31035
31438
  geometry = new THREE__namespace.BoxGeometry(1, 1, 1);
31036
31439
  material = new THREE__namespace.MeshPhysicalMaterial({
@@ -31053,7 +31456,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31053
31456
  this.dragData.previewObject.position.set(1000, 1000, 1000);
31054
31457
  this.sceneViewer.scene.add(this.dragData.previewObject);
31055
31458
  console.log("\u26A0\uFE0F Created fallback wireframe preview for: ".concat(componentId));
31056
- case 15:
31459
+ case 16:
31057
31460
  return _context2.a(2);
31058
31461
  }
31059
31462
  }, _callee2, this, [[5, 7], [1, 3]]);
@@ -31063,6 +31466,111 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31063
31466
  }
31064
31467
  return _createPreviewObject;
31065
31468
  }()
31469
+ /**
31470
+ * Load and attach IO device models to a smart component preview
31471
+ * @param {THREE.Object3D} parentObject - The parent preview object
31472
+ * @param {Object} componentData - Component dictionary entry (must have attachedDevices)
31473
+ * @param {Object} modelPreloader - ModelPreloader instance
31474
+ * @private
31475
+ */
31476
+ )
31477
+ }, {
31478
+ key: "_attachIODeviceModelsToPreview",
31479
+ value: (function () {
31480
+ var _attachIODeviceModelsToPreview2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(parentObject, componentData, modelPreloader) {
31481
+ var _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _attachment$attachmen, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, _t3;
31482
+ return _regenerator().w(function (_context3) {
31483
+ while (1) switch (_context3.n) {
31484
+ case 0:
31485
+ if (componentData.attachedDevices) {
31486
+ _context3.n = 1;
31487
+ break;
31488
+ }
31489
+ return _context3.a(2);
31490
+ case 1:
31491
+ _i = 0, _Object$entries = Object.entries(componentData.attachedDevices);
31492
+ case 2:
31493
+ if (!(_i < _Object$entries.length)) {
31494
+ _context3.n = 12;
31495
+ break;
31496
+ }
31497
+ _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2), attachmentId = _Object$entries$_i[0], attachment = _Object$entries$_i[1];
31498
+ _context3.p = 3;
31499
+ deviceData = (_modelPreloader$compo = modelPreloader.componentDictionary) === null || _modelPreloader$compo === void 0 ? void 0 : _modelPreloader$compo[attachment.deviceId];
31500
+ if (!(!deviceData || !deviceData.modelKey)) {
31501
+ _context3.n = 4;
31502
+ break;
31503
+ }
31504
+ console.warn("\u26A0\uFE0F IO device ".concat(attachment.deviceId, " not found in dictionary for preview"));
31505
+ return _context3.a(3, 11);
31506
+ case 4:
31507
+ // Ensure device model is loaded
31508
+ cachedDevice = modelPreloader.getCachedModelWithDimensions(deviceData.modelKey, attachment.deviceId);
31509
+ if (cachedDevice) {
31510
+ _context3.n = 8;
31511
+ break;
31512
+ }
31513
+ if (!((_modelPreloader$loadi = modelPreloader.loadingPromises) !== null && _modelPreloader$loadi !== void 0 && _modelPreloader$loadi.has(deviceData.modelKey))) {
31514
+ _context3.n = 6;
31515
+ break;
31516
+ }
31517
+ _context3.n = 5;
31518
+ return modelPreloader.loadingPromises.get(deviceData.modelKey);
31519
+ case 5:
31520
+ _context3.n = 7;
31521
+ break;
31522
+ case 6:
31523
+ _context3.n = 7;
31524
+ return modelPreloader.preloadSingleModel(deviceData.modelKey);
31525
+ case 7:
31526
+ cachedDevice = modelPreloader.getCachedModelWithDimensions(deviceData.modelKey, attachment.deviceId);
31527
+ case 8:
31528
+ if (cachedDevice) {
31529
+ _context3.n = 9;
31530
+ break;
31531
+ }
31532
+ console.warn("\u26A0\uFE0F Could not load IO device model: ".concat(deviceData.modelKey));
31533
+ return _context3.a(3, 11);
31534
+ case 9:
31535
+ deviceModel = cachedDevice.clone();
31536
+ this._cloneMaterials(deviceModel);
31537
+ this._storeOriginalColors(deviceModel);
31538
+ deviceModel.userData = {
31539
+ objectType: 'io-device',
31540
+ deviceId: attachment.deviceId,
31541
+ attachmentId: attachmentId,
31542
+ attachmentLabel: attachment.attachmentLabel
31543
+ };
31544
+ if ((_attachment$attachmen = attachment.attachmentPoint) !== null && _attachment$attachmen !== void 0 && _attachment$attachmen.position) {
31545
+ pos = attachment.attachmentPoint.position;
31546
+ deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
31547
+ }
31548
+
31549
+ // IO device models use their natural (1:1) scale — the stored
31550
+ // attachmentPoint.scale value is for the connector marker sphere.
31551
+ deviceModel.scale.setScalar(1);
31552
+ parentObject.add(deviceModel);
31553
+ console.log("\u2705 Attached IO device preview: ".concat(attachment.attachmentLabel || attachment.deviceId));
31554
+ _context3.n = 11;
31555
+ break;
31556
+ case 10:
31557
+ _context3.p = 10;
31558
+ _t3 = _context3.v;
31559
+ console.warn("\u26A0\uFE0F Could not attach IO device model ".concat(attachment.deviceId, " to preview:"), _t3);
31560
+ case 11:
31561
+ _i++;
31562
+ _context3.n = 2;
31563
+ break;
31564
+ case 12:
31565
+ return _context3.a(2);
31566
+ }
31567
+ }, _callee3, this, [[3, 10]]);
31568
+ }));
31569
+ function _attachIODeviceModelsToPreview(_x5, _x6, _x7) {
31570
+ return _attachIODeviceModelsToPreview2.apply(this, arguments);
31571
+ }
31572
+ return _attachIODeviceModelsToPreview;
31573
+ }()
31066
31574
  /**
31067
31575
  * Clone all materials in an object hierarchy to avoid shared material issues
31068
31576
  * @param {THREE.Object3D} object - The object to clone materials for
@@ -31210,8 +31718,8 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31210
31718
  });
31211
31719
 
31212
31720
  // Check for overlaps
31213
- for (var _i = 0, _sceneMeshes = sceneMeshes; _i < _sceneMeshes.length; _i++) {
31214
- var mesh = _sceneMeshes[_i];
31721
+ for (var _i2 = 0, _sceneMeshes = sceneMeshes; _i2 < _sceneMeshes.length; _i2++) {
31722
+ var mesh = _sceneMeshes[_i2];
31215
31723
  var meshBBox = new THREE__namespace.Box3().setFromObject(mesh);
31216
31724
  if (previewBBox.intersectsBox(meshBBox)) {
31217
31725
  console.log('⚠️ ComponentDragManager: Overlap detected with:', mesh.userData.objectType || mesh.name || mesh.uuid);
@@ -31525,50 +32033,50 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
31525
32033
  var _this3 = this;
31526
32034
  if (!element || !componentId) return;
31527
32035
  var handleMouseDown = /*#__PURE__*/function () {
31528
- var _ref = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(event) {
31529
- return _regenerator().w(function (_context3) {
31530
- while (1) switch (_context3.n) {
32036
+ var _ref = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(event) {
32037
+ return _regenerator().w(function (_context4) {
32038
+ while (1) switch (_context4.n) {
31531
32039
  case 0:
31532
32040
  if (!(event.button !== 0)) {
31533
- _context3.n = 1;
32041
+ _context4.n = 1;
31534
32042
  break;
31535
32043
  }
31536
- return _context3.a(2);
32044
+ return _context4.a(2);
31537
32045
  case 1:
31538
32046
  // Only left mouse button
31539
32047
  event.preventDefault();
31540
- _context3.n = 2;
32048
+ _context4.n = 2;
31541
32049
  return _this3.startComponentDrag(componentId, element, event);
31542
32050
  case 2:
31543
- return _context3.a(2);
32051
+ return _context4.a(2);
31544
32052
  }
31545
- }, _callee3);
32053
+ }, _callee4);
31546
32054
  }));
31547
- return function handleMouseDown(_x5) {
32055
+ return function handleMouseDown(_x8) {
31548
32056
  return _ref.apply(this, arguments);
31549
32057
  };
31550
32058
  }();
31551
32059
  var handleTouchStart = /*#__PURE__*/function () {
31552
- var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(event) {
31553
- return _regenerator().w(function (_context4) {
31554
- while (1) switch (_context4.n) {
32060
+ var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(event) {
32061
+ return _regenerator().w(function (_context5) {
32062
+ while (1) switch (_context5.n) {
31555
32063
  case 0:
31556
32064
  if (!(event.touches.length !== 1)) {
31557
- _context4.n = 1;
32065
+ _context5.n = 1;
31558
32066
  break;
31559
32067
  }
31560
- return _context4.a(2);
32068
+ return _context5.a(2);
31561
32069
  case 1:
31562
32070
  // Only single touch
31563
32071
  event.preventDefault();
31564
- _context4.n = 2;
32072
+ _context5.n = 2;
31565
32073
  return _this3.startComponentDrag(componentId, element, event);
31566
32074
  case 2:
31567
- return _context4.a(2);
32075
+ return _context5.a(2);
31568
32076
  }
31569
- }, _callee4);
32077
+ }, _callee5);
31570
32078
  }));
31571
- return function handleTouchStart(_x6) {
32079
+ return function handleTouchStart(_x9) {
31572
32080
  return _ref2.apply(this, arguments);
31573
32081
  };
31574
32082
  }();
@@ -34710,6 +35218,11 @@ var CentralPlantInternals = /*#__PURE__*/function () {
34710
35218
  });
34711
35219
  }
34712
35220
 
35221
+ // Add attached IO device models for smart components
35222
+ if (componentData.isSmart && componentData.attachedDevices) {
35223
+ this._attachIODevicesToComponent(componentModel, componentData, modelPreloader, componentId);
35224
+ }
35225
+
34713
35226
  // Notify the component manager about the new component
34714
35227
  if (componentManager.registerComponent) {
34715
35228
  componentManager.registerComponent(componentModel);
@@ -34760,6 +35273,74 @@ var CentralPlantInternals = /*#__PURE__*/function () {
34760
35273
  }
34761
35274
  }
34762
35275
 
35276
+ /**
35277
+ * Attach IO device models to a smart component from cached models.
35278
+ * Each device referenced in componentData.attachedDevices is looked up
35279
+ * in the model preloader cache, cloned, positioned, and added as a child.
35280
+ * @param {THREE.Object3D} componentModel - The parent component model
35281
+ * @param {Object} componentData - Component dictionary entry (has attachedDevices)
35282
+ * @param {Object} modelPreloader - ModelPreloader instance
35283
+ * @param {string} parentComponentId - The parent component's UUID
35284
+ * @private
35285
+ */
35286
+ }, {
35287
+ key: "_attachIODevicesToComponent",
35288
+ value: function _attachIODevicesToComponent(componentModel, componentData, modelPreloader, parentComponentId) {
35289
+ var attachedDevices = componentData.attachedDevices;
35290
+ console.log("\uD83D\uDD0C addComponent(): Attaching ".concat(Object.keys(attachedDevices).length, " IO devices to smart component"));
35291
+ for (var _i = 0, _Object$entries = Object.entries(attachedDevices); _i < _Object$entries.length; _i++) {
35292
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
35293
+ attachmentId = _Object$entries$_i[0],
35294
+ attachment = _Object$entries$_i[1];
35295
+ try {
35296
+ var _modelPreloader$compo, _attachment$attachmen;
35297
+ var deviceData = (_modelPreloader$compo = modelPreloader.componentDictionary) === null || _modelPreloader$compo === void 0 ? void 0 : _modelPreloader$compo[attachment.deviceId];
35298
+ if (!deviceData || !deviceData.modelKey) {
35299
+ console.warn("\u26A0\uFE0F IO device ".concat(attachment.deviceId, " not found in dictionary, skipping"));
35300
+ continue;
35301
+ }
35302
+ var deviceModel = modelPreloader.getCachedModelWithDimensions(deviceData.modelKey, attachment.deviceId);
35303
+ if (!deviceModel) {
35304
+ console.warn("\u26A0\uFE0F IO device model not in cache: ".concat(deviceData.modelKey, ", skipping"));
35305
+ continue;
35306
+ }
35307
+
35308
+ // Name the device model
35309
+ deviceModel.name = "".concat(attachment.attachmentLabel || 'IO Device', " (").concat(attachmentId, ")");
35310
+
35311
+ // Set user data for identification
35312
+ deviceModel.userData = {
35313
+ objectType: 'io-device',
35314
+ deviceId: attachment.deviceId,
35315
+ attachmentId: attachmentId,
35316
+ attachmentLabel: attachment.attachmentLabel,
35317
+ parentComponentId: parentComponentId
35318
+ };
35319
+
35320
+ // Position at the attachment point
35321
+ if ((_attachment$attachmen = attachment.attachmentPoint) !== null && _attachment$attachmen !== void 0 && _attachment$attachmen.position) {
35322
+ var pos = attachment.attachmentPoint.position;
35323
+ deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
35324
+ }
35325
+
35326
+ // IO device models are authored at the same real-world unit scale
35327
+ // as the host component, so keep them at their natural (1:1) size.
35328
+ // Note: attachmentPoint.scale is the connector marker sphere size,
35329
+ // NOT a desired device model scale.
35330
+ deviceModel.scale.setScalar(1);
35331
+
35332
+ // Add as child of the component
35333
+ componentModel.add(deviceModel);
35334
+ console.log("\u2705 Attached IO device: ".concat(attachment.attachmentLabel || attachment.deviceId, " at"), {
35335
+ position: deviceModel.position,
35336
+ scale: deviceModel.scale
35337
+ });
35338
+ } catch (err) {
35339
+ console.error("\u274C Error attaching IO device ".concat(attachment.deviceId, ":"), err);
35340
+ }
35341
+ }
35342
+ }
35343
+
34763
35344
  /**
34764
35345
  * Delete a component from the scene by componentId (internal implementation)
34765
35346
  * @param {string} componentId - The UUID of the component to delete
@@ -34865,7 +35446,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
34865
35446
  * Initialize the CentralPlant manager
34866
35447
  *
34867
35448
  * @constructor
34868
- * @version 0.1.76
35449
+ * @version 0.1.78
34869
35450
  * @updated 2025-10-22
34870
35451
  *
34871
35452
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.