release: bump version to 0.67.0
This commit is contained in:
@@ -2822,6 +2822,7 @@ label.is-disabled.earth-mobile-settings-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.earth-settings-segmented {
|
||||
@@ -2908,10 +2909,17 @@ label.is-disabled.earth-mobile-settings-card {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 calc(30px * var(--hud-scale));
|
||||
width: calc(30px * var(--hud-scale));
|
||||
height: calc(30px * var(--hud-scale));
|
||||
min-width: calc(30px * var(--hud-scale));
|
||||
min-height: calc(30px * var(--hud-scale));
|
||||
max-width: calc(30px * var(--hud-scale));
|
||||
max-height: calc(30px * var(--hud-scale));
|
||||
aspect-ratio: 1 / 1;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(122, 180, 255, 0.24);
|
||||
border-radius: 999px;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(circle at 35% 22%, rgba(255, 255, 255, 0.2), transparent 58%),
|
||||
linear-gradient(180deg, rgba(121, 159, 207, 0.18), rgba(72, 101, 139, 0.24));
|
||||
@@ -2926,12 +2934,16 @@ label.is-disabled.earth-mobile-settings-card {
|
||||
}
|
||||
|
||||
.earth-settings-reload-action--text {
|
||||
flex-basis: auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
min-width: calc(46px * var(--hud-scale));
|
||||
padding: 0 calc(12px * var(--hud-scale));
|
||||
aspect-ratio: auto;
|
||||
font: inherit;
|
||||
font-size: calc(0.7rem * var(--hud-scale));
|
||||
font-weight: 600;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.earth-settings-reload-action[hidden] {
|
||||
@@ -3076,6 +3088,7 @@ label.is-disabled.earth-mobile-settings-card {
|
||||
|
||||
.earth-settings-slider {
|
||||
flex: 1 1 auto;
|
||||
min-width: calc(120px * var(--hud-scale));
|
||||
width: 100%;
|
||||
height: calc(4px * var(--hud-scale));
|
||||
appearance: none;
|
||||
|
||||
@@ -214,7 +214,6 @@ const computeCenterIconLayer = createInteractableLayer({
|
||||
},
|
||||
icon: {
|
||||
coordinates: "canvas",
|
||||
colorable: false,
|
||||
fitSize: COMPUTE_CENTER_ICON_FIT_SIZE,
|
||||
glowBlur: 16,
|
||||
getSource({ marker, item }) {
|
||||
|
||||
@@ -64,6 +64,8 @@ export const SURFACE_HOVER_INFO_MODES = {
|
||||
export const DEFAULT_SURFACE_HOVER_INFO_MODE =
|
||||
SURFACE_HOVER_INFO_MODES.FULL;
|
||||
|
||||
export const EARTH_SURFACE_TEXTURE_ALTITUDE_OFFSET = 0.48;
|
||||
|
||||
export const CRUISE_CONFIG = {
|
||||
dwellMs: 7_000,
|
||||
focusDurationMs: 1_400,
|
||||
@@ -233,8 +235,12 @@ export const COUNTRY_BOUNDARY_CONFIG = {
|
||||
tilePrefetchRing: 1,
|
||||
tileDebounceMs: 180,
|
||||
tileCacheLimit: 150,
|
||||
lineAltitudeOffset: 0.115,
|
||||
hoverAltitudeOffset: 0.115,
|
||||
// Keep boundary/coastline strokes on the same shell as the high-res earth
|
||||
// texture overlay. A lower or higher radius creates visible parallax while
|
||||
// the globe rotates.
|
||||
lineAltitudeOffset: EARTH_SURFACE_TEXTURE_ALTITUDE_OFFSET,
|
||||
hoverAltitudeOffset: EARTH_SURFACE_TEXTURE_ALTITUDE_OFFSET,
|
||||
claimLineAltitudeOffset: 0,
|
||||
hoverMissStickyMs: 160,
|
||||
lineColor: 0x7fc7ff,
|
||||
lineOpacity: 0.58,
|
||||
@@ -540,7 +546,7 @@ export const EARTH_MATERIAL_CONFIG = {
|
||||
shininess: 12,
|
||||
emissive: 0x010609,
|
||||
opacity: 1,
|
||||
textureOverlayAltitudeOffset: 0.48,
|
||||
textureOverlayAltitudeOffset: EARTH_SURFACE_TEXTURE_ALTITUDE_OFFSET,
|
||||
textureOverlayOpacity: 0.88,
|
||||
textureOverlayRenderOrder: 0.96,
|
||||
textureOverlaySpecular: 0x05080d,
|
||||
|
||||
181
frontend/public/earth/js/controls.js
vendored
181
frontend/public/earth/js/controls.js
vendored
@@ -185,6 +185,11 @@ const KEYBOARD_ROTATION_STOP_SPEED = 0.012;
|
||||
const KEYBOARD_ZOOM_STEP = 0.1;
|
||||
const WHEEL_ZOOM_STEP = 0.1;
|
||||
const WHEEL_ZOOM_DURATION_MS = 180;
|
||||
const WHEEL_TRACKPAD_PIXEL_THRESHOLD = 48;
|
||||
const WHEEL_TRACKPAD_DEADZONE = 0.35;
|
||||
const WHEEL_TRACKPAD_RESIDUAL_WINDOW_MS = 140;
|
||||
const WHEEL_TRACKPAD_RESIDUAL_RATIO = 0.65;
|
||||
const WHEEL_TRACKPAD_SENSITIVITY = 0.0024;
|
||||
const TARGET_SWITCH_ZOOM_IN_PHASE = 0.28;
|
||||
const TARGET_SWITCH_ROTATE_PHASE = 0.5;
|
||||
let settingsModalTimer = null;
|
||||
@@ -655,7 +660,7 @@ function stopKeyboardRotationControl({ actionId = null, restoreAutoRotate = fals
|
||||
}
|
||||
|
||||
function applyKeyboardZoom(direction) {
|
||||
setZoomLevel(zoomLevel + direction * KEYBOARD_ZOOM_STEP, activeCamera);
|
||||
setZoomLevel(getZoomLevelFromCamera(activeCamera) + direction * KEYBOARD_ZOOM_STEP, activeCamera);
|
||||
showZoomStatusCapsule({ force: true });
|
||||
}
|
||||
|
||||
@@ -1318,6 +1323,19 @@ function clampEarthZoomLevel(nextZoom) {
|
||||
return Math.min(CONFIG.maxZoom, Math.max(CONFIG.minZoom, parsedZoom));
|
||||
}
|
||||
|
||||
function getZoomLevelFromCamera(camera = activeCamera) {
|
||||
const cameraZ = Number(camera?.position?.z);
|
||||
if (!Number.isFinite(cameraZ) || cameraZ <= 0) {
|
||||
return clampEarthZoomLevel(zoomLevel);
|
||||
}
|
||||
return clampEarthZoomLevel(CONFIG.defaultCameraZ / cameraZ);
|
||||
}
|
||||
|
||||
function syncZoomLevelFromCamera(camera = activeCamera) {
|
||||
zoomLevel = getZoomLevelFromCamera(camera);
|
||||
return zoomLevel;
|
||||
}
|
||||
|
||||
function formatZoomPercent(zoom) {
|
||||
return `${Math.round(zoom * 100)}%`;
|
||||
}
|
||||
@@ -2188,8 +2206,7 @@ function setDefaultEarthZoom(nextZoom, { persist = true, applyToCurrentView = tr
|
||||
syncDefaultEarthZoomUi(defaultEarthZoom);
|
||||
|
||||
if (applyToCurrentView && activeCamera) {
|
||||
zoomLevel = defaultEarthZoom;
|
||||
applyZoom(activeCamera);
|
||||
setZoomLevel(defaultEarthZoom, activeCamera);
|
||||
}
|
||||
|
||||
if (persist) {
|
||||
@@ -3110,12 +3127,7 @@ export function applyImmediateView(targetEarthObj, camera, options = {}) {
|
||||
targetEarthObj.rotation.x = nextRotation.x;
|
||||
targetEarthObj.rotation.y = nextRotation.y;
|
||||
targetEarthObj.rotation.z = nextRotation.z;
|
||||
zoomLevel = zoom;
|
||||
|
||||
if (camera) {
|
||||
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
||||
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
|
||||
}
|
||||
setZoomLevel(zoom, camera);
|
||||
}
|
||||
|
||||
export function setZoomLevel(nextZoom, camera = activeCamera) {
|
||||
@@ -3127,13 +3139,16 @@ export function setZoomLevel(nextZoom, camera = activeCamera) {
|
||||
return zoomLevel;
|
||||
}
|
||||
|
||||
export function showZoomStatusCapsule({ force = false } = {}) {
|
||||
export function showZoomStatusCapsule({ force = false, zoom = null } = {}) {
|
||||
const now = Date.now();
|
||||
if (!force && now - lastZoomStatusUpdateTime < ZOOM_STATUS_UPDATE_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
lastZoomStatusUpdateTime = now;
|
||||
showGestureStatusMessage(`缩放 ${Math.round(zoomLevel * 100)}%`, "info");
|
||||
const currentZoom = Number.isFinite(Number(zoom))
|
||||
? clampEarthZoomLevel(zoom)
|
||||
: syncZoomLevelFromCamera(activeCamera);
|
||||
showGestureStatusMessage(`缩放 ${Math.round(currentZoom * 100)}%`, "info");
|
||||
}
|
||||
|
||||
function cancelSettingsSheetAnimation() {
|
||||
@@ -4692,27 +4707,25 @@ function setupZoomControls(camera) {
|
||||
const MAX_PERCENT = CONFIG.maxZoom * 100;
|
||||
|
||||
function doZoomStep(direction) {
|
||||
let currentPercent = Math.round(zoomLevel * 100);
|
||||
let currentPercent = Math.round(getZoomLevelFromCamera(camera) * 100);
|
||||
let newPercent =
|
||||
direction > 0 ? currentPercent + CLICK_STEP : currentPercent - CLICK_STEP;
|
||||
|
||||
if (newPercent > MAX_PERCENT) newPercent = MAX_PERCENT;
|
||||
if (newPercent < MIN_PERCENT) newPercent = MIN_PERCENT;
|
||||
|
||||
zoomLevel = newPercent / 100;
|
||||
applyZoom(camera);
|
||||
setZoomLevel(newPercent / 100, camera);
|
||||
showZoomStatusCapsule({ force: true });
|
||||
}
|
||||
|
||||
function doContinuousZoom(direction) {
|
||||
let currentPercent = Math.round(zoomLevel * 100);
|
||||
let currentPercent = Math.round(getZoomLevelFromCamera(camera) * 100);
|
||||
let newPercent = direction > 0 ? currentPercent + 1 : currentPercent - 1;
|
||||
|
||||
if (newPercent > MAX_PERCENT) newPercent = MAX_PERCENT;
|
||||
if (newPercent < MIN_PERCENT) newPercent = MIN_PERCENT;
|
||||
|
||||
zoomLevel = newPercent / 100;
|
||||
applyZoom(camera);
|
||||
setZoomLevel(newPercent / 100, camera);
|
||||
showZoomStatusCapsule();
|
||||
}
|
||||
|
||||
@@ -4775,10 +4788,8 @@ function setupZoomControls(camera) {
|
||||
bindListener(zoomOut, "touchend", () => handleMouseUp(-1));
|
||||
|
||||
bindListener(zoomValue, "click", () => {
|
||||
const startZoomVal = zoomLevel;
|
||||
const startZoomVal = getZoomLevelFromCamera(camera);
|
||||
const targetZoom = getDefaultEarthZoomLevel();
|
||||
const startDistance = CONFIG.defaultCameraZ / startZoomVal;
|
||||
const targetDistance = CONFIG.defaultCameraZ / targetZoom;
|
||||
|
||||
animateValue(
|
||||
0,
|
||||
@@ -4786,14 +4797,11 @@ function setupZoomControls(camera) {
|
||||
600,
|
||||
(progress) => {
|
||||
const ease = 1 - Math.pow(1 - progress, 3);
|
||||
zoomLevel = startZoomVal + (targetZoom - startZoomVal) * ease;
|
||||
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
||||
const distance =
|
||||
startDistance + (targetDistance - startDistance) * ease;
|
||||
updateZoomDisplay(zoomLevel, distance.toFixed(0));
|
||||
const nextZoom = startZoomVal + (targetZoom - startZoomVal) * ease;
|
||||
setZoomLevel(nextZoom, camera);
|
||||
},
|
||||
() => {
|
||||
zoomLevel = targetZoom;
|
||||
setZoomLevel(targetZoom, camera);
|
||||
showStatusMessage(getZoomResetStatusMessage(targetZoom), "info");
|
||||
},
|
||||
);
|
||||
@@ -4802,9 +4810,51 @@ function setupZoomControls(camera) {
|
||||
|
||||
function setupWheelZoom(camera, renderer) {
|
||||
let wheelZoomFrameId = null;
|
||||
let wheelZoomTarget = zoomLevel;
|
||||
let wheelZoomStart = zoomLevel;
|
||||
let wheelZoomTarget = getZoomLevelFromCamera(camera);
|
||||
let wheelZoomStart = wheelZoomTarget;
|
||||
let wheelZoomStartAt = 0;
|
||||
let lastTrackpadDirection = 0;
|
||||
let suppressedTrackpadDirection = 0;
|
||||
let suppressedTrackpadUntil = 0;
|
||||
let suppressedTrackpadMagnitude = 0;
|
||||
|
||||
function getWheelPixelDelta(event) {
|
||||
if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
||||
return event.deltaY * 16;
|
||||
}
|
||||
if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
return event.deltaY * window.innerHeight;
|
||||
}
|
||||
return event.deltaY;
|
||||
}
|
||||
|
||||
function isTrackpadWheel(event, pixelDelta) {
|
||||
return event.deltaMode === WheelEvent.DOM_DELTA_PIXEL &&
|
||||
Math.abs(pixelDelta) < WHEEL_TRACKPAD_PIXEL_THRESHOLD;
|
||||
}
|
||||
|
||||
function shouldSuppressTrackpadResidual(pixelDelta) {
|
||||
const direction = Math.sign(pixelDelta);
|
||||
const magnitude = Math.abs(pixelDelta);
|
||||
const now = performance.now();
|
||||
return direction !== 0 &&
|
||||
direction === suppressedTrackpadDirection &&
|
||||
now < suppressedTrackpadUntil &&
|
||||
magnitude < suppressedTrackpadMagnitude * WHEEL_TRACKPAD_RESIDUAL_RATIO;
|
||||
}
|
||||
|
||||
function recordTrackpadWheel(pixelDelta) {
|
||||
const direction = Math.sign(pixelDelta);
|
||||
const magnitude = Math.abs(pixelDelta);
|
||||
if (direction === 0) return;
|
||||
const now = performance.now();
|
||||
if (lastTrackpadDirection !== 0 && direction !== lastTrackpadDirection) {
|
||||
suppressedTrackpadDirection = lastTrackpadDirection;
|
||||
suppressedTrackpadUntil = now + WHEEL_TRACKPAD_RESIDUAL_WINDOW_MS;
|
||||
suppressedTrackpadMagnitude = magnitude;
|
||||
}
|
||||
lastTrackpadDirection = direction;
|
||||
}
|
||||
|
||||
function stopWheelZoomAnimation() {
|
||||
if (wheelZoomFrameId !== null) {
|
||||
@@ -4824,28 +4874,55 @@ function setupWheelZoom(camera, renderer) {
|
||||
1,
|
||||
);
|
||||
const ease = 1 - Math.pow(1 - progress, 3);
|
||||
zoomLevel = wheelZoomStart + (wheelZoomTarget - wheelZoomStart) * ease;
|
||||
applyZoom(camera);
|
||||
setZoomLevel(
|
||||
wheelZoomStart + (wheelZoomTarget - wheelZoomStart) * ease,
|
||||
camera,
|
||||
);
|
||||
|
||||
if (progress < 1) {
|
||||
wheelZoomFrameId = window.requestAnimationFrame(animateWheelZoom);
|
||||
return;
|
||||
}
|
||||
|
||||
zoomLevel = wheelZoomTarget;
|
||||
applyZoom(camera);
|
||||
setZoomLevel(wheelZoomTarget, camera);
|
||||
wheelZoomFrameId = null;
|
||||
wheelZoomStartAt = 0;
|
||||
}
|
||||
|
||||
function startWheelZoomAnimation() {
|
||||
wheelZoomStart = zoomLevel;
|
||||
wheelZoomStart = getZoomLevelFromCamera(camera);
|
||||
wheelZoomStartAt = 0;
|
||||
if (wheelZoomFrameId === null) {
|
||||
wheelZoomFrameId = window.requestAnimationFrame(animateWheelZoom);
|
||||
}
|
||||
}
|
||||
|
||||
function applyMouseWheelZoom(direction) {
|
||||
suppressedTrackpadDirection = 0;
|
||||
suppressedTrackpadUntil = 0;
|
||||
const baseZoom = wheelZoomFrameId === null
|
||||
? getZoomLevelFromCamera(camera)
|
||||
: wheelZoomTarget;
|
||||
wheelZoomTarget = clampEarthZoomLevel(
|
||||
baseZoom + direction * WHEEL_ZOOM_STEP,
|
||||
);
|
||||
stopWheelZoomAnimation();
|
||||
startWheelZoomAnimation();
|
||||
showZoomStatusCapsule({ force: true, zoom: wheelZoomTarget });
|
||||
}
|
||||
|
||||
function applyTrackpadWheelZoom(pixelDelta) {
|
||||
if (Math.abs(pixelDelta) < WHEEL_TRACKPAD_DEADZONE) return;
|
||||
if (shouldSuppressTrackpadResidual(pixelDelta)) return;
|
||||
stopWheelZoomAnimation();
|
||||
const currentZoom = getZoomLevelFromCamera(camera);
|
||||
const nextZoom = currentZoom * Math.exp(-pixelDelta * WHEEL_TRACKPAD_SENSITIVITY);
|
||||
wheelZoomTarget = setZoomLevel(nextZoom, camera);
|
||||
wheelZoomStart = wheelZoomTarget;
|
||||
recordTrackpadWheel(pixelDelta);
|
||||
showZoomStatusCapsule({ force: true, zoom: wheelZoomTarget });
|
||||
}
|
||||
|
||||
cleanupFns.push(stopWheelZoomAnimation);
|
||||
|
||||
bindListener(
|
||||
@@ -4853,25 +4930,17 @@ function setupWheelZoom(camera, renderer) {
|
||||
"wheel",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const direction = e.deltaY < 0 ? 1 : -1;
|
||||
const baseZoom =
|
||||
wheelZoomFrameId === null ? zoomLevel : wheelZoomTarget;
|
||||
wheelZoomTarget = clampEarthZoomLevel(
|
||||
baseZoom + direction * WHEEL_ZOOM_STEP,
|
||||
);
|
||||
startWheelZoomAnimation();
|
||||
showZoomStatusCapsule({ force: true });
|
||||
const pixelDelta = getWheelPixelDelta(e);
|
||||
if (isTrackpadWheel(e, pixelDelta)) {
|
||||
applyTrackpadWheelZoom(pixelDelta);
|
||||
return;
|
||||
}
|
||||
applyMouseWheelZoom(pixelDelta < 0 ? 1 : -1);
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
}
|
||||
|
||||
function applyZoom(camera) {
|
||||
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
||||
const distance = camera.position.z.toFixed(0);
|
||||
updateZoomDisplay(zoomLevel, distance);
|
||||
}
|
||||
|
||||
function animateValue(start, end, duration, onUpdate, onComplete) {
|
||||
const animationToken = ++focusViewAnimationToken;
|
||||
const startTime = performance.now();
|
||||
@@ -5800,7 +5869,7 @@ export function focusEarthView(camera, options = {}) {
|
||||
const startRotX = earthObj.rotation.x;
|
||||
const startRotY = earthObj.rotation.y;
|
||||
const startRotZ = earthObj.rotation.z;
|
||||
const startZoom = zoomLevel;
|
||||
const startZoom = getZoomLevelFromCamera(camera);
|
||||
const defaultZoom = getDefaultEarthZoomLevel();
|
||||
const shouldRestoreZoomViaDefault =
|
||||
zoomTransitionMode === "restore-current-via-default" &&
|
||||
@@ -5829,30 +5898,26 @@ export function focusEarthView(camera, options = {}) {
|
||||
if (progress < rotateStartProgress) {
|
||||
const zoomProgress = progress / rotateStartProgress;
|
||||
const zoomEase = 1 - Math.pow(1 - zoomProgress, 3);
|
||||
zoomLevel = startZoom + (defaultZoom - startZoom) * zoomEase;
|
||||
setZoomLevel(startZoom + (defaultZoom - startZoom) * zoomEase, camera);
|
||||
} else if (progress <= rotateEndProgress) {
|
||||
zoomLevel = defaultZoom;
|
||||
setZoomLevel(defaultZoom, camera);
|
||||
} else {
|
||||
const zoomProgress = (progress - rotateEndProgress) / (1 - rotateEndProgress);
|
||||
const zoomEase = 1 - Math.pow(1 - zoomProgress, 3);
|
||||
zoomLevel = defaultZoom + (startZoom - defaultZoom) * zoomEase;
|
||||
setZoomLevel(defaultZoom + (startZoom - defaultZoom) * zoomEase, camera);
|
||||
}
|
||||
} else {
|
||||
earthObj.rotation.x = startRotX + (nextRotation.x - startRotX) * ease;
|
||||
earthObj.rotation.y = startRotY + (nextRotation.y - startRotY) * ease;
|
||||
earthObj.rotation.z = startRotZ + (nextRotation.z - startRotZ) * ease;
|
||||
zoomLevel = startZoom + (zoom - startZoom) * ease;
|
||||
setZoomLevel(startZoom + (zoom - startZoom) * ease, camera);
|
||||
}
|
||||
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
||||
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
|
||||
},
|
||||
() => {
|
||||
zoomLevel = shouldRestoreZoomViaDefault ? startZoom : zoom;
|
||||
setZoomLevel(shouldRestoreZoomViaDefault ? startZoom : zoom, camera);
|
||||
earthObj.rotation.x = nextRotation.x;
|
||||
earthObj.rotation.y = nextRotation.y;
|
||||
earthObj.rotation.z = nextRotation.z;
|
||||
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
||||
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage("视角已重置", "info");
|
||||
}
|
||||
@@ -5863,7 +5928,7 @@ export function focusEarthView(camera, options = {}) {
|
||||
}
|
||||
|
||||
export function getZoomLevel() {
|
||||
return zoomLevel;
|
||||
return syncZoomLevelFromCamera(activeCamera);
|
||||
}
|
||||
|
||||
export function getDefaultEarthZoomLevel() {
|
||||
|
||||
@@ -210,7 +210,7 @@ function boundaryLineRadius({ claim = false } = {}) {
|
||||
return (
|
||||
CONFIG.earthRadius +
|
||||
COUNTRY_BOUNDARY_CONFIG.lineAltitudeOffset +
|
||||
(claim ? 0.018 : 0)
|
||||
(claim ? COUNTRY_BOUNDARY_CONFIG.claimLineAltitudeOffset : 0)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,6 @@ import {
|
||||
showStatusMessage,
|
||||
queueStatusMessage,
|
||||
updateCoordinatesDisplay,
|
||||
updateZoomDisplay,
|
||||
updateEarthStats,
|
||||
setEarthStatValue,
|
||||
setLoading,
|
||||
@@ -301,6 +300,8 @@ export let scene;
|
||||
export let camera;
|
||||
export let renderer;
|
||||
|
||||
const WEBGL_GPU_DIAGNOSTICS_EVENT = "earth:gpu-diagnostics";
|
||||
|
||||
let isDragging = false;
|
||||
let previousMousePosition = { x: 0, y: 0 };
|
||||
let targetRotation = { x: 0, y: 0 };
|
||||
@@ -451,6 +452,29 @@ function getViewportAspect() {
|
||||
return window.innerWidth / window.innerHeight;
|
||||
}
|
||||
|
||||
function getWebGLGpuDiagnostics(activeRenderer) {
|
||||
const gl = activeRenderer?.getContext?.();
|
||||
if (!gl) return null;
|
||||
const debugInfo = gl.getExtension?.("WEBGL_debug_renderer_info");
|
||||
const attrs = gl.getContextAttributes?.() || {};
|
||||
return {
|
||||
requestedPowerPreference: "high-performance",
|
||||
contextPowerPreference: attrs.powerPreference || null,
|
||||
vendor: debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR),
|
||||
renderer: debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),
|
||||
antialias: Boolean(attrs.antialias),
|
||||
alpha: Boolean(attrs.alpha),
|
||||
};
|
||||
}
|
||||
|
||||
function publishWebGLGpuDiagnostics(activeRenderer) {
|
||||
const diagnostics = getWebGLGpuDiagnostics(activeRenderer);
|
||||
if (!diagnostics) return;
|
||||
window.__planetEarthGpu = diagnostics;
|
||||
console.info("[智能星球] WebGL GPU diagnostics", diagnostics);
|
||||
window.dispatchEvent(new CustomEvent(WEBGL_GPU_DIAGNOSTICS_EVENT, { detail: diagnostics }));
|
||||
}
|
||||
|
||||
function syncRendererViewport() {
|
||||
if (!camera || !renderer) return;
|
||||
camera.aspect = getViewportAspect();
|
||||
@@ -793,12 +817,30 @@ function isSameVessel(marker1, marker2) {
|
||||
return Boolean(marker1 && marker2 && marker1.userData?.mmsi === marker2.userData?.mmsi);
|
||||
}
|
||||
|
||||
function getFirstObjectIntersection(intersections) {
|
||||
return intersections.find((hit) => !hit?.cluster && hit?.object)?.object || null;
|
||||
}
|
||||
|
||||
function getPrimaryClusterHit(...intersectionGroups) {
|
||||
return intersectionGroups
|
||||
.flat()
|
||||
.filter((hit) => hit?.cluster && Number(hit.clusterCount) > 1)
|
||||
.sort((a, b) => a.distancePxSq - b.distancePxSq)[0] || null;
|
||||
}
|
||||
|
||||
function getClusterBriefHtml(hit) {
|
||||
const count = Number(hit?.clusterCount || hit?.clusterMarkers?.length || 0);
|
||||
return `<strong>共 ${count} 个对象</strong><br><span>放大后可查看单个图标</span>`;
|
||||
}
|
||||
|
||||
function getPrimaryBGPHoverTarget(bgpAnomalyIntersects, bgpCollectorIntersects) {
|
||||
if (bgpAnomalyIntersects.length > 0) {
|
||||
return bgpAnomalyIntersects[0].object;
|
||||
const anomalyMarker = getFirstObjectIntersection(bgpAnomalyIntersects);
|
||||
if (anomalyMarker) {
|
||||
return anomalyMarker;
|
||||
}
|
||||
if (bgpCollectorIntersects.length > 0) {
|
||||
return bgpCollectorIntersects[0].object;
|
||||
const collectorMarker = getFirstObjectIntersection(bgpCollectorIntersects);
|
||||
if (collectorMarker) {
|
||||
return collectorMarker;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -808,8 +850,8 @@ function getPrimaryBGPClickTarget(
|
||||
bgpAnomalyIntersects,
|
||||
bgpCollectorIntersects,
|
||||
) {
|
||||
const anomalyMarker = bgpAnomalyIntersects[0]?.object || null;
|
||||
const collectorMarker = bgpCollectorIntersects[0]?.object || null;
|
||||
const anomalyMarker = getFirstObjectIntersection(bgpAnomalyIntersects);
|
||||
const collectorMarker = getFirstObjectIntersection(bgpCollectorIntersects);
|
||||
if (!anomalyMarker && !collectorMarker) return null;
|
||||
if (!anomalyMarker) return collectorMarker;
|
||||
if (!collectorMarker) return anomalyMarker;
|
||||
@@ -952,19 +994,23 @@ function getMotionIconCandidates() {
|
||||
const candidates = [];
|
||||
if (getShowBGP()) {
|
||||
getBGPEventIconPointerIntersections(sharedOptions).forEach((hit) => {
|
||||
if (hit.cluster || !hit.object) return;
|
||||
candidates.push({ type: "bgp", object: hit.object, screen: getMotionScreenPointFromWorld(hit.point), distancePxSq: hit.distancePxSq });
|
||||
});
|
||||
getBGPCollectorIconPointerIntersections(sharedOptions).forEach((hit) => {
|
||||
if (hit.cluster || !hit.object) return;
|
||||
candidates.push({ type: "bgp_collector", object: hit.object, screen: getMotionScreenPointFromWorld(hit.point), distancePxSq: hit.distancePxSq });
|
||||
});
|
||||
}
|
||||
if (getShowComputeCenters()) {
|
||||
getComputeCenterIconPointerIntersections(sharedOptions).forEach((hit) => {
|
||||
if (hit.cluster || !hit.object) return;
|
||||
candidates.push({ type: "compute_center", object: hit.object, screen: getMotionScreenPointFromWorld(hit.point), distancePxSq: hit.distancePxSq });
|
||||
});
|
||||
}
|
||||
if (getShowVessels()) {
|
||||
getVesselIconPointerIntersections(sharedOptions).forEach((hit) => {
|
||||
if (hit.cluster || !hit.object) return;
|
||||
candidates.push({ type: "vessel", object: hit.object, screen: getMotionScreenPointFromWorld(hit.point), distancePxSq: hit.distancePxSq });
|
||||
});
|
||||
}
|
||||
@@ -3961,7 +4007,7 @@ export function init() {
|
||||
0.1,
|
||||
5000,
|
||||
);
|
||||
camera.position.z = CONFIG.defaultCameraZ;
|
||||
setZoomLevel(getDefaultEarthZoomLevel(), camera);
|
||||
setSatelliteCamera(camera);
|
||||
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
@@ -3972,6 +4018,7 @@ export function init() {
|
||||
syncRendererViewport();
|
||||
renderer.setClearColor(0x02040a, 1);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
publishWebGLGpuDiagnostics(renderer);
|
||||
|
||||
const container = document.getElementById("container");
|
||||
if (container) {
|
||||
@@ -5287,13 +5334,17 @@ function onMouseMove(event) {
|
||||
bgpAnomalyIntersects,
|
||||
bgpCollectorIntersects,
|
||||
);
|
||||
const hoveredClusterHit = getPrimaryClusterHit(
|
||||
bgpAnomalyIntersects,
|
||||
bgpCollectorIntersects,
|
||||
computeCenterIntersects,
|
||||
);
|
||||
|
||||
if (hoveredBGP && !isSameBGPMarker(hoveredBGP, hoveredBGPMarker)) {
|
||||
clearTransientHoverState();
|
||||
}
|
||||
|
||||
const hoveredComputeCenterMarker =
|
||||
computeCenterIntersects.length > 0 ? computeCenterIntersects[0].object : null;
|
||||
const hoveredComputeCenterMarker = getFirstObjectIntersection(computeCenterIntersects);
|
||||
const hoveredVesselMarker =
|
||||
vesselPick.checked && vesselIntersects.length > 0 ? vesselIntersects[0].object : null;
|
||||
const earthPoint = screenToEarthCoords(
|
||||
@@ -5357,6 +5408,20 @@ function onMouseMove(event) {
|
||||
let objectTooltipShown = false;
|
||||
|
||||
if (
|
||||
hoveredClusterHit &&
|
||||
!hoveredBGPMarker &&
|
||||
!hoveredComputeCenterMarker &&
|
||||
lockedObjectType !== "bgp" &&
|
||||
lockedObjectType !== "bgp_collector" &&
|
||||
lockedObjectType !== "compute_center"
|
||||
) {
|
||||
showTooltip(
|
||||
event.clientX + TOOLTIP_CURSOR_OFFSET,
|
||||
event.clientY + TOOLTIP_CURSOR_OFFSET,
|
||||
getClusterBriefHtml(hoveredClusterHit),
|
||||
);
|
||||
objectTooltipShown = true;
|
||||
} else if (
|
||||
hoveredBGPMarker &&
|
||||
getShowBGP() &&
|
||||
lockedObjectType !== "bgp" &&
|
||||
@@ -5663,9 +5728,7 @@ function onClick(event) {
|
||||
const clickedBGPMarker = getShowBGP()
|
||||
? getPrimaryBGPClickTarget(event, bgpAnomalyIntersects, bgpCollectorIntersects)
|
||||
: null;
|
||||
const clickedComputeCenterMarker = computeCenterIntersects.length > 0
|
||||
? computeCenterIntersects[0].object
|
||||
: null;
|
||||
const clickedComputeCenterMarker = getFirstObjectIntersection(computeCenterIntersects);
|
||||
const clickedVesselMarker = vesselIntersects.length > 0
|
||||
? vesselIntersects[0].object
|
||||
: null;
|
||||
|
||||
Reference in New Issue
Block a user