release: bump version to 0.38.0

This commit is contained in:
linkong
2026-04-23 17:57:35 +08:00
parent 195a8bf71c
commit d5f3784ffb
39 changed files with 4958 additions and 146 deletions

View File

@@ -2,6 +2,7 @@ import * as THREE from "three";
import {
CONFIG,
CRUISE_MODULES,
HUD_CONFIG,
CABLE_CONFIG,
CABLE_STATE,
@@ -147,6 +148,7 @@ import {
import {
setupControls,
getAutoRotate,
getCruiseModules,
getRotationMode,
getShowTerrain,
getStartupLoadLayers,
@@ -167,6 +169,7 @@ import {
import { CalloutConnector } from "./callout-connector.js";
import { CruiseSequencer } from "./cruise-sequencer.js";
import { createBGPCruiseAdapter } from "./bgp-cruise-adapter.js";
import { createNewsCruiseAdapter } from "./news-cruise-adapter.js";
import {
initInfoCard,
showInfoCard,
@@ -182,6 +185,10 @@ import { mountBrand } from "./brand.js";
import { initTVPanel } from "./tv.js";
import { initNewsPanel, updateNewsViewFocus } from "./news.js";
import { initSearchPanel } from "./search.js";
import {
registerEarthClientErrorHandlers,
reportEarthClientLog,
} from "./client-logs.js";
export let scene;
export let camera;
@@ -224,6 +231,7 @@ let sceneLights = null;
let cruisePollTimerId = null;
let calloutConnector = null;
let cruiseBGPAdapter = null;
let cruiseNewsAdapter = null;
let cruiseSequencer = null;
let activeDragPointerId = null;
let activeTouchPoints = new Map();
@@ -1256,6 +1264,143 @@ function ensureBGPCruiseAdapter() {
return cruiseBGPAdapter;
}
function ensureNewsCruiseAdapter() {
if (cruiseNewsAdapter) return cruiseNewsAdapter;
cruiseNewsAdapter = createNewsCruiseAdapter({
camera,
earth: () => getEarth(),
connector: ensureCalloutConnector(),
focusView: (options) => focusEarthView(camera, options),
});
return cruiseNewsAdapter;
}
function getCruiseModuleDefinitions() {
return [
{
id: CRUISE_MODULES.BGP,
ensureAdapter: ensureBGPCruiseAdapter,
isQueueSourceActive: () => getShowBGP(),
getSortedQueueItems: () =>
ensureBGPCruiseAdapter()
.getSortedMarkers()
.map((marker) => {
const markerId = marker?.userData?.id;
if (!markerId) return null;
const parsed = marker?.userData?.created_at_raw || marker?.userData?.created_at;
const timestamp = parsed ? new Date(parsed).getTime() : 0;
return {
id: `bgp:${markerId}`,
moduleId: CRUISE_MODULES.BGP,
sortTimestamp: Number.isFinite(timestamp) ? timestamp : 0,
payload: marker,
};
})
.filter(Boolean),
focusQueueItem: (item, options = {}) =>
ensureBGPCruiseAdapter().focusMarker(item?.payload, options),
presentQueueItem: (item, options = {}) =>
ensureBGPCruiseAdapter().presentMarker(item?.payload, options),
hidePresentation: (options = {}) =>
ensureBGPCruiseAdapter().hidePresentation(options),
clearCurrentHighlight: () => ensureBGPCruiseAdapter().clearCurrentHighlight(),
resetPresentation: () => ensureBGPCruiseAdapter().resetPresentation(),
repositionPresentation: (item) =>
ensureBGPCruiseAdapter().repositionConnector(item?.payload),
syncKnownEventIds: () => ensureBGPCruiseAdapter().syncKnownEventIds(),
pollForNewQueueItemIds: async () => {
const nextIds = await ensureBGPCruiseAdapter().pollForNewMarkerIds();
return nextIds.map((id) => `bgp:${id}`);
},
},
{
id: CRUISE_MODULES.NEWS,
ensureAdapter: ensureNewsCruiseAdapter,
isQueueSourceActive: () => true,
ensureItemsLoaded: () => ensureNewsCruiseAdapter().ensureItemsLoaded(),
getSortedQueueItems: () =>
ensureNewsCruiseAdapter()
.getSortedItems()
.map((item) => ({
...item,
moduleId: CRUISE_MODULES.NEWS,
payload: item,
})),
focusQueueItem: (item, options = {}) =>
ensureNewsCruiseAdapter().focusItem(item?.payload, options),
presentQueueItem: (item, options = {}) =>
ensureNewsCruiseAdapter().presentItem(item?.payload, options),
hidePresentation: (options = {}) =>
ensureNewsCruiseAdapter().hidePresentation(options),
clearCurrentHighlight: () => ensureNewsCruiseAdapter().clearCurrentHighlight(),
resetPresentation: () => ensureNewsCruiseAdapter().resetPresentation(),
repositionPresentation: (item) =>
ensureNewsCruiseAdapter().repositionConnector(item?.payload),
syncKnownEventIds: () => ensureNewsCruiseAdapter().syncKnownEventIds(),
externalEventName: "earth:news-payload-updated",
extractQueuedItemIdsFromEvent: (event) => {
const itemIds = Array.isArray(event?.detail?.itemIds) ? event.detail.itemIds : [];
if (itemIds.length === 0) return [];
const adapter = ensureNewsCruiseAdapter();
const newIds = adapter.diffNewEventIds(itemIds);
adapter.syncKnownEventIds();
return newIds;
},
},
];
}
function getCruiseModuleDefinition(moduleId) {
return getCruiseModuleDefinitions().find((module) => module.id === moduleId) || null;
}
function getEnabledCruiseModuleDefinitions() {
const enabledModuleIds = new Set(getCruiseModules());
return getCruiseModuleDefinitions().filter((module) => enabledModuleIds.has(module.id));
}
function sortCruiseQueueItems(items) {
return items.sort((left, right) => {
const timestampDiff = (right?.sortTimestamp || 0) - (left?.sortTimestamp || 0);
if (timestampDiff !== 0) return timestampDiff;
return String(left?.id || "").localeCompare(String(right?.id || ""));
});
}
function getCruiseQueueItemsSorted() {
const items = [];
getEnabledCruiseModuleDefinitions().forEach((module) => {
if (!module.isQueueSourceActive?.()) return;
items.push(...(module.getSortedQueueItems?.() || []));
});
return sortCruiseQueueItems(items);
}
function getCruiseModuleForItem(item) {
return getCruiseModuleDefinition(item?.moduleId);
}
function getCurrentCruiseBGPMarker() {
const item = cruiseSequencer?.getCurrentItem() ?? null;
return item?.moduleId === CRUISE_MODULES.BGP ? item.payload || null : null;
}
async function syncCruiseModuleKnownEventIds() {
const modules = getEnabledCruiseModuleDefinitions();
await Promise.all(
modules.map(async (module) => {
try {
await module.ensureItemsLoaded?.();
module.syncKnownEventIds?.();
} catch (error) {
console.warn(`同步巡航模块失败: ${module.id}`, error);
}
}),
);
}
function isCruisePresentationPinned() {
return cruiseSequencer?.isPresentationPinned() === true;
}
@@ -1265,22 +1410,23 @@ function setCruisePresentationVisible(visible) {
cruiseSequencer.setPresentationVisible(visible);
}
if (!visible) {
ensureBGPCruiseAdapter().resetPresentation();
getCruiseModuleDefinitions().forEach((module) => {
module.resetPresentation?.();
});
}
}
function clearCruiseMarkerHighlight() {
ensureBGPCruiseAdapter().clearCurrentHighlight();
}
function getCruiseMarkersSorted() {
return ensureBGPCruiseAdapter().getSortedMarkers();
function clearCruiseHighlights() {
getCruiseModuleDefinitions().forEach((module) => {
module.clearCurrentHighlight?.();
});
}
function repositionCruiseConnector() {
if (!isCruisePresentationPinned()) return;
const marker = cruiseSequencer?.getCurrentItem() ?? null;
ensureBGPCruiseAdapter().repositionConnector(marker);
const item = cruiseSequencer?.getCurrentItem() ?? null;
const module = getCruiseModuleForItem(item);
module?.repositionPresentation?.(item);
}
function isCruiseModeActive() {
@@ -1292,12 +1438,12 @@ function ensureCruiseSequencer() {
cruiseSequencer = new CruiseSequencer({
isActive: () => isCruiseModeActive() && getAutoRotate(),
getItems: () => getCruiseMarkersSorted(),
getItemId: (marker) => marker?.userData?.id || null,
getItems: () => getCruiseQueueItemsSorted(),
getItemId: (item) => item?.id || null,
dwellMs: CRUISE_CONFIG.dwellMs,
transitionGapMs: CRUISE_TRANSITION_GAP_MS,
clearCurrent: () => {
clearCruiseMarkerHighlight();
clearCruiseHighlights();
clearLockedObject();
hideInfoCard();
setCruisePresentationVisible(false);
@@ -1308,11 +1454,11 @@ function ensureCruiseSequencer() {
hideInfoCard();
}
},
focusItem: async (marker, { interrupt }) =>
ensureBGPCruiseAdapter().focusMarker(marker, { interrupt }),
presentItem: async (marker, { context }) => {
focusItem: async (item, { interrupt }) =>
getCruiseModuleForItem(item)?.focusQueueItem?.(item, { interrupt }),
presentItem: async (item, { context }) => {
setCruisePresentationVisible(true);
const presented = await ensureBGPCruiseAdapter().presentMarker(marker, {
const presented = await getCruiseModuleForItem(item)?.presentQueueItem?.(item, {
context,
});
if (!presented) {
@@ -1320,8 +1466,8 @@ function ensureCruiseSequencer() {
}
return presented;
},
hideItem: async (_marker, { context }) => {
await ensureBGPCruiseAdapter().hidePresentation({ context });
hideItem: async (item, { context }) => {
await getCruiseModuleForItem(item)?.hidePresentation?.({ context });
setCruisePresentationVisible(false);
},
});
@@ -1347,10 +1493,23 @@ async function advanceCruiseEvent({ interrupt = false } = {}) {
}
async function pollCruiseEventsIfNeeded() {
if (!isCruiseModeActive() || !getAutoRotate() || !getShowBGP()) return;
if (!isCruiseModeActive() || !getAutoRotate()) return;
try {
const newIds = await ensureBGPCruiseAdapter().pollForNewMarkerIds();
const modules = getEnabledCruiseModuleDefinitions().filter(
(module) => module.isQueueSourceActive?.() && typeof module.pollForNewQueueItemIds === "function",
);
const results = await Promise.all(
modules.map(async (module) => {
try {
return await module.pollForNewQueueItemIds();
} catch (error) {
console.warn(`巡航模式轮询失败: ${module.id}`, error);
return [];
}
}),
);
const newIds = results.flat();
if (newIds.length === 0) return;
ensureCruiseSequencer().enqueue(newIds);
@@ -1358,7 +1517,7 @@ async function pollCruiseEventsIfNeeded() {
await advanceCruiseEvent({ interrupt: true });
}
} catch (error) {
console.warn("巡航模式轮询 BGP 事件失败:", error);
console.warn("巡航模式轮询失败:", error);
}
}
@@ -1390,16 +1549,63 @@ function handleRotationModeChange(event) {
}
ensureCruisePolling();
ensureBGPCruiseAdapter().syncKnownEventIds();
if (!detailActive) {
stopCruiseMode({ preserveCard: true });
return;
}
advanceCruiseEvent({ interrupt: true }).catch((error) => {
console.warn("启动巡航模式失败:", error);
syncCruiseModuleKnownEventIds()
.catch((error) => {
console.warn("同步巡航模块失败:", error);
})
.finally(() => {
advanceCruiseEvent({ interrupt: true }).catch((error) => {
console.warn("启动巡航模式失败:", error);
});
});
}
function handleCruiseModulesChange() {
interruptCruisePresentation({ resetLoop: true });
clearBGPSelection();
const syncPromise = syncCruiseModuleKnownEventIds().catch((error) => {
console.warn("刷新巡航模块失败:", error);
});
if (!isCruiseModeActive()) {
return;
}
if (!getAutoRotate()) {
stopCruiseMode({ preserveCard: true });
return;
}
syncPromise.finally(() => {
advanceCruiseEvent({ interrupt: true }).catch((error) => {
console.warn("切换巡航模块后推进失败:", error);
});
});
}
function handleCruiseModuleItemsUpdated(moduleId, event) {
const module = getCruiseModuleDefinition(moduleId);
if (!module) return;
const newIds = module.extractQueuedItemIdsFromEvent?.(event) || [];
if (newIds.length === 0) return;
if (!isCruiseModeActive() || !getAutoRotate() || !getCruiseModules().includes(moduleId)) {
return;
}
ensureCruiseSequencer().enqueue(newIds);
if (!ensureCruiseSequencer().isBusy()) {
advanceCruiseEvent({ interrupt: true }).catch((error) => {
console.warn(`推进巡航模块失败: ${moduleId}`, error);
});
}
}
function clearSelectionAndInfo() {
@@ -1726,13 +1932,7 @@ function getCurrentViewCenterCoords() {
return vector3ToLatLon(scratchViewCenterWorld);
}
window.addEventListener("error", (event) => {
console.error("全局错误:", event.error);
});
window.addEventListener("unhandledrejection", (event) => {
console.error("未处理的 Promise 错误:", event.reason);
});
registerEarthClientErrorHandlers();
export function init() {
if (initialized && !destroyed) return;
@@ -1984,6 +2184,13 @@ async function loadData() {
getSatelliteHydrationToken: () => satelliteHydrationToken,
reportError: (label, reason) => {
errors.push({ label, reason });
void reportEarthClientLog({
level: "error",
category: "startup-load",
module: "layer-startup",
message: `${label}加载失败: ${reason?.message || String(reason)}`,
detail: reason,
});
},
});
@@ -2087,6 +2294,13 @@ export async function setCablesEnabled(
clearCableData(getEarth());
updateCableToggleUi(false);
const message = `线缆加载失败: ${error?.message || String(error)}`;
void reportEarthClientLog({
level: "error",
category: "layer-toggle",
module: "cables",
message,
detail: error,
});
if (!suppressLoadingUi) {
showError(message);
}
@@ -2133,6 +2347,13 @@ export async function setSatellitesEnabled(
resetSatelliteState();
updateSatelliteToggleUi(false, 0);
const message = `卫星加载失败: ${error?.message || String(error)}`;
void reportEarthClientLog({
level: "error",
category: "layer-toggle",
module: "satellites",
message,
detail: error,
});
if (!suppressLoadingUi) {
showError(message);
}
@@ -2156,13 +2377,21 @@ function setupEventListeners() {
const handleClick = (event) => onClick(event);
const handlePageHide = () => destroy();
const handleRotationMode = (event) => handleRotationModeChange(event);
const handleCruiseModules = () => handleCruiseModulesChange();
const handleInfoCardDrag = () => repositionCruiseConnector();
bindListener(window, "resize", handleResize);
bindListener(window, "pagehide", handlePageHide);
bindListener(window, "beforeunload", handlePageHide);
bindListener(window, "earth:rotation-mode-change", handleRotationMode);
bindListener(window, "earth:cruise-modules-change", handleCruiseModules);
bindListener(window, "earth:info-card-drag", handleInfoCardDrag);
getCruiseModuleDefinitions().forEach((module) => {
if (!module.externalEventName) return;
bindListener(window, module.externalEventName, (event) => {
handleCruiseModuleItemsUpdated(module.id, event);
});
});
bindListener(renderer.domElement, "pointerdown", handlePointerDown);
bindListener(window, "pointermove", handlePointerMove);
bindListener(window, "pointerup", handlePointerUp);
@@ -2860,7 +3089,7 @@ function animate() {
applyCableVisualState();
const activeCruiseMarker =
isCruiseModeActive() && isCruisePresentationPinned()
? cruiseSequencer?.getCurrentItem() ?? null
? getCurrentCruiseBGPMarker()
: null;
updateBGPVisualState(lockedObjectType, lockedObject, camera, activeCruiseMarker);
updateComputeCenterVisualState(lockedObjectType, lockedObject, camera);