Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
+140
-22
@@ -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<THREE.Mesh>(null);
|
||||
const { camera: localCam } = useThree();
|
||||
const [hover, setHover] = useState<number | null>(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 (
|
||||
<mesh
|
||||
ref={meshRef}
|
||||
@@ -112,12 +124,11 @@ function CubeMesh() {
|
||||
}}
|
||||
onPointerOut={() => setHover(null)}
|
||||
>
|
||||
<boxGeometry args={[1.4, 1.4, 1.4]} />
|
||||
<boxGeometry args={[SIZE, SIZE, SIZE]} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
/** Tiny axis tripod next to the cube to reinforce the UCS reading. */
|
||||
function AxisTripod() {
|
||||
return (
|
||||
<group position={[-1.2, -1.2, 0]}>
|
||||
@@ -128,22 +139,129 @@ function AxisTripod() {
|
||||
);
|
||||
}
|
||||
|
||||
const STEP_HINTS: Record<string, string> = {
|
||||
'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 (
|
||||
<div
|
||||
className="absolute right-3 top-3 z-20 h-[120px] w-[120px] rounded-md border border-primary/30 bg-background/40 backdrop-blur-sm shadow-lg"
|
||||
className="absolute right-3 top-3 z-20 w-[120px] flex flex-col items-stretch gap-1.5"
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
title="Cubo de vistas — clique numa face para girar"
|
||||
>
|
||||
<Canvas
|
||||
camera={{ position: [2, 2, 2], fov: 35, near: 0.1, far: 50 }}
|
||||
gl={{ antialias: true, alpha: true }}
|
||||
dpr={[1, 2]}
|
||||
<div
|
||||
className="h-[120px] w-[120px] rounded-md border border-primary/30 bg-background/40 backdrop-blur-sm shadow-lg"
|
||||
title="Cubo de vistas — clique numa face para girar"
|
||||
>
|
||||
<ambientLight intensity={1} />
|
||||
<CubeMesh />
|
||||
<AxisTripod />
|
||||
</Canvas>
|
||||
<Canvas
|
||||
camera={{ position: [2, 2, 2], fov: 35, near: 0.1, far: 50 }}
|
||||
gl={{ antialias: true, alpha: true }}
|
||||
dpr={[1, 2]}
|
||||
>
|
||||
<ambientLight intensity={1} />
|
||||
<CubeMesh calibrating={isCalibrating} />
|
||||
<AxisTripod />
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
{/* Calibrar button with progress fill */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClickCalibrar}
|
||||
className={[
|
||||
'relative h-7 w-full overflow-hidden rounded border font-mono text-[10px] uppercase tracking-widest transition-colors',
|
||||
isDone
|
||||
? 'border-success/60 bg-success/20 text-success'
|
||||
: isCalibrating
|
||||
? 'border-amber-400/60 bg-amber-400/10 text-amber-300'
|
||||
: 'border-primary/40 bg-background/40 text-primary hover:bg-primary/10',
|
||||
].join(' ')}
|
||||
title={isCalibrating ? 'Cancelar calibração' : 'Calibrar orientação da peça com o cubo'}
|
||||
>
|
||||
{/* progress bar */}
|
||||
<span
|
||||
className={[
|
||||
'absolute inset-y-0 left-0 transition-all duration-200',
|
||||
isDone ? 'bg-success/40' : 'bg-primary/30',
|
||||
].join(' ')}
|
||||
style={{ width: `${Math.round(progress * 100)}%` }}
|
||||
/>
|
||||
<span className="relative z-10 flex items-center justify-center gap-1">
|
||||
{isDone ? <Check className="h-3 w-3" /> : null}
|
||||
{isDone ? 'Calibrado' : isCalibrating ? 'Cancelar' : 'Calibrar'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Step hint while calibrating */}
|
||||
{(isCalibrating || isDone) && (
|
||||
<div className="rounded border border-primary/30 bg-background/80 px-2 py-1 text-center font-mono text-[9px] leading-tight text-foreground/90 backdrop-blur-sm">
|
||||
{STEP_HINTS[calibration.step] ?? ''}
|
||||
{isDone && Number.isFinite(calibration.verifyErrorDeg) && (
|
||||
<div className="mt-0.5 text-success">
|
||||
erro {calibration.verifyErrorDeg.toFixed(1)}°
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reset existing calibration */}
|
||||
{!isCalibrating && active?.calibrationQuat && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onResetCalibration}
|
||||
className="flex h-6 w-full items-center justify-center gap-1 rounded border border-muted-foreground/30 bg-background/40 font-mono text-[9px] uppercase tracking-widest text-muted-foreground hover:text-foreground hover:border-muted-foreground/60"
|
||||
title="Remover calibração salva"
|
||||
>
|
||||
<RotateCcw className="h-2.5 w-2.5" /> reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user