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