地形
diff --git a/frontend/public/earth/js/bgp-cruise-adapter.js b/frontend/public/earth/js/bgp-cruise-adapter.js
index 679b3b48..a0de8c70 100644
--- a/frontend/public/earth/js/bgp-cruise-adapter.js
+++ b/frontend/public/earth/js/bgp-cruise-adapter.js
@@ -169,13 +169,11 @@ export function createBGPCruiseAdapter({
cardPlacement = getCardScreenCoords(marker);
setMarkerLocked(marker);
showMarkerOverlay(marker);
- applySatelliteHighlights(marker);
await focusView({
lat: marker.userData?.latitude ?? 0,
lon: marker.userData?.longitude ?? 0,
rotLon: (marker.userData?.longitude ?? 0) - 270,
- zoom: 1.0,
duration: interrupt
? Math.round(CRUISE_CONFIG.focusDurationMs * 0.78)
: CRUISE_CONFIG.focusDurationMs,
@@ -203,6 +201,8 @@ export function createBGPCruiseAdapter({
return false;
}
+ applySatelliteHighlights(marker);
+
const connectorDelayCompleted = await context.wait(CRUISE_CONNECTOR_DRAW_MS);
if (!connectorDelayCompleted || !context.isCurrent()) {
cardPlacement = null;
diff --git a/frontend/public/earth/js/constants.js b/frontend/public/earth/js/constants.js
index 8cdac78f..f764be17 100644
--- a/frontend/public/earth/js/constants.js
+++ b/frontend/public/earth/js/constants.js
@@ -3,6 +3,7 @@
// Scene configuration
export const CONFIG = {
defaultCameraZ: 300,
+ defaultViewZoom: 1.0,
minZoom: 0.5,
maxZoom: 5.0,
earthRadius: 100,
diff --git a/frontend/public/earth/js/controls.js b/frontend/public/earth/js/controls.js
index 167b2ec7..b6d41e89 100644
--- a/frontend/public/earth/js/controls.js
+++ b/frontend/public/earth/js/controls.js
@@ -44,6 +44,8 @@ export let showTerrain = false;
export let layoutExpanded = false;
export let rotationMode = ROTATION_MODE.ROTATE;
let dayNightEnabled = true;
+let defaultEarthZoom = CONFIG.defaultViewZoom;
+let activeCamera = null;
let earthObj = null;
let listeners = [];
@@ -73,6 +75,7 @@ const SETTINGS_SHEET_MIN_SCALE = 0.06;
const SETTINGS_SHEET_MAX_SCALE_X = 0.22;
const SETTINGS_SHEET_MAX_SCALE_Y = 0.18;
const EARTH_SETTINGS_STORAGE_KEY = "planet.earth.settings.v1";
+const DEFAULT_EARTH_ZOOM_STEP = 0.01;
let settingsModalTimer = null;
let settingsSheetAnimation = null;
let terrainToggleToken = 0;
@@ -120,6 +123,26 @@ function shouldIncludeLayerInStartupLoad(definition) {
return Boolean(definition?.getVisible?.());
}
+function clampEarthZoomLevel(nextZoom) {
+ const parsedZoom = Number.parseFloat(nextZoom);
+ if (!Number.isFinite(parsedZoom)) {
+ return CONFIG.defaultViewZoom;
+ }
+ return Math.min(CONFIG.maxZoom, Math.max(CONFIG.minZoom, parsedZoom));
+}
+
+function formatZoomPercent(zoom) {
+ return `${Math.round(zoom * 100)}%`;
+}
+
+function getZoomResetTooltipText(zoom) {
+ return `重置缩放到${formatZoomPercent(zoom)}`;
+}
+
+function getZoomResetStatusMessage(zoom) {
+ return `缩放已重置到${formatZoomPercent(zoom)}`;
+}
+
function canUseLocalStorage() {
try {
return typeof window !== "undefined" && !!window.localStorage;
@@ -145,6 +168,7 @@ function getCurrentSettingsSnapshot() {
),
terrainOpacity: getTerrainOpacity(),
dayNightEnabled,
+ defaultEarthZoom,
};
}
@@ -160,6 +184,7 @@ function cloneEarthSettings(settings) {
rotationMode: settings.rotationMode,
terrainOpacity: settings.terrainOpacity,
dayNightEnabled: settings.dayNightEnabled,
+ defaultEarthZoom: settings.defaultEarthZoom,
panelVisibility: { ...(settings.panelVisibility || {}) },
layerVisibility: { ...(settings.layerVisibility || {}) },
};
@@ -197,6 +222,9 @@ function normalizeEarthSettings(rawSettings, defaults) {
const nextDayNightEnabled = typeof rawSettings?.dayNightEnabled === "boolean"
? rawSettings.dayNightEnabled
: defaults.dayNightEnabled;
+ const nextDefaultEarthZoom = clampEarthZoomLevel(
+ rawSettings?.defaultEarthZoom ?? defaults.defaultEarthZoom,
+ );
return {
rotationMode: nextRotationMode,
@@ -206,6 +234,7 @@ function normalizeEarthSettings(rawSettings, defaults) {
? nextTerrainOpacity
: defaults.terrainOpacity,
dayNightEnabled: nextDayNightEnabled,
+ defaultEarthZoom: nextDefaultEarthZoom,
};
}
@@ -251,6 +280,46 @@ function persistEarthSettings() {
}
}
+function syncDefaultEarthZoomUi(nextZoom) {
+ const slider = document.getElementById("default-earth-size-slider");
+ const value = document.getElementById("default-earth-size-value");
+ const zoomValue = document.getElementById("zoom-value");
+ const tooltipText = getZoomResetTooltipText(nextZoom);
+
+ if (slider instanceof HTMLInputElement) {
+ slider.min = CONFIG.minZoom.toString();
+ slider.max = CONFIG.maxZoom.toString();
+ slider.step = DEFAULT_EARTH_ZOOM_STEP.toString();
+ slider.value = nextZoom.toFixed(2);
+ }
+ if (value) {
+ value.textContent = formatZoomPercent(nextZoom);
+ }
+ if (zoomValue instanceof HTMLElement) {
+ zoomValue.title = tooltipText;
+ const tooltip = zoomValue.querySelector(".tooltip");
+ if (tooltip) {
+ tooltip.textContent = tooltipText;
+ }
+ }
+}
+
+function setDefaultEarthZoom(nextZoom, { persist = true, applyToCurrentView = true } = {}) {
+ defaultEarthZoom = clampEarthZoomLevel(nextZoom);
+ syncDefaultEarthZoomUi(defaultEarthZoom);
+
+ if (applyToCurrentView && activeCamera) {
+ zoomLevel = defaultEarthZoom;
+ applyZoom(activeCamera);
+ }
+
+ if (persist) {
+ persistEarthSettings();
+ }
+
+ return defaultEarthZoom;
+}
+
async function applyEarthSettings(settings) {
if (!settings) return;
@@ -277,6 +346,11 @@ async function applyEarthSettings(settings) {
applyDayNightEnabled(settings.dayNightEnabled, { persist: false });
}
+ setDefaultEarthZoom(settings.defaultEarthZoom, {
+ persist: false,
+ applyToCurrentView: true,
+ });
+
await applyLayerVisibilitySettings(settings.layerVisibility, {
persist: false,
silent: true,
@@ -660,7 +734,7 @@ export function applyImmediateView(targetEarthObj, camera, options = {}) {
const {
lat = EARTH_CONFIG.chinaLat,
rotLon = EARTH_CONFIG.chinaRotLon,
- zoom = 1.0,
+ zoom = getDefaultEarthZoomLevel(),
} = options;
const nextRotation = getViewRotation(lat, rotLon);
@@ -956,6 +1030,7 @@ function setupSettingsControls() {
const terrainOpacitySlider = document.getElementById("terrain-opacity-slider");
const terrainOpacityValue = document.getElementById("terrain-opacity-value");
+ const defaultEarthSizeSlider = document.getElementById("default-earth-size-slider");
const rotationModeButtons = document.querySelectorAll("[data-rotation-mode]");
const syncTerrainOpacityUi = (nextOpacity) => {
const safeOpacity = Math.round(nextOpacity * 100);
@@ -968,6 +1043,7 @@ function setupSettingsControls() {
};
syncTerrainOpacityUi(getTerrainOpacity());
+ syncDefaultEarthZoomUi(defaultEarthZoom);
if (terrainOpacitySlider instanceof HTMLInputElement) {
bindListener(terrainOpacitySlider, "input", (event) => {
@@ -982,6 +1058,18 @@ function setupSettingsControls() {
});
}
+ if (defaultEarthSizeSlider instanceof HTMLInputElement) {
+ bindListener(defaultEarthSizeSlider, "input", (event) => {
+ const target = event.currentTarget;
+ if (!(target instanceof HTMLInputElement)) return;
+ const nextZoom = Number.parseFloat(target.value);
+ setDefaultEarthZoom(
+ Number.isFinite(nextZoom) ? nextZoom : defaultEarthZoom,
+ { persist: true, applyToCurrentView: true },
+ );
+ });
+ }
+
rotationModeButtons.forEach((button) => {
bindListener(button, "click", (event) => {
const target = event.currentTarget;
@@ -1303,6 +1391,7 @@ function resetCleanup() {
export function setupControls(camera, renderer, scene, earth) {
resetCleanup();
+ activeCamera = camera;
earthObj = earth;
setupZoomControls(camera);
setupWheelZoom(camera, renderer);
@@ -1407,7 +1496,7 @@ function setupZoomControls(camera) {
bindListener(zoomValue, "click", () => {
const startZoomVal = zoomLevel;
- const targetZoom = 1.0;
+ const targetZoom = getDefaultEarthZoomLevel();
const startDistance = CONFIG.defaultCameraZ / startZoomVal;
const targetDistance = CONFIG.defaultCameraZ / targetZoom;
@@ -1424,8 +1513,8 @@ function setupZoomControls(camera) {
updateZoomDisplay(zoomLevel, distance.toFixed(0));
},
() => {
- zoomLevel = 1.0;
- showStatusMessage("缩放已重置到100%", "info");
+ zoomLevel = targetZoom;
+ showStatusMessage(getZoomResetStatusMessage(targetZoom), "info");
},
);
});
@@ -1482,6 +1571,7 @@ function animateValue(start, end, duration, onUpdate, onComplete) {
export function resetView(camera) {
if (!earthObj) return;
+ const defaultZoom = getDefaultEarthZoomLevel();
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
@@ -1490,7 +1580,7 @@ export function resetView(camera) {
lat: pos.coords.latitude,
lon: pos.coords.longitude,
rotLon: pos.coords.longitude - 270,
- zoom: 1.0,
+ zoom: defaultZoom,
duration: 800,
suppressStatus: false,
}),
@@ -1499,7 +1589,7 @@ export function resetView(camera) {
lat: EARTH_CONFIG.chinaLat,
lon: EARTH_CONFIG.chinaLon,
rotLon: EARTH_CONFIG.chinaRotLon,
- zoom: 1.0,
+ zoom: defaultZoom,
duration: 800,
suppressStatus: false,
}),
@@ -1510,7 +1600,7 @@ export function resetView(camera) {
lat: EARTH_CONFIG.chinaLat,
lon: EARTH_CONFIG.chinaLon,
rotLon: EARTH_CONFIG.chinaRotLon,
- zoom: 1.0,
+ zoom: defaultZoom,
duration: 800,
suppressStatus: false,
});
@@ -1877,6 +1967,8 @@ function setupToolbarHubCluster() {
}
let collapseTimer = null;
+ let expandedToolbarBounds = null;
+ let refreshBoundsFrameId = 0;
const layoutToolbarOrbs = () => {
const orbs = Array.from(cluster.querySelectorAll(".earth-toolbar-orb"));
@@ -1942,11 +2034,24 @@ function setupToolbarHubCluster() {
orb.style.setProperty("--orb-x", `${x.toFixed(2)}px`);
orb.style.setProperty("--orb-y", `${y.toFixed(2)}px`);
});
+
+ if (cluster.classList.contains("is-expanded")) {
+ scheduleExpandedToolbarBoundsRefresh();
+ }
};
const setExpanded = (expanded) => {
cluster.classList.toggle("is-expanded", expanded);
cluster.classList.toggle("is-collapsed", !expanded);
+ if (!expanded) {
+ expandedToolbarBounds = null;
+ if (refreshBoundsFrameId) {
+ cancelAnimationFrame(refreshBoundsFrameId);
+ refreshBoundsFrameId = 0;
+ }
+ return;
+ }
+ scheduleExpandedToolbarBoundsRefresh();
};
const scheduleCollapse = () => {
@@ -1966,6 +2071,10 @@ function setupToolbarHubCluster() {
cleanupFns.push(() => {
if (collapseTimer) clearTimeout(collapseTimer);
+ if (refreshBoundsFrameId) {
+ cancelAnimationFrame(refreshBoundsFrameId);
+ refreshBoundsFrameId = 0;
+ }
});
// Start collapsed — hub acts as the hover target to reveal the arc
@@ -2034,9 +2143,23 @@ function setupToolbarHubCluster() {
};
};
+ const scheduleExpandedToolbarBoundsRefresh = () => {
+ if (refreshBoundsFrameId) return;
+ refreshBoundsFrameId = window.requestAnimationFrame(() => {
+ expandedToolbarBounds = collectExpandedToolbarBounds();
+ refreshBoundsFrameId = 0;
+ });
+ };
+
bindListener(document, "mousemove", (event) => {
if (!cluster.classList.contains("is-expanded")) return;
- const activeBounds = collectExpandedToolbarBounds();
+ if (
+ event.target instanceof Element &&
+ event.target.closest("#toolbar-cluster, .earth-stack-toolbar")
+ ) {
+ scheduleExpandedToolbarBoundsRefresh();
+ }
+ const activeBounds = expandedToolbarBounds;
if (!activeBounds) {
scheduleCollapse();
return;
@@ -2052,10 +2175,23 @@ function setupToolbarHubCluster() {
scheduleCollapse();
}
});
+
+ bindListener(cluster, "mouseenter", () => {
+ if (cluster.classList.contains("is-expanded")) {
+ scheduleExpandedToolbarBoundsRefresh();
+ }
+ });
+
+ bindListener(cluster, "focusin", () => {
+ if (cluster.classList.contains("is-expanded")) {
+ scheduleExpandedToolbarBoundsRefresh();
+ }
+ });
}
export function teardownControls() {
resetCleanup();
+ activeCamera = null;
}
export function getAutoRotate() {
@@ -2143,7 +2279,7 @@ export function focusEarthView(camera, options = {}) {
lat = EARTH_CONFIG.chinaLat,
lon = EARTH_CONFIG.chinaLon,
rotLon = lon - 270,
- zoom = 1.0,
+ zoom = getDefaultEarthZoomLevel(),
duration = 800,
suppressStatus = true,
} = options;
@@ -2181,6 +2317,10 @@ export function getZoomLevel() {
return zoomLevel;
}
+export function getDefaultEarthZoomLevel() {
+ return defaultEarthZoom;
+}
+
export function getShowTerrain() {
return showTerrain;
}
diff --git a/frontend/public/earth/js/satellites.js b/frontend/public/earth/js/satellites.js
index de1397fd..7109d715 100644
--- a/frontend/public/earth/js/satellites.js
+++ b/frontend/public/earth/js/satellites.js
@@ -31,6 +31,10 @@ let satelliteSatrecCache = new Map();
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
const DOT_TEXTURE_SIZE = 32;
const POSITION_UPDATE_INTERVAL_MS = 250;
+const DIMMED_SATELLITE_BRIGHTNESS = 0.42;
+const DIMMED_SATELLITE_TRAIL_BRIGHTNESS = 0.24;
+const DIMMED_SATELLITE_POINT_OPACITY = 0.62;
+const DIMMED_SATELLITE_BACKDROP_OPACITY = 0.1;
const scratchWorldSatellitePosition = new THREE.Vector3();
const scratchToCamera = new THREE.Vector3();
@@ -746,16 +750,16 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
const rule = getSatelliteLegendRule(props);
const { r, g, b } = getSatelliteRuleColor(rule);
- if (highlightedSatelliteIndices !== null && !highlightedSatelliteIndices.has(i)) {
- const lum = r * 0.299 + g * 0.587 + b * 0.114;
- colors[i * 3] = lum * 0.75 + r * 0.25;
- colors[i * 3 + 1] = lum * 0.75 + g * 0.25;
- colors[i * 3 + 2] = lum * 0.75 + b * 0.25;
- } else {
- colors[i * 3] = r;
- colors[i * 3 + 1] = g;
- colors[i * 3 + 2] = b;
- }
+ const isNonFocusDimmed =
+ highlightedSatelliteIndices !== null && !highlightedSatelliteIndices.has(i);
+ const pointBrightness = isNonFocusDimmed ? DIMMED_SATELLITE_BRIGHTNESS : 1;
+ const trailBrightness = isNonFocusDimmed
+ ? DIMMED_SATELLITE_TRAIL_BRIGHTNESS
+ : 1;
+
+ colors[i * 3] = r * pointBrightness;
+ colors[i * 3 + 1] = g * pointBrightness;
+ colors[i * 3 + 2] = b * pointBrightness;
const satPosition = satellitePositions[i];
for (let j = 0; j < TRAIL_LENGTH; j++) {
@@ -771,9 +775,9 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
trailPositions[trailIdx + 1] = trailPoint.y;
trailPositions[trailIdx + 2] = trailPoint.z;
const alpha = (j + 1) / satPosition.trailCount;
- trailColors[trailIdx] = r * alpha;
- trailColors[trailIdx + 1] = g * alpha;
- trailColors[trailIdx + 2] = b * alpha;
+ trailColors[trailIdx] = r * alpha * trailBrightness;
+ trailColors[trailIdx + 1] = g * alpha * trailBrightness;
+ trailColors[trailIdx + 2] = b * alpha * trailBrightness;
continue;
}
}
@@ -1097,10 +1101,10 @@ export function setSatelliteRingState(index, state, position) {
function applyDimMaterialState(isDimmed) {
if (satellitePoints) {
- satellitePoints.material.opacity = isDimmed ? 0.32 : 0.9;
+ satellitePoints.material.opacity = isDimmed ? DIMMED_SATELLITE_POINT_OPACITY : 0.9;
}
if (satelliteBackdropPoints) {
- satelliteBackdropPoints.material.opacity = isDimmed ? 0.12 : 0.42;
+ satelliteBackdropPoints.material.opacity = isDimmed ? DIMMED_SATELLITE_BACKDROP_OPACITY : 0.42;
}
}
diff --git a/frontend/public/earth/js/ui.js b/frontend/public/earth/js/ui.js
index dcf303a9..d0d628bb 100644
--- a/frontend/public/earth/js/ui.js
+++ b/frontend/public/earth/js/ui.js
@@ -155,7 +155,15 @@ export function updateZoomDisplay(zoomLevel, distance) {
const slider = getElement("zoom-slider");
const cameraDistanceEl = getElement("camera-distance");
- if (zoomValueEl) zoomValueEl.textContent = percent + "%";
+ if (zoomValueEl) {
+ const tooltip = zoomValueEl.querySelector(".tooltip");
+ const label = `${percent}%`;
+ if (zoomValueEl.firstChild?.nodeType === Node.TEXT_NODE) {
+ zoomValueEl.firstChild.nodeValue = label;
+ } else {
+ zoomValueEl.insertBefore(document.createTextNode(label), tooltip || null);
+ }
+ }
if (zoomLevelEl) zoomLevelEl.textContent = "缩放: " + percent + "%";
if (slider) slider.value = zoomLevel;
if (cameraDistanceEl) cameraDistanceEl.textContent = distance + " km";
diff --git a/frontend/src/components/Scrollbar/Scrollbar.tsx b/frontend/src/components/Scrollbar/Scrollbar.tsx
index 81ea9fd3..5fd54519 100644
--- a/frontend/src/components/Scrollbar/Scrollbar.tsx
+++ b/frontend/src/components/Scrollbar/Scrollbar.tsx
@@ -132,13 +132,10 @@ function Scrollbar({
resizeObserver.observe(viewport)
resizeObserver.observe(trackX)
resizeObserver.observe(trackY)
- Array.from(viewport.children).forEach((child) => resizeObserver.observe(child))
mutationObserver.observe(viewport, {
childList: true,
subtree: true,
- attributes: true,
- characterData: true,
})
viewport.addEventListener('scroll', scheduleUpdate, { passive: true })
diff --git a/frontend/src/components/Scrollbar/ScrollbarOverlay.tsx b/frontend/src/components/Scrollbar/ScrollbarOverlay.tsx
index a7b0f23e..f883c699 100644
--- a/frontend/src/components/Scrollbar/ScrollbarOverlay.tsx
+++ b/frontend/src/components/Scrollbar/ScrollbarOverlay.tsx
@@ -156,7 +156,6 @@ function ScrollbarOverlay({
scheduleUpdate()
})
resizeObserver.observe(target)
- Array.from(target.children).forEach((child) => resizeObserver?.observe(child))
scheduleUpdate()
}
@@ -169,7 +168,6 @@ function ScrollbarOverlay({
mutationObserver.observe(container, {
childList: true,
subtree: true,
- attributes: true,
})
window.addEventListener('resize', scheduleUpdate)
diff --git a/pyproject.toml b/pyproject.toml
index b27d25b9..9f3e3157 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "planet"
-version = "0.31.3"
+version = "0.32.0"
description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14"
dependencies = [
diff --git a/uv.lock b/uv.lock
index f0e2f2b7..232bdb18 100644
--- a/uv.lock
+++ b/uv.lock
@@ -475,7 +475,7 @@ wheels = [
[[package]]
name = "planet"
-version = "0.31.3"
+version = "0.32.0"
source = { virtual = "." }
dependencies = [
{ name = "aiofiles" },