From eee5b80d349230960174550befd110175eea278b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 20:07:58 +0000 Subject: [PATCH] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/ViewCube.tsx | 162 +++++++++++++++++++++++++++++++----- 1 file changed, 140 insertions(+), 22 deletions(-) diff --git a/src/components/ViewCube.tsx b/src/components/ViewCube.tsx index 44a3074..0e79926 100644 --- a/src/components/ViewCube.tsx +++ b/src/components/ViewCube.tsx @@ -1,7 +1,19 @@ import { useRef, useMemo, useEffect, useState } from 'react'; import { Canvas, useFrame, useThree, ThreeEvent } from '@react-three/fiber'; import * as THREE from 'three'; -import { mainCameraRef, mainControlsRef, requestView } from './three/viewCubeBus'; +import { + mainCameraRef, + mainControlsRef, + requestView, + calibration, + startCalibration, + cancelCalibration, + pushCubeFace, + subscribeCalibration, +} from './three/viewCubeBus'; +import { useModelStore } from '@/stores/useModelStore'; +import { Check, RotateCcw } from 'lucide-react'; +import { toast } from 'sonner'; /** Builds a square canvas texture with a label centered on it. */ function makeFaceTexture(label: string, accent: boolean): THREE.CanvasTexture { @@ -10,17 +22,14 @@ function makeFaceTexture(label: string, accent: boolean): THREE.CanvasTexture { c.width = size; c.height = size; const ctx = c.getContext('2d')!; - // Background const grad = ctx.createLinearGradient(0, 0, 0, size); grad.addColorStop(0, accent ? '#1a2742' : '#101725'); grad.addColorStop(1, accent ? '#0c1424' : '#070b14'); ctx.fillStyle = grad; ctx.fillRect(0, 0, size, size); - // Border ctx.strokeStyle = '#00f3ff'; ctx.lineWidth = 6; ctx.strokeRect(3, 3, size - 6, size - 6); - // Label ctx.fillStyle = '#e6faff'; ctx.font = '700 56px JetBrains Mono, ui-monospace, monospace'; ctx.textAlign = 'center'; @@ -35,7 +44,6 @@ function makeFaceTexture(label: string, accent: boolean): THREE.CanvasTexture { } const FACE_DEFS: { label: string; dir: [number, number, number] }[] = [ - // BoxGeometry material order: +X, -X, +Y, -Y, +Z, -Z { label: 'DIR', dir: [ 1, 0, 0] }, { label: 'ESQ', dir: [-1, 0, 0] }, { label: 'TOPO', dir: [ 0, 1, 0] }, @@ -44,7 +52,7 @@ const FACE_DEFS: { label: string; dir: [number, number, number] }[] = [ { label: 'ATRÁS', dir: [ 0, 0,-1] }, ]; -function CubeMesh() { +function CubeMesh({ calibrating }: { calibrating: boolean }) { const meshRef = useRef(null); const { camera: localCam } = useThree(); const [hover, setHover] = useState(null); @@ -70,7 +78,6 @@ function CubeMesh() { }; }, [materials]); - // Apply hover tint useEffect(() => { materials.forEach((m, i) => { m.map = i === hover ? m.userData.hoverTex : m.userData.baseTex; @@ -78,14 +85,12 @@ function CubeMesh() { }); }, [hover, materials]); - // Mirror main camera orientation each frame useFrame(() => { const main = mainCameraRef.current; const controls = mainControlsRef.current; if (!main) return; const target: THREE.Vector3 = controls?.target ?? new THREE.Vector3(); const dir = new THREE.Vector3().subVectors(main.position, target).normalize(); - // Place local cam along same direction at fixed distance localCam.position.copy(dir.multiplyScalar(3)); localCam.up.copy(main.up); localCam.lookAt(0, 0, 0); @@ -97,9 +102,16 @@ function CubeMesh() { if (idx == null) return; const def = FACE_DEFS[idx]; if (!def) return; - requestView(new THREE.Vector3(...def.dir)); + const dirVec = new THREE.Vector3(...def.dir); + if (calibrating) { + pushCubeFace(dirVec); + } else { + requestView(dirVec); + } }; + // 70% of previous 1.4 → ~0.98 + const SIZE = 0.98; return ( setHover(null)} > - + ); } -/** Tiny axis tripod next to the cube to reinforce the UCS reading. */ function AxisTripod() { return ( @@ -128,22 +139,129 @@ function AxisTripod() { ); } +const STEP_HINTS: Record = { + 'await-cube-1': '1/3 — clique numa face do cubo', + 'await-model-1': '1/3 — clique na face correspondente da peça', + 'await-cube-2': '2/3 — clique noutra face do cubo', + 'await-model-2': '2/3 — clique na face correspondente da peça', + 'await-cube-3': '3/3 (verificação) — clique numa terceira face do cubo (opcional)', + 'await-model-3': '3/3 (verificação) — clique na face correspondente da peça', + 'done': 'Calibração concluída', +}; + export function ViewCube() { + const [, force] = useState(0); + useEffect(() => subscribeCalibration(() => force(t => t + 1)), []); + const activeModelId = useModelStore((s) => s.activeModelId); + const models = useModelStore((s) => s.models); + const setCalibrationStore = useModelStore((s) => s.setCalibration); + const active = models.find(m => m.id === activeModelId); + + const isCalibrating = calibration.step !== 'idle' && calibration.step !== 'done'; + const isDone = calibration.step === 'done'; + const progress = isDone ? 1 : calibration.progress; + + const onClickCalibrar = () => { + if (isCalibrating) { + // Cancel mid-flow + cancelCalibration(); + toast.info('Calibração cancelada'); + return; + } + if (isDone) { + // Reset/finish state + cancelCalibration(); + return; + } + if (!active) { + toast.error('Selecione uma peça antes de calibrar'); + return; + } + if (active.locked) { + toast.error('Peça travada — destranque o cadeado para calibrar'); + return; + } + startCalibration(active.id); + toast('Calibração iniciada — clique numa face do cubo, depois na face correspondente da peça', { duration: 4000 }); + }; + + const onResetCalibration = () => { + if (!active) return; + setCalibrationStore(active.id, null); + toast.success('Calibração removida'); + }; + return (
- - - - - + + + + + +
+ + {/* Calibrar button with progress fill */} + + + {/* Step hint while calibrating */} + {(isCalibrating || isDone) && ( +
+ {STEP_HINTS[calibration.step] ?? ''} + {isDone && Number.isFinite(calibration.verifyErrorDeg) && ( +
+ erro {calibration.verifyErrorDeg.toFixed(1)}° +
+ )} +
+ )} + + {/* Reset existing calibration */} + {!isCalibrating && active?.calibrationQuat && ( + + )} ); }