UNPKG

@2112-lab/pathfinder

Version:

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

204 lines (183 loc) 7.02 kB
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; } }