diff --git a/src/components/ViewCube.tsx b/src/components/ViewCube.tsx new file mode 100644 index 0000000..44a3074 --- /dev/null +++ b/src/components/ViewCube.tsx @@ -0,0 +1,149 @@ +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 ( +
+ + + + + +
+ ); +}