From d4f57943cedbfb3cca7a29a87b9db9bea9ff68fc Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 00:37:33 -0400 Subject: [PATCH] feat: add right-click object inspection for 3D objects Right-clicking any inspectable 3D mesh or sprite now shows a holographic tooltip with the object's name and description. Tagged objects: glass platform, platform rim, sovereignty meter, sovereignty score sprite, agent panels, and commit banners. The contextmenu handler uses THREE.Raycaster against all scene children and surfaces the first hit with userData.inspectName. Tooltip dismisses on click or Escape. Fixes #141 --- app.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 5 +++++ style.css | 32 ++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/app.js b/app.js index 3f9b2cd..8e5b37d 100644 --- a/app.js +++ b/app.js @@ -163,12 +163,16 @@ const platformFrameMat = new THREE.MeshStandardMaterial({ const platformRimGeo = new THREE.RingGeometry(4.7, 5.3, 64); const platformRim = new THREE.Mesh(platformRimGeo, platformFrameMat); platformRim.rotation.x = -Math.PI / 2; +platformRim.userData.inspectName = 'Glass Platform'; +platformRim.userData.inspectDesc = 'Central floating platform — the sovereign foundation of the Nexus.'; glassPlatformGroup.add(platformRim); // Raised border torus for visible 3-D thickness const borderTorusGeo = new THREE.TorusGeometry(5.0, 0.1, 6, 64); const borderTorus = new THREE.Mesh(borderTorusGeo, platformFrameMat); borderTorus.rotation.x = Math.PI / 2; +borderTorus.userData.inspectName = 'Platform Rim'; +borderTorus.userData.inspectDesc = 'Raised metallic torus encircling the Nexus platform.'; glassPlatformGroup.add(borderTorus); // Glass tile material — highly transmissive to reveal the void below @@ -370,6 +374,8 @@ const scoreArcMat = new THREE.MeshBasicMaterial({ }); const scoreArcMesh = new THREE.Mesh(buildScoreArcGeo(sovereigntyScore), scoreArcMat); scoreArcMesh.rotation.z = Math.PI / 2; // arc starts at 12 o'clock +scoreArcMesh.userData.inspectName = 'Sovereignty Meter'; +scoreArcMesh.userData.inspectDesc = 'Holographic arc gauge tracking Timmy\'s sovereignty score across all systems.'; sovereigntyGroup.add(scoreArcMesh); // Glow light at gauge center @@ -403,6 +409,8 @@ const meterSpriteMat = new THREE.SpriteMaterial({ }); const meterSprite = new THREE.Sprite(meterSpriteMat); meterSprite.scale.set(3.2, 1.6, 1); +meterSprite.userData.inspectName = 'Sovereignty Score'; +meterSprite.userData.inspectDesc = 'Live sovereignty score display. Reads from sovereignty-status.json.'; sovereigntyGroup.add(meterSprite); scene.add(sovereigntyGroup); @@ -749,6 +757,8 @@ async function initCommitBanners() { startDelay: i * 2.5, lifetime: 12 + i * 1.5, spawnTime: /** @type {number|null} */ (null), + inspectName: `Commit ${commit.hash}`, + inspectDesc: commit.message, }; scene.add(sprite); commitBanners.push(sprite); @@ -894,6 +904,8 @@ function rebuildAgentPanels(statusData) { baseY: BOARD_Y, floatPhase: (i / n) * Math.PI * 2, floatSpeed: 0.18 + i * 0.04, + inspectName: `Agent: ${agent.name.toUpperCase()}`, + inspectDesc: `Status: ${agent.status} · PRs today: ${agent.prs_today}${agent.issue ? '\n' + agent.issue : ''}`, }; agentBoardGroup.add(sprite); agentPanelSprites.push(sprite); @@ -922,3 +934,49 @@ async function refreshAgentBoard() { // Initial render, then poll every 30 s refreshAgentBoard(); setInterval(refreshAgentBoard, 30000); + +// === OBJECT INSPECTION (right-click) === +const inspectRaycaster = new THREE.Raycaster(); +const inspectTooltip = document.getElementById('inspect-tooltip'); +const inspectNameEl = document.getElementById('inspect-name'); +const inspectDescEl = document.getElementById('inspect-desc'); + +/** + * Shows the inspection tooltip near the cursor. + * @param {number} x - Client X position + * @param {number} y - Client Y position + * @param {string} name - Object name + * @param {string} desc - Object description + */ +function showInspectTooltip(x, y, name, desc) { + inspectNameEl.textContent = name; + inspectDescEl.textContent = desc; + // Offset from cursor; keep within viewport + const tx = Math.min(x + 14, window.innerWidth - 300); + const ty = Math.min(y + 14, window.innerHeight - 80); + inspectTooltip.style.left = tx + 'px'; + inspectTooltip.style.top = ty + 'px'; + inspectTooltip.classList.add('visible'); +} + +renderer.domElement.addEventListener('contextmenu', (/** @type {MouseEvent} */ e) => { + e.preventDefault(); + const mouse = new THREE.Vector2( + (e.clientX / window.innerWidth) * 2 - 1, + -(e.clientY / window.innerHeight) * 2 + 1 + ); + inspectRaycaster.setFromCamera(mouse, camera); + const hits = inspectRaycaster.intersectObjects(scene.children, true); + const hit = hits.find(h => h.object.userData.inspectName); + if (hit) { + const ud = hit.object.userData; + showInspectTooltip(e.clientX, e.clientY, ud.inspectName, ud.inspectDesc || ''); + } else { + inspectTooltip.classList.remove('visible'); + } +}); + +document.addEventListener('click', () => inspectTooltip.classList.remove('visible')); +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') inspectTooltip.classList.remove('visible'); +}); diff --git a/index.html b/index.html index e5f82b4..9e11ad2 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,11 @@
⚡ SOVEREIGNTY ⚡
+
+
+
+
+
diff --git a/style.css b/style.css index 8ccbc2d..8b742ec 100644 --- a/style.css +++ b/style.css @@ -184,6 +184,38 @@ body.photo-mode #overview-indicator { 100% { opacity: 0; transform: translate(-50%, -50%) scale(1); } } +/* === OBJECT INSPECTION TOOLTIP === */ +#inspect-tooltip { + display: none; + position: fixed; + z-index: 50; + background: rgba(0, 8, 24, 0.92); + border: 1px solid var(--color-primary); + padding: 8px 12px; + font-family: var(--font-body); + pointer-events: none; + max-width: 280px; +} + +#inspect-tooltip.visible { + display: block; +} + +#inspect-name { + font-size: 11px; + color: var(--color-primary); + letter-spacing: 0.15em; + text-transform: uppercase; + margin-bottom: 4px; +} + +#inspect-desc { + font-size: 11px; + color: var(--color-text); + line-height: 1.4; + opacity: 0.85; +} + /* === CRT / CYBERPUNK OVERLAY === */ .crt-overlay { position: fixed; -- 2.43.0