release: bump version to 0.32.0

This commit is contained in:
rayd1o
2026-04-22 04:41:39 +08:00
parent 437efc848c
commit 3ae4acdff8
16 changed files with 239 additions and 45 deletions

View File

@@ -566,7 +566,8 @@
.earth-settings-content {
overflow-y: auto;
padding-right: 4px;
padding-inline: 8px;
padding-right: 10px;
scrollbar-width: thin;
scrollbar-color: rgba(160, 186, 216, 0.34) transparent;
}
@@ -585,11 +586,11 @@
}
.earth-settings-section {
padding: 12px 0 20px;
padding: 6px 0 10px;
}
.earth-settings-section-title {
margin-bottom: 12px;
margin-bottom: 10px;
color: var(--hud-text-soft);
font-size: calc(0.62rem * var(--hud-scale));
letter-spacing: 0.16em;
@@ -599,7 +600,7 @@
.earth-settings-list {
display: flex;
flex-direction: column;
gap: 10px;
gap: 8px;
}
.earth-settings-item {
@@ -607,7 +608,7 @@
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 11px 14px;
padding: 10px 13px;
border-radius: 14px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent),
@@ -629,7 +630,7 @@
.earth-settings-item--stacked {
align-items: stretch;
flex-direction: column;
gap: 14px;
gap: 10px;
cursor: default;
}
@@ -644,7 +645,7 @@
.earth-settings-slider-row {
display: flex;
align-items: center;
gap: 14px;
gap: 12px;
}
.earth-settings-segmented {
@@ -783,7 +784,7 @@
.earth-settings-copy {
display: flex;
flex-direction: column;
gap: 4px;
gap: 3px;
}
.earth-settings-item-title {

View File

@@ -512,6 +512,30 @@
</label>
</div>
</section>
<section class="earth-settings-section">
<div class="earth-settings-section-title">视图</div>
<div class="earth-settings-list">
<div class="earth-settings-item earth-settings-item--stacked">
<div class="earth-settings-copy">
<span class="earth-settings-item-title">地球默认大小</span>
<span class="earth-settings-item-subtitle">用于重置视角、缩放重置和巡航视图的默认缩放比例</span>
</div>
<div class="earth-settings-slider-row">
<input
id="default-earth-size-slider"
class="earth-settings-slider"
type="range"
min="0.5"
max="5"
step="0.01"
value="1"
aria-label="调整地球默认大小"
>
<span id="default-earth-size-value" class="earth-settings-slider-value">100%</span>
</div>
</div>
</div>
</section>
<section class="earth-settings-section">
<div class="earth-settings-section-title">地形</div>
<div class="earth-settings-list">

View File

@@ -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;

View File

@@ -3,6 +3,7 @@
// Scene configuration
export const CONFIG = {
defaultCameraZ: 300,
defaultViewZoom: 1.0,
minZoom: 0.5,
maxZoom: 5.0,
earthRadius: 100,

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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";