@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.
@@ -19,7 +19,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
19
19
  * Initialize the CentralPlant manager
20
20
  *
21
21
  * @constructor
22
- * @version 0.1.76
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.
@@ -1055,6 +1055,11 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1055
1055
  });
1056
1056
  }
1057
1057
 
1058
+ // Add attached IO device models for smart components
1059
+ if (componentData.isSmart && componentData.attachedDevices) {
1060
+ this._attachIODevicesToComponent(componentModel, componentData, modelPreloader, componentId);
1061
+ }
1062
+
1058
1063
  // Notify the component manager about the new component
1059
1064
  if (componentManager.registerComponent) {
1060
1065
  componentManager.registerComponent(componentModel);
@@ -1105,6 +1110,74 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1105
1110
  }
1106
1111
  }
1107
1112
 
1113
+ /**
1114
+ * Attach IO device models to a smart component from cached models.
1115
+ * Each device referenced in componentData.attachedDevices is looked up
1116
+ * in the model preloader cache, cloned, positioned, and added as a child.
1117
+ * @param {THREE.Object3D} componentModel - The parent component model
1118
+ * @param {Object} componentData - Component dictionary entry (has attachedDevices)
1119
+ * @param {Object} modelPreloader - ModelPreloader instance
1120
+ * @param {string} parentComponentId - The parent component's UUID
1121
+ * @private
1122
+ */
1123
+ }, {
1124
+ key: "_attachIODevicesToComponent",
1125
+ value: function _attachIODevicesToComponent(componentModel, componentData, modelPreloader, parentComponentId) {
1126
+ var attachedDevices = componentData.attachedDevices;
1127
+ console.log("\uD83D\uDD0C addComponent(): Attaching ".concat(Object.keys(attachedDevices).length, " IO devices to smart component"));
1128
+ for (var _i = 0, _Object$entries = Object.entries(attachedDevices); _i < _Object$entries.length; _i++) {
1129
+ var _Object$entries$_i = _rollupPluginBabelHelpers.slicedToArray(_Object$entries[_i], 2),
1130
+ attachmentId = _Object$entries$_i[0],
1131
+ attachment = _Object$entries$_i[1];
1132
+ try {
1133
+ var _modelPreloader$compo, _attachment$attachmen;
1134
+ var deviceData = (_modelPreloader$compo = modelPreloader.componentDictionary) === null || _modelPreloader$compo === void 0 ? void 0 : _modelPreloader$compo[attachment.deviceId];
1135
+ if (!deviceData || !deviceData.modelKey) {
1136
+ console.warn("\u26A0\uFE0F IO device ".concat(attachment.deviceId, " not found in dictionary, skipping"));
1137
+ continue;
1138
+ }
1139
+ var deviceModel = modelPreloader.getCachedModelWithDimensions(deviceData.modelKey, attachment.deviceId);
1140
+ if (!deviceModel) {
1141
+ console.warn("\u26A0\uFE0F IO device model not in cache: ".concat(deviceData.modelKey, ", skipping"));
1142
+ continue;
1143
+ }
1144
+
1145
+ // Name the device model
1146
+ deviceModel.name = "".concat(attachment.attachmentLabel || 'IO Device', " (").concat(attachmentId, ")");
1147
+
1148
+ // Set user data for identification
1149
+ deviceModel.userData = {
1150
+ objectType: 'io-device',
1151
+ deviceId: attachment.deviceId,
1152
+ attachmentId: attachmentId,
1153
+ attachmentLabel: attachment.attachmentLabel,
1154
+ parentComponentId: parentComponentId
1155
+ };
1156
+
1157
+ // Position at the attachment point
1158
+ if ((_attachment$attachmen = attachment.attachmentPoint) !== null && _attachment$attachmen !== void 0 && _attachment$attachmen.position) {
1159
+ var pos = attachment.attachmentPoint.position;
1160
+ deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
1161
+ }
1162
+
1163
+ // IO device models are authored at the same real-world unit scale
1164
+ // as the host component, so keep them at their natural (1:1) size.
1165
+ // Note: attachmentPoint.scale is the connector marker sphere size,
1166
+ // NOT a desired device model scale.
1167
+ deviceModel.scale.setScalar(1);
1168
+
1169
+ // Add as child of the component
1170
+ componentModel.add(deviceModel);
1171
+ console.log("\u2705 Attached IO device: ".concat(attachment.attachmentLabel || attachment.deviceId, " at"), {
1172
+ position: deviceModel.position,
1173
+ scale: deviceModel.scale
1174
+ });
1175
+ } catch (err) {
1176
+ console.error("\u274C Error attaching IO device ".concat(attachment.deviceId, ":"), err);
1177
+ }
1178
+ }
1179
+ }
1180
+
1108
1181
  /**
1109
1182
  * Delete a component from the scene by componentId (internal implementation)
1110
1183
  * @param {string} componentId - The UUID of the component to delete
@@ -191,7 +191,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
191
191
  console.log("\uD83D\uDD0D ModelPreloader available:", !!modelPreloader);
192
192
  console.log("\uD83D\uDD0D ComponentDictionary available:", !!(modelPreloader !== null && modelPreloader !== void 0 && modelPreloader.componentDictionary));
193
193
  if (!(modelPreloader && modelPreloader.componentDictionary)) {
194
- _context2.n = 13;
194
+ _context2.n = 14;
195
195
  break;
196
196
  }
197
197
  console.log("\uD83D\uDCDA Available dictionary keys:", Object.keys(modelPreloader.componentDictionary));
@@ -207,7 +207,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
207
207
  });
208
208
  }
209
209
  if (!(componentData && componentData.modelKey)) {
210
- _context2.n = 11;
210
+ _context2.n = 12;
211
211
  break;
212
212
  }
213
213
  // Try to get cached model first
@@ -247,7 +247,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
247
247
  console.warn("\u26A0\uFE0F Failed to preload model ".concat(componentData.modelKey, ":"), _t2);
248
248
  case 8:
249
249
  if (!cachedModel) {
250
- _context2.n = 9;
250
+ _context2.n = 10;
251
251
  break;
252
252
  }
253
253
  this.dragData.previewObject = cachedModel.clone();
@@ -258,6 +258,14 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
258
258
  // Store original colors BEFORE making transparent
259
259
  this._storeOriginalColors(this.dragData.previewObject);
260
260
 
261
+ // For smart components, load and attach IO device models to the preview
262
+ if (!(componentData.isSmart && componentData.attachedDevices)) {
263
+ _context2.n = 9;
264
+ break;
265
+ }
266
+ _context2.n = 9;
267
+ return this._attachIODeviceModelsToPreview(this.dragData.previewObject, componentData, modelPreloader);
268
+ case 9:
261
269
  // Make the preview semi-transparent
262
270
  this._setPreviewTransparency(this.dragData.previewObject, 0.5);
263
271
  this.dragData.previewObject.userData = {
@@ -271,19 +279,19 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
271
279
  this.sceneViewer.scene.add(this.dragData.previewObject);
272
280
  console.log("\u2705 Created ".concat(componentData.isS3Component ? 'S3' : 'static', " GLB preview object for: ").concat(componentId));
273
281
  return _context2.a(2);
274
- case 9:
275
- console.warn("\u26A0\uFE0F Failed to load model for ".concat(componentId, ", will use fallback"));
276
282
  case 10:
277
- _context2.n = 12;
278
- break;
283
+ console.warn("\u26A0\uFE0F Failed to load model for ".concat(componentId, ", will use fallback"));
279
284
  case 11:
280
- console.warn("\u26A0\uFE0F No modelKey found for component ".concat(componentId));
281
- case 12:
282
- _context2.n = 14;
285
+ _context2.n = 13;
283
286
  break;
287
+ case 12:
288
+ console.warn("\u26A0\uFE0F No modelKey found for component ".concat(componentId));
284
289
  case 13:
285
- console.warn("\u26A0\uFE0F ModelPreloader or component dictionary not available");
290
+ _context2.n = 15;
291
+ break;
286
292
  case 14:
293
+ console.warn("\u26A0\uFE0F ModelPreloader or component dictionary not available");
294
+ case 15:
287
295
  // Fallback: Create a simple preview mesh if model not available
288
296
  geometry = new THREE__namespace.BoxGeometry(1, 1, 1);
289
297
  material = new THREE__namespace.MeshPhysicalMaterial({
@@ -306,7 +314,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
306
314
  this.dragData.previewObject.position.set(1000, 1000, 1000);
307
315
  this.sceneViewer.scene.add(this.dragData.previewObject);
308
316
  console.log("\u26A0\uFE0F Created fallback wireframe preview for: ".concat(componentId));
309
- case 15:
317
+ case 16:
310
318
  return _context2.a(2);
311
319
  }
312
320
  }, _callee2, this, [[5, 7], [1, 3]]);
@@ -316,6 +324,111 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
316
324
  }
317
325
  return _createPreviewObject;
318
326
  }()
327
+ /**
328
+ * Load and attach IO device models to a smart component preview
329
+ * @param {THREE.Object3D} parentObject - The parent preview object
330
+ * @param {Object} componentData - Component dictionary entry (must have attachedDevices)
331
+ * @param {Object} modelPreloader - ModelPreloader instance
332
+ * @private
333
+ */
334
+ )
335
+ }, {
336
+ key: "_attachIODeviceModelsToPreview",
337
+ value: (function () {
338
+ var _attachIODeviceModelsToPreview2 = _rollupPluginBabelHelpers.asyncToGenerator(/*#__PURE__*/_rollupPluginBabelHelpers.regenerator().m(function _callee3(parentObject, componentData, modelPreloader) {
339
+ var _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _attachment$attachmen, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, _t3;
340
+ return _rollupPluginBabelHelpers.regenerator().w(function (_context3) {
341
+ while (1) switch (_context3.n) {
342
+ case 0:
343
+ if (componentData.attachedDevices) {
344
+ _context3.n = 1;
345
+ break;
346
+ }
347
+ return _context3.a(2);
348
+ case 1:
349
+ _i = 0, _Object$entries = Object.entries(componentData.attachedDevices);
350
+ case 2:
351
+ if (!(_i < _Object$entries.length)) {
352
+ _context3.n = 12;
353
+ break;
354
+ }
355
+ _Object$entries$_i = _rollupPluginBabelHelpers.slicedToArray(_Object$entries[_i], 2), attachmentId = _Object$entries$_i[0], attachment = _Object$entries$_i[1];
356
+ _context3.p = 3;
357
+ deviceData = (_modelPreloader$compo = modelPreloader.componentDictionary) === null || _modelPreloader$compo === void 0 ? void 0 : _modelPreloader$compo[attachment.deviceId];
358
+ if (!(!deviceData || !deviceData.modelKey)) {
359
+ _context3.n = 4;
360
+ break;
361
+ }
362
+ console.warn("\u26A0\uFE0F IO device ".concat(attachment.deviceId, " not found in dictionary for preview"));
363
+ return _context3.a(3, 11);
364
+ case 4:
365
+ // Ensure device model is loaded
366
+ cachedDevice = modelPreloader.getCachedModelWithDimensions(deviceData.modelKey, attachment.deviceId);
367
+ if (cachedDevice) {
368
+ _context3.n = 8;
369
+ break;
370
+ }
371
+ if (!((_modelPreloader$loadi = modelPreloader.loadingPromises) !== null && _modelPreloader$loadi !== void 0 && _modelPreloader$loadi.has(deviceData.modelKey))) {
372
+ _context3.n = 6;
373
+ break;
374
+ }
375
+ _context3.n = 5;
376
+ return modelPreloader.loadingPromises.get(deviceData.modelKey);
377
+ case 5:
378
+ _context3.n = 7;
379
+ break;
380
+ case 6:
381
+ _context3.n = 7;
382
+ return modelPreloader.preloadSingleModel(deviceData.modelKey);
383
+ case 7:
384
+ cachedDevice = modelPreloader.getCachedModelWithDimensions(deviceData.modelKey, attachment.deviceId);
385
+ case 8:
386
+ if (cachedDevice) {
387
+ _context3.n = 9;
388
+ break;
389
+ }
390
+ console.warn("\u26A0\uFE0F Could not load IO device model: ".concat(deviceData.modelKey));
391
+ return _context3.a(3, 11);
392
+ case 9:
393
+ deviceModel = cachedDevice.clone();
394
+ this._cloneMaterials(deviceModel);
395
+ this._storeOriginalColors(deviceModel);
396
+ deviceModel.userData = {
397
+ objectType: 'io-device',
398
+ deviceId: attachment.deviceId,
399
+ attachmentId: attachmentId,
400
+ attachmentLabel: attachment.attachmentLabel
401
+ };
402
+ if ((_attachment$attachmen = attachment.attachmentPoint) !== null && _attachment$attachmen !== void 0 && _attachment$attachmen.position) {
403
+ pos = attachment.attachmentPoint.position;
404
+ deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
405
+ }
406
+
407
+ // IO device models use their natural (1:1) scale — the stored
408
+ // attachmentPoint.scale value is for the connector marker sphere.
409
+ deviceModel.scale.setScalar(1);
410
+ parentObject.add(deviceModel);
411
+ console.log("\u2705 Attached IO device preview: ".concat(attachment.attachmentLabel || attachment.deviceId));
412
+ _context3.n = 11;
413
+ break;
414
+ case 10:
415
+ _context3.p = 10;
416
+ _t3 = _context3.v;
417
+ console.warn("\u26A0\uFE0F Could not attach IO device model ".concat(attachment.deviceId, " to preview:"), _t3);
418
+ case 11:
419
+ _i++;
420
+ _context3.n = 2;
421
+ break;
422
+ case 12:
423
+ return _context3.a(2);
424
+ }
425
+ }, _callee3, this, [[3, 10]]);
426
+ }));
427
+ function _attachIODeviceModelsToPreview(_x5, _x6, _x7) {
428
+ return _attachIODeviceModelsToPreview2.apply(this, arguments);
429
+ }
430
+ return _attachIODeviceModelsToPreview;
431
+ }()
319
432
  /**
320
433
  * Clone all materials in an object hierarchy to avoid shared material issues
321
434
  * @param {THREE.Object3D} object - The object to clone materials for
@@ -463,8 +576,8 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
463
576
  });
464
577
 
465
578
  // Check for overlaps
466
- for (var _i = 0, _sceneMeshes = sceneMeshes; _i < _sceneMeshes.length; _i++) {
467
- var mesh = _sceneMeshes[_i];
579
+ for (var _i2 = 0, _sceneMeshes = sceneMeshes; _i2 < _sceneMeshes.length; _i2++) {
580
+ var mesh = _sceneMeshes[_i2];
468
581
  var meshBBox = new THREE__namespace.Box3().setFromObject(mesh);
469
582
  if (previewBBox.intersectsBox(meshBBox)) {
470
583
  console.log('⚠️ ComponentDragManager: Overlap detected with:', mesh.userData.objectType || mesh.name || mesh.uuid);
@@ -778,50 +891,50 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
778
891
  var _this3 = this;
779
892
  if (!element || !componentId) return;
780
893
  var handleMouseDown = /*#__PURE__*/function () {
781
- var _ref = _rollupPluginBabelHelpers.asyncToGenerator(/*#__PURE__*/_rollupPluginBabelHelpers.regenerator().m(function _callee3(event) {
782
- return _rollupPluginBabelHelpers.regenerator().w(function (_context3) {
783
- while (1) switch (_context3.n) {
894
+ var _ref = _rollupPluginBabelHelpers.asyncToGenerator(/*#__PURE__*/_rollupPluginBabelHelpers.regenerator().m(function _callee4(event) {
895
+ return _rollupPluginBabelHelpers.regenerator().w(function (_context4) {
896
+ while (1) switch (_context4.n) {
784
897
  case 0:
785
898
  if (!(event.button !== 0)) {
786
- _context3.n = 1;
899
+ _context4.n = 1;
787
900
  break;
788
901
  }
789
- return _context3.a(2);
902
+ return _context4.a(2);
790
903
  case 1:
791
904
  // Only left mouse button
792
905
  event.preventDefault();
793
- _context3.n = 2;
906
+ _context4.n = 2;
794
907
  return _this3.startComponentDrag(componentId, element, event);
795
908
  case 2:
796
- return _context3.a(2);
909
+ return _context4.a(2);
797
910
  }
798
- }, _callee3);
911
+ }, _callee4);
799
912
  }));
800
- return function handleMouseDown(_x5) {
913
+ return function handleMouseDown(_x8) {
801
914
  return _ref.apply(this, arguments);
802
915
  };
803
916
  }();
804
917
  var handleTouchStart = /*#__PURE__*/function () {
805
- var _ref2 = _rollupPluginBabelHelpers.asyncToGenerator(/*#__PURE__*/_rollupPluginBabelHelpers.regenerator().m(function _callee4(event) {
806
- return _rollupPluginBabelHelpers.regenerator().w(function (_context4) {
807
- while (1) switch (_context4.n) {
918
+ var _ref2 = _rollupPluginBabelHelpers.asyncToGenerator(/*#__PURE__*/_rollupPluginBabelHelpers.regenerator().m(function _callee5(event) {
919
+ return _rollupPluginBabelHelpers.regenerator().w(function (_context5) {
920
+ while (1) switch (_context5.n) {
808
921
  case 0:
809
922
  if (!(event.touches.length !== 1)) {
810
- _context4.n = 1;
923
+ _context5.n = 1;
811
924
  break;
812
925
  }
813
- return _context4.a(2);
926
+ return _context5.a(2);
814
927
  case 1:
815
928
  // Only single touch
816
929
  event.preventDefault();
817
- _context4.n = 2;
930
+ _context5.n = 2;
818
931
  return _this3.startComponentDrag(componentId, element, event);
819
932
  case 2:
820
- return _context4.a(2);
933
+ return _context5.a(2);
821
934
  }
822
- }, _callee4);
935
+ }, _callee5);
823
936
  }));
824
- return function handleTouchStart(_x6) {
937
+ return function handleTouchStart(_x9) {
825
938
  return _ref2.apply(this, arguments);
826
939
  };
827
940
  }();
@@ -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) {
@@ -7,6 +7,7 @@ var THREE = require('three');
7
7
  var pathfinder = require('@2112-lab/pathfinder');
8
8
  var baseDisposable = require('../../core/baseDisposable.js');
9
9
  var pathfindingData = require('../../core/pathfindingData.js');
10
+ var boundingBoxUtils = require('../../utils/boundingBoxUtils.js');
10
11
  var sceneDataManager = require('./sceneDataManager.js');
11
12
  var PathRenderingManager = require('./PathRenderingManager.js');
12
13
  var ConnectorManager = require('./ConnectorManager.js');
@@ -177,20 +178,63 @@ var PathfindingManager = /*#__PURE__*/function (_BaseDisposable) {
177
178
  // Find the actual component object in the scene
178
179
  var componentObject = _this3.sceneViewer.scene.getObjectByProperty('uuid', child.uuid);
179
180
  if (componentObject) {
180
- // Compute world bounding box
181
- var _worldBBox = new THREE__namespace.Box3().setFromObject(componentObject);
182
- 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), "]"));
181
+ // Compute FILTERED bounding box — excludes io-device and connector subtrees
182
+ // so the component body bbox is tight-fitting and doesn't envelop attached devices
183
+ var filteredBBox = boundingBoxUtils.computeFilteredBoundingBox(componentObject, ['io-device', 'connector']);
184
+ 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), "]"));
183
185
 
184
- // Return enriched component data with worldBoundingBox in userData
185
- // Note: pathfinder expects arrays [x, y, z] format for min/max
186
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, child), {}, {
186
+ // Build the enriched component entry
187
+ var enrichedChild = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, child), {}, {
187
188
  userData: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, child.userData), {}, {
188
189
  worldBoundingBox: {
189
- min: [_worldBBox.min.x, _worldBBox.min.y, _worldBBox.min.z],
190
- max: [_worldBBox.max.x, _worldBBox.max.y, _worldBBox.max.z]
190
+ min: [filteredBBox.min.x, filteredBBox.min.y, filteredBBox.min.z],
191
+ max: [filteredBBox.max.x, filteredBBox.max.y, filteredBBox.max.z]
191
192
  }
192
193
  })
193
194
  });
195
+
196
+ // Compute separate bounding boxes for each attached io-device
197
+ // These are injected as children so the pathfinder treats each as an independent obstacle
198
+ var ioDeviceBBoxes = boundingBoxUtils.computeIODeviceBoundingBoxes(componentObject);
199
+ if (ioDeviceBBoxes.length > 0) {
200
+ // Ensure children array exists (may already contain connectors)
201
+ if (!enrichedChild.children) {
202
+ enrichedChild.children = [];
203
+ }
204
+
205
+ // Inject io-device entries with their own worldBoundingBox
206
+ ioDeviceBBoxes.forEach(function (deviceBBox) {
207
+ // Check if this io-device already exists in scene data children
208
+ var existingIndex = enrichedChild.children.findIndex(function (c) {
209
+ return c.uuid === deviceBBox.uuid;
210
+ });
211
+ if (existingIndex >= 0) {
212
+ // Update existing entry with bounding box
213
+ enrichedChild.children[existingIndex] = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, enrichedChild.children[existingIndex]), {}, {
214
+ userData: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, enrichedChild.children[existingIndex].userData), {}, {
215
+ objectType: 'io-device',
216
+ worldBoundingBox: deviceBBox.worldBoundingBox
217
+ })
218
+ });
219
+ } else {
220
+ // Create new entry for the io-device
221
+ enrichedChild.children.push({
222
+ uuid: deviceBBox.uuid,
223
+ userData: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, deviceBBox.userData), {}, {
224
+ worldBoundingBox: deviceBBox.worldBoundingBox
225
+ }),
226
+ children: []
227
+ });
228
+ }
229
+ console.log("\uD83D\uDCE6 Injected io-device bbox for ".concat(deviceBBox.uuid, ": min=[").concat(deviceBBox.worldBoundingBox.min.map(function (v) {
230
+ return v.toFixed(2);
231
+ }).join(', '), "], max=[").concat(deviceBBox.worldBoundingBox.max.map(function (v) {
232
+ return v.toFixed(2);
233
+ }).join(', '), "]"));
234
+ });
235
+ console.log("\uD83D\uDCE6 Injected ".concat(ioDeviceBBoxes.length, " io-device bounding box(es) for component ").concat(child.uuid));
236
+ }
237
+ return enrichedChild;
194
238
  } else {
195
239
  console.warn("\u26A0\uFE0F Could not find component object in scene: ".concat(child.uuid));
196
240
  }
@@ -7,6 +7,7 @@ var THREE = require('three');
7
7
  var textureConfig = require('../environment/textureConfig.js');
8
8
  var modelManager = require('./modelManager.js');
9
9
  var sceneClearingUtility = require('../../utils/sceneClearingUtility.js');
10
+ var boundingBoxUtils = require('../../utils/boundingBoxUtils.js');
10
11
 
11
12
  function _interopNamespace(e) {
12
13
  if (e && e.__esModule) return e;
@@ -642,6 +643,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
642
643
 
643
644
  /**
644
645
  * Helper function to compute world bounding boxes
646
+ * For components: uses filtered bbox (excludes io-device and connector subtrees)
647
+ * For io-devices: computes separate bounding boxes and injects them as children
645
648
  */
646
649
  }, {
647
650
  key: "computeWorldBoundingBoxes",
@@ -680,12 +683,46 @@ var SceneOperationsManager = /*#__PURE__*/function () {
680
683
  };
681
684
  jsonObject = _findJsonObject(data.scene.children);
682
685
  if (jsonObject) {
683
- // Compute world bounding box
684
- var boundingBox = new THREE__namespace.Box3().setFromObject(object);
685
-
686
686
  // Store in JSON userData for pathfinder (skip for gateways - they're just routing points)
687
687
  if (!jsonObject.userData) jsonObject.userData = {};
688
- if (jsonObject.userData.objectType !== 'gateway') {
688
+ if (jsonObject.userData.objectType === 'component') {
689
+ // For components: compute filtered bounding box (excludes io-device and connector subtrees)
690
+ var filteredBBox = boundingBoxUtils.computeFilteredBoundingBox(object, ['io-device', 'connector']);
691
+ jsonObject.userData.worldBoundingBox = {
692
+ min: filteredBBox.min.toArray(),
693
+ max: filteredBBox.max.toArray()
694
+ };
695
+ console.log("Added filtered world bounding box for component:", jsonObject.userData.worldBoundingBox);
696
+
697
+ // Compute and inject separate io-device bounding boxes as children
698
+ var ioDeviceBBoxes = boundingBoxUtils.computeIODeviceBoundingBoxes(object);
699
+ if (ioDeviceBBoxes.length > 0) {
700
+ if (!jsonObject.children) jsonObject.children = [];
701
+ ioDeviceBBoxes.forEach(function (deviceBBox) {
702
+ var existingIndex = jsonObject.children.findIndex(function (c) {
703
+ return c.uuid === deviceBBox.uuid;
704
+ });
705
+ if (existingIndex >= 0) {
706
+ // Update existing entry
707
+ if (!jsonObject.children[existingIndex].userData) jsonObject.children[existingIndex].userData = {};
708
+ jsonObject.children[existingIndex].userData.objectType = 'io-device';
709
+ jsonObject.children[existingIndex].userData.worldBoundingBox = deviceBBox.worldBoundingBox;
710
+ } else {
711
+ // Create new entry
712
+ jsonObject.children.push({
713
+ uuid: deviceBBox.uuid,
714
+ userData: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, deviceBBox.userData), {}, {
715
+ worldBoundingBox: deviceBBox.worldBoundingBox
716
+ }),
717
+ children: []
718
+ });
719
+ }
720
+ });
721
+ console.log("\uD83D\uDCE6 Injected ".concat(ioDeviceBBoxes.length, " io-device bbox(es) for component ").concat(jsonObject.uuid));
722
+ }
723
+ } else if (jsonObject.userData.objectType !== 'gateway') {
724
+ // For non-component, non-gateway objects: standard bounding box
725
+ var boundingBox = new THREE__namespace.Box3().setFromObject(object);
689
726
  jsonObject.userData.worldBoundingBox = {
690
727
  min: boundingBox.min.toArray(),
691
728
  max: boundingBox.max.toArray()