From fdec79f81b397d6d3aa225b7ab82b89cd8eb954b Mon Sep 17 00:00:00 2001 From: admbrainsteel Date: Thu, 15 Oct 2026 10:02:49 -0300 Subject: [PATCH] feat(3d-viewer): replica selecao e hover para modelos WRL com destaque magenta e apontamentos --- src/components/viewer3d/ModelViewer3D.tsx | 192 +++++++++++++----- .../viewer3d/SmartModelViewer3D.tsx | 17 +- src/lib/3d/wrlLoaderService.ts | 7 +- 3 files changed, 161 insertions(+), 55 deletions(-) diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index d0322cb..6495097 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -20,6 +20,53 @@ interface ProductionPieceStatus { processOrdem?: number; } +/** + * Busca de apontamento e status fabril com máxima tolerância e amostragem de chaves para IFC e WRL. + */ +function findProductionDataForPiece( + productionData?: Map, + piece?: { + pieceMark?: string; + cleanMark?: string; + assemblyMark?: string; + cleanAssemblyMark?: string; + phase?: string; + } | null, + selectedOF?: string +): ProductionPieceStatus | undefined { + if (!productionData || !piece) return undefined; + + const pmark = String(piece.pieceMark || '').trim().toUpperCase(); + const cleanMark = String(piece.cleanMark || pmark).trim().toUpperCase(); + const assMark = String(piece.assemblyMark || '').trim().toUpperCase(); + const cleanAssMark = String(piece.cleanAssemblyMark || cleanMark).trim().toUpperCase(); + const elementPhase = piece.phase ? String(piece.phase).trim() : ''; + + const searchKeys = [ + elementPhase && selectedOF ? `${selectedOF}-${elementPhase}-${cleanAssMark}` : '', + elementPhase ? `${elementPhase}-${cleanAssMark}` : '', + elementPhase && selectedOF ? `${selectedOF}-${elementPhase}-${cleanMark}` : '', + elementPhase ? `${elementPhase}-${cleanMark}` : '', + selectedOF ? `${selectedOF}-${cleanAssMark}` : '', + selectedOF ? `${selectedOF}-${cleanMark}` : '', + assMark, + pmark, + cleanAssMark, + cleanMark, + ].filter(Boolean).map((k) => k.toUpperCase()); + + for (const key of searchKeys) { + const match = productionData.get(key); + if (match) { + if (!elementPhase || !match.fase || String(match.fase) === String(elementPhase)) { + return match; + } + } + } + + return undefined; +} + interface ModelViewer3DProps { modelData: LoadedIFCResult | null; productionData?: Map; @@ -721,7 +768,7 @@ export const ModelViewer3D: React.FC = ({ } // Se a peça física pertencer à seleção permanente, preserva a cor magenta - const ifcId = mesh.userData.ifcId; + const ifcId = mesh.userData.ifcId ?? mesh.userData.expressID; if (ifcId !== undefined && selectedElementsMapRef.current.has(ifcId)) { mesh.userData.originalMaterial = mesh.material; mesh.material = magentaMaterialRef.current; @@ -889,17 +936,21 @@ export const ModelViewer3D: React.FC = ({ } if (hitMesh) { - const expressID = hitMesh.userData.ifcId; + const expressID = hitMesh.userData.ifcId ?? hitMesh.userData.expressID; const pmark = hitMesh.userData.pieceMark; if (modelData && expressID !== undefined) { const piece = modelData.pieceByExpressID.get(expressID) || { expressID, - guid: '', - name: hitMesh.userData.section || 'Peça', - type: 'PIECE', + guid: hitMesh.userData.guid || '', + name: hitMesh.userData.name || hitMesh.userData.section || 'Peça', + type: hitMesh.userData.type || 'PIECE', pieceMark: pmark, - section: hitMesh.userData.section, + cleanMark: hitMesh.userData.cleanMark, + assemblyMark: hitMesh.userData.assemblyMark, + cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark, + section: hitMesh.userData.section || hitMesh.userData.name, + phase: hitMesh.userData.phase, }; setHoveredPiece(piece); setHoverPosition({ x: clientX, y: clientY }); @@ -956,7 +1007,7 @@ export const ModelViewer3D: React.FC = ({ } if (hitMesh && hitMesh.visible) { - const targetIfcId = hitMesh.userData.ifcId; + const targetIfcId = hitMesh.userData.ifcId ?? hitMesh.userData.expressID; if (targetIfcId === undefined) return; const nextMap = new Map(selectedElementsMapRef.current); @@ -972,16 +1023,19 @@ export const ModelViewer3D: React.FC = ({ }); nextMap.delete(targetIfcId); } else { - // Marcar peça física inteira: localiza e pinta todas as sub-malhas do mesmo ifcId + // Marcar peça física inteira: localiza e pinta todas as sub-malhas do mesmo targetIfcId const relatedMeshes: THREE.Mesh[] = []; if (modelGroupRef.current) { modelGroupRef.current.traverse((child) => { - if ((child as THREE.Mesh).isMesh && child.userData.ifcId === targetIfcId) { - const m = child as THREE.Mesh; - m.userData.originalMaterial = m.material; - m.userData.isMagentaSelected = true; - m.material = magentaMaterialRef.current; - relatedMeshes.push(m); + if ((child as THREE.Mesh).isMesh) { + const childId = child.userData.ifcId ?? child.userData.expressID; + if (childId === targetIfcId) { + const m = child as THREE.Mesh; + m.userData.originalMaterial = m.material; + m.userData.isMagentaSelected = true; + m.material = magentaMaterialRef.current; + relatedMeshes.push(m); + } } }); } @@ -989,10 +1043,13 @@ export const ModelViewer3D: React.FC = ({ const pmark = hitMesh.userData.pieceMark; const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(targetIfcId)) || { expressID: targetIfcId, - guid: '', - name: hitMesh.userData.section || 'Peça', - type: 'PIECE', + 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, }; @@ -1018,8 +1075,32 @@ export const ModelViewer3D: React.FC = ({ const mouseX = ((e.clientX - rect.left) / rect.width) * 2 - 1; const mouseY = -((e.clientY - rect.top) / rect.height) * 2 + 1; - raycasterRef.current.setFromCamera(new THREE.Vector2(mouseX, mouseY), activeCameraRef.current); - const intersects = raycasterRef.current.intersectObjects(modelGroupRef.current.children, true); + const raycaster = raycasterRef.current; + raycaster.params.Line = { threshold: 4.0 }; + raycaster.params.Points = { threshold: 4.0 }; + raycaster.setFromCamera(new THREE.Vector2(mouseX, mouseY), activeCameraRef.current); + let intersects = raycaster.intersectObjects(modelGroupRef.current.children, true); + + // Amostragem circular magnética de tolerância para peças esbeltas/finas + if (intersects.length === 0) { + const radiusOffsetsPx = [6, 12, 18]; + const angles = [0, Math.PI / 4, Math.PI / 2, (3 * Math.PI) / 4, Math.PI, (5 * Math.PI) / 4, (3 * Math.PI) / 2, (7 * Math.PI) / 4]; + + for (const rPx of radiusOffsetsPx) { + if (intersects.length > 0) break; + for (const angle of angles) { + const dx = ((rPx * Math.cos(angle)) / rect.width) * 2; + const dy = -((rPx * Math.sin(angle)) / rect.height) * 2; + const samplePos = new THREE.Vector2(mouseX + dx, mouseY + dy); + raycaster.setFromCamera(samplePos, activeCameraRef.current); + const sampleIntersects = raycaster.intersectObjects(modelGroupRef.current.children, true); + if (sampleIntersects.length > 0) { + intersects = sampleIntersects; + break; + } + } + } + } if (intersects.length > 0) { const hitPoint = intersects[0].point; @@ -1033,36 +1114,49 @@ export const ModelViewer3D: React.FC = ({ if (hitMesh) { const targetPieceMark = String(hitMesh.userData.pieceMark || '').trim().toUpperCase(); + const targetCleanMark = String(hitMesh.userData.cleanMark || targetPieceMark).trim().toUpperCase(); + const targetAssemblyMark = String(hitMesh.userData.assemblyMark || '').trim().toUpperCase(); + const targetCleanAssMark = String(hitMesh.userData.cleanAssemblyMark || targetCleanMark).trim().toUpperCase(); const targetPhase = hitMesh.userData.phase ? String(hitMesh.userData.phase).trim() : ''; - if (targetPieceMark) { + if (targetPieceMark || targetCleanMark) { const nextMap = new Map(selectedElementsMapRef.current); - // Agrupa todas as sub-malhas por elemento físico (ifcId único) - const meshesByIfcId = new Map(); + // Agrupa todas as sub-malhas por elemento físico único (ifcId ?? expressID) + const meshesByElementId = new Map(); modelGroupRef.current.traverse((child) => { if ((child as THREE.Mesh).isMesh && child.visible) { const mesh = child as THREE.Mesh; const pmark = String(mesh.userData.pieceMark || '').trim().toUpperCase(); + const cmark = String(mesh.userData.cleanMark || pmark).trim().toUpperCase(); + const amark = String(mesh.userData.assemblyMark || '').trim().toUpperCase(); + const cleanAm = String(mesh.userData.cleanAssemblyMark || cmark).trim().toUpperCase(); const phase = mesh.userData.phase ? String(mesh.userData.phase).trim() : ''; - // Mesma marca e mesma fase - const isMatch = pmark === targetPieceMark && (!targetPhase || !phase || phase === targetPhase); - if (isMatch) { - const ifcId = mesh.userData.ifcId; - if (ifcId !== undefined) { - if (!meshesByIfcId.has(ifcId)) { - meshesByIfcId.set(ifcId, { meshes: [], sampleMesh: mesh }); + // Mesma marca (peça, conjunto ou limpa) e mesma fase (se especificada) + const isMarkMatch = + (pmark && pmark === targetPieceMark) || + (cmark && cmark === targetCleanMark) || + (targetAssemblyMark && amark === targetAssemblyMark) || + (targetCleanAssMark && cleanAm === targetCleanAssMark); + + const isPhaseMatch = !targetPhase || !phase || phase === targetPhase; + + if (isMarkMatch && isPhaseMatch) { + const elId = mesh.userData.ifcId ?? mesh.userData.expressID; + if (elId !== undefined) { + if (!meshesByElementId.has(elId)) { + meshesByElementId.set(elId, { meshes: [], sampleMesh: mesh }); } - meshesByIfcId.get(ifcId)!.meshes.push(mesh); + meshesByElementId.get(elId)!.meshes.push(mesh); } } } }); // Pinta e adiciona cada peça física real como 1 unidade - meshesByIfcId.forEach(({ meshes, sampleMesh }, ifcId) => { + meshesByElementId.forEach(({ meshes, sampleMesh }, elId) => { meshes.forEach((m) => { if (!m.userData.isMagentaSelected) { m.userData.originalMaterial = m.material; @@ -1071,18 +1165,21 @@ export const ModelViewer3D: React.FC = ({ m.material = magentaMaterialRef.current; }); - const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(ifcId)) || { - expressID: ifcId, - guid: '', - name: sampleMesh.userData.section || 'Peça', - type: 'PIECE', + const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(elId)) || { + 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, }; - nextMap.set(ifcId, { - expressID: ifcId, + nextMap.set(elId, { + expressID: elId, piece, meshes, }); @@ -1118,7 +1215,7 @@ export const ModelViewer3D: React.FC = ({ const selectedPiecesList: SelectedPieceItem[] = Array.from(selectedElementsMap.values()).map( ({ expressID, piece }) => { const pmark = piece.pieceMark || 'Sem Marca'; - const prod = productionData?.get(pmark); + const prod = findProductionDataForPiece(productionData, piece, selectedOF); return { id: String(expressID), pieceMark: pmark, @@ -1338,16 +1435,17 @@ export const ModelViewer3D: React.FC = ({ { + const prod = findProductionDataForPiece(productionData, hoveredPiece, selectedOF); + return prod ? { - pointedQty: productionData.get(hoveredPiece.pieceMark)?.pointedQtd, - totalQty: productionData.get(hoveredPiece.pieceMark)?.totalQtd, - currentStage: productionData.get(hoveredPiece.pieceMark)?.currentProcessName, - stageColor: productionData.get(hoveredPiece.pieceMark)?.processColor, + pointedQty: prod.pointedQtd, + totalQty: prod.totalQtd, + currentStage: prod.currentProcessName, + stageColor: prod.processColor, } - : undefined - } + : undefined; + })()} /> {/* Modal de Lista e Exportação da Seleção 3D */} diff --git a/src/components/viewer3d/SmartModelViewer3D.tsx b/src/components/viewer3d/SmartModelViewer3D.tsx index 56eb922..22abd1d 100644 --- a/src/components/viewer3d/SmartModelViewer3D.tsx +++ b/src/components/viewer3d/SmartModelViewer3D.tsx @@ -557,7 +557,7 @@ export const SmartModelViewer3D: React.FC = ({ } // 4.3. Preserva o destaque magenta se a peça estiver selecionada pelo operador - const ifcId = mesh.userData.ifcId; + const ifcId = mesh.userData.ifcId ?? mesh.userData.expressID; if (ifcId !== undefined && selectedElementsMapRef.current.has(ifcId)) { mesh.userData.originalMaterial = mesh.material; mesh.material = magentaMaterialRef.current; @@ -620,7 +620,7 @@ export const SmartModelViewer3D: React.FC = ({ else if (hitObj.parent && (hitObj.parent as THREE.Mesh).isMesh) hitMesh = hitObj.parent as THREE.Mesh; if (hitMesh && hitMesh.visible) { - const targetIfcId = hitMesh.userData.ifcId; + const targetIfcId = hitMesh.userData.ifcId ?? hitMesh.userData.expressID; if (targetIfcId === undefined) return; const nextMap = new Map(selectedElementsMapRef.current); @@ -644,11 +644,14 @@ export const SmartModelViewer3D: React.FC = ({ // Marca a peça física inteira em Magenta permanente const relatedMeshes: THREE.Mesh[] = []; modelGroupRef.current.traverse((child) => { - if ((child as THREE.Mesh).isMesh && child.userData.ifcId === targetIfcId) { - const m = child as THREE.Mesh; - m.userData.originalMaterial = m.material; - m.material = magentaMaterialRef.current; - relatedMeshes.push(m); + if ((child as THREE.Mesh).isMesh) { + const childId = child.userData.ifcId ?? child.userData.expressID; + if (childId === targetIfcId) { + const m = child as THREE.Mesh; + m.userData.originalMaterial = m.material; + m.material = magentaMaterialRef.current; + relatedMeshes.push(m); + } } }); diff --git a/src/lib/3d/wrlLoaderService.ts b/src/lib/3d/wrlLoaderService.ts index 36a1757..3a6ebe9 100644 --- a/src/lib/3d/wrlLoaderService.ts +++ b/src/lib/3d/wrlLoaderService.ts @@ -503,6 +503,8 @@ export async function loadAndAuditWRL( } mesh.material = material; + const pieceSection = parsed.cleanMark || parsed.pieceMark || 'Perfil'; + const pieceInfo: PieceInfo = { expressID, guid, @@ -512,18 +514,21 @@ export async function loadAndAuditWRL( cleanMark: parsed.cleanMark, assemblyMark: parsed.assemblyMark, cleanAssemblyMark: parsed.cleanAssemblyMark, + section: pieceSection, phase: parsed.phase, mesh, }; - // Injeta metadados essenciais no userData do Mesh + // Injeta metadados essenciais no userData do Mesh (com total compatibilidade IFC / Three.js) mesh.userData = { + 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, + section: pieceSection, phase: parsed.phase, type: parsed.detectedType, name: rawNodeName,