release: bump version to 0.31.3
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import * as THREE from "three";
|
||||
import * as Astronomy from "astronomy-engine";
|
||||
|
||||
import { CELESTIAL_CONFIG, EARTH_CONFIG } from "./constants.js";
|
||||
import {
|
||||
CELESTIAL_CONFIG,
|
||||
EARTH_CONFIG,
|
||||
SCENE_LIGHT_CONFIG,
|
||||
} from "./constants.js";
|
||||
import { latLonToVector3 } from "./utils.js";
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
@@ -21,7 +25,11 @@ let moonDirection = defaultMoonDirection.clone();
|
||||
let lastUpdatedAt = 0;
|
||||
let linkedSunLight = null;
|
||||
let linkedBackLight = null;
|
||||
let linkedAmbientLight = null;
|
||||
let linkedPointLight = null;
|
||||
let linkedCamera = null;
|
||||
let linkedEarth = null;
|
||||
let dayNightLightingEnabled = true;
|
||||
let brightStarSprites = [];
|
||||
let celestialRotationQuaternion = new THREE.Quaternion();
|
||||
let celestialViewQuaternion = new THREE.Quaternion();
|
||||
@@ -333,10 +341,71 @@ function updateSpritePositions() {
|
||||
}
|
||||
|
||||
function updateLighting() {
|
||||
if (!dayNightLightingEnabled && CELESTIAL_CONFIG.inspectionLighting?.enabled) {
|
||||
const inspection = CELESTIAL_CONFIG.inspectionLighting;
|
||||
const cameraDirection = linkedCamera
|
||||
? linkedCamera.position.clone().normalize()
|
||||
: defaultSunDirection.clone();
|
||||
const worldUp = new THREE.Vector3(0, 1, 0);
|
||||
const right = new THREE.Vector3().crossVectors(worldUp, cameraDirection);
|
||||
if (right.lengthSq() < 1e-6) {
|
||||
right.set(1, 0, 0);
|
||||
} else {
|
||||
right.normalize();
|
||||
}
|
||||
const adjustedUp = new THREE.Vector3()
|
||||
.crossVectors(cameraDirection, right)
|
||||
.normalize();
|
||||
|
||||
const resolveInspectionDirection = (offset) =>
|
||||
cameraDirection
|
||||
.clone()
|
||||
.multiplyScalar(offset.z)
|
||||
.add(right.clone().multiplyScalar(offset.x))
|
||||
.add(adjustedUp.clone().multiplyScalar(offset.y))
|
||||
.normalize();
|
||||
|
||||
if (linkedAmbientLight) {
|
||||
linkedAmbientLight.color.setHex(inspection.ambientColor);
|
||||
linkedAmbientLight.intensity = inspection.ambientIntensity;
|
||||
}
|
||||
|
||||
if (linkedSunLight) {
|
||||
linkedSunLight.color.setHex(inspection.keyLightColor);
|
||||
linkedSunLight.intensity = inspection.keyLightIntensity;
|
||||
linkedSunLight.position
|
||||
.copy(resolveInspectionDirection(inspection.keyLightOffset))
|
||||
.multiplyScalar(inspection.keyLightDistance);
|
||||
}
|
||||
|
||||
if (linkedBackLight) {
|
||||
linkedBackLight.color.setHex(inspection.backLightColor);
|
||||
linkedBackLight.intensity = inspection.backLightIntensity;
|
||||
linkedBackLight.position
|
||||
.copy(resolveInspectionDirection(inspection.backLightOffset))
|
||||
.multiplyScalar(inspection.backLightDistance);
|
||||
}
|
||||
|
||||
if (linkedPointLight) {
|
||||
linkedPointLight.color.setHex(inspection.pointLightColor);
|
||||
linkedPointLight.intensity = inspection.pointLightIntensity;
|
||||
linkedPointLight.position
|
||||
.copy(resolveInspectionDirection(inspection.pointLightOffset))
|
||||
.multiplyScalar(inspection.pointLightDistance);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const physicalSunDirection = getPhysicalSunDirection(
|
||||
new Date(lastUpdatedAt || Date.now()),
|
||||
);
|
||||
|
||||
if (linkedAmbientLight) {
|
||||
linkedAmbientLight.color.setHex(SCENE_LIGHT_CONFIG.ambient.color);
|
||||
linkedAmbientLight.intensity = SCENE_LIGHT_CONFIG.ambient.intensity;
|
||||
}
|
||||
|
||||
if (linkedSunLight) {
|
||||
linkedSunLight.color.setHex(CELESTIAL_CONFIG.sunLightColor);
|
||||
linkedSunLight.intensity = CELESTIAL_CONFIG.sunLightIntensity;
|
||||
@@ -352,6 +421,16 @@ function updateLighting() {
|
||||
.copy(physicalSunDirection)
|
||||
.multiplyScalar(-CELESTIAL_CONFIG.sunLightDistance * 0.7);
|
||||
}
|
||||
|
||||
if (linkedPointLight) {
|
||||
linkedPointLight.color.setHex(SCENE_LIGHT_CONFIG.point.color);
|
||||
linkedPointLight.intensity = SCENE_LIGHT_CONFIG.point.intensity;
|
||||
linkedPointLight.position.set(
|
||||
SCENE_LIGHT_CONFIG.point.position.x,
|
||||
SCENE_LIGHT_CONFIG.point.position.y,
|
||||
SCENE_LIGHT_CONFIG.point.position.z,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function computeCelestialState(date = new Date()) {
|
||||
@@ -364,14 +443,24 @@ function computeCelestialState(date = new Date()) {
|
||||
|
||||
export function initCelestialLayer(
|
||||
scene,
|
||||
{ camera = null, sunLight = null, backLight = null, earth = null } = {},
|
||||
{
|
||||
camera = null,
|
||||
sunLight = null,
|
||||
backLight = null,
|
||||
ambientLight = null,
|
||||
pointLight = null,
|
||||
earth = null,
|
||||
} = {},
|
||||
) {
|
||||
if (!scene || !CELESTIAL_CONFIG.enabled) return null;
|
||||
|
||||
disposeCelestialLayer();
|
||||
|
||||
linkedCamera = camera;
|
||||
linkedSunLight = sunLight;
|
||||
linkedBackLight = backLight;
|
||||
linkedAmbientLight = ambientLight;
|
||||
linkedPointLight = pointLight;
|
||||
linkedEarth = earth;
|
||||
|
||||
celestialRoot = new THREE.Group();
|
||||
@@ -457,6 +546,10 @@ export function initCelestialLayer(
|
||||
export function updateCelestialLayer(date = new Date(), camera = null) {
|
||||
if (!celestialRoot) return;
|
||||
|
||||
if (camera) {
|
||||
linkedCamera = camera;
|
||||
}
|
||||
|
||||
refreshCelestialView();
|
||||
|
||||
const now = date.getTime();
|
||||
@@ -503,6 +596,11 @@ export function setCelestialFollow(nextFollow = {}) {
|
||||
return getCelestialDebugState();
|
||||
}
|
||||
|
||||
export function setCelestialDayNightEnabled(enabled) {
|
||||
dayNightLightingEnabled = enabled;
|
||||
updateLighting();
|
||||
}
|
||||
|
||||
export function disposeCelestialLayer() {
|
||||
if (celestialRoot?.parent) {
|
||||
celestialRoot.parent.remove(celestialRoot);
|
||||
@@ -531,7 +629,11 @@ export function disposeCelestialLayer() {
|
||||
lastUpdatedAt = 0;
|
||||
linkedSunLight = null;
|
||||
linkedBackLight = null;
|
||||
linkedAmbientLight = null;
|
||||
linkedPointLight = null;
|
||||
linkedCamera = null;
|
||||
linkedEarth = null;
|
||||
dayNightLightingEnabled = true;
|
||||
sunDirection.copy(defaultSunDirection);
|
||||
moonDirection.copy(defaultMoonDirection);
|
||||
runtimeOrientationEuler = {
|
||||
|
||||
@@ -92,6 +92,45 @@ export const CELESTIAL_CONFIG = {
|
||||
sunLightColor: 0xfff4df,
|
||||
backLightIntensity: 0.3,
|
||||
backLightColor: 0x2b4c78,
|
||||
inspectionLighting: {
|
||||
enabled: true,
|
||||
ambientIntensity: 0.64,
|
||||
ambientColor: 0x707070,
|
||||
keyLightIntensity: 0.92,
|
||||
keyLightColor: 0xfcfcfb,
|
||||
keyLightDistance: 380,
|
||||
keyLightOffset: { x: 0.42, y: 0.34, z: 0.84 },
|
||||
backLightIntensity: 0.26,
|
||||
backLightColor: 0x8f96a0,
|
||||
backLightDistance: 260,
|
||||
backLightOffset: { x: -0.52, y: -0.1, z: -0.62 },
|
||||
pointLightIntensity: 0.36,
|
||||
pointLightColor: 0xfafcff,
|
||||
pointLightDistance: 320,
|
||||
pointLightOffset: { x: 0.18, y: 0.52, z: 0.62 },
|
||||
},
|
||||
};
|
||||
|
||||
export const SCENE_LIGHT_CONFIG = {
|
||||
ambient: {
|
||||
color: 0x404060,
|
||||
intensity: 1,
|
||||
},
|
||||
sun: {
|
||||
color: 0xffffff,
|
||||
intensity: 1.2,
|
||||
position: { x: 5, y: 3, z: 5 },
|
||||
},
|
||||
back: {
|
||||
color: 0x446688,
|
||||
intensity: 0.3,
|
||||
position: { x: -5, y: 0, z: -5 },
|
||||
},
|
||||
point: {
|
||||
color: 0xffffff,
|
||||
intensity: 0.4,
|
||||
position: { x: 10, y: 10, z: 10 },
|
||||
},
|
||||
};
|
||||
|
||||
export const TERRAIN_CONFIG = {
|
||||
@@ -192,6 +231,9 @@ export const SATELLITE_CONFIG = {
|
||||
initialLoadCount: 2400,
|
||||
hydrateFullAfterInitialLoad: true,
|
||||
trailLength: 10,
|
||||
displayAltitudeOffset: 8,
|
||||
frontFacingDotThreshold: 0.015,
|
||||
overlayRenderOrder: 12,
|
||||
dotSize: 4,
|
||||
ringSize: 0.07,
|
||||
apiPath: '/api/v1/visualization/geo/satellites',
|
||||
|
||||
879
frontend/public/earth/js/controls.js
vendored
879
frontend/public/earth/js/controls.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ export let terrain = null;
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
let _earthMaterial = null;
|
||||
let _earthShader = null;
|
||||
let _dayNightEnabled = true;
|
||||
const _earthSunDirection = new THREE.Vector3(
|
||||
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.x,
|
||||
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.y,
|
||||
@@ -33,6 +34,7 @@ function applyEarthDayNightShader(material) {
|
||||
shader.uniforms.uTwilightColor = { value: twilightColor };
|
||||
shader.uniforms.uNightTintColor = { value: nightTintColor };
|
||||
shader.uniforms.uNightTintIntensity = { value: EARTH_MATERIAL_CONFIG.dayNight.nightTintIntensity };
|
||||
shader.uniforms.uDayNightEnabled = { value: _dayNightEnabled ? 1.0 : 0.0 };
|
||||
|
||||
shader.vertexShader = shader.vertexShader.replace(
|
||||
"#include <common>",
|
||||
@@ -55,7 +57,8 @@ uniform float uTwilightWidth;
|
||||
uniform float uTwilightIntensity;
|
||||
uniform vec3 uTwilightColor;
|
||||
uniform vec3 uNightTintColor;
|
||||
uniform float uNightTintIntensity;`,
|
||||
uniform float uNightTintIntensity;
|
||||
uniform float uDayNightEnabled;`,
|
||||
).replace(
|
||||
"#include <output_fragment>",
|
||||
`
|
||||
@@ -65,16 +68,27 @@ uniform float uNightTintIntensity;`,
|
||||
float daylight = smoothstep(-uTwilightWidth, uTwilightWidth, sunFacing);
|
||||
float twilight = 1.0 - smoothstep(0.0, uTwilightWidth, abs(sunFacing));
|
||||
|
||||
outgoingLight *= mix(uNightFloor, uDayBoost, daylight);
|
||||
outgoingLight += uTwilightColor * twilight * uTwilightIntensity;
|
||||
outgoingLight += uNightTintColor * (1.0 - daylight) * uNightTintIntensity;
|
||||
// Camera-facing diffuse: vNormal and vViewPosition are both in view space.
|
||||
// N·V gives 1.0 at center-facing, 0 at limb — creates depth cue regardless of earth rotation.
|
||||
float nDotV = max(0.0, dot(normalize(vNormal), normalize(vViewPosition)));
|
||||
float cameraBoost = mix(0.62, 1.08, nDotV);
|
||||
|
||||
float dn = uDayNightEnabled;
|
||||
vec3 dnLight = outgoingLight;
|
||||
dnLight *= mix(uNightFloor, uDayBoost, daylight);
|
||||
dnLight += uTwilightColor * twilight * uTwilightIntensity;
|
||||
dnLight += uNightTintColor * (1.0 - daylight) * uNightTintIntensity;
|
||||
|
||||
// dn=0: emissive base (from material, set in JS) * camera-facing boost → always readable
|
||||
// dn=1: full day/night solar lighting
|
||||
outgoingLight = mix(outgoingLight * cameraBoost, dnLight, dn);
|
||||
|
||||
#include <output_fragment>
|
||||
`,
|
||||
);
|
||||
};
|
||||
|
||||
material.customProgramCacheKey = () => "earth-day-night-v1";
|
||||
material.customProgramCacheKey = () => "earth-day-night-v5";
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
|
||||
@@ -348,6 +362,28 @@ export function setEarthSunDirection(direction) {
|
||||
}
|
||||
}
|
||||
|
||||
export function setDayNightEnabled(enabled) {
|
||||
_dayNightEnabled = enabled;
|
||||
if (_earthShader?.uniforms?.uDayNightEnabled) {
|
||||
_earthShader.uniforms.uDayNightEnabled.value = enabled ? 1.0 : 0.0;
|
||||
}
|
||||
if (_earthMaterial) {
|
||||
if (enabled) {
|
||||
// Restore normal Phong lighting + custom day/night shader
|
||||
_earthMaterial.color.setHex(EARTH_MATERIAL_CONFIG.color);
|
||||
_earthMaterial.emissive.setHex(EARTH_MATERIAL_CONFIG.emissive);
|
||||
_earthMaterial.emissiveMap = null;
|
||||
} else {
|
||||
// Full bright: zero diffuse so directional light has no effect;
|
||||
// use original color as emissive map to show texture uniformly.
|
||||
_earthMaterial.color.setRGB(0, 0, 0);
|
||||
_earthMaterial.emissive.setHex(EARTH_MATERIAL_CONFIG.color);
|
||||
_earthMaterial.emissiveMap = _earthMaterial.map;
|
||||
}
|
||||
_earthMaterial.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadEarthTexture() {
|
||||
return new Promise((resolve) => {
|
||||
if (!_earthMaterial) { resolve(); return; }
|
||||
@@ -368,6 +404,10 @@ export function loadEarthTexture() {
|
||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
_earthMaterial.map = texture;
|
||||
// If day/night is currently disabled, sync emissiveMap to the newly loaded texture
|
||||
if (!_dayNightEnabled) {
|
||||
_earthMaterial.emissiveMap = texture;
|
||||
}
|
||||
_earthMaterial.needsUpdate = true;
|
||||
resolve();
|
||||
},
|
||||
|
||||
174
frontend/public/earth/js/layer-startup-tasks.js
Normal file
174
frontend/public/earth/js/layer-startup-tasks.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import {
|
||||
loadGeoJSONFromPath,
|
||||
loadLandingPoints,
|
||||
getCableLegendItems,
|
||||
toggleCables,
|
||||
} from "./cables.js";
|
||||
import {
|
||||
clearSatelliteData,
|
||||
getSatelliteLegendItems,
|
||||
loadSatellites,
|
||||
toggleSatellites,
|
||||
} from "./satellites.js";
|
||||
import {
|
||||
loadBGPAnomalies,
|
||||
toggleBGP,
|
||||
} from "./bgp.js";
|
||||
|
||||
/**
|
||||
* Layer startup task registry.
|
||||
*
|
||||
* This module is the startup-task counterpart to the layer registry in controls.js:
|
||||
* - controls.js owns layer metadata such as startupPriority/startupMode/startupMessage
|
||||
* - this file owns the executable startup task factory for each layer id
|
||||
*
|
||||
* A startup task is registered via registerLayerStartupTask(id, taskFactory).
|
||||
* The taskFactory receives a startup context from main.js and must return an async
|
||||
* function with the signature async (layerDefinition) => void.
|
||||
*
|
||||
* Put a task here only when a layer needs dedicated startup loading work:
|
||||
* - preloading data at boot
|
||||
* - staged loading with progress/loading messages
|
||||
* - post-load UI refresh or warmup
|
||||
*
|
||||
* Do not put plain visibility toggles or persistent UI state here; those still belong
|
||||
* to the layer registry/state flow in controls.js.
|
||||
*/
|
||||
const startupTaskRegistry = new Map();
|
||||
|
||||
export function resolveStartupMessage(layer, key, fallback) {
|
||||
const message = layer?.startupMessage;
|
||||
if (message && typeof message === "object" && key in message) {
|
||||
return message[key];
|
||||
}
|
||||
if (typeof message === "string" && message.trim()) {
|
||||
return message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function createLayerStartupTaskMap(context) {
|
||||
return Object.fromEntries(
|
||||
Array.from(startupTaskRegistry.entries()).map(([id, taskFactory]) => [
|
||||
id,
|
||||
taskFactory(context),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export function registerLayerStartupTask(id, taskFactory) {
|
||||
if (typeof id !== "string" || !id.trim()) {
|
||||
throw new Error("registerLayerStartupTask 需要有效的图层 id");
|
||||
}
|
||||
if (typeof taskFactory !== "function") {
|
||||
throw new Error("registerLayerStartupTask 需要可调用的任务工厂");
|
||||
}
|
||||
startupTaskRegistry.set(id, taskFactory);
|
||||
}
|
||||
|
||||
function registerBuiltinLayerStartupTasks() {
|
||||
startupTaskRegistry.clear();
|
||||
registerCableStartupTask();
|
||||
registerSatelliteStartupTask();
|
||||
registerBGPStartupTask();
|
||||
}
|
||||
|
||||
function registerCableStartupTask() {
|
||||
registerLayerStartupTask("cables", (context) => async (layer) => {
|
||||
if (!context.isCablesEnabled()) return;
|
||||
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "prepare", "正在加载登陆点..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
await loadLandingPoints(context.scene, context.earth, { silent: true });
|
||||
} catch (error) {
|
||||
context.reportError("登陆点", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "load", "正在加载海缆..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
await loadGeoJSONFromPath(context.scene, context.earth, { silent: true });
|
||||
if (!context.isCancelled() && context.isCablesEnabled()) {
|
||||
toggleCables(true);
|
||||
context.updateCableToggleUi(true);
|
||||
context.setLegendItems("cables", getCableLegendItems());
|
||||
context.refreshLegend();
|
||||
}
|
||||
} catch (error) {
|
||||
context.reportError(layer?.startupLabel || layer?.label || "海缆", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
});
|
||||
}
|
||||
|
||||
function registerSatelliteStartupTask() {
|
||||
registerLayerStartupTask("satellites", (context) => async (layer) => {
|
||||
if (!context.isSatellitesEnabled()) return;
|
||||
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "load", "正在加载卫星..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
clearSatelliteData();
|
||||
const loadResult = await loadSatellites({
|
||||
limit: context.getInitialSatelliteLoadLimit(),
|
||||
});
|
||||
if (!context.isCancelled() && context.isSatellitesEnabled()) {
|
||||
context.updateSatelliteToggleUi(true, loadResult.count);
|
||||
context.setLegendItems("satellites", getSatelliteLegendItems());
|
||||
context.refreshLegend();
|
||||
context.scheduleSatellitePositionWarmup(() => {
|
||||
if (!context.isCancelled() && context.isSatellitesEnabled()) {
|
||||
toggleSatellites(true);
|
||||
}
|
||||
});
|
||||
|
||||
if (context.shouldHydrateFullSatelliteSet(loadResult)) {
|
||||
const hydrationToken = context.nextSatelliteHydrationToken();
|
||||
context.hydrateAllSatellitesInBackground(
|
||||
() =>
|
||||
hydrationToken === context.getSatelliteHydrationToken() &&
|
||||
!context.isCancelled() &&
|
||||
context.isSatellitesEnabled(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
context.reportError(layer?.startupLabel || layer?.label || "卫星", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
});
|
||||
}
|
||||
|
||||
function registerBGPStartupTask() {
|
||||
registerLayerStartupTask("bgp", (context) => async (layer) => {
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "load", "正在加载BGP态势..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
const bgpResult = await loadBGPAnomalies(context.scene, context.earth);
|
||||
if (!context.isCancelled()) {
|
||||
toggleBGP(context.getShowBGP());
|
||||
context.updateBGPHud(bgpResult);
|
||||
context.syncBGPKnownEventIds();
|
||||
}
|
||||
} catch (error) {
|
||||
context.reportError(layer?.startupLabel || layer?.label || "BGP态势", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
});
|
||||
}
|
||||
|
||||
registerBuiltinLayerStartupTasks();
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
BGP_CONFIG,
|
||||
CRUISE_CONFIG,
|
||||
ROTATION_MODE,
|
||||
SCENE_LIGHT_CONFIG,
|
||||
} from "./constants.js";
|
||||
import { vector3ToLatLon, screenToEarthCoords } from "./utils.js";
|
||||
import {
|
||||
@@ -36,7 +37,7 @@ import {
|
||||
clearEarthTexture,
|
||||
setEarthSunDirection,
|
||||
} from "./earth.js";
|
||||
import { registerTerrainMesh, clearTerrainData } from "./terrain.js";
|
||||
import { registerTerrainMesh, clearTerrainData, sampleElevationAt } from "./terrain.js";
|
||||
import {
|
||||
initCelestialLayer,
|
||||
updateCelestialLayer,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
getSunDirection,
|
||||
setCelestialOrientation,
|
||||
setCelestialFollow,
|
||||
setCelestialDayNightEnabled,
|
||||
} from "./celestial.js";
|
||||
import {
|
||||
loadGeoJSONFromPath,
|
||||
@@ -129,12 +131,17 @@ import {
|
||||
getAutoRotate,
|
||||
getRotationMode,
|
||||
getShowTerrain,
|
||||
getStartupLoadLayers,
|
||||
setAutoRotate,
|
||||
applyImmediateView,
|
||||
focusEarthView,
|
||||
getZoomLevel,
|
||||
teardownControls,
|
||||
} from "./controls.js";
|
||||
import {
|
||||
createLayerStartupTaskMap,
|
||||
resolveStartupMessage,
|
||||
} from "./layer-startup-tasks.js";
|
||||
import {
|
||||
setLayerButtonState,
|
||||
} from "./layer-button-state.js";
|
||||
@@ -1224,10 +1231,13 @@ export function init() {
|
||||
registerTerrainMesh(createTerrain(earthObj));
|
||||
initCelestialLayer(scene, {
|
||||
camera,
|
||||
ambientLight: sceneLights?.ambientLight ?? null,
|
||||
sunLight: sceneLights?.sunLight ?? null,
|
||||
backLight: sceneLights?.backLight ?? null,
|
||||
pointLight: sceneLights?.pointLight ?? null,
|
||||
earth: earthObj,
|
||||
});
|
||||
setCelestialDayNightEnabled(true);
|
||||
createGridLines(scene, earthObj);
|
||||
createSatellites(scene, earthObj);
|
||||
|
||||
@@ -1258,21 +1268,43 @@ function registerGlobalApi() {
|
||||
}
|
||||
|
||||
function addLights() {
|
||||
const ambientLight = new THREE.AmbientLight(0x404060);
|
||||
const ambientLight = new THREE.AmbientLight(SCENE_LIGHT_CONFIG.ambient.color);
|
||||
ambientLight.intensity = SCENE_LIGHT_CONFIG.ambient.intensity;
|
||||
scene.add(ambientLight);
|
||||
|
||||
const sunLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
sunLight.position.set(5, 3, 5);
|
||||
const sunLight = new THREE.DirectionalLight(
|
||||
SCENE_LIGHT_CONFIG.sun.color,
|
||||
SCENE_LIGHT_CONFIG.sun.intensity,
|
||||
);
|
||||
sunLight.position.set(
|
||||
SCENE_LIGHT_CONFIG.sun.position.x,
|
||||
SCENE_LIGHT_CONFIG.sun.position.y,
|
||||
SCENE_LIGHT_CONFIG.sun.position.z,
|
||||
);
|
||||
sunLight.target.position.set(0, 0, 0);
|
||||
scene.add(sunLight);
|
||||
scene.add(sunLight.target);
|
||||
|
||||
const backLight = new THREE.DirectionalLight(0x446688, 0.3);
|
||||
backLight.position.set(-5, 0, -5);
|
||||
const backLight = new THREE.DirectionalLight(
|
||||
SCENE_LIGHT_CONFIG.back.color,
|
||||
SCENE_LIGHT_CONFIG.back.intensity,
|
||||
);
|
||||
backLight.position.set(
|
||||
SCENE_LIGHT_CONFIG.back.position.x,
|
||||
SCENE_LIGHT_CONFIG.back.position.y,
|
||||
SCENE_LIGHT_CONFIG.back.position.z,
|
||||
);
|
||||
scene.add(backLight);
|
||||
|
||||
const pointLight = new THREE.PointLight(0xffffff, 0.4);
|
||||
pointLight.position.set(10, 10, 10);
|
||||
const pointLight = new THREE.PointLight(
|
||||
SCENE_LIGHT_CONFIG.point.color,
|
||||
SCENE_LIGHT_CONFIG.point.intensity,
|
||||
);
|
||||
pointLight.position.set(
|
||||
SCENE_LIGHT_CONFIG.point.position.x,
|
||||
SCENE_LIGHT_CONFIG.point.position.y,
|
||||
SCENE_LIGHT_CONFIG.point.position.z,
|
||||
);
|
||||
scene.add(pointLight);
|
||||
|
||||
return {
|
||||
@@ -1364,100 +1396,54 @@ async function loadData() {
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
|
||||
// Step 2 — Landing points
|
||||
if (cablesEnabled) {
|
||||
setLoadingMessage("正在加载登陆点...");
|
||||
await yieldFrame(12);
|
||||
try {
|
||||
await loadLandingPoints(scene, earth, { silent: true });
|
||||
} catch (err) {
|
||||
errors.push({ label: "登陆点", reason: err });
|
||||
}
|
||||
const startupLoaders = createLayerStartupTaskMap({
|
||||
scene,
|
||||
earth,
|
||||
setLoadingMessage,
|
||||
yieldFrame,
|
||||
refreshLegend,
|
||||
setLegendItems,
|
||||
updateCableToggleUi,
|
||||
updateSatelliteToggleUi,
|
||||
updateBGPHud,
|
||||
getShowBGP,
|
||||
getInitialSatelliteLoadLimit,
|
||||
shouldHydrateFullSatelliteSet,
|
||||
scheduleSatellitePositionWarmup,
|
||||
hydrateAllSatellitesInBackground,
|
||||
syncBGPKnownEventIds: () => ensureBGPCruiseAdapter().syncKnownEventIds(),
|
||||
isCancelled: () => loadToken !== currentLoadToken || destroyed,
|
||||
isCablesEnabled: () => cablesEnabled,
|
||||
isSatellitesEnabled: () => satellitesEnabled,
|
||||
nextSatelliteHydrationToken: () => ++satelliteHydrationToken,
|
||||
getSatelliteHydrationToken: () => satelliteHydrationToken,
|
||||
reportError: (label, reason) => {
|
||||
errors.push({ label, reason });
|
||||
},
|
||||
});
|
||||
|
||||
const startupLayers = getStartupLoadLayers();
|
||||
const startupLoadQueue = startupLayers
|
||||
.map((layer) => ({
|
||||
layer,
|
||||
run: startupLoaders[layer.id],
|
||||
}))
|
||||
.filter((entry) => typeof entry.run === "function");
|
||||
|
||||
for (const entry of startupLoadQueue) {
|
||||
await entry.run(entry.layer);
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
}
|
||||
|
||||
// Step 3 — Cables
|
||||
if (cablesEnabled) {
|
||||
setLoadingMessage("正在加载海缆...");
|
||||
await yieldFrame(12);
|
||||
try {
|
||||
const cableCount = await loadGeoJSONFromPath(scene, earth, {
|
||||
silent: true,
|
||||
});
|
||||
if (loadToken === currentLoadToken && cablesEnabled) {
|
||||
toggleCables(true);
|
||||
updateCableToggleUi(true);
|
||||
setLegendItems("cables", getCableLegendItems());
|
||||
refreshLegend();
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ label: "海缆", reason: err });
|
||||
}
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
}
|
||||
|
||||
// Step 4 — Satellites
|
||||
if (satellitesEnabled) {
|
||||
setLoadingMessage("正在加载卫星...");
|
||||
await yieldFrame(12);
|
||||
try {
|
||||
clearSatelliteData();
|
||||
const loadResult = await loadSatellites({
|
||||
limit: getInitialSatelliteLoadLimit(),
|
||||
});
|
||||
if (loadToken === currentLoadToken && satellitesEnabled) {
|
||||
updateSatelliteToggleUi(true, loadResult.count);
|
||||
setLegendItems("satellites", getSatelliteLegendItems());
|
||||
refreshLegend();
|
||||
scheduleSatellitePositionWarmup(() => {
|
||||
if (
|
||||
loadToken === currentLoadToken &&
|
||||
satellitesEnabled &&
|
||||
!destroyed
|
||||
) {
|
||||
toggleSatellites(true);
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldHydrateFullSatelliteSet(loadResult)) {
|
||||
const hydrationToken = ++satelliteHydrationToken;
|
||||
hydrateAllSatellitesInBackground(
|
||||
() =>
|
||||
hydrationToken === satelliteHydrationToken &&
|
||||
loadToken === currentLoadToken &&
|
||||
satellitesEnabled &&
|
||||
!destroyed,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ label: "卫星", reason: err });
|
||||
}
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
}
|
||||
|
||||
// Step 5 — BGP
|
||||
setLoadingMessage("正在加载BGP态势...");
|
||||
await yieldFrame(12);
|
||||
try {
|
||||
const bgpResult = await loadBGPAnomalies(scene, earth);
|
||||
if (loadToken === currentLoadToken) {
|
||||
toggleBGP(true);
|
||||
updateBGPHud(bgpResult);
|
||||
ensureBGPCruiseAdapter().syncKnownEventIds();
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ label: "BGP态势", reason: err });
|
||||
}
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
|
||||
// Step 6 — Terrain (if enabled)
|
||||
if (getShowTerrain()) {
|
||||
setLoadingMessage("正在渲染地形...");
|
||||
const terrainLayer = startupLayers.find((layer) => layer.id === "terrain");
|
||||
const terrainMessage = resolveStartupMessage(
|
||||
terrainLayer,
|
||||
"load",
|
||||
"正在渲染地形...",
|
||||
);
|
||||
setLoadingMessage(terrainMessage);
|
||||
await yieldFrame(24);
|
||||
}
|
||||
|
||||
@@ -1493,7 +1479,14 @@ export async function reloadData() {
|
||||
await loadData();
|
||||
}
|
||||
|
||||
export async function setCablesEnabled(enabled) {
|
||||
export function getSatellitesEnabled() {
|
||||
return satellitesEnabled;
|
||||
}
|
||||
|
||||
export async function setCablesEnabled(
|
||||
enabled,
|
||||
{ suppressStatus = false, suppressLoadingUi = false } = {},
|
||||
) {
|
||||
if (enabled === cablesEnabled) {
|
||||
updateCableToggleUi(enabled);
|
||||
return getCableLines().length;
|
||||
@@ -1502,32 +1495,47 @@ export async function setCablesEnabled(enabled) {
|
||||
if (!enabled) {
|
||||
clearSelectionAndInfo();
|
||||
disableCables();
|
||||
showStatusMessage("线缆已隐藏", "info");
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage("线缆已隐藏", "info");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
setLoadingMessage("正在加载线缆数据...");
|
||||
setLoading(true);
|
||||
hideError();
|
||||
if (!suppressLoadingUi) {
|
||||
setLoadingMessage("正在加载线缆数据...");
|
||||
setLoading(true);
|
||||
hideError();
|
||||
}
|
||||
|
||||
try {
|
||||
const cableCount = await ensureCablesEnabled();
|
||||
showStatusMessage("线缆已显示", "info");
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage("线缆已显示", "info");
|
||||
}
|
||||
return cableCount;
|
||||
} catch (error) {
|
||||
cablesEnabled = false;
|
||||
clearCableData(getEarth());
|
||||
updateCableToggleUi(false);
|
||||
const message = `线缆加载失败: ${error?.message || String(error)}`;
|
||||
showError(message);
|
||||
showStatusMessage(message, "error");
|
||||
if (!suppressLoadingUi) {
|
||||
showError(message);
|
||||
}
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage(message, "error");
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!suppressLoadingUi) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setSatellitesEnabled(enabled) {
|
||||
export async function setSatellitesEnabled(
|
||||
enabled,
|
||||
{ suppressStatus = false, suppressLoadingUi = false } = {},
|
||||
) {
|
||||
if (enabled === satellitesEnabled) {
|
||||
updateSatelliteToggleUi(enabled);
|
||||
return getSatelliteCount();
|
||||
@@ -1539,24 +1547,34 @@ export async function setSatellitesEnabled(enabled) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
setLoadingMessage("正在加载卫星数据...");
|
||||
setLoading(true);
|
||||
hideError();
|
||||
if (!suppressLoadingUi) {
|
||||
setLoadingMessage("正在加载卫星数据...");
|
||||
setLoading(true);
|
||||
hideError();
|
||||
}
|
||||
|
||||
try {
|
||||
const satelliteCount = await ensureSatellitesEnabled();
|
||||
showStatusMessage("卫星已显示", "info");
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage("卫星已显示", "info");
|
||||
}
|
||||
return satelliteCount;
|
||||
} catch (error) {
|
||||
satellitesEnabled = false;
|
||||
resetSatelliteState();
|
||||
updateSatelliteToggleUi(false, 0);
|
||||
const message = `卫星加载失败: ${error?.message || String(error)}`;
|
||||
showError(message);
|
||||
showStatusMessage(message, "error");
|
||||
if (!suppressLoadingUi) {
|
||||
showError(message);
|
||||
}
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage(message, "error");
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!suppressLoadingUi) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1618,7 +1636,10 @@ function getFrontFacingCables(cableLines) {
|
||||
scratchCableDirection
|
||||
.subVectors(scratchCableCenter, earth.position)
|
||||
.normalize();
|
||||
return scratchCameraToEarth.dot(scratchCableDirection) > 0;
|
||||
return (
|
||||
scratchCameraToEarth.dot(scratchCableDirection) >
|
||||
SATELLITE_CONFIG.frontFacingDotThreshold
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1634,7 +1655,10 @@ function getFrontFacingBGPMarkers(markers) {
|
||||
scratchBGPDirection
|
||||
.subVectors(scratchBGPWorldPosition, earth.position)
|
||||
.normalize();
|
||||
return scratchCameraToEarth.dot(scratchBGPDirection) > 0;
|
||||
return (
|
||||
scratchCameraToEarth.dot(scratchBGPDirection) >
|
||||
SATELLITE_CONFIG.frontFacingDotThreshold
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1793,10 +1817,16 @@ function onMouseMove(event) {
|
||||
if (earthPoint) {
|
||||
const coords = vector3ToLatLon(earthPoint);
|
||||
updateCoordinatesDisplay(coords.lat, coords.lon, coords.alt);
|
||||
const elevMeters = sampleElevationAt(coords.lat, coords.lon);
|
||||
const elevText = elevMeters !== null
|
||||
? elevMeters >= 1000
|
||||
? `${(elevMeters / 1000).toFixed(2)} km`
|
||||
: `${Math.round(elevMeters)} m`
|
||||
: "—";
|
||||
showTooltip(
|
||||
event.clientX + TOOLTIP_COORDS_OFFSET,
|
||||
event.clientY + TOOLTIP_COORDS_OFFSET,
|
||||
`纬度: ${coords.lat}°<br>经度: ${coords.lon}°<br>海拔: ${coords.alt.toFixed(1)} km`,
|
||||
`纬度: ${coords.lat}°<br>经度: ${coords.lon}°<br>海拔: ${elevText}`,
|
||||
);
|
||||
} else {
|
||||
hideTooltip();
|
||||
|
||||
@@ -490,7 +490,8 @@ function computeSatellitePosition(satellite, time) {
|
||||
}
|
||||
|
||||
const r = Math.sqrt(x * x + y * y + z * z);
|
||||
const displayRadius = CONFIG.earthRadius * 1.05;
|
||||
const displayRadius =
|
||||
CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
|
||||
const scale = displayRadius / r;
|
||||
|
||||
return new THREE.Vector3(x * scale, y * scale, z * scale);
|
||||
@@ -637,7 +638,7 @@ function buildTleLinesFromElements(props, fallbackTime) {
|
||||
}
|
||||
|
||||
function generateFallbackPosition(satellite, index, total) {
|
||||
const radius = CONFIG.earthRadius + 5;
|
||||
const radius = CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
|
||||
|
||||
const noradId = satellite.properties?.norad_cat_id || index;
|
||||
const inclination = satellite.properties?.inclination || 53;
|
||||
@@ -843,6 +844,10 @@ export function toggleTrails(visible) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getShowTrails() {
|
||||
return showTrails;
|
||||
}
|
||||
|
||||
export function getShowSatellites() {
|
||||
return showSatellites;
|
||||
}
|
||||
@@ -902,7 +907,10 @@ export function isSatelliteFrontFacing(index, camera = cameraRef) {
|
||||
.subVectors(scratchWorldSatellitePosition, earthObjRef.position)
|
||||
.normalize();
|
||||
|
||||
return scratchToCamera.dot(scratchToSatellite) > 0;
|
||||
return (
|
||||
scratchToCamera.dot(scratchToSatellite) >
|
||||
SATELLITE_CONFIG.frontFacingDotThreshold
|
||||
);
|
||||
}
|
||||
|
||||
function createBrighterDotCanvas() {
|
||||
@@ -948,6 +956,7 @@ function createRingSprite(position, isLocked = false) {
|
||||
const sprite = new THREE.Sprite(spriteMaterial);
|
||||
sprite.position.copy(position);
|
||||
sprite.scale.set(SATELLITE_CONFIG.ringSize, SATELLITE_CONFIG.ringSize, 1);
|
||||
sprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
|
||||
earthObjRef.add(sprite);
|
||||
return sprite;
|
||||
}
|
||||
@@ -967,6 +976,7 @@ function createRelatedSatelliteSprite(position, color = "#7dd3fc") {
|
||||
const sprite = new THREE.Sprite(spriteMaterial);
|
||||
sprite.position.copy(position);
|
||||
sprite.scale.set(SATELLITE_CONFIG.ringSize * 0.8, SATELLITE_CONFIG.ringSize * 0.8, 1);
|
||||
sprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
|
||||
earthObjRef.add(sprite);
|
||||
return sprite;
|
||||
}
|
||||
@@ -989,6 +999,7 @@ export function showHoverRing(position, isLocked = false) {
|
||||
lockedDotSprite = new THREE.Sprite(dotMaterial);
|
||||
lockedDotSprite.position.copy(position);
|
||||
lockedDotSprite.scale.set(4, 4, 1);
|
||||
lockedDotSprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder + 1;
|
||||
earthObjRef.add(lockedDotSprite);
|
||||
return lockedRingSprite;
|
||||
}
|
||||
@@ -1199,7 +1210,8 @@ function calculatePredictedOrbit(
|
||||
|
||||
if (points.length < samples * 0.5) {
|
||||
points.length = 0;
|
||||
const radius = CONFIG.earthRadius + 5;
|
||||
const radius =
|
||||
CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
|
||||
const inclination = satellite.properties?.inclination || 53;
|
||||
const raan = satellite.properties?.raan || 0;
|
||||
|
||||
@@ -1250,9 +1262,12 @@ export function showPredictedOrbit(satellite) {
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthTest: true,
|
||||
depthWrite: false,
|
||||
});
|
||||
|
||||
predictedOrbitLine = new THREE.Line(geometry, material);
|
||||
predictedOrbitLine.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
|
||||
earthObjRef.add(predictedOrbitLine);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ let terrainLoadPromise = null;
|
||||
let terrainReady = false;
|
||||
let terrainFailed = false;
|
||||
let terrainTileCache = new Map();
|
||||
let resolvedTileCache = new Map();
|
||||
let terrainVertexSamples = null;
|
||||
let terrainOpacity = TERRAIN_CONFIG.opacity;
|
||||
|
||||
@@ -67,7 +68,9 @@ async function decodeTerrainTile(z, x, y) {
|
||||
TERRAIN_CONFIG.tileSize,
|
||||
TERRAIN_CONFIG.tileSize,
|
||||
);
|
||||
return { data, width, height };
|
||||
const tileData = { data, width, height };
|
||||
resolvedTileCache.set(cacheKey, tileData);
|
||||
return tileData;
|
||||
})();
|
||||
|
||||
terrainTileCache.set(cacheKey, tilePromise);
|
||||
@@ -236,6 +239,7 @@ export function registerTerrainMesh(mesh) {
|
||||
terrainFailed = false;
|
||||
terrainLoadPromise = null;
|
||||
terrainTileCache = new Map();
|
||||
resolvedTileCache = new Map();
|
||||
terrainOpacity = TERRAIN_CONFIG.opacity;
|
||||
if (terrainMesh?.material) {
|
||||
terrainMesh.material.opacity = terrainOpacity;
|
||||
@@ -287,9 +291,19 @@ export function clearTerrainData() {
|
||||
terrainFailed = false;
|
||||
terrainVertexSamples = null;
|
||||
terrainTileCache = new Map();
|
||||
resolvedTileCache = new Map();
|
||||
terrainOpacity = TERRAIN_CONFIG.opacity;
|
||||
}
|
||||
|
||||
export function sampleElevationAt(lat, lon) {
|
||||
if (!terrainReady) return null;
|
||||
const z = TERRAIN_CONFIG.baseZoom;
|
||||
const { tileX, tileY, pixelX, pixelY } = latLonToTileSample(lat, lon, z, TERRAIN_CONFIG.tileSize);
|
||||
const tile = resolvedTileCache.get(`${z}/${tileX}/${tileY}`);
|
||||
if (!tile) return null;
|
||||
return Math.max(0, decodeTerrariumHeight(tile, pixelX, pixelY));
|
||||
}
|
||||
|
||||
export function setTerrainOpacity(nextOpacity) {
|
||||
terrainOpacity = THREE.MathUtils.clamp(nextOpacity, 0.05, 1);
|
||||
if (terrainMesh?.material) {
|
||||
|
||||
Reference in New Issue
Block a user