@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.
@@ -3,6 +3,7 @@ import * as THREE from 'three';
3
3
  import { loadTextureSet } from '../environment/textureConfig.js';
4
4
  import { ModelManager } from './modelManager.js';
5
5
  import { SceneClearingUtility } from '../../utils/sceneClearingUtility.js';
6
+ import { computeFilteredBoundingBox, computeIODeviceBoundingBoxes } from '../../utils/boundingBoxUtils.js';
6
7
 
7
8
  var _excluded = ["direction"],
8
9
  _excluded2 = ["direction"];
@@ -618,6 +619,8 @@ var SceneOperationsManager = /*#__PURE__*/function () {
618
619
 
619
620
  /**
620
621
  * Helper function to compute world bounding boxes
622
+ * For components: uses filtered bbox (excludes io-device and connector subtrees)
623
+ * For io-devices: computes separate bounding boxes and injects them as children
621
624
  */
622
625
  }, {
623
626
  key: "computeWorldBoundingBoxes",
@@ -656,12 +659,46 @@ var SceneOperationsManager = /*#__PURE__*/function () {
656
659
  };
657
660
  jsonObject = _findJsonObject(data.scene.children);
658
661
  if (jsonObject) {
659
- // Compute world bounding box
660
- var boundingBox = new THREE.Box3().setFromObject(object);
661
-
662
662
  // Store in JSON userData for pathfinder (skip for gateways - they're just routing points)
663
663
  if (!jsonObject.userData) jsonObject.userData = {};
664
- if (jsonObject.userData.objectType !== 'gateway') {
664
+ if (jsonObject.userData.objectType === 'component') {
665
+ // For components: compute filtered bounding box (excludes io-device and connector subtrees)
666
+ var filteredBBox = computeFilteredBoundingBox(object, ['io-device', 'connector']);
667
+ jsonObject.userData.worldBoundingBox = {
668
+ min: filteredBBox.min.toArray(),
669
+ max: filteredBBox.max.toArray()
670
+ };
671
+ console.log("Added filtered world bounding box for component:", jsonObject.userData.worldBoundingBox);
672
+
673
+ // Compute and inject separate io-device bounding boxes as children
674
+ var ioDeviceBBoxes = computeIODeviceBoundingBoxes(object);
675
+ if (ioDeviceBBoxes.length > 0) {
676
+ if (!jsonObject.children) jsonObject.children = [];
677
+ ioDeviceBBoxes.forEach(function (deviceBBox) {
678
+ var existingIndex = jsonObject.children.findIndex(function (c) {
679
+ return c.uuid === deviceBBox.uuid;
680
+ });
681
+ if (existingIndex >= 0) {
682
+ // Update existing entry
683
+ if (!jsonObject.children[existingIndex].userData) jsonObject.children[existingIndex].userData = {};
684
+ jsonObject.children[existingIndex].userData.objectType = 'io-device';
685
+ jsonObject.children[existingIndex].userData.worldBoundingBox = deviceBBox.worldBoundingBox;
686
+ } else {
687
+ // Create new entry
688
+ jsonObject.children.push({
689
+ uuid: deviceBBox.uuid,
690
+ userData: _objectSpread2(_objectSpread2({}, deviceBBox.userData), {}, {
691
+ worldBoundingBox: deviceBBox.worldBoundingBox
692
+ }),
693
+ children: []
694
+ });
695
+ }
696
+ });
697
+ console.log("\uD83D\uDCE6 Injected ".concat(ioDeviceBBoxes.length, " io-device bbox(es) for component ").concat(jsonObject.uuid));
698
+ }
699
+ } else if (jsonObject.userData.objectType !== 'gateway') {
700
+ // For non-component, non-gateway objects: standard bounding box
701
+ var boundingBox = new THREE.Box3().setFromObject(object);
665
702
  jsonObject.userData.worldBoundingBox = {
666
703
  min: boundingBox.min.toArray(),
667
704
  max: boundingBox.max.toArray()
@@ -0,0 +1,329 @@
1
+ import { createForOfIteratorHelper as _createForOfIteratorHelper } from '../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import * as THREE from 'three';
3
+
4
+ /**
5
+ * Creates a wireframe box helper (LineSegments) from a Box3, visually identical
6
+ * to THREE.BoxHelper but driven by an explicit Box3 instead of setFromObject().
7
+ *
8
+ * @param {THREE.Box3} box3 - The bounding box to visualize
9
+ * @param {number} color - Line color (hex)
10
+ * @returns {THREE.LineSegments} A wireframe box matching BoxHelper's visual style
11
+ * @private
12
+ */
13
+ function _createBoxHelperFromBox3(box3, color) {
14
+ 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]);
15
+ var positions = new Float32Array(8 * 3);
16
+ var geometry = new THREE.BufferGeometry();
17
+ geometry.setIndex(new THREE.BufferAttribute(indices, 1));
18
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
19
+ var helper = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({
20
+ color: color,
21
+ toneMapped: false
22
+ }));
23
+ helper.matrixAutoUpdate = false;
24
+
25
+ // Populate positions from box3
26
+ _updateBoxHelperPositions(helper, box3);
27
+ return helper;
28
+ }
29
+
30
+ /**
31
+ * Updates a box helper's geometry positions from a Box3.
32
+ * Matches the vertex layout used by THREE.BoxHelper.
33
+ *
34
+ * @param {THREE.LineSegments} helper - The wireframe helper to update
35
+ * @param {THREE.Box3} box3 - The bounding box
36
+ * @private
37
+ */
38
+ function _updateBoxHelperPositions(helper, box3) {
39
+ if (box3.isEmpty()) return;
40
+ var min = box3.min;
41
+ var max = box3.max;
42
+ var position = helper.geometry.attributes.position;
43
+ var array = position.array;
44
+
45
+ // Same vertex layout as THREE.BoxHelper
46
+ array[0] = max.x;
47
+ array[1] = max.y;
48
+ array[2] = max.z;
49
+ array[3] = min.x;
50
+ array[4] = max.y;
51
+ array[5] = max.z;
52
+ array[6] = min.x;
53
+ array[7] = min.y;
54
+ array[8] = max.z;
55
+ array[9] = max.x;
56
+ array[10] = min.y;
57
+ array[11] = max.z;
58
+ array[12] = max.x;
59
+ array[13] = max.y;
60
+ array[14] = min.z;
61
+ array[15] = min.x;
62
+ array[16] = max.y;
63
+ array[17] = min.z;
64
+ array[18] = min.x;
65
+ array[19] = min.y;
66
+ array[20] = min.z;
67
+ array[21] = max.x;
68
+ array[22] = min.y;
69
+ array[23] = min.z;
70
+ position.needsUpdate = true;
71
+ helper.geometry.computeBoundingSphere();
72
+ }
73
+
74
+ /**
75
+ * Computes a bounding box for a Three.js object, excluding descendant meshes
76
+ * that belong to subtrees rooted at objects with excluded objectTypes.
77
+ *
78
+ * This mirrors what THREE.Box3.expandByObject() does internally, but with a
79
+ * filter predicate that skips meshes whose ancestry (up to the root object)
80
+ * includes any excluded objectType.
81
+ *
82
+ * @param {THREE.Object3D} object - The root object to compute bbox for
83
+ * @param {string[]} excludeTypes - userData.objectType values to exclude (e.g., ['io-device', 'connector'])
84
+ * @returns {THREE.Box3} The filtered bounding box in world space
85
+ *
86
+ * @example
87
+ * // Compute bbox for pump body only, excluding io-devices and connectors
88
+ * const pumpBodyBBox = computeFilteredBoundingBox(pumpModel, ['io-device', 'connector'])
89
+ */
90
+ function computeFilteredBoundingBox(object) {
91
+ var excludeTypes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
92
+ var box = new THREE.Box3();
93
+ var tempBox = new THREE.Box3();
94
+ var hasGeometry = false;
95
+
96
+ // Build a Set for O(1) lookups
97
+ var excludeSet = new Set(excludeTypes);
98
+ object.updateWorldMatrix(false, true);
99
+ object.traverse(function (child) {
100
+ // Only process nodes with geometry (Mesh, SkinnedMesh, etc.)
101
+ if (!child.geometry) return;
102
+
103
+ // Walk up the ancestry from child to root object, checking for excluded types.
104
+ // If any ancestor (excluding the root object itself) has an excluded objectType,
105
+ // this mesh belongs to an excluded subtree — skip it.
106
+ var ancestor = child;
107
+ while (ancestor && ancestor !== object) {
108
+ var _ancestor$userData;
109
+ if ((_ancestor$userData = ancestor.userData) !== null && _ancestor$userData !== void 0 && _ancestor$userData.objectType && excludeSet.has(ancestor.userData.objectType)) {
110
+ return; // Skip — this mesh belongs to an excluded subtree
111
+ }
112
+ ancestor = ancestor.parent;
113
+ }
114
+
115
+ // Include this mesh's geometry in the bounding box
116
+ child.geometry.computeBoundingBox();
117
+ if (child.geometry.boundingBox) {
118
+ tempBox.copy(child.geometry.boundingBox);
119
+ tempBox.applyMatrix4(child.matrixWorld);
120
+ if (!hasGeometry) {
121
+ box.copy(tempBox);
122
+ hasGeometry = true;
123
+ } else {
124
+ box.union(tempBox);
125
+ }
126
+ }
127
+ });
128
+ if (!hasGeometry) {
129
+ // Fallback: return a zero-size box at the object's world position
130
+ var position = new THREE.Vector3();
131
+ object.getWorldPosition(position);
132
+ box.setFromCenterAndSize(position, new THREE.Vector3(0, 0, 0));
133
+ console.warn("[boundingBoxUtils] computeFilteredBoundingBox: No geometry found for ".concat(object.uuid, ", returning empty box"));
134
+ }
135
+ return box;
136
+ }
137
+
138
+ /**
139
+ * Computes individual world-space bounding boxes for each io-device child
140
+ * of a component. Uses standard THREE.Box3.setFromObject() on each io-device
141
+ * since io-devices don't have their own sub-devices that need filtering.
142
+ *
143
+ * @param {THREE.Object3D} componentObject - The component's Three.js object
144
+ * @returns {Array<{uuid: string, userData: Object, worldBoundingBox: {min: number[], max: number[]}}>}
145
+ * Array of io-device bounding box descriptors ready for injection into scene data
146
+ *
147
+ * @example
148
+ * const ioDeviceBBoxes = computeIODeviceBoundingBoxes(pumpModel)
149
+ * // Returns: [{ uuid: 'signal-light-1', userData: {...}, worldBoundingBox: { min: [...], max: [...] } }]
150
+ */
151
+ function computeIODeviceBoundingBoxes(componentObject) {
152
+ var results = [];
153
+ var _iterator = _createForOfIteratorHelper(componentObject.children),
154
+ _step;
155
+ try {
156
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
157
+ var _child$userData;
158
+ var child = _step.value;
159
+ if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) !== 'io-device') continue;
160
+ var bbox = new THREE.Box3().setFromObject(child);
161
+ if (!bbox.isEmpty()) {
162
+ results.push({
163
+ uuid: child.uuid,
164
+ userData: {
165
+ objectType: 'io-device',
166
+ deviceId: child.userData.deviceId || null,
167
+ attachmentId: child.userData.attachmentId || null,
168
+ parentComponentId: child.userData.parentComponentId || componentObject.uuid
169
+ },
170
+ worldBoundingBox: {
171
+ min: [bbox.min.x, bbox.min.y, bbox.min.z],
172
+ max: [bbox.max.x, bbox.max.y, bbox.max.z]
173
+ }
174
+ });
175
+ }
176
+ }
177
+ } catch (err) {
178
+ _iterator.e(err);
179
+ } finally {
180
+ _iterator.f();
181
+ }
182
+ return results;
183
+ }
184
+
185
+ /**
186
+ * Creates bounding box helpers for a selected object. For smart components
187
+ * (components with io-device children), this produces:
188
+ * - One filtered helper for the component body (excluding io-devices and connectors)
189
+ * - One helper per io-device child
190
+ *
191
+ * For non-smart components and other objects, produces a single standard BoxHelper.
192
+ *
193
+ * Each helper is tagged with metadata in userData for update tracking:
194
+ * - `isBoundingBox: true`
195
+ * - `sourceObjectUuid: string` — the object this helper represents
196
+ * - `isFiltered: boolean` — whether filtered computation was used
197
+ * - `excludeTypes: string[]` — types excluded (for recomputation)
198
+ *
199
+ * @param {THREE.Object3D} object - The selected scene object
200
+ * @param {number} color - Line color (hex), default green
201
+ * @returns {THREE.LineSegments[]} Array of box helpers to add to the scene
202
+ *
203
+ * @example
204
+ * const helpers = createSelectionBoxHelpers(pumpModel, 0x00ff00)
205
+ * helpers.forEach(h => scene.add(h))
206
+ */
207
+ function createSelectionBoxHelpers(object) {
208
+ var _object$children;
209
+ var color = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0x00ff00;
210
+ var helpers = [];
211
+ var excludeTypes = ['io-device', 'connector'];
212
+
213
+ // Check if this object has io-device children (smart component)
214
+ var hasIODevices = (_object$children = object.children) === null || _object$children === void 0 ? void 0 : _object$children.some(function (child) {
215
+ var _child$userData2;
216
+ return ((_child$userData2 = child.userData) === null || _child$userData2 === void 0 ? void 0 : _child$userData2.objectType) === 'io-device';
217
+ });
218
+ if (hasIODevices) {
219
+ // 1. Create filtered helper for the component body
220
+ var filteredBox = computeFilteredBoundingBox(object, excludeTypes);
221
+ var componentHelper = _createBoxHelperFromBox3(filteredBox, color);
222
+ componentHelper.isHelper = true;
223
+ componentHelper.userData = {
224
+ isBoundingBox: true,
225
+ sourceObjectUuid: object.uuid,
226
+ isFiltered: true,
227
+ excludeTypes: excludeTypes
228
+ };
229
+ helpers.push(componentHelper);
230
+
231
+ // 2. Create individual helpers for each io-device
232
+ var _iterator2 = _createForOfIteratorHelper(object.children),
233
+ _step2;
234
+ try {
235
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
236
+ var _child$userData3;
237
+ var child = _step2.value;
238
+ if (((_child$userData3 = child.userData) === null || _child$userData3 === void 0 ? void 0 : _child$userData3.objectType) !== 'io-device') continue;
239
+ var deviceBox = new THREE.Box3().setFromObject(child);
240
+ if (deviceBox.isEmpty()) continue;
241
+ var deviceHelper = _createBoxHelperFromBox3(deviceBox, color);
242
+ deviceHelper.isHelper = true;
243
+ deviceHelper.userData = {
244
+ isBoundingBox: true,
245
+ sourceObjectUuid: child.uuid,
246
+ isFiltered: false,
247
+ isIODevice: true,
248
+ parentComponentUuid: object.uuid
249
+ };
250
+ helpers.push(deviceHelper);
251
+ }
252
+ } catch (err) {
253
+ _iterator2.e(err);
254
+ } finally {
255
+ _iterator2.f();
256
+ }
257
+ } else {
258
+ // Standard BoxHelper for non-smart objects
259
+ var boxHelper = new THREE.BoxHelper(object, color);
260
+ boxHelper.isHelper = true;
261
+ boxHelper.userData = {
262
+ isBoundingBox: true,
263
+ sourceObjectUuid: object.uuid,
264
+ isFiltered: false
265
+ };
266
+ helpers.push(boxHelper);
267
+ }
268
+ return helpers;
269
+ }
270
+
271
+ /**
272
+ * Updates a set of bounding box helpers to reflect current object transforms.
273
+ * Handles both standard BoxHelpers and filtered/io-device helpers.
274
+ *
275
+ * @param {THREE.LineSegments[]} helpers - Array of box helpers
276
+ * @param {THREE.Object3D[]} selectedObjects - The selected scene objects
277
+ * @param {THREE.Scene} scene - The scene (for finding objects by uuid)
278
+ */
279
+ function updateSelectionBoxHelpers(helpers, selectedObjects, scene) {
280
+ var _iterator3 = _createForOfIteratorHelper(helpers),
281
+ _step3;
282
+ try {
283
+ var _loop = function _loop() {
284
+ var helper = _step3.value;
285
+ var _helper$userData = helper.userData,
286
+ sourceObjectUuid = _helper$userData.sourceObjectUuid,
287
+ isFiltered = _helper$userData.isFiltered,
288
+ excludeTypes = _helper$userData.excludeTypes,
289
+ isIODevice = _helper$userData.isIODevice,
290
+ parentComponentUuid = _helper$userData.parentComponentUuid;
291
+ var sourceObject;
292
+ if (isIODevice && parentComponentUuid) {
293
+ var _parent$children;
294
+ // Find the parent component first, then the io-device child
295
+ var parent = scene.getObjectByProperty('uuid', parentComponentUuid);
296
+ sourceObject = parent === null || parent === void 0 || (_parent$children = parent.children) === null || _parent$children === void 0 ? void 0 : _parent$children.find(function (c) {
297
+ return c.uuid === sourceObjectUuid;
298
+ });
299
+ } else {
300
+ sourceObject = selectedObjects.find(function (obj) {
301
+ return obj.uuid === sourceObjectUuid;
302
+ }) || scene.getObjectByProperty('uuid', sourceObjectUuid);
303
+ }
304
+ if (!sourceObject) return 1; // continue
305
+ sourceObject.updateMatrixWorld(true);
306
+ if (isFiltered && excludeTypes) {
307
+ // Recompute filtered bbox
308
+ var box = computeFilteredBoundingBox(sourceObject, excludeTypes);
309
+ _updateBoxHelperPositions(helper, box);
310
+ } else if (isIODevice) {
311
+ // Recompute io-device bbox
312
+ var _box = new THREE.Box3().setFromObject(sourceObject);
313
+ _updateBoxHelperPositions(helper, _box);
314
+ } else if (helper.update) {
315
+ // Standard BoxHelper — use built-in update
316
+ helper.update();
317
+ }
318
+ };
319
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
320
+ if (_loop()) continue;
321
+ }
322
+ } catch (err) {
323
+ _iterator3.e(err);
324
+ } finally {
325
+ _iterator3.f();
326
+ }
327
+ }
328
+
329
+ export { computeFilteredBoundingBox, computeIODeviceBoundingBoxes, createSelectionBoxHelpers, updateSelectionBoxHelpers };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2112-lab/central-plant",
3
- "version": "0.1.77",
3
+ "version": "0.1.78",
4
4
  "description": "Utility modules for the Central Plant Application",
5
5
  "main": "dist/bundle/index.js",
6
6
  "module": "dist/esm/index.js",