@2112-lab/pathfinder
Version:
Pure JavaScript 3D pathfinding algorithm library for industrial plant pipe routing
204 lines (183 loc) • 7.02 kB
JavaScript
import { Vector3 } from './Vector3.js';
/**
* Manages scene operations including object finding, position calculations, and hierarchy traversal
*/
export class SceneManager {
/**
* 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
*/
constructor(scene, connectorBBoxSize = 0.1) {
this.scene = scene;
this.connectorBBoxSize = connectorBBoxSize;
}
/**
* Get the root scene object
* @returns {Object} The root scene object
*/
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
*/
getWorldPosition(object) {
// New format: direct position
if (object.userData?.position) {
const [x, y, z] = object.userData.position;
return new Vector3(x, y, z);
}
// Legacy format: worldBoundingBox
if (object.userData?.worldBoundingBox) {
const { min, max } = object.userData.worldBoundingBox;
return new Vector3(
(min[0] + max[0]) / 2,
(min[1] + max[1]) / 2,
(min[2] + max[2]) / 2
);
}
console.warn(`[SceneManager] Object ${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
*/
getBoundingBox(object) {
// Legacy format: use existing worldBoundingBox
if (object.userData?.worldBoundingBox) {
const { min, max } = object.userData.worldBoundingBox;
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.userData?.position) {
const [x, y, z] = object.userData.position;
const 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 ${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
*/
isPositionBased(object) {
return !!(object.userData?.position);
}
/**
* Find an object by UUID in the scene hierarchy
* @param {string} uuid - UUID to search for
* @returns {Object|null} Found object or null
*/
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
*/
_findObjectByUUIDRecursive(node, uuid) {
if (node.uuid === uuid) {
return node;
}
if (node.children) {
for (const child of node.children) {
const found = this._findObjectByUUIDRecursive(child, uuid);
if (found) return found;
}
}
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
*/
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
*/
_findParentObjectRecursive(root, target) {
if (root.children) {
for (const child of root.children) {
if (child.uuid === target.uuid) {
// If parent is the scene root, return null
if (root === this.scene) {
return null;
}
return root;
}
}
for (const child of root.children) {
const parent = this._findParentObjectRecursive(child, target);
if (parent) {
return parent;
}
}
}
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
*/
isVoxelOccupiedByMesh(voxelKey, gridSize) {
const [x, y, z] = voxelKey.split(',').map(Number);
const 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
*/
_checkObjectOccupancy(object, worldPos) {
const 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) {
for (const child of object.children) {
if (this._checkObjectOccupancy(child, worldPos)) return true;
}
}
return false;
}
}