@2112-lab/central-plant 0.2.4 → 0.2.8

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);
@@ -36018,6 +36042,59 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36018
36042
  }]);
36019
36043
  }(BaseDisposable);
36020
36044
 
36045
+ // ─────────────────────────────────────────────────────────────────────────────
36046
+ // Flow-direction helpers (module-level)
36047
+ // ─────────────────────────────────────────────────────────────────────────────
36048
+
36049
+ /**
36050
+ * Returns the flow direction of a connector from the current scene data.
36051
+ * @param {Object} sceneData - currentSceneData object
36052
+ * @param {string} connectorId
36053
+ * @returns {'in'|'out'|'bi'} Defaults to 'bi' if not set.
36054
+ */
36055
+ function _getConnectorFlow(sceneData, connectorId) {
36056
+ var _sceneData$scene;
36057
+ var children = (sceneData === null || sceneData === void 0 || (_sceneData$scene = sceneData.scene) === null || _sceneData$scene === void 0 ? void 0 : _sceneData$scene.children) || [];
36058
+ var _iterator = _createForOfIteratorHelper(children),
36059
+ _step;
36060
+ try {
36061
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
36062
+ var component = _step.value;
36063
+ var _iterator2 = _createForOfIteratorHelper(component.children || []),
36064
+ _step2;
36065
+ try {
36066
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
36067
+ var _child$userData;
36068
+ var child = _step2.value;
36069
+ if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'connector' && child.uuid === connectorId) {
36070
+ return child.userData.flow || 'bi';
36071
+ }
36072
+ }
36073
+ } catch (err) {
36074
+ _iterator2.e(err);
36075
+ } finally {
36076
+ _iterator2.f();
36077
+ }
36078
+ }
36079
+ } catch (err) {
36080
+ _iterator.e(err);
36081
+ } finally {
36082
+ _iterator.f();
36083
+ }
36084
+ return 'bi';
36085
+ }
36086
+
36087
+ /**
36088
+ * Returns true if fromFlow → toFlow is a valid connection.
36089
+ * @param {'in'|'out'|'bi'} fromFlow
36090
+ * @param {'in'|'out'|'bi'} toFlow
36091
+ * @returns {boolean}
36092
+ */
36093
+ function _areFlowsCompatible$1(fromFlow, toFlow) {
36094
+ if (fromFlow === 'bi' || toFlow === 'bi') return true;
36095
+ return fromFlow !== toFlow;
36096
+ }
36097
+
36021
36098
  /**
36022
36099
  * CentralPlantInternals class containing internal methods and functionality
36023
36100
  */
@@ -36469,12 +36546,12 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36469
36546
  console.log("\uD83D\uDD27 Translating ".concat(selectedObjects.length, " selected object(s) on ").concat(axis, " axis by ").concat(value));
36470
36547
 
36471
36548
  // Translate each selected object using the appropriate method
36472
- var _iterator = _createForOfIteratorHelper(selectedObjects),
36473
- _step;
36549
+ var _iterator3 = _createForOfIteratorHelper(selectedObjects),
36550
+ _step3;
36474
36551
  try {
36475
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
36552
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
36476
36553
  var _obj$userData;
36477
- var obj = _step.value;
36554
+ var obj = _step3.value;
36478
36555
  var objectType = (_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType;
36479
36556
  var objectId = obj.uuid;
36480
36557
  var success = false;
@@ -36501,9 +36578,9 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36501
36578
  }
36502
36579
  }
36503
36580
  } catch (err) {
36504
- _iterator.e(err);
36581
+ _iterator3.e(err);
36505
36582
  } finally {
36506
- _iterator.f();
36583
+ _iterator3.f();
36507
36584
  }
36508
36585
  result.success = result.translatedCount === result.totalCount;
36509
36586
  if (result.success) {
@@ -36687,7 +36764,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36687
36764
  }, {
36688
36765
  key: "addConnection",
36689
36766
  value: function addConnection(fromConnectorId, toConnectorId) {
36690
- var _this$centralPlant$sc4;
36767
+ var _this$centralPlant$sc4, _this$centralPlant$sc5;
36691
36768
  // Use centralized validation for connection parameters
36692
36769
  var existingConnections = ((_this$centralPlant$sc4 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc4 === void 0 || (_this$centralPlant$sc4 = _this$centralPlant$sc4.currentSceneData) === null || _this$centralPlant$sc4 === void 0 ? void 0 : _this$centralPlant$sc4.connections) || [];
36693
36770
  var validation = this.validator.validateConnectionParams(fromConnectorId, toConnectorId, existingConnections);
@@ -36695,6 +36772,17 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36695
36772
  return false; // Validator already logged the error
36696
36773
  }
36697
36774
 
36775
+ // Validate flow direction compatibility
36776
+ var sceneData = (_this$centralPlant$sc5 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc5 === void 0 ? void 0 : _this$centralPlant$sc5.currentSceneData;
36777
+ if (sceneData) {
36778
+ var fromFlow = _getConnectorFlow(sceneData, fromConnectorId);
36779
+ var toFlow = _getConnectorFlow(sceneData, toConnectorId);
36780
+ if (!_areFlowsCompatible$1(fromFlow, toFlow)) {
36781
+ console.error("\u274C addConnection(): Incompatible flow directions \u2014 '".concat(fromConnectorId, "' is '").concat(fromFlow, "' and '").concat(toConnectorId, "' is '").concat(toFlow, "'. ") + "'".concat(fromFlow, "' \u2192 '").concat(toFlow, "' connections are not allowed."));
36782
+ return false;
36783
+ }
36784
+ }
36785
+
36698
36786
  // Validate scene availability
36699
36787
  var sceneValidation = this.validator.validateSceneViewer(this.centralPlant.sceneViewer);
36700
36788
  if (!sceneValidation.isValid) {
@@ -36815,7 +36903,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36815
36903
  }, {
36816
36904
  key: "addComponent",
36817
36905
  value: function addComponent(libraryId) {
36818
- var _this$centralPlant$sc5;
36906
+ var _this$centralPlant$sc6;
36819
36907
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
36820
36908
  // Use centralized validation for component addition parameters
36821
36909
  var existingIds = this.getComponentIds();
@@ -36825,7 +36913,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36825
36913
  }
36826
36914
 
36827
36915
  // Validate scene availability
36828
- var sceneValidation = this.validator.validateSceneViewer(this.centralPlant.sceneViewer, (_this$centralPlant$sc5 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc5 === void 0 ? void 0 : _this$centralPlant$sc5.scene);
36916
+ var sceneValidation = this.validator.validateSceneViewer(this.centralPlant.sceneViewer, (_this$centralPlant$sc6 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc6 === void 0 ? void 0 : _this$centralPlant$sc6.scene);
36829
36917
  if (!sceneValidation.isValid) {
36830
36918
  return false;
36831
36919
  }
@@ -36844,7 +36932,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36844
36932
  return false;
36845
36933
  }
36846
36934
  try {
36847
- var _componentData$childr, _componentData$childr2, _this$centralPlant$sc6, _componentData$childr3, _componentData$defaul;
36935
+ var _componentData$childr, _componentData$childr2, _this$centralPlant$sc7, _componentData$childr3, _componentData$defaul;
36848
36936
  // Generate a unique component ID if not provided
36849
36937
  var componentId = options.customId || this.generateUniqueComponentId(libraryId);
36850
36938
 
@@ -36956,7 +37044,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36956
37044
  componentModel.updateMatrixWorld(true);
36957
37045
 
36958
37046
  // Check if component is underground and fix if needed (based on settings)
36959
- var checkUnderground = (_this$centralPlant$sc6 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc6 === void 0 || (_this$centralPlant$sc6 = _this$centralPlant$sc6.managers) === null || _this$centralPlant$sc6 === void 0 || (_this$centralPlant$sc6 = _this$centralPlant$sc6.settingsManager) === null || _this$centralPlant$sc6 === void 0 ? void 0 : _this$centralPlant$sc6.getSetting('scene', 'checkUnderground');
37047
+ var checkUnderground = (_this$centralPlant$sc7 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc7 === void 0 || (_this$centralPlant$sc7 = _this$centralPlant$sc7.managers) === null || _this$centralPlant$sc7 === void 0 || (_this$centralPlant$sc7 = _this$centralPlant$sc7.settingsManager) === null || _this$centralPlant$sc7 === void 0 ? void 0 : _this$centralPlant$sc7.getSetting('scene', 'checkUnderground');
36960
37048
  if (checkUnderground) {
36961
37049
  var wasFixed = this.fixUndergroundComponent(componentModel);
36962
37050
  if (wasFixed) {
@@ -37054,8 +37142,8 @@ var CentralPlantInternals = /*#__PURE__*/function () {
37054
37142
  // responds to tooltip-driven state changes immediately after drop.
37055
37143
  // (The scene-load path uses _processBehaviors instead, which runs on loadSceneData.)
37056
37144
  if ((_componentData$defaul = componentData.defaultBehaviors) !== null && _componentData$defaul !== void 0 && _componentData$defaul.length) {
37057
- var _this$centralPlant$sc7, _som$registerBehavior;
37058
- var som = (_this$centralPlant$sc7 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc7 === void 0 ? void 0 : _this$centralPlant$sc7.sceneOperationsManager;
37145
+ var _this$centralPlant$sc8, _som$registerBehavior;
37146
+ var som = (_this$centralPlant$sc8 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc8 === void 0 ? void 0 : _this$centralPlant$sc8.sceneOperationsManager;
37059
37147
  som === null || som === void 0 || (_som$registerBehavior = som.registerBehaviorsForComponentInstance) === null || _som$registerBehavior === void 0 || _som$registerBehavior.call(som, componentData, componentId);
37060
37148
  }
37061
37149
 
@@ -37117,9 +37205,9 @@ var CentralPlantInternals = /*#__PURE__*/function () {
37117
37205
  }, {
37118
37206
  key: "deleteComponent",
37119
37207
  value: function deleteComponent(componentId) {
37120
- var _this$centralPlant$sc8;
37208
+ var _this$centralPlant$sc9;
37121
37209
  // Check if component manager is available
37122
- var componentManager = (_this$centralPlant$sc8 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc8 === void 0 ? void 0 : _this$centralPlant$sc8.componentManager;
37210
+ var componentManager = (_this$centralPlant$sc9 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc9 === void 0 ? void 0 : _this$centralPlant$sc9.componentManager;
37123
37211
  if (!componentManager) {
37124
37212
  console.error('❌ deleteComponent(): Component manager not available');
37125
37213
  return false;
@@ -37194,8 +37282,8 @@ var CentralPlantInternals = /*#__PURE__*/function () {
37194
37282
  }
37195
37283
  var componentIds = [];
37196
37284
  this.centralPlant.sceneViewer.scene.traverse(function (child) {
37197
- var _child$userData;
37198
- if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'component') {
37285
+ var _child$userData2;
37286
+ if (((_child$userData2 = child.userData) === null || _child$userData2 === void 0 ? void 0 : _child$userData2.objectType) === 'component') {
37199
37287
  componentIds.push(child.uuid || child.userData.originalUuid);
37200
37288
  }
37201
37289
  });
@@ -37204,6 +37292,22 @@ var CentralPlantInternals = /*#__PURE__*/function () {
37204
37292
  }]);
37205
37293
  }();
37206
37294
 
37295
+ // ─────────────────────────────────────────────────────────────────────────────
37296
+ // Flow-direction compatibility helper (module-level, no class dependency)
37297
+ // ─────────────────────────────────────────────────────────────────────────────
37298
+
37299
+ /**
37300
+ * Returns true if the two flow directions are compatible for a connection.
37301
+ * @param {string} fromFlow - 'in' | 'out' | 'bi'
37302
+ * @param {string} toFlow - 'in' | 'out' | 'bi'
37303
+ * @returns {boolean}
37304
+ */
37305
+ function _areFlowsCompatible(fromFlow, toFlow) {
37306
+ if (fromFlow === 'bi' || toFlow === 'bi') return true;
37307
+ // in ↔ out are compatible; in → in and out → out are not
37308
+ return fromFlow !== toFlow;
37309
+ }
37310
+
37207
37311
  /**
37208
37312
  * CentralPlant class that manages all scene utility instances and provides public API
37209
37313
  *
@@ -37214,7 +37318,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
37214
37318
  * Initialize the CentralPlant manager
37215
37319
  *
37216
37320
  * @constructor
37217
- * @version 0.2.4
37321
+ * @version 0.2.8
37218
37322
  * @updated 2025-10-22
37219
37323
  *
37220
37324
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -38147,6 +38251,107 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
38147
38251
  return availableConnectorIds;
38148
38252
  }
38149
38253
 
38254
+ /**
38255
+ * Get available connectors with their flow direction metadata.
38256
+ * Same filtering logic as getAvailableConnections() but returns objects instead of strings.
38257
+ * @returns {Array<{id: string, flow: string}>} Array of connector info objects.
38258
+ * flow is one of 'in', 'out', 'bi'. Defaults to 'bi' if not set in userData.
38259
+ * @example
38260
+ * const infos = centralPlant.getAvailableConnectionsInfo()
38261
+ * // [{ id: 'PUMP-1-CONNECTOR-1', flow: 'out' }, ...]
38262
+ */
38263
+ }, {
38264
+ key: "getAvailableConnectionsInfo",
38265
+ value: function getAvailableConnectionsInfo() {
38266
+ if (!this.sceneViewer || !this.sceneViewer.currentSceneData) {
38267
+ console.warn('⚠️ getAvailableConnectionsInfo(): Scene viewer or current scene data not available');
38268
+ return [];
38269
+ }
38270
+ var sceneData = this.sceneViewer.currentSceneData;
38271
+ if (!sceneData.scene || !sceneData.scene.children) {
38272
+ console.warn('⚠️ getAvailableConnectionsInfo(): Invalid scene data structure');
38273
+ return [];
38274
+ }
38275
+ var allConnectorInfos = [];
38276
+ sceneData.scene.children.forEach(function (component) {
38277
+ if (component.children && Array.isArray(component.children)) {
38278
+ component.children.forEach(function (child) {
38279
+ if (child.userData && child.userData.objectType === 'connector' && child.uuid) {
38280
+ allConnectorInfos.push({
38281
+ id: child.uuid,
38282
+ flow: child.userData.flow || 'bi'
38283
+ });
38284
+ }
38285
+ });
38286
+ }
38287
+ });
38288
+ var existingConnections = this.getConnections();
38289
+ var usedConnectorIds = new Set();
38290
+ existingConnections.forEach(function (connection) {
38291
+ if (connection.from) usedConnectorIds.add(connection.from);
38292
+ if (connection.to) usedConnectorIds.add(connection.to);
38293
+ });
38294
+ return allConnectorInfos.filter(function (info) {
38295
+ return !usedConnectorIds.has(info.id);
38296
+ });
38297
+ }
38298
+
38299
+ /**
38300
+ * Validate all connections in the current scene for flow direction compatibility.
38301
+ * @returns {{ valid: Array<Object>, invalid: Array<{connection: Object, reason: string}> }}
38302
+ * @example
38303
+ * const result = centralPlant.validateConnections()
38304
+ * result.invalid.forEach(({ connection, reason }) => console.warn(reason, connection))
38305
+ */
38306
+ }, {
38307
+ key: "validateConnections",
38308
+ value: function validateConnections() {
38309
+ if (!this.sceneViewer || !this.sceneViewer.currentSceneData) {
38310
+ console.warn('⚠️ validateConnections(): Scene viewer or current scene data not available');
38311
+ return {
38312
+ valid: [],
38313
+ invalid: []
38314
+ };
38315
+ }
38316
+ var connections = this.getConnections();
38317
+ var sceneData = this.sceneViewer.currentSceneData;
38318
+
38319
+ // Build lookup map: connectorId → flow
38320
+ var flowMap = {};
38321
+ var scene = sceneData.scene || {};
38322
+ var children = scene.children || [];
38323
+ children.forEach(function (component) {
38324
+ if (component.children && Array.isArray(component.children)) {
38325
+ component.children.forEach(function (child) {
38326
+ if (child.userData && child.userData.objectType === 'connector' && child.uuid) {
38327
+ flowMap[child.uuid] = child.userData.flow || 'bi';
38328
+ }
38329
+ });
38330
+ }
38331
+ });
38332
+ var valid = [];
38333
+ var invalid = [];
38334
+ connections.forEach(function (connection) {
38335
+ var fromFlow = flowMap[connection.from] || 'bi';
38336
+ var toFlow = flowMap[connection.to] || 'bi';
38337
+ if (_areFlowsCompatible(fromFlow, toFlow)) {
38338
+ valid.push(connection);
38339
+ } else {
38340
+ var reason = "Incompatible flow directions: connector '".concat(connection.from, "' is '").concat(fromFlow, "' and connector '").concat(connection.to, "' is '").concat(toFlow, "' \u2014 ").concat(fromFlow, " \u2192 ").concat(toFlow, " is not allowed");
38341
+ console.warn("\u26A0\uFE0F validateConnections(): ".concat(reason));
38342
+ invalid.push({
38343
+ connection: connection,
38344
+ reason: reason
38345
+ });
38346
+ }
38347
+ });
38348
+ console.log("\u2705 validateConnections(): ".concat(valid.length, " valid, ").concat(invalid.length, " invalid connections"));
38349
+ return {
38350
+ valid: valid,
38351
+ invalid: invalid
38352
+ };
38353
+ }
38354
+
38150
38355
  // ─────────────────────────────────────────────────────────────────────────
38151
38356
  // BEHAVIORS API
38152
38357
  // ─────────────────────────────────────────────────────────────────────────
@@ -9,6 +9,22 @@ var DisposalUtilities = require('../utils/DisposalUtilities.js');
9
9
  var centralPlantInternals = require('./centralPlantInternals.js');
10
10
  require('../rendering/modelPreloader.js');
11
11
 
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ // Flow-direction compatibility helper (module-level, no class dependency)
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ /**
17
+ * Returns true if the two flow directions are compatible for a connection.
18
+ * @param {string} fromFlow - 'in' | 'out' | 'bi'
19
+ * @param {string} toFlow - 'in' | 'out' | 'bi'
20
+ * @returns {boolean}
21
+ */
22
+ function _areFlowsCompatible(fromFlow, toFlow) {
23
+ if (fromFlow === 'bi' || toFlow === 'bi') return true;
24
+ // in ↔ out are compatible; in → in and out → out are not
25
+ return fromFlow !== toFlow;
26
+ }
27
+
12
28
  /**
13
29
  * CentralPlant class that manages all scene utility instances and provides public API
14
30
  *
@@ -19,7 +35,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
19
35
  * Initialize the CentralPlant manager
20
36
  *
21
37
  * @constructor
22
- * @version 0.2.4
38
+ * @version 0.2.8
23
39
  * @updated 2025-10-22
24
40
  *
25
41
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -952,6 +968,107 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
952
968
  return availableConnectorIds;
953
969
  }
954
970
 
971
+ /**
972
+ * Get available connectors with their flow direction metadata.
973
+ * Same filtering logic as getAvailableConnections() but returns objects instead of strings.
974
+ * @returns {Array<{id: string, flow: string}>} Array of connector info objects.
975
+ * flow is one of 'in', 'out', 'bi'. Defaults to 'bi' if not set in userData.
976
+ * @example
977
+ * const infos = centralPlant.getAvailableConnectionsInfo()
978
+ * // [{ id: 'PUMP-1-CONNECTOR-1', flow: 'out' }, ...]
979
+ */
980
+ }, {
981
+ key: "getAvailableConnectionsInfo",
982
+ value: function getAvailableConnectionsInfo() {
983
+ if (!this.sceneViewer || !this.sceneViewer.currentSceneData) {
984
+ console.warn('⚠️ getAvailableConnectionsInfo(): Scene viewer or current scene data not available');
985
+ return [];
986
+ }
987
+ var sceneData = this.sceneViewer.currentSceneData;
988
+ if (!sceneData.scene || !sceneData.scene.children) {
989
+ console.warn('⚠️ getAvailableConnectionsInfo(): Invalid scene data structure');
990
+ return [];
991
+ }
992
+ var allConnectorInfos = [];
993
+ sceneData.scene.children.forEach(function (component) {
994
+ if (component.children && Array.isArray(component.children)) {
995
+ component.children.forEach(function (child) {
996
+ if (child.userData && child.userData.objectType === 'connector' && child.uuid) {
997
+ allConnectorInfos.push({
998
+ id: child.uuid,
999
+ flow: child.userData.flow || 'bi'
1000
+ });
1001
+ }
1002
+ });
1003
+ }
1004
+ });
1005
+ var existingConnections = this.getConnections();
1006
+ var usedConnectorIds = new Set();
1007
+ existingConnections.forEach(function (connection) {
1008
+ if (connection.from) usedConnectorIds.add(connection.from);
1009
+ if (connection.to) usedConnectorIds.add(connection.to);
1010
+ });
1011
+ return allConnectorInfos.filter(function (info) {
1012
+ return !usedConnectorIds.has(info.id);
1013
+ });
1014
+ }
1015
+
1016
+ /**
1017
+ * Validate all connections in the current scene for flow direction compatibility.
1018
+ * @returns {{ valid: Array<Object>, invalid: Array<{connection: Object, reason: string}> }}
1019
+ * @example
1020
+ * const result = centralPlant.validateConnections()
1021
+ * result.invalid.forEach(({ connection, reason }) => console.warn(reason, connection))
1022
+ */
1023
+ }, {
1024
+ key: "validateConnections",
1025
+ value: function validateConnections() {
1026
+ if (!this.sceneViewer || !this.sceneViewer.currentSceneData) {
1027
+ console.warn('⚠️ validateConnections(): Scene viewer or current scene data not available');
1028
+ return {
1029
+ valid: [],
1030
+ invalid: []
1031
+ };
1032
+ }
1033
+ var connections = this.getConnections();
1034
+ var sceneData = this.sceneViewer.currentSceneData;
1035
+
1036
+ // Build lookup map: connectorId → flow
1037
+ var flowMap = {};
1038
+ var scene = sceneData.scene || {};
1039
+ var children = scene.children || [];
1040
+ children.forEach(function (component) {
1041
+ if (component.children && Array.isArray(component.children)) {
1042
+ component.children.forEach(function (child) {
1043
+ if (child.userData && child.userData.objectType === 'connector' && child.uuid) {
1044
+ flowMap[child.uuid] = child.userData.flow || 'bi';
1045
+ }
1046
+ });
1047
+ }
1048
+ });
1049
+ var valid = [];
1050
+ var invalid = [];
1051
+ connections.forEach(function (connection) {
1052
+ var fromFlow = flowMap[connection.from] || 'bi';
1053
+ var toFlow = flowMap[connection.to] || 'bi';
1054
+ if (_areFlowsCompatible(fromFlow, toFlow)) {
1055
+ valid.push(connection);
1056
+ } else {
1057
+ var reason = "Incompatible flow directions: connector '".concat(connection.from, "' is '").concat(fromFlow, "' and connector '").concat(connection.to, "' is '").concat(toFlow, "' \u2014 ").concat(fromFlow, " \u2192 ").concat(toFlow, " is not allowed");
1058
+ console.warn("\u26A0\uFE0F validateConnections(): ".concat(reason));
1059
+ invalid.push({
1060
+ connection: connection,
1061
+ reason: reason
1062
+ });
1063
+ }
1064
+ });
1065
+ console.log("\u2705 validateConnections(): ".concat(valid.length, " valid, ").concat(invalid.length, " invalid connections"));
1066
+ return {
1067
+ valid: valid,
1068
+ invalid: invalid
1069
+ };
1070
+ }
1071
+
955
1072
  // ─────────────────────────────────────────────────────────────────────────
956
1073
  // BEHAVIORS API
957
1074
  // ─────────────────────────────────────────────────────────────────────────
@@ -50,6 +50,59 @@ function _interopNamespace(e) {
50
50
 
51
51
  var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
52
52
 
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+ // Flow-direction helpers (module-level)
55
+ // ─────────────────────────────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Returns the flow direction of a connector from the current scene data.
59
+ * @param {Object} sceneData - currentSceneData object
60
+ * @param {string} connectorId
61
+ * @returns {'in'|'out'|'bi'} Defaults to 'bi' if not set.
62
+ */
63
+ function _getConnectorFlow(sceneData, connectorId) {
64
+ var _sceneData$scene;
65
+ var children = (sceneData === null || sceneData === void 0 || (_sceneData$scene = sceneData.scene) === null || _sceneData$scene === void 0 ? void 0 : _sceneData$scene.children) || [];
66
+ var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(children),
67
+ _step;
68
+ try {
69
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
70
+ var component = _step.value;
71
+ var _iterator2 = _rollupPluginBabelHelpers.createForOfIteratorHelper(component.children || []),
72
+ _step2;
73
+ try {
74
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
75
+ var _child$userData;
76
+ var child = _step2.value;
77
+ if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'connector' && child.uuid === connectorId) {
78
+ return child.userData.flow || 'bi';
79
+ }
80
+ }
81
+ } catch (err) {
82
+ _iterator2.e(err);
83
+ } finally {
84
+ _iterator2.f();
85
+ }
86
+ }
87
+ } catch (err) {
88
+ _iterator.e(err);
89
+ } finally {
90
+ _iterator.f();
91
+ }
92
+ return 'bi';
93
+ }
94
+
95
+ /**
96
+ * Returns true if fromFlow → toFlow is a valid connection.
97
+ * @param {'in'|'out'|'bi'} fromFlow
98
+ * @param {'in'|'out'|'bi'} toFlow
99
+ * @returns {boolean}
100
+ */
101
+ function _areFlowsCompatible(fromFlow, toFlow) {
102
+ if (fromFlow === 'bi' || toFlow === 'bi') return true;
103
+ return fromFlow !== toFlow;
104
+ }
105
+
53
106
  /**
54
107
  * CentralPlantInternals class containing internal methods and functionality
55
108
  */
@@ -501,12 +554,12 @@ var CentralPlantInternals = /*#__PURE__*/function () {
501
554
  console.log("\uD83D\uDD27 Translating ".concat(selectedObjects.length, " selected object(s) on ").concat(axis, " axis by ").concat(value));
502
555
 
503
556
  // Translate each selected object using the appropriate method
504
- var _iterator = _rollupPluginBabelHelpers.createForOfIteratorHelper(selectedObjects),
505
- _step;
557
+ var _iterator3 = _rollupPluginBabelHelpers.createForOfIteratorHelper(selectedObjects),
558
+ _step3;
506
559
  try {
507
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
560
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
508
561
  var _obj$userData;
509
- var obj = _step.value;
562
+ var obj = _step3.value;
510
563
  var objectType = (_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType;
511
564
  var objectId = obj.uuid;
512
565
  var success = false;
@@ -533,9 +586,9 @@ var CentralPlantInternals = /*#__PURE__*/function () {
533
586
  }
534
587
  }
535
588
  } catch (err) {
536
- _iterator.e(err);
589
+ _iterator3.e(err);
537
590
  } finally {
538
- _iterator.f();
591
+ _iterator3.f();
539
592
  }
540
593
  result.success = result.translatedCount === result.totalCount;
541
594
  if (result.success) {
@@ -719,7 +772,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
719
772
  }, {
720
773
  key: "addConnection",
721
774
  value: function addConnection(fromConnectorId, toConnectorId) {
722
- var _this$centralPlant$sc4;
775
+ var _this$centralPlant$sc4, _this$centralPlant$sc5;
723
776
  // Use centralized validation for connection parameters
724
777
  var existingConnections = ((_this$centralPlant$sc4 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc4 === void 0 || (_this$centralPlant$sc4 = _this$centralPlant$sc4.currentSceneData) === null || _this$centralPlant$sc4 === void 0 ? void 0 : _this$centralPlant$sc4.connections) || [];
725
778
  var validation = this.validator.validateConnectionParams(fromConnectorId, toConnectorId, existingConnections);
@@ -727,6 +780,17 @@ var CentralPlantInternals = /*#__PURE__*/function () {
727
780
  return false; // Validator already logged the error
728
781
  }
729
782
 
783
+ // Validate flow direction compatibility
784
+ var sceneData = (_this$centralPlant$sc5 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc5 === void 0 ? void 0 : _this$centralPlant$sc5.currentSceneData;
785
+ if (sceneData) {
786
+ var fromFlow = _getConnectorFlow(sceneData, fromConnectorId);
787
+ var toFlow = _getConnectorFlow(sceneData, toConnectorId);
788
+ if (!_areFlowsCompatible(fromFlow, toFlow)) {
789
+ console.error("\u274C addConnection(): Incompatible flow directions \u2014 '".concat(fromConnectorId, "' is '").concat(fromFlow, "' and '").concat(toConnectorId, "' is '").concat(toFlow, "'. ") + "'".concat(fromFlow, "' \u2192 '").concat(toFlow, "' connections are not allowed."));
790
+ return false;
791
+ }
792
+ }
793
+
730
794
  // Validate scene availability
731
795
  var sceneValidation = this.validator.validateSceneViewer(this.centralPlant.sceneViewer);
732
796
  if (!sceneValidation.isValid) {
@@ -847,7 +911,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
847
911
  }, {
848
912
  key: "addComponent",
849
913
  value: function addComponent(libraryId) {
850
- var _this$centralPlant$sc5;
914
+ var _this$centralPlant$sc6;
851
915
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
852
916
  // Use centralized validation for component addition parameters
853
917
  var existingIds = this.getComponentIds();
@@ -857,7 +921,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
857
921
  }
858
922
 
859
923
  // Validate scene availability
860
- var sceneValidation = this.validator.validateSceneViewer(this.centralPlant.sceneViewer, (_this$centralPlant$sc5 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc5 === void 0 ? void 0 : _this$centralPlant$sc5.scene);
924
+ var sceneValidation = this.validator.validateSceneViewer(this.centralPlant.sceneViewer, (_this$centralPlant$sc6 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc6 === void 0 ? void 0 : _this$centralPlant$sc6.scene);
861
925
  if (!sceneValidation.isValid) {
862
926
  return false;
863
927
  }
@@ -876,7 +940,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
876
940
  return false;
877
941
  }
878
942
  try {
879
- var _componentData$childr, _componentData$childr2, _this$centralPlant$sc6, _componentData$childr3, _componentData$defaul;
943
+ var _componentData$childr, _componentData$childr2, _this$centralPlant$sc7, _componentData$childr3, _componentData$defaul;
880
944
  // Generate a unique component ID if not provided
881
945
  var componentId = options.customId || this.generateUniqueComponentId(libraryId);
882
946
 
@@ -988,7 +1052,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
988
1052
  componentModel.updateMatrixWorld(true);
989
1053
 
990
1054
  // Check if component is underground and fix if needed (based on settings)
991
- var checkUnderground = (_this$centralPlant$sc6 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc6 === void 0 || (_this$centralPlant$sc6 = _this$centralPlant$sc6.managers) === null || _this$centralPlant$sc6 === void 0 || (_this$centralPlant$sc6 = _this$centralPlant$sc6.settingsManager) === null || _this$centralPlant$sc6 === void 0 ? void 0 : _this$centralPlant$sc6.getSetting('scene', 'checkUnderground');
1055
+ var checkUnderground = (_this$centralPlant$sc7 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc7 === void 0 || (_this$centralPlant$sc7 = _this$centralPlant$sc7.managers) === null || _this$centralPlant$sc7 === void 0 || (_this$centralPlant$sc7 = _this$centralPlant$sc7.settingsManager) === null || _this$centralPlant$sc7 === void 0 ? void 0 : _this$centralPlant$sc7.getSetting('scene', 'checkUnderground');
992
1056
  if (checkUnderground) {
993
1057
  var wasFixed = this.fixUndergroundComponent(componentModel);
994
1058
  if (wasFixed) {
@@ -1086,8 +1150,8 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1086
1150
  // responds to tooltip-driven state changes immediately after drop.
1087
1151
  // (The scene-load path uses _processBehaviors instead, which runs on loadSceneData.)
1088
1152
  if ((_componentData$defaul = componentData.defaultBehaviors) !== null && _componentData$defaul !== void 0 && _componentData$defaul.length) {
1089
- var _this$centralPlant$sc7, _som$registerBehavior;
1090
- var som = (_this$centralPlant$sc7 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc7 === void 0 ? void 0 : _this$centralPlant$sc7.sceneOperationsManager;
1153
+ var _this$centralPlant$sc8, _som$registerBehavior;
1154
+ var som = (_this$centralPlant$sc8 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc8 === void 0 ? void 0 : _this$centralPlant$sc8.sceneOperationsManager;
1091
1155
  som === null || som === void 0 || (_som$registerBehavior = som.registerBehaviorsForComponentInstance) === null || _som$registerBehavior === void 0 || _som$registerBehavior.call(som, componentData, componentId);
1092
1156
  }
1093
1157
 
@@ -1149,9 +1213,9 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1149
1213
  }, {
1150
1214
  key: "deleteComponent",
1151
1215
  value: function deleteComponent(componentId) {
1152
- var _this$centralPlant$sc8;
1216
+ var _this$centralPlant$sc9;
1153
1217
  // Check if component manager is available
1154
- var componentManager = (_this$centralPlant$sc8 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc8 === void 0 ? void 0 : _this$centralPlant$sc8.componentManager;
1218
+ var componentManager = (_this$centralPlant$sc9 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc9 === void 0 ? void 0 : _this$centralPlant$sc9.componentManager;
1155
1219
  if (!componentManager) {
1156
1220
  console.error('❌ deleteComponent(): Component manager not available');
1157
1221
  return false;
@@ -1226,8 +1290,8 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1226
1290
  }
1227
1291
  var componentIds = [];
1228
1292
  this.centralPlant.sceneViewer.scene.traverse(function (child) {
1229
- var _child$userData;
1230
- if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'component') {
1293
+ var _child$userData2;
1294
+ if (((_child$userData2 = child.userData) === null || _child$userData2 === void 0 ? void 0 : _child$userData2.objectType) === 'component') {
1231
1295
  componentIds.push(child.uuid || child.userData.originalUuid);
1232
1296
  }
1233
1297
  });
@@ -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,
@@ -5,6 +5,22 @@ import { DisposalUtilities } from '../utils/DisposalUtilities.js';
5
5
  import { CentralPlantInternals } from './centralPlantInternals.js';
6
6
  import '../rendering/modelPreloader.js';
7
7
 
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ // Flow-direction compatibility helper (module-level, no class dependency)
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ /**
13
+ * Returns true if the two flow directions are compatible for a connection.
14
+ * @param {string} fromFlow - 'in' | 'out' | 'bi'
15
+ * @param {string} toFlow - 'in' | 'out' | 'bi'
16
+ * @returns {boolean}
17
+ */
18
+ function _areFlowsCompatible(fromFlow, toFlow) {
19
+ if (fromFlow === 'bi' || toFlow === 'bi') return true;
20
+ // in ↔ out are compatible; in → in and out → out are not
21
+ return fromFlow !== toFlow;
22
+ }
23
+
8
24
  /**
9
25
  * CentralPlant class that manages all scene utility instances and provides public API
10
26
  *
@@ -15,7 +31,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
15
31
  * Initialize the CentralPlant manager
16
32
  *
17
33
  * @constructor
18
- * @version 0.2.4
34
+ * @version 0.2.8
19
35
  * @updated 2025-10-22
20
36
  *
21
37
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -948,6 +964,107 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
948
964
  return availableConnectorIds;
949
965
  }
950
966
 
967
+ /**
968
+ * Get available connectors with their flow direction metadata.
969
+ * Same filtering logic as getAvailableConnections() but returns objects instead of strings.
970
+ * @returns {Array<{id: string, flow: string}>} Array of connector info objects.
971
+ * flow is one of 'in', 'out', 'bi'. Defaults to 'bi' if not set in userData.
972
+ * @example
973
+ * const infos = centralPlant.getAvailableConnectionsInfo()
974
+ * // [{ id: 'PUMP-1-CONNECTOR-1', flow: 'out' }, ...]
975
+ */
976
+ }, {
977
+ key: "getAvailableConnectionsInfo",
978
+ value: function getAvailableConnectionsInfo() {
979
+ if (!this.sceneViewer || !this.sceneViewer.currentSceneData) {
980
+ console.warn('⚠️ getAvailableConnectionsInfo(): Scene viewer or current scene data not available');
981
+ return [];
982
+ }
983
+ var sceneData = this.sceneViewer.currentSceneData;
984
+ if (!sceneData.scene || !sceneData.scene.children) {
985
+ console.warn('⚠️ getAvailableConnectionsInfo(): Invalid scene data structure');
986
+ return [];
987
+ }
988
+ var allConnectorInfos = [];
989
+ sceneData.scene.children.forEach(function (component) {
990
+ if (component.children && Array.isArray(component.children)) {
991
+ component.children.forEach(function (child) {
992
+ if (child.userData && child.userData.objectType === 'connector' && child.uuid) {
993
+ allConnectorInfos.push({
994
+ id: child.uuid,
995
+ flow: child.userData.flow || 'bi'
996
+ });
997
+ }
998
+ });
999
+ }
1000
+ });
1001
+ var existingConnections = this.getConnections();
1002
+ var usedConnectorIds = new Set();
1003
+ existingConnections.forEach(function (connection) {
1004
+ if (connection.from) usedConnectorIds.add(connection.from);
1005
+ if (connection.to) usedConnectorIds.add(connection.to);
1006
+ });
1007
+ return allConnectorInfos.filter(function (info) {
1008
+ return !usedConnectorIds.has(info.id);
1009
+ });
1010
+ }
1011
+
1012
+ /**
1013
+ * Validate all connections in the current scene for flow direction compatibility.
1014
+ * @returns {{ valid: Array<Object>, invalid: Array<{connection: Object, reason: string}> }}
1015
+ * @example
1016
+ * const result = centralPlant.validateConnections()
1017
+ * result.invalid.forEach(({ connection, reason }) => console.warn(reason, connection))
1018
+ */
1019
+ }, {
1020
+ key: "validateConnections",
1021
+ value: function validateConnections() {
1022
+ if (!this.sceneViewer || !this.sceneViewer.currentSceneData) {
1023
+ console.warn('⚠️ validateConnections(): Scene viewer or current scene data not available');
1024
+ return {
1025
+ valid: [],
1026
+ invalid: []
1027
+ };
1028
+ }
1029
+ var connections = this.getConnections();
1030
+ var sceneData = this.sceneViewer.currentSceneData;
1031
+
1032
+ // Build lookup map: connectorId → flow
1033
+ var flowMap = {};
1034
+ var scene = sceneData.scene || {};
1035
+ var children = scene.children || [];
1036
+ children.forEach(function (component) {
1037
+ if (component.children && Array.isArray(component.children)) {
1038
+ component.children.forEach(function (child) {
1039
+ if (child.userData && child.userData.objectType === 'connector' && child.uuid) {
1040
+ flowMap[child.uuid] = child.userData.flow || 'bi';
1041
+ }
1042
+ });
1043
+ }
1044
+ });
1045
+ var valid = [];
1046
+ var invalid = [];
1047
+ connections.forEach(function (connection) {
1048
+ var fromFlow = flowMap[connection.from] || 'bi';
1049
+ var toFlow = flowMap[connection.to] || 'bi';
1050
+ if (_areFlowsCompatible(fromFlow, toFlow)) {
1051
+ valid.push(connection);
1052
+ } else {
1053
+ var reason = "Incompatible flow directions: connector '".concat(connection.from, "' is '").concat(fromFlow, "' and connector '").concat(connection.to, "' is '").concat(toFlow, "' \u2014 ").concat(fromFlow, " \u2192 ").concat(toFlow, " is not allowed");
1054
+ console.warn("\u26A0\uFE0F validateConnections(): ".concat(reason));
1055
+ invalid.push({
1056
+ connection: connection,
1057
+ reason: reason
1058
+ });
1059
+ }
1060
+ });
1061
+ console.log("\u2705 validateConnections(): ".concat(valid.length, " valid, ").concat(invalid.length, " invalid connections"));
1062
+ return {
1063
+ valid: valid,
1064
+ invalid: invalid
1065
+ };
1066
+ }
1067
+
951
1068
  // ─────────────────────────────────────────────────────────────────────────
952
1069
  // BEHAVIORS API
953
1070
  // ─────────────────────────────────────────────────────────────────────────
@@ -26,6 +26,59 @@ import { generateUuidFromName, getHardcodedUuid, findObjectByHardcodedUuid, gene
26
26
  import { attachIODevicesToComponent } from '../utils/ioDeviceUtils.js';
27
27
  import modelPreloader from '../rendering/modelPreloader.js';
28
28
 
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // Flow-direction helpers (module-level)
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ /**
34
+ * Returns the flow direction of a connector from the current scene data.
35
+ * @param {Object} sceneData - currentSceneData object
36
+ * @param {string} connectorId
37
+ * @returns {'in'|'out'|'bi'} Defaults to 'bi' if not set.
38
+ */
39
+ function _getConnectorFlow(sceneData, connectorId) {
40
+ var _sceneData$scene;
41
+ var children = (sceneData === null || sceneData === void 0 || (_sceneData$scene = sceneData.scene) === null || _sceneData$scene === void 0 ? void 0 : _sceneData$scene.children) || [];
42
+ var _iterator = _createForOfIteratorHelper(children),
43
+ _step;
44
+ try {
45
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
46
+ var component = _step.value;
47
+ var _iterator2 = _createForOfIteratorHelper(component.children || []),
48
+ _step2;
49
+ try {
50
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
51
+ var _child$userData;
52
+ var child = _step2.value;
53
+ if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'connector' && child.uuid === connectorId) {
54
+ return child.userData.flow || 'bi';
55
+ }
56
+ }
57
+ } catch (err) {
58
+ _iterator2.e(err);
59
+ } finally {
60
+ _iterator2.f();
61
+ }
62
+ }
63
+ } catch (err) {
64
+ _iterator.e(err);
65
+ } finally {
66
+ _iterator.f();
67
+ }
68
+ return 'bi';
69
+ }
70
+
71
+ /**
72
+ * Returns true if fromFlow → toFlow is a valid connection.
73
+ * @param {'in'|'out'|'bi'} fromFlow
74
+ * @param {'in'|'out'|'bi'} toFlow
75
+ * @returns {boolean}
76
+ */
77
+ function _areFlowsCompatible(fromFlow, toFlow) {
78
+ if (fromFlow === 'bi' || toFlow === 'bi') return true;
79
+ return fromFlow !== toFlow;
80
+ }
81
+
29
82
  /**
30
83
  * CentralPlantInternals class containing internal methods and functionality
31
84
  */
@@ -477,12 +530,12 @@ var CentralPlantInternals = /*#__PURE__*/function () {
477
530
  console.log("\uD83D\uDD27 Translating ".concat(selectedObjects.length, " selected object(s) on ").concat(axis, " axis by ").concat(value));
478
531
 
479
532
  // Translate each selected object using the appropriate method
480
- var _iterator = _createForOfIteratorHelper(selectedObjects),
481
- _step;
533
+ var _iterator3 = _createForOfIteratorHelper(selectedObjects),
534
+ _step3;
482
535
  try {
483
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
536
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
484
537
  var _obj$userData;
485
- var obj = _step.value;
538
+ var obj = _step3.value;
486
539
  var objectType = (_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType;
487
540
  var objectId = obj.uuid;
488
541
  var success = false;
@@ -509,9 +562,9 @@ var CentralPlantInternals = /*#__PURE__*/function () {
509
562
  }
510
563
  }
511
564
  } catch (err) {
512
- _iterator.e(err);
565
+ _iterator3.e(err);
513
566
  } finally {
514
- _iterator.f();
567
+ _iterator3.f();
515
568
  }
516
569
  result.success = result.translatedCount === result.totalCount;
517
570
  if (result.success) {
@@ -695,7 +748,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
695
748
  }, {
696
749
  key: "addConnection",
697
750
  value: function addConnection(fromConnectorId, toConnectorId) {
698
- var _this$centralPlant$sc4;
751
+ var _this$centralPlant$sc4, _this$centralPlant$sc5;
699
752
  // Use centralized validation for connection parameters
700
753
  var existingConnections = ((_this$centralPlant$sc4 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc4 === void 0 || (_this$centralPlant$sc4 = _this$centralPlant$sc4.currentSceneData) === null || _this$centralPlant$sc4 === void 0 ? void 0 : _this$centralPlant$sc4.connections) || [];
701
754
  var validation = this.validator.validateConnectionParams(fromConnectorId, toConnectorId, existingConnections);
@@ -703,6 +756,17 @@ var CentralPlantInternals = /*#__PURE__*/function () {
703
756
  return false; // Validator already logged the error
704
757
  }
705
758
 
759
+ // Validate flow direction compatibility
760
+ var sceneData = (_this$centralPlant$sc5 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc5 === void 0 ? void 0 : _this$centralPlant$sc5.currentSceneData;
761
+ if (sceneData) {
762
+ var fromFlow = _getConnectorFlow(sceneData, fromConnectorId);
763
+ var toFlow = _getConnectorFlow(sceneData, toConnectorId);
764
+ if (!_areFlowsCompatible(fromFlow, toFlow)) {
765
+ console.error("\u274C addConnection(): Incompatible flow directions \u2014 '".concat(fromConnectorId, "' is '").concat(fromFlow, "' and '").concat(toConnectorId, "' is '").concat(toFlow, "'. ") + "'".concat(fromFlow, "' \u2192 '").concat(toFlow, "' connections are not allowed."));
766
+ return false;
767
+ }
768
+ }
769
+
706
770
  // Validate scene availability
707
771
  var sceneValidation = this.validator.validateSceneViewer(this.centralPlant.sceneViewer);
708
772
  if (!sceneValidation.isValid) {
@@ -823,7 +887,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
823
887
  }, {
824
888
  key: "addComponent",
825
889
  value: function addComponent(libraryId) {
826
- var _this$centralPlant$sc5;
890
+ var _this$centralPlant$sc6;
827
891
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
828
892
  // Use centralized validation for component addition parameters
829
893
  var existingIds = this.getComponentIds();
@@ -833,7 +897,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
833
897
  }
834
898
 
835
899
  // Validate scene availability
836
- var sceneValidation = this.validator.validateSceneViewer(this.centralPlant.sceneViewer, (_this$centralPlant$sc5 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc5 === void 0 ? void 0 : _this$centralPlant$sc5.scene);
900
+ var sceneValidation = this.validator.validateSceneViewer(this.centralPlant.sceneViewer, (_this$centralPlant$sc6 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc6 === void 0 ? void 0 : _this$centralPlant$sc6.scene);
837
901
  if (!sceneValidation.isValid) {
838
902
  return false;
839
903
  }
@@ -852,7 +916,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
852
916
  return false;
853
917
  }
854
918
  try {
855
- var _componentData$childr, _componentData$childr2, _this$centralPlant$sc6, _componentData$childr3, _componentData$defaul;
919
+ var _componentData$childr, _componentData$childr2, _this$centralPlant$sc7, _componentData$childr3, _componentData$defaul;
856
920
  // Generate a unique component ID if not provided
857
921
  var componentId = options.customId || this.generateUniqueComponentId(libraryId);
858
922
 
@@ -964,7 +1028,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
964
1028
  componentModel.updateMatrixWorld(true);
965
1029
 
966
1030
  // Check if component is underground and fix if needed (based on settings)
967
- var checkUnderground = (_this$centralPlant$sc6 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc6 === void 0 || (_this$centralPlant$sc6 = _this$centralPlant$sc6.managers) === null || _this$centralPlant$sc6 === void 0 || (_this$centralPlant$sc6 = _this$centralPlant$sc6.settingsManager) === null || _this$centralPlant$sc6 === void 0 ? void 0 : _this$centralPlant$sc6.getSetting('scene', 'checkUnderground');
1031
+ var checkUnderground = (_this$centralPlant$sc7 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc7 === void 0 || (_this$centralPlant$sc7 = _this$centralPlant$sc7.managers) === null || _this$centralPlant$sc7 === void 0 || (_this$centralPlant$sc7 = _this$centralPlant$sc7.settingsManager) === null || _this$centralPlant$sc7 === void 0 ? void 0 : _this$centralPlant$sc7.getSetting('scene', 'checkUnderground');
968
1032
  if (checkUnderground) {
969
1033
  var wasFixed = this.fixUndergroundComponent(componentModel);
970
1034
  if (wasFixed) {
@@ -1062,8 +1126,8 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1062
1126
  // responds to tooltip-driven state changes immediately after drop.
1063
1127
  // (The scene-load path uses _processBehaviors instead, which runs on loadSceneData.)
1064
1128
  if ((_componentData$defaul = componentData.defaultBehaviors) !== null && _componentData$defaul !== void 0 && _componentData$defaul.length) {
1065
- var _this$centralPlant$sc7, _som$registerBehavior;
1066
- var som = (_this$centralPlant$sc7 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc7 === void 0 ? void 0 : _this$centralPlant$sc7.sceneOperationsManager;
1129
+ var _this$centralPlant$sc8, _som$registerBehavior;
1130
+ var som = (_this$centralPlant$sc8 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc8 === void 0 ? void 0 : _this$centralPlant$sc8.sceneOperationsManager;
1067
1131
  som === null || som === void 0 || (_som$registerBehavior = som.registerBehaviorsForComponentInstance) === null || _som$registerBehavior === void 0 || _som$registerBehavior.call(som, componentData, componentId);
1068
1132
  }
1069
1133
 
@@ -1125,9 +1189,9 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1125
1189
  }, {
1126
1190
  key: "deleteComponent",
1127
1191
  value: function deleteComponent(componentId) {
1128
- var _this$centralPlant$sc8;
1192
+ var _this$centralPlant$sc9;
1129
1193
  // Check if component manager is available
1130
- var componentManager = (_this$centralPlant$sc8 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc8 === void 0 ? void 0 : _this$centralPlant$sc8.componentManager;
1194
+ var componentManager = (_this$centralPlant$sc9 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc9 === void 0 ? void 0 : _this$centralPlant$sc9.componentManager;
1131
1195
  if (!componentManager) {
1132
1196
  console.error('❌ deleteComponent(): Component manager not available');
1133
1197
  return false;
@@ -1202,8 +1266,8 @@ var CentralPlantInternals = /*#__PURE__*/function () {
1202
1266
  }
1203
1267
  var componentIds = [];
1204
1268
  this.centralPlant.sceneViewer.scene.traverse(function (child) {
1205
- var _child$userData;
1206
- if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'component') {
1269
+ var _child$userData2;
1270
+ if (((_child$userData2 = child.userData) === null || _child$userData2 === void 0 ? void 0 : _child$userData2.objectType) === 'component') {
1207
1271
  componentIds.push(child.uuid || child.userData.originalUuid);
1208
1272
  }
1209
1273
  });
@@ -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/dist/index.d.ts CHANGED
@@ -5,6 +5,25 @@
5
5
  * organized by their functional categories.
6
6
  */
7
7
 
8
+ // ─── Connector flow types ─────────────────────────────────────────────────────
9
+
10
+ /** The flow direction of a connector port. */
11
+ export type FlowDirection = 'in' | 'out' | 'bi'
12
+
13
+ /** Connector descriptor returned by getAvailableConnectionsInfo(). */
14
+ export interface ConnectorInfo {
15
+ /** The connector's UUID as it appears in the scene JSON. */
16
+ id: string
17
+ /** Flow direction. Defaults to 'bi' for connectors that do not declare one. */
18
+ flow: FlowDirection
19
+ }
20
+
21
+ /** Result of validateConnections(). */
22
+ export interface ConnectionValidationResult {
23
+ valid: Array<{ from: string; to: string }>
24
+ invalid: Array<{ connection: { from: string; to: string }; reason: string }>
25
+ }
26
+
8
27
  // ─── I/O Device types ────────────────────────────────────────────────────────
9
28
 
10
29
  export interface IoDeviceState {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2112-lab/central-plant",
3
- "version": "0.2.4",
3
+ "version": "0.2.8",
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",