diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index 6495097..e464c3b 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -18,6 +18,8 @@ interface ProductionPieceStatus { currentProcessName?: string; processColor?: string; processOrdem?: number; + perfilPrincipal?: string; + descricao?: string; } /** @@ -67,6 +69,42 @@ function findProductionDataForPiece( return undefined; } +/** + * Helper para formatar a Marca Principal da Peça no padrão de Engenharia (OF-Fase-Marca, ex: B138-2-1 / B135-2-8) + * e obter o Perfil/Bitola oficial a partir do banco de dados ou do modelo 3D. + */ +function resolvePieceEngineeringDetails( + rawPieceMark: string, + rawSection: string | undefined, + phase: string | undefined, + singlePartMark: string | undefined, + prod: ProductionPieceStatus | undefined, + selectedOF: string | undefined +): { + displayMark: string; + displaySection: string; +} { + const baseMark = prod?.marca || rawPieceMark || 'Peça'; + let displayMark = baseMark; + + // Se tivermos OF e Fase e a marca for pura (ex: "1" ou "8"), formata como "B138-2-1" idêntico ao Print 2 "B135-2-8" + if (selectedOF && phase && !baseMark.toUpperCase().startsWith(selectedOF.toUpperCase())) { + displayMark = `${selectedOF}-${phase}-${baseMark}`; + } + + // Perfil / Bitola oficial: prioriza banco de produção (TS_ERP.pecas.perfil_principal), seguido de section limpo do modelo + let displaySection = prod?.perfilPrincipal || ''; + if (!displaySection || displaySection === '1001' || displaySection === 'Perfil') { + if (rawSection && rawSection !== '1001' && rawSection !== 'Perfil') { + displaySection = rawSection; + } else { + displaySection = prod?.descricao || 'Perfil'; + } + } + + return { displayMark, displaySection }; +} + interface ModelViewer3DProps { modelData: LoadedIFCResult | null; productionData?: Map; @@ -940,17 +978,40 @@ export const ModelViewer3D: React.FC = ({ const pmark = hitMesh.userData.pieceMark; if (modelData && expressID !== undefined) { - const piece = modelData.pieceByExpressID.get(expressID) || { + const rawPiece = modelData.pieceByExpressID.get(expressID); + const rawMark = hitMesh.userData.pieceMark || rawPiece?.pieceMark || ''; + const phase = hitMesh.userData.phase || rawPiece?.phase; + const singlePartMark = hitMesh.userData.singlePartMark || rawPiece?.singlePartMark; + + const prod = findProductionDataForPiece(productionData, { + pieceMark: rawMark, + cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark, + assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark, + cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark, + phase, + }, selectedOF); + + const { displayMark, displaySection } = resolvePieceEngineeringDetails( + rawMark, + hitMesh.userData.section || rawPiece?.section, + phase, + singlePartMark, + prod, + selectedOF + ); + + const piece: PieceInfo = { expressID, - guid: hitMesh.userData.guid || '', - name: hitMesh.userData.name || hitMesh.userData.section || 'Peça', - type: hitMesh.userData.type || 'PIECE', - pieceMark: pmark, - cleanMark: hitMesh.userData.cleanMark, - assemblyMark: hitMesh.userData.assemblyMark, - cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark, - section: hitMesh.userData.section || hitMesh.userData.name, - phase: hitMesh.userData.phase, + guid: hitMesh.userData.guid || rawPiece?.guid || '', + name: hitMesh.userData.name || rawPiece?.name || 'Peça', + type: hitMesh.userData.type || rawPiece?.type || 'PIECE', + pieceMark: displayMark, + cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark, + assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark, + cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark, + section: displaySection, + phase, + singlePartMark, }; setHoveredPiece(piece); setHoverPosition({ x: clientX, y: clientY }); @@ -1040,18 +1101,40 @@ export const ModelViewer3D: React.FC = ({ }); } - const pmark = hitMesh.userData.pieceMark; - const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(targetIfcId)) || { + const rawPiece = modelData?.pieceByExpressID.get(targetIfcId); + const rawMark = hitMesh.userData.pieceMark || rawPiece?.pieceMark || ''; + const phase = hitMesh.userData.phase || rawPiece?.phase; + const singlePartMark = hitMesh.userData.singlePartMark || rawPiece?.singlePartMark; + + const prod = findProductionDataForPiece(productionData, { + pieceMark: rawMark, + cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark, + assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark, + cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark, + phase, + }, selectedOF); + + const { displayMark, displaySection } = resolvePieceEngineeringDetails( + rawMark, + hitMesh.userData.section || rawPiece?.section, + phase, + singlePartMark, + prod, + selectedOF + ); + + const piece: PieceInfo = { expressID: targetIfcId, - guid: hitMesh.userData.guid || '', - name: hitMesh.userData.name || hitMesh.userData.section || 'Peça', - type: hitMesh.userData.type || 'PIECE', - pieceMark: pmark, - cleanMark: hitMesh.userData.cleanMark, - assemblyMark: hitMesh.userData.assemblyMark, - cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark, - section: hitMesh.userData.section, - phase: hitMesh.userData.phase, + guid: hitMesh.userData.guid || rawPiece?.guid || '', + name: hitMesh.userData.name || rawPiece?.name || 'Peça', + type: hitMesh.userData.type || rawPiece?.type || 'PIECE', + pieceMark: displayMark, + cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark, + assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark, + cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark, + section: displaySection, + phase, + singlePartMark, }; nextMap.set(targetIfcId, { @@ -1165,17 +1248,40 @@ export const ModelViewer3D: React.FC = ({ m.material = magentaMaterialRef.current; }); - const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(elId)) || { + const rawPiece = modelData?.pieceByExpressID.get(elId); + const rawMark = sampleMesh.userData.pieceMark || rawPiece?.pieceMark || ''; + const phase = sampleMesh.userData.phase || rawPiece?.phase || targetPhase; + const singlePartMark = sampleMesh.userData.singlePartMark || rawPiece?.singlePartMark; + + const prod = findProductionDataForPiece(productionData, { + pieceMark: rawMark, + cleanMark: sampleMesh.userData.cleanMark || rawPiece?.cleanMark, + assemblyMark: sampleMesh.userData.assemblyMark || rawPiece?.assemblyMark, + cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark, + phase, + }, selectedOF); + + const { displayMark, displaySection } = resolvePieceEngineeringDetails( + rawMark, + sampleMesh.userData.section || rawPiece?.section, + phase, + singlePartMark, + prod, + selectedOF + ); + + const piece: PieceInfo = { expressID: elId, - guid: sampleMesh.userData.guid || '', - name: sampleMesh.userData.name || sampleMesh.userData.section || 'Peça', - type: sampleMesh.userData.type || 'PIECE', - pieceMark: sampleMesh.userData.pieceMark, - cleanMark: sampleMesh.userData.cleanMark, - assemblyMark: sampleMesh.userData.assemblyMark, - cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark, - section: sampleMesh.userData.section, - phase: sampleMesh.userData.phase || targetPhase, + guid: sampleMesh.userData.guid || rawPiece?.guid || '', + name: sampleMesh.userData.name || rawPiece?.name || 'Peça', + type: sampleMesh.userData.type || rawPiece?.type || 'PIECE', + pieceMark: displayMark, + cleanMark: sampleMesh.userData.cleanMark || rawPiece?.cleanMark, + assemblyMark: sampleMesh.userData.assemblyMark || rawPiece?.assemblyMark, + cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark, + section: displaySection, + phase, + singlePartMark, }; nextMap.set(elId, { diff --git a/src/components/viewer3d/PieceHoverTooltip.tsx b/src/components/viewer3d/PieceHoverTooltip.tsx index 4c2cd31..98f41d3 100644 --- a/src/components/viewer3d/PieceHoverTooltip.tsx +++ b/src/components/viewer3d/PieceHoverTooltip.tsx @@ -62,6 +62,15 @@ export const PieceHoverTooltip: React.FC = ({ )} + {piece.singlePartMark && piece.singlePartMark !== piece.pieceMark && ( +
+ Sub-peça / Corte: + + Pos. {piece.singlePartMark} + +
+ )} + {piece.childrenIDs && piece.childrenIDs.length > 0 && (
Componentes: diff --git a/src/lib/3d/wrlLoaderService.ts b/src/lib/3d/wrlLoaderService.ts index 3a6ebe9..a997a53 100644 --- a/src/lib/3d/wrlLoaderService.ts +++ b/src/lib/3d/wrlLoaderService.ts @@ -1,6 +1,6 @@ import * as THREE from 'three'; import { VRMLLoader } from 'three-stdlib'; -import { PieceInfo, LoadedIFCResult, IFCQualityAudit, getColorForMaterialName, isValidMark } from '@/lib/ifc/ifcLoaderService'; +import { PieceInfo, LoadedIFCResult, IFCQualityAudit, getColorForMaterialName, isValidMark, isSinglePartCutNumber } from '@/lib/ifc/ifcLoaderService'; /** * Função utilitária para descompactar arquivos gzip (como .wrz ou .wrl comprimidos) no navegador. @@ -57,6 +57,9 @@ function extractPhaseFromAncestors(ancestors: string[]): string | undefined { return undefined; } +/** + * Analisa e normaliza a marca da peça, marca do conjunto e tipo estrutural com suporte completo a Tekla / Bocad. + */ /** * Analisa e normaliza a marca da peça, marca do conjunto e tipo estrutural com suporte completo a Tekla / Bocad. */ @@ -65,10 +68,15 @@ function parseWrlNodeName( inheritedPhase?: string, ancestorAssembly?: string ): { + mainMark: string; + cleanMainMark: string; pieceMark: string; cleanMark: string; assemblyMark?: string; cleanAssemblyMark?: string; + singlePartMark?: string; + profile?: string; + section?: string; phase?: string; detectedType: string; } { @@ -79,20 +87,22 @@ function parseWrlNodeName( let assemblyMark = ancestorAssembly ? ancestorAssembly.trim() : undefined; let pieceMark = name; + let singlePartMark: string | undefined = undefined; let profile = ''; const parts = name.split('_'); - // CASO 1: Tekla composto com 2 números no início: [Conjunto]_[Peca]_[Perfil] (ex: "121_1127_TUBO44x45X3_121293") + // CASO 1: Tekla composto com 2 números no início: [Conjunto]_[Peca]_[Perfil] (ex: "1_1001_W360X44_0", "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]; + singlePartMark = parts[1]; + let rest = parts.slice(2).join('_'); + rest = rest.replace(/_\d{5,}$/, ''); + profile = rest; } // 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]; profile = parts.slice(1).join('_'); @@ -100,7 +110,7 @@ function parseWrlNodeName( // 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] + pieceMark = parts[0]; profile = parts.slice(1).join('_'); } // CASO 4: Parafusos / Fixadores @@ -113,18 +123,32 @@ function parseWrlNodeName( pieceMark = pieceMark.replace(/^(?:Part|Piece|Peca|Elem|Item|Member|Profile|Mesh)[-_]/i, ''); } + // Normalização do Perfil / Bitola (ex: W360X44_0 -> W360X44.0, W360X32_9 -> W360X32.9) + if (profile) { + profile = profile.replace(/_(\d+)$/, '.$1').replace(/_/g, ' ').trim(); + } + let cleanMark = pieceMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase(); if (!cleanMark) cleanMark = pieceMark.toUpperCase(); const cleanAssemblyMark = assemblyMark ? assemblyMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase() : cleanMark; + // Em estruturas metálicas, a marca da peça apontada na produção é a marca do conjunto (Assembly Mark). + // Se a sub-peça for um número de corte (>= 1000) e houver conjunto pai, a marca principal é o conjunto! + let mainMark = pieceMark; + let cleanMainMark = cleanMark; + if (assemblyMark && (isSinglePartCutNumber(pieceMark) || !mainMark || mainMark === 'indefinido')) { + mainMark = assemblyMark; + cleanMainMark = cleanAssemblyMark; + } + // Detecção de tipo de elemento estrutural let detectedType = 'ELEMENT'; - const searchStr = `${profile} ${name} ${cleanMark} ${cleanAssemblyMark}`.toUpperCase(); + const searchStr = `${profile} ${name} ${cleanMark} ${cleanAssemblyMark} ${mainMark}`.toUpperCase(); - if (searchStr.includes('VIGA') || searchStr.includes('BEAM') || cleanMark.startsWith('V')) { + if (searchStr.includes('VIGA') || searchStr.includes('BEAM') || cleanMainMark.startsWith('V')) { detectedType = 'BEAM'; - } else if (searchStr.includes('COLUNA') || searchStr.includes('PILAR') || searchStr.includes('COLUMN') || cleanMark.startsWith('P')) { + } else if (searchStr.includes('COLUNA') || searchStr.includes('PILAR') || searchStr.includes('COLUMN') || cleanMainMark.startsWith('P')) { detectedType = 'COLUMN'; } else if (searchStr.includes('CHAPA') || searchStr.includes('PLATE') || cleanMark.startsWith('PL') || cleanMark.startsWith('CH')) { detectedType = 'PLATE'; @@ -145,10 +169,15 @@ function parseWrlNodeName( } return { + mainMark: mainMark.toUpperCase(), + cleanMainMark: cleanMainMark.toUpperCase(), pieceMark: pieceMark.toUpperCase(), cleanMark, assemblyMark: assemblyMark ? assemblyMark.toUpperCase() : undefined, cleanAssemblyMark, + singlePartMark, + profile: profile || undefined, + section: profile || undefined, phase: inheritedPhase, detectedType, }; @@ -503,19 +532,22 @@ export async function loadAndAuditWRL( } mesh.material = material; - const pieceSection = parsed.cleanMark || parsed.pieceMark || 'Perfil'; + const pieceSection = parsed.profile || parsed.section || parsed.cleanMainMark || 'Perfil'; + const mainMark = parsed.mainMark || parsed.cleanMainMark || parsed.pieceMark; + const cleanMain = parsed.cleanMainMark || parsed.cleanMark; const pieceInfo: PieceInfo = { expressID, guid, name: rawNodeName, type: parsed.detectedType, - pieceMark: parsed.pieceMark, - cleanMark: parsed.cleanMark, - assemblyMark: parsed.assemblyMark, - cleanAssemblyMark: parsed.cleanAssemblyMark, + pieceMark: mainMark, + cleanMark: cleanMain, + assemblyMark: parsed.assemblyMark || mainMark, + cleanAssemblyMark: parsed.cleanAssemblyMark || cleanMain, section: pieceSection, phase: parsed.phase, + singlePartMark: parsed.singlePartMark, mesh, }; @@ -524,10 +556,11 @@ export async function loadAndAuditWRL( ifcId: expressID, // Identificador primordial compatível com Raycaster, Hover e Seleção do visualizador expressID, guid, - pieceMark: parsed.pieceMark, - cleanMark: parsed.cleanMark, - assemblyMark: parsed.assemblyMark, - cleanAssemblyMark: parsed.cleanAssemblyMark, + pieceMark: mainMark, // Peça principal da produção (ex: 1, 8, V101) + cleanMark: cleanMain, + singlePartMark: parsed.singlePartMark, // Componente de corte (ex: 1001) + assemblyMark: parsed.assemblyMark || mainMark, + cleanAssemblyMark: parsed.cleanAssemblyMark || cleanMain, section: pieceSection, phase: parsed.phase, type: parsed.detectedType, @@ -538,17 +571,12 @@ export async function loadAndAuditWRL( pieceByExpressID.set(expressID, pieceInfo); - // Agrupa peças por marca - const markKey = parsed.cleanMark || parsed.pieceMark; - if (markKey) { - const list = piecesByMark.get(markKey) || []; + // Agrupa peças por marca de conjunto e por marca de componente + const markKeys = [mainMark, cleanMain, parsed.singlePartMark, parsed.pieceMark, parsed.cleanMark].filter(Boolean) as string[]; + for (const mKey of Array.from(new Set(markKeys))) { + const list = piecesByMark.get(mKey) || []; 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); + piecesByMark.set(mKey, list); } } }); diff --git a/src/lib/ifc/ifcLoaderService.ts b/src/lib/ifc/ifcLoaderService.ts index 1602e0f..52710f4 100644 --- a/src/lib/ifc/ifcLoaderService.ts +++ b/src/lib/ifc/ifcLoaderService.ts @@ -33,6 +33,7 @@ export interface PieceInfo { parentAssemblyID?: number; phase?: string; ofNumber?: string; + singlePartMark?: string; section?: string; span?: number; material?: string; diff --git a/src/pages/Visualizador3D.tsx b/src/pages/Visualizador3D.tsx index 9e497c9..0bd08b1 100644 --- a/src/pages/Visualizador3D.tsx +++ b/src/pages/Visualizador3D.tsx @@ -148,6 +148,8 @@ export default function Visualizador3D() { marca: marcaStr, fase, ofNumber: selectedOF, + descricao: p.descricao || '', + perfilPrincipal: p.perfil_principal || p.descricao || '', totalQtd: Number(p.quantidade || 1), pesoTotal: Number(p.peso_total || 0), pesoUnitario: Number(p.peso_unitario || 0),