@2112-lab/central-plant 0.1.0 → 0.1.2

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.
Files changed (43) hide show
  1. package/dist/bundle/index.js +7782 -6543
  2. package/dist/cjs/_virtual/_rollupPluginBabelHelpers.js +23 -10
  3. package/dist/cjs/node_modules/@2112-lab/pathfinder/dist/index.esm.js +1448 -0
  4. package/dist/cjs/src/animationManager.js +15 -15
  5. package/dist/cjs/src/componentManager.js +8 -8
  6. package/dist/cjs/src/disposalManager.js +12 -12
  7. package/dist/cjs/src/environmentManager.js +392 -99
  8. package/dist/cjs/src/hotReloadManager.js +26 -26
  9. package/dist/cjs/src/index.js +19 -66
  10. package/dist/cjs/src/keyboardControlsManager.js +23 -23
  11. package/dist/cjs/src/nameUtils.js +21 -21
  12. package/dist/cjs/src/pathfindingManager.js +311 -129
  13. package/dist/cjs/src/performanceMonitor.js +52 -52
  14. package/dist/cjs/src/sceneExportManager.js +23 -23
  15. package/dist/cjs/src/sceneInitializationManager.js +18 -18
  16. package/dist/cjs/src/textureConfig.js +469 -40
  17. package/dist/cjs/src/transformControlsManager.js +79 -79
  18. package/dist/esm/_virtual/_rollupPluginBabelHelpers.js +21 -10
  19. package/dist/esm/node_modules/@2112-lab/pathfinder/dist/index.esm.js +1444 -0
  20. package/dist/esm/src/animationManager.js +15 -15
  21. package/dist/esm/src/componentManager.js +8 -8
  22. package/dist/esm/src/disposalManager.js +12 -12
  23. package/dist/esm/src/environmentManager.js +393 -100
  24. package/dist/esm/src/hotReloadManager.js +26 -26
  25. package/dist/esm/src/index.js +19 -66
  26. package/dist/esm/src/keyboardControlsManager.js +23 -23
  27. package/dist/esm/src/nameUtils.js +21 -21
  28. package/dist/esm/src/pathfindingManager.js +313 -129
  29. package/dist/esm/src/performanceMonitor.js +52 -52
  30. package/dist/esm/src/sceneExportManager.js +23 -23
  31. package/dist/esm/src/sceneInitializationManager.js +18 -18
  32. package/dist/esm/src/textureConfig.js +469 -42
  33. package/dist/esm/src/transformControlsManager.js +79 -79
  34. package/dist/index.d.ts +255 -259
  35. package/package.json +52 -53
  36. package/dist/cjs/src/ConnectionManager.js +0 -114
  37. package/dist/cjs/src/Pathfinder.js +0 -88
  38. package/dist/cjs/src/modelPreloader.js +0 -360
  39. package/dist/cjs/src/sceneOperationsManager.js +0 -560
  40. package/dist/esm/src/ConnectionManager.js +0 -110
  41. package/dist/esm/src/Pathfinder.js +0 -84
  42. package/dist/esm/src/modelPreloader.js +0 -337
  43. package/dist/esm/src/sceneOperationsManager.js +0 -536
@@ -0,0 +1,1444 @@
1
+ /**
2
+ * A simple 3D vector class for internal use
3
+ * @class Vector3
4
+ */
5
+ class Vector3 {
6
+ /**
7
+ * Create a new Vector3
8
+ * @param {number} [x=0] - The x component
9
+ * @param {number} [y=0] - The y component
10
+ * @param {number} [z=0] - The z component
11
+ */
12
+ constructor(x = 0, y = 0, z = 0) {
13
+ this.x = x;
14
+ this.y = y;
15
+ this.z = z;
16
+ }
17
+
18
+ /**
19
+ * Create a copy of this vector
20
+ * @returns {Vector3} A new vector with the same components
21
+ */
22
+ clone() {
23
+ return new Vector3(this.x, this.y, this.z);
24
+ }
25
+
26
+ /**
27
+ * Convert the vector to an array
28
+ * @returns {Array<number>} Array representation [x, y, z]
29
+ */
30
+ toArray() {
31
+ return [this.x, this.y, this.z];
32
+ }
33
+
34
+ /**
35
+ * Normalize this vector (make it unit length)
36
+ * @returns {Vector3} This vector (for chaining)
37
+ */
38
+ normalize() {
39
+ const length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
40
+ if (length > 0) {
41
+ this.x /= length;
42
+ this.y /= length;
43
+ this.z /= length;
44
+ }
45
+ return this;
46
+ }
47
+
48
+ /**
49
+ * Subtract another vector from this one
50
+ * @param {Vector3} v - Vector to subtract
51
+ * @returns {Vector3} This vector (for chaining)
52
+ */
53
+ sub(v) {
54
+ this.x -= v.x;
55
+ this.y -= v.y;
56
+ this.z -= v.z;
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * Multiply this vector by a scalar value
62
+ * @param {number} scalar - The scalar value to multiply by
63
+ * @returns {Vector3} This vector (for chaining)
64
+ */
65
+ multiplyScalar(scalar) {
66
+ this.x *= scalar;
67
+ this.y *= scalar;
68
+ this.z *= scalar;
69
+ return this;
70
+ }
71
+
72
+ /**
73
+ * Add another vector to this one
74
+ * @param {Vector3} v - Vector to add
75
+ * @returns {Vector3} This vector (for chaining)
76
+ */
77
+ add(v) {
78
+ this.x += v.x;
79
+ this.y += v.y;
80
+ this.z += v.z;
81
+ return this;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Manages scene operations including object finding, position calculations, and hierarchy traversal
87
+ */
88
+ class SceneManager {
89
+ /**
90
+ * Create a new SceneManager instance
91
+ * @param {Object} scene - The scene configuration object
92
+ */
93
+ constructor(scene) {
94
+ this.scene = scene;
95
+ }
96
+
97
+ /**
98
+ * Get the root scene object
99
+ * @returns {Object} The root scene object
100
+ */
101
+ getRoot() {
102
+ return this.scene.object;
103
+ }
104
+
105
+ /**
106
+ * Get world position from worldBoundingBox center
107
+ * @param {Object} object - Scene object
108
+ * @returns {Vector3} World position
109
+ */
110
+ getWorldPosition(object) {
111
+ if (!object.userData?.worldBoundingBox) {
112
+ console.log(`[DEBUG] Object ${object.uuid} has no worldBoundingBox in userData`);
113
+ return new Vector3();
114
+ }
115
+
116
+ const { min, max } = object.userData.worldBoundingBox;
117
+ return new Vector3(
118
+ (min[0] + max[0]) / 2,
119
+ (min[1] + max[1]) / 2,
120
+ (min[2] + max[2]) / 2
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Get bounding box from worldBoundingBox in userData
126
+ * @param {Object} object - Scene object
127
+ * @returns {{min: Vector3, max: Vector3}|null} Bounding box or null if no worldBoundingBox
128
+ */
129
+ getBoundingBox(object) {
130
+ if (!object.userData?.worldBoundingBox) {
131
+ console.log(`[DEBUG] Object ${object.uuid} has no worldBoundingBox in userData`);
132
+ return null;
133
+ }
134
+
135
+ const { min, max } = object.userData.worldBoundingBox;
136
+ return {
137
+ min: new Vector3(min[0], min[1], min[2]),
138
+ max: new Vector3(max[0], max[1], max[2])
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Find an object by UUID in the scene hierarchy
144
+ * @param {string} uuid - UUID to search for
145
+ * @returns {Object|null} Found object or null
146
+ */
147
+ findObjectByUUID(uuid) {
148
+ return this._findObjectByUUIDRecursive(this.getRoot(), uuid);
149
+ }
150
+
151
+ /**
152
+ * Recursive helper for findObjectByUUID
153
+ * @private
154
+ * @param {Object} node - Current node to search
155
+ * @param {string} uuid - UUID to search for
156
+ * @returns {Object|null} Found object or null
157
+ */
158
+ _findObjectByUUIDRecursive(node, uuid) {
159
+ if (node.uuid === uuid) {
160
+ return node;
161
+ }
162
+ if (node.children) {
163
+ for (const child of node.children) {
164
+ const found = this._findObjectByUUIDRecursive(child, uuid);
165
+ if (found) return found;
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+
171
+ /**
172
+ * Find the parent object of a given object in the scene hierarchy
173
+ * @param {Object} target - Target object to find parent for
174
+ * @returns {Object|null} Parent object or null if not found or if parent is the scene
175
+ */
176
+ findParentObject(target) {
177
+ if (target.userData && Array.isArray(target.userData.direction)) {
178
+ return { isDirectionParent: true };
179
+ }
180
+
181
+ return this._findParentObjectRecursive(this.getRoot(), target);
182
+ }
183
+
184
+ /**
185
+ * Recursive helper for findParentObject
186
+ * @private
187
+ * @param {Object} root - Root object to search from
188
+ * @param {Object} target - Target object to find parent for
189
+ * @returns {Object|null} Parent object or null
190
+ */
191
+ _findParentObjectRecursive(root, target) {
192
+ if (root.children) {
193
+ for (const child of root.children) {
194
+ if (child.uuid === target.uuid) {
195
+ if (root.type === 'Scene') {
196
+ return null;
197
+ }
198
+ return root;
199
+ }
200
+ }
201
+
202
+ for (const child of root.children) {
203
+ const parent = this._findParentObjectRecursive(child, target);
204
+ if (parent) {
205
+ return parent;
206
+ }
207
+ }
208
+ }
209
+
210
+ return null;
211
+ }
212
+
213
+ /**
214
+ * Check if a voxel is occupied by any mesh object
215
+ * @param {string} voxelKey - Voxel key to check
216
+ * @param {number} gridSize - Size of each grid cell
217
+ * @returns {boolean} True if the voxel is occupied by a mesh object
218
+ */
219
+ isVoxelOccupiedByMesh(voxelKey, gridSize) {
220
+ const [x, y, z] = voxelKey.split(',').map(Number);
221
+ const worldPos = new Vector3(
222
+ x * gridSize,
223
+ y * gridSize,
224
+ z * gridSize
225
+ );
226
+
227
+ return this._checkObjectOccupancy(this.getRoot(), worldPos);
228
+ }
229
+
230
+ /**
231
+ * Recursive helper for isVoxelOccupiedByMesh
232
+ * @private
233
+ * @param {Object} object - Object to check
234
+ * @param {Vector3} worldPos - World position to check
235
+ * @returns {boolean} True if the position is occupied
236
+ */
237
+ _checkObjectOccupancy(object, worldPos) {
238
+ if (object.type === 'Mesh') {
239
+ const bbox = this.getBoundingBox(object);
240
+ if (bbox) {
241
+ if (worldPos.x >= bbox.min.x && worldPos.x <= bbox.max.x &&
242
+ worldPos.y >= bbox.min.y && worldPos.y <= bbox.max.y &&
243
+ worldPos.z >= bbox.min.z && worldPos.z <= bbox.max.z) {
244
+ return true;
245
+ }
246
+ }
247
+ }
248
+
249
+ if (object.children) {
250
+ for (const child of object.children) {
251
+ if (this._checkObjectOccupancy(child, worldPos)) return true;
252
+ }
253
+ }
254
+
255
+ return false;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Manages grid operations including voxel conversions, neighbor calculations, and distance metrics
261
+ */
262
+ class GridSystem {
263
+ /**
264
+ * Create a new GridSystem instance
265
+ * @param {number} gridSize - Size of each grid cell in world units
266
+ * @param {number} safetyMargin - Safety margin around obstacles in world units
267
+ */
268
+ constructor(gridSize = 0.5, safetyMargin = 0) {
269
+ this.gridSize = gridSize;
270
+ this.safetyMargin = safetyMargin;
271
+ }
272
+
273
+ /**
274
+ * Convert a position to a voxel key
275
+ * @param {Vector3|Object} pos - Position to convert
276
+ * @param {number} pos.x - X coordinate
277
+ * @param {number} pos.y - Y coordinate
278
+ * @param {number} pos.z - Z coordinate
279
+ * @returns {string} Voxel key in format "x,y,z"
280
+ */
281
+ voxelKey(pos) {
282
+ return [
283
+ Math.round(pos.x / this.gridSize),
284
+ Math.round(pos.y / this.gridSize),
285
+ Math.round(pos.z / this.gridSize)
286
+ ].join(',');
287
+ }
288
+
289
+ /**
290
+ * Convert a voxel key to a Vector3
291
+ * @param {string} key - Voxel key
292
+ * @returns {Vector3} Vector3 position
293
+ */
294
+ voxelToVec3(key) {
295
+ const [x, y, z] = key.split(',').map(Number);
296
+ return new Vector3(x * this.gridSize, y * this.gridSize, z * this.gridSize);
297
+ }
298
+
299
+ /**
300
+ * Get neighboring voxels (axis-aligned)
301
+ * @param {string} vox - Voxel key
302
+ * @returns {string[]} Array of neighboring voxel keys
303
+ */
304
+ getNeighbors(vox) {
305
+ const [x, y, z] = vox.split(',').map(Number);
306
+ return [
307
+ [x+1, y, z], [x-1, y, z],
308
+ [x, y+1, z], [x, y-1, z],
309
+ [x, y, z+1], [x, y, z-1]
310
+ ].map(arr => arr.join(','));
311
+ }
312
+
313
+ /**
314
+ * Calculate Manhattan distance between two voxels
315
+ * @param {string} a - First voxel key
316
+ * @param {string} b - Second voxel key
317
+ * @returns {number} Manhattan distance
318
+ */
319
+ manhattan(a, b) {
320
+ const [ax, ay, az] = a.split(',').map(Number);
321
+ const [bx, by, bz] = b.split(',').map(Number);
322
+ return Math.abs(ax-bx) + Math.abs(ay-by) + Math.abs(az-bz);
323
+ }
324
+
325
+ /**
326
+ * Calculate the number of voxels needed for a given world distance
327
+ * @param {number} worldDistance - Distance in world units
328
+ * @returns {number} Number of voxels
329
+ */
330
+ worldToVoxelDistance(worldDistance) {
331
+ return Math.ceil(worldDistance / this.gridSize);
332
+ }
333
+
334
+ /**
335
+ * Calculate the world distance for a given number of voxels
336
+ * @param {number} voxelCount - Number of voxels
337
+ * @returns {number} Distance in world units
338
+ */
339
+ voxelToWorldDistance(voxelCount) {
340
+ return voxelCount * this.gridSize;
341
+ }
342
+
343
+ /**
344
+ * Get the safety margin in voxel units
345
+ * @returns {number} Safety margin in voxels
346
+ */
347
+ getSafetyMarginVoxels() {
348
+ return this.worldToVoxelDistance(this.safetyMargin);
349
+ }
350
+
351
+ /**
352
+ * Check if a voxel is within the safety margin of a bounding box
353
+ * @param {string} voxelKey - Voxel key to check
354
+ * @param {Object} bbox - Bounding box
355
+ * @param {Vector3} bbox.min - Minimum coordinates
356
+ * @param {Vector3} bbox.max - Maximum coordinates
357
+ * @returns {boolean} True if the voxel is within the safety margin
358
+ */
359
+ isVoxelInSafetyMargin(voxelKey, bbox) {
360
+ const voxelPos = this.voxelToVec3(voxelKey);
361
+ const margin = this.safetyMargin;
362
+
363
+ return (
364
+ voxelPos.x >= bbox.min.x - margin && voxelPos.x <= bbox.max.x + margin &&
365
+ voxelPos.y >= bbox.min.y - margin && voxelPos.y <= bbox.max.y + margin &&
366
+ voxelPos.z >= bbox.min.z - margin && voxelPos.z <= bbox.max.z + margin
367
+ );
368
+ }
369
+
370
+ /**
371
+ * Get all voxel keys within a bounding box plus safety margin
372
+ * @param {Object} bbox - Bounding box
373
+ * @param {Vector3} bbox.min - Minimum coordinates
374
+ * @param {Vector3} bbox.max - Maximum coordinates
375
+ * @returns {string[]} Array of voxel keys
376
+ */
377
+ getVoxelsInBoundingBox(bbox) {
378
+ const margin = this.getSafetyMarginVoxels();
379
+ const min = bbox.min;
380
+ const max = bbox.max;
381
+
382
+ const voxels = [];
383
+ for (let x = Math.round(min.x / this.gridSize) - margin; x <= Math.round(max.x / this.gridSize) + margin; x++) {
384
+ for (let y = Math.round(min.y / this.gridSize) - margin; y <= Math.round(max.y / this.gridSize) + margin; y++) {
385
+ for (let z = Math.round(min.z / this.gridSize) - margin; z <= Math.round(max.z / this.gridSize) + margin; z++) {
386
+ voxels.push(`${x},${y},${z}`);
387
+ }
388
+ }
389
+ }
390
+ return voxels;
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Manages connector-related operations for the pathfinding system
396
+ * @class ConnectorManager
397
+ */
398
+ class ConnectorManager {
399
+ /**
400
+ * Create a new ConnectorManager instance
401
+ * @param {Object} config - The configuration object
402
+ * @param {Array<Object>} config.connections - Array of connections between objects
403
+ * @param {string} config.connections[].from - UUID of the source object
404
+ * @param {string} config.connections[].to - UUID of the target object
405
+ * @param {number} minSegmentLength - Minimum length for straight pipe segments in world units
406
+ * @param {SceneManager} sceneManager - Instance of SceneManager for scene operations
407
+ * @param {GridSystem} gridSystem - Instance of GridSystem for grid operations
408
+ */
409
+ constructor(config, minSegmentLength, sceneManager, gridSystem) {
410
+ this.config = config;
411
+ this.MIN_SEGMENT_LENGTH = minSegmentLength;
412
+ this.sceneManager = sceneManager;
413
+ this.gridSystem = gridSystem;
414
+ }
415
+
416
+ /**
417
+ * Check if an object is a connector by checking if it's mentioned in connections
418
+ * @param {Object} object - Scene object
419
+ * @returns {boolean} True if the object is a connector
420
+ */
421
+ isConnector(object) {
422
+ if (!this.config.connections) return false;
423
+
424
+ // Check if the object's UUID appears in any connection
425
+ return this.config.connections.some(conn =>
426
+ conn.from === object.uuid || conn.to === object.uuid
427
+ );
428
+ }
429
+
430
+ /**
431
+ * Calculate direction vector for a connector
432
+ * @param {Object} connector - Connector object
433
+ * @returns {Vector3|null} Direction vector or null if no parent or direction
434
+ */
435
+ getConnectorDirection(connector) {
436
+ // First check if direction is directly specified in userData
437
+ if (connector.userData && Array.isArray(connector.userData.direction)) {
438
+ const [x, y, z] = connector.userData.direction;
439
+ return new Vector3(x, y, z);
440
+ }
441
+
442
+ // If no direction in userData and no parent, return null
443
+ if (!connector.parent) {
444
+ return null;
445
+ }
446
+
447
+ // For backward compatibility, calculate direction from parent if no userData.direction
448
+ console.log(`[WARNING] No direction in userData for ${connector.name || connector.uuid} - using parent-based calculation (legacy mode)`);
449
+
450
+ // Get world position of the connector
451
+ const connectorPos = this.sceneManager.getWorldPosition(connector);
452
+
453
+ // For the parent, use the center of its bounding box if available
454
+ let parentPos;
455
+ const parentBBox = this.sceneManager.getBoundingBox(connector.parent);
456
+
457
+ if (parentBBox) {
458
+ // Calculate the center of the parent's bounding box
459
+ parentPos = new Vector3(
460
+ (parentBBox.min.x + parentBBox.max.x) / 2,
461
+ (parentBBox.min.y + parentBBox.max.y) / 2,
462
+ (parentBBox.min.z + parentBBox.max.z) / 2
463
+ );
464
+ } else {
465
+ // Fall back to parent's position if bounding box is not available
466
+ parentPos = this.sceneManager.getWorldPosition(connector.parent);
467
+ }
468
+
469
+ // Calculate the direction vector from parent to connector
470
+ const direction = new Vector3(
471
+ connectorPos.x - parentPos.x,
472
+ connectorPos.y - parentPos.y,
473
+ connectorPos.z - parentPos.z
474
+ ).normalize();
475
+
476
+ // Enforce orthogonality by aligning with the dominant axis
477
+ const absX = Math.abs(direction.x);
478
+ const absY = Math.abs(direction.y);
479
+ const absZ = Math.abs(direction.z);
480
+
481
+ if (absX >= absY && absX >= absZ) {
482
+ // X is dominant
483
+ direction.y = 0;
484
+ direction.z = 0;
485
+ direction.x = Math.sign(direction.x);
486
+ } else if (absY >= absX && absY >= absZ) {
487
+ // Y is dominant
488
+ direction.x = 0;
489
+ direction.z = 0;
490
+ direction.y = Math.sign(direction.y);
491
+ } else {
492
+ // Z is dominant
493
+ direction.x = 0;
494
+ direction.y = 0;
495
+ direction.z = Math.sign(direction.z);
496
+ }
497
+
498
+ return direction;
499
+ }
500
+
501
+ /**
502
+ * Create a virtual segment for a connector
503
+ * @param {Object} connector - Connector object
504
+ * @param {Vector3} connectorPos - World position of the connector
505
+ * @returns {Object|null} Virtual segment info or null if no parent
506
+ */
507
+ createVirtualSegment(connector, connectorPos) {
508
+ const direction = this.getConnectorDirection(connector);
509
+ if (!direction) {
510
+ return null;
511
+ }
512
+
513
+ // Create a virtual segment starting from the connector position
514
+ // and extending in the direction vector for MIN_SEGMENT_LENGTH
515
+ const endPos = new Vector3(
516
+ connectorPos.x + direction.x * this.MIN_SEGMENT_LENGTH,
517
+ connectorPos.y + direction.y * this.MIN_SEGMENT_LENGTH,
518
+ connectorPos.z + direction.z * this.MIN_SEGMENT_LENGTH
519
+ );
520
+
521
+ // Convert to voxel keys
522
+ const startKey = this.gridSystem.voxelKey(connectorPos);
523
+ const endKey = this.gridSystem.voxelKey(endPos);
524
+
525
+ return {
526
+ startPos: connectorPos,
527
+ endPos: endPos,
528
+ direction: direction,
529
+ startKey: startKey,
530
+ endKey: endKey
531
+ };
532
+ }
533
+
534
+ /**
535
+ * Process all connectors in the scene and mark their virtual segments as occupied
536
+ * @param {Object} sceneRoot - Root scene object
537
+ * @param {Set<string>} pathOccupied - Set of occupied voxel keys
538
+ * @param {string} excludeUUID1 - UUID of first object to exclude
539
+ * @param {string} excludeUUID2 - UUID of second object to exclude
540
+ */
541
+ processConnectors(sceneRoot, pathOccupied, excludeUUID1, excludeUUID2) {
542
+ this._processConnectorsRecursive(sceneRoot, pathOccupied, excludeUUID1, excludeUUID2);
543
+ }
544
+
545
+ /**
546
+ * Recursive helper for processConnectors
547
+ * @private
548
+ * @param {Object} object - Current object to process
549
+ * @param {Set<string>} pathOccupied - Set of occupied voxel keys
550
+ * @param {string} excludeUUID1 - UUID of first object to exclude
551
+ * @param {string} excludeUUID2 - UUID of second object to exclude
552
+ */
553
+ _processConnectorsRecursive(object, pathOccupied, excludeUUID1, excludeUUID2) {
554
+ // Process current object if it's a mesh
555
+ if (object.type === 'Mesh') {
556
+ // Skip the excluded objects
557
+ if (object.uuid === excludeUUID1 || object.uuid === excludeUUID2) {
558
+ return;
559
+ }
560
+
561
+ const connectorPos = this.sceneManager.getWorldPosition(object);
562
+ const segment = this.createVirtualSegment(object, connectorPos);
563
+
564
+ if (segment) {
565
+ this._markVirtualSegmentAsOccupied(segment, connectorPos, pathOccupied);
566
+ }
567
+ }
568
+
569
+ // Process children recursively
570
+ if (object.children) {
571
+ for (const child of object.children) {
572
+ this._processConnectorsRecursive(child, pathOccupied, excludeUUID1, excludeUUID2);
573
+ }
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Mark voxels in a virtual segment as occupied
579
+ * @private
580
+ * @param {Object} segment - Virtual segment info
581
+ * @param {Vector3} connectorPos - Position of the connector
582
+ * @param {Set<string>} pathOccupied - Set of occupied voxel keys
583
+ */
584
+ _markVirtualSegmentAsOccupied(segment, connectorPos, pathOccupied) {
585
+ const direction = segment.direction;
586
+ const distance = this.gridSystem.worldToVoxelDistance(this.MIN_SEGMENT_LENGTH + (this.gridSystem.safetyMargin * 2));
587
+
588
+ for (let i = 0; i <= distance; i++) {
589
+ const x = Math.round(connectorPos.x / this.gridSystem.gridSize + direction.x * i);
590
+ const y = Math.round(connectorPos.y / this.gridSystem.gridSize + direction.y * i);
591
+ const z = Math.round(connectorPos.z / this.gridSystem.gridSize + direction.z * i);
592
+ const key = `${x},${y},${z}`;
593
+
594
+ pathOccupied.add(key);
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Cluster connections by shared objects
600
+ * @param {Array<Object>} connections - Array of connections between objects
601
+ * @returns {Array<Object>} Array of clusters
602
+ */
603
+ clusterConnections(connections) {
604
+ const clusters = new Map(); // Map of object UUID to its cluster
605
+ const clusterMap = new Map(); // Map of cluster ID to set of object UUIDs
606
+ let nextClusterId = 0;
607
+
608
+ // First pass: create initial clusters for each object
609
+ connections.forEach(conn => {
610
+ const { from, to } = conn;
611
+
612
+ // If neither object is in a cluster, create new cluster
613
+ if (!clusters.has(from) && !clusters.has(to)) {
614
+ const clusterId = nextClusterId++;
615
+ clusters.set(from, clusterId);
616
+ clusters.set(to, clusterId);
617
+ clusterMap.set(clusterId, new Set([from, to]));
618
+ }
619
+ // If only 'from' is in a cluster, add 'to' to that cluster
620
+ else if (clusters.has(from) && !clusters.has(to)) {
621
+ const clusterId = clusters.get(from);
622
+ clusters.set(to, clusterId);
623
+ clusterMap.get(clusterId).add(to);
624
+ }
625
+ // If only 'to' is in a cluster, add 'from' to that cluster
626
+ else if (!clusters.has(from) && clusters.has(to)) {
627
+ const clusterId = clusters.get(to);
628
+ clusters.set(from, clusterId);
629
+ clusterMap.get(clusterId).add(from);
630
+ }
631
+ // If both are in different clusters, merge the clusters
632
+ else if (clusters.has(from) && clusters.has(to)) {
633
+ const fromCluster = clusters.get(from);
634
+ const toCluster = clusters.get(to);
635
+
636
+ if (fromCluster !== toCluster) {
637
+ // Merge toCluster into fromCluster
638
+ const toClusterObjects = clusterMap.get(toCluster);
639
+ toClusterObjects.forEach(objId => {
640
+ clusters.set(objId, fromCluster);
641
+ clusterMap.get(fromCluster).add(objId);
642
+ });
643
+ clusterMap.delete(toCluster);
644
+ }
645
+ }
646
+ });
647
+
648
+ // Convert clusters to array format
649
+ const clustersArray = Array.from(clusterMap.entries()).map(([clusterId, objects]) => ({
650
+ clusterId,
651
+ objects: Array.from(objects)
652
+ }));
653
+
654
+ // Filter out clusters containing objects with "GATEWAY" in their UUID
655
+ const filteredClusters = clustersArray.filter(cluster =>
656
+ !cluster.objects.some(uuid => uuid.includes('GATEWAY'))
657
+ );
658
+
659
+ // Enrich clusters with point and direction information
660
+ filteredClusters.forEach(cluster => {
661
+ cluster.objectPoints = cluster.objects.map(uuid => {
662
+ const object = this.sceneManager.findObjectByUUID(uuid);
663
+ if (object && object.userData.worldBoundingBox) {
664
+ const center = this.sceneManager.getWorldPosition(object);
665
+
666
+ return {
667
+ uuid,
668
+ point: center,
669
+ direction: object.userData.direction ?
670
+ new Vector3(...object.userData.direction) : null
671
+ };
672
+ }
673
+ return null;
674
+ }).filter(point => point !== null);
675
+
676
+ // Add displaced points to each cluster
677
+ cluster.displacedPoints = cluster.objectPoints.map(objPoint => {
678
+ const displacedPoint = objPoint.direction
679
+ ? objPoint.point.clone().add(objPoint.direction.multiplyScalar(0.5))
680
+ : objPoint.point.clone();
681
+
682
+ return {
683
+ uuid: objPoint.uuid,
684
+ originalPoint: objPoint.point,
685
+ displacedPoint: displacedPoint,
686
+ direction: objPoint.direction
687
+ };
688
+ });
689
+ });
690
+
691
+
692
+ return filteredClusters;
693
+ }
694
+ }
695
+
696
+ /**
697
+ * Manages path-related operations for the pathfinding system
698
+ * @class PathManager
699
+ */
700
+ class PathManager {
701
+ /**
702
+ * Create a new PathManager instance
703
+ * @param {SceneManager} sceneManager - Instance of SceneManager for scene operations
704
+ * @param {GridSystem} gridSystem - Instance of GridSystem for grid operations
705
+ * @param {ConnectorManager} connectorManager - Instance of ConnectorManager for connector operations
706
+ * @param {number} minSegmentLength - Minimum length for straight pipe segments in world units
707
+ * @param {number} astarTimeout - Timeout for A* pathfinding in milliseconds
708
+ */
709
+ constructor(sceneManager, gridSystem, connectorManager, minSegmentLength, astarTimeout) {
710
+ this.sceneManager = sceneManager;
711
+ this.gridSystem = gridSystem;
712
+ this.connectorManager = connectorManager;
713
+ this.MIN_SEGMENT_LENGTH = minSegmentLength;
714
+ this.ASTAR_TIMEOUT = astarTimeout;
715
+ }
716
+
717
+ /**
718
+ * A* pathfinding algorithm
719
+ * @private
720
+ * @param {string} start - Start voxel key
721
+ * @param {string} goal - Goal voxel key
722
+ * @param {Set<string>} occupied - Set of occupied voxel keys
723
+ * @returns {string[]|null} Path as array of voxel keys or null if no path found
724
+ */
725
+ astar(start, goal, occupied) {
726
+ const open = new Set([start]);
727
+ const cameFrom = {};
728
+ const gScore = {[start]: 0};
729
+ const fScore = {[start]: this.gridSystem.manhattan(start, goal)};
730
+
731
+ // Add timeout to prevent infinite loops
732
+ const startTime = Date.now();
733
+
734
+ while (open.size > 0) {
735
+ // Check for timeout
736
+ if (Date.now() - startTime > this.ASTAR_TIMEOUT) {
737
+ console.log('[Warning] Pathfinding timed out');
738
+ return null;
739
+ }
740
+
741
+ // Get node in open with lowest fScore
742
+ let current = null;
743
+ let minF = Infinity;
744
+ for (const node of open) {
745
+ if (fScore[node] < minF) {
746
+ minF = fScore[node];
747
+ current = node;
748
+ }
749
+ }
750
+ if (current === goal) {
751
+ // Reconstruct path
752
+ const path = [current];
753
+ while (cameFrom[current]) {
754
+ current = cameFrom[current];
755
+ path.unshift(current);
756
+ }
757
+ return path;
758
+ }
759
+ open.delete(current);
760
+ for (const neighbor of this.gridSystem.getNeighbors(current)) {
761
+ if (occupied.has(neighbor)) continue;
762
+ const tentativeG = gScore[current] + 1;
763
+ if (tentativeG < (gScore[neighbor] ?? Infinity)) {
764
+ cameFrom[neighbor] = current;
765
+ gScore[neighbor] = tentativeG;
766
+ fScore[neighbor] = tentativeG + this.gridSystem.manhattan(neighbor, goal);
767
+ open.add(neighbor);
768
+ }
769
+ }
770
+ }
771
+ return null;
772
+ }
773
+
774
+ /**
775
+ * Find a path respecting virtual segments
776
+ * @private
777
+ * @param {string} startKey - Start voxel key
778
+ * @param {string} endKey - End voxel key
779
+ * @param {Set<string>} occupied - Set of occupied voxel keys
780
+ * @param {Object} startSegment - Virtual segment for start connector (optional)
781
+ * @param {Object} endSegment - Virtual segment for end connector (optional)
782
+ * @returns {string[]|null} Path as array of voxel keys or null if no path found
783
+ */
784
+ findPathWithVirtualSegments(startKey, endKey, occupied, startSegment, endSegment) {
785
+ // If we have virtual segments, we need to ensure the path starts and ends with them
786
+ let actualStartKey = startKey;
787
+ let actualEndKey = endKey;
788
+
789
+ // Create a copy of occupied set to avoid modifying the original
790
+ const pathOccupied = new Set(occupied);
791
+
792
+ // If we have a start segment, use its end as the actual start for pathfinding
793
+ // and mark the segment as occupied
794
+ if (startSegment) {
795
+ actualStartKey = startSegment.endKey;
796
+ // Mark all voxels in the start segment as occupied
797
+ const startPos = startSegment.startPos;
798
+ const direction = startSegment.direction;
799
+ const distance = this.gridSystem.worldToVoxelDistance(this.MIN_SEGMENT_LENGTH);
800
+ // Note: we go up to distance-1 to exclude the connecting end point
801
+ for (let i = 0; i < distance; i++) {
802
+ const x = Math.round(startPos.x / this.gridSystem.gridSize + direction.x * i);
803
+ const y = Math.round(startPos.y / this.gridSystem.gridSize + direction.y * i);
804
+ const z = Math.round(startPos.z / this.gridSystem.gridSize + direction.z * i);
805
+ const key = `${x},${y},${z}`;
806
+ pathOccupied.add(key);
807
+ }
808
+ }
809
+
810
+ // If we have an end segment, use its end as the actual end for pathfinding
811
+ // and mark the segment as occupied
812
+ if (endSegment) {
813
+ actualEndKey = endSegment.endKey;
814
+ // Mark all voxels in the end segment as occupied
815
+ const endPos = endSegment.startPos;
816
+ const direction = endSegment.direction;
817
+ const distance = this.gridSystem.worldToVoxelDistance(this.MIN_SEGMENT_LENGTH);
818
+ // Note: we go up to distance-1 to exclude the connecting end point
819
+ for (let i = 0; i < distance; i++) {
820
+ const x = Math.round(endPos.x / this.gridSystem.gridSize + direction.x * i);
821
+ const y = Math.round(endPos.y / this.gridSystem.gridSize + direction.y * i);
822
+ const z = Math.round(endPos.z / this.gridSystem.gridSize + direction.z * i);
823
+ const key = `${x},${y},${z}`;
824
+ pathOccupied.add(key);
825
+ }
826
+ }
827
+
828
+ // Find a path between the actual start and end
829
+ const middlePath = this.astar(actualStartKey, actualEndKey, pathOccupied);
830
+
831
+ if (!middlePath) {
832
+ return null;
833
+ }
834
+
835
+ // Construct the full path
836
+ let fullPath = [];
837
+
838
+ // Add the start segment if we have one
839
+ if (startSegment) {
840
+ fullPath.push(startSegment.startKey);
841
+ }
842
+
843
+ // Add the middle path
844
+ fullPath = fullPath.concat(middlePath);
845
+
846
+ // Add the end segment if we have one
847
+ if (endSegment) {
848
+ fullPath.push(endSegment.startKey);
849
+ }
850
+
851
+ return fullPath;
852
+ }
853
+
854
+ /**
855
+ * Mark all mesh objects' voxels as occupied in the scene
856
+ * @private
857
+ * @param {Object} sceneRoot - Root scene object
858
+ * @returns {Object} Object containing occupied sets and maps
859
+ */
860
+ markAllMeshesAsOccupiedVoxels(sceneRoot) {
861
+ const occupied = new Set();
862
+ const occupiedByUUID = new Map();
863
+
864
+ const processObject = (object) => {
865
+ // Skip non-mesh objects
866
+ if (object.type !== 'Mesh') return;
867
+
868
+ // Get object's bounding box
869
+ const bbox = this.sceneManager.getBoundingBox(object);
870
+ if (!bbox) return;
871
+
872
+ // Get all voxels in the bounding box plus safety margin
873
+ const voxels = this.gridSystem.getVoxelsInBoundingBox(bbox);
874
+
875
+ // Create a set for this object's occupied voxels
876
+ const objectOccupied = new Set(voxels);
877
+
878
+ // Add all voxels to the global occupied set
879
+ for (const key of voxels) {
880
+ occupied.add(key);
881
+ }
882
+
883
+ // Store the object's occupied voxels in the map
884
+ occupiedByUUID.set(object.uuid, objectOccupied);
885
+
886
+ // Process children recursively
887
+ if (object.children) {
888
+ for (const child of object.children) {
889
+ processObject(child);
890
+ }
891
+ }
892
+ };
893
+
894
+ // Process all objects in the scene's children array
895
+ if (sceneRoot.children) {
896
+ for (const object of sceneRoot.children) {
897
+ processObject(object);
898
+ }
899
+ }
900
+
901
+ return { occupied, occupiedByUUID };
902
+ }
903
+
904
+ /**
905
+ * Process a single connection and find its path
906
+ * @private
907
+ * @param {Object} connection - Connection object
908
+ * @param {Set<string>} occupied - Set of occupied voxel keys
909
+ * @param {Map<string, Set<string>>} occupiedByUUID - Map of occupied voxels by UUID
910
+ * @returns {Object|null} Path result object or null if connection is invalid
911
+ */
912
+ processConnection(connection, occupied, occupiedByUUID) {
913
+ const fromObject = this.sceneManager.findObjectByUUID(connection.from);
914
+ const toObject = this.sceneManager.findObjectByUUID(connection.to);
915
+
916
+ if (!fromObject || !toObject) {
917
+ console.log(`[ERROR] Could not find objects for connection ${connection.from} - ${connection.to}`);
918
+ return null;
919
+ }
920
+
921
+ // Verify that both objects are meshes
922
+ if (fromObject.type !== 'Mesh' || toObject.type !== 'Mesh') {
923
+ console.log(`[ERROR] Connection endpoints must be meshes: ${fromObject.type} - ${toObject.type}`);
924
+ return null;
925
+ }
926
+
927
+ // Store the parent for each object to access later
928
+ fromObject.parent = this.sceneManager.findParentObject(fromObject);
929
+ toObject.parent = this.sceneManager.findParentObject(toObject);
930
+
931
+ // Get world positions for both objects
932
+ const worldPos1 = this.sceneManager.getWorldPosition(fromObject);
933
+ const worldPos2 = this.sceneManager.getWorldPosition(toObject);
934
+
935
+ // Create virtual segments for both connectors
936
+ const startSegment = this.connectorManager.createVirtualSegment(fromObject, worldPos1);
937
+ const endSegment = this.connectorManager.createVirtualSegment(toObject, worldPos2);
938
+
939
+ // Create a copy of occupied set for this path
940
+ const pathOccupied = new Set(occupied);
941
+
942
+ // Debug copy 1: Initial state after copying from global occupied
943
+ const debugOccupiedInitial = Array.from(pathOccupied)
944
+ .map(key => this.gridSystem.voxelToVec3(key));
945
+
946
+ // Get bounding boxes for the two objects
947
+ const fromBbox = this.sceneManager.getBoundingBox(fromObject);
948
+ const toBbox = this.sceneManager.getBoundingBox(toObject);
949
+
950
+ if (!fromBbox || !toBbox) return null;
951
+
952
+ // Clear voxels in bounding boxes only for objects that do NOT have virtual segments
953
+ if (!startSegment) {
954
+ const voxels = this.gridSystem.getVoxelsInBoundingBox(fromBbox);
955
+ for (const key of voxels) {
956
+ pathOccupied.delete(key);
957
+ }
958
+ }
959
+
960
+ if (!endSegment) {
961
+ const voxels = this.gridSystem.getVoxelsInBoundingBox(toBbox);
962
+ for (const key of voxels) {
963
+ pathOccupied.delete(key);
964
+ }
965
+ }
966
+
967
+ // Also clear the voxels for the virtual segments if they exist
968
+ if (startSegment) {
969
+ const direction = startSegment.direction;
970
+ const distance = this.gridSystem.worldToVoxelDistance(this.MIN_SEGMENT_LENGTH + (this.gridSystem.safetyMargin * 2));
971
+
972
+ for (let i = 0; i <= distance; i++) {
973
+ const x = Math.round(worldPos1.x / this.gridSystem.gridSize + direction.x * i);
974
+ const y = Math.round(worldPos1.y / this.gridSystem.gridSize + direction.y * i);
975
+ const z = Math.round(worldPos1.z / this.gridSystem.gridSize + direction.z * i);
976
+ pathOccupied.delete(`${x},${y},${z}`);
977
+ }
978
+ }
979
+
980
+ if (endSegment) {
981
+ const direction = endSegment.direction;
982
+ const distance = this.gridSystem.worldToVoxelDistance(this.MIN_SEGMENT_LENGTH + (this.gridSystem.safetyMargin * 2));
983
+
984
+ for (let i = 0; i <= distance; i++) {
985
+ const x = Math.round(worldPos2.x / this.gridSystem.gridSize + direction.x * i);
986
+ const y = Math.round(worldPos2.y / this.gridSystem.gridSize + direction.y * i);
987
+ const z = Math.round(worldPos2.z / this.gridSystem.gridSize + direction.z * i);
988
+ pathOccupied.delete(`${x},${y},${z}`);
989
+ }
990
+ }
991
+
992
+ // Process all other connectors in the scene
993
+ this.connectorManager.processConnectors(this.sceneManager.getRoot(), pathOccupied, fromObject.uuid, toObject.uuid);
994
+
995
+ // Debug copy 3: After clearing virtual segments and adding back previous paths
996
+ const debugOccupiedAfterSegments = Array.from(pathOccupied)
997
+ .map(key => this.gridSystem.voxelToVec3(key));
998
+
999
+ // Get start and end voxel keys
1000
+ const startKey = this.gridSystem.voxelKey(worldPos1);
1001
+ const endKey = this.gridSystem.voxelKey(worldPos2);
1002
+
1003
+ // Find a path with virtual segments
1004
+ const pathKeys = this.findPathWithVirtualSegments(
1005
+ startKey,
1006
+ endKey,
1007
+ pathOccupied,
1008
+ startSegment,
1009
+ endSegment
1010
+ );
1011
+
1012
+ const occupiedVoxels = Array.from(pathOccupied)
1013
+ .map(key => this.gridSystem.voxelToVec3(key));
1014
+
1015
+ if (pathKeys) {
1016
+ const path = pathKeys.map(key => this.gridSystem.voxelToVec3(key));
1017
+
1018
+ // Create a set for this path's occupied voxels
1019
+ const pathOccupied = new Set();
1020
+
1021
+ // Mark path points as occupied for next routes
1022
+ for (const p of path) {
1023
+ const key = this.gridSystem.voxelKey(p);
1024
+ occupied.add(key);
1025
+ pathOccupied.add(key);
1026
+ }
1027
+
1028
+ // Store the path's occupied voxels in the map with a special key
1029
+ const pathKey = `path_${connection.from}_${connection.to}`;
1030
+ occupiedByUUID.set(pathKey, pathOccupied);
1031
+
1032
+ return {
1033
+ from: connection.from,
1034
+ to: connection.to,
1035
+ path: path,
1036
+ occupied: occupiedVoxels,
1037
+ debug: {
1038
+ initial: debugOccupiedInitial,
1039
+ afterSegments: debugOccupiedAfterSegments
1040
+ },
1041
+ occupiedByUUID: occupiedByUUID // Include the map in the result
1042
+ };
1043
+ } else {
1044
+ console.log(`[ERROR] No orthogonal path found for ${connection.from}-${connection.to}`);
1045
+ return {
1046
+ from: connection.from,
1047
+ to: connection.to,
1048
+ path: null,
1049
+ start: worldPos1,
1050
+ end: worldPos2,
1051
+ occupied: occupiedVoxels,
1052
+ debug: {
1053
+ initial: debugOccupiedInitial,
1054
+ afterSegments: debugOccupiedAfterSegments
1055
+ },
1056
+ occupiedByUUID: occupiedByUUID // Include the map in the result
1057
+ };
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * Find paths for all connections
1063
+ * @param {Array<Object>} connections - Array of connections to process
1064
+ * @returns {Array<Object>} Array of path results
1065
+ */
1066
+ findPaths(connections) {
1067
+ console.log('[DEBUG] Starting findPaths()');
1068
+
1069
+ // Validate scene structure
1070
+ if (!this.sceneManager.getRoot()) {
1071
+ console.log('[ERROR] Invalid scene structure: missing scene root');
1072
+ return [];
1073
+ }
1074
+
1075
+ // Process all objects in the scene
1076
+ const { occupied, occupiedByUUID } = this.markAllMeshesAsOccupiedVoxels(this.sceneManager.getRoot());
1077
+
1078
+ const paths = [];
1079
+
1080
+ // For each connection in the config, find a path
1081
+ for (const connection of connections) {
1082
+ const result = this.processConnection(connection, occupied, occupiedByUUID);
1083
+ if (result) {
1084
+ paths.push(result);
1085
+ }
1086
+ }
1087
+
1088
+ return paths;
1089
+ }
1090
+ }
1091
+
1092
+ /**
1093
+ * Manages Steiner tree calculations and Minimum Spanning Tree (MST) operations
1094
+ * @class SteinerTreeManager
1095
+ */
1096
+ class SteinerTreeManager {
1097
+ constructor() {}
1098
+
1099
+ /**
1100
+ * Calculate distance between two points
1101
+ * @param {Vector3} p1 - First point
1102
+ * @param {Vector3} p2 - Second point
1103
+ * @returns {number} Distance between points
1104
+ */
1105
+ distance(p1, p2) {
1106
+ const dx = p2.x - p1.x;
1107
+ const dy = p2.y - p1.y;
1108
+ const dz = p2.z - p1.z;
1109
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
1110
+ }
1111
+
1112
+ /**
1113
+ * Find Minimum Spanning Tree using Prim's algorithm
1114
+ * @param {Array<Vector3>} points - Array of points to connect
1115
+ * @returns {Array<Array<number>>} Array of edges in the MST
1116
+ */
1117
+ findMST(points) {
1118
+ const n = points.length;
1119
+ const mst = [];
1120
+ const visited = new Set();
1121
+
1122
+ // Start with the first point
1123
+ visited.add(0);
1124
+
1125
+ while (visited.size < n) {
1126
+ let minDist = Infinity;
1127
+ let minEdge = null;
1128
+
1129
+ // Find the minimum edge from visited to unvisited
1130
+ for (const i of visited) {
1131
+ for (let j = 0; j < n; j++) {
1132
+ if (!visited.has(j)) {
1133
+ const dist = this.distance(points[i], points[j]);
1134
+ if (dist < minDist) {
1135
+ minDist = dist;
1136
+ minEdge = [i, j];
1137
+ }
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ if (minEdge) {
1143
+ mst.push(minEdge);
1144
+ visited.add(minEdge[1]);
1145
+ } else {
1146
+ break; // Break if we can't find any more edges
1147
+ }
1148
+ }
1149
+
1150
+ return mst;
1151
+ }
1152
+
1153
+ /**
1154
+ * Find Steiner point using Fermat-Torricelli point approximation
1155
+ * @param {Array<Vector3>} points - Array of points to connect
1156
+ * @returns {Vector3|null} Steiner point or null if not possible
1157
+ */
1158
+ findSteinerPoint(points) {
1159
+ if (points.length <= 2) {
1160
+ return null;
1161
+ }
1162
+
1163
+ // Find MST first
1164
+ const mst = this.findMST(points);
1165
+
1166
+ if (mst.length === 0) {
1167
+ return null;
1168
+ }
1169
+
1170
+ // Find the shortest edge in MST
1171
+ let shortestEdge = null;
1172
+ let minEdgeLength = Infinity;
1173
+
1174
+ mst.forEach(([i, j]) => {
1175
+ const edgeLength = this.distance(points[i], points[j]);
1176
+ if (edgeLength < minEdgeLength) {
1177
+ minEdgeLength = edgeLength;
1178
+ shortestEdge = [points[i], points[j]];
1179
+ }
1180
+ });
1181
+
1182
+ if (!shortestEdge) {
1183
+ return null;
1184
+ }
1185
+
1186
+ // Place Steiner point at the midpoint of the shortest edge
1187
+ const steinerPoint = new Vector3(
1188
+ (shortestEdge[0].x + shortestEdge[1].x) * 0.5,
1189
+ (shortestEdge[0].y + shortestEdge[1].y) * 0.5,
1190
+ (shortestEdge[0].z + shortestEdge[1].z) * 0.5
1191
+ );
1192
+
1193
+ // Snap to 0.5 grid
1194
+ const snapToGrid = (value) => Math.round(value * 2) / 2;
1195
+ steinerPoint.x = snapToGrid(steinerPoint.x);
1196
+ steinerPoint.y = snapToGrid(steinerPoint.y);
1197
+ steinerPoint.z = snapToGrid(steinerPoint.z);
1198
+
1199
+ return steinerPoint;
1200
+ }
1201
+ }
1202
+
1203
+ /**
1204
+ * A 3D pathfinding system that finds orthogonal paths between objects
1205
+ * @class Pathfinder
1206
+ */
1207
+ class Pathfinder {
1208
+ /**
1209
+ * Create a new Pathfinding instance
1210
+ * @param {Object} [config] - Optional configuration object
1211
+ * @param {Object} [config.grid] - Grid system configuration
1212
+ * @param {number} [config.grid.size=0.5] - Size of each grid cell in world units
1213
+ * @param {number} [config.grid.safetyMargin=0] - Safety margin around obstacles in world units
1214
+ * @param {number} [config.grid.minSegmentLength=0.5] - Minimum length for straight pipe segments in world units
1215
+ * @param {number} [config.grid.timeout=1000] - Timeout for A* pathfinding in milliseconds
1216
+ */
1217
+ constructor(config = {}) {
1218
+ this.config = config;
1219
+
1220
+ // Grid system settings with defaults
1221
+ const gridConfig = config.grid || {};
1222
+ this.gridSystem = new GridSystem(
1223
+ gridConfig.size ?? 0.5,
1224
+ gridConfig.safetyMargin ?? 0
1225
+ );
1226
+ this.MIN_SEGMENT_LENGTH = gridConfig.minSegmentLength ?? 0.5;
1227
+ this.ASTAR_TIMEOUT = gridConfig.timeout ?? 1000;
1228
+
1229
+ // Initialize connector manager
1230
+ this.connectorManager = new ConnectorManager(
1231
+ { grid: gridConfig }, // Only pass grid config
1232
+ this.MIN_SEGMENT_LENGTH,
1233
+ null, // sceneManager will be set in findPaths
1234
+ this.gridSystem
1235
+ );
1236
+
1237
+ // Initialize path manager
1238
+ this.pathManager = new PathManager(
1239
+ null, // sceneManager will be set in findPaths
1240
+ this.gridSystem,
1241
+ this.connectorManager,
1242
+ this.MIN_SEGMENT_LENGTH,
1243
+ this.ASTAR_TIMEOUT
1244
+ );
1245
+
1246
+ // Initialize Steiner tree manager
1247
+ this.steinerTreeManager = new SteinerTreeManager();
1248
+ }
1249
+
1250
+ /**
1251
+ * Find paths for all connections
1252
+ * @param {Object} scene - The scene configuration
1253
+ * @param {Object} scene.object - The scene object containing children
1254
+ * @param {string} scene.object.uuid - Unique identifier for the scene object
1255
+ * @param {string} scene.object.type - Type of the scene object (typically "Scene")
1256
+ * @param {Array<Object>} scene.object.children - Array of scene objects
1257
+ * @param {string} scene.object.children[].uuid - Unique identifier for the child object
1258
+ * @param {string} scene.object.children[].type - Type of the child object (must be "Mesh")
1259
+ * @param {Object} scene.object.children[].userData - User data for the child object
1260
+ * @param {Object} scene.object.children[].userData.worldBoundingBox - Bounding box data
1261
+ * @param {Array<number>} scene.object.children[].userData.worldBoundingBox.min - Minimum coordinates [x,y,z]
1262
+ * @param {Array<number>} scene.object.children[].userData.worldBoundingBox.max - Maximum coordinates [x,y,z]
1263
+ * @param {Array<number>} [scene.object.children[].userData.direction] - Optional direction vector [x,y,z]
1264
+ * @param {Array<Object>} connections - Array of connections between objects
1265
+ * @param {string} connections[].from - UUID of the source object
1266
+ * @param {string} connections[].to - UUID of the target object
1267
+ * @returns {Object} Result object containing paths, rewired connections, and gateways
1268
+ * @property {Array<Object>} paths - Array of path results
1269
+ * @property {string} paths[].from - Source object UUID
1270
+ * @property {string} paths[].to - Target object UUID
1271
+ * @property {Array<Vector3>|null} paths[].path - Array of path points or null if no path found
1272
+ * @property {Array<Vector3>} [paths[].occupied] - Array of occupied points
1273
+ * @property {Vector3} [paths[].start] - Start position (if no path found)
1274
+ * @property {Vector3} [paths[].end] - End position (if no path found)
1275
+ * @property {Array<Object>} rewiredConnections - Array of connections after gateway optimization
1276
+ * @property {string} rewiredConnections[].from - Source object UUID
1277
+ * @property {string} rewiredConnections[].to - Target object UUID
1278
+ * @property {Array<Object>} gateways - Array of gateway information
1279
+ * @property {string} gateways[].clusterId - ID of the cluster this gateway belongs to
1280
+ * @property {number} gateways[].id - Unique identifier for the gateway
1281
+ * @property {Vector3} gateways[].position - Position of the gateway in 3D space
1282
+ */
1283
+ findPaths(scene, connections) {
1284
+ // Create scene manager with the provided scene
1285
+ const sceneManager = new SceneManager(scene);
1286
+
1287
+ // Update scene manager references
1288
+ this.connectorManager.sceneManager = sceneManager;
1289
+ this.pathManager.sceneManager = sceneManager;
1290
+
1291
+ // Cluster the connections
1292
+ const clusters = this.connectorManager.clusterConnections(connections);
1293
+
1294
+ // Filter clusters to only include those with more than 2 objects
1295
+ const filteredClusters = clusters.filter(cluster => cluster.objects.length > 2);
1296
+
1297
+ // Calculate Steiner points and MST for each cluster
1298
+ filteredClusters.forEach((cluster, index) => {
1299
+ const points = cluster.displacedPoints.map(p => p.displacedPoint);
1300
+ cluster.mst = this.steinerTreeManager.findMST(points);
1301
+ cluster.steinerPoint = this.steinerTreeManager.findSteinerPoint(points);
1302
+ });
1303
+
1304
+ let gatewayCounter = 1;
1305
+ const gatewayMap = new Map();
1306
+
1307
+ filteredClusters.forEach(cluster => {
1308
+ if (cluster.steinerPoint) {
1309
+ const gatewayId = gatewayCounter++;
1310
+ gatewayMap.set(cluster.clusterId, gatewayId);
1311
+ }
1312
+ });
1313
+
1314
+ // Create virtual gateway objects in the scene
1315
+ filteredClusters.forEach(cluster => {
1316
+ if (cluster.steinerPoint) {
1317
+ const gatewayId = gatewayMap.get(cluster.clusterId);
1318
+ const gatewayObject = {
1319
+ uuid: `GATEWAY-${gatewayId}`,
1320
+ type: 'Mesh',
1321
+ userData: {
1322
+ worldBoundingBox: {
1323
+ min: [
1324
+ cluster.steinerPoint.x - 0.25,
1325
+ cluster.steinerPoint.y - 0.25,
1326
+ cluster.steinerPoint.z - 0.25
1327
+ ],
1328
+ max: [
1329
+ cluster.steinerPoint.x + 0.25,
1330
+ cluster.steinerPoint.y + 0.25,
1331
+ cluster.steinerPoint.z + 0.25
1332
+ ]
1333
+ }
1334
+ }
1335
+ };
1336
+ scene.object.children.push(gatewayObject);
1337
+ }
1338
+ });
1339
+
1340
+ // Rewire connections through gateways
1341
+ const rewiredConnections = [];
1342
+ const connectionSet = new Set(); // Track unique connections
1343
+
1344
+ // Find direct connections (not part of clusters with >2 objects)
1345
+ const directConnections = connections.filter(conn => {
1346
+ const fromCluster = clusters.find(cluster => cluster.objects.includes(conn.from));
1347
+ const toCluster = clusters.find(cluster => cluster.objects.includes(conn.to));
1348
+ return (!fromCluster || !toCluster || fromCluster.objects.length <= 2 || toCluster.objects.length <= 2);
1349
+ });
1350
+
1351
+ // First add direct connections
1352
+ directConnections.forEach(conn => {
1353
+ if (!connectionSet.has(JSON.stringify(conn))) {
1354
+ rewiredConnections.push(conn);
1355
+ connectionSet.add(JSON.stringify(conn));
1356
+ }
1357
+ });
1358
+
1359
+ // Then add gateway connections
1360
+ const gatewayConnections = [];
1361
+ connections.forEach(conn => {
1362
+ const fromCluster = clusters.find(cluster =>
1363
+ cluster.objects.includes(conn.from)
1364
+ )?.clusterId;
1365
+ const toCluster = clusters.find(cluster =>
1366
+ cluster.objects.includes(conn.to)
1367
+ )?.clusterId;
1368
+
1369
+ if (fromCluster !== undefined && toCluster !== undefined) {
1370
+ const fromGateway = `GATEWAY-${gatewayMap.get(fromCluster)}`;
1371
+ const toGateway = `GATEWAY-${gatewayMap.get(toCluster)}`;
1372
+
1373
+ if (fromGateway && toGateway) {
1374
+ // If both objects are in the same cluster, connect through their gateway
1375
+ if (fromCluster === toCluster) {
1376
+ const conn1 = { from: conn.from, to: fromGateway };
1377
+ const conn2 = { from: fromGateway, to: conn.to };
1378
+
1379
+ // Only add if not already present
1380
+ if (!connectionSet.has(JSON.stringify(conn1))) {
1381
+ gatewayConnections.push(conn1);
1382
+ connectionSet.add(JSON.stringify(conn1));
1383
+ }
1384
+ if (!connectionSet.has(JSON.stringify(conn2))) {
1385
+ gatewayConnections.push(conn2);
1386
+ connectionSet.add(JSON.stringify(conn2));
1387
+ }
1388
+ } else {
1389
+ // If objects are in different clusters, connect through both gateways
1390
+ const conn1 = { from: conn.from, to: fromGateway };
1391
+ const conn2 = { from: fromGateway, to: toGateway };
1392
+ const conn3 = { from: toGateway, to: conn.to };
1393
+
1394
+ // Only add if not already present
1395
+ if (!connectionSet.has(JSON.stringify(conn1))) {
1396
+ gatewayConnections.push(conn1);
1397
+ connectionSet.add(JSON.stringify(conn1));
1398
+ }
1399
+ if (!connectionSet.has(JSON.stringify(conn2))) {
1400
+ gatewayConnections.push(conn2);
1401
+ connectionSet.add(JSON.stringify(conn2));
1402
+ }
1403
+ if (!connectionSet.has(JSON.stringify(conn3))) {
1404
+ gatewayConnections.push(conn3);
1405
+ connectionSet.add(JSON.stringify(conn3));
1406
+ }
1407
+ }
1408
+ }
1409
+ }
1410
+ });
1411
+
1412
+ // Sort gateway connections by edge length
1413
+ gatewayConnections.sort((a, b) => {
1414
+ const objA1 = sceneManager.findObjectByUUID(a.from);
1415
+ const objA2 = sceneManager.findObjectByUUID(a.to);
1416
+ const objB1 = sceneManager.findObjectByUUID(b.from);
1417
+ const objB2 = sceneManager.findObjectByUUID(b.to);
1418
+
1419
+ if (!objA1 || !objA2 || !objB1 || !objB2) return 0;
1420
+
1421
+ const distA = this.steinerTreeManager.distance(sceneManager.getWorldPosition(objA1), sceneManager.getWorldPosition(objA2));
1422
+ const distB = this.steinerTreeManager.distance(sceneManager.getWorldPosition(objB1), sceneManager.getWorldPosition(objB2));
1423
+
1424
+ return distA - distB;
1425
+ });
1426
+
1427
+ // Add sorted gateway connections to rewiredConnections
1428
+ rewiredConnections.push(...gatewayConnections);
1429
+
1430
+ const paths = this.pathManager.findPaths(rewiredConnections || []);
1431
+
1432
+ return {
1433
+ paths,
1434
+ rewiredConnections,
1435
+ gateways: Array.from(gatewayMap.entries()).map(([clusterId, gatewayId]) => ({
1436
+ clusterId,
1437
+ id: gatewayId,
1438
+ position: clusters.find(c => c.clusterId === clusterId)?.steinerPoint
1439
+ }))
1440
+ };
1441
+ }
1442
+ }
1443
+
1444
+ export { Pathfinder };