Warning, /firebird/firebird-ng/src/app/utils/three.utils.ts is written in an unsupported language. File is not indexed.
0001 import outmatch from 'outmatch';
0002
0003 import * as THREE from "three";
0004 import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils';
0005 import { GeoNodeWalkCallback, walkGeoNodes } from "../../lib-root-geometry/root-geo-navigation";
0006 import { MergeResult } from "./three-geometry-merge";
0007
0008 /**
0009 * Callback function type for walking through Object3D nodes.
0010 *
0011 * @callback NodeWalkCallback
0012 * @param {any} node - The current node being processed.
0013 * @param {string} nodeFullPath - The full hierarchical path to the current node.
0014 * @param {number} level - The current depth level in the hierarchy.
0015 * @returns {boolean} - Determines whether to continue walking the tree.
0016 */
0017 export type NodeWalkCallback = (node: any, nodeFullPath: string, level: number) => boolean;
0018
0019 /**
0020 * Options for walking through Object3D nodes.
0021 *
0022 * @interface NodeWalkOptions
0023 * @property {number} [maxLevel=Infinity] - The maximum depth level to traverse.
0024 * @property {number} [level=0] - The current depth level in the traversal.
0025 * @property {string} [parentPath=""] - The hierarchical path of the parent node.
0026 * @property {any} [pattern=null] - A pattern to match node paths against.
0027 */
0028 interface NodeWalkOptions {
0029 maxLevel?: number;
0030 level?: number;
0031 parentPath?: string;
0032 pattern?: any;
0033 }
0034
0035 /**
0036 * Recursively walks through a THREE.Object3D hierarchy, invoking a callback on each node.
0037 *
0038 * @function walkObject3DNodes
0039 * @param {any} node - The current node in the Object3D hierarchy.
0040 * @param {NodeWalkCallback|null} callback - The function to execute on each node.
0041 * @param {NodeWalkOptions} [options={}] - Configuration options for the traversal.
0042 * @returns {number} - The total number of nodes processed.
0043 */
0044 export function walkObject3DNodes(node: any, callback: NodeWalkCallback | null, options: NodeWalkOptions = {}): number {
0045
0046 // Destructure and set default values for options
0047 let { maxLevel = Infinity, level = 0, parentPath = "", pattern = null } = options;
0048
0049 // Compile the pattern using outmatch if it's a string
0050 if (pattern) {
0051 pattern = typeof pattern === "string" ? outmatch(pattern) : pattern;
0052 }
0053
0054 // Construct the full path of the current node
0055 const fullPath = parentPath ? `${parentPath}/${node.name}` : node.name;
0056 let processedNodes = 1;
0057
0058 // Invoke the callback if the pattern matches or if no pattern is provided
0059 if (!pattern || pattern(fullPath)) {
0060 if (callback) {
0061 callback(node, fullPath, level);
0062 }
0063 }
0064
0065 // Continue recursion if the node has children and the maximum level hasn't been reached
0066 if (node?.children && level < maxLevel) {
0067 // Iterate backwards to safely handle node removal during traversal
0068 for (let i = node.children.length - 1; i >= 0; i--) {
0069 let child = node.children[i];
0070 if (child) {
0071 processedNodes += walkObject3DNodes(child, callback, { maxLevel, level: level + 1, parentPath: fullPath, pattern });
0072 }
0073 }
0074 }
0075
0076 return processedNodes;
0077 }
0078
0079 /**
0080 * Represents the results of a node searching operation within a THREE.Object3D hierarchy.
0081 *
0082 * @interface FindResults
0083 * @property {any[]} nodes - An array of nodes that matched the search criteria. These nodes are part of the THREE.Object3D hierarchy.
0084 * @property {string[]} fullPaths - An array of strings, each representing the full path to a corresponding node in the `nodes` array. The full path is constructed by concatenating parent node names, providing a clear hierarchical structure.
0085 * @property {number} deepestLevel - The deepest level reached in the hierarchy during the search. This value helps understand the depth of the search and which level had the last matched node.
0086 * @property {number} totalWalked - The total number of nodes visited during the search process. This count includes all nodes checked, regardless of whether they matched the criteria.
0087 */
0088 export interface FindResults {
0089 nodes: any[];
0090 fullPaths: string[];
0091 deepestLevel: number;
0092 totalWalked: number;
0093 }
0094
0095 /**
0096 * Searches for and collects nodes in a THREE.Object3D hierarchy based on a given pattern and type.
0097 *
0098 * @function findObject3DNodes
0099 * @param {any} parentNode - The root node of the hierarchy to search within.
0100 * @param {string} pattern - A string pattern to match node names against.
0101 * @param {string} [matchType=""] - Optional filter to restrict results to nodes of a specific type.
0102 * @param {number} [maxLevel=Infinity] - The maximum depth to search within the node hierarchy.
0103 * @returns {FindResults} - An object containing the results of the search:
0104 * - `nodes`: Array of nodes that match the criteria.
0105 * - `fullPaths`: Array of full path strings corresponding to each matched node.
0106 * - `deepestLevel`: The deepest level reached in the hierarchy during the search.
0107 * - `totalWalked`: Total number of nodes visited during the search.
0108 */
0109 export function findObject3DNodes(parentNode: any, pattern: string, matchType: string = "", maxLevel: number = Infinity): FindResults {
0110 let nodes: any[] = [];
0111 let fullPaths: string[] = [];
0112 let deepestLevel = 0;
0113
0114 /**
0115 * Callback function to collect matching nodes.
0116 *
0117 * @param {any} node - The current node being processed.
0118 * @param {string} fullPath - The full hierarchical path to the current node.
0119 * @param {number} level - The current depth level in the hierarchy.
0120 * @returns {boolean} - Continues traversal.
0121 */
0122 const collectNodes: NodeWalkCallback = (node, fullPath, level) => {
0123 if (!matchType || matchType === node.type) {
0124 nodes.push(node);
0125 fullPaths.push(fullPath);
0126 if (level > deepestLevel) {
0127 deepestLevel = level;
0128 }
0129 }
0130 return true; // Continue traversal
0131 };
0132
0133 // Execute the node walk with the collecting callback and the specified pattern
0134 let totalWalked = walkObject3DNodes(parentNode, collectNodes, { maxLevel, pattern });
0135
0136 return {
0137 nodes,
0138 fullPaths,
0139 deepestLevel,
0140 totalWalked
0141 };
0142 }
0143
0144 /**
0145 * Interface representing objects that have a color property.
0146 *
0147 * @interface Colorable
0148 * @property {THREE.Color} color - The color of the object.
0149 */
0150 export interface Colorable {
0151 color: THREE.Color;
0152 }
0153
0154 /**
0155 * Type guard function to check if the material is colorable.
0156 *
0157 * @function isColorable
0158 * @param {any} material - The material to check.
0159 * @returns {material is Colorable} - Returns true if the material has a 'color' property, false otherwise.
0160 */
0161 export function isColorable(material: any): material is Colorable {
0162 return 'color' in material;
0163 }
0164
0165 /**
0166 * Retrieves the color of a material if it is colorable; otherwise, returns a default color.
0167 *
0168 * @function getColorOrDefault
0169 * @param {any} material - The material whose color is to be retrieved.
0170 * @param {THREE.Color} defaultColor - The default color to return if the material is not colorable.
0171 * @returns {THREE.Color} - The color of the material if colorable, or the default color.
0172 */
0173 export function getColorOrDefault(material: any, defaultColor: THREE.Color): THREE.Color {
0174 if (isColorable(material)) {
0175 return material.color;
0176 } else {
0177 return defaultColor;
0178 }
0179 }
0180
0181 /**
0182 * Custom error class thrown when a mesh or object does not contain geometry.
0183 *
0184 * @class NoGeometryError
0185 * @extends {Error}
0186 */
0187 class NoGeometryError extends Error {
0188 /**
0189 * The mesh or object that caused the error.
0190 *
0191 * @type {any}
0192 */
0193 mesh: any = undefined;
0194
0195 /**
0196 * Creates an instance of NoGeometryError.
0197 *
0198 * @constructor
0199 * @param {any} mesh - The mesh or object that lacks geometry.
0200 * @param {string} [message="Mesh (or whatever is provided) does not contain geometry."] - The error message.
0201 */
0202 constructor(mesh: any, message: string = "Mesh (or whatever is provided) does not contain geometry.") {
0203 super(message);
0204 this.name = "NoGeometryError";
0205 this.mesh = mesh;
0206 }
0207 }
0208
0209 /**
0210 * Options for creating an outline around a mesh.
0211 *
0212 * @interface CreateOutlineOptions
0213 * @property {THREE.ColorRepresentation} [color=0x555555] - The color of the outline.
0214 * @property {THREE.Material} [material] - The material to use for the outline.
0215 * @property {number} [thresholdAngle=40] - The angle threshold for edge detection.
0216 */
0217 export interface CreateOutlineOptions {
0218 color?: THREE.ColorRepresentation;
0219 material?: THREE.Material;
0220 thresholdAngle?: number;
0221 }
0222
0223 let globalOutlineCount = 0;
0224
0225 /**
0226 * Applies an outline mesh from lines to a mesh and adds the outline to the mesh's parent.
0227 *
0228 * @function createOutline
0229 * @param {any} mesh - A THREE.Object3D (expected to be a Mesh) to process.
0230 * @param {CreateOutlineOptions} [options={}] - Configuration options for the outline.
0231 * @throws {NoGeometryError} - Throws an error if the mesh does not contain geometry.
0232 */
0233 export function createOutline(mesh: any, options: CreateOutlineOptions = {}): void {
0234 if (!mesh?.geometry) {
0235 throw new NoGeometryError(mesh);
0236 }
0237
0238 let { color = 0x555555, material, thresholdAngle = 40 } = options || {};
0239
0240 // Generate edges geometry based on the threshold angle
0241 let edges = new THREE.EdgesGeometry(mesh.geometry, thresholdAngle);
0242 let lineMaterial = material as THREE.LineBasicMaterial;
0243
0244 // If no material is provided, create a default LineBasicMaterial
0245 if (!lineMaterial) {
0246 lineMaterial = new THREE.LineBasicMaterial({
0247 color: color ?? new THREE.Color(0x555555),
0248 fog: false,
0249 clippingPlanes: mesh.material?.clippingPlanes ? mesh.material.clippingPlanes : [],
0250 clipIntersection: false,
0251 clipShadows: true,
0252 transparent: true
0253 });
0254 }
0255
0256 // Create a LineSegments object for the outline
0257 const edgesLine = new THREE.LineSegments(edges, lineMaterial);
0258 edgesLine.name = (mesh.name ?? "") + "_outline";
0259 edgesLine.userData = {};
0260
0261 // Add the outline to the parent of the mesh
0262 mesh.updateMatrixWorld(true);
0263 mesh?.parent?.add(edgesLine);
0264 globalOutlineCount++;
0265 if(globalOutlineCount>0 && !(globalOutlineCount%10000)) {
0266 console.warn(`createOutline: Created: ${globalOutlineCount} outlines. (it is many)`);
0267 }
0268 }
0269
0270 /**
0271 * Extended properties for THREE.Material to include various texture maps.
0272 *
0273 * @interface ExtendedMaterialProperties
0274 * @property {THREE.Texture | null} [map] - The main texture map.
0275 * @property {THREE.Texture | null} [lightMap] - The light map texture.
0276 * @property {THREE.Texture | null} [bumpMap] - The bump map texture.
0277 * @property {THREE.Texture | null} [normalMap] - The normal map texture.
0278 * @property {THREE.Texture | null} [specularMap] - The specular map texture.
0279 * @property {THREE.Texture | null} [envMap] - The environment map texture.
0280 * @property {THREE.Texture | null} [alphaMap] - The alpha map texture.
0281 * @property {THREE.Texture | null} [aoMap] - The ambient occlusion map texture.
0282 * @property {THREE.Texture | null} [displacementMap] - The displacement map texture.
0283 * @property {THREE.Texture | null} [emissiveMap] - The emissive map texture.
0284 * @property {THREE.Texture | null} [gradientMap] - The gradient map texture.
0285 * @property {THREE.Texture | null} [metalnessMap] - The metalness map texture.
0286 * @property {THREE.Texture | null} [roughnessMap] - The roughness map texture.
0287 */
0288 type ExtendedMaterialProperties = {
0289 map?: THREE.Texture | null,
0290 lightMap?: THREE.Texture | null,
0291 bumpMap?: THREE.Texture | null,
0292 normalMap?: THREE.Texture | null,
0293 specularMap?: THREE.Texture | null,
0294 envMap?: THREE.Texture | null,
0295 alphaMap?: THREE.Texture | null,
0296 aoMap?: THREE.Texture | null,
0297 displacementMap?: THREE.Texture | null,
0298 emissiveMap?: THREE.Texture | null,
0299 gradientMap?: THREE.Texture | null,
0300 metalnessMap?: THREE.Texture | null,
0301 roughnessMap?: THREE.Texture | null,
0302 };
0303
0304 /**
0305 * Disposes of a THREE.Material and its associated texture maps.
0306 *
0307 * @function disposeMaterial
0308 * @param {any} material - The material to dispose of.
0309 */
0310 function disposeMaterial(material: any): void {
0311 const extMaterial = material as THREE.Material & ExtendedMaterialProperties;
0312
0313 // Dispose of each texture map if it exists
0314 if (material?.map) material.map.dispose();
0315 if (material?.lightMap) material.lightMap.dispose();
0316 if (material?.bumpMap) material.bumpMap.dispose();
0317 if (material?.normalMap) material.normalMap.dispose();
0318 if (material?.specularMap) material.specularMap.dispose();
0319 if (material?.envMap) material.envMap.dispose();
0320 if (material?.alphaMap) material.alphaMap.dispose();
0321 if (material?.aoMap) material.aoMap.dispose();
0322 if (material?.displacementMap) material.displacementMap.dispose();
0323 if (material?.emissiveMap) material.emissiveMap.dispose();
0324 if (material?.gradientMap) material.gradientMap.dispose();
0325 if (material?.metalnessMap) material.metalnessMap.dispose();
0326 if (material?.roughnessMap) material.roughnessMap.dispose();
0327
0328 // Dispose of the material itself
0329 if ('dispose' in material) {
0330 material.dispose();
0331 }
0332 }
0333
0334 /**
0335 * Disposes of a THREE.Object3D node by disposing its geometry and materials.
0336 *
0337 * @function disposeNode
0338 * @param {any} node - The node to dispose of.
0339 */
0340 export function disposeNode(node: any): void {
0341 // Dispose of geometry if it exists
0342 if (node?.geometry) {
0343 node.geometry.dispose();
0344 }
0345
0346 // Dispose of materials if they exist
0347 if (node?.material) {
0348 if (Array.isArray(node.material)) {
0349 node.material.forEach(disposeMaterial);
0350 } else {
0351 disposeMaterial(node.material);
0352 }
0353 }
0354
0355 // Remove the node from its parent
0356 node.removeFromParent();
0357 }
0358
0359 /**
0360 * Disposes of the original meshes after merging geometries.
0361 *
0362 * @function disposeOriginalMeshesAfterMerge
0363 * @param {MergeResult} mergeResult - The result of the geometry merge containing nodes to remove.
0364 */
0365 export function disposeOriginalMeshesAfterMerge(mergeResult: MergeResult): void {
0366 // Iterate through the children to remove in reverse order
0367 for (let i = mergeResult.childrenToRemove.length - 1; i >= 0; i--) {
0368 disposeNode(mergeResult.childrenToRemove[i]);
0369 mergeResult.childrenToRemove[i].removeFromParent();
0370 }
0371 }
0372
0373 /**
0374 * Recursively disposes of a THREE.Object3D hierarchy.
0375 *
0376 * @function disposeHierarchy
0377 * @param {THREE.Object3D} node - The root node of the hierarchy to dispose of.
0378 * @param disposeSelf - disposes this node too (if false - only children and their hierarchies will be disposed)
0379 */
0380 export function disposeHierarchy(node: THREE.Object3D, disposeSelf=true): void {
0381 // Clone the children array and iterate in reverse order
0382 node.children.slice().reverse().forEach(child => {
0383 disposeHierarchy(child);
0384 });
0385
0386 if(disposeSelf) {
0387 disposeNode(node);
0388 }
0389 }
0390
0391 /**
0392 * Recursively removes empty branches from a THREE.Object3D tree.
0393 * An empty branch is a node without geometry and without any non-empty children.
0394 *
0395 * Removing useless nodes that were left without geometries speeds up overall rendering.
0396 *
0397 * @function pruneEmptyNodes
0398 * @param {THREE.Object3D} node - The starting node to prune empty branches from.
0399 */
0400 export function pruneEmptyNodes(node: THREE.Object3D): void {
0401 // Traverse children from last to first to avoid index shifting issues after removal
0402 for (let i = node.children.length - 1; i >= 0; i--) {
0403 pruneEmptyNodes(node.children[i]); // Recursively prune children first
0404 }
0405
0406 // After pruning children, determine if the current node is now empty
0407 if (node.children.length === 0 && !((node as any)?.geometry)) {
0408 node.removeFromParent();
0409 }
0410 }