@2112-lab/central-plant 0.1.86 → 0.1.87

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.
@@ -11375,6 +11375,12 @@ var SceneExportManager = /*#__PURE__*/function () {
11375
11375
  // Computed from geometry - not in input format
11376
11376
  'name',
11377
11377
  // Redundant with GLB node names - not in input format
11378
+ 'addedTimestamp',
11379
+ // Internal tracking - not needed in export
11380
+ 'addedBy',
11381
+ // Internal tracking - not needed in export
11382
+ 'initialPosition',
11383
+ // Internal tracking - not needed in export
11378
11384
  // Exclude internal segment tracking properties
11379
11385
  'segmentId',
11380
11386
  // Internal tracking
@@ -33790,24 +33796,14 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
33790
33796
  }, {
33791
33797
  key: "toggleIODeviceBinaryState",
33792
33798
  value: function toggleIODeviceBinaryState(ioDeviceObject) {
33793
- var _this$sceneViewer;
33799
+ var _ref, _this$sceneViewer;
33794
33800
  if (!ioDeviceObject || !this._stateAdapter) return;
33795
33801
  var ud = ioDeviceObject.userData;
33796
33802
  var attachmentId = ud === null || ud === void 0 ? void 0 : ud.attachmentId;
33797
33803
  var dataPoints = (ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [];
33798
33804
  if (!attachmentId) return;
33799
33805
 
33800
- // Find the first binary state
33801
- var binaryState = dataPoints.find(function (dp) {
33802
- return dp.stateType === 'binary' || dp.type === 'binary';
33803
- });
33804
- if (!binaryState) return;
33805
- var dpId = binaryState.id;
33806
- var currentVal = this._stateAdapter.getState(attachmentId, dpId);
33807
- var newVal = !Boolean(currentVal);
33808
- this._stateAdapter.setState(attachmentId, dpId, newVal);
33809
-
33810
- // Walk up to find parent component UUID for scoped behavior triggering
33806
+ // Walk up to find parent component UUID for scoped state/behavior handling
33811
33807
  var parentUuid = null;
33812
33808
  var obj = ioDeviceObject.parent;
33813
33809
  while (obj) {
@@ -33818,8 +33814,24 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
33818
33814
  }
33819
33815
  obj = obj.parent;
33820
33816
  }
33817
+
33818
+ // Create a scoped attachment key to prevent state sharing between instances
33819
+ // of the same smart component that share the same attachmentId
33820
+ var scopedAttachmentId = this._getScopedAttachmentKey(attachmentId, parentUuid);
33821
+
33822
+ // Find the first binary state
33823
+ var binaryState = dataPoints.find(function (dp) {
33824
+ return dp.stateType === 'binary' || dp.type === 'binary';
33825
+ });
33826
+ if (!binaryState) return;
33827
+ var dpId = binaryState.id;
33828
+ var storedVal = this._stateAdapter.getState(scopedAttachmentId, dpId);
33829
+ // Fall back to defaultValue when state is uninitialized (null/undefined)
33830
+ var currentVal = (_ref = storedVal !== null && storedVal !== void 0 ? storedVal : binaryState.defaultValue) !== null && _ref !== void 0 ? _ref : false;
33831
+ var newVal = !Boolean(currentVal);
33832
+ this._stateAdapter.setState(scopedAttachmentId, dpId, newVal);
33821
33833
  (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.managers) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.behaviorManager) === null || _this$sceneViewer === void 0 || _this$sceneViewer.triggerState(attachmentId, dpId, newVal, parentUuid);
33822
- console.log("\uD83D\uDD04 [IODevice] Toggled ".concat(attachmentId, ".").concat(dpId, ": ").concat(currentVal, " \u2192 ").concat(newVal));
33834
+ console.log("\uD83D\uDD04 [IODevice] Toggled ".concat(scopedAttachmentId, ".").concat(dpId, ": ").concat(currentVal, " \u2192 ").concat(newVal));
33823
33835
  }
33824
33836
 
33825
33837
  /**
@@ -33911,23 +33923,42 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
33911
33923
  this._styleInjected = false;
33912
33924
  }
33913
33925
 
33926
+ /**
33927
+ * Generate a scoped attachment key that includes the parent component UUID.
33928
+ * This ensures each instance of a smart component has isolated IO device state.
33929
+ * @param {string} attachmentId - The original attachment ID from the component data
33930
+ * @param {string|null} parentUuid - The UUID of the parent smart component instance
33931
+ * @returns {string} A scoped key in the format "parentUuid::attachmentId" or just attachmentId if no parent
33932
+ * @private
33933
+ */
33934
+ }, {
33935
+ key: "_getScopedAttachmentKey",
33936
+ value: function _getScopedAttachmentKey(attachmentId, parentUuid) {
33937
+ if (!parentUuid) return attachmentId;
33938
+ return "".concat(parentUuid, "::").concat(attachmentId);
33939
+ }
33940
+
33914
33941
  /**
33915
33942
  * Gather I/O device children from a component's Three.js hierarchy.
33916
33943
  * Returns richer data including attachmentId and data point definitions.
33917
- * @param {THREE.Object3D} object
33918
- * @returns {{ label: string, deviceId: string, attachmentId: string, dataPoints: Array }[]}
33944
+ * @param {THREE.Object3D} object - The parent component object
33945
+ * @returns {{ label: string, deviceId: string, attachmentId: string, scopedAttachmentId: string, dataPoints: Array }[]}
33919
33946
  */
33920
33947
  }, {
33921
33948
  key: "_getIODevices",
33922
33949
  value: function _getIODevices(object) {
33950
+ var _this2 = this;
33923
33951
  var devices = [];
33952
+ var parentUuid = object.uuid; // The component's own UUID
33924
33953
  object.traverse(function (child) {
33925
33954
  var _child$userData;
33926
33955
  if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'io-device') {
33956
+ var attachmentId = child.userData.attachmentId || '';
33927
33957
  devices.push({
33928
33958
  label: child.userData.attachmentLabel || child.name || child.userData.deviceId || 'Unknown Device',
33929
33959
  deviceId: child.userData.deviceId || '',
33930
- attachmentId: child.userData.attachmentId || '',
33960
+ attachmentId: attachmentId,
33961
+ scopedAttachmentId: _this2._getScopedAttachmentKey(attachmentId, parentUuid),
33931
33962
  dataPoints: child.userData.dataPoints || [],
33932
33963
  direction: child.userData.ioDirection || 'output'
33933
33964
  });
@@ -33943,7 +33974,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
33943
33974
  }, {
33944
33975
  key: "_buildTooltip",
33945
33976
  value: function _buildTooltip(object) {
33946
- var _this2 = this;
33977
+ var _this3 = this;
33947
33978
  // Remove any existing tooltip first
33948
33979
  this.hide();
33949
33980
  // Re-assign selected object since hide() clears it
@@ -34011,9 +34042,10 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
34011
34042
  list.appendChild(item);
34012
34043
 
34013
34044
  // Data point rows (one per data point definition stored in userData)
34014
- if (device.attachmentId && device.dataPoints.length > 0) {
34045
+ // Use scopedAttachmentId to ensure state is isolated per component instance
34046
+ if (device.scopedAttachmentId && device.dataPoints.length > 0) {
34015
34047
  device.dataPoints.forEach(function (dp) {
34016
- var row = _this2._buildDataPointRow(device.attachmentId, dp, device.direction);
34048
+ var row = _this3._buildDataPointRow(device.scopedAttachmentId, dp, device.direction, device.attachmentId);
34017
34049
  list.appendChild(row);
34018
34050
  });
34019
34051
  }
@@ -34024,11 +34056,11 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
34024
34056
  // Hover expand/collapse
34025
34057
  trigger.addEventListener('mouseenter', function () {
34026
34058
  ioSection.classList.add('expanded');
34027
- _this2._ioExpanded = true;
34059
+ _this3._ioExpanded = true;
34028
34060
  });
34029
34061
  ioSection.addEventListener('mouseleave', function () {
34030
34062
  ioSection.classList.remove('expanded');
34031
- _this2._ioExpanded = false;
34063
+ _this3._ioExpanded = false;
34032
34064
  });
34033
34065
  card.appendChild(ioSection);
34034
34066
  } else {
@@ -34127,18 +34159,19 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
34127
34159
  * Output / read-only direction → shows a state badge (updated each frame).
34128
34160
  * Input / bidirectional → shows an interactive control.
34129
34161
  *
34130
- * @param {string} attachmentId
34162
+ * @param {string} scopedAttachmentId - Scoped attachment ID (parentUuid::attachmentId) for state isolation
34131
34163
  * @param {Object} dp - data point definition from ioConfig.dataPoints
34132
34164
  * @param {string} [deviceDirection] - device-level direction ('input'|'output'), overrides dp.direction
34165
+ * @param {string} [originalAttachmentId] - Original attachment ID for behavior triggering
34133
34166
  * @returns {HTMLElement}
34134
34167
  */
34135
34168
  }, {
34136
34169
  key: "_buildDataPointRow",
34137
- value: function _buildDataPointRow(attachmentId, dp, deviceDirection) {
34138
- var _ref,
34170
+ value: function _buildDataPointRow(scopedAttachmentId, dp, deviceDirection, originalAttachmentId) {
34171
+ var _ref2,
34139
34172
  _this$_stateAdapter$g,
34140
34173
  _this$_stateAdapter,
34141
- _this3 = this;
34174
+ _this4 = this;
34142
34175
  var row = document.createElement('div');
34143
34176
  row.className = 'cp-tooltip__dp-row';
34144
34177
  var nameEl = document.createElement('span');
@@ -34146,20 +34179,21 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
34146
34179
  nameEl.textContent = dp.name || dp.id || '?';
34147
34180
  row.appendChild(nameEl);
34148
34181
  var dpId = dp.id || dp.name;
34149
- var key = "".concat(attachmentId, "::").concat(dpId);
34182
+ var key = "".concat(scopedAttachmentId, "::").concat(dpId);
34150
34183
  // Device-level direction takes precedence; fall back to per-dp direction
34151
34184
  var resolvedDirection = deviceDirection || dp.direction || 'output';
34152
34185
  var isInput = resolvedDirection === 'input' || resolvedDirection === 'bidirectional';
34153
- var currentVal = (_ref = (_this$_stateAdapter$g = (_this$_stateAdapter = this._stateAdapter) === null || _this$_stateAdapter === void 0 ? void 0 : _this$_stateAdapter.getState(attachmentId, dpId)) !== null && _this$_stateAdapter$g !== void 0 ? _this$_stateAdapter$g : dp.defaultValue) !== null && _ref !== void 0 ? _ref : null;
34186
+ var currentVal = (_ref2 = (_this$_stateAdapter$g = (_this$_stateAdapter = this._stateAdapter) === null || _this$_stateAdapter === void 0 ? void 0 : _this$_stateAdapter.getState(scopedAttachmentId, dpId)) !== null && _this$_stateAdapter$g !== void 0 ? _this$_stateAdapter$g : dp.defaultValue) !== null && _ref2 !== void 0 ? _ref2 : null;
34154
34187
  if (isInput) {
34155
34188
  var ctrl = this._buildInputControl(dp, currentVal, function (newVal) {
34156
- var _this3$_stateAdapter, _this3$selectedObject, _this3$sceneViewer;
34157
- (_this3$_stateAdapter = _this3._stateAdapter) === null || _this3$_stateAdapter === void 0 || _this3$_stateAdapter.setState(attachmentId, dpId, newVal);
34189
+ var _this4$_stateAdapter, _this4$selectedObject, _this4$sceneViewer;
34190
+ (_this4$_stateAdapter = _this4._stateAdapter) === null || _this4$_stateAdapter === void 0 || _this4$_stateAdapter.setState(scopedAttachmentId, dpId, newVal);
34158
34191
  // Also fire BehaviorManager so any wired behaviors react immediately.
34159
34192
  // Pass the parent component UUID so behaviors scoped to a specific instance
34160
34193
  // don't bleed across clones that share the same attachmentId.
34161
- var parentUuid = ((_this3$selectedObject = _this3.selectedObject) === null || _this3$selectedObject === void 0 ? void 0 : _this3$selectedObject.uuid) || null;
34162
- (_this3$sceneViewer = _this3.sceneViewer) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.managers) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.behaviorManager) === null || _this3$sceneViewer === void 0 || _this3$sceneViewer.triggerState(attachmentId, dpId, newVal, parentUuid);
34194
+ // Use originalAttachmentId for behavior triggering as behaviors are keyed by original ID
34195
+ var parentUuid = ((_this4$selectedObject = _this4.selectedObject) === null || _this4$selectedObject === void 0 ? void 0 : _this4$selectedObject.uuid) || null;
34196
+ (_this4$sceneViewer = _this4.sceneViewer) === null || _this4$sceneViewer === void 0 || (_this4$sceneViewer = _this4$sceneViewer.managers) === null || _this4$sceneViewer === void 0 || (_this4$sceneViewer = _this4$sceneViewer.behaviorManager) === null || _this4$sceneViewer === void 0 || _this4$sceneViewer.triggerState(originalAttachmentId || scopedAttachmentId, dpId, newVal, parentUuid);
34163
34197
  });
34164
34198
  row.appendChild(ctrl);
34165
34199
  this._stateElements.set(key, {
@@ -34309,20 +34343,23 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
34309
34343
  /**
34310
34344
  * Re-read all tracked read-only badge values from the state adapter.
34311
34345
  * Called each frame from update() — skips if no adapter is configured.
34346
+ * Key format is scopedAttachmentId::dataPointId where scopedAttachmentId
34347
+ * can be parentUuid::attachmentId, resulting in parentUuid::attachmentId::dataPointId
34312
34348
  */
34313
34349
  }, {
34314
34350
  key: "_refreshStateDisplays",
34315
34351
  value: function _refreshStateDisplays() {
34316
- var _this4 = this;
34352
+ var _this5 = this;
34317
34353
  if (!this._stateAdapter || !this._stateElements.size) return;
34318
34354
  this._stateElements.forEach(function (entry, key) {
34319
34355
  if (entry.isInput) return; // interactive controls are user-driven; don't overwrite
34320
- var sepIdx = key.indexOf('::');
34356
+ // Use lastIndexOf since scopedAttachmentId may contain '::' (parentUuid::attachmentId)
34357
+ var sepIdx = key.lastIndexOf('::');
34321
34358
  if (sepIdx === -1) return;
34322
- var attachmentId = key.slice(0, sepIdx);
34359
+ var scopedAttachmentId = key.slice(0, sepIdx);
34323
34360
  var dataPointId = key.slice(sepIdx + 2);
34324
- var val = _this4._stateAdapter.getState(attachmentId, dataPointId);
34325
- _this4._applyBadgeValue(entry.el, val, entry.dp);
34361
+ var val = _this5._stateAdapter.getState(scopedAttachmentId, dataPointId);
34362
+ _this5._applyBadgeValue(entry.el, val, entry.dp);
34326
34363
  });
34327
34364
  }
34328
34365
  }]);
@@ -36734,7 +36771,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
36734
36771
  * Initialize the CentralPlant manager
36735
36772
  *
36736
36773
  * @constructor
36737
- * @version 0.1.86
36774
+ * @version 0.1.87
36738
36775
  * @updated 2025-10-22
36739
36776
  *
36740
36777
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -19,7 +19,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
19
19
  * Initialize the CentralPlant manager
20
20
  *
21
21
  * @constructor
22
- * @version 0.1.86
22
+ * @version 0.1.87
23
23
  * @updated 2025-10-22
24
24
  *
25
25
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -129,24 +129,14 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
129
129
  }, {
130
130
  key: "toggleIODeviceBinaryState",
131
131
  value: function toggleIODeviceBinaryState(ioDeviceObject) {
132
- var _this$sceneViewer;
132
+ var _ref, _this$sceneViewer;
133
133
  if (!ioDeviceObject || !this._stateAdapter) return;
134
134
  var ud = ioDeviceObject.userData;
135
135
  var attachmentId = ud === null || ud === void 0 ? void 0 : ud.attachmentId;
136
136
  var dataPoints = (ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [];
137
137
  if (!attachmentId) return;
138
138
 
139
- // Find the first binary state
140
- var binaryState = dataPoints.find(function (dp) {
141
- return dp.stateType === 'binary' || dp.type === 'binary';
142
- });
143
- if (!binaryState) return;
144
- var dpId = binaryState.id;
145
- var currentVal = this._stateAdapter.getState(attachmentId, dpId);
146
- var newVal = !Boolean(currentVal);
147
- this._stateAdapter.setState(attachmentId, dpId, newVal);
148
-
149
- // Walk up to find parent component UUID for scoped behavior triggering
139
+ // Walk up to find parent component UUID for scoped state/behavior handling
150
140
  var parentUuid = null;
151
141
  var obj = ioDeviceObject.parent;
152
142
  while (obj) {
@@ -157,8 +147,24 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
157
147
  }
158
148
  obj = obj.parent;
159
149
  }
150
+
151
+ // Create a scoped attachment key to prevent state sharing between instances
152
+ // of the same smart component that share the same attachmentId
153
+ var scopedAttachmentId = this._getScopedAttachmentKey(attachmentId, parentUuid);
154
+
155
+ // Find the first binary state
156
+ var binaryState = dataPoints.find(function (dp) {
157
+ return dp.stateType === 'binary' || dp.type === 'binary';
158
+ });
159
+ if (!binaryState) return;
160
+ var dpId = binaryState.id;
161
+ var storedVal = this._stateAdapter.getState(scopedAttachmentId, dpId);
162
+ // Fall back to defaultValue when state is uninitialized (null/undefined)
163
+ var currentVal = (_ref = storedVal !== null && storedVal !== void 0 ? storedVal : binaryState.defaultValue) !== null && _ref !== void 0 ? _ref : false;
164
+ var newVal = !Boolean(currentVal);
165
+ this._stateAdapter.setState(scopedAttachmentId, dpId, newVal);
160
166
  (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.managers) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.behaviorManager) === null || _this$sceneViewer === void 0 || _this$sceneViewer.triggerState(attachmentId, dpId, newVal, parentUuid);
161
- console.log("\uD83D\uDD04 [IODevice] Toggled ".concat(attachmentId, ".").concat(dpId, ": ").concat(currentVal, " \u2192 ").concat(newVal));
167
+ console.log("\uD83D\uDD04 [IODevice] Toggled ".concat(scopedAttachmentId, ".").concat(dpId, ": ").concat(currentVal, " \u2192 ").concat(newVal));
162
168
  }
163
169
 
164
170
  /**
@@ -250,23 +256,42 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
250
256
  this._styleInjected = false;
251
257
  }
252
258
 
259
+ /**
260
+ * Generate a scoped attachment key that includes the parent component UUID.
261
+ * This ensures each instance of a smart component has isolated IO device state.
262
+ * @param {string} attachmentId - The original attachment ID from the component data
263
+ * @param {string|null} parentUuid - The UUID of the parent smart component instance
264
+ * @returns {string} A scoped key in the format "parentUuid::attachmentId" or just attachmentId if no parent
265
+ * @private
266
+ */
267
+ }, {
268
+ key: "_getScopedAttachmentKey",
269
+ value: function _getScopedAttachmentKey(attachmentId, parentUuid) {
270
+ if (!parentUuid) return attachmentId;
271
+ return "".concat(parentUuid, "::").concat(attachmentId);
272
+ }
273
+
253
274
  /**
254
275
  * Gather I/O device children from a component's Three.js hierarchy.
255
276
  * Returns richer data including attachmentId and data point definitions.
256
- * @param {THREE.Object3D} object
257
- * @returns {{ label: string, deviceId: string, attachmentId: string, dataPoints: Array }[]}
277
+ * @param {THREE.Object3D} object - The parent component object
278
+ * @returns {{ label: string, deviceId: string, attachmentId: string, scopedAttachmentId: string, dataPoints: Array }[]}
258
279
  */
259
280
  }, {
260
281
  key: "_getIODevices",
261
282
  value: function _getIODevices(object) {
283
+ var _this2 = this;
262
284
  var devices = [];
285
+ var parentUuid = object.uuid; // The component's own UUID
263
286
  object.traverse(function (child) {
264
287
  var _child$userData;
265
288
  if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'io-device') {
289
+ var attachmentId = child.userData.attachmentId || '';
266
290
  devices.push({
267
291
  label: child.userData.attachmentLabel || child.name || child.userData.deviceId || 'Unknown Device',
268
292
  deviceId: child.userData.deviceId || '',
269
- attachmentId: child.userData.attachmentId || '',
293
+ attachmentId: attachmentId,
294
+ scopedAttachmentId: _this2._getScopedAttachmentKey(attachmentId, parentUuid),
270
295
  dataPoints: child.userData.dataPoints || [],
271
296
  direction: child.userData.ioDirection || 'output'
272
297
  });
@@ -282,7 +307,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
282
307
  }, {
283
308
  key: "_buildTooltip",
284
309
  value: function _buildTooltip(object) {
285
- var _this2 = this;
310
+ var _this3 = this;
286
311
  // Remove any existing tooltip first
287
312
  this.hide();
288
313
  // Re-assign selected object since hide() clears it
@@ -350,9 +375,10 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
350
375
  list.appendChild(item);
351
376
 
352
377
  // Data point rows (one per data point definition stored in userData)
353
- if (device.attachmentId && device.dataPoints.length > 0) {
378
+ // Use scopedAttachmentId to ensure state is isolated per component instance
379
+ if (device.scopedAttachmentId && device.dataPoints.length > 0) {
354
380
  device.dataPoints.forEach(function (dp) {
355
- var row = _this2._buildDataPointRow(device.attachmentId, dp, device.direction);
381
+ var row = _this3._buildDataPointRow(device.scopedAttachmentId, dp, device.direction, device.attachmentId);
356
382
  list.appendChild(row);
357
383
  });
358
384
  }
@@ -363,11 +389,11 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
363
389
  // Hover expand/collapse
364
390
  trigger.addEventListener('mouseenter', function () {
365
391
  ioSection.classList.add('expanded');
366
- _this2._ioExpanded = true;
392
+ _this3._ioExpanded = true;
367
393
  });
368
394
  ioSection.addEventListener('mouseleave', function () {
369
395
  ioSection.classList.remove('expanded');
370
- _this2._ioExpanded = false;
396
+ _this3._ioExpanded = false;
371
397
  });
372
398
  card.appendChild(ioSection);
373
399
  } else {
@@ -466,18 +492,19 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
466
492
  * Output / read-only direction → shows a state badge (updated each frame).
467
493
  * Input / bidirectional → shows an interactive control.
468
494
  *
469
- * @param {string} attachmentId
495
+ * @param {string} scopedAttachmentId - Scoped attachment ID (parentUuid::attachmentId) for state isolation
470
496
  * @param {Object} dp - data point definition from ioConfig.dataPoints
471
497
  * @param {string} [deviceDirection] - device-level direction ('input'|'output'), overrides dp.direction
498
+ * @param {string} [originalAttachmentId] - Original attachment ID for behavior triggering
472
499
  * @returns {HTMLElement}
473
500
  */
474
501
  }, {
475
502
  key: "_buildDataPointRow",
476
- value: function _buildDataPointRow(attachmentId, dp, deviceDirection) {
477
- var _ref,
503
+ value: function _buildDataPointRow(scopedAttachmentId, dp, deviceDirection, originalAttachmentId) {
504
+ var _ref2,
478
505
  _this$_stateAdapter$g,
479
506
  _this$_stateAdapter,
480
- _this3 = this;
507
+ _this4 = this;
481
508
  var row = document.createElement('div');
482
509
  row.className = 'cp-tooltip__dp-row';
483
510
  var nameEl = document.createElement('span');
@@ -485,20 +512,21 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
485
512
  nameEl.textContent = dp.name || dp.id || '?';
486
513
  row.appendChild(nameEl);
487
514
  var dpId = dp.id || dp.name;
488
- var key = "".concat(attachmentId, "::").concat(dpId);
515
+ var key = "".concat(scopedAttachmentId, "::").concat(dpId);
489
516
  // Device-level direction takes precedence; fall back to per-dp direction
490
517
  var resolvedDirection = deviceDirection || dp.direction || 'output';
491
518
  var isInput = resolvedDirection === 'input' || resolvedDirection === 'bidirectional';
492
- var currentVal = (_ref = (_this$_stateAdapter$g = (_this$_stateAdapter = this._stateAdapter) === null || _this$_stateAdapter === void 0 ? void 0 : _this$_stateAdapter.getState(attachmentId, dpId)) !== null && _this$_stateAdapter$g !== void 0 ? _this$_stateAdapter$g : dp.defaultValue) !== null && _ref !== void 0 ? _ref : null;
519
+ var currentVal = (_ref2 = (_this$_stateAdapter$g = (_this$_stateAdapter = this._stateAdapter) === null || _this$_stateAdapter === void 0 ? void 0 : _this$_stateAdapter.getState(scopedAttachmentId, dpId)) !== null && _this$_stateAdapter$g !== void 0 ? _this$_stateAdapter$g : dp.defaultValue) !== null && _ref2 !== void 0 ? _ref2 : null;
493
520
  if (isInput) {
494
521
  var ctrl = this._buildInputControl(dp, currentVal, function (newVal) {
495
- var _this3$_stateAdapter, _this3$selectedObject, _this3$sceneViewer;
496
- (_this3$_stateAdapter = _this3._stateAdapter) === null || _this3$_stateAdapter === void 0 || _this3$_stateAdapter.setState(attachmentId, dpId, newVal);
522
+ var _this4$_stateAdapter, _this4$selectedObject, _this4$sceneViewer;
523
+ (_this4$_stateAdapter = _this4._stateAdapter) === null || _this4$_stateAdapter === void 0 || _this4$_stateAdapter.setState(scopedAttachmentId, dpId, newVal);
497
524
  // Also fire BehaviorManager so any wired behaviors react immediately.
498
525
  // Pass the parent component UUID so behaviors scoped to a specific instance
499
526
  // don't bleed across clones that share the same attachmentId.
500
- var parentUuid = ((_this3$selectedObject = _this3.selectedObject) === null || _this3$selectedObject === void 0 ? void 0 : _this3$selectedObject.uuid) || null;
501
- (_this3$sceneViewer = _this3.sceneViewer) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.managers) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.behaviorManager) === null || _this3$sceneViewer === void 0 || _this3$sceneViewer.triggerState(attachmentId, dpId, newVal, parentUuid);
527
+ // Use originalAttachmentId for behavior triggering as behaviors are keyed by original ID
528
+ var parentUuid = ((_this4$selectedObject = _this4.selectedObject) === null || _this4$selectedObject === void 0 ? void 0 : _this4$selectedObject.uuid) || null;
529
+ (_this4$sceneViewer = _this4.sceneViewer) === null || _this4$sceneViewer === void 0 || (_this4$sceneViewer = _this4$sceneViewer.managers) === null || _this4$sceneViewer === void 0 || (_this4$sceneViewer = _this4$sceneViewer.behaviorManager) === null || _this4$sceneViewer === void 0 || _this4$sceneViewer.triggerState(originalAttachmentId || scopedAttachmentId, dpId, newVal, parentUuid);
502
530
  });
503
531
  row.appendChild(ctrl);
504
532
  this._stateElements.set(key, {
@@ -648,20 +676,23 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
648
676
  /**
649
677
  * Re-read all tracked read-only badge values from the state adapter.
650
678
  * Called each frame from update() — skips if no adapter is configured.
679
+ * Key format is scopedAttachmentId::dataPointId where scopedAttachmentId
680
+ * can be parentUuid::attachmentId, resulting in parentUuid::attachmentId::dataPointId
651
681
  */
652
682
  }, {
653
683
  key: "_refreshStateDisplays",
654
684
  value: function _refreshStateDisplays() {
655
- var _this4 = this;
685
+ var _this5 = this;
656
686
  if (!this._stateAdapter || !this._stateElements.size) return;
657
687
  this._stateElements.forEach(function (entry, key) {
658
688
  if (entry.isInput) return; // interactive controls are user-driven; don't overwrite
659
- var sepIdx = key.indexOf('::');
689
+ // Use lastIndexOf since scopedAttachmentId may contain '::' (parentUuid::attachmentId)
690
+ var sepIdx = key.lastIndexOf('::');
660
691
  if (sepIdx === -1) return;
661
- var attachmentId = key.slice(0, sepIdx);
692
+ var scopedAttachmentId = key.slice(0, sepIdx);
662
693
  var dataPointId = key.slice(sepIdx + 2);
663
- var val = _this4._stateAdapter.getState(attachmentId, dataPointId);
664
- _this4._applyBadgeValue(entry.el, val, entry.dp);
694
+ var val = _this5._stateAdapter.getState(scopedAttachmentId, dataPointId);
695
+ _this5._applyBadgeValue(entry.el, val, entry.dp);
665
696
  });
666
697
  }
667
698
  }]);
@@ -160,6 +160,12 @@ var SceneExportManager = /*#__PURE__*/function () {
160
160
  // Computed from geometry - not in input format
161
161
  'name',
162
162
  // Redundant with GLB node names - not in input format
163
+ 'addedTimestamp',
164
+ // Internal tracking - not needed in export
165
+ 'addedBy',
166
+ // Internal tracking - not needed in export
167
+ 'initialPosition',
168
+ // Internal tracking - not needed in export
163
169
  // Exclude internal segment tracking properties
164
170
  'segmentId',
165
171
  // Internal tracking
@@ -15,7 +15,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
15
15
  * Initialize the CentralPlant manager
16
16
  *
17
17
  * @constructor
18
- * @version 0.1.86
18
+ * @version 0.1.87
19
19
  * @updated 2025-10-22
20
20
  *
21
21
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -105,24 +105,14 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
105
105
  }, {
106
106
  key: "toggleIODeviceBinaryState",
107
107
  value: function toggleIODeviceBinaryState(ioDeviceObject) {
108
- var _this$sceneViewer;
108
+ var _ref, _this$sceneViewer;
109
109
  if (!ioDeviceObject || !this._stateAdapter) return;
110
110
  var ud = ioDeviceObject.userData;
111
111
  var attachmentId = ud === null || ud === void 0 ? void 0 : ud.attachmentId;
112
112
  var dataPoints = (ud === null || ud === void 0 ? void 0 : ud.dataPoints) || [];
113
113
  if (!attachmentId) return;
114
114
 
115
- // Find the first binary state
116
- var binaryState = dataPoints.find(function (dp) {
117
- return dp.stateType === 'binary' || dp.type === 'binary';
118
- });
119
- if (!binaryState) return;
120
- var dpId = binaryState.id;
121
- var currentVal = this._stateAdapter.getState(attachmentId, dpId);
122
- var newVal = !Boolean(currentVal);
123
- this._stateAdapter.setState(attachmentId, dpId, newVal);
124
-
125
- // Walk up to find parent component UUID for scoped behavior triggering
115
+ // Walk up to find parent component UUID for scoped state/behavior handling
126
116
  var parentUuid = null;
127
117
  var obj = ioDeviceObject.parent;
128
118
  while (obj) {
@@ -133,8 +123,24 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
133
123
  }
134
124
  obj = obj.parent;
135
125
  }
126
+
127
+ // Create a scoped attachment key to prevent state sharing between instances
128
+ // of the same smart component that share the same attachmentId
129
+ var scopedAttachmentId = this._getScopedAttachmentKey(attachmentId, parentUuid);
130
+
131
+ // Find the first binary state
132
+ var binaryState = dataPoints.find(function (dp) {
133
+ return dp.stateType === 'binary' || dp.type === 'binary';
134
+ });
135
+ if (!binaryState) return;
136
+ var dpId = binaryState.id;
137
+ var storedVal = this._stateAdapter.getState(scopedAttachmentId, dpId);
138
+ // Fall back to defaultValue when state is uninitialized (null/undefined)
139
+ var currentVal = (_ref = storedVal !== null && storedVal !== void 0 ? storedVal : binaryState.defaultValue) !== null && _ref !== void 0 ? _ref : false;
140
+ var newVal = !Boolean(currentVal);
141
+ this._stateAdapter.setState(scopedAttachmentId, dpId, newVal);
136
142
  (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.managers) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.behaviorManager) === null || _this$sceneViewer === void 0 || _this$sceneViewer.triggerState(attachmentId, dpId, newVal, parentUuid);
137
- console.log("\uD83D\uDD04 [IODevice] Toggled ".concat(attachmentId, ".").concat(dpId, ": ").concat(currentVal, " \u2192 ").concat(newVal));
143
+ console.log("\uD83D\uDD04 [IODevice] Toggled ".concat(scopedAttachmentId, ".").concat(dpId, ": ").concat(currentVal, " \u2192 ").concat(newVal));
138
144
  }
139
145
 
140
146
  /**
@@ -226,23 +232,42 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
226
232
  this._styleInjected = false;
227
233
  }
228
234
 
235
+ /**
236
+ * Generate a scoped attachment key that includes the parent component UUID.
237
+ * This ensures each instance of a smart component has isolated IO device state.
238
+ * @param {string} attachmentId - The original attachment ID from the component data
239
+ * @param {string|null} parentUuid - The UUID of the parent smart component instance
240
+ * @returns {string} A scoped key in the format "parentUuid::attachmentId" or just attachmentId if no parent
241
+ * @private
242
+ */
243
+ }, {
244
+ key: "_getScopedAttachmentKey",
245
+ value: function _getScopedAttachmentKey(attachmentId, parentUuid) {
246
+ if (!parentUuid) return attachmentId;
247
+ return "".concat(parentUuid, "::").concat(attachmentId);
248
+ }
249
+
229
250
  /**
230
251
  * Gather I/O device children from a component's Three.js hierarchy.
231
252
  * Returns richer data including attachmentId and data point definitions.
232
- * @param {THREE.Object3D} object
233
- * @returns {{ label: string, deviceId: string, attachmentId: string, dataPoints: Array }[]}
253
+ * @param {THREE.Object3D} object - The parent component object
254
+ * @returns {{ label: string, deviceId: string, attachmentId: string, scopedAttachmentId: string, dataPoints: Array }[]}
234
255
  */
235
256
  }, {
236
257
  key: "_getIODevices",
237
258
  value: function _getIODevices(object) {
259
+ var _this2 = this;
238
260
  var devices = [];
261
+ var parentUuid = object.uuid; // The component's own UUID
239
262
  object.traverse(function (child) {
240
263
  var _child$userData;
241
264
  if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'io-device') {
265
+ var attachmentId = child.userData.attachmentId || '';
242
266
  devices.push({
243
267
  label: child.userData.attachmentLabel || child.name || child.userData.deviceId || 'Unknown Device',
244
268
  deviceId: child.userData.deviceId || '',
245
- attachmentId: child.userData.attachmentId || '',
269
+ attachmentId: attachmentId,
270
+ scopedAttachmentId: _this2._getScopedAttachmentKey(attachmentId, parentUuid),
246
271
  dataPoints: child.userData.dataPoints || [],
247
272
  direction: child.userData.ioDirection || 'output'
248
273
  });
@@ -258,7 +283,7 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
258
283
  }, {
259
284
  key: "_buildTooltip",
260
285
  value: function _buildTooltip(object) {
261
- var _this2 = this;
286
+ var _this3 = this;
262
287
  // Remove any existing tooltip first
263
288
  this.hide();
264
289
  // Re-assign selected object since hide() clears it
@@ -326,9 +351,10 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
326
351
  list.appendChild(item);
327
352
 
328
353
  // Data point rows (one per data point definition stored in userData)
329
- if (device.attachmentId && device.dataPoints.length > 0) {
354
+ // Use scopedAttachmentId to ensure state is isolated per component instance
355
+ if (device.scopedAttachmentId && device.dataPoints.length > 0) {
330
356
  device.dataPoints.forEach(function (dp) {
331
- var row = _this2._buildDataPointRow(device.attachmentId, dp, device.direction);
357
+ var row = _this3._buildDataPointRow(device.scopedAttachmentId, dp, device.direction, device.attachmentId);
332
358
  list.appendChild(row);
333
359
  });
334
360
  }
@@ -339,11 +365,11 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
339
365
  // Hover expand/collapse
340
366
  trigger.addEventListener('mouseenter', function () {
341
367
  ioSection.classList.add('expanded');
342
- _this2._ioExpanded = true;
368
+ _this3._ioExpanded = true;
343
369
  });
344
370
  ioSection.addEventListener('mouseleave', function () {
345
371
  ioSection.classList.remove('expanded');
346
- _this2._ioExpanded = false;
372
+ _this3._ioExpanded = false;
347
373
  });
348
374
  card.appendChild(ioSection);
349
375
  } else {
@@ -442,18 +468,19 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
442
468
  * Output / read-only direction → shows a state badge (updated each frame).
443
469
  * Input / bidirectional → shows an interactive control.
444
470
  *
445
- * @param {string} attachmentId
471
+ * @param {string} scopedAttachmentId - Scoped attachment ID (parentUuid::attachmentId) for state isolation
446
472
  * @param {Object} dp - data point definition from ioConfig.dataPoints
447
473
  * @param {string} [deviceDirection] - device-level direction ('input'|'output'), overrides dp.direction
474
+ * @param {string} [originalAttachmentId] - Original attachment ID for behavior triggering
448
475
  * @returns {HTMLElement}
449
476
  */
450
477
  }, {
451
478
  key: "_buildDataPointRow",
452
- value: function _buildDataPointRow(attachmentId, dp, deviceDirection) {
453
- var _ref,
479
+ value: function _buildDataPointRow(scopedAttachmentId, dp, deviceDirection, originalAttachmentId) {
480
+ var _ref2,
454
481
  _this$_stateAdapter$g,
455
482
  _this$_stateAdapter,
456
- _this3 = this;
483
+ _this4 = this;
457
484
  var row = document.createElement('div');
458
485
  row.className = 'cp-tooltip__dp-row';
459
486
  var nameEl = document.createElement('span');
@@ -461,20 +488,21 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
461
488
  nameEl.textContent = dp.name || dp.id || '?';
462
489
  row.appendChild(nameEl);
463
490
  var dpId = dp.id || dp.name;
464
- var key = "".concat(attachmentId, "::").concat(dpId);
491
+ var key = "".concat(scopedAttachmentId, "::").concat(dpId);
465
492
  // Device-level direction takes precedence; fall back to per-dp direction
466
493
  var resolvedDirection = deviceDirection || dp.direction || 'output';
467
494
  var isInput = resolvedDirection === 'input' || resolvedDirection === 'bidirectional';
468
- var currentVal = (_ref = (_this$_stateAdapter$g = (_this$_stateAdapter = this._stateAdapter) === null || _this$_stateAdapter === void 0 ? void 0 : _this$_stateAdapter.getState(attachmentId, dpId)) !== null && _this$_stateAdapter$g !== void 0 ? _this$_stateAdapter$g : dp.defaultValue) !== null && _ref !== void 0 ? _ref : null;
495
+ var currentVal = (_ref2 = (_this$_stateAdapter$g = (_this$_stateAdapter = this._stateAdapter) === null || _this$_stateAdapter === void 0 ? void 0 : _this$_stateAdapter.getState(scopedAttachmentId, dpId)) !== null && _this$_stateAdapter$g !== void 0 ? _this$_stateAdapter$g : dp.defaultValue) !== null && _ref2 !== void 0 ? _ref2 : null;
469
496
  if (isInput) {
470
497
  var ctrl = this._buildInputControl(dp, currentVal, function (newVal) {
471
- var _this3$_stateAdapter, _this3$selectedObject, _this3$sceneViewer;
472
- (_this3$_stateAdapter = _this3._stateAdapter) === null || _this3$_stateAdapter === void 0 || _this3$_stateAdapter.setState(attachmentId, dpId, newVal);
498
+ var _this4$_stateAdapter, _this4$selectedObject, _this4$sceneViewer;
499
+ (_this4$_stateAdapter = _this4._stateAdapter) === null || _this4$_stateAdapter === void 0 || _this4$_stateAdapter.setState(scopedAttachmentId, dpId, newVal);
473
500
  // Also fire BehaviorManager so any wired behaviors react immediately.
474
501
  // Pass the parent component UUID so behaviors scoped to a specific instance
475
502
  // don't bleed across clones that share the same attachmentId.
476
- var parentUuid = ((_this3$selectedObject = _this3.selectedObject) === null || _this3$selectedObject === void 0 ? void 0 : _this3$selectedObject.uuid) || null;
477
- (_this3$sceneViewer = _this3.sceneViewer) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.managers) === null || _this3$sceneViewer === void 0 || (_this3$sceneViewer = _this3$sceneViewer.behaviorManager) === null || _this3$sceneViewer === void 0 || _this3$sceneViewer.triggerState(attachmentId, dpId, newVal, parentUuid);
503
+ // Use originalAttachmentId for behavior triggering as behaviors are keyed by original ID
504
+ var parentUuid = ((_this4$selectedObject = _this4.selectedObject) === null || _this4$selectedObject === void 0 ? void 0 : _this4$selectedObject.uuid) || null;
505
+ (_this4$sceneViewer = _this4.sceneViewer) === null || _this4$sceneViewer === void 0 || (_this4$sceneViewer = _this4$sceneViewer.managers) === null || _this4$sceneViewer === void 0 || (_this4$sceneViewer = _this4$sceneViewer.behaviorManager) === null || _this4$sceneViewer === void 0 || _this4$sceneViewer.triggerState(originalAttachmentId || scopedAttachmentId, dpId, newVal, parentUuid);
478
506
  });
479
507
  row.appendChild(ctrl);
480
508
  this._stateElements.set(key, {
@@ -624,20 +652,23 @@ var ComponentTooltipManager = /*#__PURE__*/function (_BaseDisposable) {
624
652
  /**
625
653
  * Re-read all tracked read-only badge values from the state adapter.
626
654
  * Called each frame from update() — skips if no adapter is configured.
655
+ * Key format is scopedAttachmentId::dataPointId where scopedAttachmentId
656
+ * can be parentUuid::attachmentId, resulting in parentUuid::attachmentId::dataPointId
627
657
  */
628
658
  }, {
629
659
  key: "_refreshStateDisplays",
630
660
  value: function _refreshStateDisplays() {
631
- var _this4 = this;
661
+ var _this5 = this;
632
662
  if (!this._stateAdapter || !this._stateElements.size) return;
633
663
  this._stateElements.forEach(function (entry, key) {
634
664
  if (entry.isInput) return; // interactive controls are user-driven; don't overwrite
635
- var sepIdx = key.indexOf('::');
665
+ // Use lastIndexOf since scopedAttachmentId may contain '::' (parentUuid::attachmentId)
666
+ var sepIdx = key.lastIndexOf('::');
636
667
  if (sepIdx === -1) return;
637
- var attachmentId = key.slice(0, sepIdx);
668
+ var scopedAttachmentId = key.slice(0, sepIdx);
638
669
  var dataPointId = key.slice(sepIdx + 2);
639
- var val = _this4._stateAdapter.getState(attachmentId, dataPointId);
640
- _this4._applyBadgeValue(entry.el, val, entry.dp);
670
+ var val = _this5._stateAdapter.getState(scopedAttachmentId, dataPointId);
671
+ _this5._applyBadgeValue(entry.el, val, entry.dp);
641
672
  });
642
673
  }
643
674
  }]);
@@ -138,6 +138,12 @@ var SceneExportManager = /*#__PURE__*/function () {
138
138
  // Computed from geometry - not in input format
139
139
  'name',
140
140
  // Redundant with GLB node names - not in input format
141
+ 'addedTimestamp',
142
+ // Internal tracking - not needed in export
143
+ 'addedBy',
144
+ // Internal tracking - not needed in export
145
+ 'initialPosition',
146
+ // Internal tracking - not needed in export
141
147
  // Exclude internal segment tracking properties
142
148
  'segmentId',
143
149
  // Internal tracking
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2112-lab/central-plant",
3
- "version": "0.1.86",
3
+ "version": "0.1.87",
4
4
  "description": "Utility modules for the Central Plant Application",
5
5
  "main": "dist/bundle/index.js",
6
6
  "module": "dist/esm/src/index.js",