From 609f66c21813a1f52482c99463691540bacdf9cc Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Sun, 20 Sep 2026 01:54:07 +0000 Subject: [PATCH] fix(visualizador3d): integracao fiel do driver SteelXR web-ifc@0.0.57 e estilizacao grafica vibrante --- package.json | 2 +- src/components/viewer3d/ModelViewer3D.tsx | 43 ++-- src/lib/ifc/ifcLoaderService.ts | 267 ++++++++++++++-------- 3 files changed, 196 insertions(+), 116 deletions(-) diff --git a/package.json b/package.json index 63552fb..55b1de5 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "tailwindcss-animate": "^1.0.7", "three": "^0.186.0", "three-stdlib": "^2.36.1", - "web-ifc": "^0.0.74", + "web-ifc": "^0.0.57", "xlsx": "^0.18.5", "zod": "^3.25.76" }, diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index 0d3d529..b41988d 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; import * as THREE from 'three'; import { OrbitControls } from 'three-stdlib'; -import { PieceInfo, LoadedIFCResult } from '@/lib/ifc/ifcLoaderService'; +import { PieceInfo, LoadedIFCResult, getColorForMaterialName } from '@/lib/ifc/ifcLoaderService'; import { ViewCube } from './ViewCube'; import { Viewer3DToolbar } from './Viewer3DToolbar'; import { PieceHoverTooltip } from './PieceHoverTooltip'; @@ -99,12 +99,15 @@ export const ModelViewer3D: React.FC = ({ activeCameraRef.current = perspCamera; - // 3. Renderer - const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + // 3. Renderer com padronizacao grafica idêntica ao SteelXR + const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: 'high-performance' }); renderer.setSize(width, height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.shadowMap.enabled = true; - renderer.shadowMap.type = THREE.PCFSoftShadowMap; + renderer.shadowMap.type = THREE.PCFShadowMap; + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1.05; + renderer.outputColorSpace = THREE.SRGBColorSpace; rendererRef.current = renderer; containerRef.current.innerHTML = ''; @@ -279,23 +282,35 @@ export const ModelViewer3D: React.FC = ({ side: THREE.DoubleSide, }); } else { - // Não apontada / Pendente: Cinza claro padrão industrial + // Não apontada / Pendente: Cinza claro padrão industrial (#a1a1aa) mesh.material = new THREE.MeshStandardMaterial({ - color: 0x94a3b8, - metalness: 0.25, - roughness: 0.65, + color: new THREE.Color(0xa1a1aa), + metalness: 0.2, + roughness: 0.7, transparent: opacity < 100, opacity: opacity / 100, side: THREE.DoubleSide, }); } } else { - // Color Mode "Descrição" (Palette by Profile/Section) - const colorKey = mesh.userData.colorKey; - const cachedMat = modelData.materialsByColor.get(colorKey); - if (cachedMat) { - mesh.material = cachedMat; - } + // 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); + mesh.material = new THREE.MeshStandardMaterial({ + color: new THREE.Color(hsl), + metalness: 0.25, + roughness: 0.65, + transparent: opacity < 100, + opacity: opacity / 100, + side: THREE.DoubleSide, + }); } // Apply Opacity & Wireframe diff --git a/src/lib/ifc/ifcLoaderService.ts b/src/lib/ifc/ifcLoaderService.ts index a919a8c..1950976 100644 --- a/src/lib/ifc/ifcLoaderService.ts +++ b/src/lib/ifc/ifcLoaderService.ts @@ -50,21 +50,20 @@ export interface LoadedIFCResult { totalMeshes: number; } -// Paleta por Descrição/Seção (Modo "Descrição") -const DESCRIPTION_PALETTE = [ - '#3b82f6', // azul - '#ec4899', // rosa - '#10b981', // esmeralda - '#f59e0b', // ambar - '#8b5cf6', // roxo - '#06b6d4', // ciano - '#ef4444', // vermelho - '#84cc16', // lima - '#6366f1', // indigo - '#14b8a6', // teal - '#f97316', // laranja - '#eab308', // amarelo -]; +/** + * Função idêntica ao SteelXR para gerar cores vibrantes e distintas por descrição / perfil / material. + */ +export function getColorForMaterialName(name: string): string { + if (!name) return '#a1a1aa'; + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + const h = Math.abs(hash) % 360; + const s = 65 + (Math.abs(hash >> 8) % 15); + const l = 45 + (Math.abs(hash >> 16) % 10); + return `hsl(${h}, ${s}%, ${l}%)`; +} // Helper: Valida se um valor extraído é uma marca útil real export function isValidMark(val: any): boolean { @@ -111,14 +110,12 @@ export function getCleanMarkValue(rawMark: string): string { if (!rawMark) return ''; let cleaned = rawMark.trim(); - // Se tiver hífen, o último segmento costuma ser a marca unitária da peça (ex: OF-FASE-MARCA -> MARCA) if (cleaned.includes('-')) { const parts = cleaned.split('-'); const last = parts[parts.length - 1].trim(); if (last && isValidMark(last)) return last; } - // Se contiver prefixo tipo "Pos: 4" ou "Mark: 4" const prefixMatch = cleaned.match(/(?:Mark|Pos|Marca|Posicao)[:\s=]*([^\s,;]+)/i); if (prefixMatch && isValidMark(prefixMatch[1])) { return prefixMatch[1].trim(); @@ -127,19 +124,66 @@ export function getCleanMarkValue(rawMark: string): string { return cleaned; } +// 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; + const matId = matRef.value; + if (!matId) return null; + try { + const matLine = ifcApi.GetLine(modelID, matId); + if (!matLine) return null; + + if (matLine.Name && matLine.Name.value) { + return matLine.Name.value; + } + + if (matLine.Materials) { + for (const mRef of matLine.Materials) { + const name = getMaterialName(ifcApi, modelID, mRef); + if (name) return name; + } + } + + if (matLine.MaterialConstituents) { + for (const mcRef of matLine.MaterialConstituents) { + const mcLine = ifcApi.GetLine(modelID, mcRef.value); + if (mcLine && mcLine.Material) { + const name = getMaterialName(ifcApi, modelID, mcLine.Material); + if (name) return name; + } + } + } + + if (matLine.MaterialProfileSet) { + return getMaterialName(ifcApi, modelID, matLine.MaterialProfileSet); + } + if (matLine.MaterialProfiles) { + for (const mpRef of matLine.MaterialProfiles) { + const mpLine = ifcApi.GetLine(modelID, mpRef.value); + if (mpLine && mpLine.Material) { + const name = getMaterialName(ifcApi, modelID, mpLine.Material); + if (name) return name; + } + } + } + } catch { + // ignore + } + return null; +} + export async function loadAndAuditIFC( fileOrBuffer: File | ArrayBuffer, onProgress?: (percent: number, step: string) => void ): Promise { const ifcApi = new WebIFC.IfcAPI(); - // 1. Inicializa Web-IFC WASM com fallback - if (onProgress) onProgress(10, 'Carregando motor Web-IFC (WASM)...'); + // 1. Inicializa Web-IFC com a versão 0.0.57 (mesma versão estável do SteelXR) + if (onProgress) onProgress(10, 'Carregando motor SteelXR IFC (WASM)...'); - const LOCAL_WASM_PATH = '/wasm/'; - const CDN_WASM_PATH = 'https://unpkg.com/web-ifc@0.0.74/'; + const WASM_PATH_057 = 'https://unpkg.com/web-ifc@0.0.57/'; - // Intercepta logs do console temporariamente para filtrar avisos internos da engine C++ + // Filtra avisos internos da biblioteca C++ para manter o console limpo const originalConsoleError = console.error; const originalConsoleWarn = console.warn; console.error = (...args: any[]) => { @@ -159,17 +203,17 @@ export async function loadAndAuditIFC( let wasmInitSuccess = false; try { - ifcApi.SetWasmPath(LOCAL_WASM_PATH, true); + ifcApi.SetWasmPath(WASM_PATH_057, true); await ifcApi.Init(); wasmInitSuccess = true; - } catch (localErr) { - console.warn('[IFC Loader] Falha ao carregar WASM local (/wasm/), tentando CDN...', localErr); + } catch (cdnErr) { + console.warn('[IFC Loader] Tentando fallback para /wasm/...', cdnErr); try { - ifcApi.SetWasmPath(CDN_WASM_PATH, true); + ifcApi.SetWasmPath('/wasm/', true); await ifcApi.Init(); wasmInitSuccess = true; - } catch (cdnErr) { - console.error('[IFC Loader] Falha ao inicializar Web-IFC via CDN:', cdnErr); + } catch (localErr) { + console.error('[IFC Loader] Falha ao inicializar Web-IFC:', localErr); } } @@ -192,12 +236,9 @@ export async function loadAndAuditIFC( rawHeader = decoder.decode(data.slice(0, 4096)); } - // 3. Abertura do Modelo com suporte a coordenadas relativas + // 3. Abertura do Modelo idêntica ao SteelXR (OpenModel puro, compatibilidade máxima) if (onProgress) onProgress(40, 'Decodificando entidades IFC...'); - const modelID = ifcApi.OpenModel(data, { - COORDINATE_TO_ORIGIN: true, - USE_FAST_BOOLS: true - } as any); + const modelID = ifcApi.OpenModel(data); if (modelID < 0) { throw new Error('Falha ao decodificar a estrutura do arquivo IFC.'); } @@ -244,7 +285,27 @@ export async function loadAndAuditIFC( } } - // 4.2 Grafo de Soldas (IFCRELCONNECTSWITHREALIZINGELEMENTS) + // 4.2 Mapeamento de Materiais do SteelXR (IFCRELASSOCIATESMATERIAL) + const elementMaterialMap = new Map(); + try { + const rels = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELASSOCIATESMATERIAL); + for (let i = 0; i < rels.size(); i++) { + const relId = rels.get(i); + const rel = ifcApi.GetLine(modelID, relId); + if (rel && rel.RelatedObjects && rel.RelatingMaterial) { + const matName = getMaterialName(ifcApi, modelID, rel.RelatingMaterial); + if (matName) { + for (const objRef of rel.RelatedObjects) { + elementMaterialMap.set(objRef.value, matName); + } + } + } + } + } catch { + // ignore + } + + // 4.3 Mapeamento de Soldas (IFCRELCONNECTSWITHREALIZINGELEMENTS) const weldConnections = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELCONNECTSWITHREALIZINGELEMENTS); const adjacencyList = new Map>(); @@ -269,7 +330,6 @@ export async function loadAndAuditIFC( } } - // Componentes conectados soldados const visited = new Set(); const weldedGroupMap = new Map(); let totalWeldedGroups = 0; @@ -299,7 +359,7 @@ export async function loadAndAuditIFC( } } - // 4.3 Indexação profunda de propriedades (IFCRELDEFINESBYPROPERTIES) + // 4.4 Indexação completa de propriedades (IFCRELDEFINESBYPROPERTIES) idêntica ao SteelXR const elementPropertiesMap = new Map>(); try { const relsProp = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELDEFINESBYPROPERTIES); @@ -314,7 +374,7 @@ export async function loadAndAuditIFC( if (propDef) { const props: Record = {}; - // Leitura de HasProperties (Pset_*) + // HasProperties (Pset_*) if (propDef.HasProperties && Array.isArray(propDef.HasProperties)) { for (const pRef of propDef.HasProperties) { const pId = pRef?.value; @@ -338,7 +398,7 @@ export async function loadAndAuditIFC( } } - // Leitura de Quantities (Qto_*) + // Quantities (Qto_*) if (propDef.Quantities && Array.isArray(propDef.Quantities)) { for (const qRef of propDef.Quantities) { const qId = qRef?.value; @@ -401,7 +461,6 @@ export async function loadAndAuditIFC( const descParsed = parseDescriptionFields(rawDesc); - // Prioridade de propriedades que representam Marca/Posição da peça const candidates: (string | undefined)[] = [ props.PieceMark, props.PartMark, @@ -421,7 +480,7 @@ export async function loadAndAuditIFC( props.PartNumber, rawTag, props.Tag, - props.Reference, // Validado por isValidMark + props.Reference, rawObjType, rawName ]; @@ -434,7 +493,6 @@ export async function loadAndAuditIFC( } } - // Se não encontrou no próprio elemento, tenta herdar da montagem pai if (!chosenMark && childToAssembly.has(expressID)) { const parentId = childToAssembly.get(expressID)!; const parentProps = elementPropertiesMap.get(parentId) || {}; @@ -484,8 +542,6 @@ export async function loadAndAuditIFC( const piecesByMark = new Map(); const pieceByExpressID = new Map(); const uniqueMarksSet = new Set(); - const sectionColorMap = new Map(); - let colorIndex = 0; for (let i = 0; i < assemblies.size(); i++) { const assID = assemblies.get(i); @@ -520,57 +576,48 @@ export async function loadAndAuditIFC( } } - // 5. Geração de Malhas 3D com Three.js corrigindo o empacotamento de vértices/normais + // 5. Geração de Malhas 3D idêntica ao SteelXR if (onProgress) onProgress(75, 'Gerando malhas geométricas 3D...'); const sceneGroup = new THREE.Group(); sceneGroup.name = 'IFC_ROOT_MODEL'; - const defaultGrayMat = new THREE.MeshStandardMaterial({ - color: 0x94a3b8, - metalness: 0.3, - roughness: 0.6, - side: THREE.DoubleSide - }); - - const materialCache = new Map(); + const materialsCache: Map = new Map(); let totalMeshes = 0; - ifcApi.StreamAllMeshes(modelID, (flatMesh: WebIFC.FlatMesh) => { - const expressID = flatMesh.expressID; - const placedGeometries = flatMesh.geometries; + ifcApi.StreamAllMeshes(modelID, (mesh: WebIFC.FlatMesh) => { + const placedGeometries = mesh.geometries; + const expressID = (mesh as unknown as { expressID?: number }).expressID ?? 0; const markInfo = extractBestMark(expressID); - const colorKey = markInfo.section || 'PADRAO'; - - if (!sectionColorMap.has(colorKey)) { - sectionColorMap.set(colorKey, DESCRIPTION_PALETTE[colorIndex % DESCRIPTION_PALETTE.length]); - colorIndex++; - } - - const hexColor = sectionColorMap.get(colorKey)!; - if (!materialCache.has(hexColor)) { - materialCache.set( - hexColor, - new THREE.MeshStandardMaterial({ - color: new THREE.Color(hexColor), - metalness: 0.35, - roughness: 0.55, - side: THREE.DoubleSide - }) - ); - } - const mat = materialCache.get(hexColor) || defaultGrayMat; + const materialName = elementMaterialMap.get(expressID) ?? ''; const connectedGroup = weldedGroupMap.get(expressID) || [expressID]; - for (let g = 0; g < placedGeometries.size(); g++) { - const placedGeo = placedGeometries.get(g); - const ifcGeo = ifcApi.GetGeometry(modelID, placedGeo.geometryExpressID); + const elementGroup = new THREE.Group(); + elementGroup.name = `ifc_${expressID}`; + elementGroup.userData = { + ifcElement: true, + ifcId: expressID, + pieceMark: markInfo.mark, + cleanMark: markInfo.cleanMark, + section: markInfo.section, + materialName, + connectedMeshIDs: connectedGroup + }; - // Web-IFC GetVertexArray retorna [px, py, pz, nx, ny, nz, ...] (interleaved de 6 floats) - const verts = ifcApi.GetVertexArray(ifcGeo.GetVertexData(), ifcGeo.GetVertexDataSize()); - const indices = ifcApi.GetIndexArray(ifcGeo.GetIndexData(), ifcGeo.GetIndexDataSize()); + for (let i = 0; i < placedGeometries.size(); i++) { + const placedGeometry = placedGeometries.get(i); + const ifcGeometry = ifcApi.GetGeometry(modelID, placedGeometry.geometryExpressID); - const bufferGeo = new THREE.BufferGeometry(); + const verts = ifcApi.GetVertexArray( + ifcGeometry.GetVertexData(), + ifcGeometry.GetVertexDataSize() + ); + const indices = ifcApi.GetIndexArray( + ifcGeometry.GetIndexData(), + ifcGeometry.GetIndexDataSize() + ); + + const geometry = new THREE.BufferGeometry(); const numVerts = verts.length / 6; const positionArray = new Float32Array(numVerts * 3); const normalArray = new Float32Array(numVerts * 3); @@ -585,26 +632,43 @@ export async function loadAndAuditIFC( normalArray[idx * 3 + 2] = verts[j + 5]; } - bufferGeo.setAttribute('position', new THREE.BufferAttribute(positionArray, 3)); - bufferGeo.setAttribute('normal', new THREE.BufferAttribute(normalArray, 3)); - bufferGeo.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1)); + geometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3)); + geometry.setAttribute('normal', new THREE.BufferAttribute(normalArray, 3)); + geometry.setIndex(new THREE.BufferAttribute(indices, 1)); - const mesh = new THREE.Mesh(bufferGeo, mat); - const matrix = new THREE.Matrix4(); - matrix.fromArray(placedGeo.flatTransformation); - mesh.applyMatrix4(matrix); + // Cor original nativa do IFC igual ao SteelXR + 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); + } - mesh.userData = { - expressID, + const mesh3 = new THREE.Mesh(geometry, material); + mesh3.userData = { + ifcId: expressID, pieceMark: markInfo.mark, cleanMark: markInfo.cleanMark, section: markInfo.section, - colorKey, + materialName, + nativeColor: new THREE.Color(color.x, color.y, color.z), connectedMeshIDs: connectedGroup }; - sceneGroup.add(mesh); - ifcGeo.delete(); + const matrix = new THREE.Matrix4(); + matrix.fromArray(placedGeometry.flatTransformation); + mesh3.applyMatrix4(matrix); + + elementGroup.add(mesh3); + ifcGeometry.delete(); totalMeshes++; if (!pieceByExpressID.has(expressID)) { @@ -616,8 +680,9 @@ export async function loadAndAuditIFC( pieceMark: markInfo.mark, cleanMark: markInfo.cleanMark, section: markInfo.section, + material: materialName, connectedMeshIDs: connectedGroup, - mesh + mesh: mesh3 }; pieceByExpressID.set(expressID, piece); if (markInfo.mark && markInfo.mark !== 'indefinido') { @@ -626,14 +691,14 @@ export async function loadAndAuditIFC( if (!piecesByMark.has(markInfo.mark)) piecesByMark.set(markInfo.mark, []); piecesByMark.get(markInfo.mark)!.push(piece); } - } else { - pieceByExpressID.get(expressID)!.mesh = mesh; - pieceByExpressID.get(expressID)!.connectedMeshIDs = connectedGroup; } } + + if (elementGroup.children.length > 0) { + sceneGroup.add(elementGroup); + } }); - // Fecha o modelo após extração para liberar memória try { ifcApi.CloseModel(modelID); } catch {} @@ -649,7 +714,7 @@ export async function loadAndAuditIFC( qualityMessage = 'Modelo exportado no padrão AISC EM.11 (SteelFabricationView). Todas as montagens e marcas principais de conjuntos estão 100% integradas nativamente.'; } else if (totalWeldedGroups > 0 || uniqueMarksSet.size > 0) { qualityScore = 'BOM'; - qualityMessage = `Modo Avançado Ativado: ${uniqueMarksSet.size} marcas de peças e ${totalWeldedGroups} conjuntos topológicos identificados com sucesso.`; + qualityMessage = `Modo SteelXR Ativado: ${uniqueMarksSet.size} marcas e conjuntos identificados com qualidade gráfica calibrada.`; recommendation = 'O modelo está totalmente operacional e sincronizado com os apontamentos da fábrica.'; } else { qualityScore = 'ATENÇÃO'; @@ -685,7 +750,7 @@ export async function loadAndAuditIFC( piecesByMark, pieceByExpressID, weldedGroupMap, - materialsByColor: materialCache as any, + materialsByColor: materialsCache as any, totalMeshes }; }