UNPKG

@2112-lab/pathfinder

Version:

Pure JavaScript 3D pathfinding algorithm library for industrial plant pipe routing

1,365 lines (1,280 loc) 111 kB
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var n = 0, F = function () {}; return { s: F, n: function () { return n >= r.length ? { done: !0 } : { done: !1, value: r[n++] }; }, e: function (r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function () { t = t.call(r); }, n: function () { var r = t.next(); return a = r.done, r; }, e: function (r) { u = !0, o = r; }, f: function () { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } /** * A simple 3D vector class for internal use * @class Vector3 */ var Vector3 = /*#__PURE__*/function () { /** * Create a new Vector3 * @param {number} [x=0] - The x component * @param {number} [y=0] - The y component * @param {number} [z=0] - The z component */ function Vector3() { var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var z = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; _classCallCheck(this, Vector3); this.x = x; this.y = y; this.z = z; } /** * Create a copy of this vector * @returns {Vector3} A new vector with the same components */ return _createClass(Vector3, [{ key: "clone", value: function clone() { return new Vector3(this.x, this.y, this.z); } /** * Convert the vector to an array * @returns {Array<number>} Array representation [x, y, z] */ }, { key: "toArray", value: function toArray() { return [this.x, this.y, this.z]; } /** * Normalize this vector (make it unit length) * @returns {Vector3} This vector (for chaining) */ }, { key: "normalize", value: function normalize() { var length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); if (length > 0) { this.x /= length; this.y /= length; this.z /= length; } return this; } /** * Subtract another vector from this one * @param {Vector3} v - Vector to subtract * @returns {Vector3} This vector (for chaining) */ }, { key: "sub", value: function sub(v) { this.x -= v.x; this.y -= v.y; this.z -= v.z; return this; } /** * Multiply this vector by a scalar value * @param {number} scalar - The scalar value to multiply by * @returns {Vector3} This vector (for chaining) */ }, { key: "multiplyScalar", value: function multiplyScalar(scalar) { this.x *= scalar; this.y *= scalar; this.z *= scalar; return this; } /** * Add another vector to this one * @param {Vector3} v - Vector to add * @returns {Vector3} This vector (for chaining) */ }, { key: "add", value: function add(v) { this.x += v.x; this.y += v.y; this.z += v.z; return this; } }]); }(); /** * Manages scene operations including object finding, position calculations, and hierarchy traversal */ var SceneManager = /*#__PURE__*/function () { /** * Create a new SceneManager instance * @param {Object} scene - The scene configuration object * @param {number} [connectorBBoxSize=0.1] - Default bounding box size for position-based connectors */ function SceneManager(scene) { var connectorBBoxSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0.1; _classCallCheck(this, SceneManager); this.scene = scene; this.connectorBBoxSize = connectorBBoxSize; } /** * Get the root scene object * @returns {Object} The root scene object */ return _createClass(SceneManager, [{ key: "getRoot", value: function getRoot() { // The scene passed to SceneManager should already be the root object with children return this.scene; } /** * Get world position - supports both position and worldBoundingBox formats * @param {Object} object - Scene object * @returns {Vector3} World position */ }, { key: "getWorldPosition", value: function getWorldPosition(object) { var _object$userData, _object$userData2; // New format: direct position if ((_object$userData = object.userData) !== null && _object$userData !== void 0 && _object$userData.position) { var _object$userData$posi = _slicedToArray(object.userData.position, 3), x = _object$userData$posi[0], y = _object$userData$posi[1], z = _object$userData$posi[2]; return new Vector3(x, y, z); } // Legacy format: worldBoundingBox if ((_object$userData2 = object.userData) !== null && _object$userData2 !== void 0 && _object$userData2.worldBoundingBox) { var _object$userData$worl = object.userData.worldBoundingBox, min = _object$userData$worl.min, max = _object$userData$worl.max; return new Vector3((min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2); } console.warn("[SceneManager] Object ".concat(object.uuid, " has no position or worldBoundingBox")); return new Vector3(); } /** * Get bounding box - calculates for position format, returns existing for worldBoundingBox * @param {Object} object - Scene object * @returns {{min: Vector3, max: Vector3}|null} Bounding box or null */ }, { key: "getBoundingBox", value: function getBoundingBox(object) { var _object$userData3, _object$userData4; // Legacy format: use existing worldBoundingBox if ((_object$userData3 = object.userData) !== null && _object$userData3 !== void 0 && _object$userData3.worldBoundingBox) { var _object$userData$worl2 = object.userData.worldBoundingBox, min = _object$userData$worl2.min, max = _object$userData$worl2.max; return { min: new Vector3(min[0], min[1], min[2]), max: new Vector3(max[0], max[1], max[2]) }; } // New format: calculate small bounding box from position if ((_object$userData4 = object.userData) !== null && _object$userData4 !== void 0 && _object$userData4.position) { var _object$userData$posi2 = _slicedToArray(object.userData.position, 3), x = _object$userData$posi2[0], y = _object$userData$posi2[1], z = _object$userData$posi2[2]; var halfSize = this.connectorBBoxSize / 2; return { min: new Vector3(x - halfSize, y - halfSize, z - halfSize), max: new Vector3(x + halfSize, y + halfSize, z + halfSize) }; } console.warn("[SceneManager] Object ".concat(object.uuid, " has no position or worldBoundingBox")); return null; } /** * Check if object is using new position-based format * @param {Object} object - Scene object * @returns {boolean} True if using position format */ }, { key: "isPositionBased", value: function isPositionBased(object) { var _object$userData5; return !!((_object$userData5 = object.userData) !== null && _object$userData5 !== void 0 && _object$userData5.position); } /** * Find an object by UUID in the scene hierarchy * @param {string} uuid - UUID to search for * @returns {Object|null} Found object or null */ }, { key: "findObjectByUUID", value: function findObjectByUUID(uuid) { return this._findObjectByUUIDRecursive(this.getRoot(), uuid); } /** * Recursive helper for findObjectByUUID * @private * @param {Object} node - Current node to search * @param {string} uuid - UUID to search for * @returns {Object|null} Found object or null */ }, { key: "_findObjectByUUIDRecursive", value: function _findObjectByUUIDRecursive(node, uuid) { if (node.uuid === uuid) { return node; } if (node.children) { var _iterator = _createForOfIteratorHelper(node.children), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var child = _step.value; var found = this._findObjectByUUIDRecursive(child, uuid); if (found) return found; } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } return null; } /** * Find the parent object of a given object in the scene hierarchy * @param {Object} target - Target object to find parent for * @returns {Object|null} Parent object or null if not found or if parent is the scene */ }, { key: "findParentObject", value: function findParentObject(target) { if (target.userData && Array.isArray(target.userData.direction)) { return { isDirectionParent: true }; } return this._findParentObjectRecursive(this.getRoot(), target); } /** * Recursive helper for findParentObject * @private * @param {Object} root - Root object to search from * @param {Object} target - Target object to find parent for * @returns {Object|null} Parent object or null */ }, { key: "_findParentObjectRecursive", value: function _findParentObjectRecursive(root, target) { if (root.children) { var _iterator2 = _createForOfIteratorHelper(root.children), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var child = _step2.value; if (child.uuid === target.uuid) { // If parent is the scene root, return null if (root === this.scene) { return null; } return root; } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } var _iterator3 = _createForOfIteratorHelper(root.children), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var _child = _step3.value; var parent = this._findParentObjectRecursive(_child, target); if (parent) { return parent; } } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } } return null; } /** * Check if a voxel is occupied by any mesh object * @param {string} voxelKey - Voxel key to check * @param {number} gridSize - Size of each grid cell * @returns {boolean} True if the voxel is occupied by a mesh object */ }, { key: "isVoxelOccupiedByMesh", value: function isVoxelOccupiedByMesh(voxelKey, gridSize) { var _voxelKey$split$map = voxelKey.split(',').map(Number), _voxelKey$split$map2 = _slicedToArray(_voxelKey$split$map, 3), x = _voxelKey$split$map2[0], y = _voxelKey$split$map2[1], z = _voxelKey$split$map2[2]; var worldPos = new Vector3(x * gridSize, y * gridSize, z * gridSize); return this._checkObjectOccupancy(this.getRoot(), worldPos); } /** * Recursive helper for isVoxelOccupiedByMesh * @private * @param {Object} object - Object to check * @param {Vector3} worldPos - World position to check * @returns {boolean} True if the position is occupied */ }, { key: "_checkObjectOccupancy", value: function _checkObjectOccupancy(object, worldPos) { var bbox = this.getBoundingBox(object); if (bbox) { if (worldPos.x >= bbox.min.x && worldPos.x <= bbox.max.x && worldPos.y >= bbox.min.y && worldPos.y <= bbox.max.y && worldPos.z >= bbox.min.z && worldPos.z <= bbox.max.z) { return true; } } if (object.children) { var _iterator4 = _createForOfIteratorHelper(object.children), _step4; try { for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { var child = _step4.value; if (this._checkObjectOccupancy(child, worldPos)) return true; } } catch (err) { _iterator4.e(err); } finally { _iterator4.f(); } } return false; } }]); }(); /** * Manages grid operations including voxel conversions, neighbor calculations, and distance metrics */ var GridSystem = /*#__PURE__*/function () { /** * Create a new GridSystem instance * @param {number} gridSize - Size of each grid cell in world units * @param {number} safetyMargin - Safety margin around obstacles in world units */ function GridSystem() { var gridSize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0.5; var safetyMargin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; _classCallCheck(this, GridSystem); this.gridSize = gridSize; this.safetyMargin = safetyMargin; } /** * Convert a position to a voxel key * @param {Vector3|Object} pos - Position to convert * @param {number} pos.x - X coordinate * @param {number} pos.y - Y coordinate * @param {number} pos.z - Z coordinate * @returns {string} Voxel key in format "x,y,z" */ return _createClass(GridSystem, [{ key: "voxelKey", value: function voxelKey(pos) { return [Math.round(pos.x / this.gridSize), Math.round(pos.y / this.gridSize), Math.round(pos.z / this.gridSize)].join(','); } /** * Convert a voxel key to a Vector3 * @param {string} key - Voxel key * @returns {Vector3} Vector3 position */ }, { key: "voxelToVec3", value: function voxelToVec3(key) { var _key$split$map = key.split(',').map(Number), _key$split$map2 = _slicedToArray(_key$split$map, 3), x = _key$split$map2[0], y = _key$split$map2[1], z = _key$split$map2[2]; return new Vector3(x * this.gridSize, y * this.gridSize, z * this.gridSize); } /** * Get neighboring voxels (axis-aligned) * @param {string} vox - Voxel key * @returns {string[]} Array of neighboring voxel keys */ }, { key: "getNeighbors", value: function getNeighbors(vox) { var _vox$split$map = vox.split(',').map(Number), _vox$split$map2 = _slicedToArray(_vox$split$map, 3), x = _vox$split$map2[0], y = _vox$split$map2[1], z = _vox$split$map2[2]; return [[x + 1, y, z], [x - 1, y, z], [x, y + 1, z], [x, y - 1, z], [x, y, z + 1], [x, y, z - 1]].map(function (arr) { return arr.join(','); }); } /** * Calculate Manhattan distance between two voxels * @param {string} a - First voxel key * @param {string} b - Second voxel key * @returns {number} Manhattan distance */ }, { key: "manhattan", value: function manhattan(a, b) { var _a$split$map = a.split(',').map(Number), _a$split$map2 = _slicedToArray(_a$split$map, 3), ax = _a$split$map2[0], ay = _a$split$map2[1], az = _a$split$map2[2]; var _b$split$map = b.split(',').map(Number), _b$split$map2 = _slicedToArray(_b$split$map, 3), bx = _b$split$map2[0], by = _b$split$map2[1], bz = _b$split$map2[2]; return Math.abs(ax - bx) + Math.abs(ay - by) + Math.abs(az - bz); } /** * Calculate the number of voxels needed for a given world distance * @param {number} worldDistance - Distance in world units * @returns {number} Number of voxels */ }, { key: "worldToVoxelDistance", value: function worldToVoxelDistance(worldDistance) { return Math.ceil(worldDistance / this.gridSize); } /** * Calculate the world distance for a given number of voxels * @param {number} voxelCount - Number of voxels * @returns {number} Distance in world units */ }, { key: "voxelToWorldDistance", value: function voxelToWorldDistance(voxelCount) { return voxelCount * this.gridSize; } /** * Get the safety margin in voxel units * @returns {number} Safety margin in voxels */ }, { key: "getSafetyMarginVoxels", value: function getSafetyMarginVoxels() { return this.worldToVoxelDistance(this.safetyMargin); } /** * Check if a voxel is within the safety margin of a bounding box * @param {string} voxelKey - Voxel key to check * @param {Object} bbox - Bounding box * @param {Vector3} bbox.min - Minimum coordinates * @param {Vector3} bbox.max - Maximum coordinates * @returns {boolean} True if the voxel is within the safety margin */ }, { key: "isVoxelInSafetyMargin", value: function isVoxelInSafetyMargin(voxelKey, bbox) { var voxelPos = this.voxelToVec3(voxelKey); var margin = this.safetyMargin; return voxelPos.x >= bbox.min.x - margin && voxelPos.x <= bbox.max.x + margin && voxelPos.y >= bbox.min.y - margin && voxelPos.y <= bbox.max.y + margin && voxelPos.z >= bbox.min.z - margin && voxelPos.z <= bbox.max.z + margin; } /** * Get all voxel keys within a bounding box plus safety margin * @param {Object} bbox - Bounding box * @param {Vector3} bbox.min - Minimum coordinates * @param {Vector3} bbox.max - Maximum coordinates * @returns {string[]} Array of voxel keys */ }, { key: "getVoxelsInBoundingBox", value: function getVoxelsInBoundingBox(bbox) { var margin = this.getSafetyMarginVoxels(); var min = bbox.min; var max = bbox.max; var voxels = []; for (var x = Math.round(min.x / this.gridSize) - margin; x <= Math.round(max.x / this.gridSize) + margin; x++) { for (var y = Math.round(min.y / this.gridSize) - margin; y <= Math.round(max.y / this.gridSize) + margin; y++) { for (var z = Math.round(min.z / this.gridSize) - margin; z <= Math.round(max.z / this.gridSize) + margin; z++) { voxels.push("".concat(x, ",").concat(y, ",").concat(z)); } } } return voxels; } }]); }(); /** * Manages connector-related operations for the pathfinding system * @class ConnectorManager */ var ConnectorManager = /*#__PURE__*/function () { /** * Create a new ConnectorManager instance * @param {Object} config - The configuration object * @param {Array<Object>} config.connections - Array of connections between objects * @param {string} config.connections[].from - UUID of the source object * @param {string} config.connections[].to - UUID of the target object * @param {number} minSegmentLength - Minimum length for straight pipe segments in world units * @param {SceneManager} sceneManager - Instance of SceneManager for scene operations * @param {GridSystem} gridSystem - Instance of GridSystem for grid operations */ function ConnectorManager(config, minSegmentLength, sceneManager, gridSystem) { _classCallCheck(this, ConnectorManager); this.config = config; this.MIN_SEGMENT_LENGTH = minSegmentLength; this.sceneManager = sceneManager; this.gridSystem = gridSystem; } /** * Check if an object is a connector by checking if it's mentioned in connections * @param {Object} object - Scene object * @returns {boolean} True if the object is a connector */ return _createClass(ConnectorManager, [{ key: "isConnector", value: function isConnector(object) { if (!this.config.connections) return false; // Check if the object's UUID appears in any connection return this.config.connections.some(function (conn) { return conn.from === object.uuid || conn.to === object.uuid; }); } /** * Calculate direction vector for a connector * @param {Object} connector - Connector object * @returns {Vector3|null} Direction vector or null if no parent or direction */ }, { key: "getConnectorDirection", value: function getConnectorDirection(connector) { // First check if direction is directly specified in userData if (connector.userData && Array.isArray(connector.userData.direction)) { var _connector$userData$d = _slicedToArray(connector.userData.direction, 3), x = _connector$userData$d[0], y = _connector$userData$d[1], z = _connector$userData$d[2]; return new Vector3(x, y, z); } // If no direction in userData and no parent, return null if (!connector.parent) { return null; } // For backward compatibility, calculate direction from parent if no userData.direction console.log("[WARNING] No direction in userData for ".concat(connector.name || connector.uuid, " - using parent-based calculation (legacy mode)")); // Get world position of the connector var connectorPos = this.sceneManager.getWorldPosition(connector); // For the parent, use the center of its bounding box if available var parentPos; var parentBBox = this.sceneManager.getBoundingBox(connector.parent); if (parentBBox) { // Calculate the center of the parent's bounding box parentPos = new Vector3((parentBBox.min.x + parentBBox.max.x) / 2, (parentBBox.min.y + parentBBox.max.y) / 2, (parentBBox.min.z + parentBBox.max.z) / 2); } else { // Fall back to parent's position if bounding box is not available parentPos = this.sceneManager.getWorldPosition(connector.parent); } // Calculate the direction vector from parent to connector var direction = new Vector3(connectorPos.x - parentPos.x, connectorPos.y - parentPos.y, connectorPos.z - parentPos.z).normalize(); // Enforce orthogonality by aligning with the dominant axis var absX = Math.abs(direction.x); var absY = Math.abs(direction.y); var absZ = Math.abs(direction.z); if (absX >= absY && absX >= absZ) { // X is dominant direction.y = 0; direction.z = 0; direction.x = Math.sign(direction.x); } else if (absY >= absX && absY >= absZ) { // Y is dominant direction.x = 0; direction.z = 0; direction.y = Math.sign(direction.y); } else { // Z is dominant direction.x = 0; direction.y = 0; direction.z = Math.sign(direction.z); } return direction; } /** * Create a virtual segment for a connector * @param {Object} connector - Connector object * @param {Vector3} connectorPos - World position of the connector * @returns {Object|null} Virtual segment info or null if no parent */ }, { key: "createVirtualSegment", value: function createVirtualSegment(connector, connectorPos) { var direction = this.getConnectorDirection(connector); if (!direction) { return null; } // Create a virtual segment starting from the connector position // and extending in the direction vector for MIN_SEGMENT_LENGTH var endPos = new Vector3(connectorPos.x + direction.x * this.MIN_SEGMENT_LENGTH, connectorPos.y + direction.y * this.MIN_SEGMENT_LENGTH, connectorPos.z + direction.z * this.MIN_SEGMENT_LENGTH); // Convert to voxel keys var startKey = this.gridSystem.voxelKey(connectorPos); var endKey = this.gridSystem.voxelKey(endPos); return { startPos: connectorPos, endPos: endPos, direction: direction, startKey: startKey, endKey: endKey }; } /** * Process all connectors in the scene and mark their virtual segments as occupied * @param {Object} sceneRoot - Root scene object * @param {Set<string>} pathOccupied - Set of occupied voxel keys * @param {string} excludeUUID1 - UUID of first object to exclude * @param {string} excludeUUID2 - UUID of second object to exclude */ }, { key: "processConnectors", value: function processConnectors(sceneRoot, pathOccupied, excludeUUID1, excludeUUID2) { this._processConnectorsRecursive(sceneRoot, pathOccupied, excludeUUID1, excludeUUID2); } /** * Recursive helper for processConnectors * @private * @param {Object} object - Current object to process * @param {Set<string>} pathOccupied - Set of occupied voxel keys * @param {string} excludeUUID1 - UUID of first object to exclude * @param {string} excludeUUID2 - UUID of second object to exclude */ }, { key: "_processConnectorsRecursive", value: function _processConnectorsRecursive(object, pathOccupied, excludeUUID1, excludeUUID2) { // Skip the excluded objects if (object.uuid === excludeUUID1 || object.uuid === excludeUUID2) { return; } var connectorPos = this.sceneManager.getWorldPosition(object); var segment = this.createVirtualSegment(object, connectorPos); if (segment) { this._markVirtualSegmentAsOccupied(segment, connectorPos, pathOccupied); } // Process children recursively if (object.children) { var _iterator = _createForOfIteratorHelper(object.children), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var child = _step.value; this._processConnectorsRecursive(child, pathOccupied, excludeUUID1, excludeUUID2); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } } /** * Mark voxels in a virtual segment as occupied * @private * @param {Object} segment - Virtual segment info * @param {Vector3} connectorPos - Position of the connector * @param {Set<string>} pathOccupied - Set of occupied voxel keys */ }, { key: "_markVirtualSegmentAsOccupied", value: function _markVirtualSegmentAsOccupied(segment, connectorPos, pathOccupied) { var direction = segment.direction; var distance = this.gridSystem.worldToVoxelDistance(this.MIN_SEGMENT_LENGTH + this.gridSystem.safetyMargin * 2); for (var i = 0; i <= distance; i++) { var x = Math.round(connectorPos.x / this.gridSystem.gridSize + direction.x * i); var y = Math.round(connectorPos.y / this.gridSystem.gridSize + direction.y * i); var z = Math.round(connectorPos.z / this.gridSystem.gridSize + direction.z * i); var key = "".concat(x, ",").concat(y, ",").concat(z); pathOccupied.add(key); } } /** * Cluster connections by shared objects * @param {Array<Object>} connections - Array of connections between objects * @returns {Array<Object>} Array of clusters with enriched metadata */ }, { key: "clusterConnections", value: function clusterConnections(connections) { // Input validation if (!connections || !Array.isArray(connections)) { console.warn('[ConnectorManager] clusterConnections: Invalid connections input'); return []; } if (connections.length === 0) { return []; } var clusters = new Map(); // Map of object UUID to its cluster ID var clusterMap = new Map(); // Map of cluster ID to set of object UUIDs var nextClusterId = 0; // Build clusters using union-find approach connections.forEach(function (conn, index) { // Validate connection structure if (!conn || typeof conn.from !== 'string' || typeof conn.to !== 'string') { console.warn("[ConnectorManager] Invalid connection at index ".concat(index, ":"), conn); return; // Skip invalid connection } var from = conn.from, to = conn.to; var fromClusterId = clusters.get(from); var toClusterId = clusters.get(to); // Case 1: Neither object has a cluster - create new cluster if (fromClusterId === undefined && toClusterId === undefined) { var clusterId = nextClusterId++; clusters.set(from, clusterId); clusters.set(to, clusterId); clusterMap.set(clusterId, new Set([from, to])); } // Case 2: Only 'from' has a cluster - add 'to' to it else if (fromClusterId !== undefined && toClusterId === undefined) { clusters.set(to, fromClusterId); clusterMap.get(fromClusterId).add(to); } // Case 3: Only 'to' has a cluster - add 'from' to it else if (fromClusterId === undefined && toClusterId !== undefined) { clusters.set(from, toClusterId); clusterMap.get(toClusterId).add(from); } // Case 4: Both have clusters - merge if different else if (fromClusterId !== toClusterId) { // Merge smaller cluster into larger one for efficiency var fromCluster = clusterMap.get(fromClusterId); var toCluster = clusterMap.get(toClusterId); var _ref = fromCluster.size >= toCluster.size ? [toClusterId, fromClusterId, toCluster, fromCluster] : [fromClusterId, toClusterId, fromCluster, toCluster], _ref2 = _slicedToArray(_ref, 4), sourceId = _ref2[0], targetId = _ref2[1], sourceSet = _ref2[2], targetSet = _ref2[3]; // Update all objects in source cluster sourceSet.forEach(function (objId) { clusters.set(objId, targetId); targetSet.add(objId); }); clusterMap["delete"](sourceId); } // Case 5: Both in same cluster - no action needed }); // Convert to array format var clustersArray = Array.from(clusterMap.entries()).map(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), clusterId = _ref4[0], objects = _ref4[1]; return { clusterId: clusterId, objects: Array.from(objects) }; }); console.log('[Pathfinder] Initial clusters:', clustersArray); // Filter clusters based on gateway rules var filteredClusters = this._filterGatewayClusters(clustersArray); console.log('[Pathfinder] Filtered clusters:', filteredClusters); // Enrich clusters with spatial data var enrichedClusters = this._enrichClustersWithSpatialData(filteredClusters); return enrichedClusters; } /** * Filter out clusters containing only computed gateways * @private * @param {Array<Object>} clusters - Array of cluster objects * @returns {Array<Object>} Filtered clusters */ }, { key: "_filterGatewayClusters", value: function _filterGatewayClusters(clusters) { var _this = this; return clusters.filter(function (cluster) { // Find all gateway UUIDs in this cluster var gatewayUUIDs = cluster.objects.filter(function (uuid) { return _this._isGatewayUUID(uuid); }); // No gateways? Keep the cluster if (gatewayUUIDs.length === 0) { return true; } // Has gateways - check if any are manually declared var hasManualGateway = gatewayUUIDs.some(function (uuid) { var _gatewayObj$userData; var gatewayObj = _this.sceneManager.findObjectByUUID(uuid); if (!gatewayObj) { console.warn("[ConnectorManager] Gateway object not found: ".concat(uuid)); return false; // Treat missing gateways as computed (filter out) } // A gateway is "manual" (declared) if isDeclared is explicitly true // Computed gateways have isDeclared === false or undefined var isDeclared = (_gatewayObj$userData = gatewayObj.userData) === null || _gatewayObj$userData === void 0 ? void 0 : _gatewayObj$userData.isDeclared; var isManual = isDeclared === true; if (isManual) { console.log("[Pathfinder] Found manual (declared) gateway: ".concat(uuid, " - preserving cluster")); } return isManual; }); return hasManualGateway; }); } /** * Check if a UUID represents a gateway object * @private * @param {string} uuid - Object UUID to check * @returns {boolean} True if UUID is for a gateway */ }, { key: "_isGatewayUUID", value: function _isGatewayUUID(uuid) { return typeof uuid === 'string' && uuid.includes('Gateway'); } /** * Enrich clusters with position and direction data * @private * @param {Array<Object>} clusters - Array of cluster objects * @returns {Array<Object>} Enriched clusters */ }, { key: "_enrichClustersWithSpatialData", value: function _enrichClustersWithSpatialData(clusters) { var _this2 = this; return clusters.map(function (cluster) { // Extract object points with error handling cluster.objectPoints = cluster.objects.map(function (uuid) { try { var _object$userData, _object$userData2; var object = _this2.sceneManager.findObjectByUUID(uuid); if (!object) { console.warn("[ConnectorManager] Object not found for UUID: ".concat(uuid)); return null; } // Check if object has required spatial data var hasPosition = (_object$userData = object.userData) === null || _object$userData === void 0 ? void 0 : _object$userData.position; var hasBoundingBox = (_object$userData2 = object.userData) === null || _object$userData2 === void 0 ? void 0 : _object$userData2.worldBoundingBox; if (!hasPosition && !hasBoundingBox) { console.warn("[ConnectorManager] No spatial data for: ".concat(uuid)); return null; } var center = _this2.sceneManager.getWorldPosition(object); var direction = _this2._extractDirection(object); return { uuid: uuid, point: center, direction: direction }; } catch (error) { console.error("[ConnectorManager] Error processing object ".concat(uuid, ":"), error); return null; } }).filter(function (point) { return point !== null; }); // Generate displaced points for pathfinding cluster.displacedPoints = cluster.objectPoints.map(function (objPoint) { var displacedPoint = objPoint.direction ? objPoint.point.clone().add(objPoint.direction.clone().multiplyScalar(0.5)) : objPoint.point.clone(); return { uuid: objPoint.uuid, originalPoint: objPoint.point, displacedPoint: displacedPoint, direction: objPoint.direction }; }); return cluster; }); } /** * Extract direction vector from object userData * @private * @param {Object} object - Scene object * @returns {Vector3|null} Direction vector or null */ }, { key: "_extractDirection", value: function _extractDirection(object) { var _object$userData3; if (!((_object$userData3 = object.userData) !== null && _object$userData3 !== void 0 && _object$userData3.direction)) { return null; } var dir = object.userData.direction; if (!Array.isArray(dir) || dir.length !== 3) { console.warn("[ConnectorManager] Invalid direction format for ".concat(object.uuid)); return null; } return new Vector3(dir[0], dir[1], dir[2]); } }]); }(); /** * Manages path-related operations for the pathfinding system * @class PathManager */ var PathManager = /*#__PURE__*/function () { /** * Create a new PathManager instance * @param {SceneManager} sceneManager - Instance of SceneManager for scene operations * @param {GridSystem} gridSystem - Instance of GridSystem for grid operations * @param {ConnectorManager} connectorManager - Instance of ConnectorManager for connector operations * @param {number} minSegmentLength - Minimum length for straight pipe segments in world units * @param {number} astarTimeout - Timeout for A* pathfinding in milliseconds * @param {number} protectedSegmentCount - Number of segments adjacent to connectors to protect from merging (default: 1) */ function PathManager(sceneManager, gridSystem, connectorManager, minSegmentLength, astarTimeout) { var protectedSegmentCount = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1; _classCallCheck(this, PathManager); this.sceneManager = sceneManager; this.gridSystem = gridSystem; this.connectorManager = connectorManager; this.MIN_SEGMENT_LENGTH = minSegmentLength; this.ASTAR_TIMEOUT = astarTimeout; this.PROTECTED_SEGMENT_COUNT = protectedSegmentCount; } /** * A* pathfinding algorithm with bend minimization * @private * @param {string} start - Start voxel key * @param {string} goal - Goal voxel key * @param {Set<string>} occupied - Set of occupied voxel keys * @returns {string[]|null} Path as array of voxel keys or null if no path found */ return _createClass(PathManager, [{ key: "astar", value: function astar(start, goal, occupied) { var open = new Set([start]); var cameFrom = {}; var gScore = _defineProperty({}, start, 0); var fScore = _defineProperty({}, start, this.gridSystem.manhattan(start, goal)); // Add timeout to prevent infinite loops var startTime = Date.now(); while (open.size > 0) { // Check for timeout if (Date.now() - startTime > this.ASTAR_TIMEOUT) { console.log('[Warning] Pathfinding timed out'); return null; } // Get node in open with lowest fScore var current = null; var minF = Infinity; var _iterator = _createForOfIteratorHelper(open), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var node = _step.value; if (fScore[node] < minF) { minF = fScore[node]; current = node; } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } if (current === goal) { // Reconstruct path var path = [current]; while (cameFrom[current]) { current = cameFrom[current]; path.unshift(current); } return path; } open["delete"](current); var _iterator2 = _createForOfIteratorHelper(this.gridSystem.getNeighbors(current)), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var _gScore$neighbor; var neighbor = _step2.value; if (occupied.has(neighbor)) continue; // Calculate cost with bend penalty var tentativeG = this.calculateGScore(current, neighbor, cameFrom[current], gScore[current]); // const tentativeG = gScore[current] + 1; if (tentativeG < ((_gScore$neighbor = gScore[neighbor]) !== null && _gScore$neighbor !== void 0 ? _gScore$neighbor : Infinity)) { cameFrom[neighbor] = current; gScore[neighbor] = tentativeG; fScore[neighbor] = tentativeG + this.gridSystem.manhattan(neighbor, goal); open.add(neighbor); } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } return null; } /** * Calculate G score with bend penalty * @private * @param {string} current - Current voxel key * @param {string} neighbor - Neighbor voxel key * @param {string} parent - Parent voxel key (null if current is start) * @param {number} currentG - Current G score * @returns {number} G score with bend penalty */ }, { key: "calculateGScore", value: function calculateGScore(current, neighbor, parent, currentG) { var cost = currentG + 1; // Base movement cost if (parent) { // Check if this move creates a bend var _parent$split$map = parent.split(',').map(Number), _parent$split$map2 = _slicedToArray(_parent$split$map, 3), px = _parent$split$map2[0], py = _parent$split$map2[1], pz = _parent$split$map2[2]; var _current$split$map = current.split(',').map(Number), _current$split$map2 = _slicedToArray(_current$split$map, 3), cx = _current$split$map2[0], cy = _current$split$map2[1], cz = _current$split$map2[2]; var _neighbor$split$map = neighbor.split(',').map(Number), _neighbor$split$map2 = _slicedToArray(_neighbor$split$map, 3), nx = _neighbor$split$map2[0], ny = _neighbor$split$map2[1], nz = _neighbor$split$map2[2]; // Direction from parent to current var prevDir = { x: cx - px, y: cy - py, z: cz - pz }; // Direction from current to neighbor var nextDir = { x: nx - cx, y: ny - cy, z: nz - cz }; // If directions are different, add bend penalty if (prevDir.x !== nextDir.x || prevDir.y !== nextDir.y || prevDir.z !== nextDir.z) { cost += 0.00001; // Bend penalty - adjust this value to control how much bends are avoided } } return cost; } /** * Find a path respecting virtual segments * @private * @param {string} startKey - Start voxel key * @param {string} endKey - End voxel key * @param {Set<string>} occupied - Set of occupied voxel keys * @param {Object} startSegment - Virtual segment for start connector (optional) * @param {Object} endSegment - Virtual segment for end connector (optional) * @returns {string[]|null} Path as array of voxel keys or null if no path found */ }, { key: "findPathWithVirtualSegments", value: function findPathWithVirtualSegments(startKey, endKey, occupied, startSegment, endSegment) { // If we have virtual segments, we need to ensure the path starts and ends with them var actualStartKey = startKey; var actualEndKey = endKey; // Create a copy of occupied set to avoid modifying the original var pathOccupied = new Set(occupied); // If we have a start segment, use its end as the actual start for pathfinding // and mark the segment as occupied if (startSegment) { actualStartKey = startSegment.endKey; // Mark all voxels in the start segment as occupied var startPos = startSegment.startPos; var direction = startSegment.direction; var distance = this.gridSystem.worldToVoxelDistance(this.MIN_SEGMENT_LENGTH); // Note: we go up to distance-1 to exclude the connecting end point for (var i = 0; i < distance; i++) { var x = Math.round(startPos.x / this.gridSystem.gridSize + direction.x * i); var y = Math.round(startPos.y / this.gridSystem.gridSize + direction.y * i); var z = Math.round(startPos.z / this.gridSystem.gridSize + direction.z * i); var key = "".concat(x, ",").concat(y, ",").concat(z); pathOccupied.add(key); } } // If we have an end segment, use its end as the actual end for pathfinding // and mark the segment as occupied if (endSegment) { actualEndKey = endSegment.endKey; // Mark all voxels in the end segment as occupied var endPos = endSegment.startPos; var _direction = endSegment.direction; var _distance = this.gridSystem.worldToVoxelDistance(this.MIN_SEGMENT_LENGTH); // Note: we go up to distance-1 to exclude the connecting end point for (var _i = 0; _i < _distance; _i++) { var _x = Math.round(endPos.x / this.gridSystem.gridSize + _direction.x * _i); var _y = Math.round(endPos.y / this.gridSystem.gridSize + _direction.y * _i); var _z = Math.round(endPos.z / this.gridSystem.gridSize + _direction.z * _i); var _key = "".concat(_x, ",").concat(_y, ",").concat(_z); pathOccupied.add(_key); } } // Find a path between the actual start and end var middlePath = this.astar(actualStartKey, actualEndKey, pathOccupied); if (!middlePath) { return null; } // Construct the full path var fullPath = []; // Add the start segment if we have one if (startSegment) { fullPath.push(startSegment.startKey); } // Add the middle path fullPath = fullPath.concat(middlePath); // Add the end segment if we have one if (endSegment) { fullPath.push(endSegment.startKey); } return fullPath; } /** * Mark all mesh objects' voxels as occupied in the scene * @private * @param {Object} sceneRoot - Root scene object * @returns {Object} Object containing occupied sets and maps */ }, { key: "markAllMeshesAsOccupiedVoxels", value: function markAllMeshesAsOccupiedVoxels(sceneRoot) { var _this = this; var occupied = new Set(); var occupiedByUUID = new Map(); var _processObject = function processObject(object) { // Skip objects that aren't components or connectors (e.g., Groups, Cameras, Lights) // Check for userData.objectType to identify relevant objects if (!object.userData || !object.userData.objectType) return; // Get object's bounding box var bbox = _this.sceneManager.getBoundingBox(object); if (!bbox) return; // Get all voxels in the bounding box plus safety margin var voxels = _this.gridSyst