diff --git a/app.js b/app.js index adb18c0..2dc14a2 100644 --- a/app.js +++ b/app.js @@ -231,6 +231,9 @@ voidLight.position.set(0, -3.5, 0); glassPlatformGroup.add(voidLight); scene.add(glassPlatformGroup); +glassPlatformGroup.traverse(obj => { + if (obj.isMesh) obj.userData.zoomLabel = 'Glass Platform'; +}); // === COMMIT HEATMAP === // Canvas-texture overlay on the floor. Each agent occupies a polar sector; @@ -269,6 +272,7 @@ const heatmapMesh = new THREE.Mesh( ); heatmapMesh.rotation.x = -Math.PI / 2; heatmapMesh.position.y = 0.005; +heatmapMesh.userData.zoomLabel = 'Activity Heatmap'; scene.add(heatmapMesh); // Per-zone intensity [0..1], updated by updateHeatmap() @@ -408,6 +412,66 @@ document.addEventListener('keydown', (e) => { } }); +// === ZOOM-TO-OBJECT === +const _zoomRaycaster = new THREE.Raycaster(); +const _zoomMouse = new THREE.Vector2(); +const _zoomCamTarget = new THREE.Vector3(); +const _zoomLookTarget = new THREE.Vector3(); +let zoomT = 0; +let zoomTargetT = 0; +let zoomActive = false; + +const zoomIndicator = document.getElementById('zoom-indicator'); +const zoomLabelEl = document.getElementById('zoom-label'); + +function getZoomLabel(/** @type {THREE.Object3D} */ obj) { + let o = /** @type {THREE.Object3D|null} */ (obj); + while (o) { + if (o.userData && o.userData.zoomLabel) return o.userData.zoomLabel; + o = o.parent; + } + return 'Object'; +} + +function exitZoom() { + zoomTargetT = 0; + zoomActive = false; + if (zoomIndicator) zoomIndicator.classList.remove('visible'); +} + +renderer.domElement.addEventListener('dblclick', (/** @type {MouseEvent} */ e) => { + if (overviewMode || photoMode) return; + + _zoomMouse.x = (e.clientX / window.innerWidth) * 2 - 1; + _zoomMouse.y = -(e.clientY / window.innerHeight) * 2 + 1; + _zoomRaycaster.setFromCamera(_zoomMouse, camera); + + const hits = _zoomRaycaster.intersectObjects(scene.children, true) + .filter(h => !(h.object instanceof THREE.Points) && !(h.object instanceof THREE.Line)); + + if (!hits.length) { + exitZoom(); + return; + } + + const hit = hits[0]; + const label = getZoomLabel(hit.object); + const dir = new THREE.Vector3().subVectors(camera.position, hit.point).normalize(); + const flyDist = Math.max(1.5, Math.min(5, hit.distance * 0.45)); + _zoomCamTarget.copy(hit.point).addScaledVector(dir, flyDist); + _zoomLookTarget.copy(hit.point); + zoomT = 0; + zoomTargetT = 1; + zoomActive = true; + + if (zoomLabelEl) zoomLabelEl.textContent = label; + if (zoomIndicator) zoomIndicator.classList.add('visible'); +}); + +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') exitZoom(); +}); + // === PHOTO MODE === let photoMode = false; @@ -550,6 +614,9 @@ meterSprite.scale.set(3.2, 1.6, 1); sovereigntyGroup.add(meterSprite); scene.add(sovereigntyGroup); +sovereigntyGroup.traverse(obj => { + if (obj.isMesh || obj.isSprite) obj.userData.zoomLabel = 'Sovereignty Meter'; +}); async function loadSovereigntyStatus() { try { @@ -590,8 +657,19 @@ function animate() { // Smooth camera transition for overview mode const targetT = overviewMode ? 1 : 0; overviewT += (targetT - overviewT) * 0.04; - camera.position.lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT); - camera.lookAt(0, 0, 0); + const _basePos = new THREE.Vector3().lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT); + + // Zoom-to-object interpolation + if (!photoMode) { + zoomT += (zoomTargetT - zoomT) * 0.07; + } + if (zoomT > 0.001 && !photoMode && !overviewMode) { + camera.position.lerpVectors(_basePos, _zoomCamTarget, zoomT); + camera.lookAt(new THREE.Vector3(0, 0, 0).lerp(_zoomLookTarget, zoomT)); + } else { + camera.position.copy(_basePos); + camera.lookAt(0, 0, 0); + } // Slow auto-rotation — suppressed during overview and photo mode const rotationScale = photoMode ? 0 : (1 - overviewT); @@ -931,6 +1009,7 @@ async function initCommitBanners() { startDelay: i * 2.5, lifetime: 12 + i * 1.5, spawnTime: /** @type {number|null} */ (null), + zoomLabel: `Commit: ${commit.hash}`, }; scene.add(sprite); commitBanners.push(sprite); @@ -1076,6 +1155,7 @@ function rebuildAgentPanels(statusData) { baseY: BOARD_Y, floatPhase: (i / n) * Math.PI * 2, floatSpeed: 0.18 + i * 0.04, + zoomLabel: `Agent: ${agent.name}`, }; agentBoardGroup.add(sprite); agentPanelSprites.push(sprite); diff --git a/index.html b/index.html index 69d6b65..3864372 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,11 @@