@2112-lab/central-plant 0.2.5 → 0.2.10

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.
@@ -25783,10 +25783,19 @@ function createPathfindingRequest(startConnector, endConnector) {
25783
25783
  };
25784
25784
  }
25785
25785
 
25786
+ /**
25787
+ * The official set of flow-related attribute keys that can be set on a path.
25788
+ * These attributes belong to the full path and are inherited by all segments.
25789
+ *
25790
+ * @type {string[]}
25791
+ */
25792
+ var FLOW_ATTRIBUTE_KEYS = ['flowDirection', 'flowSpeed', 'flowTemperature', 'flowMaterial'];
25793
+
25786
25794
  /**
25787
25795
  * Data structure for storing path information with segments.
25788
25796
  * Tracks both computed and declared (manually edited) segments.
25789
- *
25797
+ * Also stores path-level flow attributes shared by all segments.
25798
+ *
25790
25799
  * Business logic layer - stores coordinate data without Three.js dependencies.
25791
25800
  */
25792
25801
  var PathData = /*#__PURE__*/function () {
@@ -25806,16 +25815,60 @@ var PathData = /*#__PURE__*/function () {
25806
25815
  * @type {Array<{start: {x, y, z}, end: {x, y, z}, isDeclared: boolean, modifiedAt: number|null}>}
25807
25816
  */
25808
25817
  this.segments = [];
25818
+
25819
+ /**
25820
+ * Path-level flow attributes shared by all segments within this path.
25821
+ * Keys must come from FLOW_ATTRIBUTE_KEYS. Values are application-defined.
25822
+ * @type {Record<string, any>}
25823
+ */
25824
+ this.flowAttributes = {};
25809
25825
  this.createdAt = Date.now();
25810
25826
  }
25811
25827
 
25812
25828
  /**
25813
- * Add a computed segment (from pathfinding algorithm)
25814
- *
25815
- * @param {{x: number, y: number, z: number}} start - Start coordinate
25816
- * @param {{x: number, y: number, z: number}} end - End coordinate
25829
+ * Set a flow attribute on this path.
25830
+ * All segments within this path inherit the value.
25831
+ *
25832
+ * @param {string} key - Attribute key (should be one of FLOW_ATTRIBUTE_KEYS)
25833
+ * @param {any} value - Attribute value
25817
25834
  */
25818
25835
  return _createClass(PathData, [{
25836
+ key: "setFlowAttribute",
25837
+ value: function setFlowAttribute(key, value) {
25838
+ this.flowAttributes[key] = value;
25839
+ }
25840
+
25841
+ /**
25842
+ * Get the declared value of a flow attribute.
25843
+ * Returns null if the attribute has not been set.
25844
+ *
25845
+ * @param {string} key - Attribute key
25846
+ * @returns {any|null}
25847
+ */
25848
+ }, {
25849
+ key: "getFlowAttribute",
25850
+ value: function getFlowAttribute(key) {
25851
+ return Object.prototype.hasOwnProperty.call(this.flowAttributes, key) ? this.flowAttributes[key] : null;
25852
+ }
25853
+
25854
+ /**
25855
+ * Get a shallow copy of all declared flow attributes.
25856
+ *
25857
+ * @returns {Record<string, any>}
25858
+ */
25859
+ }, {
25860
+ key: "getAllFlowAttributes",
25861
+ value: function getAllFlowAttributes() {
25862
+ return _objectSpread2({}, this.flowAttributes);
25863
+ }
25864
+
25865
+ /**
25866
+ * Add a computed segment (from pathfinding algorithm)
25867
+ *
25868
+ * @param {{x: number, y: number, z: number}} start - Start coordinate
25869
+ * @param {{x: number, y: number, z: number}} end - End coordinate
25870
+ */
25871
+ }, {
25819
25872
  key: "addSegment",
25820
25873
  value: function addSegment(start, end) {
25821
25874
  this.segments.push({
@@ -25892,6 +25945,7 @@ var PathData = /*#__PURE__*/function () {
25892
25945
  segments: this.segments.map(function (seg) {
25893
25946
  return _objectSpread2({}, seg);
25894
25947
  }),
25948
+ flowAttributes: _objectSpread2({}, this.flowAttributes),
25895
25949
  createdAt: this.createdAt
25896
25950
  };
25897
25951
  }
@@ -25952,6 +26006,7 @@ var PathData = /*#__PURE__*/function () {
25952
26006
  pathData.segments = json.segments.map(function (seg) {
25953
26007
  return _objectSpread2({}, seg);
25954
26008
  });
26009
+ pathData.flowAttributes = json.flowAttributes ? _objectSpread2({}, json.flowAttributes) : {};
25955
26010
  pathData.createdAt = json.createdAt || Date.now();
25956
26011
  return pathData;
25957
26012
  }
@@ -26109,16 +26164,58 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
26109
26164
  emissive: 0
26110
26165
  });
26111
26166
  _this.registerDisposable(_this._gatewayGeometry, _this._gatewayMaterial);
26167
+
26168
+ /**
26169
+ * Map of pathId -> THREE.Material for per-path color control.
26170
+ * pathId format: "${from}-->${to}"
26171
+ * All segments within a path share the same material instance.
26172
+ * @type {Map<string, THREE.Material>}
26173
+ */
26174
+ _this._pathMaterials = new Map();
26112
26175
  return _this;
26113
26176
  }
26114
26177
 
26115
26178
  /**
26116
- * Get path colors for visual distinction
26117
- * @param {number} index - Path index
26118
- * @returns {string} Hex color string
26179
+ * Dispose all tracked per-path materials and clear the map.
26180
+ * Called at the start of createPipePaths to clean up stale materials.
26181
+ * @private
26119
26182
  */
26120
26183
  _inherits(PathRenderingManager, _BaseDisposable);
26121
26184
  return _createClass(PathRenderingManager, [{
26185
+ key: "_clearPathMaterials",
26186
+ value: function _clearPathMaterials() {
26187
+ this._pathMaterials.forEach(function (mat) {
26188
+ return mat.dispose();
26189
+ });
26190
+ this._pathMaterials.clear();
26191
+ }
26192
+
26193
+ /**
26194
+ * Update the color of all pipe segments belonging to a specific path.
26195
+ * Operates on the shared per-path material — no scene traversal needed.
26196
+ *
26197
+ * @param {string} from - Source connector ID
26198
+ * @param {string} to - Target connector ID
26199
+ * @param {string|number} color - Any value accepted by THREE.Color.set()
26200
+ */
26201
+ }, {
26202
+ key: "updatePathColor",
26203
+ value: function updatePathColor(from, to, color) {
26204
+ var pathId = "".concat(from, "-->").concat(to);
26205
+ var mat = this._pathMaterials.get(pathId);
26206
+ if (mat) {
26207
+ mat.color.set(color);
26208
+ } else {
26209
+ console.warn("\u26A0\uFE0F PathRenderingManager.updatePathColor: no material found for path \"".concat(pathId, "\""));
26210
+ }
26211
+ }
26212
+
26213
+ /**
26214
+ * Get path colors for visual distinction
26215
+ * @param {number} index - Path index
26216
+ * @returns {string} Hex color string
26217
+ */
26218
+ }, {
26122
26219
  key: "getPathColor",
26123
26220
  value: function getPathColor(index) {
26124
26221
  var colors = [
@@ -26273,7 +26370,11 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
26273
26370
  var globalSegmentIndex = maxExistingIndex + 1;
26274
26371
  console.log("\uD83D\uDD22 Starting segment index at ".concat(globalSegmentIndex, " (max existing: ").concat(maxExistingIndex, ")"));
26275
26372
  var pipeRadius = 0.1;
26276
- var pipeMaterial = this.createPipeMaterial(crosscubeTextureSet);
26373
+ // Base material created once; per-path materials are cloned from it below.
26374
+ var baseMaterial = this.createPipeMaterial(crosscubeTextureSet);
26375
+
26376
+ // Dispose previous path materials (they were attached to now-removed segments)
26377
+ this._clearPathMaterials();
26277
26378
  paths.forEach(function (pathData, index) {
26278
26379
  if (pathData.path && pathData.path.length >= 2) {
26279
26380
  // Convert path points to Vector3 objects for consistent handling
@@ -26290,6 +26391,12 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
26290
26391
  }
26291
26392
  });
26292
26393
 
26394
+ // Create a per-path material clone so each path can have its color updated independently.
26395
+ var pathId = "".concat(pathData.from, "-->").concat(pathData.to);
26396
+ var perPathMaterial = baseMaterial.clone();
26397
+ _this3._pathMaterials.set(pathId, perPathMaterial);
26398
+ console.log("\uD83C\uDFA8 Created per-path material for \"".concat(pathId, "\""));
26399
+
26293
26400
  // Check if endpoints are component connectors (from pathfinder result)
26294
26401
  var fromIsComponentConnector = pathData.fromObjectType === 'component-connector';
26295
26402
  var toIsComponentConnector = pathData.toObjectType === 'component-connector';
@@ -26314,13 +26421,13 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
26314
26421
  var cylinderGeometry = new THREE__namespace.CylinderGeometry(pipeRadius, pipeRadius, length, 16, 1, false);
26315
26422
 
26316
26423
  // Determine material (debug red if rectified and in dev mode)
26317
- var materialToUse = pipeMaterial;
26424
+ var materialToUse = perPathMaterial;
26318
26425
 
26319
26426
  // Check for dev mode (strict localhost only)
26320
26427
  var isDev = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
26321
26428
  if (isDev && pathData.rectifiedSegments && pathData.rectifiedSegments.includes(j)) {
26322
- // Create red debug material by cloning the pipe material to match the look
26323
- materialToUse = pipeMaterial.clone();
26429
+ // Create red debug material by cloning the per-path material to match the look
26430
+ materialToUse = perPathMaterial.clone();
26324
26431
  materialToUse.color.setHex(0xff0000);
26325
26432
  console.log("\uD83C\uDFA8 Coloring rectified segment ".concat(j, " red"));
26326
26433
  }
@@ -26382,7 +26489,7 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
26382
26489
  sceneViewer.scene.add(cylinder);
26383
26490
 
26384
26491
  // Add smooth elbow joints only at actual direction changes (not at every point)
26385
- _this3.createAndAddElbowIfNeeded(pathPoints, j, pipeRadius, pipeMaterial, pathData, index, cylinder // Pass the segment so elbow can be added as a child
26492
+ _this3.createAndAddElbowIfNeeded(pathPoints, j, pipeRadius, perPathMaterial, pathData, index, cylinder // Pass the segment so elbow can be added as a child
26386
26493
  );
26387
26494
  }
26388
26495
  }
@@ -26487,6 +26594,9 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
26487
26594
  value: function dispose() {
26488
26595
  console.log('🗑️ Disposing PathRenderingManager...');
26489
26596
 
26597
+ // Dispose per-path materials
26598
+ this._clearPathMaterials();
26599
+
26490
26600
  // Call parent dispose to clean up registered resources (shared gateway geometry/material)
26491
26601
  _superPropGet(PathRenderingManager, "dispose", this, 3)([]);
26492
26602
 
@@ -27923,7 +28033,11 @@ var PathfindingManager = /*#__PURE__*/function (_BaseDisposable) {
27923
28033
  key: "_executePathfinding",
27924
28034
  value: (function () {
27925
28035
  var _executePathfinding2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(sceneData, connections) {
27926
- var _sceneDataCopy$childr, _sceneDataCopy$childr2, _pathfindingResult$pa, _pathfindingResult$pa2;
28036
+ var _this4 = this,
28037
+ _sceneDataCopy$childr,
28038
+ _sceneDataCopy$childr2,
28039
+ _pathfindingResult$pa,
28040
+ _pathfindingResult$pa2;
27927
28041
  var options,
27928
28042
  _options$context,
27929
28043
  context,
@@ -27984,6 +28098,15 @@ var PathfindingManager = /*#__PURE__*/function (_BaseDisposable) {
27984
28098
 
27985
28099
  // Create pipe paths with materials using the paths from pathfinder
27986
28100
  this.renderingManager.createPipePaths(pathfindingResult.paths, this.crosscubeTextureSet);
28101
+
28102
+ // ── Stage 3b: Ensure PathData entries exist for all rendered paths ──────
28103
+ // This preserves any flowAttributes already set on existing entries.
28104
+ if (pathfindingResult.paths) {
28105
+ pathfindingResult.paths.forEach(function (path) {
28106
+ var pathId = "".concat(path.from, "-->").concat(path.to);
28107
+ _this4._getOrCreatePathData(pathId, path.from, path.to);
28108
+ });
28109
+ }
27987
28110
  timers.pathRendering = performance.now() - renderStart;
27988
28111
 
27989
28112
  // ── Performance Summary ────────────────────────────────────────────
@@ -28021,6 +28144,26 @@ var PathfindingManager = /*#__PURE__*/function (_BaseDisposable) {
28021
28144
  return this.sceneDataManager.getSimplifiedSceneData();
28022
28145
  }
28023
28146
 
28147
+ /**
28148
+ * Return an existing PathData for pathId from the store, or create and register a new one.
28149
+ * Preserves existing flowAttributes when the entry already exists.
28150
+ *
28151
+ * @param {string} pathId - Canonical path identifier "${from}-->${to}"
28152
+ * @param {string} from - Source connector ID
28153
+ * @param {string} to - Target connector ID
28154
+ * @returns {PathData}
28155
+ * @private
28156
+ */
28157
+ }, {
28158
+ key: "_getOrCreatePathData",
28159
+ value: function _getOrCreatePathData(pathId, from, to) {
28160
+ if (!this.pathDataStore.has(pathId)) {
28161
+ var pd = new PathData(pathId, from, to);
28162
+ this.pathDataStore.set(pathId, pd);
28163
+ }
28164
+ return this.pathDataStore.get(pathId);
28165
+ }
28166
+
28024
28167
  /**
28025
28168
  * Initialize pathfinder and create paths
28026
28169
  */
@@ -28028,12 +28171,33 @@ var PathfindingManager = /*#__PURE__*/function (_BaseDisposable) {
28028
28171
  key: "initializePathfinder",
28029
28172
  value: (function () {
28030
28173
  var _initializePathfinder = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(data, crosscubeTextureSet) {
28031
- var pathfindingResult;
28174
+ var _this5 = this,
28175
+ _this$sceneViewer;
28176
+ var pathfindingResult, originalToSubPaths, fromEndpointAttrs, flowMgr;
28032
28177
  return _regenerator().w(function (_context2) {
28033
28178
  while (1) switch (_context2.n) {
28034
28179
  case 0:
28035
28180
  this.crosscubeTextureSet = crosscubeTextureSet;
28036
28181
 
28182
+ // ── Pre-load declared flowAttributes from connections JSON ─────────────
28183
+ // This must happen before pathfinding so that PathData entries carrying
28184
+ // flowAttributes are available for visualization right after rendering.
28185
+ if (Array.isArray(data.connections)) {
28186
+ data.connections.forEach(function (conn) {
28187
+ if (conn.flowAttributes && _typeof(conn.flowAttributes) === 'object') {
28188
+ var pathId = "".concat(conn.from, "-->").concat(conn.to);
28189
+ var pd = _this5._getOrCreatePathData(pathId, conn.from, conn.to);
28190
+ Object.entries(conn.flowAttributes).forEach(function (_ref) {
28191
+ var _ref2 = _slicedToArray(_ref, 2),
28192
+ key = _ref2[0],
28193
+ value = _ref2[1];
28194
+ pd.setFlowAttribute(key, value);
28195
+ });
28196
+ console.log("\uD83C\uDF0A Loaded flowAttributes for path \"".concat(pathId, "\":"), conn.flowAttributes);
28197
+ }
28198
+ });
28199
+ }
28200
+
28037
28201
  // Use shared pathfinding logic with gateway creation enabled
28038
28202
  _context2.n = 1;
28039
28203
  return this._executePathfinding(data.scene, data.connections, {
@@ -28042,6 +28206,83 @@ var PathfindingManager = /*#__PURE__*/function (_BaseDisposable) {
28042
28206
  });
28043
28207
  case 1:
28044
28208
  pathfindingResult = _context2.v;
28209
+ // ── Propagate flowAttributes to gateway-split rendered paths ──────────
28210
+ // The pathfinder may split a connection A→B through gateway waypoints,
28211
+ // producing sub-paths like A→G and G→B with different pathIds.
28212
+ // We need to copy the original connection's flowAttributes onto every
28213
+ // rendered sub-path entry in pathDataStore so the visualisation can
28214
+ // find them by their rendered pathId.
28215
+ if (Array.isArray(data.connections) && pathfindingResult) {
28216
+ // Pass 1: use the explicit gateway connection mappings to get a precise
28217
+ // original→sub-path map.
28218
+ originalToSubPaths = new Map(); // origPathId → Set<renderedPathId>
28219
+ if (pathfindingResult.gateways) {
28220
+ pathfindingResult.gateways.forEach(function (gateway) {
28221
+ var _ref3 = gateway.connections || {},
28222
+ removed = _ref3.removed,
28223
+ added = _ref3.added;
28224
+ if (!(removed !== null && removed !== void 0 && removed.length) || !(added !== null && added !== void 0 && added.length)) return;
28225
+ removed.forEach(function (removedConn) {
28226
+ var origId = "".concat(removedConn.from, "-->").concat(removedConn.to);
28227
+ if (!originalToSubPaths.has(origId)) originalToSubPaths.set(origId, new Set());
28228
+ added.forEach(function (addedConn) {
28229
+ originalToSubPaths.get(origId).add("".concat(addedConn.from, "-->").concat(addedConn.to));
28230
+ });
28231
+ });
28232
+ });
28233
+ }
28234
+ data.connections.forEach(function (conn) {
28235
+ if (!conn.flowAttributes) return;
28236
+ var origId = "".concat(conn.from, "-->").concat(conn.to);
28237
+ var subPathIds = new Set(originalToSubPaths.get(origId) || []);
28238
+ subPathIds.add(origId); // include the direct path if it wasn't rewired
28239
+
28240
+ subPathIds.forEach(function (subPathId) {
28241
+ var pd = _this5.pathDataStore.get(subPathId);
28242
+ if (pd && Object.keys(pd.flowAttributes).length === 0) {
28243
+ Object.entries(conn.flowAttributes).forEach(function (_ref4) {
28244
+ var _ref5 = _slicedToArray(_ref4, 2),
28245
+ key = _ref5[0],
28246
+ value = _ref5[1];
28247
+ pd.setFlowAttribute(key, value);
28248
+ });
28249
+ console.log("\uD83C\uDF0A Propagated flowAttributes to sub-path \"".concat(subPathId, "\" from \"").concat(origId, "\""));
28250
+ }
28251
+ });
28252
+ });
28253
+
28254
+ // Pass 2: endpoint fallback for any rendered path still missing attributes
28255
+ // (covers Gateway→Gateway intermediate segments not captured by gateway.connections.added)
28256
+ fromEndpointAttrs = new Map(); // connectorId → flowAttributes
28257
+ data.connections.forEach(function (conn) {
28258
+ if (conn.flowAttributes && !fromEndpointAttrs.has(conn.from)) {
28259
+ fromEndpointAttrs.set(conn.from, conn.flowAttributes);
28260
+ }
28261
+ });
28262
+ this.pathDataStore.forEach(function (pd, pathId) {
28263
+ if (Object.keys(pd.flowAttributes).length > 0) return;
28264
+ var sepIdx = pathId.indexOf('-->');
28265
+ if (sepIdx === -1) return;
28266
+ var from = pathId.slice(0, sepIdx);
28267
+ var attrs = fromEndpointAttrs.get(from);
28268
+ if (attrs) {
28269
+ Object.entries(attrs).forEach(function (_ref6) {
28270
+ var _ref7 = _slicedToArray(_ref6, 2),
28271
+ key = _ref7[0],
28272
+ value = _ref7[1];
28273
+ return pd.setFlowAttribute(key, value);
28274
+ });
28275
+ console.log("\uD83C\uDF0A Endpoint-matched flowAttributes for path \"".concat(pathId, "\""));
28276
+ }
28277
+ });
28278
+ }
28279
+
28280
+ // ── Apply flow visualizations now that paths are rendered ──────────────
28281
+ flowMgr = (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.managers) === null || _this$sceneViewer === void 0 ? void 0 : _this$sceneViewer.pathFlowManager;
28282
+ if (flowMgr) {
28283
+ flowMgr.applyAllVisualizations();
28284
+ }
28285
+
28045
28286
  // Update connections with rewired connections
28046
28287
  if (pathfindingResult.rewiredConnections) {
28047
28288
  // data.connections = pathfindingResult.rewiredConnections;
@@ -28221,6 +28462,262 @@ var PathfindingManager = /*#__PURE__*/function (_BaseDisposable) {
28221
28462
  }]);
28222
28463
  }(BaseDisposable);
28223
28464
 
28465
+ // ── Temperature visualisation constants ────────────────────────────────────
28466
+
28467
+ /** Minimum temperature in the colour ramp (maps to pure blue). */
28468
+ var TEMP_MIN = 0;
28469
+
28470
+ /** Maximum temperature in the colour ramp (maps to pure red). */
28471
+ var TEMP_MAX = 100;
28472
+
28473
+ /**
28474
+ * Convert a temperature value to a CSS hex colour string using a blue→red HSL
28475
+ * ramp. Values are clamped to [TEMP_MIN, TEMP_MAX].
28476
+ *
28477
+ * • TEMP_MIN → hsl(240, 80%, 50%) — blue
28478
+ * • TEMP_MAX → hsl(0, 80%, 50%) — red
28479
+ *
28480
+ * @param {number} temp
28481
+ * @returns {string} CSS hex colour string e.g. '#2255cc'
28482
+ */
28483
+ function temperatureToColor(temp) {
28484
+ var clamped = Math.max(TEMP_MIN, Math.min(TEMP_MAX, temp));
28485
+ var t = (clamped - TEMP_MIN) / (TEMP_MAX - TEMP_MIN); // 0 (cold) → 1 (hot)
28486
+ // Map t=0 → hue 240 (blue), t=1 → hue 0 (red)
28487
+ var hue = Math.round(240 - t * 240);
28488
+ return "hsl(".concat(hue, ", 80%, 50%)");
28489
+ }
28490
+
28491
+ // ── PathFlowManager ─────────────────────────────────────────────────────────
28492
+
28493
+ var PathFlowManager = /*#__PURE__*/function (_BaseDisposable) {
28494
+ /**
28495
+ * @param {Object} sceneViewer - The central sceneViewer hub
28496
+ */
28497
+ function PathFlowManager(sceneViewer) {
28498
+ var _this;
28499
+ _classCallCheck(this, PathFlowManager);
28500
+ _this = _callSuper(this, PathFlowManager);
28501
+ _this.sceneViewer = sceneViewer;
28502
+
28503
+ /**
28504
+ * Runtime-only attribute overrides.
28505
+ * Map<pathId, Record<attributeKey, value>>
28506
+ * Cleared when the session ends or resetDerived() is called.
28507
+ * @type {Map<string, Record<string, any>>}
28508
+ */
28509
+ _this._derivedStore = new Map();
28510
+ return _this;
28511
+ }
28512
+
28513
+ // ── Public API — attribute write ──────────────────────────────────────────
28514
+
28515
+ /**
28516
+ * Set a declared (persistent) flow attribute on a path.
28517
+ * The value is stored in PathData and will be serialised with the scene.
28518
+ * Triggers a visual update.
28519
+ *
28520
+ * @param {string} pathId - e.g. "PUMP-1-CONN-1-->CHILLER-1-CONN-2"
28521
+ * @param {string} key - One of FLOW_ATTRIBUTE_KEYS
28522
+ * @param {any} value
28523
+ */
28524
+ _inherits(PathFlowManager, _BaseDisposable);
28525
+ return _createClass(PathFlowManager, [{
28526
+ key: "setDeclared",
28527
+ value: function setDeclared(pathId, key, value) {
28528
+ var pd = this._getPathData(pathId);
28529
+ if (!pd) {
28530
+ console.warn("PathFlowManager.setDeclared: no PathData found for \"".concat(pathId, "\""));
28531
+ return;
28532
+ }
28533
+ pd.setFlowAttribute(key, value);
28534
+ this.applyVisualizationForPath(pathId);
28535
+ }
28536
+
28537
+ /**
28538
+ * Set a derived (runtime) flow attribute on a path.
28539
+ * Overrides the declared value during this session but is not persisted.
28540
+ * Triggers a visual update.
28541
+ *
28542
+ * @param {string} pathId
28543
+ * @param {string} key
28544
+ * @param {any} value
28545
+ */
28546
+ }, {
28547
+ key: "setDerived",
28548
+ value: function setDerived(pathId, key, value) {
28549
+ if (!this._derivedStore.has(pathId)) {
28550
+ this._derivedStore.set(pathId, {});
28551
+ }
28552
+ this._derivedStore.get(pathId)[key] = value;
28553
+ this.applyVisualizationForPath(pathId);
28554
+ }
28555
+
28556
+ /**
28557
+ * Clear derived overrides for one path or all paths, then re-apply
28558
+ * visualizations so that declared values take effect again.
28559
+ *
28560
+ * @param {string} [pathId] - Omit to reset all paths.
28561
+ */
28562
+ }, {
28563
+ key: "resetDerived",
28564
+ value: function resetDerived(pathId) {
28565
+ var _this2 = this;
28566
+ if (pathId !== undefined) {
28567
+ this._derivedStore.delete(pathId);
28568
+ this.applyVisualizationForPath(pathId);
28569
+ } else {
28570
+ var affected = _toConsumableArray(this._derivedStore.keys());
28571
+ this._derivedStore.clear();
28572
+ affected.forEach(function (id) {
28573
+ return _this2.applyVisualizationForPath(id);
28574
+ });
28575
+ }
28576
+ }
28577
+
28578
+ // ── Public API — attribute read ───────────────────────────────────────────
28579
+
28580
+ /**
28581
+ * Resolve the effective value of a single attribute for a path.
28582
+ * Resolution order: derived > declared > null.
28583
+ *
28584
+ * @param {string} pathId
28585
+ * @param {string} key
28586
+ * @returns {any|null}
28587
+ */
28588
+ }, {
28589
+ key: "resolve",
28590
+ value: function resolve(pathId, key) {
28591
+ var derived = this._derivedStore.get(pathId);
28592
+ if (derived && Object.prototype.hasOwnProperty.call(derived, key)) {
28593
+ return derived[key];
28594
+ }
28595
+ var pd = this._getPathData(pathId);
28596
+ if (pd) {
28597
+ return pd.getFlowAttribute(key);
28598
+ }
28599
+ return null;
28600
+ }
28601
+
28602
+ /**
28603
+ * Resolve all four flow attributes for a path as a plain object.
28604
+ * Each key is either the effective value or null if unset.
28605
+ *
28606
+ * @param {string} pathId
28607
+ * @returns {{ flowDirection: any, flowSpeed: any, flowTemperature: any, flowMaterial: any }}
28608
+ */
28609
+ }, {
28610
+ key: "resolveAll",
28611
+ value: function resolveAll(pathId) {
28612
+ var _this3 = this;
28613
+ return Object.fromEntries(FLOW_ATTRIBUTE_KEYS.map(function (key) {
28614
+ return [key, _this3.resolve(pathId, key)];
28615
+ }));
28616
+ }
28617
+
28618
+ // ── Visualization ─────────────────────────────────────────────────────────
28619
+
28620
+ /**
28621
+ * Compute and apply the visual colour for a single path based on its resolved
28622
+ * flow attributes. Currently maps flowTemperature to a blue→red colour ramp.
28623
+ * No-ops gracefully if the path has no renderable attributes or no pipe
28624
+ * material exists yet.
28625
+ *
28626
+ * @param {string} pathId
28627
+ */
28628
+ }, {
28629
+ key: "applyVisualizationForPath",
28630
+ value: function applyVisualizationForPath(pathId) {
28631
+ var temp = this.resolve(pathId, 'flowTemperature');
28632
+ if (temp === null || temp === undefined) {
28633
+ return; // No temperature declared — leave default pipe color
28634
+ }
28635
+ var color = temperatureToColor(temp);
28636
+ var renderingManager = this._getRenderingManager();
28637
+ if (!renderingManager) return;
28638
+
28639
+ // pathId = "from-->to"; extract the two halves
28640
+ var sepIdx = pathId.indexOf('-->');
28641
+ if (sepIdx === -1) {
28642
+ console.warn("PathFlowManager: malformed pathId \"".concat(pathId, "\""));
28643
+ return;
28644
+ }
28645
+ var from = pathId.slice(0, sepIdx);
28646
+ var to = pathId.slice(sepIdx + 3);
28647
+ renderingManager.updatePathColor(from, to, color);
28648
+ console.log("\uD83C\uDF21\uFE0F PathFlowManager: \"".concat(pathId, "\" \u2192 flowTemperature ").concat(temp, " \u2192 ").concat(color));
28649
+ }
28650
+
28651
+ /**
28652
+ * Apply visualizations for every known path (union of pathDataStore and derived store).
28653
+ * Call this after scene load or after bulk attribute changes.
28654
+ */
28655
+ }, {
28656
+ key: "applyAllVisualizations",
28657
+ value: function applyAllVisualizations() {
28658
+ var _this4 = this;
28659
+ var pathIds = new Set();
28660
+ var pfMgr = this._getPathfindingManager();
28661
+ if (pfMgr !== null && pfMgr !== void 0 && pfMgr.pathDataStore) {
28662
+ pfMgr.pathDataStore.forEach(function (_, id) {
28663
+ return pathIds.add(id);
28664
+ });
28665
+ }
28666
+ this._derivedStore.forEach(function (_, id) {
28667
+ return pathIds.add(id);
28668
+ });
28669
+ pathIds.forEach(function (id) {
28670
+ return _this4.applyVisualizationForPath(id);
28671
+ });
28672
+ console.log("\uD83C\uDF0A PathFlowManager.applyAllVisualizations: processed ".concat(pathIds.size, " path(s)"));
28673
+ }
28674
+
28675
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
28676
+ }, {
28677
+ key: "dispose",
28678
+ value: function dispose() {
28679
+ this._derivedStore.clear();
28680
+ this.sceneViewer = null;
28681
+ _superPropGet(PathFlowManager, "dispose", this, 3)([]);
28682
+ }
28683
+
28684
+ // ── Private helpers ───────────────────────────────────────────────────────
28685
+
28686
+ /**
28687
+ * @returns {import('../../core/pathfindingData.js').PathData|null}
28688
+ * @private
28689
+ */
28690
+ }, {
28691
+ key: "_getPathData",
28692
+ value: function _getPathData(pathId) {
28693
+ var _this$_getPathfinding, _this$_getPathfinding2;
28694
+ return (_this$_getPathfinding = (_this$_getPathfinding2 = this._getPathfindingManager()) === null || _this$_getPathfinding2 === void 0 || (_this$_getPathfinding2 = _this$_getPathfinding2.pathDataStore) === null || _this$_getPathfinding2 === void 0 ? void 0 : _this$_getPathfinding2.get(pathId)) !== null && _this$_getPathfinding !== void 0 ? _this$_getPathfinding : null;
28695
+ }
28696
+
28697
+ /**
28698
+ * @returns {import('../pathfinding/pathfindingManager.js').PathfindingManager|null}
28699
+ * @private
28700
+ */
28701
+ }, {
28702
+ key: "_getPathfindingManager",
28703
+ value: function _getPathfindingManager() {
28704
+ var _this$sceneViewer$man, _this$sceneViewer;
28705
+ return (_this$sceneViewer$man = (_this$sceneViewer = this.sceneViewer) === null || _this$sceneViewer === void 0 || (_this$sceneViewer = _this$sceneViewer.managers) === null || _this$sceneViewer === void 0 ? void 0 : _this$sceneViewer.pathfindingManager) !== null && _this$sceneViewer$man !== void 0 ? _this$sceneViewer$man : null;
28706
+ }
28707
+
28708
+ /**
28709
+ * @returns {import('../pathfinding/PathRenderingManager.js').PathRenderingManager|null}
28710
+ * @private
28711
+ */
28712
+ }, {
28713
+ key: "_getRenderingManager",
28714
+ value: function _getRenderingManager() {
28715
+ var _this$_getPathfinding3, _this$_getPathfinding4;
28716
+ return (_this$_getPathfinding3 = (_this$_getPathfinding4 = this._getPathfindingManager()) === null || _this$_getPathfinding4 === void 0 ? void 0 : _this$_getPathfinding4.renderingManager) !== null && _this$_getPathfinding3 !== void 0 ? _this$_getPathfinding3 : null;
28717
+ }
28718
+ }]);
28719
+ }(BaseDisposable);
28720
+
28224
28721
  var BehaviorManager = /*#__PURE__*/function (_BaseDisposable) {
28225
28722
  function BehaviorManager(sceneViewer) {
28226
28723
  var _this;
@@ -36042,6 +36539,59 @@ var Viewport2DManager = /*#__PURE__*/function (_BaseDisposable2) {
36042
36539
  }]);
36043
36540
  }(BaseDisposable);
36044
36541
 
36542
+ // ─────────────────────────────────────────────────────────────────────────────
36543
+ // Flow-direction helpers (module-level)
36544
+ // ─────────────────────────────────────────────────────────────────────────────
36545
+
36546
+ /**
36547
+ * Returns the flow direction of a connector from the current scene data.
36548
+ * @param {Object} sceneData - currentSceneData object
36549
+ * @param {string} connectorId
36550
+ * @returns {'in'|'out'|'bi'} Defaults to 'bi' if not set.
36551
+ */
36552
+ function _getConnectorFlow(sceneData, connectorId) {
36553
+ var _sceneData$scene;
36554
+ var children = (sceneData === null || sceneData === void 0 || (_sceneData$scene = sceneData.scene) === null || _sceneData$scene === void 0 ? void 0 : _sceneData$scene.children) || [];
36555
+ var _iterator = _createForOfIteratorHelper(children),
36556
+ _step;
36557
+ try {
36558
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
36559
+ var component = _step.value;
36560
+ var _iterator2 = _createForOfIteratorHelper(component.children || []),
36561
+ _step2;
36562
+ try {
36563
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
36564
+ var _child$userData;
36565
+ var child = _step2.value;
36566
+ if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'connector' && child.uuid === connectorId) {
36567
+ return child.userData.flow || 'bi';
36568
+ }
36569
+ }
36570
+ } catch (err) {
36571
+ _iterator2.e(err);
36572
+ } finally {
36573
+ _iterator2.f();
36574
+ }
36575
+ }
36576
+ } catch (err) {
36577
+ _iterator.e(err);
36578
+ } finally {
36579
+ _iterator.f();
36580
+ }
36581
+ return 'bi';
36582
+ }
36583
+
36584
+ /**
36585
+ * Returns true if fromFlow → toFlow is a valid connection.
36586
+ * @param {'in'|'out'|'bi'} fromFlow
36587
+ * @param {'in'|'out'|'bi'} toFlow
36588
+ * @returns {boolean}
36589
+ */
36590
+ function _areFlowsCompatible$1(fromFlow, toFlow) {
36591
+ if (fromFlow === 'bi' || toFlow === 'bi') return true;
36592
+ return fromFlow !== toFlow;
36593
+ }
36594
+
36045
36595
  /**
36046
36596
  * CentralPlantInternals class containing internal methods and functionality
36047
36597
  */
@@ -36103,6 +36653,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36103
36653
  this.centralPlant.managers.environmentManager = new EnvironmentManager(this.centralPlant.sceneViewer);
36104
36654
  this.centralPlant.managers.keyboardControlsManager = new KeyboardControlsManager(this.centralPlant.sceneViewer);
36105
36655
  this.centralPlant.managers.pathfindingManager = new PathfindingManager(this.centralPlant.sceneViewer);
36656
+ this.centralPlant.managers.pathFlowManager = new PathFlowManager(this.centralPlant.sceneViewer);
36106
36657
  this.centralPlant.managers.behaviorManager = new BehaviorManager(this.centralPlant.sceneViewer);
36107
36658
  this.centralPlant.managers.sceneOperationsManager = new SceneOperationsManager(this.centralPlant.sceneViewer);
36108
36659
  this.centralPlant.managers.animationManager = new AnimationManager(this.centralPlant.sceneViewer);
@@ -36493,12 +37044,12 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36493
37044
  console.log("\uD83D\uDD27 Translating ".concat(selectedObjects.length, " selected object(s) on ").concat(axis, " axis by ").concat(value));
36494
37045
 
36495
37046
  // Translate each selected object using the appropriate method
36496
- var _iterator = _createForOfIteratorHelper(selectedObjects),
36497
- _step;
37047
+ var _iterator3 = _createForOfIteratorHelper(selectedObjects),
37048
+ _step3;
36498
37049
  try {
36499
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
37050
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
36500
37051
  var _obj$userData;
36501
- var obj = _step.value;
37052
+ var obj = _step3.value;
36502
37053
  var objectType = (_obj$userData = obj.userData) === null || _obj$userData === void 0 ? void 0 : _obj$userData.objectType;
36503
37054
  var objectId = obj.uuid;
36504
37055
  var success = false;
@@ -36525,9 +37076,9 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36525
37076
  }
36526
37077
  }
36527
37078
  } catch (err) {
36528
- _iterator.e(err);
37079
+ _iterator3.e(err);
36529
37080
  } finally {
36530
- _iterator.f();
37081
+ _iterator3.f();
36531
37082
  }
36532
37083
  result.success = result.translatedCount === result.totalCount;
36533
37084
  if (result.success) {
@@ -36711,7 +37262,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36711
37262
  }, {
36712
37263
  key: "addConnection",
36713
37264
  value: function addConnection(fromConnectorId, toConnectorId) {
36714
- var _this$centralPlant$sc4;
37265
+ var _this$centralPlant$sc4, _this$centralPlant$sc5;
36715
37266
  // Use centralized validation for connection parameters
36716
37267
  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) || [];
36717
37268
  var validation = this.validator.validateConnectionParams(fromConnectorId, toConnectorId, existingConnections);
@@ -36719,6 +37270,17 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36719
37270
  return false; // Validator already logged the error
36720
37271
  }
36721
37272
 
37273
+ // Validate flow direction compatibility
37274
+ var sceneData = (_this$centralPlant$sc5 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc5 === void 0 ? void 0 : _this$centralPlant$sc5.currentSceneData;
37275
+ if (sceneData) {
37276
+ var fromFlow = _getConnectorFlow(sceneData, fromConnectorId);
37277
+ var toFlow = _getConnectorFlow(sceneData, toConnectorId);
37278
+ if (!_areFlowsCompatible$1(fromFlow, toFlow)) {
37279
+ 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."));
37280
+ return false;
37281
+ }
37282
+ }
37283
+
36722
37284
  // Validate scene availability
36723
37285
  var sceneValidation = this.validator.validateSceneViewer(this.centralPlant.sceneViewer);
36724
37286
  if (!sceneValidation.isValid) {
@@ -36839,7 +37401,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36839
37401
  }, {
36840
37402
  key: "addComponent",
36841
37403
  value: function addComponent(libraryId) {
36842
- var _this$centralPlant$sc5;
37404
+ var _this$centralPlant$sc6;
36843
37405
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
36844
37406
  // Use centralized validation for component addition parameters
36845
37407
  var existingIds = this.getComponentIds();
@@ -36849,7 +37411,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36849
37411
  }
36850
37412
 
36851
37413
  // Validate scene availability
36852
- 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);
37414
+ 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);
36853
37415
  if (!sceneValidation.isValid) {
36854
37416
  return false;
36855
37417
  }
@@ -36868,7 +37430,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36868
37430
  return false;
36869
37431
  }
36870
37432
  try {
36871
- var _componentData$childr, _componentData$childr2, _this$centralPlant$sc6, _componentData$childr3, _componentData$defaul;
37433
+ var _componentData$childr, _componentData$childr2, _this$centralPlant$sc7, _componentData$childr3, _componentData$defaul;
36872
37434
  // Generate a unique component ID if not provided
36873
37435
  var componentId = options.customId || this.generateUniqueComponentId(libraryId);
36874
37436
 
@@ -36980,7 +37542,7 @@ var CentralPlantInternals = /*#__PURE__*/function () {
36980
37542
  componentModel.updateMatrixWorld(true);
36981
37543
 
36982
37544
  // Check if component is underground and fix if needed (based on settings)
36983
- 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');
37545
+ 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');
36984
37546
  if (checkUnderground) {
36985
37547
  var wasFixed = this.fixUndergroundComponent(componentModel);
36986
37548
  if (wasFixed) {
@@ -37078,8 +37640,8 @@ var CentralPlantInternals = /*#__PURE__*/function () {
37078
37640
  // responds to tooltip-driven state changes immediately after drop.
37079
37641
  // (The scene-load path uses _processBehaviors instead, which runs on loadSceneData.)
37080
37642
  if ((_componentData$defaul = componentData.defaultBehaviors) !== null && _componentData$defaul !== void 0 && _componentData$defaul.length) {
37081
- var _this$centralPlant$sc7, _som$registerBehavior;
37082
- var som = (_this$centralPlant$sc7 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc7 === void 0 ? void 0 : _this$centralPlant$sc7.sceneOperationsManager;
37643
+ var _this$centralPlant$sc8, _som$registerBehavior;
37644
+ var som = (_this$centralPlant$sc8 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc8 === void 0 ? void 0 : _this$centralPlant$sc8.sceneOperationsManager;
37083
37645
  som === null || som === void 0 || (_som$registerBehavior = som.registerBehaviorsForComponentInstance) === null || _som$registerBehavior === void 0 || _som$registerBehavior.call(som, componentData, componentId);
37084
37646
  }
37085
37647
 
@@ -37141,9 +37703,9 @@ var CentralPlantInternals = /*#__PURE__*/function () {
37141
37703
  }, {
37142
37704
  key: "deleteComponent",
37143
37705
  value: function deleteComponent(componentId) {
37144
- var _this$centralPlant$sc8;
37706
+ var _this$centralPlant$sc9;
37145
37707
  // Check if component manager is available
37146
- var componentManager = (_this$centralPlant$sc8 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc8 === void 0 ? void 0 : _this$centralPlant$sc8.componentManager;
37708
+ var componentManager = (_this$centralPlant$sc9 = this.centralPlant.sceneViewer) === null || _this$centralPlant$sc9 === void 0 ? void 0 : _this$centralPlant$sc9.componentManager;
37147
37709
  if (!componentManager) {
37148
37710
  console.error('❌ deleteComponent(): Component manager not available');
37149
37711
  return false;
@@ -37218,8 +37780,8 @@ var CentralPlantInternals = /*#__PURE__*/function () {
37218
37780
  }
37219
37781
  var componentIds = [];
37220
37782
  this.centralPlant.sceneViewer.scene.traverse(function (child) {
37221
- var _child$userData;
37222
- if (((_child$userData = child.userData) === null || _child$userData === void 0 ? void 0 : _child$userData.objectType) === 'component') {
37783
+ var _child$userData2;
37784
+ if (((_child$userData2 = child.userData) === null || _child$userData2 === void 0 ? void 0 : _child$userData2.objectType) === 'component') {
37223
37785
  componentIds.push(child.uuid || child.userData.originalUuid);
37224
37786
  }
37225
37787
  });
@@ -37228,6 +37790,22 @@ var CentralPlantInternals = /*#__PURE__*/function () {
37228
37790
  }]);
37229
37791
  }();
37230
37792
 
37793
+ // ─────────────────────────────────────────────────────────────────────────────
37794
+ // Flow-direction compatibility helper (module-level, no class dependency)
37795
+ // ─────────────────────────────────────────────────────────────────────────────
37796
+
37797
+ /**
37798
+ * Returns true if the two flow directions are compatible for a connection.
37799
+ * @param {string} fromFlow - 'in' | 'out' | 'bi'
37800
+ * @param {string} toFlow - 'in' | 'out' | 'bi'
37801
+ * @returns {boolean}
37802
+ */
37803
+ function _areFlowsCompatible(fromFlow, toFlow) {
37804
+ if (fromFlow === 'bi' || toFlow === 'bi') return true;
37805
+ // in ↔ out are compatible; in → in and out → out are not
37806
+ return fromFlow !== toFlow;
37807
+ }
37808
+
37231
37809
  /**
37232
37810
  * CentralPlant class that manages all scene utility instances and provides public API
37233
37811
  *
@@ -37238,7 +37816,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
37238
37816
  * Initialize the CentralPlant manager
37239
37817
  *
37240
37818
  * @constructor
37241
- * @version 0.2.5
37819
+ * @version 0.2.10
37242
37820
  * @updated 2025-10-22
37243
37821
  *
37244
37822
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -38171,6 +38749,107 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
38171
38749
  return availableConnectorIds;
38172
38750
  }
38173
38751
 
38752
+ /**
38753
+ * Get available connectors with their flow direction metadata.
38754
+ * Same filtering logic as getAvailableConnections() but returns objects instead of strings.
38755
+ * @returns {Array<{id: string, flow: string}>} Array of connector info objects.
38756
+ * flow is one of 'in', 'out', 'bi'. Defaults to 'bi' if not set in userData.
38757
+ * @example
38758
+ * const infos = centralPlant.getAvailableConnectionsInfo()
38759
+ * // [{ id: 'PUMP-1-CONNECTOR-1', flow: 'out' }, ...]
38760
+ */
38761
+ }, {
38762
+ key: "getAvailableConnectionsInfo",
38763
+ value: function getAvailableConnectionsInfo() {
38764
+ if (!this.sceneViewer || !this.sceneViewer.currentSceneData) {
38765
+ console.warn('⚠️ getAvailableConnectionsInfo(): Scene viewer or current scene data not available');
38766
+ return [];
38767
+ }
38768
+ var sceneData = this.sceneViewer.currentSceneData;
38769
+ if (!sceneData.scene || !sceneData.scene.children) {
38770
+ console.warn('⚠️ getAvailableConnectionsInfo(): Invalid scene data structure');
38771
+ return [];
38772
+ }
38773
+ var allConnectorInfos = [];
38774
+ sceneData.scene.children.forEach(function (component) {
38775
+ if (component.children && Array.isArray(component.children)) {
38776
+ component.children.forEach(function (child) {
38777
+ if (child.userData && child.userData.objectType === 'connector' && child.uuid) {
38778
+ allConnectorInfos.push({
38779
+ id: child.uuid,
38780
+ flow: child.userData.flow || 'bi'
38781
+ });
38782
+ }
38783
+ });
38784
+ }
38785
+ });
38786
+ var existingConnections = this.getConnections();
38787
+ var usedConnectorIds = new Set();
38788
+ existingConnections.forEach(function (connection) {
38789
+ if (connection.from) usedConnectorIds.add(connection.from);
38790
+ if (connection.to) usedConnectorIds.add(connection.to);
38791
+ });
38792
+ return allConnectorInfos.filter(function (info) {
38793
+ return !usedConnectorIds.has(info.id);
38794
+ });
38795
+ }
38796
+
38797
+ /**
38798
+ * Validate all connections in the current scene for flow direction compatibility.
38799
+ * @returns {{ valid: Array<Object>, invalid: Array<{connection: Object, reason: string}> }}
38800
+ * @example
38801
+ * const result = centralPlant.validateConnections()
38802
+ * result.invalid.forEach(({ connection, reason }) => console.warn(reason, connection))
38803
+ */
38804
+ }, {
38805
+ key: "validateConnections",
38806
+ value: function validateConnections() {
38807
+ if (!this.sceneViewer || !this.sceneViewer.currentSceneData) {
38808
+ console.warn('⚠️ validateConnections(): Scene viewer or current scene data not available');
38809
+ return {
38810
+ valid: [],
38811
+ invalid: []
38812
+ };
38813
+ }
38814
+ var connections = this.getConnections();
38815
+ var sceneData = this.sceneViewer.currentSceneData;
38816
+
38817
+ // Build lookup map: connectorId → flow
38818
+ var flowMap = {};
38819
+ var scene = sceneData.scene || {};
38820
+ var children = scene.children || [];
38821
+ children.forEach(function (component) {
38822
+ if (component.children && Array.isArray(component.children)) {
38823
+ component.children.forEach(function (child) {
38824
+ if (child.userData && child.userData.objectType === 'connector' && child.uuid) {
38825
+ flowMap[child.uuid] = child.userData.flow || 'bi';
38826
+ }
38827
+ });
38828
+ }
38829
+ });
38830
+ var valid = [];
38831
+ var invalid = [];
38832
+ connections.forEach(function (connection) {
38833
+ var fromFlow = flowMap[connection.from] || 'bi';
38834
+ var toFlow = flowMap[connection.to] || 'bi';
38835
+ if (_areFlowsCompatible(fromFlow, toFlow)) {
38836
+ valid.push(connection);
38837
+ } else {
38838
+ 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");
38839
+ console.warn("\u26A0\uFE0F validateConnections(): ".concat(reason));
38840
+ invalid.push({
38841
+ connection: connection,
38842
+ reason: reason
38843
+ });
38844
+ }
38845
+ });
38846
+ console.log("\u2705 validateConnections(): ".concat(valid.length, " valid, ").concat(invalid.length, " invalid connections"));
38847
+ return {
38848
+ valid: valid,
38849
+ invalid: invalid
38850
+ };
38851
+ }
38852
+
38174
38853
  // ─────────────────────────────────────────────────────────────────────────
38175
38854
  // BEHAVIORS API
38176
38855
  // ─────────────────────────────────────────────────────────────────────────
@@ -40154,7 +40833,7 @@ var sceneViewer = /*#__PURE__*/function (_BaseDisposable) {
40154
40833
  this.centralPlant.attachToComponent();
40155
40834
 
40156
40835
  // Sync our managers tracking object after attachment
40157
- managerKeys = ['threeJSResourceManager', 'performanceMonitorManager', 'settingsManager', 'sceneExportManager', 'componentManager', 'sceneInitializationManager', 'environmentManager', 'keyboardControlsManager', 'pathfindingManager', 'behaviorManager', 'sceneOperationsManager', 'animationManager', 'cameraControlsManager', 'componentDragManager', 'tooltipsManager', 'componentTooltipManager']; // Populate our managers tracking object
40836
+ managerKeys = ['threeJSResourceManager', 'performanceMonitorManager', 'settingsManager', 'sceneExportManager', 'componentManager', 'sceneInitializationManager', 'environmentManager', 'keyboardControlsManager', 'pathfindingManager', 'pathFlowManager', 'behaviorManager', 'sceneOperationsManager', 'animationManager', 'cameraControlsManager', 'componentDragManager', 'tooltipsManager', 'componentTooltipManager']; // Populate our managers tracking object
40158
40837
  managerKeys.forEach(function (key) {
40159
40838
  if (_this2[key]) {
40160
40839
  _this2.managers[key] = _this2[key];
@@ -43770,10 +44449,12 @@ exports.ComponentDragManager = ComponentDragManager;
43770
44449
  exports.ComponentManager = ComponentManager;
43771
44450
  exports.ComponentTooltipManager = ComponentTooltipManager;
43772
44451
  exports.EnvironmentManager = EnvironmentManager;
44452
+ exports.FLOW_ATTRIBUTE_KEYS = FLOW_ATTRIBUTE_KEYS;
43773
44453
  exports.KeyboardControlsManager = KeyboardControlsManager;
43774
44454
  exports.ModelManager = ModelManager;
43775
44455
  exports.OperationHistoryManager = OperationHistoryManager;
43776
44456
  exports.PathData = PathData;
44457
+ exports.PathFlowManager = PathFlowManager;
43777
44458
  exports.PathfindingManager = PathfindingManager;
43778
44459
  exports.PerformanceMonitorManager = PerformanceMonitorManager;
43779
44460
  exports.Rendering2D = rendering2D;