@2112-lab/central-plant 0.1.7 → 0.1.9

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.
@@ -15420,12 +15420,19 @@ class ConnectorManager {
15420
15420
  * @returns {Array<Object>} Array of clusters
15421
15421
  */
15422
15422
  clusterConnections(connections) {
15423
+ // Filter out connections where either from or to UUID starts with "gateway" (case insensitive)
15424
+ const filteredConnections = connections.filter(conn => {
15425
+ const fromStartsWithGateway = conn.from.toLowerCase().startsWith('gateway');
15426
+ const toStartsWithGateway = conn.to.toLowerCase().startsWith('gateway');
15427
+ return !fromStartsWithGateway && !toStartsWithGateway;
15428
+ });
15429
+
15423
15430
  const clusters = new Map(); // Map of object UUID to its cluster
15424
15431
  const clusterMap = new Map(); // Map of cluster ID to set of object UUIDs
15425
15432
  let nextClusterId = 0;
15426
15433
 
15427
15434
  // First pass: create initial clusters for each object
15428
- connections.forEach(conn => {
15435
+ filteredConnections.forEach(conn => {
15429
15436
  const { from, to } = conn;
15430
15437
 
15431
15438
  // If neither object is in a cluster, create new cluster
@@ -15470,13 +15477,8 @@ class ConnectorManager {
15470
15477
  objects: Array.from(objects)
15471
15478
  }));
15472
15479
 
15473
- // Filter out clusters containing objects with "GATEWAY" in their UUID
15474
- const filteredClusters = clustersArray.filter(cluster =>
15475
- !cluster.objects.some(uuid => uuid.includes('GATEWAY'))
15476
- );
15477
-
15478
15480
  // Enrich clusters with point and direction information
15479
- filteredClusters.forEach(cluster => {
15481
+ clustersArray.forEach(cluster => {
15480
15482
  cluster.objectPoints = cluster.objects.map(uuid => {
15481
15483
  const object = this.sceneManager.findObjectByUUID(uuid);
15482
15484
  if (object && object.userData.worldBoundingBox) {
@@ -15508,7 +15510,7 @@ class ConnectorManager {
15508
15510
  });
15509
15511
 
15510
15512
 
15511
- return filteredClusters;
15513
+ return clustersArray;
15512
15514
  }
15513
15515
  }
15514
15516
 
@@ -15909,11 +15911,14 @@ class PathManager {
15909
15911
  }
15910
15912
 
15911
15913
  /**
15912
- * Manages Steiner tree calculations and Minimum Spanning Tree (MST) operations
15913
- * @class SteinerTreeManager
15914
+ * Manages tree path calculations and Minimum Spanning Tree (MST) operations
15915
+ * @class TreePathManager
15914
15916
  */
15915
- class SteinerTreeManager {
15916
- constructor() {}
15917
+ class TreePathManager {
15918
+ constructor(gridSystem = null, sceneManager = null) {
15919
+ this.gridSystem = gridSystem;
15920
+ this.sceneManager = sceneManager;
15921
+ }
15917
15922
 
15918
15923
  /**
15919
15924
  * Calculate distance between two points
@@ -15968,13 +15973,62 @@ class SteinerTreeManager {
15968
15973
 
15969
15974
  return mst;
15970
15975
  }
15976
+
15977
+ /**
15978
+ * Find the nearest free voxel to a given position
15979
+ * @param {Vector3} position - Position to find free voxel near
15980
+ * @param {number} maxSearchRadius - Maximum search radius in voxel units
15981
+ * @returns {Vector3|null} Nearest free position or null if none found
15982
+ */
15983
+ findNearestFreeVoxel(position, maxSearchRadius = 5) {
15984
+ if (!this.gridSystem || !this.sceneManager) {
15985
+ return position; // Return original position if systems not available
15986
+ }
15987
+
15988
+ const originalVoxelKey = this.gridSystem.voxelKey(position);
15989
+
15990
+ // Check if original position is free
15991
+ if (!this.sceneManager.isVoxelOccupiedByMesh(originalVoxelKey, this.gridSystem.gridSize)) {
15992
+ return position;
15993
+ }
15994
+
15995
+ // Search in expanding rings around the position
15996
+ for (let radius = 1; radius <= maxSearchRadius; radius++) {
15997
+ const [x, y, z] = originalVoxelKey.split(',').map(Number);
15998
+
15999
+ // Generate all voxels at this radius
16000
+ const candidates = [];
16001
+
16002
+ // Generate all combinations of offsets at this radius
16003
+ for (let dx = -radius; dx <= radius; dx++) {
16004
+ for (let dy = -radius; dy <= radius; dy++) {
16005
+ for (let dz = -radius; dz <= radius; dz++) {
16006
+ // Only consider voxels exactly at this radius
16007
+ if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) === radius) {
16008
+ candidates.push([x + dx, y + dy, z + dz]);
16009
+ }
16010
+ }
16011
+ }
16012
+ }
16013
+
16014
+ // Check each candidate
16015
+ for (const [cx, cy, cz] of candidates) {
16016
+ const candidateKey = `${cx},${cy},${cz}`;
16017
+ if (!this.sceneManager.isVoxelOccupiedByMesh(candidateKey, this.gridSystem.gridSize)) {
16018
+ return this.gridSystem.voxelToVec3(candidateKey);
16019
+ }
16020
+ }
16021
+ }
16022
+
16023
+ return null;
16024
+ }
15971
16025
 
15972
16026
  /**
15973
- * Find Steiner point using Fermat-Torricelli point approximation
16027
+ * Find joint point
15974
16028
  * @param {Array<Vector3>} points - Array of points to connect
15975
- * @returns {Vector3|null} Steiner point or null if not possible
16029
+ * @returns {Vector3|null} Joint point or null if not possible
15976
16030
  */
15977
- findSteinerPoint(points) {
16031
+ findJointPoint(points) {
15978
16032
  if (points.length <= 2) {
15979
16033
  return null;
15980
16034
  }
@@ -15986,36 +16040,61 @@ class SteinerTreeManager {
15986
16040
  return null;
15987
16041
  }
15988
16042
 
15989
- // Find the shortest edge in MST
15990
- let shortestEdge = null;
15991
- let minEdgeLength = Infinity;
16043
+ // Find the best edge in MST
16044
+ let bestEdge = null;
16045
+ let maxScore = -1;
15992
16046
 
15993
- mst.forEach(([i, j]) => {
15994
- const edgeLength = this.distance(points[i], points[j]);
15995
- if (edgeLength < minEdgeLength) {
15996
- minEdgeLength = edgeLength;
15997
- shortestEdge = [points[i], points[j]];
16047
+ mst.forEach(([i, j], edgeIndex) => {
16048
+ const pointA = points[i];
16049
+ const pointB = points[j];
16050
+
16051
+ // Calculate edge components
16052
+ const dx = Math.abs(pointB.x - pointA.x);
16053
+ const dy = Math.abs(pointB.y - pointA.y);
16054
+ const dz = Math.abs(pointB.z - pointA.z);
16055
+
16056
+ // Calculate total length
16057
+ const totalLength = dx + dy + dz;
16058
+
16059
+ if (totalLength > 0) {
16060
+ // Calculate score
16061
+ // Higher score = more orthogonal (closer to being axis-aligned) relative to the total length of the edge
16062
+ const maxComponent = Math.max(dx, dy, dz);
16063
+ const score = (maxComponent / totalLength) / totalLength;
16064
+
16065
+ // Check if this edge has a better orthogonality score
16066
+ if (score > maxScore) {
16067
+ maxScore = score;
16068
+ bestEdge = [pointA, pointB];
16069
+ }
15998
16070
  }
15999
16071
  });
16000
16072
 
16001
- if (!shortestEdge) {
16073
+ if (!bestEdge) {
16002
16074
  return null;
16003
16075
  }
16004
16076
 
16005
- // Place Steiner point at the midpoint of the shortest edge
16006
- const steinerPoint = new Vector3(
16007
- (shortestEdge[0].x + shortestEdge[1].x) * 0.5,
16008
- (shortestEdge[0].y + shortestEdge[1].y) * 0.5,
16009
- (shortestEdge[0].z + shortestEdge[1].z) * 0.5
16077
+ // Place joint point at the midpoint of the most orthogonal edge
16078
+ const jointPoint = new Vector3(
16079
+ (bestEdge[0].x + bestEdge[1].x) * 0.5,
16080
+ (bestEdge[0].y + bestEdge[1].y) * 0.5,
16081
+ (bestEdge[0].z + bestEdge[1].z) * 0.5
16010
16082
  );
16011
16083
 
16012
16084
  // Snap to 0.5 grid
16013
16085
  const snapToGrid = (value) => Math.round(value * 2) / 2;
16014
- steinerPoint.x = snapToGrid(steinerPoint.x);
16015
- steinerPoint.y = snapToGrid(steinerPoint.y);
16016
- steinerPoint.z = snapToGrid(steinerPoint.z);
16086
+ jointPoint.x = snapToGrid(jointPoint.x);
16087
+ jointPoint.y = snapToGrid(jointPoint.y);
16088
+ jointPoint.z = snapToGrid(jointPoint.z);
16089
+
16090
+ // Check if the joint point is in an occupied voxel and move it if necessary
16091
+ const freeJointPoint = this.findNearestFreeVoxel(jointPoint);
16017
16092
 
16018
- return steinerPoint;
16093
+ if (freeJointPoint) {
16094
+ return freeJointPoint;
16095
+ } else {
16096
+ return jointPoint; // Return original if no free position found
16097
+ }
16019
16098
  }
16020
16099
  }
16021
16100
 
@@ -16062,8 +16141,8 @@ class Pathfinder {
16062
16141
  this.ASTAR_TIMEOUT
16063
16142
  );
16064
16143
 
16065
- // Initialize Steiner tree manager
16066
- this.steinerTreeManager = new SteinerTreeManager();
16144
+ // Initialize tree path manager
16145
+ this.treePathManager = new TreePathManager(this.gridSystem, null);
16067
16146
  }
16068
16147
 
16069
16148
  /**
@@ -16098,6 +16177,13 @@ class Pathfinder {
16098
16177
  * @property {string} gateways[].clusterId - ID of the cluster this gateway belongs to
16099
16178
  * @property {number} gateways[].id - Unique identifier for the gateway
16100
16179
  * @property {Vector3} gateways[].position - Position of the gateway in 3D space
16180
+ * @property {Object} gateways[].connections - Connection mapping information
16181
+ * @property {Array<Object>} gateways[].connections.removed - Array of original connections that were removed
16182
+ * @property {string} gateways[].connections.removed[].from - Source object UUID
16183
+ * @property {string} gateways[].connections.removed[].to - Target object UUID
16184
+ * @property {Array<Object>} gateways[].connections.added - Array of new connections that were added
16185
+ * @property {string} gateways[].connections.added[].from - Source object UUID
16186
+ * @property {string} gateways[].connections.added[].to - Target object UUID
16101
16187
  */
16102
16188
  findPaths(scene, connections) {
16103
16189
  // Create scene manager with the provided scene
@@ -16106,6 +16192,7 @@ class Pathfinder {
16106
16192
  // Update scene manager references
16107
16193
  this.connectorManager.sceneManager = sceneManager;
16108
16194
  this.pathManager.sceneManager = sceneManager;
16195
+ this.treePathManager.sceneManager = sceneManager;
16109
16196
 
16110
16197
  // Cluster the connections
16111
16198
  const clusters = this.connectorManager.clusterConnections(connections);
@@ -16113,41 +16200,41 @@ class Pathfinder {
16113
16200
  // Filter clusters to only include those with more than 2 objects
16114
16201
  const filteredClusters = clusters.filter(cluster => cluster.objects.length > 2);
16115
16202
 
16116
- // Calculate Steiner points and MST for each cluster
16203
+ // Calculate joint points for each cluster
16117
16204
  filteredClusters.forEach((cluster, index) => {
16118
16205
  const points = cluster.displacedPoints.map(p => p.displacedPoint);
16119
- cluster.mst = this.steinerTreeManager.findMST(points);
16120
- cluster.steinerPoint = this.steinerTreeManager.findSteinerPoint(points);
16206
+ cluster.jointPoint = this.treePathManager.findJointPoint(points);
16121
16207
  });
16122
16208
 
16123
- let gatewayCounter = 1;
16124
16209
  const gatewayMap = new Map();
16125
16210
 
16126
16211
  filteredClusters.forEach(cluster => {
16127
- if (cluster.steinerPoint) {
16128
- const gatewayId = gatewayCounter++;
16212
+ if (cluster.jointPoint) {
16213
+ // Generate a shorter unique ID (8 characters)
16214
+ const uuid = crypto.randomUUID().replace(/-/g, '').substring(0, 8);
16215
+ const gatewayId = `Gateway-${uuid}`;
16129
16216
  gatewayMap.set(cluster.clusterId, gatewayId);
16130
16217
  }
16131
16218
  });
16132
16219
 
16133
16220
  // Create virtual gateway objects in the scene
16134
16221
  filteredClusters.forEach(cluster => {
16135
- if (cluster.steinerPoint) {
16222
+ if (cluster.jointPoint) {
16136
16223
  const gatewayId = gatewayMap.get(cluster.clusterId);
16137
16224
  const gatewayObject = {
16138
- uuid: `GATEWAY-${gatewayId}`,
16225
+ uuid: gatewayId,
16139
16226
  type: 'Mesh',
16140
16227
  userData: {
16141
16228
  worldBoundingBox: {
16142
16229
  min: [
16143
- cluster.steinerPoint.x - 0.25,
16144
- cluster.steinerPoint.y - 0.25,
16145
- cluster.steinerPoint.z - 0.25
16230
+ cluster.jointPoint.x - 0.25,
16231
+ cluster.jointPoint.y - 0.25,
16232
+ cluster.jointPoint.z - 0.25
16146
16233
  ],
16147
16234
  max: [
16148
- cluster.steinerPoint.x + 0.25,
16149
- cluster.steinerPoint.y + 0.25,
16150
- cluster.steinerPoint.z + 0.25
16235
+ cluster.jointPoint.x + 0.25,
16236
+ cluster.jointPoint.y + 0.25,
16237
+ cluster.jointPoint.z + 0.25
16151
16238
  ]
16152
16239
  }
16153
16240
  }
@@ -16159,6 +16246,7 @@ class Pathfinder {
16159
16246
  // Rewire connections through gateways
16160
16247
  const rewiredConnections = [];
16161
16248
  const connectionSet = new Set(); // Track unique connections
16249
+ const gatewayConnectionMappings = new Map(); // Track which original connections are replaced by which gateway connections
16162
16250
 
16163
16251
  // Find direct connections (not part of clusters with >2 objects)
16164
16252
  const directConnections = connections.filter(conn => {
@@ -16186,44 +16274,63 @@ class Pathfinder {
16186
16274
  )?.clusterId;
16187
16275
 
16188
16276
  if (fromCluster !== undefined && toCluster !== undefined) {
16189
- const fromGateway = `GATEWAY-${gatewayMap.get(fromCluster)}`;
16190
- const toGateway = `GATEWAY-${gatewayMap.get(toCluster)}`;
16191
-
16192
- if (fromGateway && toGateway) {
16277
+ const fromGateway = gatewayMap.get(fromCluster);
16278
+ const toGateway = gatewayMap.get(toCluster);
16279
+ if (fromGateway !== undefined && toGateway !== undefined) {
16280
+ const fromGatewayId = fromGateway;
16281
+ const toGatewayId = toGateway;
16282
+
16283
+ // Track the original connection that will be replaced
16284
+ const originalConnection = { from: conn.from, to: conn.to };
16285
+ const addedConnections = [];
16286
+
16193
16287
  // If both objects are in the same cluster, connect through their gateway
16194
16288
  if (fromCluster === toCluster) {
16195
- const conn1 = { from: conn.from, to: fromGateway };
16196
- const conn2 = { from: fromGateway, to: conn.to };
16289
+ const conn1 = { from: conn.from, to: fromGatewayId };
16290
+ const conn2 = { from: fromGatewayId, to: conn.to };
16197
16291
 
16198
16292
  // Only add if not already present
16199
16293
  if (!connectionSet.has(JSON.stringify(conn1))) {
16200
16294
  gatewayConnections.push(conn1);
16201
16295
  connectionSet.add(JSON.stringify(conn1));
16296
+ addedConnections.push(conn1);
16202
16297
  }
16203
16298
  if (!connectionSet.has(JSON.stringify(conn2))) {
16204
16299
  gatewayConnections.push(conn2);
16205
16300
  connectionSet.add(JSON.stringify(conn2));
16301
+ addedConnections.push(conn2);
16206
16302
  }
16207
16303
  } else {
16208
16304
  // If objects are in different clusters, connect through both gateways
16209
- const conn1 = { from: conn.from, to: fromGateway };
16210
- const conn2 = { from: fromGateway, to: toGateway };
16211
- const conn3 = { from: toGateway, to: conn.to };
16305
+ const conn1 = { from: conn.from, to: fromGatewayId };
16306
+ const conn2 = { from: fromGatewayId, to: toGatewayId };
16307
+ const conn3 = { from: toGatewayId, to: conn.to };
16212
16308
 
16213
16309
  // Only add if not already present
16214
16310
  if (!connectionSet.has(JSON.stringify(conn1))) {
16215
16311
  gatewayConnections.push(conn1);
16216
16312
  connectionSet.add(JSON.stringify(conn1));
16313
+ addedConnections.push(conn1);
16217
16314
  }
16218
16315
  if (!connectionSet.has(JSON.stringify(conn2))) {
16219
16316
  gatewayConnections.push(conn2);
16220
16317
  connectionSet.add(JSON.stringify(conn2));
16318
+ addedConnections.push(conn2);
16221
16319
  }
16222
16320
  if (!connectionSet.has(JSON.stringify(conn3))) {
16223
16321
  gatewayConnections.push(conn3);
16224
16322
  connectionSet.add(JSON.stringify(conn3));
16323
+ addedConnections.push(conn3);
16225
16324
  }
16226
16325
  }
16326
+
16327
+ // Store the mapping for this connection
16328
+ if (addedConnections.length > 0) {
16329
+ gatewayConnectionMappings.set(JSON.stringify(originalConnection), {
16330
+ removed: [originalConnection],
16331
+ added: addedConnections
16332
+ });
16333
+ }
16227
16334
  }
16228
16335
  }
16229
16336
  });
@@ -16237,8 +16344,8 @@ class Pathfinder {
16237
16344
 
16238
16345
  if (!objA1 || !objA2 || !objB1 || !objB2) return 0;
16239
16346
 
16240
- const distA = this.steinerTreeManager.distance(sceneManager.getWorldPosition(objA1), sceneManager.getWorldPosition(objA2));
16241
- const distB = this.steinerTreeManager.distance(sceneManager.getWorldPosition(objB1), sceneManager.getWorldPosition(objB2));
16347
+ const distA = this.treePathManager.distance(sceneManager.getWorldPosition(objA1), sceneManager.getWorldPosition(objA2));
16348
+ const distB = this.treePathManager.distance(sceneManager.getWorldPosition(objB1), sceneManager.getWorldPosition(objB2));
16242
16349
 
16243
16350
  return distA - distB;
16244
16351
  });
@@ -16248,13 +16355,69 @@ class Pathfinder {
16248
16355
 
16249
16356
  const paths = this.pathManager.findPaths(rewiredConnections || []);
16250
16357
 
16358
+ // Aggregate connection mappings by gateway
16359
+ const gatewayConnectionsMap = new Map();
16360
+
16361
+ // Initialize connection mappings for each gateway
16362
+ Array.from(gatewayMap.entries()).forEach(([clusterId, gatewayId]) => {
16363
+ gatewayConnectionsMap.set(gatewayId, {
16364
+ removed: [],
16365
+ added: []
16366
+ });
16367
+ });
16368
+
16369
+ // Populate connection mappings
16370
+ gatewayConnectionMappings.forEach((mapping, connectionKey) => {
16371
+ const originalConnection = JSON.parse(connectionKey);
16372
+ const fromCluster = clusters.find(cluster =>
16373
+ cluster.objects.includes(originalConnection.from)
16374
+ )?.clusterId;
16375
+ const toCluster = clusters.find(cluster =>
16376
+ cluster.objects.includes(originalConnection.to)
16377
+ )?.clusterId;
16378
+
16379
+ if (fromCluster !== undefined && toCluster !== undefined) {
16380
+ const fromGateway = gatewayMap.get(fromCluster);
16381
+ const toGateway = gatewayMap.get(toCluster);
16382
+
16383
+ if (fromGateway !== undefined && toGateway !== undefined) {
16384
+ // Add to both gateways if they're different
16385
+ if (fromGateway === toGateway) {
16386
+ // Same gateway
16387
+ const gatewayConnections = gatewayConnectionsMap.get(fromGateway);
16388
+ gatewayConnections.removed.push(originalConnection);
16389
+ gatewayConnections.added.push(...mapping.added);
16390
+ } else {
16391
+ // Different gateways - split the connections
16392
+ const fromGatewayConnections = gatewayConnectionsMap.get(fromGateway);
16393
+ const toGatewayConnections = gatewayConnectionsMap.get(toGateway);
16394
+
16395
+ // Add the original connection to both gateways' removed list
16396
+ fromGatewayConnections.removed.push(originalConnection);
16397
+ toGatewayConnections.removed.push(originalConnection);
16398
+
16399
+ // Split added connections by gateway
16400
+ mapping.added.forEach(conn => {
16401
+ if (conn.from === originalConnection.from || conn.to === fromGateway) {
16402
+ fromGatewayConnections.added.push(conn);
16403
+ }
16404
+ if (conn.from === toGateway || conn.to === originalConnection.to) {
16405
+ toGatewayConnections.added.push(conn);
16406
+ }
16407
+ });
16408
+ }
16409
+ }
16410
+ }
16411
+ });
16412
+
16251
16413
  return {
16252
16414
  paths,
16253
16415
  rewiredConnections,
16254
16416
  gateways: Array.from(gatewayMap.entries()).map(([clusterId, gatewayId]) => ({
16255
16417
  clusterId,
16256
16418
  id: gatewayId,
16257
- position: clusters.find(c => c.clusterId === clusterId)?.steinerPoint
16419
+ position: clusters.find(c => c.clusterId === clusterId)?.jointPoint,
16420
+ connections: gatewayConnectionsMap.get(gatewayId) || { removed: [], added: [] }
16258
16421
  }))
16259
16422
  };
16260
16423
  }