From 2f6e8834adaf4cc070d382412fb3f1a073f91601 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Thu, 24 Sep 2026 21:08:35 +0000 Subject: [PATCH] fix: centralizacao geometrica WRL e amaracao de marcas numericas no visualizador 3D --- src/components/viewer3d/ModelViewer3D.tsx | 32 +++++++-- src/lib/3d/wrlLoaderService.ts | 86 +++++++++++++++-------- 2 files changed, 83 insertions(+), 35 deletions(-) diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index 4ca264c..574474b 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -274,26 +274,48 @@ export const ModelViewer3D: React.FC = ({ const targetGroup = group || modelGroupRef.current; if (!targetGroup || !activeCameraRef.current || !controlsRef.current) return; + targetGroup.updateMatrixWorld(true); const box = new THREE.Box3().setFromObject(targetGroup); if (box.isEmpty()) return; const center = box.getCenter(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3()); const maxDim = Math.max(size.x, size.y, size.z); + if (maxDim <= 0) return; const fov = 45 * (Math.PI / 180); - let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 1.5; - cameraZ = Math.max(cameraZ, 20); + let cameraDistance = Math.abs(maxDim / (2 * Math.tan(fov / 2))) * 1.6; + cameraDistance = Math.max(cameraDistance, 5); const camera = activeCameraRef.current; - camera.position.set(center.x + cameraZ * 0.7, center.y + cameraZ * 0.5, center.z + cameraZ * 0.7); - camera.lookAt(center); + if ((camera as THREE.PerspectiveCamera).isPerspectiveCamera) { + const pCam = camera as THREE.PerspectiveCamera; + pCam.near = Math.max(0.01, maxDim / 500); + pCam.far = Math.max(2000, maxDim * 20); + pCam.updateProjectionMatrix(); + pCam.position.set(center.x + cameraDistance * 0.7, center.y + cameraDistance * 0.5, center.z + cameraDistance * 0.7); + pCam.lookAt(center); + } else if ((camera as THREE.OrthographicCamera).isOrthographicCamera) { + const oCam = camera as THREE.OrthographicCamera; + const aspect = oCam.right / oCam.top; + const frustum = maxDim * 1.5; + oCam.left = (frustum * aspect) / -2; + oCam.right = (frustum * aspect) / 2; + oCam.top = frustum / 2; + oCam.bottom = frustum / -2; + oCam.near = Math.max(0.01, maxDim / 500); + oCam.far = Math.max(2000, maxDim * 20); + oCam.position.set(center.x + maxDim, center.y + maxDim * 0.8, center.z + maxDim); + oCam.lookAt(center); + oCam.updateProjectionMatrix(); + } controlsRef.current.target.copy(center); + controlsRef.current.maxDistance = Math.max(1500, maxDim * 10); controlsRef.current.update(); if (gridHelperRef.current) { - gridHelperRef.current.position.y = box.min.y; + gridHelperRef.current.position.set(center.x, box.min.y, center.z); } }, []); diff --git a/src/lib/3d/wrlLoaderService.ts b/src/lib/3d/wrlLoaderService.ts index ed0c63d..2bbb49e 100644 --- a/src/lib/3d/wrlLoaderService.ts +++ b/src/lib/3d/wrlLoaderService.ts @@ -26,8 +26,6 @@ async function decompressGzipIfNeeded(buffer: ArrayBuffer): Promise { /** * Extrai a Fase da estrutura a partir dos ancestrais na árvore 3D (ex: "00001_COLUNAS", "00004_GUARDA_CORPO"). - * Garante que apenas o nó de primeiro nível (raiz da cena) ou nós explicitamente marcados como FASE/ETAPA sejam considerados. - * Evita rigorosamente que marcas de montagem (ex: "106", "121") ou dimensões (ex: "10x57", "1127") sejam confundidas com fases. */ function extractPhaseFromAncestors(ancestors: string[]): string | undefined { if (!ancestors || ancestors.length === 0) return undefined; @@ -35,7 +33,7 @@ function extractPhaseFromAncestors(ancestors: string[]): string | undefined { // 1. O nó de primeiro nível (ancestors[0]) representa a pasta raiz no WRL (ex: "00001_COLUNAS", "00004_GUARDA_CORPO") const rootNode = ancestors[0].replace(/^DEF\s+/i, '').replace(/^ID_/, '').trim(); - // Padrão Tekla / Bocad com prefixo numérico seguido de nome de grupo (ex: "00001_COLUNAS", "00004_GUARDA_CORPO") + // Padrão Tekla / Bocad com prefixo numérico seguido de nome de grupo (ex: "00001_COLUNAS", "00002_VIGAS") const mZeros = rootNode.match(/^(?:0*([1-9]\d*))[-_ ]+([A-Za-zÀ-ÿ]+.*)$/i); if (mZeros) { return String(Number(mZeros[1])); // "1", "2", "3", "4" @@ -47,7 +45,7 @@ function extractPhaseFromAncestors(ancestors: string[]): string | undefined { return String(Number(mRootFase[1])); } - // 2. Se o primeiro nó não for fase, pesquisa nos ancestrais intermediários APENAS se contiver palavra-chave explícita FASE ou ETAPA + // 2. Pesquisa nos ancestrais intermediários APENAS se contiver palavra-chave explícita FASE ou ETAPA for (let i = 1; i < ancestors.length; i++) { const clean = ancestors[i].replace(/^DEF\s+/i, '').replace(/^ID_/, '').trim(); const mExplicit = clean.match(/^(?:fase|etapa|phase|stage)[-_ ]*0*([1-9]\d*)/i); @@ -60,8 +58,7 @@ function extractPhaseFromAncestors(ancestors: string[]): string | undefined { } /** - * Analisa e normaliza a marca da peça, marca do conjunto e tipo estrutural, - * levando em conta a hierarquia de pastas (ex: Fase -> Conjunto '121' -> Componente '121_1127_TUBO...'). + * Analisa e normaliza a marca da peça, marca do conjunto e tipo estrutural com suporte completo a Tekla / Bocad. */ function parseWrlNodeName( rawName: string, @@ -84,22 +81,34 @@ function parseWrlNodeName( let pieceMark = name; let profile = ''; - // Formato padrão Tekla/Bocad: [Conjunto]_[Peca]_[Perfil]_[Instancia] - // Ex: "121_1127_TUBO44x45X3_121293" -> assembly="121", piece="1127", profile="TUBO44x45X3" const parts = name.split('_'); + // CASO 1: Tekla composto com 2 números no início: [Conjunto]_[Peca]_[Perfil] (ex: "121_1127_TUBO44x45X3_121293") if (parts.length >= 3 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) { assemblyMark = parts[0]; pieceMark = parts[1]; profile = parts[2]; - } else if (parts.length >= 2 && /^\d+$/.test(parts[0])) { + } + // CASO 2: Tekla padrão [MarcaPeca]_[Perfil] (ex: "5_W360X44_0", "6_W360X32_9", "1_W360X44_0", "6_VIGA") + else if (parts.length >= 2 && /^\d+$/.test(parts[0])) { + // Se o primeiro segmento é numérico e o segundo não é outro número (é perfil ou descrição), + // a marca real da peça é o primeiro segmento (ex: "5", "6", "1")! + pieceMark = parts[0]; assemblyMark = parts[0]; - pieceMark = parts[1]; - if (parts.length > 2) profile = parts.slice(2).join('_'); - } else if (/^(?:PARAF|BOLT)/i.test(name)) { + profile = parts.slice(1).join('_'); + } + // CASO 3: Prefixo de conjunto alfanumérico (ex: "V101_1_CHAPA", "P1_2_CANTON") + else if (parts.length >= 2 && /^[A-Za-z]+\d+$/i.test(parts[0])) { + assemblyMark = parts[0]; + pieceMark = parts[0]; // ou parts[1] + profile = parts.slice(1).join('_'); + } + // CASO 4: Parafusos / Fixadores + else if (/^(?:PARAF|BOLT)/i.test(name)) { pieceMark = parts[0]; profile = 'PARAFUSO'; - } else { + } + else { // Remoção de prefixos comuns como "Part_", "Piece_", "Peca_", "Elem_" pieceMark = pieceMark.replace(/^(?:Part|Piece|Peca|Elem|Item|Member|Profile|Mesh)[-_]/i, ''); } @@ -111,7 +120,7 @@ function parseWrlNodeName( // Detecção de tipo de elemento estrutural let detectedType = 'ELEMENT'; - const searchStr = `${profile} ${name} ${cleanMark}`.toUpperCase(); + const searchStr = `${profile} ${name} ${cleanMark} ${cleanAssemblyMark}`.toUpperCase(); if (searchStr.includes('VIGA') || searchStr.includes('BEAM') || cleanMark.startsWith('V')) { detectedType = 'BEAM'; @@ -146,9 +155,7 @@ function parseWrlNodeName( } /** - * Sanitiza o texto VRML para compatibilidade com o parser do Three.js: - * 1. Ajusta cabeçalho para #VRML V2.0 utf8. - * 2. Converte identificadores que iniciam com dígitos ou contêm hífens para identificadores válidos. + * Sanitiza o texto VRML para máxima compatibilidade com o parser do Three.js */ function sanitizeVrmlForLoader(rawText: string): { sanitized: string; @@ -181,8 +188,6 @@ function sanitizeVrmlForLoader(rawText: string): { /** * Parser de Fallback Direto e Resiliente para arquivos VRML (.wrl 1.0 ou 2.0). - * Extrai blocos IndexedFaceSet com Coordinate/Coordinate3 diretamente via expressões regulares, - * garantindo renderização mesmo quando o parser Chevrotain do VRMLLoader encontra erros de sintaxe. */ function parseVrmlDirectFallback( vrmlText: string, @@ -437,14 +442,13 @@ export async function loadAndAuditWRL( p = p.parent; } - // 1. Extração rigorosa da Fase a partir dos nós ancestrais (evita falsos positivos em nomes de peças) + // 1. Extração rigorosa da Fase a partir dos nós ancestrais const inheritedPhase = extractPhaseFromAncestors(ancestors); // 2. Extração do conjunto pai se houver na árvore (ex: pasta '121') let ancestorAssembly: string | undefined = undefined; for (const anc of ancestors) { const cleanAnc = anc.replace(/^DEF\s+/i, '').replace(/^ID_/, '').trim(); - // Se for um identificador de conjunto (ex: "121", "106", "V101") que não seja a fase if (/^\d+$/.test(cleanAnc) && cleanAnc !== inheritedPhase) { ancestorAssembly = cleanAnc; break; @@ -464,6 +468,9 @@ export async function loadAndAuditWRL( if (parsed.cleanMark && isValidMark(parsed.cleanMark)) { marksFound.add(parsed.cleanMark); } + if (parsed.cleanAssemblyMark && isValidMark(parsed.cleanAssemblyMark)) { + marksFound.add(parsed.cleanAssemblyMark); + } // Contabiliza tipos if (parsed.detectedType === 'BEAM') totalBeams++; @@ -498,7 +505,7 @@ export async function loadAndAuditWRL( mesh, }; - // Injeta metadados essenciais no userData do Mesh (exatamente o que o ModelViewer3D espera) + // Injeta metadados essenciais no userData do Mesh mesh.userData = { expressID, guid, @@ -522,21 +529,40 @@ export async function loadAndAuditWRL( list.push(pieceInfo); piecesByMark.set(markKey, list); } + if (parsed.cleanAssemblyMark && parsed.cleanAssemblyMark !== markKey) { + const listAss = piecesByMark.get(parsed.cleanAssemblyMark) || []; + listAss.push(pieceInfo); + piecesByMark.set(parsed.cleanAssemblyMark, listAss); + } } }); // Adiciona a cena VRML ao grupo final sceneGroup.add(vrmlScene); - // Normaliza dimensões e orientação: se o modelo estiver em milímetros (típico CAD metálico > 1000 unidades), converte para metros - const bbox = new THREE.Box3().setFromObject(sceneGroup); - const size = bbox.getSize(new THREE.Vector3()); - const maxDim = Math.max(size.x, size.y, size.z); + // ------------------------------------------------------------- + // CENTRALIZAÇÃO ABSOLUTA E NORMALIZAÇÃO DE ESCALA DO MODELO + // ------------------------------------------------------------- + // Garante que o centro do modelo fique exatamente em (0, 0, 0) local + // para que qualquer rotação (ex: 270° no eixo X) ocorra em torno do centro da estrutura + // e não lance o modelo para fora da visualização da câmera. + const rawBox = new THREE.Box3().setFromObject(vrmlScene); + if (!rawBox.isEmpty()) { + const rawCenter = rawBox.getCenter(new THREE.Vector3()); + const rawSize = rawBox.getSize(new THREE.Vector3()); - // Se o modelo estiver em milímetros (ex: 20 metros = 20000mm) - if (maxDim > 500) { - const scaleFactor = 0.001; - sceneGroup.scale.set(scaleFactor, scaleFactor, scaleFactor); + // Desloca o vrmlScene interno para centrar em (0,0,0) + vrmlScene.position.x -= rawCenter.x; + vrmlScene.position.y -= rawCenter.y; + vrmlScene.position.z -= rawCenter.z; + vrmlScene.updateMatrixWorld(true); + + // Se as dimensões forem em milímetros (> 500 unidades), converte para metros (escala 0.001) + const maxDim = Math.max(rawSize.x, rawSize.y, rawSize.z); + if (maxDim > 500) { + const scaleFactor = 0.001; + sceneGroup.scale.set(scaleFactor, scaleFactor, scaleFactor); + } sceneGroup.updateMatrixWorld(true); }