From bb6b18fe3ba13a3beee4d2883b6a9b8bfd5a28b6 Mon Sep 17 00:00:00 2001 From: rayd1o Date: Thu, 19 Mar 2026 17:41:53 +0800 Subject: [PATCH] feat(earth): satellite dot rendering with hover/lock rings, dim cables when satellite locked - Change satellite points from squares to circular dots - Add hover ring (white) and lock ring (yellow) for satellites - Fix satellite hover/lock ring state management - Dim all cables when satellite is locked - Increase MAX_SATELLITES to 2000 - Fix satIntersects scoping bug --- frontend/public/earth/js/constants.js | 2 +- frontend/public/earth/js/main.js | 51 ++++++++- frontend/public/earth/js/satellites.js | 147 ++++++++++++++++++++++++- 3 files changed, 191 insertions(+), 9 deletions(-) diff --git a/frontend/public/earth/js/constants.js b/frontend/public/earth/js/constants.js index b7e6f272..d72ff709 100644 --- a/frontend/public/earth/js/constants.js +++ b/frontend/public/earth/js/constants.js @@ -6,7 +6,7 @@ export const CONFIG = { minZoom: 0.5, maxZoom: 5.0, earthRadius: 100, - rotationSpeed: 0.002, + rotationSpeed: 0.0005, }; // Earth coordinate constants diff --git a/frontend/public/earth/js/main.js b/frontend/public/earth/js/main.js index 0dbd5ebe..0d9e2de3 100644 --- a/frontend/public/earth/js/main.js +++ b/frontend/public/earth/js/main.js @@ -14,7 +14,7 @@ import { } from './ui.js'; import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js'; import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById, lockedCable as cableLocked, getCableState, setCableState, clearAllCableStates } from './cables.js'; -import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, selectSatellite, getSatelliteData, getSatellitePoints } from './satellites.js'; +import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, selectSatellite, getSatelliteData, getSatellitePoints, setSatelliteRingState, updateLockedRingPosition, updateHoverRingPosition, getSatellitePositions } from './satellites.js'; import { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js'; import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js'; @@ -24,8 +24,10 @@ let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; let hoveredCable = null; let hoveredSatellite = null; +let hoveredSatelliteIndex = null; let cableLockedData = null; let lockedSatellite = null; +let lockedSatelliteIndex = null; let lockedObject = null; let lockedObjectType = null; let dragStartTime = 0; @@ -33,10 +35,14 @@ let isLongDrag = false; function clearLockedObject() { hoveredCable = null; + hoveredSatellite = null; + hoveredSatelliteIndex = null; clearAllCableStates(); + setSatelliteRingState(null, 'none', null); lockedObject = null; lockedObjectType = null; lockedSatellite = null; + lockedSatelliteIndex = null; cableLockedData = null; } @@ -95,7 +101,7 @@ function applyCableVisualState() { break; case CABLE_STATE.NORMAL: default: - if (lockedObjectType === 'cable' && lockedObject) { + if ((lockedObjectType === 'cable' && lockedObject) || (lockedObjectType === 'satellite' && lockedSatellite)) { c.material.opacity = CABLE_CONFIG.otherOpacity; const origColor = c.userData.originalColor; const brightness = CABLE_CONFIG.otherBrightness; @@ -271,13 +277,14 @@ function onMouseMove(event, camera) { const hasHoveredCable = intersects.length > 0; let hoveredSat = null; + let hoveredSatIndexFromIntersect = null; if (getShowSatellites()) { const satPoints = getSatellitePoints(); if (satPoints) { const satIntersects = raycaster.intersectObject(satPoints); if (satIntersects.length > 0) { - const index = satIntersects[0].index; - hoveredSat = selectSatellite(index); + hoveredSatIndexFromIntersect = satIntersects[0].index; + hoveredSat = selectSatellite(hoveredSatIndexFromIntersect); } } } @@ -292,6 +299,13 @@ function onMouseMove(event, camera) { } } + if (hoveredSatelliteIndex !== null && hoveredSatelliteIndex !== hoveredSatIndexFromIntersect) { + if (hoveredSatelliteIndex !== lockedSatelliteIndex) { + setSatelliteRingState(hoveredSatelliteIndex, 'none', null); + } + hoveredSatelliteIndex = null; + } + if (hasHoveredCable) { const cable = intersects[0].object; if (!isSameCable(cable, lockedObject)) { @@ -306,11 +320,24 @@ function onMouseMove(event, camera) { hideTooltip(); } else if (hasHoveredSatellite) { hoveredSatellite = hoveredSat; + hoveredSatelliteIndex = hoveredSatIndexFromIntersect; + if (hoveredSatelliteIndex !== lockedSatelliteIndex) { + const satPositions = getSatellitePositions(); + if (satPositions && satPositions[hoveredSatelliteIndex]) { + setSatelliteRingState(hoveredSatelliteIndex, 'hover', satPositions[hoveredSatelliteIndex].current); + } + } showSatelliteInfo(hoveredSat.properties); setInfoCardNoBorder(true); } else if (lockedObjectType === 'cable' && lockedObject) { showCableInfo(lockedObject); } else if (lockedObjectType === 'satellite' && lockedSatellite) { + if (lockedSatelliteIndex !== null && lockedSatelliteIndex !== undefined) { + const satPositions = getSatellitePositions(); + if (satPositions && satPositions[lockedSatelliteIndex]) { + setSatelliteRingState(lockedSatelliteIndex, 'locked', satPositions[lockedSatelliteIndex].current); + } + } showSatelliteInfo(lockedSatellite.properties); } else { hideInfoCard(); @@ -399,8 +426,14 @@ function onClick(event, camera, renderer) { lockedObject = sat; lockedObjectType = 'satellite'; lockedSatellite = sat; + lockedSatelliteIndex = index; setAutoRotate(false); + const satPositions = getSatellitePositions(); + if (satPositions && satPositions[index]) { + setSatelliteRingState(index, 'locked', satPositions[index].current); + } + const props = sat.properties; const meanMotion = props.mean_motion || 0; @@ -444,6 +477,16 @@ function animate() { updateSatellitePositions(16); + const satPositions = getSatellitePositions(); + + if (lockedObjectType === 'satellite' && lockedSatelliteIndex !== null) { + if (satPositions && satPositions[lockedSatelliteIndex]) { + updateLockedRingPosition(satPositions[lockedSatelliteIndex].current); + } + } else if (hoveredSatelliteIndex !== null && satPositions && satPositions[hoveredSatelliteIndex]) { + updateHoverRingPosition(satPositions[hoveredSatelliteIndex].current); + } + renderer.render(scene, camera); } diff --git a/frontend/public/earth/js/satellites.js b/frontend/public/earth/js/satellites.js index 5156f6a3..0ca67533 100644 --- a/frontend/public/earth/js/satellites.js +++ b/frontend/public/earth/js/satellites.js @@ -12,25 +12,76 @@ let showTrails = true; let animationTime = 0; let selectedSatellite = null; let satellitePositions = []; +let hoverRingSprite = null; +let lockedRingSprite = null; const SATELLITE_API = '/api/v1/visualization/geo/satellites?limit=2000'; -const MAX_SATELLITES = 500; +const MAX_SATELLITES = 2000; const TRAIL_LENGTH = 30; +const DOT_TEXTURE_SIZE = 32; + +function createCircularDotTexture() { + const canvas = document.createElement('canvas'); + canvas.width = DOT_TEXTURE_SIZE; + canvas.height = DOT_TEXTURE_SIZE; + const ctx = canvas.getContext('2d'); + const center = DOT_TEXTURE_SIZE / 2; + const radius = center - 2; + + const gradient = ctx.createRadialGradient(center, center, 0, center, center, radius); + gradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); + gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.8)'); + gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); + + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(center, center, radius, 0, Math.PI * 2); + ctx.fill(); + + const texture = new THREE.CanvasTexture(canvas); + texture.needsUpdate = true; + return texture; +} + +function createRingTexture(innerRadius, outerRadius, color = '#ffffff') { + const size = DOT_TEXTURE_SIZE * 2; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + const center = size / 2; + + ctx.strokeStyle = color; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(center, center, (innerRadius + outerRadius) / 2, 0, Math.PI * 2); + ctx.stroke(); + + const texture = new THREE.CanvasTexture(canvas); + texture.needsUpdate = true; + return texture; +} export function createSatellites(scene, earthObj) { + initSatelliteScene(scene, earthObj); + const positions = new Float32Array(MAX_SATELLITES * 3); const colors = new Float32Array(MAX_SATELLITES * 3); + const dotTexture = createCircularDotTexture(); + const pointsGeometry = new THREE.BufferGeometry(); pointsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); pointsGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); const pointsMaterial = new THREE.PointsMaterial({ - size: 3, + size: 1.5, + map: dotTexture, vertexColors: true, transparent: true, opacity: 0.9, - sizeAttenuation: true + sizeAttenuation: true, + alphaTest: 0.1 }); satellitePoints = new THREE.Points(pointsGeometry, pointsMaterial); @@ -176,7 +227,7 @@ export function updateSatellitePositions(deltaTime = 0) { const trailColors = satelliteTrails.geometry.attributes.color.array; const baseTime = new Date(); - const count = Math.min(satelliteData.length, 500); + const count = Math.min(satelliteData.length, MAX_SATELLITES); for (let i = 0; i < count; i++) { const satellite = satelliteData[i]; @@ -315,3 +366,91 @@ export function selectSatellite(index) { export function getSatellitePoints() { return satellitePoints; } + +export function getSatellitePositions() { + return satellitePositions; +} + +let earthObjRef = null; +let sceneRef = null; + +export function showHoverRing(position, isLocked = false) { + if (!sceneRef || !earthObjRef) return; + + const ringTexture = createRingTexture(8, 12, isLocked ? '#ffcc00' : '#ffffff'); + const spriteMaterial = new THREE.SpriteMaterial({ + map: ringTexture, + transparent: true, + opacity: 0.8, + depthTest: false + }); + + const sprite = new THREE.Sprite(spriteMaterial); + sprite.position.copy(position); + sprite.scale.set(3, 3, 1); + + earthObjRef.add(sprite); + + if (isLocked) { + if (lockedRingSprite) { + earthObjRef.remove(lockedRingSprite); + } + lockedRingSprite = sprite; + } else { + if (hoverRingSprite) { + earthObjRef.remove(hoverRingSprite); + } + hoverRingSprite = sprite; + } + + return sprite; +} + +export function hideHoverRings() { + if (!earthObjRef) return; + + if (hoverRingSprite) { + earthObjRef.remove(hoverRingSprite); + hoverRingSprite = null; + } +} + +export function hideLockedRing() { + if (!earthObjRef || !lockedRingSprite) return; + earthObjRef.remove(lockedRingSprite); + lockedRingSprite = null; +} + +export function updateLockedRingPosition(position) { + if (lockedRingSprite && position) { + lockedRingSprite.position.copy(position); + } +} + +export function updateHoverRingPosition(position) { + if (hoverRingSprite && position) { + hoverRingSprite.position.copy(position); + } +} + +export function setSatelliteRingState(index, state, position) { + switch (state) { + case 'hover': + hideHoverRings(); + showHoverRing(position, false); + break; + case 'locked': + hideHoverRings(); + showHoverRing(position, true); + break; + case 'none': + hideHoverRings(); + hideLockedRing(); + break; + } +} + +export function initSatelliteScene(scene, earth) { + sceneRef = scene; + earthObjRef = earth; +}