diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index d745452..0d3d529 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -233,31 +233,45 @@ export const ModelViewer3D: React.FC = ({ group.traverse((child) => { if ((child as THREE.Mesh).isMesh) { const mesh = child as THREE.Mesh; - const pmark = mesh.userData.pieceMark || ''; + const pmark = String(mesh.userData.pieceMark || '').trim(); + const cleanMark = String(mesh.userData.cleanMark || pmark).trim(); if (colorMode === 'production' && productionData) { - // Look up production pointing status - let prod = productionData.get(pmark); + // Look up production pointing status: tenta pmark, depois cleanMark, depois busca flexível + let prod = productionData.get(pmark) || productionData.get(cleanMark); if (!prod) { - // Fallback lookup: IFC marks sometimes have prefixes like B135-1-V1 or suffixes. for (const [key, val] of productionData.entries()) { - if (pmark === key || pmark.endsWith(`-${key}`) || key.endsWith(`-${pmark}`) || pmark.includes(key)) { + 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; } } } - // Debug log (somente primeira vez para as primeiras 5 peças para não travar o console) + // 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}" -> DB match:`, prod ? `Achou! Marca DB: "${prod.marca}", Qtd: ${prod.pointedQtd}, Cor: ${prod.processColor}` : `NÃO ACHOU. Chaves disponíveis: ${Array.from(productionData.keys()).slice(0,5).join(', ')}...`); + 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 && prod.pointedQtd > 0) { - // Apply stage color (e.g., Light Green #4ade80 for pointed) - const stageHex = prod.processColor || '#4ade80'; + // Peça apontada: aplica a cor do estágio de fabricação + const stageHex = prod.processColor || '#10b981'; mesh.material = new THREE.MeshStandardMaterial({ color: new THREE.Color(stageHex), metalness: 0.35, @@ -265,11 +279,11 @@ export const ModelViewer3D: React.FC = ({ side: THREE.DoubleSide, }); } else { - // Unpointed / Pending (Default Gray) + // Não apontada / Pendente: Cinza claro padrão industrial mesh.material = new THREE.MeshStandardMaterial({ - color: 0x64748b, - metalness: 0.3, - roughness: 0.6, + color: 0x94a3b8, + metalness: 0.25, + roughness: 0.65, transparent: opacity < 100, opacity: opacity / 100, side: THREE.DoubleSide, diff --git a/src/lib/ifc/ifcLoaderService.ts b/src/lib/ifc/ifcLoaderService.ts index 0b5b3e5..a919a8c 100644 --- a/src/lib/ifc/ifcLoaderService.ts +++ b/src/lib/ifc/ifcLoaderService.ts @@ -27,6 +27,7 @@ export interface PieceInfo { name: string; type: string; pieceMark: string; + cleanMark?: string; assemblyMark?: string; parentAssemblyID?: number; phase?: string; @@ -44,34 +45,95 @@ export interface LoadedIFCResult { audit: IFCQualityAudit; piecesByMark: Map; pieceByExpressID: Map; - weldedGroupMap: Map; // expressID -> all connected expressIDs + weldedGroupMap: Map; materialsByColor: Map; totalMeshes: number; } -// Helper: Palette by Section/Description (Mode "Descrição") +// Paleta por Descrição/Seção (Modo "Descrição") const DESCRIPTION_PALETTE = [ - '#3b82f6', // blue - '#ec4899', // pink/magenta - '#10b981', // emerald - '#f59e0b', // amber - '#8b5cf6', // purple - '#06b6d4', // cyan - '#ef4444', // red - '#84cc16', // lime + '#3b82f6', // azul + '#ec4899', // rosa + '#10b981', // esmeralda + '#f59e0b', // ambar + '#8b5cf6', // roxo + '#06b6d4', // ciano + '#ef4444', // vermelho + '#84cc16', // lima '#6366f1', // indigo '#14b8a6', // teal - '#f97316', // orange - '#eab308', // yellow + '#f97316', // laranja + '#eab308', // amarelo ]; +// Helper: Valida se um valor extraído é uma marca útil real +export function isValidMark(val: any): boolean { + if (val === null || val === undefined) return false; + const s = String(val).trim(); + if (s.length === 0) return false; + const lower = s.toLowerCase(); + if ( + lower === '-' || + lower === '?' || + lower === 'indefinido' || + lower === 'undefined' || + lower === 'null' || + lower === 'none' || + lower === 'default' || + lower === 'n/a' || + lower === 'na' || + lower === '$' + ) { + return false; + } + return true; +} + +// Helper: Extrai campos formatados comuns em Description (ex: "Mark:TEST10 Pos:TEST10 Material:A36") +export function parseDescriptionFields(desc: string): Record { + const result: Record = {}; + if (!desc || typeof desc !== 'string') return result; + + const markMatch = desc.match(/(?:Mark|Marca|Part|Piece)[:\s=]*([^\s,;]+)/i); + if (markMatch && isValidMark(markMatch[1])) result.mark = markMatch[1].trim(); + + const posMatch = desc.match(/(?:Pos|Posição|Posicao)[:\s=]*([^\s,;]+)/i); + if (posMatch && isValidMark(posMatch[1])) result.pos = posMatch[1].trim(); + + const matMatch = desc.match(/(?:Material|Mat)[:\s=]*([^\s,;]+)/i); + if (matMatch && isValidMark(matMatch[1])) result.material = matMatch[1].trim(); + + return result; +} + +// Helper: Extrai o valor limpo da marca (ex: "B135-1-4" -> "4", "Pos: 4" -> "4") +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(); + } + + return cleaned; +} + export async function loadAndAuditIFC( fileOrBuffer: File | ArrayBuffer, onProgress?: (percent: number, step: string) => void ): Promise { const ifcApi = new WebIFC.IfcAPI(); - // 1. Initialize Web-IFC WASM locally with fallback + // 1. Inicializa Web-IFC WASM com fallback if (onProgress) onProgress(10, 'Carregando motor Web-IFC (WASM)...'); const LOCAL_WASM_PATH = '/wasm/'; @@ -115,7 +177,7 @@ export async function loadAndAuditIFC( throw new Error('Não foi possível inicializar o motor Web-IFC WebAssembly.'); } - // 2. Read bytes & Raw String for Header + // 2. Leitura dos dados binários e cabeçalho if (onProgress) onProgress(25, 'Lendo arquivo IFC...'); let data: Uint8Array; let rawHeader = ''; @@ -130,7 +192,7 @@ export async function loadAndAuditIFC( rawHeader = decoder.decode(data.slice(0, 4096)); } - // 3. Open Model + // 3. Abertura do Modelo com suporte a coordenadas relativas if (onProgress) onProgress(40, 'Decodificando entidades IFC...'); const modelID = ifcApi.OpenModel(data, { COORDINATE_TO_ORIGIN: true, @@ -140,10 +202,10 @@ export async function loadAndAuditIFC( throw new Error('Falha ao decodificar a estrutura do arquivo IFC.'); } - // 4. Header Detection + // 4. Detecção de Header const isEM11 = rawHeader.includes('SteelFabricationView') || rawHeader.includes('EM.11'); - // Collect Elements + // Coleta de entidades estruturais if (onProgress) onProgress(50, 'Mapeando topologia estrutural e soldas...'); const assemblies = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCELEMENTASSEMBLY); const beams = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCBEAM); @@ -160,9 +222,10 @@ export async function loadAndAuditIFC( const totalFasteners = fasteners.size(); const totalStructural = totalBeams + totalColumns + totalMembers + totalPlates; - // 4.1 Native Aggregates mapping (IFCRELAGGREGATES) + // 4.1 Mapeamento nativo de montagens (IFCRELAGGREGATES) const aggregates = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELAGGREGATES); const assemblyToChildren = new Map(); + const childToAssembly = new Map(); for (let i = 0; i < aggregates.size(); i++) { const relID = aggregates.get(i); @@ -172,14 +235,16 @@ export async function loadAndAuditIFC( const parentID = rel.RelatingObject.value; const childIDs = (rel.RelatedObjects as any[]).map((r: any) => r.value); assemblyToChildren.set(parentID, childIDs); + for (const cId of childIDs) { + childToAssembly.set(cId, parentID); + } } } catch { // ignore } } - // 4.2 FALLBACK: Topologic Weld Graph (IFCRELCONNECTSWITHREALIZINGELEMENTS) - // Connects parts (Plates <-> Columns <-> Beams) when IfcElementAssembly is absent + // 4.2 Grafo de Soldas (IFCRELCONNECTSWITHREALIZINGELEMENTS) const weldConnections = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELCONNECTSWITHREALIZINGELEMENTS); const adjacencyList = new Map>(); @@ -204,7 +269,7 @@ export async function loadAndAuditIFC( } } - // Compute Connected Components for Welded Assemblies (DSU/BFS) + // Componentes conectados soldados const visited = new Set(); const weldedGroupMap = new Map(); let totalWeldedGroups = 0; @@ -234,7 +299,7 @@ export async function loadAndAuditIFC( } } - // 4.3 Element Properties Map from IFCRELDEFINESBYPROPERTIES + // 4.3 Indexação profunda de propriedades (IFCRELDEFINESBYPROPERTIES) const elementPropertiesMap = new Map>(); try { const relsProp = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELDEFINESBYPROPERTIES); @@ -246,30 +311,60 @@ export async function loadAndAuditIFC( const propDefId = rel.RelatingPropertyDefinition.value; if (propDefId && typeof propDefId === 'number' && propDefId > 0) { const propDef = ifcApi.GetLine(modelID, propDefId); - if (propDef && propDef.HasProperties) { + if (propDef) { const props: Record = {}; - for (const pRef of propDef.HasProperties) { - const pId = pRef?.value; - if (pId && typeof pId === 'number' && pId > 0) { - const pLine = ifcApi.GetLine(modelID, pId); - if (pLine && pLine.Name) { - const name = pLine.Name.value; - let value = ''; - if (pLine.NominalValue) { - value = String(pLine.NominalValue.value); - } - if (name && value) { - props[name] = value; + + // Leitura de HasProperties (Pset_*) + if (propDef.HasProperties && Array.isArray(propDef.HasProperties)) { + for (const pRef of propDef.HasProperties) { + const pId = pRef?.value; + if (pId && typeof pId === 'number' && pId > 0) { + const pLine = ifcApi.GetLine(modelID, pId); + if (pLine && pLine.Name) { + const name = pLine.Name.value || pLine.Name; + let value = ''; + if (pLine.NominalValue !== undefined && pLine.NominalValue !== null) { + if (typeof pLine.NominalValue === 'object' && pLine.NominalValue.value !== undefined) { + value = String(pLine.NominalValue.value); + } else { + value = String(pLine.NominalValue); + } + } + if (name && value && isValidMark(value)) { + props[name] = value; + } } } } } - const relatedObjs = Array.isArray(rel.RelatedObjects) ? rel.RelatedObjects : [rel.RelatedObjects]; - for (const objRef of relatedObjs) { - const objId = objRef?.value || objRef; - if (objId && typeof objId === 'number') { - const existing = elementPropertiesMap.get(objId) || {}; - elementPropertiesMap.set(objId, { ...existing, ...props }); + + // Leitura de Quantities (Qto_*) + if (propDef.Quantities && Array.isArray(propDef.Quantities)) { + for (const qRef of propDef.Quantities) { + const qId = qRef?.value; + if (qId && typeof qId === 'number' && qId > 0) { + const qLine = ifcApi.GetLine(modelID, qId); + if (qLine && qLine.Name) { + const name = qLine.Name.value || qLine.Name; + let value = ''; + if (qLine.LengthValue !== undefined) value = `${Number(qLine.LengthValue).toFixed(1)} mm`; + else if (qLine.NominalValue) value = String(qLine.NominalValue.value || qLine.NominalValue); + if (name && value && isValidMark(value)) { + props[name] = value; + } + } + } + } + } + + if (Object.keys(props).length > 0) { + const relatedObjs = Array.isArray(rel.RelatedObjects) ? rel.RelatedObjects : [rel.RelatedObjects]; + for (const objRef of relatedObjs) { + const objId = objRef?.value || objRef; + if (objId && typeof objId === 'number') { + const existing = elementPropertiesMap.get(objId) || {}; + elementPropertiesMap.set(objId, { ...existing, ...props }); + } } } } @@ -281,40 +376,143 @@ export async function loadAndAuditIFC( console.debug('[IFC Loader] Property indexing note:', propErr); } - // Collect piece marks & properties + // Função interna de extração inteligente da marca + function extractBestMark(expressID: number): { + mark: string; + cleanMark: string; + section: string; + description: string; + } { + const props = elementPropertiesMap.get(expressID) || {}; + + let rawName = ''; + let rawTag = ''; + let rawDesc = ''; + let rawObjType = ''; + try { + const rawLine = ifcApi.GetLine(modelID, expressID); + if (rawLine) { + rawName = rawLine.Name?.value ?? (typeof rawLine.Name === 'string' ? rawLine.Name : ''); + rawTag = rawLine.Tag?.value ?? (typeof rawLine.Tag === 'string' ? rawLine.Tag : ''); + rawDesc = rawLine.Description?.value ?? (typeof rawLine.Description === 'string' ? rawLine.Description : ''); + rawObjType = rawLine.ObjectType?.value ?? (typeof rawLine.ObjectType === 'string' ? rawLine.ObjectType : ''); + } + } catch {} + + const descParsed = parseDescriptionFields(rawDesc); + + // Prioridade de propriedades que representam Marca/Posição da peça + const candidates: (string | undefined)[] = [ + props.PieceMark, + props.PartMark, + props.SinglePartMark, + props.MainPartMark, + props.Marca, + props.Pos, + props.Posição, + props.Posicao, + descParsed.pos, + descParsed.mark, + props.Mark, + props['Part Number'], + props.PartPos, + props['Item Number'], + props.ItemNumber, + props.PartNumber, + rawTag, + props.Tag, + props.Reference, // Validado por isValidMark + rawObjType, + rawName + ]; + + let chosenMark = ''; + for (const cand of candidates) { + if (cand && isValidMark(cand)) { + chosenMark = String(cand).trim(); + break; + } + } + + // 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) || {}; + try { + const parentLine = ifcApi.GetLine(modelID, parentId); + const parentName = parentLine?.Name?.value ?? ''; + const parentTag = parentLine?.Tag?.value ?? ''; + const parentCandidates = [ + parentProps.PieceMark, + parentProps.AssemblyMark, + parentProps['Assembly Mark'], + parentProps.Marca, + parentProps.Mark, + parentProps.Pos, + parentTag, + parentName + ]; + for (const pCand of parentCandidates) { + if (pCand && isValidMark(pCand)) { + chosenMark = String(pCand).trim(); + break; + } + } + } catch {} + } + + const section = + props.Section || + props.Profile || + props.Perfil || + props.Description || + rawDesc || + rawName || + ''; + + const clean = getCleanMarkValue(chosenMark); + + return { + mark: chosenMark || 'indefinido', + cleanMark: clean || chosenMark || 'indefinido', + section, + description: rawDesc + }; + } + + // Coleta de montagens nativas const piecesByMark = new Map(); const pieceByExpressID = new Map(); const uniqueMarksSet = new Set(); const sectionColorMap = new Map(); let colorIndex = 0; - // Process Native Assemblies if present for (let i = 0; i < assemblies.size(); i++) { const assID = assemblies.get(i); try { const assObj = ifcApi.GetLine(modelID, assID); - const props = elementPropertiesMap.get(assID) || {}; - const rawProps = getSyncProperties(ifcApi, modelID, assID); - const pmark = props.PieceMark || props.Reference || props.Mark || props.Tag || rawProps.Name || 'indefinido'; - + const markInfo = extractBestMark(assID); const children = assemblyToChildren.get(assID) || []; + const piece: PieceInfo = { expressID: assID, guid: assObj?.GlobalId?.value || '', name: assObj?.Name?.value || 'Montagem Estrutural', type: 'IFCELEMENTASSEMBLY', - pieceMark: pmark, + pieceMark: markInfo.mark, + cleanMark: markInfo.cleanMark, childrenIDs: children, connectedMeshIDs: children, - section: props.Section || props.Profile, - phase: props.Phase || extractPhaseFromMark(pmark), - ofNumber: extractOFFromMark(pmark) + section: markInfo.section, + phase: extractPhaseFromMark(markInfo.mark), + ofNumber: extractOFFromMark(markInfo.mark) }; - if (pmark !== 'indefinido') { - uniqueMarksSet.add(pmark); - if (!piecesByMark.has(pmark)) piecesByMark.set(pmark, []); - piecesByMark.get(pmark)!.push(piece); + if (markInfo.mark !== 'indefinido') { + uniqueMarksSet.add(markInfo.mark); + if (markInfo.cleanMark !== 'indefinido') uniqueMarksSet.add(markInfo.cleanMark); + if (!piecesByMark.has(markInfo.mark)) piecesByMark.set(markInfo.mark, []); + piecesByMark.get(markInfo.mark)!.push(piece); } pieceByExpressID.set(assID, piece); } catch { @@ -322,34 +520,28 @@ export async function loadAndAuditIFC( } } - // 5. Generate 3D Meshes with Three.js + // 5. Geração de Malhas 3D com Three.js corrigindo o empacotamento de vértices/normais 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.35, - roughness: 0.55, + metalness: 0.3, + roughness: 0.6, side: THREE.DoubleSide }); const materialCache = new Map(); - let totalMeshes = 0; ifcApi.StreamAllMeshes(modelID, (flatMesh: WebIFC.FlatMesh) => { const expressID = flatMesh.expressID; const placedGeometries = flatMesh.geometries; - // Check properties for this element - const props = elementPropertiesMap.get(expressID) || {}; - const rawProps = getSyncProperties(ifcApi, modelID, expressID); - const pmark = props.PieceMark || props.Reference || props.Mark || props.Tag || rawProps.Name || ''; - const section = props.Section || props.Profile || props.Description || rawProps.Name || ''; + const markInfo = extractBestMark(expressID); + const colorKey = markInfo.section || 'PADRAO'; - // Determine section color - const colorKey = section || 'PADRAO'; if (!sectionColorMap.has(colorKey)) { sectionColorMap.set(colorKey, DESCRIPTION_PALETTE[colorIndex % DESCRIPTION_PALETTE.length]); colorIndex++; @@ -361,61 +553,78 @@ export async function loadAndAuditIFC( hexColor, new THREE.MeshStandardMaterial({ color: new THREE.Color(hexColor), - metalness: 0.4, - roughness: 0.5, + metalness: 0.35, + roughness: 0.55, side: THREE.DoubleSide }) ); } const mat = materialCache.get(hexColor) || defaultGrayMat; - - // Get welded connected peers (Fallback topology) 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 positions = ifcApi.GetVertexArray(ifcGeo.GetVertexData(), ifcGeo.GetVertexDataSize()); + // 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()); const bufferGeo = new THREE.BufferGeometry(); - bufferGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3)); + const numVerts = verts.length / 6; + const positionArray = new Float32Array(numVerts * 3); + const normalArray = new Float32Array(numVerts * 3); + + for (let j = 0; j < verts.length; j += 6) { + const idx = j / 6; + positionArray[idx * 3] = verts[j]; + positionArray[idx * 3 + 1] = verts[j + 1]; + positionArray[idx * 3 + 2] = verts[j + 2]; + normalArray[idx * 3] = verts[j + 3]; + normalArray[idx * 3 + 1] = verts[j + 4]; + 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)); - bufferGeo.computeVertexNormals(); const mesh = new THREE.Mesh(bufferGeo, mat); - mesh.matrixAutoUpdate = false; - mesh.matrix.fromArray(placedGeo.flatTransformation); - mesh.matrixWorldNeedsUpdate = true; + const matrix = new THREE.Matrix4(); + matrix.fromArray(placedGeo.flatTransformation); + mesh.applyMatrix4(matrix); + mesh.userData = { expressID, - pieceMark: pmark, - section, + pieceMark: markInfo.mark, + cleanMark: markInfo.cleanMark, + section: markInfo.section, colorKey, connectedMeshIDs: connectedGroup }; sceneGroup.add(mesh); + ifcGeo.delete(); totalMeshes++; - // Register piece in lookup map if (!pieceByExpressID.has(expressID)) { const piece: PieceInfo = { expressID, guid: '', - name: section || 'Peça Estrutural', + name: markInfo.section || 'Peça Estrutural', type: 'ELEMENT', - pieceMark: pmark, - section, + pieceMark: markInfo.mark, + cleanMark: markInfo.cleanMark, + section: markInfo.section, connectedMeshIDs: connectedGroup, mesh }; pieceByExpressID.set(expressID, piece); - if (pmark && pmark !== 'indefinido') { - uniqueMarksSet.add(pmark); - if (!piecesByMark.has(pmark)) piecesByMark.set(pmark, []); - piecesByMark.get(pmark)!.push(piece); + if (markInfo.mark && markInfo.mark !== 'indefinido') { + uniqueMarksSet.add(markInfo.mark); + if (markInfo.cleanMark !== 'indefinido') uniqueMarksSet.add(markInfo.cleanMark); + if (!piecesByMark.has(markInfo.mark)) piecesByMark.set(markInfo.mark, []); + piecesByMark.get(markInfo.mark)!.push(piece); } } else { pieceByExpressID.get(expressID)!.mesh = mesh; @@ -424,7 +633,12 @@ export async function loadAndAuditIFC( } }); - // Calculate Quality Audit & Fallback Status + // Fecha o modelo após extração para liberar memória + try { + ifcApi.CloseModel(modelID); + } catch {} + + // Auditoria de Qualidade let qualityScore: 'EXCELENTE' | 'BOM' | 'ATENÇÃO' = 'ATENÇÃO'; let qualityMessage = ''; let recommendation = ''; @@ -435,8 +649,8 @@ 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 Fallback Ativado: ${totalWeldedGroups} conjuntos soldados identificados por conectividade topológica (IFCRELCONNECTS). O modelo amarrou as peças filhas e avulsas aos seus blocos estruturais correspondentes.`; - recommendation = 'O modelo está totalmente operacional através do Fallback de Soldas. Para máxima compatibilidade nativa, utilize a opção "IFC MDV EM.11" nas exportações futuras do Advance Steel.'; + qualityMessage = `Modo Avançado Ativado: ${uniqueMarksSet.size} marcas de peças e ${totalWeldedGroups} conjuntos topológicos identificados com sucesso.`; + recommendation = 'O modelo está totalmente operacional e sincronizado com os apontamentos da fábrica.'; } else { qualityScore = 'ATENÇÃO'; qualityMessage = 'Não foram identificadas marcas de peças no padrão da fábrica nas propriedades do IFC.'; @@ -476,19 +690,6 @@ export async function loadAndAuditIFC( }; } -// Helpers - -function getSyncProperties(ifcApi: WebIFC.IfcAPI, modelID: number, expressID: number): Record { - const result: Record = {}; - try { - const raw = ifcApi.GetLine(modelID, expressID); - if (raw && raw.Name && raw.Name.value) { - result.Name = raw.Name.value; - } - } catch {} - return result; -} - function extractPhaseFromMark(mark: string): string { const parts = mark.split('-'); if (parts.length >= 3) return parts[1]; diff --git a/src/pages/Visualizador3D.tsx b/src/pages/Visualizador3D.tsx index 674d056..b1f9962 100644 --- a/src/pages/Visualizador3D.tsx +++ b/src/pages/Visualizador3D.tsx @@ -114,9 +114,9 @@ export default function Visualizador3D() { console.error("Erro ao buscar apontamentos no visualizador:", errorApontamentos); } - // Build phases list const phases = new Set(); const prodMap = new Map(); + const uniqueItems: any[] = []; let totalPecas = 0; let totalPeso = 0; @@ -127,17 +127,29 @@ export default function Visualizador3D() { totalPecas += Number(p.quantidade || 0); totalPeso += Number(p.peso_total || 0); - const key = p.marca; - prodMap.set(key, { + const marcaStr = String(p.marca || '').trim(); + const item = { pecaId: p.id, - marca: p.marca, + marca: marcaStr, fase, + ofNumber: selectedOF, totalQtd: Number(p.quantidade || 1), pointedQtd: 0, currentProcessName: 'Pendente', processColor: '#64748b', processOrdem: 0 - }); + }; + + uniqueItems.push(item); + + // Indexa por marca simples (ex: "4") + prodMap.set(marcaStr, item); + // Indexa por fase e marca (ex: "2-4") + prodMap.set(`${fase}-${marcaStr}`, item); + // Indexa por OF, fase e marca (ex: "B135-2-4") + prodMap.set(`${selectedOF}-${fase}-${marcaStr}`, item); + // Indexa por OF e marca (ex: "B135-4") + prodMap.set(`${selectedOF}-${marcaStr}`, item); }); } @@ -146,15 +158,19 @@ export default function Visualizador3D() { apontamentosData.sort((a: any, b: any) => (a.processo?.ordem || 0) - (b.processo?.ordem || 0)); apontamentosData.forEach((ap: any) => { - const marca = ap.peca?.marca; + const marca = ap.peca?.marca ? String(ap.peca.marca).trim() : ''; + const fase = ap.peca?.etapa_fase ? String(ap.peca.etapa_fase).trim() : ''; if (marca) { - const key = marca; - const existing = prodMap.get(key); + const existing = + prodMap.get(`${selectedOF}-${fase}-${marca}`) || + prodMap.get(`${fase}-${marca}`) || + prodMap.get(marca); + if (existing) { const qty = Number(ap.quantidade_produzida || 0); if (qty > 0) { // Atualiza a peça para o estágio mais avançado alcançado - existing.pointedQtd = Math.max(existing.pointedQtd, qty); // Evita duplicar apontamentos de processos anteriores + existing.pointedQtd = Math.max(existing.pointedQtd, qty); existing.currentProcessName = ap.processo?.nome || existing.currentProcessName; existing.processColor = ap.processo?.cor || PROCESS_COLORS[ap.processo?.nome || ''] || '#10b981'; existing.processOrdem = ap.processo?.ordem || existing.processOrdem; @@ -167,9 +183,10 @@ export default function Visualizador3D() { let pointedTotal = 0; let sumProgress = 0; // Para evolução da OF - prodMap.forEach((val) => { + // Itera sobre uniqueItems para não duplicar métricas com os múltiplos índices de busca + uniqueItems.forEach((val) => { pointedTotal += val.pointedQtd; - // Considerando maxOrdem = 5 como final para cálculo de %. Ajuste conforme necessário. + // Considerando maxOrdem = 5 como final para cálculo de %. const pecaProgress = val.pointedQtd > 0 ? Math.min(val.processOrdem / 5, 1) * (val.pointedQtd / val.totalQtd) : 0; sumProgress += pecaProgress * val.totalQtd; });