Spaces:
Sleeping
Sleeping
| import * as THREE from 'three'; | |
| import { STATE } from '../core/State.js'; | |
| import { updateNavigation } from './Navigation.js'; | |
| import { raycaster } from './Interaction.js'; | |
| import { getActiveReliefObjects } from '../visualizer/ReliefRenderer.js'; | |
| import { updateHUD } from '../ui/HUD.js'; | |
| import { renderAxisGizmo } from './AxisGizmo.js'; | |
| let rafId = null; | |
| /** | |
| * Starts the main requestAnimationFrame loop: navigation, raycast selection, | |
| * relief HUD per-frame updates, HUD refresh, and scene render. | |
| * | |
| * Relief index/HUD helpers are injected so H11.2 can relocate them to | |
| * ReliefHUDController without touching the loop wiring again. | |
| */ | |
| export function startRenderLoop({ | |
| scene, | |
| camera, | |
| renderer, | |
| controls, | |
| computeReliefIndices, | |
| fillReliefHUD, | |
| }) { | |
| stopRenderLoop(); | |
| const clock = new THREE.Clock(); | |
| const lastCamPos = new THREE.Vector3(); | |
| const lastCamRot = new THREE.Euler(); | |
| let lastIntersects = []; | |
| function frame() { | |
| rafId = requestAnimationFrame(frame); | |
| if (!STATE.isReady) return; | |
| const delta = Math.min(clock.getDelta(), 0.1); | |
| if (STATE.engineMode === 'MICRO') { | |
| STATE.threads.forEach(t => { | |
| if (t.uniforms) t.uniforms.highlightIndex.value = STATE.currentSelectedIndex; | |
| }); | |
| } | |
| const isRelief = (STATE.layoutMode === 'BLANKET' || STATE.layoutMode === 'COMPARE'); | |
| const isReliefInteractive = isRelief && STATE.comparisonMode !== 'C'; | |
| if (controls.isLocked) { | |
| updateNavigation(camera, controls, delta); | |
| const crosshair = document.getElementById('hud-crosshair'); | |
| const positionChanged = lastCamPos.distanceToSquared(camera.position) > 0.0001; | |
| const rotationChanged = Math.abs(lastCamRot.x - camera.rotation.x) + | |
| Math.abs(lastCamRot.y - camera.rotation.y) + | |
| Math.abs(lastCamRot.z - camera.rotation.z) > 0.0001; | |
| let intersects = lastIntersects; | |
| if (positionChanged || rotationChanged) { | |
| raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); | |
| if (isRelief) { | |
| const reliefObjects = getActiveReliefObjects(); | |
| intersects = raycaster.intersectObjects(reliefObjects, false); | |
| } else if (STATE.engineMode === 'MACRO') { | |
| if (STATE.macroMeshes && STATE.macroMeshes.points) { | |
| intersects = raycaster.intersectObject(STATE.macroMeshes.points); | |
| } else { | |
| intersects = []; | |
| } | |
| } else { | |
| const meshes = []; | |
| STATE.threads.forEach(t => { | |
| if (t.pointsMesh && t.pointsMesh.visible) meshes.push(t.pointsMesh); | |
| }); | |
| intersects = raycaster.intersectObjects(meshes, false); | |
| } | |
| lastCamPos.copy(camera.position); | |
| lastCamRot.copy(camera.rotation); | |
| lastIntersects = intersects; | |
| } | |
| if (STATE.isTargetLocked) { | |
| if (crosshair) { | |
| crosshair.style.borderColor = 'var(--color-semantic-error)'; | |
| const dot = crosshair.querySelector('div'); | |
| if (dot) dot.style.background = 'var(--color-semantic-error)'; | |
| } | |
| if (isReliefInteractive && STATE.reliefPinned) { | |
| fillReliefHUD(STATE.reliefPinned.chunkIndex, STATE.reliefPinned.dimIndex, true, STATE.reliefPinned.source); | |
| } | |
| } else if (intersects.length > 0) { | |
| if (crosshair) { | |
| crosshair.style.borderColor = 'var(--color-semantic-warning)'; | |
| const dot = crosshair.querySelector('div'); | |
| if (dot) dot.style.background = 'var(--color-semantic-warning)'; | |
| } | |
| const hit = intersects[0]; | |
| if (isReliefInteractive) { | |
| const indices = computeReliefIndices(hit); | |
| if (indices) { | |
| STATE.reliefHover = indices; | |
| if (!STATE.reliefPinned) { | |
| STATE.currentSelectedIndex = indices.dimIndex; | |
| fillReliefHUD(indices.chunkIndex, indices.dimIndex, false); | |
| } else { | |
| fillReliefHUD(STATE.reliefPinned.chunkIndex, STATE.reliefPinned.dimIndex, true, STATE.reliefPinned.source); | |
| } | |
| } | |
| } else if (STATE.engineMode === 'MACRO') { | |
| if (hit.index !== undefined) { | |
| const firstThread = STATE.threads[0]; | |
| const len = firstThread && firstThread.embedding ? firstThread.embedding.length : 0; | |
| if (len > 0) STATE.currentSelectedIndex = hit.index % len; | |
| } | |
| } else if (hit.index !== undefined) { | |
| STATE.currentSelectedIndex = hit.index; | |
| } | |
| } else { | |
| if (crosshair) { | |
| crosshair.style.borderColor = 'var(--color-semantic-success)'; | |
| const dot = crosshair.querySelector('div'); | |
| if (dot) dot.style.background = 'var(--color-semantic-success)'; | |
| } | |
| if (isReliefInteractive && STATE.reliefPinned) { | |
| fillReliefHUD(STATE.reliefPinned.chunkIndex, STATE.reliefPinned.dimIndex, true, STATE.reliefPinned.source); | |
| } else { | |
| const hudEl = document.getElementById('relief-hud'); | |
| if (hudEl && isRelief) hudEl.style.display = 'none'; | |
| const coordsEl = document.getElementById('relief-coords'); | |
| if (coordsEl) coordsEl.style.display = 'none'; | |
| } | |
| } | |
| } else { | |
| if (isReliefInteractive && STATE.reliefPinned) { | |
| fillReliefHUD(STATE.reliefPinned.chunkIndex, STATE.reliefPinned.dimIndex, true, STATE.reliefPinned.source); | |
| const coordsEl = document.getElementById('relief-coords'); | |
| if (coordsEl) coordsEl.style.display = 'none'; | |
| } else { | |
| const hudEl = document.getElementById('relief-hud'); | |
| if (hudEl) hudEl.style.display = 'none'; | |
| const coordsEl = document.getElementById('relief-coords'); | |
| if (coordsEl) coordsEl.style.display = 'none'; | |
| } | |
| } | |
| updateHUD(camera); | |
| renderer.render(scene, camera); | |
| renderAxisGizmo(renderer, camera); | |
| } | |
| rafId = requestAnimationFrame(frame); | |
| } | |
| /** Cancels the active animation frame loop, if any. */ | |
| export function stopRenderLoop() { | |
| if (rafId !== null) { | |
| cancelAnimationFrame(rafId); | |
| rafId = null; | |
| } | |
| } | |