From 0ecc1bc53780b7c59c8d438a06c8a915d9bc18ba Mon Sep 17 00:00:00 2001 From: rayd1o Date: Thu, 19 Mar 2026 16:46:40 +0800 Subject: [PATCH] feat(earth): cable state management, hover/lock visual separation, fix isSameCable undefined bug --- frontend/public/earth/js/cables.js | 24 +++- frontend/public/earth/js/constants.js | 6 + frontend/public/earth/js/main.js | 160 ++++++++++++++------------ 3 files changed, 113 insertions(+), 77 deletions(-) diff --git a/frontend/public/earth/js/cables.js b/frontend/public/earth/js/cables.js index f6450d2a..30c550d6 100644 --- a/frontend/public/earth/js/cables.js +++ b/frontend/public/earth/js/cables.js @@ -2,7 +2,7 @@ import * as THREE from 'three'; -import { CONFIG, CABLE_COLORS, PATHS } from './constants.js'; +import { CONFIG, CABLE_COLORS, PATHS, CABLE_STATE } from './constants.js'; import { latLonToVector3 } from './utils.js'; import { updateEarthStats, showStatusMessage } from './ui.js'; import { showInfoCard } from './info-card.js'; @@ -340,3 +340,25 @@ export function getCablesById(cableId) { export function getLandingPoints() { return landingPoints; } + +const cableStates = new Map(); + +export function getCableState(cableId) { + return cableStates.get(cableId) || CABLE_STATE.NORMAL; +} + +export function setCableState(cableId, state) { + cableStates.set(cableId, state); +} + +export function clearAllCableStates() { + cableStates.clear(); +} + +export function getCableStateInfo() { + const states = {}; + cableStates.forEach((state, cableId) => { + states[cableId] = state; + }); + return states; +} diff --git a/frontend/public/earth/js/constants.js b/frontend/public/earth/js/constants.js index a1c68a0a..b7e6f272 100644 --- a/frontend/public/earth/js/constants.js +++ b/frontend/public/earth/js/constants.js @@ -47,6 +47,12 @@ export const CABLE_CONFIG = { pulseCoefficient: 0.4 }; +export const CABLE_STATE = { + NORMAL: 'normal', + HOVERED: 'hovered', + LOCKED: 'locked' +}; + export const GRID_CONFIG = { latitudeStep: 10, longitudeStep: 30, diff --git a/frontend/public/earth/js/main.js b/frontend/public/earth/js/main.js index 843702ec..0dbd5ebe 100644 --- a/frontend/public/earth/js/main.js +++ b/frontend/public/earth/js/main.js @@ -1,7 +1,7 @@ import * as THREE from 'three'; import { createNoise3D } from 'simplex-noise'; -import { CONFIG, CABLE_CONFIG } from './constants.js'; +import { CONFIG, CABLE_CONFIG, CABLE_STATE } from './constants.js'; import { latLonToVector3, vector3ToLatLon, screenToEarthCoords } from './utils.js'; import { showStatusMessage, @@ -13,7 +13,7 @@ import { hideTooltip } from './ui.js'; import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js'; -import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById, lockedCable as cableLocked } from './cables.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 { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js'; import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js'; @@ -33,6 +33,7 @@ let isLongDrag = false; function clearLockedObject() { hoveredCable = null; + clearAllCableStates(); lockedObject = null; lockedObjectType = null; lockedSatellite = null; @@ -40,36 +41,73 @@ function clearLockedObject() { } function isSameCable(cable1, cable2) { - return cable1 && cable2 && cable1.userData.cableId === cable2.userData.cableId; + if (!cable1 || !cable2) return false; + const id1 = cable1.userData?.cableId; + const id2 = cable2.userData?.cableId; + if (id1 === undefined || id2 === undefined) return false; + return id1 === id2; +} + +function showCableInfo(cable) { + showInfoCard('cable', { + name: cable.userData.name, + owner: cable.userData.owner, + status: cable.userData.status, + length: cable.userData.length, + coords: cable.userData.coords, + rfs: cable.userData.rfs + }); +} + +function showSatelliteInfo(props) { + const meanMotion = props?.mean_motion || 0; + const period = meanMotion > 0 ? (1440 / meanMotion).toFixed(1) : '-'; + const ecc = props?.eccentricity || 0; + const perigee = (6371 * (1 - ecc)).toFixed(0); + const apogee = (6371 * (1 + ecc)).toFixed(0); + + showInfoCard('satellite', { + name: props?.name || '-', + norad_id: props?.norad_cat_id, + inclination: props?.inclination ? props.inclination.toFixed(2) : '-', + period: period, + perigee: perigee, + apogee: apogee + }); } function applyCableVisualState() { const allCables = getCableLines(); const pulse = (Math.sin(Date.now() * CABLE_CONFIG.pulseSpeed) + 1) * 0.5; - const isLocked = lockedObjectType === 'cable' && lockedObject; allCables.forEach(c => { - const cableIsLocked = isSameCable(c, lockedObject); - const cableIsHovered = isSameCable(c, hoveredCable); + const cableId = c.userData.cableId; + const state = getCableState(cableId); - if (cableIsLocked) { - c.material.opacity = CABLE_CONFIG.lockedOpacityMin + pulse * CABLE_CONFIG.pulseCoefficient; - c.material.color.setRGB(1, 1, 1); - } else if (cableIsHovered) { - c.material.opacity = 1; - c.material.color.setRGB(1, 1, 1); - } else if (isLocked) { - c.material.opacity = CABLE_CONFIG.otherOpacity; - const origColor = c.userData.originalColor; - const brightness = CABLE_CONFIG.otherBrightness; - c.material.color.setRGB( - ((origColor >> 16) & 255) / 255 * brightness, - ((origColor >> 8) & 255) / 255 * brightness, - (origColor & 255) / 255 * brightness - ); - } else { - c.material.opacity = 1; - c.material.color.setHex(c.userData.originalColor); + switch (state) { + case CABLE_STATE.LOCKED: + c.material.opacity = CABLE_CONFIG.lockedOpacityMin + pulse * CABLE_CONFIG.pulseCoefficient; + c.material.color.setRGB(1, 1, 1); + break; + case CABLE_STATE.HOVERED: + c.material.opacity = 1; + c.material.color.setRGB(1, 1, 1); + break; + case CABLE_STATE.NORMAL: + default: + if (lockedObjectType === 'cable' && lockedObject) { + c.material.opacity = CABLE_CONFIG.otherOpacity; + const origColor = c.userData.originalColor; + const brightness = CABLE_CONFIG.otherBrightness; + c.material.color.setRGB( + ((origColor >> 16) & 255) / 255 * brightness, + ((origColor >> 8) & 255) / 255 * brightness, + (origColor & 255) / 255 * brightness + ); + } else { + c.material.opacity = 1; + c.material.color.setHex(c.userData.originalColor); + } } }); } @@ -231,10 +269,6 @@ function onMouseMove(event, camera) { const frontCables = getFrontFacingCables(allCableLines, camera); const intersects = raycaster.intersectObjects(frontCables); - if (hoveredCable && (lockedObjectType !== 'cable' || !isSameCable(hoveredCable, lockedObject))) { - hoveredCable = null; - } - const hasHoveredCable = intersects.length > 0; let hoveredSat = null; if (getShowSatellites()) { @@ -249,57 +283,35 @@ function onMouseMove(event, camera) { } const hasHoveredSatellite = hoveredSat && hoveredSat.properties; + if (hoveredCable) { + if (!hasHoveredCable || !isSameCable(intersects[0]?.object, hoveredCable)) { + if (!isSameCable(hoveredCable, lockedObject)) { + setCableState(hoveredCable.userData.cableId, CABLE_STATE.NORMAL); + } + hoveredCable = null; + } + } + if (hasHoveredCable) { const cable = intersects[0].object; - hoveredCable = cable; + if (!isSameCable(cable, lockedObject)) { + hoveredCable = cable; + setCableState(cable.userData.cableId, CABLE_STATE.HOVERED); + } else { + hoveredCable = cable; + } - showInfoCard('cable', { - name: cable.userData.name, - owner: cable.userData.owner, - status: cable.userData.status, - length: cable.userData.length, - coords: cable.userData.coords, - rfs: cable.userData.rfs - }); + showCableInfo(cable); setInfoCardNoBorder(true); hideTooltip(); } else if (hasHoveredSatellite) { hoveredSatellite = hoveredSat; - const props = hoveredSat.properties; - - const meanMotion = props.mean_motion || 0; - const period = meanMotion > 0 ? (1440 / meanMotion).toFixed(1) : '-'; - const ecc = props.eccentricity || 0; - const perigee = (6371 * (1 - ecc)).toFixed(0); - const apogee = (6371 * (1 + ecc)).toFixed(0); - - showInfoCard('satellite', { - name: props.name, - norad_id: props.norad_cat_id, - inclination: props.inclination ? props.inclination.toFixed(2) : '-', - period: period, - perigee: perigee, - apogee: apogee - }); + showSatelliteInfo(hoveredSat.properties); setInfoCardNoBorder(true); - } else if (lockedObjectType === 'cable') { - handleCableClick(lockedObject); - } else if (lockedObjectType === 'satellite') { - const props = lockedSatellite.properties; - const meanMotion = props?.mean_motion || 0; - const period = meanMotion > 0 ? (1440 / meanMotion).toFixed(1) : '-'; - const ecc = props?.eccentricity || 0; - const perigee = (6371 * (1 - ecc)).toFixed(0); - const apogee = (6371 * (1 + ecc)).toFixed(0); - - showInfoCard('satellite', { - name: props?.name || '-', - norad_id: props?.norad_cat_id, - inclination: props?.inclination ? props.inclination.toFixed(2) : '-', - period: period, - perigee: perigee, - apogee: apogee - }); + } else if (lockedObjectType === 'cable' && lockedObject) { + showCableInfo(lockedObject); + } else if (lockedObjectType === 'satellite' && lockedSatellite) { + showSatelliteInfo(lockedSatellite.properties); } else { hideInfoCard(); } @@ -368,12 +380,8 @@ function onClick(event, camera, renderer) { const clickedCable = intersects[0].object; const cableId = clickedCable.userData.cableId; - const sameCables = getCablesById(cableId); - sameCables.forEach(c => { - c.material.color.setHex(0xffffff); - c.material.opacity = 1; - }); + setCableState(cableId, CABLE_STATE.LOCKED); lockedObject = clickedCable; lockedObjectType = 'cable';