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'; /** 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')!; // 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'; 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] }[] = [ // 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] }, { label: 'BASE', dir: [ 0,-1, 0] }, { label: 'FRENTE', dir: [ 0, 0, 1] }, { label: 'ATRÁS', dir: [ 0, 0,-1] }, ]; function CubeMesh() { const meshRef = 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]); // Apply hover tint useEffect(() => { materials.forEach((m, i) => { m.map = i === hover ? m.userData.hoverTex : m.userData.baseTex; m.needsUpdate = true; }); }, [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); }); const onClick = (e: ThreeEvent) => { e.stopPropagation(); const idx = e.face?.materialIndex; if (idx == null) return; const def = FACE_DEFS[idx]; if (!def) return; requestView(new THREE.Vector3(...def.dir)); }; return ( { e.stopPropagation(); const idx = e.face?.materialIndex ?? null; setHover(idx); }} onPointerOut={() => setHover(null)} > ); } /** Tiny axis tripod next to the cube to reinforce the UCS reading. */ function AxisTripod() { return ( ); } export function ViewCube() { return (
); }