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, calibration, startCalibration, cancelCalibration, pushCubeFace, subscribeCalibration, } from './three/viewCubeBus'; import { useModelStore } from '@/stores/useModelStore'; import { getModelLocalGroup } from '@/lib/modelTransforms'; import { Check, RotateCcw, Maximize2 } 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 { const size = 256; const c = document.createElement('canvas'); c.width = size; c.height = size; const ctx = c.getContext('2d')!; 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); ctx.strokeStyle = '#00f3ff'; ctx.lineWidth = 6; ctx.strokeRect(3, 3, size - 6, size - 6); ctx.fillStyle = '#e6faff'; ctx.font = '700 56px JetBrains Mono, ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.shadowColor = '#00f3ff'; ctx.shadowBlur = 18; ctx.fillText(label, size / 2, size / 2); const tex = new THREE.CanvasTexture(c); tex.anisotropy = 4; tex.needsUpdate = true; return tex; } const FACE_DEFS: { label: string; dir: [number, number, number] }[] = [ { label: 'DIR', dir: [ 1, 0, 0] }, { label: 'ESQ', dir: [-1, 0, 0] }, { label: 'TOPO', dir: [ 0, 1, 0] }, { label: 'BASE', dir: [ 0,-1, 0] }, { label: 'FRENTE', dir: [ 0, 0, 1] }, { label: 'ATRÁS', dir: [ 0, 0,-1] }, ]; function CubeMesh({ calibrating, activeModelId }: { calibrating: boolean; activeModelId: string | null }) { const meshRef = useRef(null); const groupRef = useRef(null); const { camera: localCam } = useThree(); const [hover, setHover] = useState(null); const materials = useMemo(() => { return FACE_DEFS.map((f, i) => { const tex = makeFaceTexture(f.label, false); const mat = new THREE.MeshBasicMaterial({ map: tex }); mat.userData.baseTex = tex; mat.userData.hoverTex = makeFaceTexture(f.label, true); mat.userData.faceIndex = i; return mat; }); }, []); useEffect(() => { return () => { materials.forEach((m) => { m.userData.baseTex?.dispose?.(); m.userData.hoverTex?.dispose?.(); m.dispose(); }); }; }, [materials]); useEffect(() => { materials.forEach((m, i) => { m.map = i === hover ? m.userData.hoverTex : m.userData.baseTex; m.needsUpdate = true; }); }, [hover, materials]); // Reusable temp objects for the per-frame model-quaternion sync. const modelQuat = useMemo(() => new THREE.Quaternion(), []); /** Returns the active model's current world quaternion (fineTuning rotation * + calibration). Used to keep the cube glued to the piece's pose. */ const getModelWorldQuat = (): THREE.Quaternion => { const g = getModelLocalGroup(activeModelId); if (g) { g.updateWorldMatrix(true, false); g.getWorldQuaternion(modelQuat); return modelQuat; } modelQuat.identity(); return modelQuat; }; 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(); localCam.position.copy(dir.multiplyScalar(3)); localCam.up.copy(main.up); localCam.lookAt(0, 0, 0); // Sync the cube group orientation with the active model so it always // mirrors the piece's pose (rotation from Posicionar, calibration, etc). if (groupRef.current) { groupRef.current.quaternion.copy(getModelWorldQuat()); } }); const onClick = (e: ThreeEvent) => { e.stopPropagation(); const idx = e.face?.materialIndex; if (idx == null) return; const def = FACE_DEFS[idx]; if (!def) return; // Convert the local cube-face direction to world space using the model's // current world quaternion — this keeps cube ↔ model coherent after any // rotation/translation/reset of the piece. const dirWorld = new THREE.Vector3(...def.dir).applyQuaternion(getModelWorldQuat()).normalize(); if (calibrating) { pushCubeFace(dirWorld); } else { requestView(dirWorld); } }; // 70% of previous 1.4 → ~0.98 const SIZE = 0.98; return ( { e.stopPropagation(); const idx = e.face?.materialIndex ?? null; setHover(idx); }} onPointerOut={() => setHover(null)} > ); } function AxisTripod() { return ( ); } 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 = () => { const store = useModelStore.getState(); if (isCalibrating) { // Cancel mid-flow — restore previous camera mode if we forced ortho. const prev = (window as any).__tsxr_prevCamMode as 'ortho' | 'persp' | undefined; if (prev) { store.setCameraMode(prev); (window as any).__tsxr_prevCamMode = undefined; } cancelCalibration(); toast.info('Calibração cancelada'); return; } if (isDone) { const prev = (window as any).__tsxr_prevCamMode as 'ortho' | 'persp' | undefined; if (prev) { store.setCameraMode(prev); (window as any).__tsxr_prevCamMode = undefined; } 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; } // Force orthographic view for accurate face picking. (window as any).__tsxr_prevCamMode = store.cameraMode; if (store.cameraMode !== 'ortho') store.setCameraMode('ortho'); startCalibration(active.id); toast('Calibração em vista ortogonal — 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 && ( )} {/* Botão de Tela Cheia */}
); }