Enhance Earth interaction and bump version to 0.21.0

This commit is contained in:
linkong
2026-03-26 11:09:57 +08:00
parent a04f4f9e67
commit 7b53cf9a06
12 changed files with 211 additions and 56 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.20.0",
"version": "0.21.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.2.6",

View File

@@ -50,7 +50,7 @@
<button id="toggle-cables" class="toolbar-btn active" title="显示/隐藏线缆">🌐<span class="tooltip">隐藏线缆</span></button>
<button id="toggle-terrain" class="toolbar-btn" title="显示/隐藏地形">⛰️<span class="tooltip">显示/隐藏地形</span></button>
<button id="toggle-satellites" class="toolbar-btn" title="显示/隐藏卫星">🛰️<span class="tooltip">显示卫星</span></button>
<button id="toggle-trails" class="toolbar-btn" title="显示/隐藏轨迹"><span class="tooltip">显示/隐藏轨迹</span></button>
<button id="toggle-trails" class="toolbar-btn active" title="显示/隐藏轨迹"><span class="tooltip">隐藏轨迹</span></button>
<button id="reload-data" class="toolbar-btn" title="重新加载数据">🔃<span class="tooltip">重新加载数据</span></button>
</div>
<button id="toolbar-toggle" class="toolbar-btn" title="展开/收起工具栏"><span class="toggle-arrow"></span></button>

View File

@@ -54,7 +54,7 @@ export const CABLE_STATE = {
};
export const SATELLITE_CONFIG = {
maxCount: 10000,
maxCount: -1,
trailLength: 10,
dotSize: 4,
ringSize: 0.07,

View File

@@ -293,6 +293,12 @@ function setupTerrainControls() {
const toolbarToggle = document.getElementById("toolbar-toggle");
const toolbar = document.getElementById("control-toolbar");
if (trailsBtn) {
trailsBtn.classList.add("active");
const tooltip = trailsBtn.querySelector(".tooltip");
if (tooltip) tooltip.textContent = "隐藏轨迹";
}
bindListener(terrainBtn, "click", function () {
showTerrain = !showTerrain;
toggleTerrain(showTerrain);

View File

@@ -81,6 +81,8 @@ export let renderer;
let simplex;
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
let targetRotation = { x: 0, y: 0 };
let inertialVelocity = { x: 0, y: 0 };
let hoveredCable = null;
let hoveredSatellite = null;
let hoveredSatelliteIndex = null;
@@ -108,6 +110,10 @@ const scratchCableCenter = new THREE.Vector3();
const scratchCableDirection = new THREE.Vector3();
const cleanupFns = [];
const DRAG_ROTATION_FACTOR = 0.005;
const DRAG_SMOOTHING_FACTOR = 0.18;
const INERTIA_DAMPING = 0.92;
const INERTIA_MIN_VELOCITY = 0.00008;
function bindListener(target, eventName, handler, options) {
if (!target) return;
@@ -327,6 +333,11 @@ export function init() {
addLights();
initInfoCard();
const earthObj = createEarth(scene);
targetRotation = {
x: earthObj.rotation.x,
y: earthObj.rotation.y,
};
inertialVelocity = { x: 0, y: 0 };
createClouds(scene, earthObj);
createTerrain(scene, earthObj, simplex);
createStars(scene);
@@ -461,16 +472,17 @@ function setupEventListeners() {
const handleMouseMove = (event) => onMouseMove(event);
const handleMouseDown = (event) => onMouseDown(event);
const handleMouseUp = () => onMouseUp();
const handleMouseLeave = () => onMouseLeave();
const handleClick = (event) => onClick(event);
const handlePageHide = () => destroy();
bindListener(window, "resize", handleResize);
bindListener(window, "pagehide", handlePageHide);
bindListener(window, "beforeunload", handlePageHide);
bindListener(renderer.domElement, "mousemove", handleMouseMove);
bindListener(window, "mousemove", handleMouseMove);
bindListener(renderer.domElement, "mousedown", handleMouseDown);
bindListener(renderer.domElement, "mouseup", handleMouseUp);
bindListener(renderer.domElement, "mouseleave", handleMouseUp);
bindListener(window, "mouseup", handleMouseUp);
bindListener(renderer.domElement, "mouseleave", handleMouseLeave);
bindListener(renderer.domElement, "click", handleClick);
}
@@ -512,8 +524,13 @@ function onMouseMove(event) {
const deltaX = event.clientX - previousMousePosition.x;
const deltaY = event.clientY - previousMousePosition.y;
earth.rotation.y += deltaX * 0.005;
earth.rotation.x += deltaY * 0.005;
const rotationDeltaY = deltaX * DRAG_ROTATION_FACTOR;
const rotationDeltaX = deltaY * DRAG_ROTATION_FACTOR;
targetRotation.y += rotationDeltaY;
targetRotation.x += rotationDeltaX;
inertialVelocity.y = rotationDeltaY;
inertialVelocity.x = rotationDeltaX;
previousMousePosition = { x: event.clientX, y: event.clientY };
hideTooltip();
return;
@@ -624,10 +641,18 @@ function onMouseMove(event) {
}
function onMouseDown(event) {
const earth = getEarth();
isDragging = true;
dragStartTime = Date.now();
isLongDrag = false;
previousMousePosition = { x: event.clientX, y: event.clientY };
inertialVelocity = { x: 0, y: 0 };
if (earth) {
targetRotation = {
x: earth.rotation.x,
y: earth.rotation.y,
};
}
document.getElementById("container")?.classList.add("dragging");
hideTooltip();
}
@@ -637,6 +662,10 @@ function onMouseUp() {
document.getElementById("container")?.classList.remove("dragging");
}
function onMouseLeave() {
hideTooltip();
}
function onClick(event) {
const earth = getEarth();
if (!earth) return;
@@ -735,6 +764,36 @@ function animate() {
if (getAutoRotate() && earth) {
earth.rotation.y += CONFIG.rotationSpeed * (deltaTime / 16);
targetRotation.y = earth.rotation.y;
targetRotation.x = earth.rotation.x;
}
if (earth) {
if (isDragging) {
// Smoothly follow the drag target to match the legacy interaction feel.
earth.rotation.x +=
(targetRotation.x - earth.rotation.x) * DRAG_SMOOTHING_FACTOR;
earth.rotation.y +=
(targetRotation.y - earth.rotation.y) * DRAG_SMOOTHING_FACTOR;
} else if (
Math.abs(inertialVelocity.x) > INERTIA_MIN_VELOCITY ||
Math.abs(inertialVelocity.y) > INERTIA_MIN_VELOCITY
) {
// Continue rotating after release and gradually decay the motion.
targetRotation.x += inertialVelocity.x * (deltaTime / 16);
targetRotation.y += inertialVelocity.y * (deltaTime / 16);
earth.rotation.x +=
(targetRotation.x - earth.rotation.x) * DRAG_SMOOTHING_FACTOR;
earth.rotation.y +=
(targetRotation.y - earth.rotation.y) * DRAG_SMOOTHING_FACTOR;
inertialVelocity.x *= Math.pow(INERTIA_DAMPING, deltaTime / 16);
inertialVelocity.y *= Math.pow(INERTIA_DAMPING, deltaTime / 16);
} else {
inertialVelocity.x = 0;
inertialVelocity.y = 0;
targetRotation.x = earth.rotation.x;
targetRotation.y = earth.rotation.y;
}
}
applyCableVisualState();

View File

@@ -19,9 +19,10 @@ let earthObjRef = null;
let sceneRef = null;
let cameraRef = null;
let lockedSatelliteIndex = null;
let hoveredSatelliteIndex = null;
let positionUpdateAccumulator = 0;
let satelliteCapacity = 0;
const MAX_SATELLITES = SATELLITE_CONFIG.maxCount;
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
const DOT_TEXTURE_SIZE = 32;
const POSITION_UPDATE_INTERVAL_MS = 250;
@@ -114,17 +115,9 @@ function createRingTexture(innerRadius, outerRadius, color = "#ffffff") {
export function createSatellites(scene, earthObj) {
initSatelliteScene(scene, earthObj);
const positions = new Float32Array(MAX_SATELLITES * 3);
const colors = new Float32Array(MAX_SATELLITES * 3);
const dotTexture = createDotTexture();
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: SATELLITE_CONFIG.dotSize,
@@ -159,18 +152,7 @@ export function createSatellites(scene, earthObj) {
earthObj.add(satellitePoints);
const trailPositions = new Float32Array(MAX_SATELLITES * TRAIL_LENGTH * 3);
const trailColors = new Float32Array(MAX_SATELLITES * TRAIL_LENGTH * 3);
const trailGeometry = new THREE.BufferGeometry();
trailGeometry.setAttribute(
"position",
new THREE.BufferAttribute(trailPositions, 3),
);
trailGeometry.setAttribute(
"color",
new THREE.BufferAttribute(trailColors, 3),
);
const trailMaterial = new THREE.LineBasicMaterial({
vertexColors: true,
@@ -183,16 +165,59 @@ export function createSatellites(scene, earthObj) {
satelliteTrails.visible = false;
satelliteTrails.userData = { type: "satelliteTrails" };
earthObj.add(satelliteTrails);
ensureSatelliteCapacity(0);
satellitePositions = Array.from({ length: MAX_SATELLITES }, () => ({
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
return satellitePoints;
}
function getRequestedSatelliteLimit() {
return SATELLITE_CONFIG.maxCount < 0 ? null : SATELLITE_CONFIG.maxCount;
}
function createSatellitePositionState() {
return {
current: new THREE.Vector3(),
trail: [],
trailIndex: 0,
trailCount: 0,
}));
};
}
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
return satellitePoints;
function ensureSatelliteCapacity(count) {
if (!satellitePoints || !satelliteTrails) return;
const nextCapacity = Math.max(count, 0);
if (nextCapacity === satelliteCapacity) return;
const positions = new Float32Array(nextCapacity * 3);
const colors = new Float32Array(nextCapacity * 3);
satellitePoints.geometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3),
);
satellitePoints.geometry.setAttribute(
"color",
new THREE.BufferAttribute(colors, 3),
);
satellitePoints.geometry.setDrawRange(0, 0);
const trailPositions = new Float32Array(nextCapacity * TRAIL_LENGTH * 3);
const trailColors = new Float32Array(nextCapacity * TRAIL_LENGTH * 3);
satelliteTrails.geometry.setAttribute(
"position",
new THREE.BufferAttribute(trailPositions, 3),
);
satelliteTrails.geometry.setAttribute(
"color",
new THREE.BufferAttribute(trailColors, 3),
);
satellitePositions = Array.from(
{ length: nextCapacity },
createSatellitePositionState,
);
satelliteCapacity = nextCapacity;
}
function computeSatellitePosition(satellite, time) {
@@ -357,15 +382,20 @@ function generateFallbackPosition(satellite, index, total) {
}
export async function loadSatellites() {
const response = await fetch(
`${SATELLITE_CONFIG.apiPath}?limit=${SATELLITE_CONFIG.maxCount}`,
);
const limit = getRequestedSatelliteLimit();
const url = new URL(SATELLITE_CONFIG.apiPath, window.location.origin);
if (limit !== null) {
url.searchParams.set("limit", String(limit));
}
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`卫星接口返回 HTTP ${response.status}`);
}
const data = await response.json();
satelliteData = data.features || [];
ensureSatelliteCapacity(satelliteData.length);
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
return satelliteData.length;
}
@@ -392,7 +422,7 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
const trailPositions = satelliteTrails.geometry.attributes.position.array;
const trailColors = satelliteTrails.geometry.attributes.color.array;
const baseTime = new Date(Date.now() + elapsedMs);
const count = Math.min(satelliteData.length, MAX_SATELLITES);
const count = Math.min(satelliteData.length, satelliteCapacity);
for (let i = 0; i < count; i++) {
const satellite = satelliteData[i];
@@ -485,7 +515,7 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
}
}
for (let i = count; i < MAX_SATELLITES; i++) {
for (let i = count; i < satelliteCapacity; i++) {
positions[i * 3] = 0;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = 0;
@@ -504,6 +534,17 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
satelliteTrails.geometry.attributes.position.needsUpdate = true;
satelliteTrails.geometry.attributes.color.needsUpdate = true;
// Keep the hover ring synced with the propagated satellite position even
// when the pointer stays still and no new hover event is emitted.
if (
hoveredSatelliteIndex !== null &&
hoveredSatelliteIndex >= 0 &&
hoveredSatelliteIndex < count &&
hoveredSatelliteIndex !== lockedSatelliteIndex
) {
updateHoverRingPosition(satellitePositions[hoveredSatelliteIndex].current);
}
}
export function toggleSatellites(visible) {
@@ -563,6 +604,10 @@ export function setLockedSatelliteIndex(index) {
lockedSatelliteIndex = index;
}
export function setHoveredSatelliteIndex(index) {
hoveredSatelliteIndex = index;
}
export function isSatelliteFrontFacing(index, camera = cameraRef) {
if (!earthObjRef || !camera) return true;
if (!satellitePositions || !satellitePositions[index]) return true;
@@ -718,14 +763,17 @@ export function updateHoverRingPosition(position) {
export function setSatelliteRingState(index, state, position) {
switch (state) {
case "hover":
hoveredSatelliteIndex = index;
hideHoverRings();
showHoverRing(position, false);
break;
case "locked":
hoveredSatelliteIndex = null;
hideHoverRings();
showHoverRing(position, true);
break;
case "none":
hoveredSatelliteIndex = null;
hideHoverRings();
hideLockedRing();
break;
@@ -826,6 +874,7 @@ export function clearSatelliteData() {
satelliteData = [];
selectedSatellite = null;
lockedSatelliteIndex = null;
hoveredSatelliteIndex = null;
positionUpdateAccumulator = 0;
satellitePositions.forEach((position) => {
@@ -836,22 +885,30 @@ export function clearSatelliteData() {
});
if (satellitePoints) {
const positions = satellitePoints.geometry.attributes.position.array;
const colors = satellitePoints.geometry.attributes.color.array;
positions.fill(0);
colors.fill(0);
satellitePoints.geometry.attributes.position.needsUpdate = true;
satellitePoints.geometry.attributes.color.needsUpdate = true;
const positionAttr = satellitePoints.geometry.attributes.position;
const colorAttr = satellitePoints.geometry.attributes.color;
if (positionAttr?.array) {
positionAttr.array.fill(0);
positionAttr.needsUpdate = true;
}
if (colorAttr?.array) {
colorAttr.array.fill(0);
colorAttr.needsUpdate = true;
}
satellitePoints.geometry.setDrawRange(0, 0);
}
if (satelliteTrails) {
const trailPositions = satelliteTrails.geometry.attributes.position.array;
const trailColors = satelliteTrails.geometry.attributes.color.array;
trailPositions.fill(0);
trailColors.fill(0);
satelliteTrails.geometry.attributes.position.needsUpdate = true;
satelliteTrails.geometry.attributes.color.needsUpdate = true;
const trailPositionAttr = satelliteTrails.geometry.attributes.position;
const trailColorAttr = satelliteTrails.geometry.attributes.color;
if (trailPositionAttr?.array) {
trailPositionAttr.array.fill(0);
trailPositionAttr.needsUpdate = true;
}
if (trailColorAttr?.array) {
trailColorAttr.array.fill(0);
trailColorAttr.needsUpdate = true;
}
}
hideHoverRings();
@@ -873,6 +930,7 @@ export function resetSatelliteState() {
}
satellitePositions = [];
satelliteCapacity = 0;
showSatellites = false;
showTrails = true;
}