From b29482b450be024df1456008ab20b37206e7b236 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Sun, 20 Sep 2026 02:32:34 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20inicializacao=20em=20cinza=20claro=20e?= =?UTF-8?q?=20amarra=C3=A7=C3=A3o=20hier=C3=A1rquica=20assembly/peca=20no?= =?UTF-8?q?=20visualizador=20IFC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/viewer3d/ModelViewer3D.tsx | 118 +++++++++++++--------- src/lib/ifc/ifcLoaderService.ts | 72 +++++++++---- src/pages/Visualizador3D.tsx | 16 +++ 3 files changed, 140 insertions(+), 66 deletions(-) diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index b41988d..0c1a1d0 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -19,6 +19,8 @@ interface ModelViewer3DProps { modelData: LoadedIFCResult | null; productionData?: Map; selectedPieceMark?: string | null; + selectedOF?: string; + selectedPhase?: string; onSelectPiece?: (piece: PieceInfo | null) => void; } @@ -26,6 +28,8 @@ export const ModelViewer3D: React.FC = ({ modelData, productionData, selectedPieceMark, + selectedOF, + selectedPhase, onSelectPiece, }) => { const containerRef = useRef(null); @@ -232,48 +236,69 @@ export const ModelViewer3D: React.FC = ({ if (!modelData || !modelGroupRef.current) return; const group = modelGroupRef.current; + let matchedCount = 0; + let pointedCount = 0; + let totalMeshCount = 0; + let sampleMatch: any = null; group.traverse((child) => { if ((child as THREE.Mesh).isMesh) { const mesh = child as THREE.Mesh; - const pmark = String(mesh.userData.pieceMark || '').trim(); - const cleanMark = String(mesh.userData.cleanMark || pmark).trim(); + totalMeshCount++; - if (colorMode === 'production' && productionData) { - // Look up production pointing status: tenta pmark, depois cleanMark, depois busca flexível - let prod = productionData.get(pmark) || productionData.get(cleanMark); - if (!prod) { - for (const [key, val] of productionData.entries()) { - if ( - pmark === key || - cleanMark === key || - pmark === val.marca || - cleanMark === val.marca || - pmark.endsWith(`-${key}`) || - pmark.endsWith(`-${val.marca}`) || - key.endsWith(`-${pmark}`) || - key.endsWith(`-${cleanMark}`) - ) { - prod = val; - break; + const pmark = String(mesh.userData.pieceMark || '').trim().toUpperCase(); + const cleanMark = String(mesh.userData.cleanMark || pmark).trim().toUpperCase(); + const assMark = String(mesh.userData.assemblyMark || '').trim().toUpperCase(); + const cleanAssMark = String(mesh.userData.cleanAssemblyMark || cleanMark).trim().toUpperCase(); + const section = String(mesh.userData.section || '').trim().toUpperCase(); + + if (colorMode === 'production' && productionData && productionData.size > 0) { + let prod: ProductionPieceStatus | undefined = undefined; + + // 1. Prioridade: Busca pela Assembly Mark do conjunto estrutural (ex: B135-3-7 -> "B135-3-7", "3-7", "7") + if (assMark && assMark !== 'INDEFINIDO') { + prod = productionData.get(assMark) || + productionData.get(cleanAssMark) || + (selectedPhase && selectedPhase !== 'all' ? productionData.get(`${selectedPhase}-${cleanAssMark}`.toUpperCase()) : undefined) || + (selectedOF ? productionData.get(`${selectedOF}-${cleanAssMark}`.toUpperCase()) : undefined); + } + + // 2. Busca pela marca própria da peça (ex: B135-3-22 -> "B135-3-22", "3-22", "22") + if (!prod && pmark && pmark !== 'INDEFINIDO') { + prod = productionData.get(pmark) || + productionData.get(cleanMark) || + (selectedPhase && selectedPhase !== 'all' ? productionData.get(`${selectedPhase}-${cleanMark}`.toUpperCase()) : undefined) || + (selectedOF ? productionData.get(`${selectedOF}-${cleanMark}`.toUpperCase()) : undefined); + } + + // 3. Fallback inteligente para perfis / elementos sem marca (ex: barras redondas 'RD19', tubos 'Box150X150X9.') + if (!prod && section && section !== '-' && section !== 'INDEFINIDO') { + prod = productionData.get(`PERFIL:${section}`) || + productionData.get(`DESC:${section}`); + + if (!prod) { + for (const [key, val] of productionData.entries()) { + if (key.startsWith('DESC:') || key.startsWith('PERFIL:')) { + const term = key.replace('DESC:', '').replace('PERFIL:', ''); + if (term && (section.includes(term) || term.includes(section))) { + prod = val; + break; + } + } } } } - // Debug log (primeiras 10 peças para validação no console) - if ((window as any)._debugIfcColors === undefined) (window as any)._debugIfcColors = 0; - if ((window as any)._debugIfcColors < 10) { - console.log( - `[IFC Color Debug] IFC pmark: "${pmark}" (clean: "${cleanMark}") -> DB match:`, - prod - ? `ACHOU! Marca DB: "${prod.marca}", Apontadas: ${prod.pointedQtd}/${prod.totalQtd}, Processo: "${prod.currentProcessName}", Cor: ${prod.processColor}` - : `NÃO ACHOU. Chaves DB disponíveis: ${Array.from(productionData.keys()).slice(0, 8).join(', ')}...` - ); - (window as any)._debugIfcColors++; + if (prod) { + matchedCount++; + if (!sampleMatch && prod.pointedQtd > 0) { + sampleMatch = { pmark: assMark || pmark, dbMarca: prod.marca, proc: prod.currentProcessName, cor: prod.processColor }; + } } if (prod && prod.pointedQtd > 0) { - // Peça apontada: aplica a cor do estágio de fabricação + pointedCount++; + // Peça apontada no banco: aplica com precisão a cor do estágio de fabricação const stageHex = prod.processColor || '#10b981'; mesh.material = new THREE.MeshStandardMaterial({ color: new THREE.Color(stageHex), @@ -282,29 +307,20 @@ export const ModelViewer3D: React.FC = ({ side: THREE.DoubleSide, }); } else { - // Não apontada / Pendente: Cinza claro padrão industrial (#a1a1aa) + // Não apontada / Pendente ou Não vinculada: 100% CINZA CLARO INDUSTRIAL (#a1a1aa) mesh.material = new THREE.MeshStandardMaterial({ color: new THREE.Color(0xa1a1aa), - metalness: 0.2, - roughness: 0.7, + metalness: 0.25, + roughness: 0.65, transparent: opacity < 100, opacity: opacity / 100, side: THREE.DoubleSide, }); } } else { - // Color Mode "Descrição": Cores ricas por perfil e material idênticas ao SteelXR - const parentData = mesh.parent?.userData || {}; - const label = - mesh.userData.section || - parentData.section || - mesh.userData.materialName || - parentData.materialName || - mesh.userData.pieceMark || - 'PADRAO'; - const hsl = getColorForMaterialName(label); + // Todas as peças iniciam / permanecem em Cinza Claro Industrial (#a1a1aa) mesh.material = new THREE.MeshStandardMaterial({ - color: new THREE.Color(hsl), + color: new THREE.Color(0xa1a1aa), metalness: 0.25, roughness: 0.65, transparent: opacity < 100, @@ -327,7 +343,19 @@ export const ModelViewer3D: React.FC = ({ } } }); - }, [modelData, colorMode, productionData, opacity, isWireframe]); + + if (totalMeshCount > 0 && productionData && productionData.size > 0) { + console.log( + `%c[IFC Sync] Amarração de Peças: ${matchedCount}/${totalMeshCount} malhas vinculadas | ${pointedCount} apontadas na produção | ${totalMeshCount - pointedCount} pendentes em cinza claro`, + 'color: #06b6d4; font-weight: bold;' + ); + if (sampleMatch) { + console.log( + `[IFC Sync] Exemplo de amarração: IFC "${sampleMatch.pmark}" -> Banco "${sampleMatch.dbMarca}" (Processo: ${sampleMatch.proc}, Cor: ${sampleMatch.cor})` + ); + } + } + }, [modelData, colorMode, productionData, selectedPhase, selectedOF, opacity, isWireframe]); // Camera Toggle (Ortogonal / Perspectiva) const handleToggleCamera = useCallback(() => { diff --git a/src/lib/ifc/ifcLoaderService.ts b/src/lib/ifc/ifcLoaderService.ts index 1950976..c3e004d 100644 --- a/src/lib/ifc/ifcLoaderService.ts +++ b/src/lib/ifc/ifcLoaderService.ts @@ -29,6 +29,7 @@ export interface PieceInfo { pieceMark: string; cleanMark?: string; assemblyMark?: string; + cleanAssemblyMark?: string; parentAssemblyID?: number; phase?: string; ofNumber?: string; @@ -124,6 +125,14 @@ export function getCleanMarkValue(rawMark: string): string { return cleaned; } +// Helper: Identifica se uma marca é número de peça solta/corte (ex: 1000, 1001, 1002, 1010) +export function isSinglePartCutNumber(val: string): boolean { + if (!val) return false; + const clean = getCleanMarkValue(val); + const num = parseInt(clean, 10); + return !isNaN(num) && num >= 1000; +} + // Helper: Mapeamento de nome de material idêntico ao SteelXR function getMaterialName(ifcApi: WebIFC.IfcAPI, modelID: number, matRef: any): string | null { if (!matRef) return null; @@ -440,6 +449,8 @@ export async function loadAndAuditIFC( function extractBestMark(expressID: number): { mark: string; cleanMark: string; + assemblyMark: string; + cleanAssemblyMark: string; section: string; description: string; } { @@ -493,7 +504,10 @@ export async function loadAndAuditIFC( } } - if (!chosenMark && childToAssembly.has(expressID)) { + // Busca Assembly Mark no conjunto pai (IFCRELAGGREGATES) + let assemblyMark = ''; + let cleanAssemblyMark = ''; + if (childToAssembly.has(expressID)) { const parentId = childToAssembly.get(expressID)!; const parentProps = elementPropertiesMap.get(parentId) || {}; try { @@ -512,13 +526,30 @@ export async function loadAndAuditIFC( ]; for (const pCand of parentCandidates) { if (pCand && isValidMark(pCand)) { - chosenMark = String(pCand).trim(); + assemblyMark = String(pCand).trim(); + cleanAssemblyMark = getCleanMarkValue(assemblyMark); break; } } } catch {} } + if (!assemblyMark && props.AssemblyMark && isValidMark(props.AssemblyMark)) { + assemblyMark = String(props.AssemblyMark).trim(); + cleanAssemblyMark = getCleanMarkValue(assemblyMark); + } + + // Em estruturas metálicas, a marca da peça apontada na produção é a marca do conjunto (Assembly Mark), + // a menos que a peça seja avulsa ou que não tenha conjunto pai + let finalMark = chosenMark; + if (assemblyMark && isValidMark(assemblyMark)) { + if (!finalMark || finalMark === 'indefinido' || isSinglePartCutNumber(finalMark)) { + finalMark = assemblyMark; + } + } + + const clean = getCleanMarkValue(finalMark); + const section = props.Section || props.Profile || @@ -528,11 +559,11 @@ export async function loadAndAuditIFC( rawName || ''; - const clean = getCleanMarkValue(chosenMark); - return { - mark: chosenMark || 'indefinido', - cleanMark: clean || chosenMark || 'indefinido', + mark: finalMark || 'indefinido', + cleanMark: clean || finalMark || 'indefinido', + assemblyMark: assemblyMark || finalMark || '', + cleanAssemblyMark: cleanAssemblyMark || clean || '', section, description: rawDesc }; @@ -557,6 +588,8 @@ export async function loadAndAuditIFC( type: 'IFCELEMENTASSEMBLY', pieceMark: markInfo.mark, cleanMark: markInfo.cleanMark, + assemblyMark: markInfo.assemblyMark, + cleanAssemblyMark: markInfo.cleanAssemblyMark, childrenIDs: children, connectedMeshIDs: children, section: markInfo.section, @@ -599,6 +632,8 @@ export async function loadAndAuditIFC( ifcId: expressID, pieceMark: markInfo.mark, cleanMark: markInfo.cleanMark, + assemblyMark: markInfo.assemblyMark, + cleanAssemblyMark: markInfo.cleanAssemblyMark, section: markInfo.section, materialName, connectedMeshIDs: connectedGroup @@ -636,27 +671,22 @@ export async function loadAndAuditIFC( geometry.setAttribute('normal', new THREE.BufferAttribute(normalArray, 3)); geometry.setIndex(new THREE.BufferAttribute(indices, 1)); - // Cor original nativa do IFC igual ao SteelXR + // Padrão industrial solicitado: O modelo 3D IFC SEMPRE inicia com todas as peças 100% em cinza claro (#a1a1aa) const color = placedGeometry.color; - const colorKey = (color.x * 255) << 16 | (color.y * 255) << 8 | (color.z * 255); - let material = materialsCache.get(colorKey); - if (!material) { - material = new THREE.MeshStandardMaterial({ - color: new THREE.Color(color.x, color.y, color.z), - metalness: 0.25, - roughness: 0.65, - transparent: color.w < 1, - opacity: color.w, - side: THREE.DoubleSide, - }); - materialsCache.set(colorKey, material); - } + const initialGrayMaterial = new THREE.MeshStandardMaterial({ + color: new THREE.Color(0xa1a1aa), + metalness: 0.25, + roughness: 0.65, + side: THREE.DoubleSide, + }); - const mesh3 = new THREE.Mesh(geometry, material); + const mesh3 = new THREE.Mesh(geometry, initialGrayMaterial); mesh3.userData = { ifcId: expressID, pieceMark: markInfo.mark, cleanMark: markInfo.cleanMark, + assemblyMark: markInfo.assemblyMark, + cleanAssemblyMark: markInfo.cleanAssemblyMark, section: markInfo.section, materialName, nativeColor: new THREE.Color(color.x, color.y, color.z), diff --git a/src/pages/Visualizador3D.tsx b/src/pages/Visualizador3D.tsx index b1f9962..2670b30 100644 --- a/src/pages/Visualizador3D.tsx +++ b/src/pages/Visualizador3D.tsx @@ -144,12 +144,26 @@ export default function Visualizador3D() { // Indexa por marca simples (ex: "4") prodMap.set(marcaStr, item); + prodMap.set(marcaStr.toUpperCase(), item); // Indexa por fase e marca (ex: "2-4") prodMap.set(`${fase}-${marcaStr}`, item); + prodMap.set(`${fase}-${marcaStr}`.toUpperCase(), item); // Indexa por OF, fase e marca (ex: "B135-2-4") prodMap.set(`${selectedOF}-${fase}-${marcaStr}`, item); + prodMap.set(`${selectedOF}-${fase}-${marcaStr}`.toUpperCase(), item); // Indexa por OF e marca (ex: "B135-4") prodMap.set(`${selectedOF}-${marcaStr}`, item); + prodMap.set(`${selectedOF}-${marcaStr}`.toUpperCase(), item); + + // Indexa descrição e perfil principal para amarração de elementos sem numeração (ex: barras redondas RD19) + if (p.descricao) { + const d = String(p.descricao).trim().toUpperCase(); + prodMap.set(`DESC:${d}`, item); + } + if (p.perfil_principal) { + const perf = String(p.perfil_principal).trim().toUpperCase(); + prodMap.set(`PERFIL:${perf}`, item); + } }); } @@ -436,6 +450,8 @@ export default function Visualizador3D() {