@2112-lab/pathfinder
Version:
Pure JavaScript 3D pathfinding algorithm library for industrial plant pipe routing
1,365 lines (1,280 loc) • 111 kB
JavaScript
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