@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.
@@ -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()
@@ -0,0 +1,356 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js');
6
+ var THREE = require('three');
7
+
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n["default"] = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
27
+
28
+ /**
29
+ * Creates a wireframe box helper (LineSegments) from a Box3, visually identical
30
+ * to THREE.BoxHelper but driven by an explicit Box3 instead of setFromObject().
31
+ *
32
+ * @param {THREE.Box3} box3 - The bounding box to visualize
33
+ * @param {number} color - Line color (hex)
34
+ * @returns {THREE.LineSegments} A wireframe box matching BoxHelper's visual style
35
+ * @private
36
+ */
37
+ function _createBoxHelperFromBox3(box3, color) {
38
+ 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]);
39
+ var positions = new Float32Array(8 * 3);
40
+ var geometry = new THREE__namespace.BufferGeometry();
41
+ geometry.setIndex(new THREE__namespace.BufferAttribute(indices, 1));
42
+ geometry.setAttribute('position', new THREE__namespace.BufferAttribute(positions, 3));
43
+ var helper = new THREE__namespace.LineSegments(geometry, new THREE__namespace.LineBasicMaterial({
44
+ color: color,
45
+ toneMapped: false
46
+ }));
47
+ helper.matrixAutoUpdate = false;
48
+
49
+ // Populate positions from box3
50
+ _updateBoxHelperPositions(helper, box3);
51
+ return helper;
52
+ }
53
+
54
+ /**
55
+ * Updates a box helper's geometry positions from a Box3.
56
+ * Matches the vertex layout used by THREE.BoxHelper.
57
+ *
58
+ * @param {THREE.LineSegments} helper - The wireframe helper to update
59
+ * @param {THREE.Box3} box3 - The bounding box
60
+ * @private
61
+ */
62
+ function _updateBoxHelperPositions(helper, box3) {
63
+ if (box3.isEmpty()) return;
64
+ var min = box3.min;
65
+ var max = box3.max;
66
+ var position = helper.geometry.attributes.position;
67
+ var array = position.array;
68
+
69
+ // Same vertex layout as THREE.BoxHelper
70
+ array[0] = max.x;
71
+ array[1] = max.y;
72
+ array[2] = max.z;
73
+ array[3] = min.x;
74
+ array[4] = max.y;
75
+ array[5] = max.z;
76
+ array[6] = min.x;
77
+ array[7] = min.y;
78
+ array[8] = max.z;
79
+ array[9] = max.x;
80
+ array[10] = min.y;
81
+ array[11] = max.z;
82
+ array[12] = max.x;
83
+ array[13] = max.y;
84
+ array[14] = min.z;
85
+ array[15] = min.x;
86
+ array[16] = max.y;
87
+ array[17] = min.z;
88
+ array[18] = min.x;
89
+ array[19] = min.y;
90
+ array[20] = min.z;
91
+ array[21] = max.x;
92
+ array[22] = min.y;
93
+ array[23] = min.z;
94
+ position.needsUpdate = true;
95
+ helper.geometry.computeBoundingSphere();
96
+ }
97
+
98
+ /**
99
+ * Computes a bounding box for a Three.js object, excluding descendant meshes
100
+ * that belong to subtrees rooted at objects with excluded objectTypes.
101
+ *
102
+ * This mirrors what THREE.Box3.expandByObject() does internally, but with a
103
+ * filter predicate that skips meshes whose ancestry (up to the root object)
104
+ * includes any excluded objectType.
105
+ *
106
+ * @param {THREE.Object3D} object - The root object to compute bbox for
107
+ * @param {string[]} excludeTypes - userData.objectType values to exclude (e.g., ['io-device', 'connector'])
108
+ * @returns {THREE.Box3} The filtered bounding box in world space
109
+ *
110
+ * @example
111
+ * // Compute bbox for pump body only, excluding io-devices and connectors
112
+ * const pumpBodyBBox = computeFilteredBoundingBox(pumpModel, ['io-device', 'connector'])
113
+ */
114
+ function computeFilteredBoundingBox(object) {
115
+ var excludeTypes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
116
+ var box = new THREE__namespace.Box3();
117
+ var tempBox = new THREE__namespace.Box3();
118
+ var hasGeometry = false;
119
+
120
+ // Build a Set for O(1) lookups
121
+ var excludeSet = new Set(excludeTypes);
122
+ object.updateWorldMatrix(false, true);
123
+ object.traverse(function (child) {
124
+ // Only process nodes with geometry (Mesh, SkinnedMesh, etc.)
125
+ if (!child.geometry) return;
126
+
127
+ // Walk up the ancestry from child to root object, checking for excluded types.
128
+ // If any ancestor (excluding the root object itself) has an excluded objectType,
129
+ // this mesh belongs to an excluded subtree — skip it.
130
+ var ancestor = child;
131
+ while (ancestor && ancestor !== object) {
132
+ var _ancestor$userData;
133
+ if ((_ancestor$userData = ancestor.userData) !== null && _ancestor$userData !== void 0 && _ancestor$userData.objectType && excludeSet.has(ancestor.userData.objectType)) {
134
+ return; // Skip — this mesh belongs to an excluded subtree
135
+ }
136
+ ancestor = ancestor.parent;
137
+ }
138
+
139
+ // Include this mesh's geometry in the bounding box
140
+ child.geometry.computeBoundingBox();
141
+ if (child.geometry.boundingBox) {
142
+ tempBox.copy(child.geometry.boundingBox);
143
+ tempBox.applyMatrix4(child.matrixWorld);
144
+ if (!hasGeometry) {
145
+ box.copy(tempBox);
146
+ hasGeometry = true;
147
+ } else {
148
+ box.union(tempBox);
149
+ }
150
+ }
151
+ });
152
+ if (!hasGeometry) {
153
+ // Fallback: return a zero-size box at the object's world position
154
+ var position = new THREE__namespace.Vector3();
155
+ object.getWorldPosition(position);
156
+ box.setFromCenterAndSize(position, new THREE__namespace.Vector3(0, 0, 0));
157
+ console.warn("[boundingBoxUtils] computeFilteredBoundingBox: No geometry found for ".concat(object.uuid, ", returning empty box"));
158
+ }
159
+ return box;
160
+ }
161
+
162
+ /**
163
+ * Computes individual world-space bounding boxes for each io-device child
164
+ * of a component. Uses standard THREE.Box3.setFromObject() on each io-device
165
+ * since io-devices don't have their own sub-devices that need filtering.
166
+ *
167
+ * @param {THREE.Object3D} componentObject - The component's Three.js object
168
+ * @returns {Array<{uuid: string, userData: Object, worldBoundingBox: {min: number[], max: number[]}}>}
169
+ * Array of io-device bounding box descriptors ready for injection into scene data
170
+ *
171
+ * @example
172
+ * const ioDeviceBBoxes = computeIODeviceBoundingBoxes(pumpModel)
173
+ * // Returns: [{ uuid: 'signal-light-1', userData: {...}, worldBoundingBox: { min: [...], max: [...] } }]
174
+ */
175
+ function computeIODeviceBoundingBoxes(componentObject) {
176
+ var results = [];
177
+ var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(componentObject.children),
178
+ _step;
179
+ try {
180
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
181
+ var _child$userData;
182
+ var child = _step.value;
183
+ if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) !== 'io-device') continue;
184
+ var bbox = new THREE__namespace.Box3().setFromObject(child);
185
+ if (!bbox.isEmpty()) {
186
+ results.push({
187
+ uuid: child.uuid,
188
+ userData: {
189
+ objectType: 'io-device',
190
+ deviceId: child.userData.deviceId || null,
191
+ attachmentId: child.userData.attachmentId || null,
192
+ parentComponentId: child.userData.parentComponentId || componentObject.uuid
193
+ },
194
+ worldBoundingBox: {
195
+ min: [bbox.min.x, bbox.min.y, bbox.min.z],
196
+ max: [bbox.max.x, bbox.max.y, bbox.max.z]
197
+ }
198
+ });
199
+ }
200
+ }
201
+ } catch (err) {
202
+ _iterator.e(err);
203
+ } finally {
204
+ _iterator.f();
205
+ }
206
+ return results;
207
+ }
208
+
209
+ /**
210
+ * Creates bounding box helpers for a selected object. For smart components
211
+ * (components with io-device children), this produces:
212
+ * - One filtered helper for the component body (excluding io-devices and connectors)
213
+ * - One helper per io-device child
214
+ *
215
+ * For non-smart components and other objects, produces a single standard BoxHelper.
216
+ *
217
+ * Each helper is tagged with metadata in userData for update tracking:
218
+ * - `isBoundingBox: true`
219
+ * - `sourceObjectUuid: string` — the object this helper represents
220
+ * - `isFiltered: boolean` — whether filtered computation was used
221
+ * - `excludeTypes: string[]` — types excluded (for recomputation)
222
+ *
223
+ * @param {THREE.Object3D} object - The selected scene object
224
+ * @param {number} color - Line color (hex), default green
225
+ * @returns {THREE.LineSegments[]} Array of box helpers to add to the scene
226
+ *
227
+ * @example
228
+ * const helpers = createSelectionBoxHelpers(pumpModel, 0x00ff00)
229
+ * helpers.forEach(h => scene.add(h))
230
+ */
231
+ function createSelectionBoxHelpers(object) {
232
+ var _object$children;
233
+ var color = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0x00ff00;
234
+ var helpers = [];
235
+ var excludeTypes = ['io-device', 'connector'];
236
+
237
+ // Check if this object has io-device children (smart component)
238
+ var hasIODevices = (_object$children = object.children) === null || _object$children === void 0 ? void 0 : _object$children.some(function (child) {
239
+ var _child$userData2;
240
+ return ((_child$userData2 = child.userData) === null || _child$userData2 === void 0 ? void 0 : _child$userData2.objectType) === 'io-device';
241
+ });
242
+ if (hasIODevices) {
243
+ // 1. Create filtered helper for the component body
244
+ var filteredBox = computeFilteredBoundingBox(object, excludeTypes);
245
+ var componentHelper = _createBoxHelperFromBox3(filteredBox, color);
246
+ componentHelper.isHelper = true;
247
+ componentHelper.userData = {
248
+ isBoundingBox: true,
249
+ sourceObjectUuid: object.uuid,
250
+ isFiltered: true,
251
+ excludeTypes: excludeTypes
252
+ };
253
+ helpers.push(componentHelper);
254
+
255
+ // 2. Create individual helpers for each io-device
256
+ var _iterator2 = _rollupPluginBabelHelpers.createForOfIteratorHelper(object.children),
257
+ _step2;
258
+ try {
259
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
260
+ var _child$userData3;
261
+ var child = _step2.value;
262
+ if (((_child$userData3 = child.userData) === null || _child$userData3 === void 0 ? void 0 : _child$userData3.objectType) !== 'io-device') continue;
263
+ var deviceBox = new THREE__namespace.Box3().setFromObject(child);
264
+ if (deviceBox.isEmpty()) continue;
265
+ var deviceHelper = _createBoxHelperFromBox3(deviceBox, color);
266
+ deviceHelper.isHelper = true;
267
+ deviceHelper.userData = {
268
+ isBoundingBox: true,
269
+ sourceObjectUuid: child.uuid,
270
+ isFiltered: false,
271
+ isIODevice: true,
272
+ parentComponentUuid: object.uuid
273
+ };
274
+ helpers.push(deviceHelper);
275
+ }
276
+ } catch (err) {
277
+ _iterator2.e(err);
278
+ } finally {
279
+ _iterator2.f();
280
+ }
281
+ } else {
282
+ // Standard BoxHelper for non-smart objects
283
+ var boxHelper = new THREE__namespace.BoxHelper(object, color);
284
+ boxHelper.isHelper = true;
285
+ boxHelper.userData = {
286
+ isBoundingBox: true,
287
+ sourceObjectUuid: object.uuid,
288
+ isFiltered: false
289
+ };
290
+ helpers.push(boxHelper);
291
+ }
292
+ return helpers;
293
+ }
294
+
295
+ /**
296
+ * Updates a set of bounding box helpers to reflect current object transforms.
297
+ * Handles both standard BoxHelpers and filtered/io-device helpers.
298
+ *
299
+ * @param {THREE.LineSegments[]} helpers - Array of box helpers
300
+ * @param {THREE.Object3D[]} selectedObjects - The selected scene objects
301
+ * @param {THREE.Scene} scene - The scene (for finding objects by uuid)
302
+ */
303
+ function updateSelectionBoxHelpers(helpers, selectedObjects, scene) {
304
+ var _iterator3 = _rollupPluginBabelHelpers.createForOfIteratorHelper(helpers),
305
+ _step3;
306
+ try {
307
+ var _loop = function _loop() {
308
+ var helper = _step3.value;
309
+ var _helper$userData = helper.userData,
310
+ sourceObjectUuid = _helper$userData.sourceObjectUuid,
311
+ isFiltered = _helper$userData.isFiltered,
312
+ excludeTypes = _helper$userData.excludeTypes,
313
+ isIODevice = _helper$userData.isIODevice,
314
+ parentComponentUuid = _helper$userData.parentComponentUuid;
315
+ var sourceObject;
316
+ if (isIODevice && parentComponentUuid) {
317
+ var _parent$children;
318
+ // Find the parent component first, then the io-device child
319
+ var parent = scene.getObjectByProperty('uuid', parentComponentUuid);
320
+ sourceObject = parent === null || parent === void 0 || (_parent$children = parent.children) === null || _parent$children === void 0 ? void 0 : _parent$children.find(function (c) {
321
+ return c.uuid === sourceObjectUuid;
322
+ });
323
+ } else {
324
+ sourceObject = selectedObjects.find(function (obj) {
325
+ return obj.uuid === sourceObjectUuid;
326
+ }) || scene.getObjectByProperty('uuid', sourceObjectUuid);
327
+ }
328
+ if (!sourceObject) return 1; // continue
329
+ sourceObject.updateMatrixWorld(true);
330
+ if (isFiltered && excludeTypes) {
331
+ // Recompute filtered bbox
332
+ var box = computeFilteredBoundingBox(sourceObject, excludeTypes);
333
+ _updateBoxHelperPositions(helper, box);
334
+ } else if (isIODevice) {
335
+ // Recompute io-device bbox
336
+ var _box = new THREE__namespace.Box3().setFromObject(sourceObject);
337
+ _updateBoxHelperPositions(helper, _box);
338
+ } else if (helper.update) {
339
+ // Standard BoxHelper — use built-in update
340
+ helper.update();
341
+ }
342
+ };
343
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
344
+ if (_loop()) continue;
345
+ }
346
+ } catch (err) {
347
+ _iterator3.e(err);
348
+ } finally {
349
+ _iterator3.f();
350
+ }
351
+ }
352
+
353
+ exports.computeFilteredBoundingBox = computeFilteredBoundingBox;
354
+ exports.computeIODeviceBoundingBoxes = computeIODeviceBoundingBoxes;
355
+ exports.createSelectionBoxHelpers = createSelectionBoxHelpers;
356
+ exports.updateSelectionBoxHelpers = updateSelectionBoxHelpers;
@@ -15,7 +15,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
15
15
  * Initialize the CentralPlant manager
16
16
  *
17
17
  * @constructor
18
- * @version 0.1.77
18
+ * @version 0.1.78
19
19
  * @updated 2025-10-22
20
20
  *
21
21
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -2,6 +2,7 @@ import { createClass as _createClass, asyncToGenerator as _asyncToGenerator, reg
2
2
  import * as THREE from 'three';
3
3
  import { transformControls } from './transformControls.js';
4
4
  import { isSegment, isGateway } from '../../utils/objectTypes.js';
5
+ import { createSelectionBoxHelpers, updateSelectionBoxHelpers } from '../../utils/boundingBoxUtils.js';
5
6
 
6
7
  var TransformControlsManager = /*#__PURE__*/function () {
7
8
  function TransformControlsManager(scene, camera, renderer) {
@@ -918,23 +919,16 @@ var TransformControlsManager = /*#__PURE__*/function () {
918
919
  return;
919
920
  }
920
921
  try {
921
- // Create individual bounding boxes for each selected object
922
+ // Create bounding box helpers for each selected object
923
+ // Smart components get filtered helpers (component body + individual io-device boxes)
922
924
  this.selectedObjects.forEach(function (obj) {
923
- var boundingBoxHelper = new THREE.BoxHelper(obj, _this5.config.boundingBoxColor);
924
-
925
- // Mark it as a helper to avoid selection
926
- boundingBoxHelper.isHelper = true;
927
- boundingBoxHelper.userData = {
928
- isBoundingBox: true
929
- };
930
-
931
- // Add to scene
932
- _this5.scene.add(boundingBoxHelper);
933
-
934
- // Store in array for later cleanup
935
- _this5.boundingBoxHelpers.push(boundingBoxHelper);
925
+ var helpers = createSelectionBoxHelpers(obj, _this5.config.boundingBoxColor);
926
+ helpers.forEach(function (helper) {
927
+ _this5.scene.add(helper);
928
+ _this5.boundingBoxHelpers.push(helper);
929
+ });
936
930
  });
937
- console.log("\uD83D\uDCE6 Bounding boxes created for ".concat(this.selectedObjects.length, " object(s)"));
931
+ console.log("\uD83D\uDCE6 Bounding boxes created for ".concat(this.selectedObjects.length, " object(s) (").concat(this.boundingBoxHelpers.length, " helpers)"));
938
932
  } catch (error) {
939
933
  console.warn('⚠️ Failed to create bounding boxes:', error);
940
934
  }
@@ -1065,21 +1059,19 @@ var TransformControlsManager = /*#__PURE__*/function () {
1065
1059
  // Update bounding boxes for all selected objects
1066
1060
  if (this.selectedObjects.length > 0 && this.boundingBoxHelpers.length > 0) {
1067
1061
  try {
1068
- // Update each bounding box helper
1069
- this.boundingBoxHelpers.forEach(function (helper, index) {
1070
- var obj = _this6.selectedObjects[index];
1071
- if (obj) {
1072
- // Force object matrix update to ensure correct bounding box
1073
- obj.updateMatrixWorld(true);
1074
-
1075
- // Update bounding box
1076
- helper.update();
1077
-
1078
- // Also update the cached bounding box if it exists
1079
- if (_this6.boundingBoxCache.has(obj)) {
1080
- var updatedBoundingBox = new THREE.Box3().setFromObject(obj);
1081
- _this6.boundingBoxCache.set(obj, updatedBoundingBox);
1082
- }
1062
+ // Ensure all selected objects have up-to-date matrices
1063
+ this.selectedObjects.forEach(function (obj) {
1064
+ return obj.updateMatrixWorld(true);
1065
+ });
1066
+
1067
+ // Use the centralized update function which handles filtered, io-device, and standard helpers
1068
+ updateSelectionBoxHelpers(this.boundingBoxHelpers, this.selectedObjects, this.scene);
1069
+
1070
+ // Also update the cached bounding box if it exists
1071
+ this.selectedObjects.forEach(function (obj) {
1072
+ if (_this6.boundingBoxCache.has(obj)) {
1073
+ var updatedBoundingBox = new THREE.Box3().setFromObject(obj);
1074
+ _this6.boundingBoxCache.set(obj, updatedBoundingBox);
1083
1075
  }
1084
1076
  });
1085
1077
  } catch (error) {
@@ -3,6 +3,7 @@ import * as THREE from 'three';
3
3
  import { Pathfinder } from '@2112-lab/pathfinder';
4
4
  import { BaseDisposable } from '../../core/baseDisposable.js';
5
5
  import { PathData } from '../../core/pathfindingData.js';
6
+ import { computeFilteredBoundingBox, computeIODeviceBoundingBoxes } from '../../utils/boundingBoxUtils.js';
6
7
  import { SceneDataManager } from './sceneDataManager.js';
7
8
  import { PathRenderingManager } from './PathRenderingManager.js';
8
9
  import { ConnectorManager } from './ConnectorManager.js';
@@ -153,20 +154,63 @@ var PathfindingManager = /*#__PURE__*/function (_BaseDisposable) {
153
154
  // Find the actual component object in the scene
154
155
  var componentObject = _this3.sceneViewer.scene.getObjectByProperty('uuid', child.uuid);
155
156
  if (componentObject) {
156
- // Compute world bounding box
157
- var _worldBBox = new THREE.Box3().setFromObject(componentObject);
158
- 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), "]"));
157
+ // Compute FILTERED bounding box — excludes io-device and connector subtrees
158
+ // so the component body bbox is tight-fitting and doesn't envelop attached devices
159
+ var filteredBBox = computeFilteredBoundingBox(componentObject, ['io-device', 'connector']);
160
+ 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), "]"));
159
161
 
160
- // Return enriched component data with worldBoundingBox in userData
161
- // Note: pathfinder expects arrays [x, y, z] format for min/max
162
- return _objectSpread2(_objectSpread2({}, child), {}, {
162
+ // Build the enriched component entry
163
+ var enrichedChild = _objectSpread2(_objectSpread2({}, child), {}, {
163
164
  userData: _objectSpread2(_objectSpread2({}, child.userData), {}, {
164
165
  worldBoundingBox: {
165
- min: [_worldBBox.min.x, _worldBBox.min.y, _worldBBox.min.z],
166
- max: [_worldBBox.max.x, _worldBBox.max.y, _worldBBox.max.z]
166
+ min: [filteredBBox.min.x, filteredBBox.min.y, filteredBBox.min.z],
167
+ max: [filteredBBox.max.x, filteredBBox.max.y, filteredBBox.max.z]
167
168
  }
168
169
  })
169
170
  });
171
+
172
+ // Compute separate bounding boxes for each attached io-device
173
+ // These are injected as children so the pathfinder treats each as an independent obstacle
174
+ var ioDeviceBBoxes = computeIODeviceBoundingBoxes(componentObject);
175
+ if (ioDeviceBBoxes.length > 0) {
176
+ // Ensure children array exists (may already contain connectors)
177
+ if (!enrichedChild.children) {
178
+ enrichedChild.children = [];
179
+ }
180
+
181
+ // Inject io-device entries with their own worldBoundingBox
182
+ ioDeviceBBoxes.forEach(function (deviceBBox) {
183
+ // Check if this io-device already exists in scene data children
184
+ var existingIndex = enrichedChild.children.findIndex(function (c) {
185
+ return c.uuid === deviceBBox.uuid;
186
+ });
187
+ if (existingIndex >= 0) {
188
+ // Update existing entry with bounding box
189
+ enrichedChild.children[existingIndex] = _objectSpread2(_objectSpread2({}, enrichedChild.children[existingIndex]), {}, {
190
+ userData: _objectSpread2(_objectSpread2({}, enrichedChild.children[existingIndex].userData), {}, {
191
+ objectType: 'io-device',
192
+ worldBoundingBox: deviceBBox.worldBoundingBox
193
+ })
194
+ });
195
+ } else {
196
+ // Create new entry for the io-device
197
+ enrichedChild.children.push({
198
+ uuid: deviceBBox.uuid,
199
+ userData: _objectSpread2(_objectSpread2({}, deviceBBox.userData), {}, {
200
+ worldBoundingBox: deviceBBox.worldBoundingBox
201
+ }),
202
+ children: []
203
+ });
204
+ }
205
+ console.log("\uD83D\uDCE6 Injected io-device bbox for ".concat(deviceBBox.uuid, ": min=[").concat(deviceBBox.worldBoundingBox.min.map(function (v) {
206
+ return v.toFixed(2);
207
+ }).join(', '), "], max=[").concat(deviceBBox.worldBoundingBox.max.map(function (v) {
208
+ return v.toFixed(2);
209
+ }).join(', '), "]"));
210
+ });
211
+ console.log("\uD83D\uDCE6 Injected ".concat(ioDeviceBBoxes.length, " io-device bounding box(es) for component ").concat(child.uuid));
212
+ }
213
+ return enrichedChild;
170
214
  } else {
171
215
  console.warn("\u26A0\uFE0F Could not find component object in scene: ".concat(child.uuid));
172
216
  }