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 }