@2112-lab/central-plant 0.1.45 → 0.1.46

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.
@@ -19579,6 +19579,7 @@ var TransformOperationsManager = /*#__PURE__*/function () {
19579
19579
  return null;
19580
19580
  }
19581
19581
  var collisionRadius = 0.5; // Radius around connector that triggers collision
19582
+ var endpointTolerance = 0.55; // Tolerance for determining if connector is at segment endpoint
19582
19583
 
19583
19584
  // Get segment endpoints in world coordinates
19584
19585
  var endpoints = this.calculateSegmentEndpoints(segment);
@@ -19597,6 +19598,13 @@ var TransformOperationsManager = /*#__PURE__*/function () {
19597
19598
  var connectorWorldPos = new THREE__namespace.Vector3();
19598
19599
  child.getWorldPosition(connectorWorldPos);
19599
19600
 
19601
+ // Skip connectors that are at the segment's endpoints (legitimate connection points)
19602
+ var distanceToStart = connectorWorldPos.distanceTo(startPoint);
19603
+ var distanceToEnd = connectorWorldPos.distanceTo(endPoint);
19604
+ if (distanceToStart <= endpointTolerance || distanceToEnd <= endpointTolerance) {
19605
+ return; // Skip this connector - it's at an endpoint
19606
+ }
19607
+
19600
19608
  // Find the closest point on the segment to the connector
19601
19609
  var segmentVector = new THREE__namespace.Vector3().subVectors(endPoint, startPoint);
19602
19610
  var pointVector = new THREE__namespace.Vector3().subVectors(connectorWorldPos, startPoint);
@@ -19795,6 +19803,37 @@ var TransformOperationsManager = /*#__PURE__*/function () {
19795
19803
  return c1.distanceTo(c2);
19796
19804
  }
19797
19805
 
19806
+ /**
19807
+ * Constrain a position to maintain orthogonal alignment with a reference position
19808
+ * Movement is restricted to only along the segment's direction vector
19809
+ * @param {THREE.Vector3} newPosition - The proposed new position
19810
+ * @param {THREE.Vector3} referencePosition - The stationary reference position
19811
+ * @param {THREE.Object3D} segment - The segment being adjusted
19812
+ * @returns {THREE.Vector3} Constrained position that maintains orthogonality
19813
+ * @private
19814
+ */
19815
+ }, {
19816
+ key: "constrainPositionToOrthogonal",
19817
+ value: function constrainPositionToOrthogonal(newPosition, referencePosition, segment) {
19818
+ // Get the segment's direction vector
19819
+ var direction = new THREE__namespace.Vector3(0, 1, 0);
19820
+ direction.applyQuaternion(segment.quaternion);
19821
+ direction.normalize();
19822
+
19823
+ // Calculate vector from reference position to new position
19824
+ var moveVector = new THREE__namespace.Vector3().subVectors(newPosition, referencePosition);
19825
+
19826
+ // Project the move vector onto the segment's direction
19827
+ // This gives us the component of movement along the segment's axis only
19828
+ var projectionLength = moveVector.dot(direction);
19829
+
19830
+ // Constrained position = reference + (projection along segment direction)
19831
+ var constrainedPos = new THREE__namespace.Vector3().copy(referencePosition).add(direction.clone().multiplyScalar(projectionLength));
19832
+ console.log("\uD83D\uDD12 Constrained movement along segment direction: [".concat(direction.x.toFixed(3), ", ").concat(direction.y.toFixed(3), ", ").concat(direction.z.toFixed(3), "]"));
19833
+ console.log(" Projection length: ".concat(projectionLength.toFixed(3)));
19834
+ return constrainedPos;
19835
+ }
19836
+
19798
19837
  /**
19799
19838
  * Update adjacent segment connectors to follow the moved segment's new position
19800
19839
  * Finds segments that were connected to the moved segment BEFORE the move,
@@ -19913,18 +19952,23 @@ var TransformOperationsManager = /*#__PURE__*/function () {
19913
19952
  var stationaryWorldPos = new THREE__namespace.Vector3();
19914
19953
  stationaryConnector.getWorldPosition(stationaryWorldPos);
19915
19954
 
19955
+ // Constrain movement to maintain orthogonal alignment
19956
+ // Horizontal segments: only adjust X and Y, keep Z constant
19957
+ // Vertical segments: only adjust Z, keep X and Y constant
19958
+ var constrainedPosition = _this2.constrainPositionToOrthogonal(pair.newPosition, stationaryWorldPos, adjacentSegment);
19959
+
19916
19960
  // Recreate the segment mesh with new length using explicit endpoint positions
19917
19961
  // Pass the intended positions, not the connector objects (which may have temporary positions)
19918
- _this2.recreateSegmentMeshWithNewLength(adjacentSegment, adjacentConnectors, pair.newPosition, stationaryWorldPos);
19962
+ _this2.recreateSegmentMeshWithNewLength(adjacentSegment, adjacentConnectors, constrainedPosition, stationaryWorldPos);
19919
19963
 
19920
19964
  // CRITICAL: After recreating the mesh, BOTH connectors need to be repositioned
19921
19965
  // because the segment's center and rotation have changed
19922
19966
 
19923
- // Position moving connector at the new position
19924
- var movingLocalPos = adjacentSegment.worldToLocal(pair.newPosition.clone());
19967
+ // Position moving connector at the constrained position (not the raw new position)
19968
+ var movingLocalPos = adjacentSegment.worldToLocal(constrainedPosition.clone());
19925
19969
  movingConnector.position.copy(movingLocalPos);
19926
19970
  movingConnector.updateMatrixWorld(true);
19927
- _this2.updateConnectorPositionInSceneData(movingConnector, pair.newPosition, adjacentSegment);
19971
+ _this2.updateConnectorPositionInSceneData(movingConnector, constrainedPosition, adjacentSegment);
19928
19972
 
19929
19973
  // Position stationary connector at its original world position
19930
19974
  var stationaryLocalPos = adjacentSegment.worldToLocal(stationaryWorldPos.clone());
@@ -24665,10 +24709,6 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
24665
24709
 
24666
24710
  var pipeRadius = 0.1;
24667
24711
  var pipeMaterial = this.createPipeMaterial(crosscubeTextureSet);
24668
-
24669
- // Shared geometry for all connector caps to improve performance
24670
- var capGeometry = new THREE__namespace.SphereGeometry(pipeRadius, 16, 16);
24671
- this.registerDisposable(capGeometry);
24672
24712
  paths.forEach(function (pathData, index) {
24673
24713
  if (pathData.path && pathData.path.length >= 2) {
24674
24714
  // Convert path points to Vector3 objects for consistent handling
@@ -24722,14 +24762,15 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
24722
24762
  pathTo: pathData.to
24723
24763
  };
24724
24764
 
24725
- // Create caps at both ends of every segment
24726
- _this3.createSegmentCaps(cylinder, capGeometry, pipeMaterial, globalSegmentIndex);
24727
-
24728
24765
  // Increment global segment counter
24729
24766
  globalSegmentIndex++;
24730
24767
 
24731
24768
  // Add segment directly to scene instead of to polyline
24732
24769
  sceneViewer.scene.add(cylinder);
24770
+
24771
+ // Add smooth elbow joints only at actual direction changes (not at every point)
24772
+ _this3.createAndAddElbowIfNeeded(pathPoints, j, pipeRadius, pipeMaterial, pathData, index, sceneViewer.scene // Pass scene instead of polyline
24773
+ );
24733
24774
  }
24734
24775
  console.log("\u2705 Created pipe path ".concat(pathData.from, "-").concat(pathData.to, " with ").concat(pathPoints.length - 1, " segments"));
24735
24776
  }
@@ -24737,49 +24778,130 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
24737
24778
  }
24738
24779
 
24739
24780
  /**
24740
- * Create connector caps at both segment endpoints as children of the segment
24741
- * Caps are positioned in local coordinates to match manual segment connector positioning
24742
- * @param {THREE.Mesh} segment - The parent segment mesh
24743
- * @param {THREE.BufferGeometry} capGeometry - Shared geometry for caps
24744
- * @param {THREE.Material} material - Material for the caps
24745
- * @param {number} segmentIndex - Global segment index
24781
+ * Create and add elbow joint if there's a direction change at the current segment
24782
+ * @param {Array<THREE.Vector3>} pathPoints - Array of path points
24783
+ * @param {number} j - Current segment index
24784
+ * @param {number} pipeRadius - Radius of the pipe
24785
+ * @param {THREE.Material} pipeMaterial - Material for the elbow
24786
+ * @param {Object} pathData - Path data object with from/to information
24787
+ * @param {number} index - Path index for naming
24788
+ * @param {THREE.Scene} scene - Scene object to add elbow to directly
24746
24789
  */
24747
24790
  }, {
24748
- key: "createSegmentCaps",
24749
- value: function createSegmentCaps(segment, capGeometry, material, segmentIndex) {
24750
- var length = segment.geometry.parameters.height;
24751
-
24752
- // Create start cap
24753
- var startCap = new THREE__namespace.Mesh(capGeometry, material);
24754
- // Position at segment start in local space (matches manual segment connector positioning)
24755
- startCap.position.set(0, -length / 2, 0);
24756
- startCap.uuid = "SEGMENT-".concat(segmentIndex, "-CAP-START");
24757
- startCap.userData = {
24758
- objectType: 'segment-cap',
24759
- capType: 'start',
24760
- segmentId: segment.uuid,
24761
- segmentIndex: segmentIndex
24762
- };
24763
- startCap.castShadow = true;
24764
- startCap.receiveShadow = true;
24765
- segment.add(startCap);
24766
- console.log("\uD83D\uDD35 Created START cap for segment ".concat(segmentIndex, " at local position (0, ").concat(-length / 2, ", 0)"));
24767
-
24768
- // Create end cap
24769
- var endCap = new THREE__namespace.Mesh(capGeometry, material);
24770
- // Position at segment end in local space (matches manual segment connector positioning)
24771
- endCap.position.set(0, length / 2, 0);
24772
- endCap.uuid = "SEGMENT-".concat(segmentIndex, "-CAP-END");
24773
- endCap.userData = {
24774
- objectType: 'segment-cap',
24775
- capType: 'end',
24776
- segmentId: segment.uuid,
24777
- segmentIndex: segmentIndex
24791
+ key: "createAndAddElbowIfNeeded",
24792
+ value: function createAndAddElbowIfNeeded(pathPoints, j, pipeRadius, pipeMaterial, pathData, index, scene) {
24793
+ // Only check if there are at least 2 more points ahead
24794
+ if (j >= pathPoints.length - 2) {
24795
+ return;
24796
+ }
24797
+ var currentSegment = pathPoints[j + 1].clone().sub(pathPoints[j]).normalize();
24798
+ var nextSegment = pathPoints[j + 2].clone().sub(pathPoints[j + 1]).normalize();
24799
+
24800
+ // Check if there's actually a direction change (not just continuing straight)
24801
+ var angle = currentSegment.angleTo(nextSegment);
24802
+ var minAngle = Math.PI / 36; // 5 degrees minimum for creating an elbow
24803
+
24804
+ if (angle <= minAngle) {
24805
+ return; // No significant direction change
24806
+ }
24807
+ var elbowGeometry = this.createElbowGeometry(pathPoints[j], pathPoints[j + 1], pathPoints[j + 2], pipeRadius);
24808
+ if (!elbowGeometry) {
24809
+ return; // Failed to create geometry
24810
+ }
24811
+ var elbow = new THREE__namespace.Mesh(elbowGeometry, pipeMaterial);
24812
+ elbow.castShadow = true;
24813
+ elbow.receiveShadow = true;
24814
+
24815
+ // Make elbows selectable as well
24816
+ var elbowId = "pipe-elbow-".concat(pathData.from, "-").concat(pathData.to, "-").concat(j);
24817
+ elbow.uuid = "Pipe Elbow ".concat(j + 1, ": ").concat(pathData.from, "-").concat(pathData.to);
24818
+
24819
+ // Add userData for elbows too
24820
+ elbow.userData = {
24821
+ isPipeElbow: true,
24822
+ elbowId: elbowId,
24823
+ elbowIndex: j,
24824
+ pathFrom: pathData.from,
24825
+ pathTo: pathData.to,
24826
+ angle: (angle * 180 / Math.PI).toFixed(1),
24827
+ // Add component data for tooltips
24828
+ component: {
24829
+ type: 'PipeElbow',
24830
+ attributes: {
24831
+ angle: {
24832
+ key: 'Bend Angle',
24833
+ value: (angle * 180 / Math.PI).toFixed(1),
24834
+ unit: '°'
24835
+ }
24836
+ }
24837
+ }
24778
24838
  };
24779
- endCap.castShadow = true;
24780
- endCap.receiveShadow = true;
24781
- segment.add(endCap);
24782
- console.log("\uD83D\uDD35 Created END cap for segment ".concat(segmentIndex, " at local position (0, ").concat(length / 2, ", 0)"));
24839
+ scene.add(elbow);
24840
+ }
24841
+
24842
+ /**
24843
+ * Create smooth elbow geometry to connect two pipe segments (optimized for 90-degree angles)
24844
+ * @param {THREE.Vector3} point1 - First point (before the joint)
24845
+ * @param {THREE.Vector3} point2 - Joint point (center of the elbow)
24846
+ * @param {THREE.Vector3} point3 - Third point (after the joint)
24847
+ * @param {number} radius - Pipe radius
24848
+ * @returns {THREE.BufferGeometry} Elbow geometry
24849
+ */
24850
+ }, {
24851
+ key: "createElbowGeometry",
24852
+ value: function createElbowGeometry(point1, point2, point3, radius) {
24853
+ try {
24854
+ // Fixed elbow radius for 90-degree bends (simplified)
24855
+ var elbowRadius = radius * 3;
24856
+
24857
+ // Create a curve for the 90-degree elbow
24858
+ var curve = this.createElbowCurve(point1, point2, point3, elbowRadius);
24859
+
24860
+ // Fixed tubular segments for 90-degree bends (quarter circle)
24861
+ var tubularSegments = 12; // Good balance for smooth 90° curves
24862
+ var radialSegments = 16;
24863
+ var closed = false;
24864
+ var elbowGeometry = new THREE__namespace.TubeGeometry(curve, tubularSegments, radius, radialSegments, closed);
24865
+ return elbowGeometry;
24866
+ } catch (error) {
24867
+ console.warn('Failed to create elbow geometry:', error);
24868
+ return null;
24869
+ }
24870
+ }
24871
+
24872
+ /**
24873
+ * Create a smooth curve for the 90-degree elbow joint (simplified)
24874
+ * @param {THREE.Vector3} point1 - First point
24875
+ * @param {THREE.Vector3} point2 - Joint point
24876
+ * @param {THREE.Vector3} point3 - Third point
24877
+ * @param {number} elbowRadius - Radius of the elbow curve
24878
+ * @returns {THREE.Curve} Curve for the elbow
24879
+ */
24880
+ }, {
24881
+ key: "createElbowCurve",
24882
+ value: function createElbowCurve(point1, point2, point3, elbowRadius) {
24883
+ // Calculate direction vectors
24884
+ var dir1 = point2.clone().sub(point1).normalize();
24885
+ var dir2 = point3.clone().sub(point2).normalize();
24886
+
24887
+ // For 90-degree bends, we can use a simpler approach with a quarter circle
24888
+ // The curve radius is proportional to the elbow radius
24889
+ var curveRadius = elbowRadius * 0.001;
24890
+
24891
+ // Calculate the offset distance from the joint point
24892
+ var offset = curveRadius;
24893
+
24894
+ // Calculate start and end points of the curve (offset from the joint)
24895
+ var curveStart = point2.clone().sub(dir1.clone().multiplyScalar(offset));
24896
+ var curveEnd = point2.clone().add(dir2.clone().multiplyScalar(offset));
24897
+
24898
+ // For a 90-degree bend, the control point is at the corner
24899
+ // We can use a quadratic bezier curve for simplicity
24900
+ var controlPoint = point2.clone();
24901
+
24902
+ // Create a quadratic bezier curve (simpler than cubic for 90° bends)
24903
+ var curve = new THREE__namespace.QuadraticBezierCurve3(curveStart, controlPoint, curveEnd);
24904
+ return curve;
24783
24905
  }
24784
24906
 
24785
24907
  /**
@@ -32446,7 +32568,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
32446
32568
  * Initialize the CentralPlant manager
32447
32569
  *
32448
32570
  * @constructor
32449
- * @version 0.1.45
32571
+ * @version 0.1.46
32450
32572
  * @updated 2025-10-22
32451
32573
  *
32452
32574
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -19,7 +19,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
19
19
  * Initialize the CentralPlant manager
20
20
  *
21
21
  * @constructor
22
- * @version 0.1.45
22
+ * @version 0.1.46
23
23
  * @updated 2025-10-22
24
24
  *
25
25
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -940,6 +940,7 @@ var TransformOperationsManager = /*#__PURE__*/function () {
940
940
  return null;
941
941
  }
942
942
  var collisionRadius = 0.5; // Radius around connector that triggers collision
943
+ var endpointTolerance = 0.55; // Tolerance for determining if connector is at segment endpoint
943
944
 
944
945
  // Get segment endpoints in world coordinates
945
946
  var endpoints = this.calculateSegmentEndpoints(segment);
@@ -958,6 +959,13 @@ var TransformOperationsManager = /*#__PURE__*/function () {
958
959
  var connectorWorldPos = new THREE__namespace.Vector3();
959
960
  child.getWorldPosition(connectorWorldPos);
960
961
 
962
+ // Skip connectors that are at the segment's endpoints (legitimate connection points)
963
+ var distanceToStart = connectorWorldPos.distanceTo(startPoint);
964
+ var distanceToEnd = connectorWorldPos.distanceTo(endPoint);
965
+ if (distanceToStart <= endpointTolerance || distanceToEnd <= endpointTolerance) {
966
+ return; // Skip this connector - it's at an endpoint
967
+ }
968
+
961
969
  // Find the closest point on the segment to the connector
962
970
  var segmentVector = new THREE__namespace.Vector3().subVectors(endPoint, startPoint);
963
971
  var pointVector = new THREE__namespace.Vector3().subVectors(connectorWorldPos, startPoint);
@@ -1156,6 +1164,37 @@ var TransformOperationsManager = /*#__PURE__*/function () {
1156
1164
  return c1.distanceTo(c2);
1157
1165
  }
1158
1166
 
1167
+ /**
1168
+ * Constrain a position to maintain orthogonal alignment with a reference position
1169
+ * Movement is restricted to only along the segment's direction vector
1170
+ * @param {THREE.Vector3} newPosition - The proposed new position
1171
+ * @param {THREE.Vector3} referencePosition - The stationary reference position
1172
+ * @param {THREE.Object3D} segment - The segment being adjusted
1173
+ * @returns {THREE.Vector3} Constrained position that maintains orthogonality
1174
+ * @private
1175
+ */
1176
+ }, {
1177
+ key: "constrainPositionToOrthogonal",
1178
+ value: function constrainPositionToOrthogonal(newPosition, referencePosition, segment) {
1179
+ // Get the segment's direction vector
1180
+ var direction = new THREE__namespace.Vector3(0, 1, 0);
1181
+ direction.applyQuaternion(segment.quaternion);
1182
+ direction.normalize();
1183
+
1184
+ // Calculate vector from reference position to new position
1185
+ var moveVector = new THREE__namespace.Vector3().subVectors(newPosition, referencePosition);
1186
+
1187
+ // Project the move vector onto the segment's direction
1188
+ // This gives us the component of movement along the segment's axis only
1189
+ var projectionLength = moveVector.dot(direction);
1190
+
1191
+ // Constrained position = reference + (projection along segment direction)
1192
+ var constrainedPos = new THREE__namespace.Vector3().copy(referencePosition).add(direction.clone().multiplyScalar(projectionLength));
1193
+ console.log("\uD83D\uDD12 Constrained movement along segment direction: [".concat(direction.x.toFixed(3), ", ").concat(direction.y.toFixed(3), ", ").concat(direction.z.toFixed(3), "]"));
1194
+ console.log(" Projection length: ".concat(projectionLength.toFixed(3)));
1195
+ return constrainedPos;
1196
+ }
1197
+
1159
1198
  /**
1160
1199
  * Update adjacent segment connectors to follow the moved segment's new position
1161
1200
  * Finds segments that were connected to the moved segment BEFORE the move,
@@ -1274,18 +1313,23 @@ var TransformOperationsManager = /*#__PURE__*/function () {
1274
1313
  var stationaryWorldPos = new THREE__namespace.Vector3();
1275
1314
  stationaryConnector.getWorldPosition(stationaryWorldPos);
1276
1315
 
1316
+ // Constrain movement to maintain orthogonal alignment
1317
+ // Horizontal segments: only adjust X and Y, keep Z constant
1318
+ // Vertical segments: only adjust Z, keep X and Y constant
1319
+ var constrainedPosition = _this2.constrainPositionToOrthogonal(pair.newPosition, stationaryWorldPos, adjacentSegment);
1320
+
1277
1321
  // Recreate the segment mesh with new length using explicit endpoint positions
1278
1322
  // Pass the intended positions, not the connector objects (which may have temporary positions)
1279
- _this2.recreateSegmentMeshWithNewLength(adjacentSegment, adjacentConnectors, pair.newPosition, stationaryWorldPos);
1323
+ _this2.recreateSegmentMeshWithNewLength(adjacentSegment, adjacentConnectors, constrainedPosition, stationaryWorldPos);
1280
1324
 
1281
1325
  // CRITICAL: After recreating the mesh, BOTH connectors need to be repositioned
1282
1326
  // because the segment's center and rotation have changed
1283
1327
 
1284
- // Position moving connector at the new position
1285
- var movingLocalPos = adjacentSegment.worldToLocal(pair.newPosition.clone());
1328
+ // Position moving connector at the constrained position (not the raw new position)
1329
+ var movingLocalPos = adjacentSegment.worldToLocal(constrainedPosition.clone());
1286
1330
  movingConnector.position.copy(movingLocalPos);
1287
1331
  movingConnector.updateMatrixWorld(true);
1288
- _this2.updateConnectorPositionInSceneData(movingConnector, pair.newPosition, adjacentSegment);
1332
+ _this2.updateConnectorPositionInSceneData(movingConnector, constrainedPosition, adjacentSegment);
1289
1333
 
1290
1334
  // Position stationary connector at its original world position
1291
1335
  var stationaryLocalPos = adjacentSegment.worldToLocal(stationaryWorldPos.clone());
@@ -184,10 +184,6 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
184
184
 
185
185
  var pipeRadius = 0.1;
186
186
  var pipeMaterial = this.createPipeMaterial(crosscubeTextureSet);
187
-
188
- // Shared geometry for all connector caps to improve performance
189
- var capGeometry = new THREE__namespace.SphereGeometry(pipeRadius, 16, 16);
190
- this.registerDisposable(capGeometry);
191
187
  paths.forEach(function (pathData, index) {
192
188
  if (pathData.path && pathData.path.length >= 2) {
193
189
  // Convert path points to Vector3 objects for consistent handling
@@ -241,14 +237,15 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
241
237
  pathTo: pathData.to
242
238
  };
243
239
 
244
- // Create caps at both ends of every segment
245
- _this3.createSegmentCaps(cylinder, capGeometry, pipeMaterial, globalSegmentIndex);
246
-
247
240
  // Increment global segment counter
248
241
  globalSegmentIndex++;
249
242
 
250
243
  // Add segment directly to scene instead of to polyline
251
244
  sceneViewer.scene.add(cylinder);
245
+
246
+ // Add smooth elbow joints only at actual direction changes (not at every point)
247
+ _this3.createAndAddElbowIfNeeded(pathPoints, j, pipeRadius, pipeMaterial, pathData, index, sceneViewer.scene // Pass scene instead of polyline
248
+ );
252
249
  }
253
250
  console.log("\u2705 Created pipe path ".concat(pathData.from, "-").concat(pathData.to, " with ").concat(pathPoints.length - 1, " segments"));
254
251
  }
@@ -256,49 +253,130 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
256
253
  }
257
254
 
258
255
  /**
259
- * Create connector caps at both segment endpoints as children of the segment
260
- * Caps are positioned in local coordinates to match manual segment connector positioning
261
- * @param {THREE.Mesh} segment - The parent segment mesh
262
- * @param {THREE.BufferGeometry} capGeometry - Shared geometry for caps
263
- * @param {THREE.Material} material - Material for the caps
264
- * @param {number} segmentIndex - Global segment index
256
+ * Create and add elbow joint if there's a direction change at the current segment
257
+ * @param {Array<THREE.Vector3>} pathPoints - Array of path points
258
+ * @param {number} j - Current segment index
259
+ * @param {number} pipeRadius - Radius of the pipe
260
+ * @param {THREE.Material} pipeMaterial - Material for the elbow
261
+ * @param {Object} pathData - Path data object with from/to information
262
+ * @param {number} index - Path index for naming
263
+ * @param {THREE.Scene} scene - Scene object to add elbow to directly
265
264
  */
266
265
  }, {
267
- key: "createSegmentCaps",
268
- value: function createSegmentCaps(segment, capGeometry, material, segmentIndex) {
269
- var length = segment.geometry.parameters.height;
270
-
271
- // Create start cap
272
- var startCap = new THREE__namespace.Mesh(capGeometry, material);
273
- // Position at segment start in local space (matches manual segment connector positioning)
274
- startCap.position.set(0, -length / 2, 0);
275
- startCap.uuid = "SEGMENT-".concat(segmentIndex, "-CAP-START");
276
- startCap.userData = {
277
- objectType: 'segment-cap',
278
- capType: 'start',
279
- segmentId: segment.uuid,
280
- segmentIndex: segmentIndex
281
- };
282
- startCap.castShadow = true;
283
- startCap.receiveShadow = true;
284
- segment.add(startCap);
285
- console.log("\uD83D\uDD35 Created START cap for segment ".concat(segmentIndex, " at local position (0, ").concat(-length / 2, ", 0)"));
286
-
287
- // Create end cap
288
- var endCap = new THREE__namespace.Mesh(capGeometry, material);
289
- // Position at segment end in local space (matches manual segment connector positioning)
290
- endCap.position.set(0, length / 2, 0);
291
- endCap.uuid = "SEGMENT-".concat(segmentIndex, "-CAP-END");
292
- endCap.userData = {
293
- objectType: 'segment-cap',
294
- capType: 'end',
295
- segmentId: segment.uuid,
296
- segmentIndex: segmentIndex
266
+ key: "createAndAddElbowIfNeeded",
267
+ value: function createAndAddElbowIfNeeded(pathPoints, j, pipeRadius, pipeMaterial, pathData, index, scene) {
268
+ // Only check if there are at least 2 more points ahead
269
+ if (j >= pathPoints.length - 2) {
270
+ return;
271
+ }
272
+ var currentSegment = pathPoints[j + 1].clone().sub(pathPoints[j]).normalize();
273
+ var nextSegment = pathPoints[j + 2].clone().sub(pathPoints[j + 1]).normalize();
274
+
275
+ // Check if there's actually a direction change (not just continuing straight)
276
+ var angle = currentSegment.angleTo(nextSegment);
277
+ var minAngle = Math.PI / 36; // 5 degrees minimum for creating an elbow
278
+
279
+ if (angle <= minAngle) {
280
+ return; // No significant direction change
281
+ }
282
+ var elbowGeometry = this.createElbowGeometry(pathPoints[j], pathPoints[j + 1], pathPoints[j + 2], pipeRadius);
283
+ if (!elbowGeometry) {
284
+ return; // Failed to create geometry
285
+ }
286
+ var elbow = new THREE__namespace.Mesh(elbowGeometry, pipeMaterial);
287
+ elbow.castShadow = true;
288
+ elbow.receiveShadow = true;
289
+
290
+ // Make elbows selectable as well
291
+ var elbowId = "pipe-elbow-".concat(pathData.from, "-").concat(pathData.to, "-").concat(j);
292
+ elbow.uuid = "Pipe Elbow ".concat(j + 1, ": ").concat(pathData.from, "-").concat(pathData.to);
293
+
294
+ // Add userData for elbows too
295
+ elbow.userData = {
296
+ isPipeElbow: true,
297
+ elbowId: elbowId,
298
+ elbowIndex: j,
299
+ pathFrom: pathData.from,
300
+ pathTo: pathData.to,
301
+ angle: (angle * 180 / Math.PI).toFixed(1),
302
+ // Add component data for tooltips
303
+ component: {
304
+ type: 'PipeElbow',
305
+ attributes: {
306
+ angle: {
307
+ key: 'Bend Angle',
308
+ value: (angle * 180 / Math.PI).toFixed(1),
309
+ unit: '°'
310
+ }
311
+ }
312
+ }
297
313
  };
298
- endCap.castShadow = true;
299
- endCap.receiveShadow = true;
300
- segment.add(endCap);
301
- console.log("\uD83D\uDD35 Created END cap for segment ".concat(segmentIndex, " at local position (0, ").concat(length / 2, ", 0)"));
314
+ scene.add(elbow);
315
+ }
316
+
317
+ /**
318
+ * Create smooth elbow geometry to connect two pipe segments (optimized for 90-degree angles)
319
+ * @param {THREE.Vector3} point1 - First point (before the joint)
320
+ * @param {THREE.Vector3} point2 - Joint point (center of the elbow)
321
+ * @param {THREE.Vector3} point3 - Third point (after the joint)
322
+ * @param {number} radius - Pipe radius
323
+ * @returns {THREE.BufferGeometry} Elbow geometry
324
+ */
325
+ }, {
326
+ key: "createElbowGeometry",
327
+ value: function createElbowGeometry(point1, point2, point3, radius) {
328
+ try {
329
+ // Fixed elbow radius for 90-degree bends (simplified)
330
+ var elbowRadius = radius * 3;
331
+
332
+ // Create a curve for the 90-degree elbow
333
+ var curve = this.createElbowCurve(point1, point2, point3, elbowRadius);
334
+
335
+ // Fixed tubular segments for 90-degree bends (quarter circle)
336
+ var tubularSegments = 12; // Good balance for smooth 90° curves
337
+ var radialSegments = 16;
338
+ var closed = false;
339
+ var elbowGeometry = new THREE__namespace.TubeGeometry(curve, tubularSegments, radius, radialSegments, closed);
340
+ return elbowGeometry;
341
+ } catch (error) {
342
+ console.warn('Failed to create elbow geometry:', error);
343
+ return null;
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Create a smooth curve for the 90-degree elbow joint (simplified)
349
+ * @param {THREE.Vector3} point1 - First point
350
+ * @param {THREE.Vector3} point2 - Joint point
351
+ * @param {THREE.Vector3} point3 - Third point
352
+ * @param {number} elbowRadius - Radius of the elbow curve
353
+ * @returns {THREE.Curve} Curve for the elbow
354
+ */
355
+ }, {
356
+ key: "createElbowCurve",
357
+ value: function createElbowCurve(point1, point2, point3, elbowRadius) {
358
+ // Calculate direction vectors
359
+ var dir1 = point2.clone().sub(point1).normalize();
360
+ var dir2 = point3.clone().sub(point2).normalize();
361
+
362
+ // For 90-degree bends, we can use a simpler approach with a quarter circle
363
+ // The curve radius is proportional to the elbow radius
364
+ var curveRadius = elbowRadius * 0.001;
365
+
366
+ // Calculate the offset distance from the joint point
367
+ var offset = curveRadius;
368
+
369
+ // Calculate start and end points of the curve (offset from the joint)
370
+ var curveStart = point2.clone().sub(dir1.clone().multiplyScalar(offset));
371
+ var curveEnd = point2.clone().add(dir2.clone().multiplyScalar(offset));
372
+
373
+ // For a 90-degree bend, the control point is at the corner
374
+ // We can use a quadratic bezier curve for simplicity
375
+ var controlPoint = point2.clone();
376
+
377
+ // Create a quadratic bezier curve (simpler than cubic for 90° bends)
378
+ var curve = new THREE__namespace.QuadraticBezierCurve3(curveStart, controlPoint, curveEnd);
379
+ return curve;
302
380
  }
303
381
 
304
382
  /**
@@ -15,7 +15,7 @@ var CentralPlant = /*#__PURE__*/function (_BaseDisposable) {
15
15
  * Initialize the CentralPlant manager
16
16
  *
17
17
  * @constructor
18
- * @version 0.1.45
18
+ * @version 0.1.46
19
19
  * @updated 2025-10-22
20
20
  *
21
21
  * @description Creates a new CentralPlant instance and initializes internal managers and utilities.
@@ -916,6 +916,7 @@ var TransformOperationsManager = /*#__PURE__*/function () {
916
916
  return null;
917
917
  }
918
918
  var collisionRadius = 0.5; // Radius around connector that triggers collision
919
+ var endpointTolerance = 0.55; // Tolerance for determining if connector is at segment endpoint
919
920
 
920
921
  // Get segment endpoints in world coordinates
921
922
  var endpoints = this.calculateSegmentEndpoints(segment);
@@ -934,6 +935,13 @@ var TransformOperationsManager = /*#__PURE__*/function () {
934
935
  var connectorWorldPos = new THREE.Vector3();
935
936
  child.getWorldPosition(connectorWorldPos);
936
937
 
938
+ // Skip connectors that are at the segment's endpoints (legitimate connection points)
939
+ var distanceToStart = connectorWorldPos.distanceTo(startPoint);
940
+ var distanceToEnd = connectorWorldPos.distanceTo(endPoint);
941
+ if (distanceToStart <= endpointTolerance || distanceToEnd <= endpointTolerance) {
942
+ return; // Skip this connector - it's at an endpoint
943
+ }
944
+
937
945
  // Find the closest point on the segment to the connector
938
946
  var segmentVector = new THREE.Vector3().subVectors(endPoint, startPoint);
939
947
  var pointVector = new THREE.Vector3().subVectors(connectorWorldPos, startPoint);
@@ -1132,6 +1140,37 @@ var TransformOperationsManager = /*#__PURE__*/function () {
1132
1140
  return c1.distanceTo(c2);
1133
1141
  }
1134
1142
 
1143
+ /**
1144
+ * Constrain a position to maintain orthogonal alignment with a reference position
1145
+ * Movement is restricted to only along the segment's direction vector
1146
+ * @param {THREE.Vector3} newPosition - The proposed new position
1147
+ * @param {THREE.Vector3} referencePosition - The stationary reference position
1148
+ * @param {THREE.Object3D} segment - The segment being adjusted
1149
+ * @returns {THREE.Vector3} Constrained position that maintains orthogonality
1150
+ * @private
1151
+ */
1152
+ }, {
1153
+ key: "constrainPositionToOrthogonal",
1154
+ value: function constrainPositionToOrthogonal(newPosition, referencePosition, segment) {
1155
+ // Get the segment's direction vector
1156
+ var direction = new THREE.Vector3(0, 1, 0);
1157
+ direction.applyQuaternion(segment.quaternion);
1158
+ direction.normalize();
1159
+
1160
+ // Calculate vector from reference position to new position
1161
+ var moveVector = new THREE.Vector3().subVectors(newPosition, referencePosition);
1162
+
1163
+ // Project the move vector onto the segment's direction
1164
+ // This gives us the component of movement along the segment's axis only
1165
+ var projectionLength = moveVector.dot(direction);
1166
+
1167
+ // Constrained position = reference + (projection along segment direction)
1168
+ var constrainedPos = new THREE.Vector3().copy(referencePosition).add(direction.clone().multiplyScalar(projectionLength));
1169
+ console.log("\uD83D\uDD12 Constrained movement along segment direction: [".concat(direction.x.toFixed(3), ", ").concat(direction.y.toFixed(3), ", ").concat(direction.z.toFixed(3), "]"));
1170
+ console.log(" Projection length: ".concat(projectionLength.toFixed(3)));
1171
+ return constrainedPos;
1172
+ }
1173
+
1135
1174
  /**
1136
1175
  * Update adjacent segment connectors to follow the moved segment's new position
1137
1176
  * Finds segments that were connected to the moved segment BEFORE the move,
@@ -1250,18 +1289,23 @@ var TransformOperationsManager = /*#__PURE__*/function () {
1250
1289
  var stationaryWorldPos = new THREE.Vector3();
1251
1290
  stationaryConnector.getWorldPosition(stationaryWorldPos);
1252
1291
 
1292
+ // Constrain movement to maintain orthogonal alignment
1293
+ // Horizontal segments: only adjust X and Y, keep Z constant
1294
+ // Vertical segments: only adjust Z, keep X and Y constant
1295
+ var constrainedPosition = _this2.constrainPositionToOrthogonal(pair.newPosition, stationaryWorldPos, adjacentSegment);
1296
+
1253
1297
  // Recreate the segment mesh with new length using explicit endpoint positions
1254
1298
  // Pass the intended positions, not the connector objects (which may have temporary positions)
1255
- _this2.recreateSegmentMeshWithNewLength(adjacentSegment, adjacentConnectors, pair.newPosition, stationaryWorldPos);
1299
+ _this2.recreateSegmentMeshWithNewLength(adjacentSegment, adjacentConnectors, constrainedPosition, stationaryWorldPos);
1256
1300
 
1257
1301
  // CRITICAL: After recreating the mesh, BOTH connectors need to be repositioned
1258
1302
  // because the segment's center and rotation have changed
1259
1303
 
1260
- // Position moving connector at the new position
1261
- var movingLocalPos = adjacentSegment.worldToLocal(pair.newPosition.clone());
1304
+ // Position moving connector at the constrained position (not the raw new position)
1305
+ var movingLocalPos = adjacentSegment.worldToLocal(constrainedPosition.clone());
1262
1306
  movingConnector.position.copy(movingLocalPos);
1263
1307
  movingConnector.updateMatrixWorld(true);
1264
- _this2.updateConnectorPositionInSceneData(movingConnector, pair.newPosition, adjacentSegment);
1308
+ _this2.updateConnectorPositionInSceneData(movingConnector, constrainedPosition, adjacentSegment);
1265
1309
 
1266
1310
  // Position stationary connector at its original world position
1267
1311
  var stationaryLocalPos = adjacentSegment.worldToLocal(stationaryWorldPos.clone());
@@ -160,10 +160,6 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
160
160
 
161
161
  var pipeRadius = 0.1;
162
162
  var pipeMaterial = this.createPipeMaterial(crosscubeTextureSet);
163
-
164
- // Shared geometry for all connector caps to improve performance
165
- var capGeometry = new THREE.SphereGeometry(pipeRadius, 16, 16);
166
- this.registerDisposable(capGeometry);
167
163
  paths.forEach(function (pathData, index) {
168
164
  if (pathData.path && pathData.path.length >= 2) {
169
165
  // Convert path points to Vector3 objects for consistent handling
@@ -217,14 +213,15 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
217
213
  pathTo: pathData.to
218
214
  };
219
215
 
220
- // Create caps at both ends of every segment
221
- _this3.createSegmentCaps(cylinder, capGeometry, pipeMaterial, globalSegmentIndex);
222
-
223
216
  // Increment global segment counter
224
217
  globalSegmentIndex++;
225
218
 
226
219
  // Add segment directly to scene instead of to polyline
227
220
  sceneViewer.scene.add(cylinder);
221
+
222
+ // Add smooth elbow joints only at actual direction changes (not at every point)
223
+ _this3.createAndAddElbowIfNeeded(pathPoints, j, pipeRadius, pipeMaterial, pathData, index, sceneViewer.scene // Pass scene instead of polyline
224
+ );
228
225
  }
229
226
  console.log("\u2705 Created pipe path ".concat(pathData.from, "-").concat(pathData.to, " with ").concat(pathPoints.length - 1, " segments"));
230
227
  }
@@ -232,49 +229,130 @@ var PathRenderingManager = /*#__PURE__*/function (_BaseDisposable) {
232
229
  }
233
230
 
234
231
  /**
235
- * Create connector caps at both segment endpoints as children of the segment
236
- * Caps are positioned in local coordinates to match manual segment connector positioning
237
- * @param {THREE.Mesh} segment - The parent segment mesh
238
- * @param {THREE.BufferGeometry} capGeometry - Shared geometry for caps
239
- * @param {THREE.Material} material - Material for the caps
240
- * @param {number} segmentIndex - Global segment index
232
+ * Create and add elbow joint if there's a direction change at the current segment
233
+ * @param {Array<THREE.Vector3>} pathPoints - Array of path points
234
+ * @param {number} j - Current segment index
235
+ * @param {number} pipeRadius - Radius of the pipe
236
+ * @param {THREE.Material} pipeMaterial - Material for the elbow
237
+ * @param {Object} pathData - Path data object with from/to information
238
+ * @param {number} index - Path index for naming
239
+ * @param {THREE.Scene} scene - Scene object to add elbow to directly
241
240
  */
242
241
  }, {
243
- key: "createSegmentCaps",
244
- value: function createSegmentCaps(segment, capGeometry, material, segmentIndex) {
245
- var length = segment.geometry.parameters.height;
246
-
247
- // Create start cap
248
- var startCap = new THREE.Mesh(capGeometry, material);
249
- // Position at segment start in local space (matches manual segment connector positioning)
250
- startCap.position.set(0, -length / 2, 0);
251
- startCap.uuid = "SEGMENT-".concat(segmentIndex, "-CAP-START");
252
- startCap.userData = {
253
- objectType: 'segment-cap',
254
- capType: 'start',
255
- segmentId: segment.uuid,
256
- segmentIndex: segmentIndex
257
- };
258
- startCap.castShadow = true;
259
- startCap.receiveShadow = true;
260
- segment.add(startCap);
261
- console.log("\uD83D\uDD35 Created START cap for segment ".concat(segmentIndex, " at local position (0, ").concat(-length / 2, ", 0)"));
262
-
263
- // Create end cap
264
- var endCap = new THREE.Mesh(capGeometry, material);
265
- // Position at segment end in local space (matches manual segment connector positioning)
266
- endCap.position.set(0, length / 2, 0);
267
- endCap.uuid = "SEGMENT-".concat(segmentIndex, "-CAP-END");
268
- endCap.userData = {
269
- objectType: 'segment-cap',
270
- capType: 'end',
271
- segmentId: segment.uuid,
272
- segmentIndex: segmentIndex
242
+ key: "createAndAddElbowIfNeeded",
243
+ value: function createAndAddElbowIfNeeded(pathPoints, j, pipeRadius, pipeMaterial, pathData, index, scene) {
244
+ // Only check if there are at least 2 more points ahead
245
+ if (j >= pathPoints.length - 2) {
246
+ return;
247
+ }
248
+ var currentSegment = pathPoints[j + 1].clone().sub(pathPoints[j]).normalize();
249
+ var nextSegment = pathPoints[j + 2].clone().sub(pathPoints[j + 1]).normalize();
250
+
251
+ // Check if there's actually a direction change (not just continuing straight)
252
+ var angle = currentSegment.angleTo(nextSegment);
253
+ var minAngle = Math.PI / 36; // 5 degrees minimum for creating an elbow
254
+
255
+ if (angle <= minAngle) {
256
+ return; // No significant direction change
257
+ }
258
+ var elbowGeometry = this.createElbowGeometry(pathPoints[j], pathPoints[j + 1], pathPoints[j + 2], pipeRadius);
259
+ if (!elbowGeometry) {
260
+ return; // Failed to create geometry
261
+ }
262
+ var elbow = new THREE.Mesh(elbowGeometry, pipeMaterial);
263
+ elbow.castShadow = true;
264
+ elbow.receiveShadow = true;
265
+
266
+ // Make elbows selectable as well
267
+ var elbowId = "pipe-elbow-".concat(pathData.from, "-").concat(pathData.to, "-").concat(j);
268
+ elbow.uuid = "Pipe Elbow ".concat(j + 1, ": ").concat(pathData.from, "-").concat(pathData.to);
269
+
270
+ // Add userData for elbows too
271
+ elbow.userData = {
272
+ isPipeElbow: true,
273
+ elbowId: elbowId,
274
+ elbowIndex: j,
275
+ pathFrom: pathData.from,
276
+ pathTo: pathData.to,
277
+ angle: (angle * 180 / Math.PI).toFixed(1),
278
+ // Add component data for tooltips
279
+ component: {
280
+ type: 'PipeElbow',
281
+ attributes: {
282
+ angle: {
283
+ key: 'Bend Angle',
284
+ value: (angle * 180 / Math.PI).toFixed(1),
285
+ unit: '°'
286
+ }
287
+ }
288
+ }
273
289
  };
274
- endCap.castShadow = true;
275
- endCap.receiveShadow = true;
276
- segment.add(endCap);
277
- console.log("\uD83D\uDD35 Created END cap for segment ".concat(segmentIndex, " at local position (0, ").concat(length / 2, ", 0)"));
290
+ scene.add(elbow);
291
+ }
292
+
293
+ /**
294
+ * Create smooth elbow geometry to connect two pipe segments (optimized for 90-degree angles)
295
+ * @param {THREE.Vector3} point1 - First point (before the joint)
296
+ * @param {THREE.Vector3} point2 - Joint point (center of the elbow)
297
+ * @param {THREE.Vector3} point3 - Third point (after the joint)
298
+ * @param {number} radius - Pipe radius
299
+ * @returns {THREE.BufferGeometry} Elbow geometry
300
+ */
301
+ }, {
302
+ key: "createElbowGeometry",
303
+ value: function createElbowGeometry(point1, point2, point3, radius) {
304
+ try {
305
+ // Fixed elbow radius for 90-degree bends (simplified)
306
+ var elbowRadius = radius * 3;
307
+
308
+ // Create a curve for the 90-degree elbow
309
+ var curve = this.createElbowCurve(point1, point2, point3, elbowRadius);
310
+
311
+ // Fixed tubular segments for 90-degree bends (quarter circle)
312
+ var tubularSegments = 12; // Good balance for smooth 90° curves
313
+ var radialSegments = 16;
314
+ var closed = false;
315
+ var elbowGeometry = new THREE.TubeGeometry(curve, tubularSegments, radius, radialSegments, closed);
316
+ return elbowGeometry;
317
+ } catch (error) {
318
+ console.warn('Failed to create elbow geometry:', error);
319
+ return null;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Create a smooth curve for the 90-degree elbow joint (simplified)
325
+ * @param {THREE.Vector3} point1 - First point
326
+ * @param {THREE.Vector3} point2 - Joint point
327
+ * @param {THREE.Vector3} point3 - Third point
328
+ * @param {number} elbowRadius - Radius of the elbow curve
329
+ * @returns {THREE.Curve} Curve for the elbow
330
+ */
331
+ }, {
332
+ key: "createElbowCurve",
333
+ value: function createElbowCurve(point1, point2, point3, elbowRadius) {
334
+ // Calculate direction vectors
335
+ var dir1 = point2.clone().sub(point1).normalize();
336
+ var dir2 = point3.clone().sub(point2).normalize();
337
+
338
+ // For 90-degree bends, we can use a simpler approach with a quarter circle
339
+ // The curve radius is proportional to the elbow radius
340
+ var curveRadius = elbowRadius * 0.001;
341
+
342
+ // Calculate the offset distance from the joint point
343
+ var offset = curveRadius;
344
+
345
+ // Calculate start and end points of the curve (offset from the joint)
346
+ var curveStart = point2.clone().sub(dir1.clone().multiplyScalar(offset));
347
+ var curveEnd = point2.clone().add(dir2.clone().multiplyScalar(offset));
348
+
349
+ // For a 90-degree bend, the control point is at the corner
350
+ // We can use a quadratic bezier curve for simplicity
351
+ var controlPoint = point2.clone();
352
+
353
+ // Create a quadratic bezier curve (simpler than cubic for 90° bends)
354
+ var curve = new THREE.QuadraticBezierCurve3(curveStart, controlPoint, curveEnd);
355
+ return curve;
278
356
  }
279
357
 
280
358
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2112-lab/central-plant",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "description": "Utility modules for the Central Plant Application",
5
5
  "main": "dist/bundle/index.js",
6
6
  "module": "dist/esm/index.js",