release: bump version to 0.46.2

This commit is contained in:
linkong
2026-04-30 14:30:12 +08:00
parent 7418ce2fc1
commit 9f737fdb89
24 changed files with 596 additions and 104 deletions

View File

@@ -28,7 +28,7 @@ const _lpCameraToPoint = new THREE.Vector3();
const LANDING_POINT_SPRITE_HEIGHT = 3;
const LANDING_POINT_SPRITE_ASPECT = 1;
const LANDING_POINT_SIZE_REFERENCE_FOV = 75;
const LANDING_POINT_SIZE_SCALE_MIN = 0.36;
const LANDING_POINT_SIZE_SCALE_MIN = 0.16;
const LANDING_POINT_SIZE_SCALE_MAX = 3;
const LANDING_POINT_ATLAS_CELL_SIZE = 128;
let landingPointTexture = null;

View File

@@ -232,7 +232,7 @@ export const VESSEL_CONFIG = {
max: 2.4,
},
track: {
altitudeOffset: 0.22,
altitudeOffset: 0.2,
color: 0x7dd3fc,
opacity: 0.82,
},

View File

@@ -19,6 +19,7 @@ import {
import {
toggleTerrain,
setDayNightEnabled,
toggleClouds,
toggleGridLines,
getShowGridLines,
} from "./earth.js";
@@ -53,7 +54,7 @@ import {
} from "./satellites.js";
import { getShowCables } from "./cables.js";
import { toggleBGP, getShowBGP, getBGPCount } from "./bgp.js";
import { getShowCountryBoundaries } from "./country-boundaries.js";
import { getShowCountryBoundaries, toggleCountryBoundaries } from "./country-boundaries.js";
import {
toggleComputeCenters,
getShowComputeCenters,
@@ -136,6 +137,7 @@ let focusViewAnimationToken = 0;
let earthSettingsDefaults = null;
let lastZoomStatusUpdateTime = 0;
let earthSettingsState = null;
let deferredLayerVisibilitySettings = null;
let layerRegistry = new Map();
let layerPanelInitialized = false;
let layoutMode = "desktop";
@@ -269,7 +271,7 @@ function closeTransientMobileOverlays({ except = null } = {}) {
setMobileDrawerOpen("layer-toggles", false);
}
if (except !== "media" && isTVPanelVisible()) {
if (except !== "media" && except !== "search" && isTVPanelVisible()) {
setTVPanelVisible(false);
}
}
@@ -670,6 +672,14 @@ function shouldIncludeLayerInStartupLoad(definition) {
return false;
}
const persistedVisible = getPersistedLayerVisibilityOverride(definition.id);
if (typeof persistedVisible === "boolean") {
if (definition.startupMode === "preload" && definition.startupAlwaysLoad) {
return true;
}
return persistedVisible;
}
if (definition.startupMode === "preload") {
return true;
}
@@ -677,6 +687,14 @@ function shouldIncludeLayerInStartupLoad(definition) {
return Boolean(definition?.getVisible?.());
}
function getPersistedLayerVisibilityOverride(layerId) {
if (!layerId) return null;
const layerVisibility =
deferredLayerVisibilitySettings || earthSettingsState?.shared?.layerVisibility;
const persistedVisible = layerVisibility?.[layerId];
return typeof persistedVisible === "boolean" ? persistedVisible : null;
}
function clampEarthZoomLevel(nextZoom) {
const parsedZoom = Number.parseFloat(nextZoom);
if (!Number.isFinite(parsedZoom)) {
@@ -1127,7 +1145,7 @@ function setDefaultEarthZoom(nextZoom, { persist = true, applyToCurrentView = tr
return defaultEarthZoom;
}
async function applyEarthSettings(settings) {
async function applyEarthSettings(settings, { applyLayers = true } = {}) {
if (!settings) return;
earthSettingsState = cloneEarthSettings(settings);
@@ -1161,12 +1179,31 @@ async function applyEarthSettings(settings) {
applyToCurrentView: true,
});
if (!applyLayers) {
const layerVisibility = { ...(settings.shared.layerVisibility || {}) };
applyImmediateLayerVisibilityHints(layerVisibility);
deferredLayerVisibilitySettings = layerVisibility;
return;
}
deferredLayerVisibilitySettings = null;
await applyLayerVisibilitySettings(settings.shared.layerVisibility, {
persist: false,
silent: true,
});
}
export async function applyDeferredLayerVisibilitySettings(options = {}) {
const layerVisibility = deferredLayerVisibilitySettings;
deferredLayerVisibilitySettings = null;
if (!layerVisibility) return;
await applyLayerVisibilitySettings(layerVisibility, {
persist: false,
silent: true,
...options,
});
}
function resetEarthSettings() {
const defaults = cloneEarthSettings(captureEarthSettingsDefaults());
earthSettingsState = cloneEarthSettings(defaults);
@@ -1307,8 +1344,15 @@ async function setCountryBoundariesLayerEnabled(button, enabled, { persist = tru
}
}
function setHighResTextureLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
setHighResTextureEnabled(enabled, { suppressStatus: silent });
async function setHighResTextureLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
if (enabled) {
setLayerButtonState(button, {
active: false,
loading: true,
tooltip: "高清材质加载中...",
});
}
await setHighResTextureEnabled(enabled, { suppressStatus: silent });
setLayerButtonState(button, {
active: enabled,
loading: false,
@@ -1319,8 +1363,15 @@ function setHighResTextureLayerEnabled(button, enabled, { persist = true, silent
return enabled;
}
function setAtmosphereCloudsLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
setAtmosphereCloudsEnabled(enabled, { suppressStatus: silent });
async function setAtmosphereCloudsLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
if (enabled) {
setLayerButtonState(button, {
active: false,
loading: true,
tooltip: "大气云图加载中...",
});
}
await setAtmosphereCloudsEnabled(enabled, { suppressStatus: silent });
setLayerButtonState(button, {
active: enabled,
loading: false,
@@ -1449,6 +1500,44 @@ async function applyLayerVisibilitySettings(layerVisibility = {}, options = {})
}
}
function applyImmediateLayerVisibilityHints(layerVisibility = {}) {
if (typeof layerVisibility.gridLines === "boolean") {
setGridLinesLayerEnabled(getLayerButton("gridLines"), layerVisibility.gridLines, {
persist: false,
silent: true,
});
}
if (typeof layerVisibility.countryBoundaries === "boolean") {
toggleCountryBoundaries(layerVisibility.countryBoundaries, {
showLandFill: true,
});
setLayerButtonState(getLayerButton("countryBoundaries"), {
active: layerVisibility.countryBoundaries,
loading: false,
tooltip: layerVisibility.countryBoundaries ? "隐藏国界线" : "显示国界线",
});
}
if (layerVisibility.earthHighResTexture === false) {
void setHighResTextureEnabled(false, { suppressStatus: true });
setLayerButtonState(getLayerButton("earthHighResTexture"), {
active: false,
loading: false,
tooltip: "显示高清材质",
});
}
if (typeof layerVisibility.atmosphereClouds === "boolean") {
toggleClouds(layerVisibility.atmosphereClouds);
setLayerButtonState(getLayerButton("atmosphereClouds"), {
active: layerVisibility.atmosphereClouds,
loading: false,
tooltip: layerVisibility.atmosphereClouds ? "隐藏大气云图" : "显示大气云图",
});
}
}
function getBuiltinLayerDefinitions() {
return [
{
@@ -1472,13 +1561,14 @@ function getBuiltinLayerDefinitions() {
id: "countryBoundaries",
buttonId: "toggle-country-boundaries",
icon: "public",
label: "国界",
label: "国界线",
meta: "Country Borders",
keywords: "国界 国家 borders countries boundary",
defaultActive: true,
displayOrder: 90,
startupPriority: 20,
startupMode: "preload",
startupAlwaysLoad: true,
startupLabel: "海陆基座",
startupMessage: "正在加载海陆基座...",
getVisible: () => getShowCountryBoundaries(),
@@ -2268,7 +2358,7 @@ function setupSettingsControls() {
});
captureEarthSettingsDefaults();
settingsApplyPromise = applyEarthSettings(loadEarthSettings());
settingsApplyPromise = applyEarthSettings(loadEarthSettings(), { applyLayers: false });
syncAllHudPanelToggles();
syncRotationModeButtons();
syncCruiseModuleControls();

View File

@@ -25,8 +25,11 @@ let _earthTextureOverlayMaterial = null;
let _earthShaders = [];
let _dayNightEnabled = true;
let _loadedTexture = null;
let _textureLoadPromise = null;
let _textureVisible = true;
let _earthRimGlow = null;
let _cloudTexture = null;
let _cloudTextureLoadPromise = null;
const _earthSunDirection = new THREE.Vector3(
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.x,
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.y,
@@ -301,28 +304,16 @@ export function createClouds(scene, earthObj) {
clouds = new THREE.Mesh(geometry, material);
clouds.name = "earth-atmosphere-clouds";
clouds.visible = showClouds;
clouds.visible = false;
earthObj.add(clouds);
textureLoader.load(
CLOUD_LAYER_CONFIG.textureUrl,
function(texture) {
material.map = texture;
material.needsUpdate = true;
},
undefined,
function(err) {
console.log('云层纹理加载失败');
}
);
return clouds;
}
export function toggleClouds(visible) {
showClouds = Boolean(visible);
if (clouds) {
clouds.visible = showClouds;
clouds.visible = showClouds && Boolean(clouds.material?.map);
}
}
@@ -330,6 +321,39 @@ export function getShowClouds() {
return showClouds;
}
export function loadCloudTexture() {
if (_cloudTexture) return Promise.resolve(_cloudTexture);
if (_cloudTextureLoadPromise) return _cloudTextureLoadPromise;
_cloudTextureLoadPromise = new Promise((resolve, reject) => {
if (!clouds?.material) {
resolve(null);
return;
}
textureLoader.load(
CLOUD_LAYER_CONFIG.textureUrl,
(texture) => {
_cloudTexture = texture;
clouds.material.map = texture;
clouds.material.needsUpdate = true;
clouds.visible = showClouds;
resolve(texture);
},
undefined,
(error) => {
console.warn("云层纹理加载失败");
reject(error);
},
);
});
_cloudTextureLoadPromise.finally(() => {
_cloudTextureLoadPromise = null;
});
return _cloudTextureLoadPromise;
}
export function createTerrain(earthObj) {
const geometry = new THREE.SphereGeometry(
CONFIG.earthRadius + TERRAIN_CONFIG.baseRadiusOffset,
@@ -478,6 +502,7 @@ export function getClouds() {
export function clearEarthTexture() {
_loadedTexture = null;
_textureLoadPromise = null;
if (_earthTextureOverlayMaterial) {
_earthTextureOverlayMaterial.map = null;
_earthTextureOverlayMaterial.needsUpdate = true;
@@ -522,7 +547,10 @@ export function setDayNightEnabled(enabled) {
}
export function loadEarthTexture() {
return new Promise((resolve) => {
if (_loadedTexture) return Promise.resolve(_loadedTexture);
if (_textureLoadPromise) return _textureLoadPromise;
_textureLoadPromise = new Promise((resolve) => {
if (!_earthTextureOverlayMaterial) { resolve(); return; }
const urls = EARTH_MATERIAL_CONFIG.textureUrls;
@@ -549,7 +577,7 @@ export function loadEarthTexture() {
if (_earthRimGlow) {
_earthRimGlow.visible = !_textureVisible;
}
resolve();
resolve(texture);
},
null,
() => tryLoad(index + 1),
@@ -557,6 +585,11 @@ export function loadEarthTexture() {
};
tryLoad(0);
});
_textureLoadPromise.finally(() => {
_textureLoadPromise = null;
});
return _textureLoadPromise;
}
export function setEarthTextureVisible(visible) {

View File

@@ -23,6 +23,10 @@ import {
loadVessels,
toggleVessels,
} from "./vessels.js";
import {
loadCloudTexture,
loadEarthTexture,
} from "./earth.js";
import {
getCountryBoundaryLegendItems,
loadCountryBoundaries,
@@ -83,6 +87,8 @@ export function registerLayerStartupTask(id, taskFactory) {
function registerBuiltinLayerStartupTasks() {
startupTaskRegistry.clear();
registerCountryBoundaryStartupTask();
registerEarthTextureStartupTask();
registerCloudStartupTask();
registerCableStartupTask();
registerComputeCenterStartupTask();
registerVesselStartupTask();
@@ -210,6 +216,42 @@ function registerBGPStartupTask() {
});
}
function registerEarthTextureStartupTask() {
registerLayerStartupTask("earthHighResTexture", (context) => async (layer) => {
if (!context.isEarthTextureVisible()) return;
context.setLoadingMessage(
resolveStartupMessage(layer, "load", "正在加载地球纹理..."),
);
await context.yieldFrame(12);
try {
await loadEarthTexture();
} catch (error) {
console.warn("地球纹理加载失败:", error);
}
if (context.isCancelled()) return;
await context.yieldFrame(16);
});
}
function registerCloudStartupTask() {
registerLayerStartupTask("atmosphereClouds", (context) => async (layer) => {
if (!context.isCloudsEnabled()) return;
context.setLoadingMessage(
resolveStartupMessage(layer, "load", "正在加载大气云图..."),
);
await context.yieldFrame(12);
try {
await loadCloudTexture();
} catch (error) {
context.reportError(layer?.startupLabel || layer?.label || "大气云图", error);
}
if (context.isCancelled()) return;
await context.yieldFrame(16);
});
}
function registerCountryBoundaryStartupTask() {
registerLayerStartupTask("countryBoundaries", (context) => async (layer) => {
context.setLoadingMessage(

View File

@@ -37,6 +37,7 @@ import {
createGridLines,
getEarth,
getEarthSurfacePickTarget,
loadCloudTexture,
loadEarthTexture,
clearEarthTexture,
setEarthSunDirection,
@@ -203,6 +204,7 @@ import {
setDayNightEnabledExternal,
setTerrainLayerInteractable,
setDayNightInteractable,
applyDeferredLayerVisibilitySettings,
} from "./controls.js";
import {
createLayerStartupTaskMap,
@@ -1470,23 +1472,7 @@ function formatBGPStatusFromSummary(summary) {
return "当前无活跃事件";
}
function applyEarthStatsSummary(summary) {
if (!summary) return;
updateEarthStats({
cableCount: `${summary.cableCount}`,
landingPointCount: `${summary.landingPointCount}`,
satelliteCount: `${summary.satelliteCount}`,
computeCenterCount: `${summary.computeCenterCount}`,
vesselCount: `${summary.vesselCount}`,
bgpAnomalyCount: `${summary.bgpEventCount}`,
bgpCollectorCount: `${summary.bgpCollectorCount}`,
bgpStatusSummary: formatBGPStatusFromSummary(summary),
terrainOn: getShowTerrain(),
textureQuality: "8K 卫星图",
});
}
async function loadEarthStatsSummary() {
async function loadEarthStatsSummary({ shouldApply = () => true } = {}) {
try {
const response = await fetch(PATHS.earthSummaryApi);
if (!response.ok) {
@@ -1505,7 +1491,9 @@ async function loadEarthStatsSummary() {
bgpAnomalyCount: toCount(stats.bgp_anomaly_count),
bgpCollectorCount: toCount(stats.bgp_collector_count),
};
applyEarthStatsSummary(earthStatsSummary);
if (shouldApply()) {
updateStatsSummary();
}
} catch (error) {
console.warn("全球态势聚合统计加载失败:", error);
}
@@ -2594,24 +2582,14 @@ async function loadData() {
await yieldFrame(18);
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
setLoadingMessage("正在读取全球态势统计...");
await loadEarthStatsSummary();
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
await yieldFrame(12);
loadEarthStatsSummary({
shouldApply: () => loadToken === currentLoadToken && !destroyed,
}).catch((error) => {
console.warn("后台刷新全球态势统计失败:", error);
});
const errors = [];
// Step 1 — Earth texture
setLoadingMessage("正在加载地球纹理...");
await yieldFrame(12);
try {
await loadEarthTexture();
} catch (err) {
// texture failure is non-fatal
}
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
await yieldFrame(16);
const startupLoaders = createLayerStartupTaskMap({
scene,
earth,
@@ -2630,6 +2608,7 @@ async function loadData() {
getShowCountryBoundaries,
getShowBGP,
isEarthTextureVisible: () => getEarthTextureVisible(),
isCloudsEnabled: () => getShowClouds(),
getInitialSatelliteLoadLimit,
shouldHydrateFullSatelliteSet,
scheduleSatellitePositionWarmup,
@@ -2704,6 +2683,10 @@ async function loadData() {
hideError();
queueStatusMessage("数据已加载", "success");
}
applyDeferredLayerVisibilitySettings().catch((error) => {
console.warn("恢复 Earth 图层可见性失败:", error);
});
}
const POSITION_UPDATE_FORCE_DELTA = 250;
@@ -2827,8 +2810,17 @@ export async function setCountryBoundariesEnabled(
let _dayNightBeforeTextureOff = null;
let _terrainBeforeTextureOff = null;
export function setHighResTextureEnabled(enabled, { suppressStatus = false } = {}) {
export async function setHighResTextureEnabled(enabled, { suppressStatus = false } = {}) {
setEarthTextureVisible(enabled);
if (enabled) {
try {
await loadEarthTexture();
setEarthTextureVisible(true);
} catch (error) {
console.warn("高清材质加载失败:", error);
setEarthTextureVisible(false);
}
}
setSurfaceTintEnabled(!enabled);
setLandFillEnabled(true);
setLandFillSuppressed(false);
@@ -2863,8 +2855,17 @@ export function getHighResTextureEnabled() {
return getEarthTextureVisible();
}
export function setAtmosphereCloudsEnabled(enabled, { suppressStatus = false } = {}) {
export async function setAtmosphereCloudsEnabled(enabled, { suppressStatus = false } = {}) {
toggleClouds(enabled);
if (enabled) {
try {
await loadCloudTexture();
toggleClouds(true);
} catch (error) {
console.warn("大气云图加载失败:", error);
toggleClouds(false);
}
}
if (!suppressStatus) {
showStatusMessage(enabled ? "大气云图已显示" : "大气云图已隐藏", "info");
}

View File

@@ -2139,7 +2139,7 @@ function showIridiumReservedStyle(position) {
lockedIridiumFootprintMesh = createIridiumFootprintAdapter({
earthObj: earthObjRef,
earthRadiusWorld: CONFIG.earthRadius,
renderOrder: 0,
renderOrder: GROUND_FOOTPRINT_RENDER_ORDER,
});
updateIridiumReservedStyle(position);
}

View File

@@ -11,6 +11,7 @@ const VESSEL_RENDER_ORDER = 4.4;
const VESSEL_POINT_SIZE = 34;
const VESSEL_ATLAS_CELL_SIZE = 128;
const VESSEL_COURSE_BINS = 32;
const VESSEL_TRACK_ENDPOINT_EPSILON = 0.001;
function normalizeVesselType(value, code) {
const type = String(value || "").trim().toLowerCase();
@@ -72,6 +73,26 @@ function getCourseBin(marker) {
return Math.round((normalized / 360) * VESSEL_COURSE_BINS) % VESSEL_COURSE_BINS;
}
function buildTrackPoint(lon, lat) {
const latitude = Number(lat);
const longitude = Number(lon);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
return latLonToVector3(
latitude,
longitude,
CONFIG.earthRadius + VESSEL_CONFIG.track.altitudeOffset,
);
}
function appendCurrentMarkerTrackPoint(points, marker) {
if (!(marker?.position instanceof THREE.Vector3)) return;
const markerPosition = marker.position.clone();
const lastPoint = points[points.length - 1];
if (!lastPoint || lastPoint.distanceToSquared(markerPosition) > VESSEL_TRACK_ENDPOINT_EPSILON) {
points.push(markerPosition);
}
}
const vesselIconLayer = createInteractableLayer({
id: "vessels",
objectType: "vessel",
@@ -207,14 +228,15 @@ export async function showVesselTrack(marker, earth) {
if (coordinates.length < 2) return null;
const points = coordinates
.map(([lon, lat]) =>
latLonToVector3(
Number(lat),
Number(lon),
CONFIG.earthRadius + VESSEL_CONFIG.track.altitudeOffset,
),
)
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z));
.map(([lon, lat]) => buildTrackPoint(lon, lat))
.filter(
(point) =>
point &&
Number.isFinite(point.x) &&
Number.isFinite(point.y) &&
Number.isFinite(point.z),
);
appendCurrentMarkerTrackPoint(points, marker);
if (points.length < 2) return null;
const geometry = new THREE.BufferGeometry().setFromPoints(points);