Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/firebird-ng/src/app/utils/three-geometry-editor.ts is written in an unsupported language. File is not indexed.

0001 import {Color, Material, Mesh, Object3D} from "three";
0002 import {createOutline, disposeOriginalMeshesAfterMerge, findObject3DNodes, pruneEmptyNodes} from "./three.utils";
0003 import {mergeBranchGeometries, mergeMeshList, MergeResult} from "./three-geometry-merge";
0004 import * as THREE from "three";
0005 import {ColorRepresentation} from "three";
0006 import {SimplifyModifier} from "three/examples/jsm/modifiers/SimplifyModifier.js";
0007 
0008 /**
0009  * Flag name used to mark objects that have already been processed by geometry editing rules.
0010  * When set to true, subsequent rules without patterns ("the rest") will skip this object.
0011  */
0012 export const GEOMETRY_EDITING_SKIP_FLAG = 'geometryEditingSkipRules';
0013 
0014 /**
0015  * Clears the geometryEditingSkipRules flag on all nodes in the tree.
0016  * Should be called at the start of processing a new ruleset for a detector.
0017  */
0018 export function clearGeometryEditingFlags(root: Object3D): void {
0019   root.traverse((child) => {
0020     if (child.userData && child.userData[GEOMETRY_EDITING_SKIP_FLAG] !== undefined) {
0021       delete child.userData[GEOMETRY_EDITING_SKIP_FLAG];
0022     }
0023   });
0024 }
0025 
0026 /**
0027  * Marks an object as processed by geometry editing rules.
0028  */
0029 export function markAsProcessed(obj: Object3D): void {
0030   if (!obj.userData) {
0031     obj.userData = {};
0032   }
0033   obj.userData[GEOMETRY_EDITING_SKIP_FLAG] = true;
0034 }
0035 
0036 /**
0037  * Checks if an object has been marked as already processed.
0038  */
0039 export function isAlreadyProcessed(obj: Object3D): boolean {
0040   return obj.userData?.[GEOMETRY_EDITING_SKIP_FLAG] === true;
0041 }
0042 
0043 /**
0044  * Checks if an object or any of its ancestors has been marked as processed.
0045  * This implements hierarchical skipping - if a parent branch was processed,
0046  * all descendants should be skipped too.
0047  */
0048 export function isInProcessedBranch(obj: Object3D): boolean {
0049   let current: Object3D | null = obj;
0050   while (current) {
0051     if (current.userData?.[GEOMETRY_EDITING_SKIP_FLAG] === true) {
0052       return true;
0053     }
0054     current = current.parent;
0055   }
0056   return false;
0057 }
0058 
0059 export enum EditThreeNodeActions {
0060 
0061   Merge,   /** Merge children matching patterns (if patterns are provided) or all meshes of the node*/
0062 
0063 }
0064 
0065 export interface EditThreeNodeRule {
0066 
0067   patterns?: string[] | string;
0068   merge?: boolean;
0069   newName?: string;
0070   deleteOrigins?: boolean;
0071   cleanupNodes?: boolean;
0072   outline?: boolean;
0073   outlineThresholdAngle?: number;
0074   simplifyMeshes?: boolean;
0075   simplifyRatio?: number;
0076 
0077   /**
0078    * When true and merge=false, if a pattern matches a node, all descendant meshes
0079    * of that node will also be included (styled the same way).
0080    * Defaults to true when merge=false and patterns are provided.
0081    */
0082   applyToDescendants?: boolean;
0083 
0084   /** [degrees] */
0085   outlineColor?: ColorRepresentation;
0086   material?: Material;
0087   color?: ColorRepresentation;
0088 
0089 }
0090 
0091 function simplifyMeshTree(object: THREE.Object3D, simplifyRatio = 0.5): void {
0092   const simplifier = new SimplifyModifier();
0093   const minVerts = 10;
0094 
0095   object.traverse((child: THREE.Object3D) => {
0096 
0097     // Type coercions and type validations
0098     if (!(child as THREE.Mesh).isMesh) {
0099       return
0100     }
0101     const mesh = child as THREE.Mesh;
0102 
0103     if(!(mesh.geometry as THREE.BufferGeometry).isBufferGeometry) {
0104       return;
0105     }
0106     const geom = mesh.geometry as THREE.BufferGeometry;
0107 
0108     if (!geom.attributes['position']) {
0109       return;
0110     }
0111 
0112     // Do we need to convert looking at the number of vertices?
0113     const verticeCount  = geom.attributes['position'].count;
0114     const targetVerticeCount = Math.floor(verticeCount  * simplifyRatio);
0115 
0116     if (verticeCount < minVerts) {
0117       console.log(`[SimplifyMeshTree] Mesh "${mesh.name || '(unnamed)'}": skipped (too small, vertices=${verticeCount })`);
0118       return;
0119     }
0120 
0121     if (verticeCount < targetVerticeCount) {
0122       console.log(`[SimplifyMeshTree] Mesh "${mesh.name || '(unnamed)'}": skipped (too small targetVerticeCount, targetVerticeCount=${targetVerticeCount})`);
0123       return;
0124     }
0125 
0126 
0127     // Actual simplification
0128     const timeStart = performance.now();
0129     console.log(`[SimplifyMeshTree] Processing "${mesh.name || '(unnamed)'}": vertices before=${verticeCount}, after=${targetVerticeCount}`);
0130 
0131     mesh.geometry = simplifier.modify(geom, targetVerticeCount);
0132 
0133     // Recompute bounding limits
0134     mesh.geometry.computeBoundingBox();
0135     mesh.geometry.computeBoundingSphere();
0136     mesh.geometry.computeVertexNormals();
0137 
0138     // Make sure positions and normals will be updated
0139     mesh.geometry.attributes["position"]["needsUpdate"] = true;
0140     if (mesh.geometry.attributes["normal"]) {
0141       mesh.geometry.attributes["normal"]["needsUpdate"] = true;
0142     }
0143 
0144     const timeEnd = performance.now()
0145     if (timeEnd - timeStart > 500) {
0146       console.warn(`[SimplifyMeshTree] Warn: mesh "${mesh.name || '(unnamed)'}" took ${Math.round(timeEnd-timeStart)}ms to simplify.`);
0147     }
0148   });
0149 }
0150 
0151 function mergeWhatever(node: Object3D, rule: EditThreeNodeRule): MergeResult | undefined {
0152 
0153   let newName = !rule.newName ? node.name + "_merged" : rule.newName;
0154 
0155   if (!rule.patterns) {
0156     // If user provided patterns only children matching patterns (search goes over whole branch) will be merged,
0157     // But if no patterns given, we will merge whole node
0158     return mergeBranchGeometries(node, newName, rule.material);  // Children auto removed
0159   }
0160 
0161   // If we are here, we need to collect what to merge first
0162   // Use a Set to avoid duplicates
0163   const meshSet = new Set<Mesh>();
0164 
0165   let patterns = rule.patterns;
0166   if (typeof patterns === "string") {
0167     patterns = [patterns];
0168   }
0169 
0170   for (const pattern of patterns) {
0171     // Find any nodes matching the pattern (not just Meshes)
0172     // This allows patterns to match Groups that contain meshes
0173     const matchedNodes = findObject3DNodes(node, pattern, "").nodes;
0174 
0175     for (const matchedNode of matchedNodes) {
0176       // Collect all descendant meshes from each matched node
0177       matchedNode.traverse((child: Object3D) => {
0178         if ((child as Mesh).isMesh && (child as Mesh).geometry) {
0179           meshSet.add(child as Mesh);
0180         }
0181       });
0182     }
0183   }
0184 
0185   const mergeSubjects = Array.from(meshSet);
0186   let result = mergeMeshList(mergeSubjects, node, newName, rule.material);
0187   const deleteOrigins = rule?.deleteOrigins ?? true;
0188   if (result && deleteOrigins) {
0189     disposeOriginalMeshesAfterMerge(result);
0190   }
0191   return result;
0192 }
0193 
0194 
0195 
0196 export function editThreeNodeContent(node: Object3D, rule: EditThreeNodeRule) {
0197   let {
0198     patterns,
0199     deleteOrigins = true,
0200     cleanupNodes = true,
0201     outline = true,
0202     outlineThresholdAngle = 40,
0203     outlineColor,
0204     simplifyMeshes = false,
0205     simplifyRatio = 0.7,
0206     material,
0207     color,
0208     merge = true,
0209     newName = "",
0210     applyToDescendants
0211   } = rule;
0212 
0213   // Default applyToDescendants to true when merge=false and patterns are provided
0214   if (applyToDescendants === undefined) {
0215     applyToDescendants = !merge && !!patterns;
0216   }
0217 
0218   let targetMeshes: Mesh[] = [];
0219   // Track nodes to mark as processed (for hierarchical skip)
0220   const nodesToMarkProcessed: Object3D[] = [];
0221 
0222   if (merge) {
0223     // Existing merge logic
0224     let result = mergeWhatever(node, rule);
0225     if (!result) {
0226       console.warn("didn't find children to merge. Patterns:");
0227       console.log(patterns)
0228       return;
0229     }
0230     targetMeshes = [result.mergedMesh];
0231   } else {
0232     // New logic for when merge is false
0233     // Find all meshes that match the patterns, similar to mergeWhatever
0234     if (!patterns) {
0235       // If no patterns given, collect all meshes with geometry in the node
0236       // Skip meshes that are in a processed branch (hierarchical skip)
0237       // This means if a parent was processed, all descendants are skipped too
0238       node.traverse((child) => {
0239         if ((child as any)?.geometry && !isInProcessedBranch(child)) {
0240           targetMeshes.push(child as Mesh);
0241         }
0242       });
0243     } else {
0244       // If patterns are given, find all meshes/nodes that match
0245       if (typeof patterns === "string") {
0246         patterns = [patterns];
0247       }
0248 
0249       // Use a Set to avoid duplicates when multiple patterns match the same mesh
0250       const meshSet = new Set<Mesh>();
0251 
0252       for (const pattern of patterns) {
0253         // Find all nodes (not just meshes) matching the pattern
0254         const found = findObject3DNodes(node, pattern, "").nodes;
0255 
0256         for (const matchedNode of found) {
0257           // Skip if this node or any ancestor is already processed
0258           // This handles cases where parent and child both match the pattern
0259           if (isInProcessedBranch(matchedNode)) {
0260             continue;
0261           }
0262 
0263           // Track this node for hierarchical skip
0264           nodesToMarkProcessed.push(matchedNode);
0265 
0266           if (applyToDescendants) {
0267             // Collect this node and all descendant meshes
0268             matchedNode.traverse((child: Object3D) => {
0269               if ((child as any)?.geometry && !meshSet.has(child as Mesh)) {
0270                 meshSet.add(child as Mesh);
0271               }
0272             });
0273           } else {
0274             // Only add if it's a mesh itself
0275             if ((matchedNode as any)?.geometry) {
0276               meshSet.add(matchedNode as Mesh);
0277             }
0278           }
0279 
0280           // Mark the matched node AFTER collecting meshes
0281           // This prevents child nodes from being re-processed if they also match the pattern
0282           markAsProcessed(matchedNode);
0283         }
0284       }
0285       targetMeshes = Array.from(meshSet);
0286     }
0287   }
0288 
0289   // Apply operations to each target mesh
0290   for (const targetMesh of targetMeshes) {
0291 
0292     // Change color
0293     if (color !== undefined && color !== null) {
0294       const mat = targetMesh.material as any;
0295       if (mat) {
0296         if (mat.color) {
0297           // Use setHex for more reliable color updates
0298           mat.color.setHex(color);
0299         } else {
0300           mat.color = new Color(color);
0301         }
0302         mat.needsUpdate = true;
0303       }
0304     }
0305 
0306     // Change material
0307     if (material !== undefined && material !== null) {
0308        targetMesh.material = material;
0309     }
0310 
0311     if (simplifyMeshes) {
0312       simplifyMeshTree(targetMesh, simplifyRatio);
0313     }
0314 
0315     if (outline) {
0316       createOutline(targetMesh, {color: outlineColor, thresholdAngle: outlineThresholdAngle, markAsProcessed: true});
0317     }
0318 
0319     // Mark as processed (may already be marked for pattern-based rules, but needed for merge and no-pattern cases)
0320     markAsProcessed(targetMesh);
0321   }
0322 
0323   if (cleanupNodes) {
0324     pruneEmptyNodes(node);
0325   }
0326 }