feat(viewer3d): hover magnetico para pecas finas e modelo multicolorido no modo Todos

This commit is contained in:
2026-09-21 18:06:16 +00:00
parent 3ec68e74a2
commit ec57edf713
2 changed files with 95 additions and 31 deletions
+93 -29
View File
@@ -438,15 +438,21 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
}
mesh.userData.edgesLine.visible = true;
// Oculta sólido
// Mantém material ativo para Raycaster mas 100% translúcido visualmente
if (Array.isArray(mesh.material)) {
mesh.material.forEach((m) => { m.visible = false; });
mesh.material.forEach((m) => {
m.visible = true;
m.transparent = true;
m.opacity = 0.001;
});
} else if (mesh.material) {
mesh.material.visible = false;
mesh.material.visible = true;
mesh.material.transparent = true;
mesh.material.opacity = 0.001;
}
}
} else {
// --- MODO GERAL (Todas as peças) ---
// --- MODO GERAL (Botão "Todos": Cada peça brilha na cor do seu último processo apontado) ---
if (colorMode === 'production' && productionData && productionData.size > 0) {
if (prod) {
matchedCount++;
@@ -457,22 +463,38 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
if (prod && prod.pointedQtd > 0) {
pointedCount++;
const qtyPercent = Math.min(prod.pointedQtd / prod.totalQtd, 1.0);
const processPercent = Math.min((prod.processOrdem || 1) / 5, 1.0);
const percent = qtyPercent * processPercent;
const colorLight = new THREE.Color('#4ade80');
const colorDark = new THREE.Color('#14532d');
const finalColor = new THREE.Color().lerpColors(colorLight, colorDark, percent);
// Determina a cor do processo mais avançado da peça
const procName = String(prod.currentProcessName || '').toLowerCase();
const procsDone: string[] = Array.isArray(prod.processesCompleted) ? prod.processesCompleted : [];
const procOrder = Number(prod.processOrdem || 0);
let pieceColorHex = '#3b82f6'; // Padrão Corte
if (procName.includes('montag') || procsDone.some(p => p.includes('montag')) || procOrder >= 5) {
pieceColorHex = '#8b5cf6'; // Roxo Montagem
} else if (procName.includes('exped') || procsDone.some(p => p.includes('exped')) || procOrder === 4) {
pieceColorHex = '#06b6d4'; // Ciano Expedição
} else if (procName.includes('pint') || procName.includes('galv') || procsDone.some(p => p.includes('pint') || p.includes('galv')) || procOrder === 3) {
pieceColorHex = '#10b981'; // Verde Pintura
} else if (procName.includes('sold') || procsDone.some(p => p.includes('sold')) || procOrder === 2) {
pieceColorHex = '#f97316'; // Laranja Solda
} else if (procName.includes('corte') || procsDone.some(p => p.includes('corte')) || procOrder === 1) {
pieceColorHex = '#3b82f6'; // Azul Corte
} else if (prod.processColor) {
pieceColorHex = prod.processColor;
}
mesh.material = new THREE.MeshStandardMaterial({
color: finalColor,
color: new THREE.Color(pieceColorHex),
metalness: 0.35,
roughness: 0.45,
transparent: opacity < 100,
opacity: opacity / 100,
side: THREE.DoubleSide,
});
} else {
// Cinza Claro Industrial (#a1a1aa)
// Peça Pendente: Cinza Claro Industrial (#a1a1aa)
mesh.material = new THREE.MeshStandardMaterial({
color: new THREE.Color(0xa1a1aa),
metalness: 0.25,
@@ -632,7 +654,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
}
}, []);
// 1.5-Second Hover Detection
// 1.5-Second Hover Detection com Magnetismo e Tolerância para Peças Esbeltas / Finas (ex: barras de 13mm)
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (!containerRef.current || !activeCameraRef.current || !modelGroupRef.current) return;
@@ -654,25 +676,67 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
const clientY = e.clientY;
hoverTimerRef.current = setTimeout(() => {
raycasterRef.current.setFromCamera(mousePosRef.current, activeCameraRef.current!);
const intersects = raycasterRef.current.intersectObjects(modelGroupRef.current!.children, true);
if (!activeCameraRef.current || !modelGroupRef.current) return;
const camera = activeCameraRef.current;
const raycaster = raycasterRef.current;
raycaster.params.Line = { threshold: 4.0 };
raycaster.params.Points = { threshold: 4.0 };
// 1. Tenta o ponto central do mouse
raycaster.setFromCamera(mousePosRef.current, camera);
let intersects = raycaster.intersectObjects(modelGroupRef.current.children, true);
// 2. Se não houver acerto direto, faz amostragem circular de tolerância (magnetismo para peças finas como barras de 13mm e tirantes)
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, camera);
const sampleIntersects = raycaster.intersectObjects(modelGroupRef.current.children, true);
if (sampleIntersects.length > 0) {
intersects = sampleIntersects;
break;
}
}
}
}
if (intersects.length > 0) {
const hitMesh = intersects[0].object as THREE.Mesh;
const expressID = hitMesh.userData.ifcId;
const pmark = hitMesh.userData.pieceMark;
const hitObj = intersects[0].object;
let hitMesh: THREE.Mesh | null = null;
if (modelData && expressID !== undefined) {
const piece = modelData.pieceByExpressID.get(expressID) || {
expressID,
guid: '',
name: hitMesh.userData.section || 'Peça',
type: 'PIECE',
pieceMark: pmark,
section: hitMesh.userData.section,
};
setHoveredPiece(piece);
setHoverPosition({ x: clientX, y: clientY });
if ((hitObj as THREE.Mesh).isMesh) {
hitMesh = hitObj as THREE.Mesh;
} else if (hitObj.parent && (hitObj.parent as THREE.Mesh).isMesh) {
hitMesh = hitObj.parent as THREE.Mesh;
} else {
hitMesh = hitObj as any;
}
if (hitMesh) {
const expressID = hitMesh.userData.ifcId;
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',
pieceMark: pmark,
section: hitMesh.userData.section,
};
setHoveredPiece(piece);
setHoverPosition({ x: clientX, y: clientY });
}
}
}
}, 1500); // 1.5 seconds exact requirement
+2 -2
View File
@@ -37,12 +37,12 @@ interface Viewer3DSidebarProps {
}
const FABRICATION_PROCESSES = [
{ id: 'all', label: 'Todos', icon: Layers, color: '#0284c7' },
{ id: 'all', label: 'Todos', icon: Layers, color: '#475569' },
{ id: 'Corte', label: 'Corte', icon: Scissors, color: '#3b82f6' },
{ id: 'Solda', label: 'Solda', icon: Flame, color: '#f97316' },
{ id: 'Pintura', label: 'Pintura', icon: Paintbrush, color: '#10b981' },
{ id: 'Expedicao', label: 'Expedição', icon: Truck, color: '#06b6d4' },
{ id: 'Montagem', label: 'Montagem', icon: Wrench, color: '#6366f1' },
{ id: 'Montagem', label: 'Montagem', icon: Wrench, color: '#8b5cf6' },
];
export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({