@2112-lab/central-plant 0.2.3 → 0.2.5

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.
@@ -28480,7 +28480,7 @@ var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
28480
28480
  if (_this4._testCondition(condition.when, value)) {
28481
28481
  if (Array.isArray(condition.actions)) {
28482
28482
  condition.actions.forEach(function (action) {
28483
- _this4._applyAction(output, action.set, action.value);
28483
+ _this4._applyAction(output, action.set, action.value, action.relative === true);
28484
28484
  });
28485
28485
  }
28486
28486
  }
@@ -28558,6 +28558,7 @@ var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
28558
28558
  }, {
28559
28559
  key: "_applyAction",
28560
28560
  value: function _applyAction(object, propertyPath, value) {
28561
+ var relative = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
28561
28562
  if (!object || typeof propertyPath !== 'string') return;
28562
28563
  var parts = propertyPath.split('.');
28563
28564
 
@@ -28583,6 +28584,15 @@ var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
28583
28584
  return;
28584
28585
  }
28585
28586
 
28587
+ // If relative, capture the mesh's original value on first execution and offset from it.
28588
+ if (relative) {
28589
+ var baselineKey = "_baseline_".concat(propertyPath.replace(/\./g, '_'));
28590
+ if (!(baselineKey in object.userData)) {
28591
+ object.userData[baselineKey] = target[lastKey];
28592
+ }
28593
+ value = object.userData[baselineKey] + parseFloat(value);
28594
+ }
28595
+
28586
28596
  // THREE.Color objects must be mutated via .set() rather than replaced
28587
28597
  var existing = target[lastKey];
28588
28598
  if (existing && existing.isColor && typeof value === 'string') {
@@ -28635,7 +28645,7 @@ function attachIODevicesToComponent(_x, _x2, _x3, _x4) {
28635
28645
  }
28636
28646
  function _attachIODevicesToComponent() {
28637
28647
  _attachIODevicesToComponent = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(componentModel, componentData, modelPreloader, parentComponentId) {
28638
- var attachedDevices, _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, _t, _t2;
28648
+ var attachedDevices, _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, _attachment$attachmen2, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, rot, deg2rad, _t, _t2;
28639
28649
  return _regenerator().w(function (_context) {
28640
28650
  while (1) switch (_context.n) {
28641
28651
  case 0:
@@ -28736,6 +28746,13 @@ function _attachIODevicesToComponent() {
28736
28746
  deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
28737
28747
  }
28738
28748
 
28749
+ // Apply face-based rotation (stored in degrees, XYZ Euler order)
28750
+ if ((_attachment$attachmen2 = attachment.attachmentPoint) !== null && _attachment$attachmen2 !== void 0 && _attachment$attachmen2.rotation) {
28751
+ rot = attachment.attachmentPoint.rotation;
28752
+ deg2rad = Math.PI / 180;
28753
+ deviceModel.rotation.set((rot.x || 0) * deg2rad, (rot.y || 0) * deg2rad, (rot.z || 0) * deg2rad, 'XYZ');
28754
+ }
28755
+
28739
28756
  // IO device models are authored at the same real-world unit scale
28740
28757
  // as the host component, so keep them at their natural (1:1) size.
28741
28758
  // Note: attachmentPoint.scale is the connector marker sphere size,
@@ -32575,7 +32592,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
32575
32592
  key: "_attachIODeviceModelsToPreview",
32576
32593
  value: (function () {
32577
32594
  var _attachIODeviceModelsToPreview2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(parentObject, componentData, modelPreloader) {
32578
- var _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, _t3;
32595
+ var _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, _attachment$attachmen2, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, rot, deg2rad, _t3;
32579
32596
  return _regenerator().w(function (_context3) {
32580
32597
  while (1) switch (_context3.n) {
32581
32598
  case 0:
@@ -32650,6 +32667,13 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
32650
32667
  deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
32651
32668
  }
32652
32669
 
32670
+ // Apply face-based rotation (stored in degrees, XYZ Euler order)
32671
+ if ((_attachment$attachmen2 = attachment.attachmentPoint) !== null && _attachment$attachmen2 !== void 0 && _attachment$attachmen2.rotation) {
32672
+ rot = attachment.attachmentPoint.rotation;
32673
+ deg2rad = Math.PI / 180;
32674
+ deviceModel.rotation.set((rot.x || 0) * deg2rad, (rot.y || 0) * deg2rad, (rot.z || 0) * deg2rad, 'XYZ');
32675
+ }
32676
+
32653
32677
  // IO device models use their natural (1:1) scale — the stored
32654
32678
  // attachmentPoint.scale value is for the connector marker sphere.
32655
32679
  deviceModel.scale.setScalar(1);
@@ -37214,7 +37238,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
37214
37238
  * Initialize the CentralPlant manager
37215
37239
  *
37216
37240
  * @constructor
37217
- * @version 0.2.3
37241
+ * @version 0.2.5
37218
37242
  * @updated 2025-10-22
37219
37243
  *
37220
37244
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -38248,6 +38272,69 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
38248
38272
  bm.triggerState(attachmentId, stateId, value, parentUuid);
38249
38273
  }
38250
38274
 
38275
+ /**
38276
+ * Set the state of an I/O device instance in the Three.js scene.
38277
+ *
38278
+ * This is the primary public API for driving IO device state changes from
38279
+ * external code (real-time data feeds, AI agents, automated tests, etc.).
38280
+ * It performs three coordinated actions in order:
38281
+ * 1. Persists the new value through the state adapter (Vuex in sandbox,
38282
+ * or any custom adapter injected via componentTooltipManager.configure())
38283
+ * so that the host application's store stays consistent.
38284
+ * 2. Evaluates all behaviors whose input matches this (attachmentId, stateId)
38285
+ * pair and applies the resulting property changes to Three.js meshes.
38286
+ * 3. Emits an 'io-device-state-changed' event on the sceneViewer so that
38287
+ * host applications without a Vuex store (e.g. cp3d-viewer) can react.
38288
+ *
38289
+ * @param {string} attachmentId - The attachment ID of the IO device (matches
38290
+ * the `attachmentId` stored in the Three.js object's userData)
38291
+ * @param {string} stateId - The data-point / state ID on the device (e.g. 'power', 'level')
38292
+ * @param {*} value - The new state value (boolean, number, string, etc.)
38293
+ * @param {string} [parentUuid] - UUID of the parent component instance.
38294
+ * Required when multiple instances of the same smart component share the
38295
+ * same attachmentId — prevents cross-instance state bleed.
38296
+ * @returns {boolean} True if the behavior system was reached; false if unavailable.
38297
+ * @example
38298
+ * // Toggle a push-button on a specific pump instance
38299
+ * centralPlant.setIoDeviceState('pump-push-button-01', 'power', true, pumpUuid)
38300
+ *
38301
+ * // Drive an analog level sensor (no parentUuid needed when only one instance)
38302
+ * centralPlant.setIoDeviceState('chiller-level-sensor-01', 'level', 0.75)
38303
+ */
38304
+ }, {
38305
+ key: "setIoDeviceState",
38306
+ value: function setIoDeviceState(attachmentId, stateId, value, parentUuid) {
38307
+ var _this$sceneViewer0, _this$sceneViewer1, _this$sceneViewer10;
38308
+ var bm = (_this$sceneViewer0 = this.sceneViewer) === null || _this$sceneViewer0 === void 0 || (_this$sceneViewer0 = _this$sceneViewer0.managers) === null || _this$sceneViewer0 === void 0 ? void 0 : _this$sceneViewer0.behaviorManager;
38309
+ if (!bm) {
38310
+ console.warn('⚠️ setIoDeviceState(): BehaviorManager not available');
38311
+ return false;
38312
+ }
38313
+
38314
+ // 1. Persist via state adapter if one has been configured
38315
+ var stateAdapter = (_this$sceneViewer1 = this.sceneViewer) === null || _this$sceneViewer1 === void 0 || (_this$sceneViewer1 = _this$sceneViewer1.managers) === null || _this$sceneViewer1 === void 0 || (_this$sceneViewer1 = _this$sceneViewer1.componentTooltipManager) === null || _this$sceneViewer1 === void 0 ? void 0 : _this$sceneViewer1._stateAdapter;
38316
+ if (stateAdapter !== null && stateAdapter !== void 0 && stateAdapter.setState) {
38317
+ var scopedKey = parentUuid ? "".concat(parentUuid, "::").concat(attachmentId) : attachmentId;
38318
+ try {
38319
+ stateAdapter.setState(scopedKey, stateId, value);
38320
+ } catch (err) {
38321
+ console.warn('⚠️ setIoDeviceState(): stateAdapter.setState() threw:', err);
38322
+ }
38323
+ }
38324
+
38325
+ // 2. Apply Three.js behavior changes
38326
+ bm.triggerState(attachmentId, stateId, value, parentUuid);
38327
+
38328
+ // 3. Emit event for host apps that don't use the state adapter (e.g. cp3d-viewer)
38329
+ (_this$sceneViewer10 = this.sceneViewer) === null || _this$sceneViewer10 === void 0 || _this$sceneViewer10.emit('io-device-state-changed', {
38330
+ attachmentId: attachmentId,
38331
+ stateId: stateId,
38332
+ value: value,
38333
+ parentUuid: parentUuid || null
38334
+ });
38335
+ return true;
38336
+ }
38337
+
38251
38338
  /**
38252
38339
  * Return all io-device attachments present in the current Three.js scene.
38253
38340
  * Each entry carries the attachmentId, an optional label, and the parent
@@ -38261,8 +38348,8 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
38261
38348
  }, {
38262
38349
  key: "getSceneAttachments",
38263
38350
  value: function getSceneAttachments() {
38264
- var _this$sceneViewer0;
38265
- var scene = (_this$sceneViewer0 = this.sceneViewer) === null || _this$sceneViewer0 === void 0 ? void 0 : _this$sceneViewer0.scene;
38351
+ var _this$sceneViewer11;
38352
+ var scene = (_this$sceneViewer11 = this.sceneViewer) === null || _this$sceneViewer11 === void 0 ? void 0 : _this$sceneViewer11.scene;
38266
38353
  if (!scene) return [];
38267
38354
  var results = [];
38268
38355
  scene.traverse(function (obj) {
@@ -19,7 +19,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
19
19
  * Initialize the CentralPlant manager
20
20
  *
21
21
  * @constructor
22
- * @version 0.2.3
22
+ * @version 0.2.5
23
23
  * @updated 2025-10-22
24
24
  *
25
25
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -1053,6 +1053,69 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
1053
1053
  bm.triggerState(attachmentId, stateId, value, parentUuid);
1054
1054
  }
1055
1055
 
1056
+ /**
1057
+ * Set the state of an I/O device instance in the Three.js scene.
1058
+ *
1059
+ * This is the primary public API for driving IO device state changes from
1060
+ * external code (real-time data feeds, AI agents, automated tests, etc.).
1061
+ * It performs three coordinated actions in order:
1062
+ * 1. Persists the new value through the state adapter (Vuex in sandbox,
1063
+ * or any custom adapter injected via componentTooltipManager.configure())
1064
+ * so that the host application's store stays consistent.
1065
+ * 2. Evaluates all behaviors whose input matches this (attachmentId, stateId)
1066
+ * pair and applies the resulting property changes to Three.js meshes.
1067
+ * 3. Emits an 'io-device-state-changed' event on the sceneViewer so that
1068
+ * host applications without a Vuex store (e.g. cp3d-viewer) can react.
1069
+ *
1070
+ * @param {string} attachmentId - The attachment ID of the IO device (matches
1071
+ * the `attachmentId` stored in the Three.js object's userData)
1072
+ * @param {string} stateId - The data-point / state ID on the device (e.g. 'power', 'level')
1073
+ * @param {*} value - The new state value (boolean, number, string, etc.)
1074
+ * @param {string} [parentUuid] - UUID of the parent component instance.
1075
+ * Required when multiple instances of the same smart component share the
1076
+ * same attachmentId — prevents cross-instance state bleed.
1077
+ * @returns {boolean} True if the behavior system was reached; false if unavailable.
1078
+ * @example
1079
+ * // Toggle a push-button on a specific pump instance
1080
+ * centralPlant.setIoDeviceState('pump-push-button-01', 'power', true, pumpUuid)
1081
+ *
1082
+ * // Drive an analog level sensor (no parentUuid needed when only one instance)
1083
+ * centralPlant.setIoDeviceState('chiller-level-sensor-01', 'level', 0.75)
1084
+ */
1085
+ }, {
1086
+ key: "setIoDeviceState",
1087
+ value: function setIoDeviceState(attachmentId, stateId, value, parentUuid) {
1088
+ var _this$sceneViewer0, _this$sceneViewer1, _this$sceneViewer10;
1089
+ var bm = (_this$sceneViewer0 = this.sceneViewer) === null || _this$sceneViewer0 === void 0 || (_this$sceneViewer0 = _this$sceneViewer0.managers) === null || _this$sceneViewer0 === void 0 ? void 0 : _this$sceneViewer0.behaviorManager;
1090
+ if (!bm) {
1091
+ console.warn('⚠️ setIoDeviceState(): BehaviorManager not available');
1092
+ return false;
1093
+ }
1094
+
1095
+ // 1. Persist via state adapter if one has been configured
1096
+ var stateAdapter = (_this$sceneViewer1 = this.sceneViewer) === null || _this$sceneViewer1 === void 0 || (_this$sceneViewer1 = _this$sceneViewer1.managers) === null || _this$sceneViewer1 === void 0 || (_this$sceneViewer1 = _this$sceneViewer1.componentTooltipManager) === null || _this$sceneViewer1 === void 0 ? void 0 : _this$sceneViewer1._stateAdapter;
1097
+ if (stateAdapter !== null && stateAdapter !== void 0 && stateAdapter.setState) {
1098
+ var scopedKey = parentUuid ? "".concat(parentUuid, "::").concat(attachmentId) : attachmentId;
1099
+ try {
1100
+ stateAdapter.setState(scopedKey, stateId, value);
1101
+ } catch (err) {
1102
+ console.warn('⚠️ setIoDeviceState(): stateAdapter.setState() threw:', err);
1103
+ }
1104
+ }
1105
+
1106
+ // 2. Apply Three.js behavior changes
1107
+ bm.triggerState(attachmentId, stateId, value, parentUuid);
1108
+
1109
+ // 3. Emit event for host apps that don't use the state adapter (e.g. cp3d-viewer)
1110
+ (_this$sceneViewer10 = this.sceneViewer) === null || _this$sceneViewer10 === void 0 || _this$sceneViewer10.emit('io-device-state-changed', {
1111
+ attachmentId: attachmentId,
1112
+ stateId: stateId,
1113
+ value: value,
1114
+ parentUuid: parentUuid || null
1115
+ });
1116
+ return true;
1117
+ }
1118
+
1056
1119
  /**
1057
1120
  * Return all io-device attachments present in the current Three.js scene.
1058
1121
  * Each entry carries the attachmentId, an optional label, and the parent
@@ -1066,8 +1129,8 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
1066
1129
  }, {
1067
1130
  key: "getSceneAttachments",
1068
1131
  value: function getSceneAttachments() {
1069
- var _this$sceneViewer0;
1070
- var scene = (_this$sceneViewer0 = this.sceneViewer) === null || _this$sceneViewer0 === void 0 ? void 0 : _this$sceneViewer0.scene;
1132
+ var _this$sceneViewer11;
1133
+ var scene = (_this$sceneViewer11 = this.sceneViewer) === null || _this$sceneViewer11 === void 0 ? void 0 : _this$sceneViewer11.scene;
1071
1134
  if (!scene) return [];
1072
1135
  var results = [];
1073
1136
  scene.traverse(function (obj) {
@@ -264,7 +264,7 @@ var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
264
264
  if (_this4._testCondition(condition.when, value)) {
265
265
  if (Array.isArray(condition.actions)) {
266
266
  condition.actions.forEach(function (action) {
267
- _this4._applyAction(output, action.set, action.value);
267
+ _this4._applyAction(output, action.set, action.value, action.relative === true);
268
268
  });
269
269
  }
270
270
  }
@@ -342,6 +342,7 @@ var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
342
342
  }, {
343
343
  key: "_applyAction",
344
344
  value: function _applyAction(object, propertyPath, value) {
345
+ var relative = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
345
346
  if (!object || typeof propertyPath !== 'string') return;
346
347
  var parts = propertyPath.split('.');
347
348
 
@@ -367,6 +368,15 @@ var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
367
368
  return;
368
369
  }
369
370
 
371
+ // If relative, capture the mesh's original value on first execution and offset from it.
372
+ if (relative) {
373
+ var baselineKey = "_baseline_".concat(propertyPath.replace(/\./g, '_'));
374
+ if (!(baselineKey in object.userData)) {
375
+ object.userData[baselineKey] = target[lastKey];
376
+ }
377
+ value = object.userData[baselineKey] + parseFloat(value);
378
+ }
379
+
370
380
  // THREE.Color objects must be mutated via .set() rather than replaced
371
381
  var existing = target[lastKey];
372
382
  if (existing && existing.isColor && typeof value === 'string') {
@@ -345,7 +345,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
345
345
  key: "_attachIODeviceModelsToPreview",
346
346
  value: (function () {
347
347
  var _attachIODeviceModelsToPreview2 = _rollupPluginBabelHelpers.asyncToGenerator(/*#__PURE__*/_rollupPluginBabelHelpers.regenerator().m(function _callee3(parentObject, componentData, modelPreloader) {
348
- var _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, _t3;
348
+ var _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, _attachment$attachmen2, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, rot, deg2rad, _t3;
349
349
  return _rollupPluginBabelHelpers.regenerator().w(function (_context3) {
350
350
  while (1) switch (_context3.n) {
351
351
  case 0:
@@ -420,6 +420,13 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
420
420
  deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
421
421
  }
422
422
 
423
+ // Apply face-based rotation (stored in degrees, XYZ Euler order)
424
+ if ((_attachment$attachmen2 = attachment.attachmentPoint) !== null && _attachment$attachmen2 !== void 0 && _attachment$attachmen2.rotation) {
425
+ rot = attachment.attachmentPoint.rotation;
426
+ deg2rad = Math.PI / 180;
427
+ deviceModel.rotation.set((rot.x || 0) * deg2rad, (rot.y || 0) * deg2rad, (rot.z || 0) * deg2rad, 'XYZ');
428
+ }
429
+
423
430
  // IO device models use their natural (1:1) scale — the stored
424
431
  // attachmentPoint.scale value is for the connector marker sphere.
425
432
  deviceModel.scale.setScalar(1);
@@ -27,7 +27,7 @@ function attachIODevicesToComponent(_x, _x2, _x3, _x4) {
27
27
  }
28
28
  function _attachIODevicesToComponent() {
29
29
  _attachIODevicesToComponent = _rollupPluginBabelHelpers.asyncToGenerator(/*#__PURE__*/_rollupPluginBabelHelpers.regenerator().m(function _callee(componentModel, componentData, modelPreloader, parentComponentId) {
30
- var attachedDevices, _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, _t, _t2;
30
+ var attachedDevices, _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, _attachment$attachmen2, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, rot, deg2rad, _t, _t2;
31
31
  return _rollupPluginBabelHelpers.regenerator().w(function (_context) {
32
32
  while (1) switch (_context.n) {
33
33
  case 0:
@@ -128,6 +128,13 @@ function _attachIODevicesToComponent() {
128
128
  deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
129
129
  }
130
130
 
131
+ // Apply face-based rotation (stored in degrees, XYZ Euler order)
132
+ if ((_attachment$attachmen2 = attachment.attachmentPoint) !== null && _attachment$attachmen2 !== void 0 && _attachment$attachmen2.rotation) {
133
+ rot = attachment.attachmentPoint.rotation;
134
+ deg2rad = Math.PI / 180;
135
+ deviceModel.rotation.set((rot.x || 0) * deg2rad, (rot.y || 0) * deg2rad, (rot.z || 0) * deg2rad, 'XYZ');
136
+ }
137
+
131
138
  // IO device models are authored at the same real-world unit scale
132
139
  // as the host component, so keep them at their natural (1:1) size.
133
140
  // Note: attachmentPoint.scale is the connector marker sphere size,
@@ -15,7 +15,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
15
15
  * Initialize the CentralPlant manager
16
16
  *
17
17
  * @constructor
18
- * @version 0.2.3
18
+ * @version 0.2.5
19
19
  * @updated 2025-10-22
20
20
  *
21
21
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -1049,6 +1049,69 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
1049
1049
  bm.triggerState(attachmentId, stateId, value, parentUuid);
1050
1050
  }
1051
1051
 
1052
+ /**
1053
+ * Set the state of an I/O device instance in the Three.js scene.
1054
+ *
1055
+ * This is the primary public API for driving IO device state changes from
1056
+ * external code (real-time data feeds, AI agents, automated tests, etc.).
1057
+ * It performs three coordinated actions in order:
1058
+ * 1. Persists the new value through the state adapter (Vuex in sandbox,
1059
+ * or any custom adapter injected via componentTooltipManager.configure())
1060
+ * so that the host application's store stays consistent.
1061
+ * 2. Evaluates all behaviors whose input matches this (attachmentId, stateId)
1062
+ * pair and applies the resulting property changes to Three.js meshes.
1063
+ * 3. Emits an 'io-device-state-changed' event on the sceneViewer so that
1064
+ * host applications without a Vuex store (e.g. cp3d-viewer) can react.
1065
+ *
1066
+ * @param {string} attachmentId - The attachment ID of the IO device (matches
1067
+ * the `attachmentId` stored in the Three.js object's userData)
1068
+ * @param {string} stateId - The data-point / state ID on the device (e.g. 'power', 'level')
1069
+ * @param {*} value - The new state value (boolean, number, string, etc.)
1070
+ * @param {string} [parentUuid] - UUID of the parent component instance.
1071
+ * Required when multiple instances of the same smart component share the
1072
+ * same attachmentId — prevents cross-instance state bleed.
1073
+ * @returns {boolean} True if the behavior system was reached; false if unavailable.
1074
+ * @example
1075
+ * // Toggle a push-button on a specific pump instance
1076
+ * centralPlant.setIoDeviceState('pump-push-button-01', 'power', true, pumpUuid)
1077
+ *
1078
+ * // Drive an analog level sensor (no parentUuid needed when only one instance)
1079
+ * centralPlant.setIoDeviceState('chiller-level-sensor-01', 'level', 0.75)
1080
+ */
1081
+ }, {
1082
+ key: "setIoDeviceState",
1083
+ value: function setIoDeviceState(attachmentId, stateId, value, parentUuid) {
1084
+ var _this$sceneViewer0, _this$sceneViewer1, _this$sceneViewer10;
1085
+ var bm = (_this$sceneViewer0 = this.sceneViewer) === null || _this$sceneViewer0 === void 0 || (_this$sceneViewer0 = _this$sceneViewer0.managers) === null || _this$sceneViewer0 === void 0 ? void 0 : _this$sceneViewer0.behaviorManager;
1086
+ if (!bm) {
1087
+ console.warn('⚠️ setIoDeviceState(): BehaviorManager not available');
1088
+ return false;
1089
+ }
1090
+
1091
+ // 1. Persist via state adapter if one has been configured
1092
+ var stateAdapter = (_this$sceneViewer1 = this.sceneViewer) === null || _this$sceneViewer1 === void 0 || (_this$sceneViewer1 = _this$sceneViewer1.managers) === null || _this$sceneViewer1 === void 0 || (_this$sceneViewer1 = _this$sceneViewer1.componentTooltipManager) === null || _this$sceneViewer1 === void 0 ? void 0 : _this$sceneViewer1._stateAdapter;
1093
+ if (stateAdapter !== null && stateAdapter !== void 0 && stateAdapter.setState) {
1094
+ var scopedKey = parentUuid ? "".concat(parentUuid, "::").concat(attachmentId) : attachmentId;
1095
+ try {
1096
+ stateAdapter.setState(scopedKey, stateId, value);
1097
+ } catch (err) {
1098
+ console.warn('⚠️ setIoDeviceState(): stateAdapter.setState() threw:', err);
1099
+ }
1100
+ }
1101
+
1102
+ // 2. Apply Three.js behavior changes
1103
+ bm.triggerState(attachmentId, stateId, value, parentUuid);
1104
+
1105
+ // 3. Emit event for host apps that don't use the state adapter (e.g. cp3d-viewer)
1106
+ (_this$sceneViewer10 = this.sceneViewer) === null || _this$sceneViewer10 === void 0 || _this$sceneViewer10.emit('io-device-state-changed', {
1107
+ attachmentId: attachmentId,
1108
+ stateId: stateId,
1109
+ value: value,
1110
+ parentUuid: parentUuid || null
1111
+ });
1112
+ return true;
1113
+ }
1114
+
1052
1115
  /**
1053
1116
  * Return all io-device attachments present in the current Three.js scene.
1054
1117
  * Each entry carries the attachmentId, an optional label, and the parent
@@ -1062,8 +1125,8 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
1062
1125
  }, {
1063
1126
  key: "getSceneAttachments",
1064
1127
  value: function getSceneAttachments() {
1065
- var _this$sceneViewer0;
1066
- var scene = (_this$sceneViewer0 = this.sceneViewer) === null || _this$sceneViewer0 === void 0 ? void 0 : _this$sceneViewer0.scene;
1128
+ var _this$sceneViewer11;
1129
+ var scene = (_this$sceneViewer11 = this.sceneViewer) === null || _this$sceneViewer11 === void 0 ? void 0 : _this$sceneViewer11.scene;
1067
1130
  if (!scene) return [];
1068
1131
  var results = [];
1069
1132
  scene.traverse(function (obj) {
@@ -260,7 +260,7 @@ var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
260
260
  if (_this4._testCondition(condition.when, value)) {
261
261
  if (Array.isArray(condition.actions)) {
262
262
  condition.actions.forEach(function (action) {
263
- _this4._applyAction(output, action.set, action.value);
263
+ _this4._applyAction(output, action.set, action.value, action.relative === true);
264
264
  });
265
265
  }
266
266
  }
@@ -338,6 +338,7 @@ var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
338
338
  }, {
339
339
  key: "_applyAction",
340
340
  value: function _applyAction(object, propertyPath, value) {
341
+ var relative = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
341
342
  if (!object || typeof propertyPath !== 'string') return;
342
343
  var parts = propertyPath.split('.');
343
344
 
@@ -363,6 +364,15 @@ var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
363
364
  return;
364
365
  }
365
366
 
367
+ // If relative, capture the mesh's original value on first execution and offset from it.
368
+ if (relative) {
369
+ var baselineKey = "_baseline_".concat(propertyPath.replace(/\./g, '_'));
370
+ if (!(baselineKey in object.userData)) {
371
+ object.userData[baselineKey] = target[lastKey];
372
+ }
373
+ value = object.userData[baselineKey] + parseFloat(value);
374
+ }
375
+
366
376
  // THREE.Color objects must be mutated via .set() rather than replaced
367
377
  var existing = target[lastKey];
368
378
  if (existing && existing.isColor && typeof value === 'string') {
@@ -321,7 +321,7 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
321
321
  key: "_attachIODeviceModelsToPreview",
322
322
  value: (function () {
323
323
  var _attachIODeviceModelsToPreview2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(parentObject, componentData, modelPreloader) {
324
- var _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, _t3;
324
+ var _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, _attachment$attachmen2, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, rot, deg2rad, _t3;
325
325
  return _regenerator().w(function (_context3) {
326
326
  while (1) switch (_context3.n) {
327
327
  case 0:
@@ -396,6 +396,13 @@ var ComponentDragManager = /*#__PURE__*/function (_BaseDisposable) {
396
396
  deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
397
397
  }
398
398
 
399
+ // Apply face-based rotation (stored in degrees, XYZ Euler order)
400
+ if ((_attachment$attachmen2 = attachment.attachmentPoint) !== null && _attachment$attachmen2 !== void 0 && _attachment$attachmen2.rotation) {
401
+ rot = attachment.attachmentPoint.rotation;
402
+ deg2rad = Math.PI / 180;
403
+ deviceModel.rotation.set((rot.x || 0) * deg2rad, (rot.y || 0) * deg2rad, (rot.z || 0) * deg2rad, 'XYZ');
404
+ }
405
+
399
406
  // IO device models use their natural (1:1) scale — the stored
400
407
  // attachmentPoint.scale value is for the connector marker sphere.
401
408
  deviceModel.scale.setScalar(1);
@@ -23,7 +23,7 @@ function attachIODevicesToComponent(_x, _x2, _x3, _x4) {
23
23
  }
24
24
  function _attachIODevicesToComponent() {
25
25
  _attachIODevicesToComponent = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(componentModel, componentData, modelPreloader, parentComponentId) {
26
- var attachedDevices, _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, _t, _t2;
26
+ var attachedDevices, _i, _Object$entries, _Object$entries$_i, attachmentId, attachment, _modelPreloader$compo, _deviceData$ioConfig, _deviceData$ioConfig2, _deviceData$ioConfig3, _attachment$attachmen, _attachment$attachmen2, deviceData, cachedDevice, _modelPreloader$loadi, deviceModel, pos, rot, deg2rad, _t, _t2;
27
27
  return _regenerator().w(function (_context) {
28
28
  while (1) switch (_context.n) {
29
29
  case 0:
@@ -124,6 +124,13 @@ function _attachIODevicesToComponent() {
124
124
  deviceModel.position.set(pos.x || 0, pos.y || 0, pos.z || 0);
125
125
  }
126
126
 
127
+ // Apply face-based rotation (stored in degrees, XYZ Euler order)
128
+ if ((_attachment$attachmen2 = attachment.attachmentPoint) !== null && _attachment$attachmen2 !== void 0 && _attachment$attachmen2.rotation) {
129
+ rot = attachment.attachmentPoint.rotation;
130
+ deg2rad = Math.PI / 180;
131
+ deviceModel.rotation.set((rot.x || 0) * deg2rad, (rot.y || 0) * deg2rad, (rot.z || 0) * deg2rad, 'XYZ');
132
+ }
133
+
127
134
  // IO device models are authored at the same real-world unit scale
128
135
  // as the host component, so keep them at their natural (1:1) size.
129
136
  // Note: attachmentPoint.scale is the connector marker sphere size,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2112-lab/central-plant",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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",