@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.
@@ -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.76
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.
@@ -1,4 +1,4 @@
1
- import { createClass as _createClass, objectSpread2 as _objectSpread2, createForOfIteratorHelper as _createForOfIteratorHelper, typeof as _typeof, classCallCheck as _classCallCheck, asyncToGenerator as _asyncToGenerator, regenerator as _regenerator } from '../../_virtual/_rollupPluginBabelHelpers.js';
1
+ import { createClass as _createClass, objectSpread2 as _objectSpread2, createForOfIteratorHelper as _createForOfIteratorHelper, typeof as _typeof, slicedToArray as _slicedToArray, classCallCheck as _classCallCheck, asyncToGenerator as _asyncToGenerator, regenerator as _regenerator } from '../../_virtual/_rollupPluginBabelHelpers.js';
2
2
  import * as THREE from 'three';
3
3
  import { CentralPlantValidator } from './centralPlantValidator.js';
4
4
  import { createTransformControls } from '../managers/controls/transformControlsManager.js';
@@ -1031,6 +1031,11 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1031
1031
  });
1032
1032
  }
1033
1033
 
1034
+ // Add attached IO device models for smart components
1035
+ if (componentData.isSmart && componentData.attachedDevices) {
1036
+ this._attachIODevicesToComponent(componentModel, componentData, modelPreloader, componentId);
1037
+ }
1038
+
1034
1039
  // Notify the component manager about the new component
1035
1040
  if (componentManager.registerComponent) {
1036
1041
  componentManager.registerComponent(componentModel);
@@ -1081,6 +1086,74 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1081
1086
  }
1082
1087
  }
1083
1088
 
1089
+ /**
1090
+ * Attach IO device models to a smart component from cached models.
1091
+ * Each device referenced in componentData.attachedDevices is looked up
1092
+ * in the model preloader cache, cloned, positioned, and added as a child.
1093
+ * @param {THREE.Object3D} componentModel - The parent component model
1094
+ * @param {Object} componentData - Component dictionary entry (has attachedDevices)
1095
+ * @param {Object} modelPreloader - ModelPreloader instance
1096
+ * @param {string} parentComponentId - The parent component's UUID
1097
+ * @private
1098
+ */
1099
+ }, {
1100
+ key: "_attachIODevicesToComponent",
1101
+ value: function _attachIODevicesToComponent(componentModel, componentData, modelPreloader, parentComponentId) {
1102
+ var attachedDevices = componentData.attachedDevices;
1103
+ console.log("\uD83D\uDD0C addComponent(): Attaching ".concat(Object.keys(attachedDevices).length, " IO devices to smart component"));
1104
+ for (var _i = 0, _Object$entries = Object.entries(attachedDevices); _i < _Object$entries.length; _i++) {
1105
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
1106
+ attachmentId = _Object$entries$_i[0],
1107
+ attachment = _Object$entries$_i[1];
1108
+ try {
1109
+ var _modelPreloader$compo, _attachment$attachmen;
1110
+ var deviceData = (_modelPreloader$compo = modelPreloader.componentDictionary) === null || _modelPreloader$compo === void 0 ? void 0 : _modelPreloader$compo[attachment.deviceId];
1111
+ if (!deviceData || !deviceData.modelKey) {
1112
+ console.warn("\u26A0\uFE0F IO device ".concat(attachment.deviceId, " not found in dictionary, skipping"));
1113
+ continue;
1114
+ }
1115
+ var deviceModel = modelPreloader.getCachedModelWithDimensions(deviceData.modelKey, attachment.deviceId);
1116
+ if (!deviceModel) {
1117
+ console.warn("\u26A0\uFE0F IO device model not in cache: ".concat(deviceData.modelKey, ", skipping"));
1118
+ continue;
1119
+ }
1120
+
1121
+ // Name the device model
1122
+ deviceModel.name = "".concat(attachment.attachmentLabel || 'IO Device', " (").concat(attachmentId, ")");
1123
+
1124
+ // Set user data for identification
1125
+ deviceModel.userData = {
1126
+ objectType: 'io-device',
1127
+ deviceId: attachment.deviceId,
1128
+ attachmentId: attachmentId,
1129
+ attachmentLabel: attachment.attachmentLabel,
1130
+ parentComponentId: parentComponentId
1131
+ };
1132
+
1133
+ // Position at the attachment point
1134
+ if ((_attachment$attachmen = attachment.attachmentPoint) !== null && _attachment$attachmen !== void 0 && _attachment$attachmen.position) {
1135
+ var pos = attachment.attachmentPoint.position;
1136
+ deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
1137
+ }
1138
+
1139
+ // IO device models are authored at the same real-world unit scale
1140
+ // as the host component, so keep them at their natural (1:1) size.
1141
+ // Note: attachmentPoint.scale is the connector marker sphere size,
1142
+ // NOT a desired device model scale.
1143
+ deviceModel.scale.setScalar(1);
1144
+
1145
+ // Add as child of the component
1146
+ componentModel.add(deviceModel);
1147
+ console.log("\u2705 Attached IO device: ".concat(attachment.attachmentLabel || attachment.deviceId, " at"), {
1148
+ position: deviceModel.position,
1149
+ scale: deviceModel.scale
1150
+ });
1151
+ } catch (err) {
1152
+ console.error("\u274C Error attaching IO device ".concat(attachment.deviceId, ":"), err);
1153
+ }
1154
+ }
1155
+ }
1156
+
1084
1157
  /**
1085
1158
  * Delete a component from the scene by componentId (internal implementation)
1086
1159
  * @param {string} componentId - The UUID of the component to delete