/** * AxisGizmo.js * Indicador de orientación de ejes (estilo Blender) fijo en una esquina del * viewport. Renderiza una mini-escena con 3 flechas — X (rojo), Y (verde), * Z (azul) — y etiquetas de texto, que rotan junto con la cámara principal * para que el usuario nunca pierda la orientación mientras navega. * * Semántica en el Relieve: X = dimensión (1→1024), Y = valor, Z = chunk. * * API: * initAxisGizmo() → construye la mini-escena * renderAxisGizmo(renderer, camera) → dibujar en la esquina (post main render) */ import * as THREE from 'three'; import { PALETTE_3D } from './palette.js'; const AXIS_COLORS = { x: PALETTE_3D.AXIS_X, // rojo y: PALETTE_3D.AXIS_Y, // verde z: PALETTE_3D.AXIS_Z, // azul }; const GIZMO_SIZE = 120; // px del recuadro en la esquina const GIZMO_MARGIN = 16; // px de separación al borde let _gizmoScene = null; let _gizmoCamera = null; let _gizmoGroup = null; const _size = new THREE.Vector2(); // --------------------------------------------------------------------------- // Etiqueta de texto como sprite (canvas → textura) // --------------------------------------------------------------------------- function _makeLabel(text, colorHex) { const s = 64; const canvas = document.createElement('canvas'); canvas.width = s; canvas.height = s; const ctx = canvas.getContext('2d'); ctx.fillStyle = '#' + colorHex.toString(16).padStart(6, '0'); ctx.font = 'bold 44px Arial, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(text, s / 2, s / 2); const texture = new THREE.CanvasTexture(canvas); texture.anisotropy = 4; const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false }); const sprite = new THREE.Sprite(material); sprite.scale.set(0.55, 0.55, 0.55); return sprite; } // --------------------------------------------------------------------------- // Una flecha de eje + su etiqueta, alineada al vector dir // --------------------------------------------------------------------------- function _makeAxis(dir, colorHex, label) { const group = new THREE.Group(); const arrow = new THREE.ArrowHelper( dir.clone().normalize(), new THREE.Vector3(0, 0, 0), 1.0, // longitud colorHex, 0.32, // headLength 0.22 // headWidth ); // Engrosar la línea del eje (en plataformas que lo soporten) if (arrow.line && arrow.line.material) arrow.line.material.linewidth = 2; group.add(arrow); const sprite = _makeLabel(label, colorHex); sprite.position.copy(dir.clone().normalize().multiplyScalar(1.35)); group.add(sprite); return group; } // --------------------------------------------------------------------------- // Inicialización de la mini-escena del gizmo // --------------------------------------------------------------------------- export function initAxisGizmo() { _gizmoScene = new THREE.Scene(); _gizmoCamera = new THREE.PerspectiveCamera(50, 1, 0.1, 20); _gizmoCamera.position.set(0, 0, 3.4); _gizmoCamera.lookAt(0, 0, 0); _gizmoGroup = new THREE.Group(); _gizmoGroup.add(_makeAxis(new THREE.Vector3(1, 0, 0), AXIS_COLORS.x, 'X')); _gizmoGroup.add(_makeAxis(new THREE.Vector3(0, 1, 0), AXIS_COLORS.y, 'Y')); _gizmoGroup.add(_makeAxis(new THREE.Vector3(0, 0, 1), AXIS_COLORS.z, 'Z')); _gizmoScene.add(_gizmoGroup); return { scene: _gizmoScene, camera: _gizmoCamera, group: _gizmoGroup }; } // --------------------------------------------------------------------------- // Render del gizmo en la esquina inferior derecha (llamar tras el render principal) // --------------------------------------------------------------------------- export function renderAxisGizmo(renderer, mainCamera) { if (!_gizmoScene) return; // El gizmo muestra los ejes del MUNDO tal como los ve la cámara principal: // rotamos el grupo por el inverso de la orientación de la cámara. _gizmoGroup.quaternion.copy(mainCamera.quaternion).invert(); renderer.getSize(_size); const left = _size.x - GIZMO_SIZE - GIZMO_MARGIN; const bottom = GIZMO_MARGIN; const prevAutoClear = renderer.autoClear; renderer.autoClear = false; renderer.setViewport(left, bottom, GIZMO_SIZE, GIZMO_SIZE); renderer.setScissor(left, bottom, GIZMO_SIZE, GIZMO_SIZE); renderer.setScissorTest(true); renderer.clearDepth(); // dibujar el gizmo por encima de la escena renderer.render(_gizmoScene, _gizmoCamera); // Restaurar estado del renderer para el siguiente frame renderer.setScissorTest(false); renderer.setViewport(0, 0, _size.x, _size.y); renderer.autoClear = prevAutoClear; }