feat: apuracao de fases WRL e controle de rotacao com trava para OF
This commit is contained in:
@@ -6,7 +6,9 @@ import { ViewCube } from './ViewCube';
|
|||||||
import { Viewer3DSidebar } from './Viewer3DSidebar';
|
import { Viewer3DSidebar } from './Viewer3DSidebar';
|
||||||
import { PieceHoverTooltip } from './PieceHoverTooltip';
|
import { PieceHoverTooltip } from './PieceHoverTooltip';
|
||||||
import { SelectionListModal, SelectedPieceItem } from './SelectionListModal';
|
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 {
|
interface ProductionPieceStatus {
|
||||||
marca: string;
|
marca: string;
|
||||||
@@ -24,6 +26,7 @@ interface ModelViewer3DProps {
|
|||||||
selectedPieceMark?: string | null;
|
selectedPieceMark?: string | null;
|
||||||
selectedOF?: string;
|
selectedOF?: string;
|
||||||
selectedPhase?: string;
|
selectedPhase?: string;
|
||||||
|
initialRotation?: { x: number; y: number; z: number } | null;
|
||||||
onSelectPiece?: (piece: PieceInfo | null) => void;
|
onSelectPiece?: (piece: PieceInfo | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +36,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
selectedPieceMark,
|
selectedPieceMark,
|
||||||
selectedOF,
|
selectedOF,
|
||||||
selectedPhase,
|
selectedPhase,
|
||||||
|
initialRotation,
|
||||||
onSelectPiece,
|
onSelectPiece,
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -259,23 +263,11 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Update Model in Scene
|
// Estados de Rotação 3D e Bloqueio com Cadeado
|
||||||
useEffect(() => {
|
const [rotationX, setRotationX] = useState<number>(0);
|
||||||
if (!sceneRef.current || !modelData) return;
|
const [rotationY, setRotationY] = useState<number>(0);
|
||||||
|
const [isRotationLocked, setIsRotationLocked] = useState<boolean>(false);
|
||||||
// Remove old model if present
|
const { toast } = useToast();
|
||||||
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]);
|
|
||||||
|
|
||||||
// Function: Fit Model to View
|
// Function: Fit Model to View
|
||||||
const fitModelToView = useCallback((group?: THREE.Group | null) => {
|
const fitModelToView = useCallback((group?: THREE.Group | null) => {
|
||||||
@@ -305,6 +297,117 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 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
|
// Color Mode & Proportional Apontamentos Application
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!modelData || !modelGroupRef.current) return;
|
if (!modelData || !modelGroupRef.current) return;
|
||||||
@@ -1055,6 +1158,54 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
<span className="whitespace-nowrap">Limpar Seleção</span>
|
<span className="whitespace-nowrap">Limpar Seleção</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Card de Ajuste e Travamento de Rotação 3D (Tombamento 90° + Cadeado Memorizável) */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1.5 p-1.5 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-300 dark:border-slate-700 shadow-lg shadow-slate-300/30 dark:shadow-slate-950/50 pointer-events-auto"
|
||||||
|
style={{ minWidth: '130px' }}
|
||||||
|
>
|
||||||
|
{/* Botão Rotacionar / Tombar 90° */}
|
||||||
|
<button
|
||||||
|
onClick={handleRotate90X}
|
||||||
|
disabled={!modelData}
|
||||||
|
className={`flex-1 flex items-center justify-center gap-1.5 px-2.5 py-1.5 rounded-xl text-[11px] font-bold transition-all duration-150 active:scale-95 ${
|
||||||
|
modelData
|
||||||
|
? 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-100 cursor-pointer shadow-sm'
|
||||||
|
: 'bg-slate-100/50 dark:bg-slate-800/30 text-slate-400 dark:text-slate-600 cursor-not-allowed opacity-50'
|
||||||
|
}`}
|
||||||
|
title="Girar o modelo 90° no eixo de tombamento (adequar orientação Z/Y)"
|
||||||
|
>
|
||||||
|
<RotateCw className="w-3.5 h-3.5 text-blue-600 dark:text-blue-400" />
|
||||||
|
<span className="whitespace-nowrap">Girar 90°</span>
|
||||||
|
<span className="text-[10px] font-mono font-normal text-slate-600 dark:text-slate-300 bg-white dark:bg-slate-950 px-1 py-0.2 rounded border border-slate-200 dark:border-slate-800">
|
||||||
|
{rotationX}°
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Botão Cadeado: Travar / Gravar ou Destravar */}
|
||||||
|
<button
|
||||||
|
onClick={handleToggleLockRotation}
|
||||||
|
disabled={!modelData}
|
||||||
|
className={`flex items-center justify-center p-1.5 rounded-xl transition-all duration-200 active:scale-95 border ${
|
||||||
|
!modelData
|
||||||
|
? 'bg-slate-100/50 dark:bg-slate-800/30 text-slate-400 border-transparent cursor-not-allowed opacity-40'
|
||||||
|
: isRotationLocked
|
||||||
|
? 'bg-emerald-50 hover:bg-emerald-100 text-emerald-700 border-emerald-300 dark:bg-emerald-950/60 dark:hover:bg-emerald-900/80 dark:text-emerald-300 dark:border-emerald-700/80 shadow-sm cursor-pointer'
|
||||||
|
: 'bg-amber-50 hover:bg-amber-100 text-amber-700 border-amber-300 dark:bg-amber-950/50 dark:hover:bg-amber-900/70 dark:text-amber-300 dark:border-amber-700/70 shadow-sm cursor-pointer animate-pulse'
|
||||||
|
}`}
|
||||||
|
title={
|
||||||
|
isRotationLocked
|
||||||
|
? 'Posição TRAVADA e salva para esta OF. Clique para destravar e ajustar.'
|
||||||
|
: 'Posição DESTRAVADA. Clique aqui no cadeado para travar e salvar a posição ideal.'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isRotationLocked ? (
|
||||||
|
<Lock className="w-4 h-4 text-emerald-600 dark:text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<Unlock className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hover Tooltip (1.0s delay trigger) */}
|
{/* Hover Tooltip (1.0s delay trigger) */}
|
||||||
|
|||||||
+120
-70
@@ -25,11 +25,49 @@ async function decompressGzipIfNeeded(buffer: ArrayBuffer): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Heurística avançada para extrair e normalizar a marca da peça a partir do nó VRML (DEF).
|
* Extrai a Fase da estrutura a partir dos ancestrais na árvore 3D (ex: "00001_COLUNAS", "00004_GUARDA_CORPO").
|
||||||
* Softwares como Tekla, Advance Steel, Bocad e outros exportam identificadores variados:
|
* Garante que apenas o nó de primeiro nível (raiz da cena) ou nós explicitamente marcados como FASE/ETAPA sejam considerados.
|
||||||
* Ex: "DEF V101 Transform", "DEF Part_V101_1 Transform", "DEF C1_V101 Transform", "DEF 01-V101 Transform"
|
* 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;
|
pieceMark: string;
|
||||||
cleanMark: string;
|
cleanMark: string;
|
||||||
assemblyMark?: string;
|
assemblyMark?: string;
|
||||||
@@ -39,61 +77,61 @@ function parseWrlNodeName(rawName: string): {
|
|||||||
} {
|
} {
|
||||||
let name = String(rawName || '').trim();
|
let name = String(rawName || '').trim();
|
||||||
|
|
||||||
// Remove prefixos de sanitização interna se houver
|
// Remove prefixos internos
|
||||||
name = name.replace(/^ID_/, '');
|
name = name.replace(/^ID_/, '').replace(/^DEF\s+/i, '').replace(/[\{\}\[\]"']/g, '').trim();
|
||||||
|
|
||||||
// Remove caracteres especiais de escape ou tags extras
|
let assemblyMark = ancestorAssembly ? ancestorAssembly.trim() : undefined;
|
||||||
name = name.replace(/^DEF\s+/i, '').replace(/[\{\}\[\]"']/g, '').trim();
|
|
||||||
|
|
||||||
let phase: string | undefined = undefined;
|
|
||||||
let assemblyMark: string | undefined = undefined;
|
|
||||||
let pieceMark = name;
|
let pieceMark = name;
|
||||||
|
let profile = '';
|
||||||
|
|
||||||
// 1. Detecção de Fase no início (ex: "01-V101", "F1_V101", "B138-01-V101")
|
// Formato padrão Tekla/Bocad: [Conjunto]_[Peca]_[Perfil]_[Instancia]
|
||||||
const phaseMatch = name.match(/^(?:[A-Za-z0-9]+-)?(?:Fase|F|Etapa)?(\d+)[-_](.+)$/i);
|
// Ex: "121_1127_TUBO44x45X3_121293" -> assembly="121", piece="1127", profile="TUBO44x45X3"
|
||||||
if (phaseMatch) {
|
const parts = name.split('_');
|
||||||
phase = phaseMatch[1];
|
|
||||||
pieceMark = phaseMatch[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Remoção de prefixos comuns como "Part_", "Piece_", "Peca_", "Elem_", "Assembly_", "Conjunto_"
|
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, '');
|
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();
|
let cleanMark = pieceMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase();
|
||||||
if (!cleanMark) {
|
if (!cleanMark) cleanMark = pieceMark.toUpperCase();
|
||||||
cleanMark = pieceMark.toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanAssemblyMark = assemblyMark ? assemblyMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase() : cleanMark;
|
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';
|
let detectedType = 'ELEMENT';
|
||||||
const upper = cleanMark.toUpperCase();
|
const searchStr = `${profile} ${name} ${cleanMark}`.toUpperCase();
|
||||||
if (/^(?:V|VIGA|BEAM|VIG)/.test(upper)) {
|
|
||||||
|
if (searchStr.includes('VIGA') || searchStr.includes('BEAM') || cleanMark.startsWith('V')) {
|
||||||
detectedType = 'BEAM';
|
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';
|
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';
|
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';
|
detectedType = 'BRACE';
|
||||||
} else if (/^(?:TER|TERCA|PURLIN)/.test(upper)) {
|
} else if (searchStr.includes('TERCA') || searchStr.includes('PURLIN')) {
|
||||||
detectedType = 'PURLIN';
|
detectedType = 'PURLIN';
|
||||||
} else if (/^(?:T|TRENA|TIE|TIRANTE)/.test(upper)) {
|
} else if (searchStr.includes('TIRANTE') || searchStr.includes('TIE')) {
|
||||||
detectedType = 'TIE';
|
detectedType = 'TIE';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +140,7 @@ function parseWrlNodeName(rawName: string): {
|
|||||||
cleanMark,
|
cleanMark,
|
||||||
assemblyMark: assemblyMark ? assemblyMark.toUpperCase() : undefined,
|
assemblyMark: assemblyMark ? assemblyMark.toUpperCase() : undefined,
|
||||||
cleanAssemblyMark,
|
cleanAssemblyMark,
|
||||||
phase,
|
phase: inheritedPhase,
|
||||||
detectedType,
|
detectedType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -153,8 +191,6 @@ function parseVrmlDirectFallback(
|
|||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
group.name = 'WRL_Direct_Fallback_Root';
|
group.name = 'WRL_Direct_Fallback_Root';
|
||||||
|
|
||||||
// Procura ocorrências de DEF <nome> 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;
|
const defRegex = /DEF\s+([^\s\{\[\(\)]+)[\s\S]*?(?:point\s*\[([\s\S]*?)\])[\s\S]*?(?:coordIndex\s*\[([\s\S]*?)\])/gi;
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -166,9 +202,8 @@ function parseVrmlDirectFallback(
|
|||||||
const pointStr = match[2];
|
const pointStr = match[2];
|
||||||
const indexStr = match[3];
|
const indexStr = match[3];
|
||||||
|
|
||||||
// Extrai vértices flutuantes (x, y, z)
|
|
||||||
const rawCoords = pointStr.trim().split(/[\s,]+/).filter(Boolean).map(Number);
|
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][] = [];
|
const vertices: [number, number, number][] = [];
|
||||||
for (let i = 0; i < rawCoords.length; i += 3) {
|
for (let i = 0; i < rawCoords.length; i += 3) {
|
||||||
@@ -179,7 +214,6 @@ function parseVrmlDirectFallback(
|
|||||||
|
|
||||||
if (vertices.length === 0) continue;
|
if (vertices.length === 0) continue;
|
||||||
|
|
||||||
// Extrai índices de faces separados por -1
|
|
||||||
const rawIndices = indexStr.trim().split(/[\s,]+/).filter(Boolean).map(Number);
|
const rawIndices = indexStr.trim().split(/[\s,]+/).filter(Boolean).map(Number);
|
||||||
const triangleIndices: number[] = [];
|
const triangleIndices: number[] = [];
|
||||||
let currentPoly: number[] = [];
|
let currentPoly: number[] = [];
|
||||||
@@ -187,7 +221,6 @@ function parseVrmlDirectFallback(
|
|||||||
for (const idx of rawIndices) {
|
for (const idx of rawIndices) {
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
if (currentPoly.length >= 3) {
|
if (currentPoly.length >= 3) {
|
||||||
// Triangulação em leque (fan triangulation)
|
|
||||||
for (let p = 1; p < currentPoly.length - 1; p++) {
|
for (let p = 1; p < currentPoly.length - 1; p++) {
|
||||||
triangleIndices.push(currentPoly[0], currentPoly[p], currentPoly[p + 1]);
|
triangleIndices.push(currentPoly[0], currentPoly[p], currentPoly[p + 1]);
|
||||||
}
|
}
|
||||||
@@ -209,9 +242,7 @@ function parseVrmlDirectFallback(
|
|||||||
const positions: number[] = [];
|
const positions: number[] = [];
|
||||||
for (const tIdx of triangleIndices) {
|
for (const tIdx of triangleIndices) {
|
||||||
const v = vertices[tIdx];
|
const v = vertices[tIdx];
|
||||||
if (v) {
|
if (v) positions.push(v[0], v[1], v[2]);
|
||||||
positions.push(v[0], v[1], v[2]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (positions.length === 0) continue;
|
if (positions.length === 0) continue;
|
||||||
@@ -227,7 +258,6 @@ function parseVrmlDirectFallback(
|
|||||||
group.add(mesh);
|
group.add(mesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se a busca com DEF não encontrou malhas, busca todos os point/coordIndex sem DEF
|
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
const genericRegex = /point\s*\[([\s\S]*?)\][\s\S]*?coordIndex\s*\[([\s\S]*?)\]/gi;
|
const genericRegex = /point\s*\[([\s\S]*?)\][\s\S]*?coordIndex\s*\[([\s\S]*?)\]/gi;
|
||||||
let genMatch: RegExpExecArray | null;
|
let genMatch: RegExpExecArray | null;
|
||||||
@@ -348,19 +378,6 @@ export async function loadAndAuditWRL(
|
|||||||
let totalMembers = 0;
|
let totalMembers = 0;
|
||||||
const marksFound = new Set<string>();
|
const marksFound = new Set<string>();
|
||||||
|
|
||||||
// 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
|
// Percorre todos os nós e malhas gerados
|
||||||
vrmlScene.traverse((child) => {
|
vrmlScene.traverse((child) => {
|
||||||
if ((child as THREE.Mesh).isMesh) {
|
if ((child as THREE.Mesh).isMesh) {
|
||||||
@@ -376,8 +393,34 @@ export async function loadAndAuditWRL(
|
|||||||
mesh.receiveShadow = true;
|
mesh.receiveShadow = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawNodeName = resolveMeaningfulNodeName(mesh);
|
// Constrói a lista de ancestrais da raiz até o nó da malha
|
||||||
const parsed = parseWrlNodeName(rawNodeName);
|
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 expressID = idCounter++;
|
||||||
const guid = `wrl-${expressID}-${parsed.cleanMark}`;
|
const guid = `wrl-${expressID}-${parsed.cleanMark}`;
|
||||||
@@ -465,6 +508,13 @@ export async function loadAndAuditWRL(
|
|||||||
sceneGroup.updateMatrixWorld(true);
|
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
|
// Gera auditoria de qualidade compatível
|
||||||
const uniqueMarksFound = marksFound.size;
|
const uniqueMarksFound = marksFound.size;
|
||||||
const marksList = Array.from(marksFound).sort();
|
const marksList = Array.from(marksFound).sort();
|
||||||
@@ -488,7 +538,7 @@ export async function loadAndAuditWRL(
|
|||||||
qualityScore,
|
qualityScore,
|
||||||
qualityMessage:
|
qualityMessage:
|
||||||
uniqueMarksFound > 0
|
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.`,
|
: `Modelo WRL processado com ${totalMeshes} malhas. Nenhuma marca nominal identificada na geometria.`,
|
||||||
fallbackActive: isFallback,
|
fallbackActive: isFallback,
|
||||||
};
|
};
|
||||||
@@ -503,6 +553,6 @@ export async function loadAndAuditWRL(
|
|||||||
weldedGroupMap,
|
weldedGroupMap,
|
||||||
materialsByColor,
|
materialsByColor,
|
||||||
totalMeshes,
|
totalMeshes,
|
||||||
detectedPhases: Array.from(detectedPhasesSet).sort(),
|
detectedPhases: sortedPhases,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface OFOption {
|
|||||||
ifc_filename?: string | null;
|
ifc_filename?: string | null;
|
||||||
ifc_file_size?: number | null;
|
ifc_file_size?: number | null;
|
||||||
ifc_updated_at?: string | null;
|
ifc_updated_at?: string | null;
|
||||||
|
model_3d_rotation?: { x: number; y: number; z: number } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROCESS_COLORS: Record<string, string> = {
|
const PROCESS_COLORS: Record<string, string> = {
|
||||||
@@ -67,13 +68,13 @@ export default function Visualizador3D() {
|
|||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(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(() => {
|
useEffect(() => {
|
||||||
async function fetchOFs() {
|
async function fetchOFs() {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('ordens_fabricacao' as any)
|
.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 });
|
.order('num_of', { ascending: false });
|
||||||
|
|
||||||
if (!error && data) {
|
if (!error && data) {
|
||||||
@@ -86,6 +87,7 @@ export default function Visualizador3D() {
|
|||||||
ifc_filename: item.ifc_filename || null,
|
ifc_filename: item.ifc_filename || null,
|
||||||
ifc_file_size: item.ifc_file_size ? Number(item.ifc_file_size) : null,
|
ifc_file_size: item.ifc_file_size ? Number(item.ifc_file_size) : null,
|
||||||
ifc_updated_at: item.ifc_updated_at || null,
|
ifc_updated_at: item.ifc_updated_at || null,
|
||||||
|
model_3d_rotation: item.model_3d_rotation || null,
|
||||||
}));
|
}));
|
||||||
setOfs(list);
|
setOfs(list);
|
||||||
}
|
}
|
||||||
@@ -130,8 +132,13 @@ export default function Visualizador3D() {
|
|||||||
|
|
||||||
if (pecasData) {
|
if (pecasData) {
|
||||||
pecasData.forEach((p: any) => {
|
pecasData.forEach((p: any) => {
|
||||||
const fase = String(p.etapa_fase || '').trim();
|
let fase = String(p.etapa_fase || '').trim();
|
||||||
if (fase) phases.add(fase);
|
if (fase) {
|
||||||
|
if (/^\d+$/.test(fase)) {
|
||||||
|
fase = String(parseInt(fase, 10));
|
||||||
|
}
|
||||||
|
phases.add(fase);
|
||||||
|
}
|
||||||
totalPecas += Number(p.quantidade || 0);
|
totalPecas += Number(p.quantidade || 0);
|
||||||
totalPeso += Number(p.peso_total || 0);
|
totalPeso += Number(p.peso_total || 0);
|
||||||
|
|
||||||
@@ -269,10 +276,12 @@ export default function Visualizador3D() {
|
|||||||
setModelData(result);
|
setModelData(result);
|
||||||
setAuditData(result.audit);
|
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) {
|
if (result.detectedPhases && result.detectedPhases.length > 0) {
|
||||||
setPhasesList((prev) => {
|
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) => {
|
return Array.from(combined).sort((a, b) => {
|
||||||
const na = Number(a);
|
const na = Number(a);
|
||||||
const nb = Number(b);
|
const nb = Number(b);
|
||||||
@@ -305,6 +314,7 @@ export default function Visualizador3D() {
|
|||||||
setModelData(null);
|
setModelData(null);
|
||||||
setAuditData(null);
|
setAuditData(null);
|
||||||
setSelectedPhase('all');
|
setSelectedPhase('all');
|
||||||
|
setPhasesList([]);
|
||||||
|
|
||||||
const currentOF = ofs.find((o) => o.of_number === selectedOF);
|
const currentOF = ofs.find((o) => o.of_number === selectedOF);
|
||||||
|
|
||||||
@@ -361,10 +371,12 @@ export default function Visualizador3D() {
|
|||||||
setAuditData(result.audit);
|
setAuditData(result.audit);
|
||||||
setIsAuditModalOpen(true); // Abre modal de qualidade automaticamente
|
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) {
|
if (result.detectedPhases && result.detectedPhases.length > 0) {
|
||||||
setPhasesList((prev) => {
|
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) => {
|
return Array.from(combined).sort((a, b) => {
|
||||||
const na = Number(a);
|
const na = Number(a);
|
||||||
const nb = Number(b);
|
const nb = Number(b);
|
||||||
@@ -721,6 +733,7 @@ export default function Visualizador3D() {
|
|||||||
productionData={productionMap}
|
productionData={productionMap}
|
||||||
selectedOF={selectedOF}
|
selectedOF={selectedOF}
|
||||||
selectedPhase={selectedPhase}
|
selectedPhase={selectedPhase}
|
||||||
|
initialRotation={ofs.find((o) => o.of_number === selectedOF)?.model_3d_rotation}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user