hbauzan's picture
Release v4.17.7 — sync from GitHub main
819a47f
Raw
History Blame Contribute Delete
6.13 kB
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;
}
}