@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.
- package/dist/bundle/index.js +7782 -6543
- package/dist/cjs/_virtual/_rollupPluginBabelHelpers.js +23 -10
- package/dist/cjs/node_modules/@2112-lab/pathfinder/dist/index.esm.js +1448 -0
- package/dist/cjs/src/animationManager.js +15 -15
- package/dist/cjs/src/componentManager.js +8 -8
- package/dist/cjs/src/disposalManager.js +12 -12
- package/dist/cjs/src/environmentManager.js +392 -99
- package/dist/cjs/src/hotReloadManager.js +26 -26
- package/dist/cjs/src/index.js +19 -66
- package/dist/cjs/src/keyboardControlsManager.js +23 -23
- package/dist/cjs/src/nameUtils.js +21 -21
- package/dist/cjs/src/pathfindingManager.js +311 -129
- package/dist/cjs/src/performanceMonitor.js +52 -52
- package/dist/cjs/src/sceneExportManager.js +23 -23
- package/dist/cjs/src/sceneInitializationManager.js +18 -18
- package/dist/cjs/src/textureConfig.js +469 -40
- package/dist/cjs/src/transformControlsManager.js +79 -79
- package/dist/esm/_virtual/_rollupPluginBabelHelpers.js +21 -10
- package/dist/esm/node_modules/@2112-lab/pathfinder/dist/index.esm.js +1444 -0
- package/dist/esm/src/animationManager.js +15 -15
- package/dist/esm/src/componentManager.js +8 -8
- package/dist/esm/src/disposalManager.js +12 -12
- package/dist/esm/src/environmentManager.js +393 -100
- package/dist/esm/src/hotReloadManager.js +26 -26
- package/dist/esm/src/index.js +19 -66
- package/dist/esm/src/keyboardControlsManager.js +23 -23
- package/dist/esm/src/nameUtils.js +21 -21
- package/dist/esm/src/pathfindingManager.js +313 -129
- package/dist/esm/src/performanceMonitor.js +52 -52
- package/dist/esm/src/sceneExportManager.js +23 -23
- package/dist/esm/src/sceneInitializationManager.js +18 -18
- package/dist/esm/src/textureConfig.js +469 -42
- package/dist/esm/src/transformControlsManager.js +79 -79
- package/dist/index.d.ts +255 -259
- package/package.json +52 -53
- package/dist/cjs/src/ConnectionManager.js +0 -114
- package/dist/cjs/src/Pathfinder.js +0 -88
- package/dist/cjs/src/modelPreloader.js +0 -360
- package/dist/cjs/src/sceneOperationsManager.js +0 -560
- package/dist/esm/src/ConnectionManager.js +0 -110
- package/dist/esm/src/Pathfinder.js +0 -84
- package/dist/esm/src/modelPreloader.js +0 -337
- 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;
|