From 2bce52fe548fbd7c03a8949cd6b1e289c739fee6 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Sun, 24 May 2026 18:28:15 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Auto-deploy:=20melhoria=20no=20s?= =?UTF-8?q?nap=20e=20medi=C3=A7=C3=A3o=20AR=20em=2024/05/2026=2018:28:15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ViewCube.tsx | 221 ++++++++++++++++++++++++++ src/components/three/XRHudInWorld.tsx | 102 +++++++++--- src/pages/Viewer.tsx | 3 +- 3 files changed, 299 insertions(+), 27 deletions(-) create mode 100644 src/components/ViewCube.tsx diff --git a/src/components/ViewCube.tsx b/src/components/ViewCube.tsx new file mode 100644 index 0000000..67dfe56 --- /dev/null +++ b/src/components/ViewCube.tsx @@ -0,0 +1,221 @@ +import { useState, useEffect } from 'react'; +import { useModelStore } from '@/stores/useModelStore'; +import { Button } from '@/components/ui/button'; +import { RotateCw, RefreshCw, Move, HelpCircle } from 'lucide-react'; +import { toast } from 'sonner'; + +export function ViewCube() { + const { activeModelId, models, setFineTuning } = useModelStore(); + const activeModel = models.find((m) => m.id === activeModelId); + + // Estados locais para controlar uma rotação interativa ou visualização do cubo + const [hoveredFace, setHoveredFace] = useState(null); + + if (!activeModelId || !activeModel) return null; + + const alignToFace = (face: string) => { + let rotX = 0; + let rotY = 0; + let rotZ = 0; + + switch (face) { + case 'TOPO': + rotX = 0; + rotY = 0; + rotZ = 0; + break; + case 'FRENTE': + rotX = 90; + rotY = 0; + rotZ = 0; + break; + case 'DIREITA': + rotX = 90; + rotY = 0; + rotZ = -90; + break; + case 'ESQUERDA': + rotX = 90; + rotY = 0; + rotZ = 90; + break; + case 'ATRÁS': + rotX = -90; + rotY = 180; + rotZ = 0; + break; + case 'BASE': + rotX = 180; + rotY = 0; + rotZ = 0; + break; + } + + setFineTuning({ rotX, rotY, rotZ }); + toast.success(`Alinhado para vista ${face}`); + }; + + // Ajusta a rotação para o múltiplo de 90 graus mais próximo + const snapToNearest90 = () => { + const ft = activeModel.fineTuning; + const rotX = Math.round(ft.rotX / 90) * 90; + const rotY = Math.round(ft.rotY / 90) * 90; + const rotZ = Math.round(ft.rotZ / 90) * 90; + setFineTuning({ rotX, rotY, rotZ }); + toast.success(`Snap Ortogonal (90°): [${rotX}°, ${rotY}°, ${rotZ}°]`); + }; + + const resetAllTransforms = () => { + setFineTuning({ posX: 0, posY: 0, posZ: 0, rotX: 0, rotY: 0, rotZ: 0, scale: 1 }); + toast.success('Posição e rotação restauradas para a origem'); + }; + + return ( +
+ {/* Título do Widget */} + + Alinhamento 3D + + + {/* Cubo 3D Visual */} +
+ {/* Container do Cubo Isométrico */} +
+ {/* TOPO (Z+) */} +
alignToFace('TOPO')} + onMouseEnter={() => setHoveredFace('TOPO')} + onMouseLeave={() => setHoveredFace(null)} + className={`absolute h-14 w-14 border border-white/20 flex items-center justify-center font-mono text-[9px] font-bold cursor-pointer transition-all duration-200 ${ + hoveredFace === 'TOPO' ? 'bg-primary/40 border-primary text-white shadow-[0_0_10px_rgba(59,130,246,0.5)]' : 'bg-zinc-800/80 text-zinc-300' + }`} + style={{ + transform: 'rotateX(90deg) translateZ(28px)', + backfaceVisibility: 'hidden', + }} + > + TOPO +
+ + {/* FRENTE (Y-) */} +
alignToFace('FRENTE')} + onMouseEnter={() => setHoveredFace('FRENTE')} + onMouseLeave={() => setHoveredFace(null)} + className={`absolute h-14 w-14 border border-white/20 flex items-center justify-center font-mono text-[9px] font-bold cursor-pointer transition-all duration-200 ${ + hoveredFace === 'FRENTE' ? 'bg-primary/40 border-primary text-white shadow-[0_0_10px_rgba(59,130,246,0.5)]' : 'bg-zinc-700/80 text-zinc-300' + }`} + style={{ + transform: 'translateZ(28px)', + backfaceVisibility: 'hidden', + }} + > + FRENTE +
+ + {/* DIREITA (X+) */} +
alignToFace('DIREITA')} + onMouseEnter={() => setHoveredFace('DIREITA')} + onMouseLeave={() => setHoveredFace(null)} + className={`absolute h-14 w-14 border border-white/20 flex items-center justify-center font-mono text-[9px] font-bold cursor-pointer transition-all duration-200 ${ + hoveredFace === 'DIREITA' ? 'bg-primary/40 border-primary text-white shadow-[0_0_10px_rgba(59,130,246,0.5)]' : 'bg-zinc-850/80 text-zinc-300' + }`} + style={{ + transform: 'rotateY(90deg) translateZ(28px)', + backfaceVisibility: 'hidden', + }} + > + DIR +
+ + {/* ESQUERDA (X-) */} +
alignToFace('ESQUERDA')} + onMouseEnter={() => setHoveredFace('ESQUERDA')} + onMouseLeave={() => setHoveredFace(null)} + className={`absolute h-14 w-14 border border-white/20 flex items-center justify-center font-mono text-[9px] font-bold cursor-pointer transition-all duration-200 ${ + hoveredFace === 'ESQUERDA' ? 'bg-primary/40 border-primary text-white shadow-[0_0_10px_rgba(59,130,246,0.5)]' : 'bg-zinc-850/80 text-zinc-300' + }`} + style={{ + transform: 'rotateY(-90deg) translateZ(28px)', + backfaceVisibility: 'hidden', + }} + > + ESQ +
+ + {/* ATRÁS (Y+) */} +
alignToFace('ATRÁS')} + onMouseEnter={() => setHoveredFace('ATRÁS')} + onMouseLeave={() => setHoveredFace(null)} + className={`absolute h-14 w-14 border border-white/20 flex items-center justify-center font-mono text-[9px] font-bold cursor-pointer transition-all duration-200 ${ + hoveredFace === 'ATRÁS' ? 'bg-primary/40 border-primary text-white shadow-[0_0_10px_rgba(59,130,246,0.5)]' : 'bg-zinc-750/80 text-zinc-300' + }`} + style={{ + transform: 'rotateY(180deg) translateZ(28px)', + backfaceVisibility: 'hidden', + }} + > + ATRÁS +
+ + {/* BASE (Z-) */} +
alignToFace('BASE')} + onMouseEnter={() => setHoveredFace('BASE')} + onMouseLeave={() => setHoveredFace(null)} + className={`absolute h-14 w-14 border border-white/20 flex items-center justify-center font-mono text-[9px] font-bold cursor-pointer transition-all duration-200 ${ + hoveredFace === 'BASE' ? 'bg-primary/40 border-primary text-white shadow-[0_0_10px_rgba(59,130,246,0.5)]' : 'bg-zinc-900/80 text-zinc-300' + }`} + style={{ + transform: 'rotateX(-90deg) translateZ(28px)', + backfaceVisibility: 'hidden', + }} + > + BASE +
+
+
+ + {/* Botões Rápidos de Alinhamento */} +
+ + + +
+ + {/* Info compacta */} +
+ X: {Math.round(activeModel.fineTuning.rotX)}° + Y: {Math.round(activeModel.fineTuning.rotY)}° + Z: {Math.round(activeModel.fineTuning.rotZ)}° +
+
+ ); +} diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index 0d8f713..cb99138 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -363,75 +363,125 @@ function ToolsTab(p: XRHudInWorldProps) { const resetScale = useModelStore((s) => s.resetScale); const clearMeasurements = useModelStore((s) => s.clearMeasurements); + const activeId = useModelStore((s) => s.activeModelId); + const models = useModelStore((s) => s.models); + const activeModel = models.find((m) => m.id === activeId); + const setFineTuning = useModelStore((s) => s.setFineTuning); + + const alignToFace = (face: string) => { + let rotX = 0, rotY = 0, rotZ = 0; + if (face === 'FRENTE') { rotX = 90; } + else if (face === 'LADO') { rotX = 90; rotY = 90; } + setFineTuning({ rotX, rotY, rotZ }); + }; + + const snapTo90 = () => { + if (!activeModel) return; + const ft = activeModel.fineTuning; + setFineTuning({ + rotX: Math.round(ft.rotX / 90) * 90, + rotY: Math.round(ft.rotY / 90) * 90, + rotZ: Math.round(ft.rotZ / 90) * 90, + }); + }; + + const resetRot = () => { + setFineTuning({ rotX: 0, rotY: 0, rotZ: 0 }); + }; + return ( - Render - Render + setRenderMode('solid')} /> - setRenderMode('edges')} /> - Toggles - Toggles + setShowGrid(!showGrid)} /> - setMeasureMode(!measureMode)} /> - - - - {/* Selecionar — paridade com a tela inicial */} - Selecionar - Selecionar + 0 ? `${selectedCount} sel.` : 'Selec…') : 'Selec'} active={selectionMode} onClick={() => setSelectionMode(!selectionMode)} /> - - - - + {/* Fileira 4: Opacidade */} + Opacidade {Math.round(opacity * 100)}% - setOpacity(Math.max(0, opacity - 0.25))} /> - setOpacity(Math.max(0, opacity - 0.05))} /> - setOpacity(Math.min(1, opacity + 0.05))} /> - setOpacity(Math.min(1, opacity + 0.25))} /> - setOpacity(1)} /> - + {/* Fileira 5: Escala */} + Escala {scaleRatio.label} {SCALE_LABELS.map((lbl, i) => { const preset = SCALE_PRESETS.find((sp) => sp.label === lbl)!; return ( setScaleRatio(preset)} /> ); })} - - + {/* Fileira 6: Rotação / Alinhamento Ortogonal */} + + Alinhar Rotação + + + alignToFace('TOPO')} fontSize={0.0075} /> + alignToFace('FRENTE')} fontSize={0.0075} /> + alignToFace('LADO')} fontSize={0.0075} /> + + {measureMode && ( - + Gatilho D: marcar · A: desfazer · B: limpar · Gatilho E: alternar snap )} diff --git a/src/pages/Viewer.tsx b/src/pages/Viewer.tsx index 14010dd..26b6c2a 100644 --- a/src/pages/Viewer.tsx +++ b/src/pages/Viewer.tsx @@ -3,8 +3,8 @@ import { ArrowLeft, Glasses, Box, Ruler, FileText, Upload, Loader2 } from "lucid import { Button } from "@/components/ui/button"; import { useModelStore } from "@/stores/useModelStore"; import { ModelViewerCanvas } from "@/components/three/ModelViewer"; -import { xrStore } from "@/lib/xrStore"; import { ViewerControls } from "@/components/ViewerControls"; +import { ViewCube } from "@/components/ViewCube"; import { InspectionChecklist } from "@/components/InspectionChecklist"; import { FineTuningControls } from "@/components/FineTuningControls"; import { MeasurementsList } from "@/components/MeasurementsList"; @@ -143,6 +143,7 @@ const Viewer = () => {
+
{/* Side panel */}