From 0249f6cc33a7bf0fe73b6ad93aeea115f6bb4aa4 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Thu, 24 Sep 2026 15:13:59 +0000 Subject: [PATCH] feat: apuracao de fases WRL e controle de rotacao com trava para OF --- src/components/viewer3d/ModelViewer3D.tsx | 187 +++++++++++++++++++-- src/lib/3d/wrlLoaderService.ts | 194 ++++++++++++++-------- src/pages/Visualizador3D.tsx | 29 +++- 3 files changed, 312 insertions(+), 98 deletions(-) diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index 89d5cae..905c71c 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -6,7 +6,9 @@ import { ViewCube } from './ViewCube'; import { Viewer3DSidebar } from './Viewer3DSidebar'; import { PieceHoverTooltip } from './PieceHoverTooltip'; import { SelectionListModal, SelectedPieceItem } from './SelectionListModal'; -import { Trash2, FileSpreadsheet, RotateCcw } from 'lucide-react'; +import { Trash2, FileSpreadsheet, RotateCcw, RotateCw, Lock, Unlock } from 'lucide-react'; +import { supabase } from '@/integrations/supabase/client'; +import { useToast } from '@/components/ui/use-toast'; interface ProductionPieceStatus { marca: string; @@ -24,6 +26,7 @@ interface ModelViewer3DProps { selectedPieceMark?: string | null; selectedOF?: string; selectedPhase?: string; + initialRotation?: { x: number; y: number; z: number } | null; onSelectPiece?: (piece: PieceInfo | null) => void; } @@ -33,6 +36,7 @@ export const ModelViewer3D: React.FC = ({ selectedPieceMark, selectedOF, selectedPhase, + initialRotation, onSelectPiece, }) => { const containerRef = useRef(null); @@ -259,23 +263,11 @@ export const ModelViewer3D: React.FC = ({ }; }, []); - // Update Model in Scene - useEffect(() => { - if (!sceneRef.current || !modelData) return; - - // Remove old model if present - if (modelGroupRef.current) { - sceneRef.current.remove(modelGroupRef.current); - modelGroupRef.current = null; - } - - const group = modelData.sceneGroup; - modelGroupRef.current = group; - sceneRef.current.add(group); - - // Fit View (Center Model) - fitModelToView(group); - }, [modelData]); + // Estados de Rotação 3D e Bloqueio com Cadeado + const [rotationX, setRotationX] = useState(0); + const [rotationY, setRotationY] = useState(0); + const [isRotationLocked, setIsRotationLocked] = useState(false); + const { toast } = useToast(); // Function: Fit Model to View const fitModelToView = useCallback((group?: THREE.Group | null) => { @@ -305,6 +297,117 @@ export const ModelViewer3D: React.FC = ({ } }, []); + // Update Model in Scene & Carregar Rotação Memorizada + useEffect(() => { + if (!sceneRef.current || !modelData) return; + + // Remove old model if present + if (modelGroupRef.current) { + sceneRef.current.remove(modelGroupRef.current); + modelGroupRef.current = null; + } + + const group = modelData.sceneGroup; + modelGroupRef.current = group; + sceneRef.current.add(group); + + // Recupera rotação memorizada para esta OF (localStorage ou banco de dados) + const storageKey = selectedOF ? `tracksteel_model_rot_${selectedOF}` : null; + let savedX = 0; + let savedY = 0; + let isLocked = false; + + if (storageKey) { + const stored = localStorage.getItem(storageKey); + if (stored) { + try { + const parsed = JSON.parse(stored); + if (typeof parsed.x === 'number') savedX = parsed.x; + if (typeof parsed.y === 'number') savedY = parsed.y; + isLocked = true; + } catch (e) {} + } + } + + if (!isLocked && initialRotation) { + if (typeof initialRotation.x === 'number') savedX = initialRotation.x; + if (typeof initialRotation.y === 'number') savedY = initialRotation.y; + isLocked = true; + } + + setRotationX(savedX); + setRotationY(savedY); + setIsRotationLocked(isLocked); + + // Aplica rotação inicial no grupo do modelo (garantindo que se comporte em pé/adequado) + group.rotation.set( + THREE.MathUtils.degToRad(savedX), + THREE.MathUtils.degToRad(savedY), + 0 + ); + group.updateMatrixWorld(true); + + // Fit View (Center Model) + fitModelToView(group); + }, [modelData, selectedOF, initialRotation, fitModelToView]); + + // Gira o modelo em 90 graus no eixo X (tombamento vertical para alinhar Z-Up do CAD com Y-Up) + const handleRotate90X = useCallback(() => { + if (!modelGroupRef.current) return; + + const nextX = (rotationX + 90) % 360; + setRotationX(nextX); + setIsRotationLocked(false); // Destrava temporariamente para indicar alteração pendente de trava + + modelGroupRef.current.rotation.x = THREE.MathUtils.degToRad(nextX); + modelGroupRef.current.rotation.y = THREE.MathUtils.degToRad(rotationY); + modelGroupRef.current.updateMatrixWorld(true); + + fitModelToView(modelGroupRef.current); + }, [rotationX, rotationY, fitModelToView]); + + // Alterna o bloqueio/gravação da posição ideal de visualização no banco e no navegador + const handleToggleLockRotation = useCallback(async () => { + if (!selectedOF) { + toast({ + title: 'Aviso', + description: 'Selecione uma OF para memorizar a rotação do modelo.', + }); + return; + } + + if (!isRotationLocked) { + // Trava e grava a rotação + setIsRotationLocked(true); + const rotData = { x: rotationX, y: rotationY, z: 0 }; + + // 1. Grava no localStorage para acesso instantâneo + localStorage.setItem(`tracksteel_model_rot_${selectedOF}`, JSON.stringify(rotData)); + + // 2. Persiste na nuvem/Supabase na tabela ordens_fabricacao + try { + await supabase + .from('ordens_fabricacao' as any) + .update({ model_3d_rotation: rotData }) + .eq('num_of', selectedOF); + } catch (err) { + console.error('Erro ao salvar rotação no banco:', err); + } + + toast({ + title: 'Posição Travada e Salva', + description: `Posição ideal de visualização (${rotationX}°) gravada com sucesso para a OF ${selectedOF}.`, + }); + } else { + // Destrava para permitir novo ajuste + setIsRotationLocked(false); + toast({ + title: 'Posição Destravada', + description: 'Clique em "Girar 90°" para ajustar e trave no cadeado novamente quando estiver na posição correta.', + }); + } + }, [selectedOF, isRotationLocked, rotationX, rotationY, toast]); + // Color Mode & Proportional Apontamentos Application useEffect(() => { if (!modelData || !modelGroupRef.current) return; @@ -1055,6 +1158,54 @@ export const ModelViewer3D: React.FC = ({ Limpar Seleção + + {/* Card de Ajuste e Travamento de Rotação 3D (Tombamento 90° + Cadeado Memorizável) */} +
+ {/* Botão Rotacionar / Tombar 90° */} + + + {/* Botão Cadeado: Travar / Gravar ou Destravar */} + +
{/* Hover Tooltip (1.0s delay trigger) */} diff --git a/src/lib/3d/wrlLoaderService.ts b/src/lib/3d/wrlLoaderService.ts index 2a14edf..93bb9da 100644 --- a/src/lib/3d/wrlLoaderService.ts +++ b/src/lib/3d/wrlLoaderService.ts @@ -25,11 +25,49 @@ async function decompressGzipIfNeeded(buffer: ArrayBuffer): Promise { } /** - * Heurística avançada para extrair e normalizar a marca da peça a partir do nó VRML (DEF). - * Softwares como Tekla, Advance Steel, Bocad e outros exportam identificadores variados: - * Ex: "DEF V101 Transform", "DEF Part_V101_1 Transform", "DEF C1_V101 Transform", "DEF 01-V101 Transform" + * Extrai a Fase da estrutura a partir dos ancestrais na árvore 3D (ex: "00001_COLUNAS", "00004_GUARDA_CORPO"). + * Garante que apenas o nó de primeiro nível (raiz da cena) ou nós explicitamente marcados como FASE/ETAPA sejam considerados. + * Evita rigorosamente que marcas de montagem (ex: "106", "121") ou dimensões (ex: "10x57", "1127") sejam confundidas com fases. */ -function parseWrlNodeName(rawName: string): { +function extractPhaseFromAncestors(ancestors: string[]): string | undefined { + if (!ancestors || ancestors.length === 0) return undefined; + + // 1. O nó de primeiro nível (ancestors[0]) representa a pasta raiz no WRL (ex: "00001_COLUNAS", "00004_GUARDA_CORPO") + const rootNode = ancestors[0].replace(/^DEF\s+/i, '').replace(/^ID_/, '').trim(); + + // Padrão Tekla / Bocad com prefixo numérico seguido de nome de grupo (ex: "00001_COLUNAS", "00004_GUARDA_CORPO") + const mZeros = rootNode.match(/^(?:0*([1-9]\d*))[-_ ]+([A-Za-zÀ-ÿ]+.*)$/i); + if (mZeros) { + return String(Number(mZeros[1])); // "1", "2", "3", "4" + } + + // Padrão explícito na raiz "FASE 1", "FASE_01", "ETAPA 2" + const mRootFase = rootNode.match(/^(?:fase|etapa|phase|stage)[-_ ]*0*([1-9]\d*)/i); + if (mRootFase) { + return String(Number(mRootFase[1])); + } + + // 2. Se o primeiro nó não for fase, pesquisa nos ancestrais intermediários APENAS se contiver palavra-chave explícita FASE ou ETAPA + for (let i = 1; i < ancestors.length; i++) { + const clean = ancestors[i].replace(/^DEF\s+/i, '').replace(/^ID_/, '').trim(); + const mExplicit = clean.match(/^(?:fase|etapa|phase|stage)[-_ ]*0*([1-9]\d*)/i); + if (mExplicit) { + return String(Number(mExplicit[1])); + } + } + + return undefined; +} + +/** + * Analisa e normaliza a marca da peça, marca do conjunto e tipo estrutural, + * levando em conta a hierarquia de pastas (ex: Fase -> Conjunto '121' -> Componente '121_1127_TUBO...'). + */ +function parseWrlNodeName( + rawName: string, + inheritedPhase?: string, + ancestorAssembly?: string +): { pieceMark: string; cleanMark: string; assemblyMark?: string; @@ -39,61 +77,61 @@ function parseWrlNodeName(rawName: string): { } { let name = String(rawName || '').trim(); - // Remove prefixos de sanitização interna se houver - name = name.replace(/^ID_/, ''); + // Remove prefixos internos + name = name.replace(/^ID_/, '').replace(/^DEF\s+/i, '').replace(/[\{\}\[\]"']/g, '').trim(); - // Remove caracteres especiais de escape ou tags extras - name = name.replace(/^DEF\s+/i, '').replace(/[\{\}\[\]"']/g, '').trim(); - - let phase: string | undefined = undefined; - let assemblyMark: string | undefined = undefined; + let assemblyMark = ancestorAssembly ? ancestorAssembly.trim() : undefined; let pieceMark = name; + let profile = ''; - // 1. Detecção de Fase no início (ex: "01-V101", "F1_V101", "B138-01-V101") - const phaseMatch = name.match(/^(?:[A-Za-z0-9]+-)?(?:Fase|F|Etapa)?(\d+)[-_](.+)$/i); - if (phaseMatch) { - phase = phaseMatch[1]; - pieceMark = phaseMatch[2]; + // Formato padrão Tekla/Bocad: [Conjunto]_[Peca]_[Perfil]_[Instancia] + // Ex: "121_1127_TUBO44x45X3_121293" -> assembly="121", piece="1127", profile="TUBO44x45X3" + const parts = name.split('_'); + + if (parts.length >= 3 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) { + assemblyMark = parts[0]; + pieceMark = parts[1]; + profile = parts[2]; + } else if (parts.length >= 2 && /^\d+$/.test(parts[0])) { + assemblyMark = parts[0]; + pieceMark = parts[1]; + if (parts.length > 2) profile = parts.slice(2).join('_'); + } else if (/^(?:PARAF|BOLT)/i.test(name)) { + pieceMark = parts[0]; + profile = 'PARAFUSO'; + } else { + // Remoção de prefixos comuns como "Part_", "Piece_", "Peca_", "Elem_" + pieceMark = pieceMark.replace(/^(?:Part|Piece|Peca|Elem|Item|Member|Profile|Mesh)[-_]/i, ''); } - // 2. Remoção de prefixos comuns como "Part_", "Piece_", "Peca_", "Elem_", "Assembly_", "Conjunto_" - pieceMark = pieceMark.replace(/^(?:Part|Piece|Peca|Elem|Item|Member|Profile|Mesh)[-_]/i, ''); - - // 3. Detecção de Conjunto e Peça (ex: "C1_V101", "ASSEMBLY1-V101", "V101-PL1") - if (pieceMark.includes('_') || pieceMark.includes('-')) { - const parts = pieceMark.split(/[-_]/); - if (parts.length >= 2) { - const first = parts[0].trim(); - const second = parts.slice(1).join('-').trim(); - if (/^(?:C|M|CONJ|ASS|G)\d+$/i.test(first)) { - assemblyMark = first; - pieceMark = second; - } - } - } - - // 4. Marca limpa (remove sufixo de instância numérica gerada por exportadores, ex: "V101_1", "V101-2", "V101.1", "V101#1") let cleanMark = pieceMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase(); - if (!cleanMark) { - cleanMark = pieceMark.toUpperCase(); - } + if (!cleanMark) cleanMark = pieceMark.toUpperCase(); const cleanAssemblyMark = assemblyMark ? assemblyMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase() : cleanMark; - // 5. Detecção de Tipo de Elemento Estrutural com base na marca + // Detecção de tipo de elemento estrutural let detectedType = 'ELEMENT'; - const upper = cleanMark.toUpperCase(); - if (/^(?:V|VIGA|BEAM|VIG)/.test(upper)) { + const searchStr = `${profile} ${name} ${cleanMark}`.toUpperCase(); + + if (searchStr.includes('VIGA') || searchStr.includes('BEAM') || cleanMark.startsWith('V')) { detectedType = 'BEAM'; - } else if (/^(?:P|PILAR|COL|COLUMN|PIL)/.test(upper)) { + } else if (searchStr.includes('COLUNA') || searchStr.includes('PILAR') || searchStr.includes('COLUMN') || cleanMark.startsWith('P')) { detectedType = 'COLUMN'; - } else if (/^(?:PL|CH|CHAPA|PLATE|GUSSET|BASE|FLANGE)/.test(upper)) { + } else if (searchStr.includes('CHAPA') || searchStr.includes('PLATE') || cleanMark.startsWith('PL') || cleanMark.startsWith('CH')) { detectedType = 'PLATE'; - } else if (/^(?:CV|CONTRA|BRACE|TIE|DIAG)/.test(upper)) { + } else if (searchStr.includes('TUBO') || searchStr.includes('TUBE') || searchStr.includes('ROBN')) { + detectedType = 'TUBE'; + } else if (searchStr.includes('PARAF') || searchStr.includes('BOLT')) { + detectedType = 'FASTENER'; + } else if (searchStr.includes('ESCAD') || searchStr.includes('STAIR')) { + detectedType = 'STAIR'; + } else if (searchStr.includes('GUARDA') || searchStr.includes('CORRIM')) { + detectedType = 'RAILING'; + } else if (searchStr.includes('CONTRA') || searchStr.includes('BRACE') || searchStr.includes('DIAG')) { detectedType = 'BRACE'; - } else if (/^(?:TER|TERCA|PURLIN)/.test(upper)) { + } else if (searchStr.includes('TERCA') || searchStr.includes('PURLIN')) { detectedType = 'PURLIN'; - } else if (/^(?:T|TRENA|TIE|TIRANTE)/.test(upper)) { + } else if (searchStr.includes('TIRANTE') || searchStr.includes('TIE')) { detectedType = 'TIE'; } @@ -102,7 +140,7 @@ function parseWrlNodeName(rawName: string): { cleanMark, assemblyMark: assemblyMark ? assemblyMark.toUpperCase() : undefined, cleanAssemblyMark, - phase, + phase: inheritedPhase, detectedType, }; } @@ -153,8 +191,6 @@ function parseVrmlDirectFallback( const group = new THREE.Group(); group.name = 'WRL_Direct_Fallback_Root'; - // Procura ocorrências de DEF associadas a IndexedFaceSet - // Suporta tanto VRML 1.0 (Separator, Coordinate3) quanto VRML 2.0 (Shape, Coordinate, Transform) const defRegex = /DEF\s+([^\s\{\[\(\)]+)[\s\S]*?(?:point\s*\[([\s\S]*?)\])[\s\S]*?(?:coordIndex\s*\[([\s\S]*?)\])/gi; let match: RegExpExecArray | null; let count = 0; @@ -166,9 +202,8 @@ function parseVrmlDirectFallback( const pointStr = match[2]; const indexStr = match[3]; - // Extrai vértices flutuantes (x, y, z) const rawCoords = pointStr.trim().split(/[\s,]+/).filter(Boolean).map(Number); - if (rawCoords.length < 9) continue; // mínimo 3 vértices (1 triângulo) + if (rawCoords.length < 9) continue; const vertices: [number, number, number][] = []; for (let i = 0; i < rawCoords.length; i += 3) { @@ -179,7 +214,6 @@ function parseVrmlDirectFallback( if (vertices.length === 0) continue; - // Extrai índices de faces separados por -1 const rawIndices = indexStr.trim().split(/[\s,]+/).filter(Boolean).map(Number); const triangleIndices: number[] = []; let currentPoly: number[] = []; @@ -187,7 +221,6 @@ function parseVrmlDirectFallback( for (const idx of rawIndices) { if (idx === -1) { if (currentPoly.length >= 3) { - // Triangulação em leque (fan triangulation) for (let p = 1; p < currentPoly.length - 1; p++) { triangleIndices.push(currentPoly[0], currentPoly[p], currentPoly[p + 1]); } @@ -209,9 +242,7 @@ function parseVrmlDirectFallback( const positions: number[] = []; for (const tIdx of triangleIndices) { const v = vertices[tIdx]; - if (v) { - positions.push(v[0], v[1], v[2]); - } + if (v) positions.push(v[0], v[1], v[2]); } if (positions.length === 0) continue; @@ -227,7 +258,6 @@ function parseVrmlDirectFallback( group.add(mesh); } - // Se a busca com DEF não encontrou malhas, busca todos os point/coordIndex sem DEF if (count === 0) { const genericRegex = /point\s*\[([\s\S]*?)\][\s\S]*?coordIndex\s*\[([\s\S]*?)\]/gi; let genMatch: RegExpExecArray | null; @@ -348,19 +378,6 @@ export async function loadAndAuditWRL( let totalMembers = 0; const marksFound = new Set(); - // Helper para buscar o nome do nó mais relevante na árvore - const resolveMeaningfulNodeName = (mesh: THREE.Mesh): string => { - let curr: THREE.Object3D | null = mesh; - while (curr) { - if (curr.name && !/^(Scene|Root|Group|Transform|Shape|IndexedFaceSet|WRL_Direct_Fallback_Root)$/i.test(curr.name.trim())) { - const mapped = nameMap.get(curr.name.trim()) || curr.name.trim(); - return mapped; - } - curr = curr.parent; - } - return mesh.name || `PECA-${idCounter}`; - }; - // Percorre todos os nós e malhas gerados vrmlScene.traverse((child) => { if ((child as THREE.Mesh).isMesh) { @@ -376,8 +393,34 @@ export async function loadAndAuditWRL( mesh.receiveShadow = true; } - const rawNodeName = resolveMeaningfulNodeName(mesh); - const parsed = parseWrlNodeName(rawNodeName); + // Constrói a lista de ancestrais da raiz até o nó da malha + const ancestors: string[] = []; + let p: THREE.Object3D | null = mesh.parent; + while (p && p !== vrmlScene && p.name !== 'WRL_Root_Model') { + const pName = (p.name || '').trim(); + if (pName && !/^(Scene|Root|Group|Transform|Shape|IndexedFaceSet|WRL_Direct_Fallback_Root)$/i.test(pName)) { + const unmapped = nameMap.get(pName) || pName; + ancestors.unshift(unmapped); + } + p = p.parent; + } + + // 1. Extração rigorosa da Fase a partir dos nós ancestrais (evita falsos positivos em nomes de peças) + const inheritedPhase = extractPhaseFromAncestors(ancestors); + + // 2. Extração do conjunto pai se houver na árvore (ex: pasta '121') + let ancestorAssembly: string | undefined = undefined; + for (const anc of ancestors) { + const cleanAnc = anc.replace(/^DEF\s+/i, '').replace(/^ID_/, '').trim(); + // Se for um identificador de conjunto (ex: "121", "106", "V101") que não seja a fase + if (/^\d+$/.test(cleanAnc) && cleanAnc !== inheritedPhase) { + ancestorAssembly = cleanAnc; + break; + } + } + + const rawNodeName = nameMap.get(mesh.name) || mesh.name || `PECA-${idCounter}`; + const parsed = parseWrlNodeName(rawNodeName, inheritedPhase, ancestorAssembly); const expressID = idCounter++; const guid = `wrl-${expressID}-${parsed.cleanMark}`; @@ -465,6 +508,13 @@ export async function loadAndAuditWRL( sceneGroup.updateMatrixWorld(true); } + // Gera lista ordenada de fases limpas (ex: ["1", "2", "3", "4"]) + const sortedPhases = Array.from(detectedPhasesSet).sort((a, b) => { + const na = Number(a); + const nb = Number(b); + return !isNaN(na) && !isNaN(nb) ? na - nb : a.localeCompare(b); + }); + // Gera auditoria de qualidade compatível const uniqueMarksFound = marksFound.size; const marksList = Array.from(marksFound).sort(); @@ -488,7 +538,7 @@ export async function loadAndAuditWRL( qualityScore, qualityMessage: uniqueMarksFound > 0 - ? `Modelo WRL processado com sucesso. ${totalMeshes} malhas renderizadas e ${uniqueMarksFound} marcas identificadas para apontamento.` + ? `Modelo WRL processado com sucesso. ${totalMeshes} malhas renderizadas e ${uniqueMarksFound} marcas identificadas em ${sortedPhases.length} fases estruturais.` : `Modelo WRL processado com ${totalMeshes} malhas. Nenhuma marca nominal identificada na geometria.`, fallbackActive: isFallback, }; @@ -503,6 +553,6 @@ export async function loadAndAuditWRL( weldedGroupMap, materialsByColor, totalMeshes, - detectedPhases: Array.from(detectedPhasesSet).sort(), + detectedPhases: sortedPhases, }; } diff --git a/src/pages/Visualizador3D.tsx b/src/pages/Visualizador3D.tsx index 61f9d8b..8a4b57e 100644 --- a/src/pages/Visualizador3D.tsx +++ b/src/pages/Visualizador3D.tsx @@ -28,6 +28,7 @@ interface OFOption { ifc_filename?: string | null; ifc_file_size?: number | null; ifc_updated_at?: string | null; + model_3d_rotation?: { x: number; y: number; z: number } | null; } const PROCESS_COLORS: Record = { @@ -67,13 +68,13 @@ export default function Visualizador3D() { const fileInputRef = useRef(null); - // 1. Fetch OFs (com colunas de IFC para persistência na nuvem) + // 1. Fetch OFs (com colunas de IFC e rotação 3D para persistência na nuvem) useEffect(() => { async function fetchOFs() { try { const { data, error } = await supabase .from('ordens_fabricacao' as any) - .select('id, num_of, descritivo, peso_total, ifc_url, ifc_filename, ifc_file_size, ifc_updated_at') + .select('id, num_of, descritivo, peso_total, ifc_url, ifc_filename, ifc_file_size, ifc_updated_at, model_3d_rotation') .order('num_of', { ascending: false }); if (!error && data) { @@ -86,6 +87,7 @@ export default function Visualizador3D() { ifc_filename: item.ifc_filename || null, ifc_file_size: item.ifc_file_size ? Number(item.ifc_file_size) : null, ifc_updated_at: item.ifc_updated_at || null, + model_3d_rotation: item.model_3d_rotation || null, })); setOfs(list); } @@ -130,8 +132,13 @@ export default function Visualizador3D() { if (pecasData) { pecasData.forEach((p: any) => { - const fase = String(p.etapa_fase || '').trim(); - if (fase) phases.add(fase); + let fase = String(p.etapa_fase || '').trim(); + if (fase) { + if (/^\d+$/.test(fase)) { + fase = String(parseInt(fase, 10)); + } + phases.add(fase); + } totalPecas += Number(p.quantidade || 0); totalPeso += Number(p.peso_total || 0); @@ -269,10 +276,12 @@ export default function Visualizador3D() { setModelData(result); setAuditData(result.audit); - // Mescla fases detectadas no IFC às fases da OF + // Mescla fases detectadas no IFC/WRL às fases da OF if (result.detectedPhases && result.detectedPhases.length > 0) { setPhasesList((prev) => { - const combined = new Set([...prev, ...result.detectedPhases]); + const normPrev = prev.map((f) => (/^\d+$/.test(f) ? String(parseInt(f, 10)) : f)); + const normDetected = result.detectedPhases.map((f) => (/^\d+$/.test(f) ? String(parseInt(f, 10)) : f)); + const combined = new Set([...normPrev, ...normDetected]); return Array.from(combined).sort((a, b) => { const na = Number(a); const nb = Number(b); @@ -305,6 +314,7 @@ export default function Visualizador3D() { setModelData(null); setAuditData(null); setSelectedPhase('all'); + setPhasesList([]); const currentOF = ofs.find((o) => o.of_number === selectedOF); @@ -361,10 +371,12 @@ export default function Visualizador3D() { setAuditData(result.audit); setIsAuditModalOpen(true); // Abre modal de qualidade automaticamente - // Mescla fases detectadas no IFC + // Mescla fases detectadas no IFC/WRL às fases da OF if (result.detectedPhases && result.detectedPhases.length > 0) { setPhasesList((prev) => { - const combined = new Set([...prev, ...result.detectedPhases]); + const normPrev = prev.map((f) => (/^\d+$/.test(f) ? String(parseInt(f, 10)) : f)); + const normDetected = result.detectedPhases.map((f) => (/^\d+$/.test(f) ? String(parseInt(f, 10)) : f)); + const combined = new Set([...normPrev, ...normDetected]); return Array.from(combined).sort((a, b) => { const na = Number(a); const nb = Number(b); @@ -721,6 +733,7 @@ export default function Visualizador3D() { productionData={productionMap} selectedOF={selectedOF} selectedPhase={selectedPhase} + initialRotation={ofs.find((o) => o.of_number === selectedOF)?.model_3d_rotation} />