llm-semantic-visualizer / src /engine /Interaction.js
hbauzan's picture
Release v4.17.7 — sync from GitHub main
819a47f
Raw
History Blame Contribute Delete
9.77 kB
import * as THREE from 'three';
import { STATE, CONFIG } from '../core/State.js';
import { updateHUD } from '../ui/HUD.js';
export const raycaster = new THREE.Raycaster();
raycaster.params.Points.threshold = 0.5;
export function setupInputListeners(camera, controls) {
let isDragging = false;
let dragged = false;
let startX = 0, startY = 0;
let startRotationY = 0, startRotationX = 0;
// HUD Transparency
controls.addEventListener('lock', () => {
const hud = document.getElementById('hud-sidebar');
if (hud) hud.style.opacity = '0.2';
const guideline = document.getElementById('pointerlock-guideline');
if (guideline) guideline.style.display = 'block';
if (STATE.velocity) STATE.velocity.set(0, 0, 0);
if (STATE.moveState) {
STATE.moveState.forward = false;
STATE.moveState.backward = false;
STATE.moveState.left = false;
STATE.moveState.right = false;
STATE.moveState.up = false;
STATE.moveState.down = false;
STATE.moveState.shift = false;
}
});
controls.addEventListener('unlock', () => {
const hud = document.getElementById('hud-sidebar');
if (hud) hud.style.opacity = '1.0';
const guideline = document.getElementById('pointerlock-guideline');
if (guideline) guideline.style.display = 'none';
if (STATE.velocity) STATE.velocity.set(0, 0, 0);
if (STATE.moveState) {
STATE.moveState.forward = false;
STATE.moveState.backward = false;
STATE.moveState.left = false;
STATE.moveState.right = false;
STATE.moveState.up = false;
STATE.moveState.down = false;
STATE.moveState.shift = false;
}
if (camera && camera.rotation) {
camera.rotation.z = 0;
}
});
const onKeyDown = (e) => {
if (!STATE.isReady) return;
const active = document.activeElement;
const isTyping = active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.tagName === 'SELECT' || active.isContentEditable);
if (isTyping) return;
switch (e.code) {
case 'KeyW': STATE.moveState.forward = true; break;
case 'KeyS': STATE.moveState.backward = true; break;
case 'KeyA': STATE.moveState.left = true; break;
case 'KeyD': STATE.moveState.right = true; break;
case 'KeyQ': STATE.moveState.up = true; break;
case 'KeyE': STATE.moveState.down = true; break;
case 'ShiftLeft': STATE.moveState.shift = true; break;
case 'ArrowLeft':
if (STATE.hunterActive && STATE.hunterResults.length > 0) {
STATE.hunterIndex = (STATE.hunterIndex - 1 + STATE.hunterResults.length) % STATE.hunterResults.length;
STATE.currentSelectedIndex = STATE.hunterResults[STATE.hunterIndex];
const x = STATE.currentSelectedIndex * CONFIG.pointSpacing;
camera.position.x = x;
} else {
STATE.currentSelectedIndex = Math.max(0, STATE.currentSelectedIndex - 1);
}
STATE.currentAnalysis = null;
updateHUD();
break;
case 'ArrowRight':
if (STATE.hunterActive && STATE.hunterResults.length > 0) {
STATE.hunterIndex = (STATE.hunterIndex + 1) % STATE.hunterResults.length;
STATE.currentSelectedIndex = STATE.hunterResults[STATE.hunterIndex];
const x = STATE.currentSelectedIndex * CONFIG.pointSpacing;
camera.position.x = x;
} else {
STATE.currentSelectedIndex = Math.min(STATE.vertexCount - 1, STATE.currentSelectedIndex + 1);
}
STATE.currentAnalysis = null;
updateHUD();
break;
case 'KeyF':
const idx = STATE.currentSelectedIndex;
const mode = STATE.currentZMode;
const spacing = CONFIG.pointSpacing || 0.1;
let targetX = 0, targetY = 0, targetZ = 0;
let camX = 0, camY = 0, camZ = 0;
if (mode === 'radial') {
// Radial Mode
targetX = 0; targetY = 0; targetZ = 0;
camX = 5; camY = 5; camZ = (idx * spacing) + 15;
} else {
// Linear/Log Mode
targetX = idx * spacing;
targetY = 0; targetZ = 0;
camX = targetX; camY = 5; camZ = 10;
}
if (camera) {
camera.position.set(camX, camY, camZ);
camera.lookAt(targetX, targetY, targetZ);
}
break;
}
};
const onKeyUp = (e) => {
switch (e.code) {
case 'KeyW': STATE.moveState.forward = false; break;
case 'KeyS': STATE.moveState.backward = false; break;
case 'KeyA': STATE.moveState.left = false; break;
case 'KeyD': STATE.moveState.right = false; break;
case 'KeyQ': STATE.moveState.up = false; break;
case 'KeyE': STATE.moveState.down = false; break;
case 'ShiftLeft': STATE.moveState.shift = false; break;
}
};
const isInteractiveUIElement = (target) => {
if (!target) return false;
return target.closest(
'input, button, select, option, label, a, textarea, table, tr, td, th, svg, path, ' +
'#sidebar, #hud-sidebar, #relief-hud, #dimension-inspector, .app-panel, .modal-overlay, ' +
'[class*="panel"], [id*="panel"], [class*="modal"], [id*="modal"], [class*="hud"], [id*="hud"]'
);
};
const safeLock = () => {
if (!controls || controls.isLocked) return;
try {
const p = controls.lock();
if (p && typeof p.catch === 'function') {
p.catch(() => {});
}
} catch (e) {}
};
const safeUnlock = () => {
if (!controls || !controls.isLocked) return;
try {
controls.unlock();
} catch (e) {}
};
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
document.addEventListener('mousedown', (e) => {
if (!STATE.isReady || e.button !== 0) return;
if (isInteractiveUIElement(e.target)) return;
dragged = false;
if (controls.isLocked) {
STATE.isTargetLocked = !STATE.isTargetLocked;
// Fijar/liberar la lectura del HUD de Relieve sobre el punto apuntado al hacer clic.
if (STATE.isTargetLocked) {
STATE.reliefPinned = STATE.reliefHover ? { ...STATE.reliefHover } : null;
safeUnlock();
} else {
STATE.reliefPinned = null;
}
} else if (STATE.isTargetLocked) {
STATE.isTargetLocked = false;
STATE.reliefPinned = null;
safeLock();
} else {
// Drag-to-look fallback initialization
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startRotationY = camera.rotation.y;
startRotationX = camera.rotation.x;
}
});
document.addEventListener('mousemove', (e) => {
if (!isDragging || controls.isLocked) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
dragged = true;
}
const sensitivity = 0.002;
camera.rotation.y = startRotationY - deltaX * sensitivity;
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, startRotationX - deltaY * sensitivity));
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
// Touch events for drag-to-look fallback
document.addEventListener('touchstart', (e) => {
if (!STATE.isReady || controls.isLocked || e.touches.length === 0) return;
if (isInteractiveUIElement(e.target)) return;
isDragging = true;
dragged = false;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startRotationY = camera.rotation.y;
startRotationX = camera.rotation.x;
}, { passive: true });
document.addEventListener('touchmove', (e) => {
if (!isDragging || controls.isLocked || e.touches.length === 0) return;
const deltaX = e.touches[0].clientX - startX;
const deltaY = e.touches[0].clientY - startY;
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
dragged = true;
}
const sensitivity = 0.003;
camera.rotation.y = startRotationY - deltaX * sensitivity;
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, startRotationX - deltaY * sensitivity));
}, { passive: true });
document.addEventListener('touchend', () => {
isDragging = false;
}, { passive: true });
// PointerLockError fallback registration
document.addEventListener('pointerlockerror', (e) => {
console.warn("PointerLock error encountered, using drag-to-look fallback:", e);
});
document.addEventListener('click', (e) => {
if (!STATE.isReady || controls.isLocked || STATE.isTargetLocked) return;
if (dragged) {
dragged = false;
return;
}
if (!isInteractiveUIElement(e.target)) {
safeLock();
}
});
}