release: bump version to 0.41.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
linkong
2026-04-27 16:31:34 +08:00
parent 3ea99a9529
commit 655e2a7d2d
27 changed files with 637 additions and 83 deletions

View File

@@ -20,7 +20,56 @@ export let lockedCable = null;
let cableIdMap = new Map();
let cableStates = new Map();
let cablesVisible = true;
let landingPointGeometry = null;
let landingPointTexture = null;
const _lpWorldPos = new THREE.Vector3();
function createLandingPointTexture() {
const size = CABLE_CONFIG.landingPoint.textureSize;
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
const iconPath = new Path2D(
[
"M400 704",
"C386 704 375 697 367 684",
"L173 378",
"C117 290 144 173 229 111",
"C278 75 337 57 400 57",
"C463 57 522 75 571 111",
"C656 173 683 290 627 378",
"L433 684",
"C425 697 414 704 400 704",
"Z",
].join(" "),
);
ctx.clearRect(0, 0, size, size);
ctx.save();
ctx.translate(size * 0.12, size * 0.02);
ctx.scale(size / 1000, size / 1000);
ctx.fillStyle = "#ffffff";
ctx.fill(iconPath);
ctx.globalCompositeOperation = "destination-out";
ctx.beginPath();
ctx.arc(400, 320, 86, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.needsUpdate = true;
return texture;
}
function getLandingPointTexture() {
if (!landingPointTexture) {
landingPointTexture = createLandingPointTexture();
}
return landingPointTexture;
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
@@ -49,7 +98,7 @@ function disposeMaterial(material) {
return;
}
if (material.map) {
if (material.map && !material.userData?.sharedMap) {
material.map.dispose();
}
material.dispose();
@@ -69,6 +118,22 @@ function disposeObject(object, parent) {
}
}
function setLandingPointMaterialState(point, { color, opacity, emissive, emissiveIntensity }) {
point.material.color.set(color);
point.material.opacity = opacity;
if (point.material.emissive && emissive !== undefined) {
point.material.emissive.setHex(emissive);
}
if ("emissiveIntensity" in point.material && emissiveIntensity !== undefined) {
point.material.emissiveIntensity = emissiveIntensity;
}
}
function setLandingPointScale(point, heightScale) {
const aspect = CABLE_CONFIG.landingPoint.iconAspectRatio;
point.scale.set(heightScale * aspect, heightScale, 1);
}
function getCableColor(properties) {
if (properties.color) {
if (
@@ -357,13 +422,6 @@ export async function loadLandingPoints(scene, earthObj, options = {}) {
clearLandingPoints(earthObj);
if (!landingPointGeometry) {
landingPointGeometry = new THREE.SphereGeometry(
CABLE_CONFIG.landingPoint.radius,
CABLE_CONFIG.landingPoint.widthSegments,
CABLE_CONFIG.landingPoint.heightSegments,
);
}
let validCount = 0;
for (const feature of data.features) {
@@ -396,29 +454,35 @@ export async function loadLandingPoints(scene, earthObj, options = {}) {
continue;
}
const sphere = new THREE.Mesh(
landingPointGeometry,
new THREE.MeshStandardMaterial({
const marker = new THREE.Sprite(
new THREE.SpriteMaterial({
map: getLandingPointTexture(),
color: CABLE_CONFIG.landingPoint.color,
emissive: CABLE_CONFIG.landingPoint.emissive,
emissiveIntensity: CABLE_CONFIG.landingPoint.emissiveIntensity,
transparent: true,
opacity: CABLE_CONFIG.landingPoint.opacity,
depthTest: true,
depthWrite: false,
}),
);
sphere.position.copy(position);
sphere.userData = {
marker.material.userData.sharedMap = true;
marker.renderOrder = CABLE_CONFIG.landingPoint.renderOrder;
marker.center.set(
CABLE_CONFIG.landingPoint.anchorX,
CABLE_CONFIG.landingPoint.anchorY,
);
marker.position.copy(position);
marker.userData = {
type: "landingPoint",
name: properties.name || "未知登陆站",
cableNames: properties.cable_names || [],
country: properties.country || "未知国家",
status: properties.status || "Unknown",
baseScale: CABLE_CONFIG.landingPoint.baseScale,
sharedGeometry: true,
};
setLandingPointScale(marker, CABLE_CONFIG.landingPoint.baseScale);
earthObj.add(sphere);
landingPoints.push(sphere);
earthObj.add(marker);
landingPoints.push(marker);
validCount++;
}
@@ -533,6 +597,18 @@ export function getAllLandingPoints() {
return landingPoints;
}
// Hide pins within ~3° of the limb (normalised dot < 0.05) to prevent the
// "half-clipped-into-globe" look caused by depthTest clipping the billboard
// against closer earth-surface geometry near the horizon.
const _FACING_DOT_MIN_SQ = 0.05 * 0.05;
function isFacingCamera(lp, camera) {
lp.getWorldPosition(_lpWorldPos);
const d = _lpWorldPos.dot(camera.position);
if (d <= 0) return false;
return d * d > _FACING_DOT_MIN_SQ * _lpWorldPos.lengthSq() * camera.position.lengthSq();
}
export function applyLandingPointVisualState(lockedCableName, dimAll = false, camera = null) {
const pulse =
(Math.sin(Date.now() * CABLE_CONFIG.landingPointVisual.pulseSpeed) + 1) * 0.5;
@@ -544,58 +620,65 @@ export function applyLandingPointVisualState(lockedCableName, dimAll = false, ca
: [];
landingPoints.forEach((lp) => {
lp.visible = cablesVisible && (camera ? isFacingCamera(lp, camera) : true);
const isRelated =
!dimAll &&
Array.isArray(lp.userData.cableNames) &&
lp.userData.cableNames.some((name) => relatedNames.includes(name));
if (isRelated) {
lp.material.color.setHex(0xffd27a);
lp.material.emissive.setHex(0x7a4a00);
lp.material.emissiveIntensity =
CABLE_CONFIG.landingPointVisual.related.emissiveIntensityBase +
0.2 +
pulse * (CABLE_CONFIG.landingPointVisual.related.emissiveIntensityPulse + 0.2);
lp.material.opacity =
Math.max(
setLandingPointMaterialState(lp, {
color: 0xffd27a,
emissive: 0x7a4a00,
emissiveIntensity:
CABLE_CONFIG.landingPointVisual.related.emissiveIntensityBase +
0.2 +
pulse * (CABLE_CONFIG.landingPointVisual.related.emissiveIntensityPulse + 0.2),
opacity: Math.max(
0.92,
CABLE_CONFIG.landingPointVisual.related.opacityBase +
pulse * CABLE_CONFIG.landingPointVisual.related.opacityPulse,
);
),
});
const distanceScale = getLandingPointDistanceScale(lp, camera);
const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale;
lp.scale.setScalar(
setLandingPointScale(
lp,
(CABLE_CONFIG.landingPointVisual.related.scaleBase +
pulse * CABLE_CONFIG.landingPointVisual.related.scalePulse) *
baseScale *
distanceScale,
distanceScale,
);
} else {
const dimColor = CABLE_CONFIG.landingPointVisual.dimmed.colorRGB;
const r = dimColor.r * brightness;
const g = dimColor.g * brightness;
const b = dimColor.b * brightness;
lp.material.color.setRGB(r / 255, g / 255, b / 255);
lp.material.emissive.setHex(CABLE_CONFIG.landingPointVisual.dimmed.emissive);
lp.material.emissiveIntensity =
CABLE_CONFIG.landingPointVisual.dimmed.emissiveIntensity;
lp.material.opacity = CABLE_CONFIG.landingPointVisual.dimmed.opacity;
setLandingPointMaterialState(lp, {
color: new THREE.Color(r / 255, g / 255, b / 255),
emissive: CABLE_CONFIG.landingPointVisual.dimmed.emissive,
emissiveIntensity: CABLE_CONFIG.landingPointVisual.dimmed.emissiveIntensity,
opacity: CABLE_CONFIG.landingPointVisual.dimmed.opacity,
});
const distanceScale = getLandingPointDistanceScale(lp, camera);
const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale;
lp.scale.setScalar(baseScale * distanceScale);
setLandingPointScale(lp, baseScale * distanceScale);
}
});
}
export function resetLandingPointVisualState(camera = null) {
landingPoints.forEach((lp) => {
lp.material.color.setHex(CABLE_CONFIG.landingPoint.color);
lp.material.emissive.setHex(CABLE_CONFIG.landingPoint.emissive);
lp.material.emissiveIntensity = CABLE_CONFIG.landingPoint.emissiveIntensity;
lp.material.opacity = CABLE_CONFIG.landingPoint.opacity;
lp.visible = cablesVisible && (camera ? isFacingCamera(lp, camera) : true);
setLandingPointMaterialState(lp, {
color: CABLE_CONFIG.landingPoint.color,
emissive: CABLE_CONFIG.landingPoint.emissive,
emissiveIntensity: CABLE_CONFIG.landingPoint.emissiveIntensity,
opacity: CABLE_CONFIG.landingPoint.opacity,
});
const distanceScale = getLandingPointDistanceScale(lp, camera);
const baseScale = lp.userData?.baseScale || CABLE_CONFIG.landingPoint.baseScale;
lp.scale.setScalar(baseScale * distanceScale);
setLandingPointScale(lp, baseScale * distanceScale);
});
}

View File

@@ -201,6 +201,7 @@ export const PATHS = {
bgpApi: '/api/v1/visualization/geo/bgp-anomalies',
bgpIncidentsApi: '/api/v1/visualization/geo/bgp-incidents',
bgpCollectorsApi: '/api/v1/visualization/geo/bgp-collectors',
earthSummaryApi: '/api/v1/visualization/geo/summary',
earthClientLogsApi: '/api/v1/system/logs/earth-client',
};
@@ -259,15 +260,17 @@ export const CABLE_CONFIG = {
renderOrder: 1,
},
landingPoint: {
altitudeOffset: 0.1,
radius: 0.4,
widthSegments: 16,
heightSegments: 16,
baseScale: 2.5,
altitudeOffset: 0.48,
textureSize: 256,
iconAspectRatio: 0.82,
anchorX: 0.52,
anchorY: 0.276,
baseScale: 12,
color: 0xffaa00,
emissive: 0x442200,
emissiveIntensity: 0.5,
opacity: 1.0,
renderOrder: 4.5,
},
landingPointSizeStabilization: {
enabled: true,
@@ -277,7 +280,7 @@ export const CABLE_CONFIG = {
},
landingPointVisual: {
pulseSpeed: 0.003,
dimBrightness: 0.3,
dimBrightness: 0.62,
related: {
emissiveIntensityBase: 0.5,
emissiveIntensityPulse: 0.5,
@@ -287,10 +290,10 @@ export const CABLE_CONFIG = {
scalePulse: 0.3,
},
dimmed: {
colorRGB: { r: 255, g: 170, b: 0 },
emissive: 0x000000,
emissiveIntensity: 0,
opacity: 0.3,
colorRGB: { r: 180, g: 116, b: 28 },
emissive: 0x3a2200,
emissiveIntensity: 0.18,
opacity: 0.78,
},
},
};

View File

@@ -915,7 +915,10 @@ function syncEarthSettingsStateFromRuntime() {
const scope = getSettingsViewportScope();
nextSettings.shared = getCurrentSharedSettingsSnapshot();
nextSettings.views[scope].panelVisibility = getCurrentPanelVisibilitySnapshot();
// panelVisibility is maintained in earthSettingsState via setHudPanelVisibility.
// Do not re-snapshot from DOM here: transient hides (e.g. closeTransientMobileOverlays)
// change the DOM without going through setHudPanelVisibility and would corrupt the
// user's persisted preference.
earthSettingsState = nextSettings;
return nextSettings;
}

View File

@@ -260,6 +260,7 @@ let calloutConnector = null;
let cruiseBGPAdapter = null;
let cruiseNewsAdapter = null;
let cruiseSequencer = null;
let earthStatsSummary = null;
let activeDragPointerId = null;
let activeTouchPoints = new Map();
let pinchGesture = null;
@@ -1246,6 +1247,61 @@ function getBGPStatusText(bgpResult) {
return "当前无活跃事件";
}
function toCount(value) {
const count = Number(value);
return Number.isFinite(count) ? count : 0;
}
function formatBGPStatusFromSummary(summary) {
if (!summary) return "-";
if (summary.bgpIncidentCount > 0) {
return `${summary.bgpIncidentCount} 起活跃事件`;
}
if (summary.bgpAnomalyCount > 0) {
return `${summary.bgpAnomalyCount} 条活跃异常`;
}
return "当前无活跃事件";
}
function applyEarthStatsSummary(summary) {
if (!summary) return;
updateEarthStats({
cableCount: `${summary.cableCount}`,
landingPointCount: `${summary.landingPointCount}`,
satelliteCount: `${summary.satelliteCount}`,
computeCenterCount: `${summary.computeCenterCount}`,
bgpAnomalyCount: `${summary.bgpEventCount}`,
bgpCollectorCount: `${summary.bgpCollectorCount}`,
bgpStatusSummary: formatBGPStatusFromSummary(summary),
terrainOn: getShowTerrain(),
textureQuality: "8K 卫星图",
});
}
async function loadEarthStatsSummary() {
try {
const response = await fetch(PATHS.earthSummaryApi);
if (!response.ok) {
throw new Error(`Earth summary HTTP ${response.status}`);
}
const payload = await response.json();
const stats = payload?.stats || {};
earthStatsSummary = {
cableCount: toCount(stats.cable_count),
landingPointCount: toCount(stats.landing_point_count),
satelliteCount: toCount(stats.satellite_count),
computeCenterCount: toCount(stats.compute_center_count),
bgpEventCount: toCount(stats.bgp_event_count),
bgpIncidentCount: toCount(stats.bgp_incident_count),
bgpAnomalyCount: toCount(stats.bgp_anomaly_count),
bgpCollectorCount: toCount(stats.bgp_collector_count),
};
applyEarthStatsSummary(earthStatsSummary);
} catch (error) {
console.warn("全球态势聚合统计加载失败:", error);
}
}
function updateComputeCenterHud(computeCenterResult) {
const computeBtn = document.getElementById("toggle-compute-centers");
if (computeBtn) {
@@ -1885,7 +1941,8 @@ function updateSatelliteToggleUi(enabled, satelliteCount = getSatelliteCount())
});
}
setEarthStatValue("satellite-count", `${satelliteCount}`);
const resolvedCount = satelliteCount || earthStatsSummary?.satelliteCount || 0;
setEarthStatValue("satellite-count", `${resolvedCount}`);
}
function updateCableToggleUi(enabled) {
@@ -1898,8 +1955,11 @@ function updateCableToggleUi(enabled) {
});
}
setEarthStatValue("cable-count", `${getCableLines().length}`);
setEarthStatValue("landing-point-count", `${getLandingPoints().length}`);
const cableCount = getCableLines().length || earthStatsSummary?.cableCount || 0;
const landingPointCount =
getLandingPoints().length || earthStatsSummary?.landingPointCount || 0;
setEarthStatValue("cable-count", `${cableCount}`);
setEarthStatValue("landing-point-count", `${landingPointCount}`);
}
async function ensureCablesEnabled() {
@@ -2015,13 +2075,25 @@ function disableSatellites() {
}
function updateStatsSummary() {
const cableCount = getCableLines().length || earthStatsSummary?.cableCount || 0;
const landingPointCount =
getLandingPoints().length || earthStatsSummary?.landingPointCount || 0;
const satelliteCount = getSatelliteCount() || earthStatsSummary?.satelliteCount || 0;
const computeCenterCount =
getComputeCenterCount() || earthStatsSummary?.computeCenterCount || 0;
const bgpEventCount = getBGPCount() || earthStatsSummary?.bgpEventCount || 0;
const bgpCollectorCount =
getBGPCollectorCount() || earthStatsSummary?.bgpCollectorCount || 0;
updateEarthStats({
cableCount: getCableLines().length,
landingPointCount: getLandingPoints().length,
computeCenterCount: `${getComputeCenterCount()} `,
bgpAnomalyCount: `${getBGPCount()} `,
bgpCollectorCount: `${getBGPCollectorCount()} `,
bgpStatusSummary: getBGPStatusSummary(),
cableCount: `${cableCount}`,
landingPointCount: `${landingPointCount}`,
satelliteCount: `${satelliteCount} `,
computeCenterCount: `${computeCenterCount} `,
bgpAnomalyCount: `${bgpEventCount} `,
bgpCollectorCount: `${bgpCollectorCount}`,
bgpStatusSummary: getBGPCount()
? getBGPStatusSummary()
: formatBGPStatusFromSummary(earthStatsSummary),
terrainOn: getShowTerrain(),
textureQuality: "8K 卫星图",
});
@@ -2269,6 +2341,11 @@ async function loadData() {
await yieldFrame(18);
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
setLoadingMessage("正在读取全球态势统计...");
await loadEarthStatsSummary();
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
await yieldFrame(12);
const errors = [];
// Step 1 — Earth texture

View File

@@ -185,14 +185,22 @@ export function updateZoomDisplay(zoomLevel, distance) {
// Update earth stats
export function updateEarthStats(stats) {
setEarthStatValue("cable-count", String(stats.cableCount || 0));
setEarthStatValue("landing-point-count", String(stats.landingPointCount || 0));
setEarthStatValue("compute-center-count", String(stats.computeCenterCount || 0));
setEarthStatValue("bgp-anomaly-count", String(stats.bgpAnomalyCount || 0));
setEarthStatValue("bgp-collector-count", String(stats.bgpCollectorCount || 0));
setEarthStatValue("bgp-status-summary", stats.bgpStatusSummary || "-");
setEarthStatValue("terrain-status", stats.terrainOn ? "开启" : "关闭");
setEarthStatValue("texture-quality", stats.textureQuality || "8K 卫星图");
const has = (key) => Object.prototype.hasOwnProperty.call(stats, key);
if (has("cableCount")) setEarthStatValue("cable-count", String(stats.cableCount || 0));
if (has("landingPointCount")) {
setEarthStatValue("landing-point-count", String(stats.landingPointCount || 0));
}
if (has("satelliteCount")) setEarthStatValue("satellite-count", String(stats.satelliteCount || 0));
if (has("computeCenterCount")) {
setEarthStatValue("compute-center-count", String(stats.computeCenterCount || 0));
}
if (has("bgpAnomalyCount")) setEarthStatValue("bgp-anomaly-count", String(stats.bgpAnomalyCount || 0));
if (has("bgpCollectorCount")) {
setEarthStatValue("bgp-collector-count", String(stats.bgpCollectorCount || 0));
}
if (has("bgpStatusSummary")) setEarthStatValue("bgp-status-summary", stats.bgpStatusSummary || "-");
if (has("terrainOn")) setEarthStatValue("terrain-status", stats.terrainOn ? "开启" : "关闭");
if (has("textureQuality")) setEarthStatValue("texture-quality", stats.textureQuality || "8K 卫星图");
}
// Show/hide loading via status message