feat: refine earth bgp collector presentation
This commit is contained in:
@@ -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: "严重事件" },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1508,7 +1508,7 @@ function animate() {
|
||||
}
|
||||
|
||||
applyCableVisualState();
|
||||
updateBGPVisualState(lockedObjectType, lockedObject);
|
||||
updateBGPVisualState(lockedObjectType, lockedObject, camera);
|
||||
|
||||
if (lockedObjectType === "cable" && lockedObject) {
|
||||
applyLandingPointVisualState(lockedObject.userData.name, false);
|
||||
|
||||
Reference in New Issue
Block a user