From e384318b5020e7483a4d89ca5a8262b563e337f0 Mon Sep 17 00:00:00 2001 From: linkong Date: Tue, 31 Mar 2026 17:30:53 +0800 Subject: [PATCH] feat: refine earth bgp collector presentation --- VERSION | 2 +- docs/CHANGELOG.md | 14 + frontend/package.json | 2 +- frontend/public/earth/js/bgp.js | 518 ++++++++++++++++++++++---- frontend/public/earth/js/constants.js | 22 +- frontend/public/earth/js/main.js | 2 +- pyproject.toml | 2 +- 7 files changed, 491 insertions(+), 71 deletions(-) diff --git a/VERSION b/VERSION index a723ece7..faa5fb26 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.22.1 +0.22.2 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5e49d0ec..a349043d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -300,6 +300,20 @@ Released: 2026-03-25 - Fixed several collected-data field mapping issues. - Fixed frontend table layout inconsistencies across multiple admin pages. - Fixed TOP500 parsing and related metadata alignment issues. +## 0.22.2 + +Released: 2026-03-31 + +### Highlights + +- Refined Earth BGP collector presentation so coverage sectors, scanning behavior, and surface-aligned station markers read more like a coherent observability layer. + +### Improved + +- Improved BGP collector rendering with surface-aligned mesh icons, directional scanning sectors, and tighter sector geometry that stays closer to the globe surface. +- Improved collector icon styling by centralizing marker visual parameters in `BGP_CONFIG.collectorIcon` and restoring clearer low-saturation status color on the icon body. +- Improved dense collector readability by widening overlap spreading and reducing default non-selected glow so stations stay legible without blooming into bright clusters. + ## 0.22.1 Released: 2026-03-31 diff --git a/frontend/package.json b/frontend/package.json index 166c1df8..f6563610 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "planet-frontend", - "version": "0.22.1", + "version": "0.22.2", "private": true, "dependencies": { "@ant-design/icons": "^5.2.6", diff --git a/frontend/public/earth/js/bgp.js b/frontend/public/earth/js/bgp.js index be6d52e3..1bcb0575 100644 --- a/frontend/public/earth/js/bgp.js +++ b/frontend/public/earth/js/bgp.js @@ -15,9 +15,21 @@ let totalIncidentCount = 0; let textureCache = null; let collectorTextureCache = null; let activeEventOverlay = null; +let activeCollectorOverlayContext = null; const relativeTimeFormatter = new Intl.RelativeTimeFormat("zh-CN", { numeric: "auto", }); +const collectorWorldPosition = new THREE.Vector3(); +const collectorSurfaceNormal = new THREE.Vector3(); +const collectorNorthPole = new THREE.Vector3(0, 1, 0); +const collectorFallbackForward = new THREE.Vector3(0, 0, 1); +const collectorNorthTangent = new THREE.Vector3(); +const collectorEastTangent = new THREE.Vector3(); +const collectorOrientationMatrix = new THREE.Matrix4(); +const colorScratchA = new THREE.Color(); +const colorScratchB = new THREE.Color(); +const COLLECTOR_SCAN_SPEED_RAD = 0.00018; +const COLLECTOR_SCAN_REBUILD_MS = 80; const MATERIAL_ACCESS_POINT_PATH = "M4.93 4.93A9.97 9.97 0 0 0 2 12c0 2.76 1.12 5.26 2.93 7.07l1.41-1.41A7.94 7.94 0 0 1 4 12c0-2.21.89-4.22 2.34-5.66zm14.14 0l-1.41 1.41A7.96 7.96 0 0 1 20 12c0 2.22-.89 4.22-2.34 5.66l1.41 1.41A9.97 9.97 0 0 0 22 12c0-2.76-1.12-5.26-2.93-7.07M7.76 7.76A5.98 5.98 0 0 0 6 12c0 1.65.67 3.15 1.76 4.24l1.41-1.41A4 4 0 0 1 8 12c0-1.11.45-2.11 1.17-2.83zm8.48 0l-1.41 1.41A4 4 0 0 1 16 12c0 1.11-.45 2.11-1.17 2.83l1.41 1.41A5.98 5.98 0 0 0 18 12c0-1.65-.67-3.15-1.76-4.24M12 10a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2"; function getMarkerTexture() { @@ -62,22 +74,28 @@ function getCollectorTexture() { context.clearRect(0, 0, 128, 128); - const glow = context.createRadialGradient(64, 64, 8, 64, 64, 34); - glow.addColorStop(0, "rgba(255,255,255,0.36)"); - glow.addColorStop(0.55, "rgba(255,255,255,0.12)"); - glow.addColorStop(1, "rgba(255,255,255,0)"); - context.fillStyle = glow; + context.strokeStyle = BGP_CONFIG.collectorIcon.ringStroke; + context.lineWidth = BGP_CONFIG.collectorIcon.ringLineWidth; context.beginPath(); - context.arc(64, 64, 34, 0, Math.PI * 2); - context.fill(); + context.arc(64, 64, BGP_CONFIG.collectorIcon.ringRadius, 0, Math.PI * 2); + context.stroke(); context.save(); context.translate(16, 16); context.scale(4, 4); - context.fillStyle = "rgba(255,255,255,0.98)"; - context.shadowColor = "rgba(255,255,255,0.3)"; - context.shadowBlur = 3; - context.fill(new Path2D(MATERIAL_ACCESS_POINT_PATH)); + const path = new Path2D(MATERIAL_ACCESS_POINT_PATH); + context.lineJoin = "round"; + context.lineCap = "round"; + context.lineWidth = BGP_CONFIG.collectorIcon.pathLineWidth; + context.strokeStyle = BGP_CONFIG.collectorIcon.pathStroke; + context.stroke(path); + context.fillStyle = BGP_CONFIG.collectorIcon.pathFill; + context.shadowBlur = 0; + context.fill(path); + context.fillStyle = BGP_CONFIG.collectorIcon.centerFill; + context.beginPath(); + context.arc(12, 12, BGP_CONFIG.collectorIcon.centerRadius, 0, Math.PI * 2); + context.fill(); context.restore(); collectorTextureCache = new THREE.CanvasTexture(canvas); @@ -111,6 +129,49 @@ function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } +function blendHexColors(fromHex, toHex, ratio) { + colorScratchA.setHex(fromHex); + colorScratchB.setHex(toHex); + colorScratchA.lerp(colorScratchB, clamp(ratio, 0, 1)); + return colorScratchA.getHex(); +} + +function getCollectorDistanceScale(marker, camera) { + if (!marker || !camera) return 1; + + marker.getWorldPosition(collectorWorldPosition); + const distanceToCamera = camera.position.distanceTo(collectorWorldPosition); + const referenceDistance = CONFIG.defaultCameraZ - CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset; + + return clamp(distanceToCamera / referenceDistance, 0.6, 1.9); +} + +function orientCollectorMarkerToSurface(marker, position) { + collectorSurfaceNormal.copy(position).normalize(); + collectorNorthTangent + .copy(collectorNorthPole) + .projectOnPlane(collectorSurfaceNormal); + + if (collectorNorthTangent.lengthSq() < 1e-6) { + collectorNorthTangent + .copy(collectorFallbackForward) + .projectOnPlane(collectorSurfaceNormal); + } + + collectorNorthTangent.normalize(); + collectorEastTangent + .copy(collectorNorthTangent) + .cross(collectorSurfaceNormal) + .normalize(); + + collectorOrientationMatrix.makeBasis( + collectorEastTangent, + collectorNorthTangent, + collectorSurfaceNormal, + ); + marker.quaternion.setFromRotationMatrix(collectorOrientationMatrix); +} + function getCollectorActivityProfile(markerData) { const recent24h = Number(markerData?.recent_24h_observation_count || 0); const recent7d = Number(markerData?.recent_7d_observation_count || 0); @@ -410,13 +471,13 @@ function spreadCollectorPositions(markers) { groups.forEach((group) => { if (group.length <= 1) return; - const radius = 0.9; + const radius = 1.4; group.forEach((marker, index) => { const angle = (Math.PI * 2 * index) / group.length; marker.displayLatitude = - marker.latitude + Math.sin(angle) * radius * 0.18; + marker.latitude + Math.sin(angle) * radius * 0.28; marker.displayLongitude = - marker.longitude + Math.cos(angle) * radius * 0.18; + marker.longitude + Math.cos(angle) * radius * 0.28; marker.isSpread = true; marker.groupSize = group.length; }); @@ -597,21 +658,13 @@ function createOverlaySprite({ color, opacity, scale }) { return sprite; } -function createArcLine(start, end, color) { - const midpoint = start - .clone() - .add(end) - .multiplyScalar(0.5) - .normalize() - .multiplyScalar(CONFIG.earthRadius + BGP_CONFIG.eventHubAltitudeOffset * 0.8); - - const curve = new THREE.QuadraticBezierCurve3(start, midpoint, end); - const points = curve.getPoints(32); +function createArcLine(start, end, color, opacity = 0.82) { + const points = createArcPoints(start, end); const geometry = new THREE.BufferGeometry().setFromPoints(points); const material = new THREE.LineBasicMaterial({ color, transparent: true, - opacity: 0.82, + opacity, depthWrite: false, blending: THREE.AdditiveBlending, }); @@ -619,16 +672,197 @@ function createArcLine(start, end, color) { return new THREE.Line(geometry, material); } +function createArcPoints(start, end, heightOffset = BGP_CONFIG.eventHubAltitudeOffset * 0.8, segments = 32) { + const midpoint = start + .clone() + .add(end) + .multiplyScalar(0.5) + .normalize() + .multiplyScalar(CONFIG.earthRadius + heightOffset); + + const curve = new THREE.QuadraticBezierCurve3(start, midpoint, end); + return curve.getPoints(segments); +} + +function projectLatLon(lat, lon, bearingDeg, distanceDeg) { + const latRad = (lat * Math.PI) / 180; + const lonRad = (lon * Math.PI) / 180; + const bearing = (bearingDeg * Math.PI) / 180; + const angularDistance = (distanceDeg * Math.PI) / 180; + + const targetLat = Math.asin( + Math.sin(latRad) * Math.cos(angularDistance) + + Math.cos(latRad) * Math.sin(angularDistance) * Math.cos(bearing), + ); + const targetLon = + lonRad + + Math.atan2( + Math.sin(bearing) * Math.sin(angularDistance) * Math.cos(latRad), + Math.cos(angularDistance) - Math.sin(latRad) * Math.sin(targetLat), + ); + + return { + latitude: (targetLat * 180) / Math.PI, + longitude: ((((targetLon * 180) / Math.PI) + 540) % 360) - 180, + }; +} + +function createCoverageBoundaryLine(points, color, opacity = 0.3) { + const geometry = new THREE.BufferGeometry().setFromPoints(points); + const material = new THREE.LineBasicMaterial({ + color, + transparent: true, + opacity, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + return new THREE.Line(geometry, material); +} + +function createCoverageSector(points, color, opacity = 0.12) { + const center = points[0]; + const positions = []; + + for (let index = 1; index < points.length - 1; index += 1) { + const current = points[index]; + const next = points[index + 1]; + positions.push( + center.x, center.y, center.z, + current.x, current.y, current.z, + next.x, next.y, next.z, + ); + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); + geometry.computeVertexNormals(); + + const material = new THREE.MeshBasicMaterial({ + color, + transparent: true, + opacity, + side: THREE.DoubleSide, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + + return new THREE.Mesh(geometry, material); +} + +function createCoverageSectorMesh( + anchorLatitude, + anchorLongitude, + startBearingDeg, + endBearingDeg, + reachDeg, + altitude, + color, + opacity = 0.12, + radialSegments = 8, + angularSegments = 28, +) { + const positions = []; + + for (let radialIndex = 0; radialIndex < radialSegments; radialIndex += 1) { + const innerDistance = (reachDeg * radialIndex) / radialSegments; + const outerDistance = (reachDeg * (radialIndex + 1)) / radialSegments; + + for (let angularIndex = 0; angularIndex < angularSegments; angularIndex += 1) { + const startProgress = angularIndex / angularSegments; + const endProgress = (angularIndex + 1) / angularSegments; + const startBearing = startBearingDeg + (endBearingDeg - startBearingDeg) * startProgress; + const endBearing = startBearingDeg + (endBearingDeg - startBearingDeg) * endProgress; + + const innerStart = projectLatLon(anchorLatitude, anchorLongitude, startBearing, innerDistance); + const innerEnd = projectLatLon(anchorLatitude, anchorLongitude, endBearing, innerDistance); + const outerStart = projectLatLon(anchorLatitude, anchorLongitude, startBearing, outerDistance); + const outerEnd = projectLatLon(anchorLatitude, anchorLongitude, endBearing, outerDistance); + + const innerStartVector = latLonToVector3(innerStart.latitude, innerStart.longitude, altitude); + const innerEndVector = latLonToVector3(innerEnd.latitude, innerEnd.longitude, altitude); + const outerStartVector = latLonToVector3(outerStart.latitude, outerStart.longitude, altitude); + const outerEndVector = latLonToVector3(outerEnd.latitude, outerEnd.longitude, altitude); + + positions.push( + innerStartVector.x, innerStartVector.y, innerStartVector.z, + outerStartVector.x, outerStartVector.y, outerStartVector.z, + outerEndVector.x, outerEndVector.y, outerEndVector.z, + ); + positions.push( + innerStartVector.x, innerStartVector.y, innerStartVector.z, + outerEndVector.x, outerEndVector.y, outerEndVector.z, + innerEndVector.x, innerEndVector.y, innerEndVector.z, + ); + } + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); + geometry.computeVertexNormals(); + + const material = new THREE.MeshBasicMaterial({ + color, + transparent: true, + opacity, + side: THREE.DoubleSide, + depthWrite: false, + polygonOffset: true, + polygonOffsetFactor: 1, + polygonOffsetUnits: 1, + blending: THREE.AdditiveBlending, + }); + + return new THREE.Mesh(geometry, material); +} + +function createRadialBoundaryPoints( + anchorLatitude, + anchorLongitude, + bearingDeg, + reachDeg, + altitude, + segments = 18, +) { + const points = []; + + for (let step = 0; step <= segments; step += 1) { + const progress = step / segments; + const projected = projectLatLon( + anchorLatitude, + anchorLongitude, + bearingDeg, + reachDeg * progress, + ); + points.push( + latLonToVector3( + projected.latitude, + projected.longitude, + altitude, + ), + ); + } + + return points; +} + function createCollectorMarker(markerData) { const activity = getCollectorActivityProfile(markerData); - const sprite = new THREE.Sprite( - new THREE.SpriteMaterial({ + const baseColor = activity.color; + const idleColor = blendHexColors( + BGP_CONFIG.collectorIcon.idleBaseColor, + baseColor, + BGP_CONFIG.collectorIcon.idleBlend, + ); + const marker = new THREE.Mesh( + new THREE.PlaneGeometry(1, 1), + new THREE.MeshBasicMaterial({ map: getCollectorTexture(), - color: activity.color, + color: idleColor, transparent: true, - opacity: BGP_CONFIG.opacity.collector, + opacity: BGP_CONFIG.collectorIcon.idleOpacity, depthWrite: false, depthTest: true, + side: THREE.DoubleSide, }), ); @@ -638,52 +872,64 @@ function createCollectorMarker(markerData) { CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset, ); - sprite.position.copy(position); - sprite.scale.set(BGP_CONFIG.collectorScale * 0.88 * activity.scaleBoost, BGP_CONFIG.collectorScale * 1.08 * activity.scaleBoost, 1); - sprite.renderOrder = 3; - sprite.visible = showBGP; + marker.position.copy(position); + marker.scale.set(BGP_CONFIG.collectorScale * 0.88 * activity.scaleBoost, BGP_CONFIG.collectorScale * 1.08 * activity.scaleBoost, 1); + marker.renderOrder = 3; + marker.visible = showBGP; + orientCollectorMarkerToSurface(marker, position); const heatHalo = createOverlaySprite({ color: activity.color, - opacity: 0.018, - scale: activity.haloScale * 0.78, + opacity: 0.0, + scale: activity.haloScale * 0.58, }); heatHalo.renderOrder = 1; - sprite.add(heatHalo); + marker.add(heatHalo); const pulseHalo = createOverlaySprite({ color: activity.color, - opacity: 0.008, - scale: activity.pulseHaloScale * 0.72, + opacity: 0.0, + scale: activity.pulseHaloScale * 0.48, }); pulseHalo.renderOrder = 0; - sprite.add(pulseHalo); + marker.add(pulseHalo); + + const statusCore = createOverlaySprite({ + color: activity.color, + opacity: 0.0, + scale: Math.max(BGP_CONFIG.collectorScale * 0.12, 1.2), + }); + statusCore.position.set(0, 0, 0.02); + statusCore.renderOrder = 4; + marker.add(statusCore); const coverageHalo = createOverlaySprite({ color: BGP_CONFIG.regionColor, - opacity: 0.01, + opacity: 0.0, scale: activity.coverageHaloScale * 0.7, }); coverageHalo.renderOrder = 0; coverageHalo.scale.set(activity.coverageHaloScale * 0.82, activity.coverageHaloScale * 0.56, 1); - sprite.add(coverageHalo); + marker.add(coverageHalo); - sprite.userData = { + marker.userData = { type: "bgp_collector", state: "normal", baseScale: BGP_CONFIG.collectorScale * activity.scaleBoost, - baseColor: activity.color, + baseColor, + idleColor, pulseOffset: Math.random() * Math.PI * 2, anomaly_count: 0, activity, heatHalo, pulseHalo, + statusCore, coverageHalo, ...markerData, }; - collectorMarkers.push(sprite); - bgpGroup.add(sprite); + collectorMarkers.push(marker); + bgpGroup.add(marker); } function createAnomalyMarker(markerData) { @@ -869,8 +1115,9 @@ export async function loadBGPAnomalies(scene, earth) { }; } -export function updateBGPVisualState(lockedObjectType, lockedObject) { +export function updateBGPVisualState(lockedObjectType, lockedObject, camera) { const now = performance.now(); + updateCollectorOverlayScan(lockedObjectType, lockedObject); const hasLockedLayer = Boolean( lockedObject && ["cable", "satellite", "bgp", "bgp_collector"].includes(lockedObjectType), ); @@ -888,32 +1135,48 @@ export function updateBGPVisualState(lockedObjectType, lockedObject) { now * BGP_CONFIG.collectorPulseSpeed + marker.userData.pulseOffset, ); - let scale = marker.userData.baseScale; - let opacity = BGP_CONFIG.opacity.collector; - let haloOpacity = 0.018; - let pulseOpacity = 0.008; - let coverageOpacity = 0.01; - let markerColor = marker.userData.baseColor || BGP_CONFIG.collectorColor; + let scale = marker.userData.baseScale * getCollectorDistanceScale(marker, camera); + let opacity = BGP_CONFIG.collectorIcon.idleOpacity; + let haloOpacity = 0.0; + let pulseOpacity = 0.0; + let coverageOpacity = 0.0; + let markerColor = + marker.userData.idleColor || + blendHexColors( + BGP_CONFIG.collectorIcon.idleBaseColor, + marker.userData.baseColor || BGP_CONFIG.collectorColor, + BGP_CONFIG.collectorIcon.idleBlend, + ); if (isLocked) { scale *= 1.1 + 0.14 * pulse; opacity = BGP_CONFIG.opacity.collectorHover; - haloOpacity = 0.07; - pulseOpacity = 0.04; - coverageOpacity = 0.035; + haloOpacity = 0.022; + pulseOpacity = 0.012; + coverageOpacity = 0.02; + markerColor = blendHexColors( + BGP_CONFIG.collectorIcon.lockedNeutralColor, + marker.userData.baseColor || BGP_CONFIG.collectorColor, + BGP_CONFIG.collectorIcon.lockedBlend, + ); } else if (isHovered) { scale *= 1.08; opacity = BGP_CONFIG.opacity.collectorHover; - haloOpacity = 0.05; - pulseOpacity = 0.03; - coverageOpacity = 0.028; + haloOpacity = 0.016; + pulseOpacity = 0.008; + coverageOpacity = 0.014; + markerColor = blendHexColors( + BGP_CONFIG.collectorIcon.hoverNeutralColor, + marker.userData.baseColor || BGP_CONFIG.collectorColor, + BGP_CONFIG.collectorIcon.hoverBlend, + ); } else if (hasLockedLayer) { - scale *= BGP_CONFIG.dimmedScale; - opacity = 0.12; + scale *= 0.98; + opacity = BGP_CONFIG.collectorIcon.idleOpacity; haloOpacity = 0.0; pulseOpacity = 0.0; coverageOpacity = 0.0; - markerColor = 0x7d8ca3; + markerColor = marker.userData.idleColor || markerColor; } else { scale *= 1 + 0.05 * pulse; } @@ -927,14 +1190,22 @@ export function updateBGPVisualState(lockedObjectType, lockedObject) { marker.userData.heatHalo.material.opacity = haloOpacity; marker.userData.heatHalo.material.color.setHex(marker.userData.baseColor || BGP_CONFIG.collectorColor); marker.userData.heatHalo.scale.setScalar( - marker.userData.activity?.haloScale * 0.78 * (1 + pulse * 0.03), + marker.userData.activity?.haloScale * 0.58 * (1 + pulse * 0.01), ); } if (marker.userData.pulseHalo) { marker.userData.pulseHalo.material.opacity = pulseOpacity; marker.userData.pulseHalo.material.color.setHex(marker.userData.baseColor || BGP_CONFIG.collectorColor); marker.userData.pulseHalo.scale.setScalar( - marker.userData.activity?.pulseHaloScale * 0.72 * (1 + pulse * 0.05), + marker.userData.activity?.pulseHaloScale * 0.48 * (1 + pulse * 0.02), + ); + } + if (marker.userData.statusCore) { + marker.userData.statusCore.material.opacity = + isLocked ? 0.58 : isHovered ? 0.4 : hasLockedLayer ? 0.0 : 0.18; + marker.userData.statusCore.material.color.setHex(marker.userData.baseColor || BGP_CONFIG.collectorColor); + marker.userData.statusCore.scale.setScalar( + Math.max(BGP_CONFIG.collectorScale * 0.12, 1.2) * (isLocked ? 1.08 : isHovered ? 1.04 : 0.92), ); } if (marker.userData.coverageHalo) { @@ -1151,10 +1422,11 @@ export function showBGPEventOverlay(marker, earth) { }); activeEventOverlay = overlayItems; + activeCollectorOverlayContext = null; bgpOverlayGroup.visible = showBGP; } -export function showBGPCollectorCoverageOverlay(marker, earth) { +export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) { if (!marker?.userData || marker.userData.type !== "bgp_collector" || !earth) return; clearBGPEventOverlay(); @@ -1207,20 +1479,136 @@ export function showBGPCollectorCoverageOverlay(marker, earth) { innerRing.renderOrder = 3; bgpOverlayGroup.add(innerRing); - activeEventOverlay = [halo, pulseHalo, innerRing]; + const overlayItems = [halo, pulseHalo, innerRing]; + const anchorLatitude = marker.userData.displayLatitude ?? marker.userData.latitude; + const anchorLongitude = marker.userData.displayLongitude ?? marker.userData.longitude; + const activityMagnitude = Math.log2(prefixCount + observationCount + 1); + const corridorReach = Math.min(30, 12 + activityMagnitude * 3.2); + const orientationSeed = Array.from(marker.userData.collector || "") + .reduce((sum, char) => sum + char.charCodeAt(0), 0); + const baseRotation = ((orientationSeed % 140) - 70) * (Math.PI / 180); + const sectorRotation = baseRotation + (options.rotationOffsetRad || 0); + const sectorHalfWidth = Math.PI * 0.22; + const startBearing = (sectorRotation - sectorHalfWidth) * (180 / Math.PI); + const endBearing = (sectorRotation + sectorHalfWidth) * (180 / Math.PI); + const coverageColor = marker.userData.baseColor || BGP_CONFIG.collectorColor; + const boundaryAltitude = CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset + 0.44; + const fillAltitude = CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset + 0.4; + const leftBoundaryPoints = createRadialBoundaryPoints( + anchorLatitude, + anchorLongitude, + startBearing, + corridorReach, + boundaryAltitude, + ); + const rightBoundaryPoints = createRadialBoundaryPoints( + anchorLatitude, + anchorLongitude, + endBearing, + corridorReach, + boundaryAltitude, + ); + const outerArcPoints = []; + const outerArcSteps = 28; + for (let step = 0; step <= outerArcSteps; step += 1) { + const progress = step / outerArcSteps; + const bearingDeg = startBearing + (endBearing - startBearing) * progress; + const projected = projectLatLon(anchorLatitude, anchorLongitude, bearingDeg, corridorReach); + outerArcPoints.push( + latLonToVector3( + projected.latitude, + projected.longitude, + boundaryAltitude, + ), + ); + } + + const sectorFill = createCoverageSectorMesh( + anchorLatitude, + anchorLongitude, + startBearing, + endBearing, + corridorReach, + fillAltitude, + coverageColor, + 0.12, + ); + sectorFill.renderOrder = 2; + bgpOverlayGroup.add(sectorFill); + overlayItems.push(sectorFill); + + const outerArc = createCoverageBoundaryLine( + outerArcPoints, + coverageColor, + 0.9, + ); + outerArc.renderOrder = 3; + bgpOverlayGroup.add(outerArc); + overlayItems.push(outerArc); + + const leftBoundary = createCoverageBoundaryLine( + leftBoundaryPoints, + coverageColor, + 0.76, + ); + leftBoundary.renderOrder = 3; + bgpOverlayGroup.add(leftBoundary); + overlayItems.push(leftBoundary); + + const rightBoundary = createCoverageBoundaryLine( + rightBoundaryPoints, + coverageColor, + 0.76, + ); + rightBoundary.renderOrder = 3; + bgpOverlayGroup.add(rightBoundary); + overlayItems.push(rightBoundary); + + activeEventOverlay = overlayItems; + activeCollectorOverlayContext = { + marker, + earth, + baseRotation, + lastRebuildAt: performance.now(), + }; bgpOverlayGroup.visible = showBGP; } export function clearBGPEventOverlay() { activeEventOverlay = null; + activeCollectorOverlayContext = null; clearGroup(bgpOverlayGroup); } +function updateCollectorOverlayScan(lockedObjectType, lockedObject) { + if ( + lockedObjectType !== "bgp_collector" || + !lockedObject || + !activeCollectorOverlayContext || + activeCollectorOverlayContext.marker !== lockedObject + ) { + return; + } + + const now = performance.now(); + if (now - activeCollectorOverlayContext.lastRebuildAt < COLLECTOR_SCAN_REBUILD_MS) { + return; + } + + const rotationOffsetRad = now * COLLECTOR_SCAN_SPEED_RAD; + showBGPCollectorCoverageOverlay( + activeCollectorOverlayContext.marker, + activeCollectorOverlayContext.earth, + { rotationOffsetRad }, + ); +} + export function getBGPLegendItems() { return [ { color: "#6db7ff", label: "静态观测站" }, { color: "#fbbf24", label: "中活跃观测站" }, { color: "#ff5f57", label: "高活跃观测站" }, + { color: "#6db7ff", label: "观测范围示意" }, { color: "#8af5ff", label: "事件连线 / 枢纽" }, { color: "#2dd4bf", label: "影响区域" }, { color: "#ff4d4f", label: "严重事件" }, diff --git a/frontend/public/earth/js/constants.js b/frontend/public/earth/js/constants.js index 685017b7..7284ae57 100644 --- a/frontend/public/earth/js/constants.js +++ b/frontend/public/earth/js/constants.js @@ -92,8 +92,8 @@ export const BGP_CONFIG = { normal: 0.78, hover: 1.0, dimmed: 0.24, - collector: 0.82, - collectorHover: 1.0, + collector: 0.62, + collectorHover: 0.9, lockedMin: 0.65, lockedMax: 1.0 }, @@ -117,6 +117,24 @@ export const BGP_CONFIG = { high: 0xfb923c, hot: 0xff5f57 }, + collectorIcon: { + ringStroke: "rgba(111, 160, 197, 0.34)", + ringLineWidth: 1.2, + ringRadius: 22, + pathStroke: "rgba(64, 106, 136, 0.74)", + pathLineWidth: 0.9, + pathFill: "rgba(214,224,233,0.88)", + centerFill: "rgba(222,231,239,0.72)", + centerRadius: 0.85, + idleBaseColor: 0xb7c4cf, + idleBlend: 0.56, + idleOpacity: 0.74, + hoverBlend: 0.42, + hoverNeutralColor: 0xc8d5e0, + lockedBlend: 0.48, + lockedNeutralColor: 0xd8e4ed, + dimmedColor: 0x7d8ca3, + }, eventHubColor: 0x8af5ff, linkColor: 0x54d2ff, regionColor: 0x2dd4bf, diff --git a/frontend/public/earth/js/main.js b/frontend/public/earth/js/main.js index 3309c64e..edfea985 100644 --- a/frontend/public/earth/js/main.js +++ b/frontend/public/earth/js/main.js @@ -1508,7 +1508,7 @@ function animate() { } applyCableVisualState(); - updateBGPVisualState(lockedObjectType, lockedObject); + updateBGPVisualState(lockedObjectType, lockedObject, camera); if (lockedObjectType === "cable" && lockedObject) { applyLandingPointVisualState(lockedObject.userData.name, false); diff --git a/pyproject.toml b/pyproject.toml index f58857e3..10766016 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "planet" -version = "0.22.1" +version = "0.22.2" description = "智能星球计划 - 态势感知系统" requires-python = ">=3.14" dependencies = [