fix: preserve BGP event halo at globe limb
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled

This commit is contained in:
rayd1o
2026-09-30 18:23:49 +08:00
parent 05377bf5c6
commit a8713e58fd
3 changed files with 23 additions and 7 deletions

View File

@@ -98,7 +98,7 @@ Base, tint, land fill and HD surface now use `createEarthSurfaceGeometry()` and
### Surface marker alignment and limb visibility
Cables and landing points share R+0.48, the `earthSurfacePoint` triangle projection and renderOrder=1. Related landing halos use an emphasis attribute in the existing batch, adding no Sprite or draw call. Rounded badges use GPU anchor ray/sphere occlusion: draw the entire billboard when its anchor is visible, otherwise clip it entirely. CPU picking uses the same test to reject rear anchors. Only `icon.badge` layers (including compute and collectors; vessels independently use heading arrows) use this path; flat BGP change-event depth testing stays unchanged.
Cables and landing points share R+0.48, the `earthSurfacePoint` triangle projection and renderOrder=1. Related landing halos use an emphasis attribute in the existing batch, adding no Sprite or draw call. Rounded badges use GPU anchor ray/sphere occlusion: draw the entire billboard when its anchor is visible, otherwise clip it entirely. CPU picking uses the same test to reject rear anchors. Only `icon.badge` layers (including compute and collectors; vessels independently use heading arrows) use this path. BGP event rings use the same whole-anchor front/rear decision: front-side rings disable per-pixel depth testing and sit in the foreground adjacent to the event icon, preventing the Earth from slicing their edges; rear-side anchors hide the entire ring.
The `.map-clusters` projection layer uses z-index 8 below HUD and whole-marker occlusion from real surface anchors. Ordinary points retain their Three.js layers; transitioning members render only once in the projection layer. Spider legs use widened transparent hit areas; hover highlights the matching line and child, and both clicks resolve the same business object. Wheel input over projected markers and legs continues to zoom the globe.

View File

@@ -100,7 +100,7 @@ Earth 的地表不是单一 mesh,而是多层近似同心球:基座球、海
### 地表标记对齐与侧面显隐
海缆和登陆点现在统一使用 R+0.48 及 `earthSurfacePoint` 三角表面投影,并保持 renderOrder=1。登陆点关联光晕进入原实例批次的 emphasis 属性,不增加独立 Sprite 或 draw call。侧面的完整圆角徽标采用 GPU 锚点射线/球体遮挡:锚点可见则整个 billboard 绘制,锚点被地球挡住则整块裁除。CPU 拾取使用同样的射线判断,避免点到背面图标。该路径只作用于 `icon.badge` 层(含算力、观测站;船舶独立使用航向箭头),不改变 BGP 变化事件的原深度测试。
海缆和登陆点现在统一使用 R+0.48 及 `earthSurfacePoint` 三角表面投影,并保持 renderOrder=1。登陆点关联光晕进入原实例批次的 emphasis 属性,不增加独立 Sprite 或 draw call。侧面的完整圆角徽标采用 GPU 锚点射线/球体遮挡:锚点可见则整个 billboard 绘制,锚点被地球挡住则整块裁除。CPU 拾取使用同样的射线判断,避免点到背面图标。该路径只作用于 `icon.badge` 层(含算力、观测站;船舶独立使用航向箭头)。BGP 事件圆环同样按锚点做整块前后判断,前面锚点的圆环关闭逐像素深度测试并置于事件图标邻近前景层,避免圆环边缘被地球切断;背面锚点整圈隐藏。
聚合投影层 `.map-clusters` 的 z-index 为 8,低于 HUD;以真实地表锚点做整标遮挡。普通点仍使用原 Three.js 图层,过渡中的成员只在投影层显示一次,避免双图标。Spiderfy 引线有透明加宽命中区域,悬停联动高亮引线与子点,点击返回同一业务对象;滚轮在图标与引线上继续缩放地球。

View File

@@ -8,6 +8,7 @@ import {
SURFACE_AVOIDANCE_PROFILES,
} from "./interactable.js";
import { getSurfaceMarkerCameraScale, latLonToVector3 } from "./utils.js";
import { isSurfaceAnchorVisible } from "./surface-marker-occlusion.js";
const bgpGroup = new THREE.Group();
const bgpOverlayGroup = new THREE.Group();
@@ -32,6 +33,7 @@ const colorScratchB = new THREE.Color();
const COLLECTOR_SCAN_SPEED_RAD = 0.00018;
const COLLECTOR_SCAN_REBUILD_MS = 80;
const BGP_EVENT_RENDER_ORDER = 4.5;
const BGP_EVENT_RING_RENDER_ORDER = BGP_EVENT_RENDER_ORDER + 0.02;
const BGP_EVENT_POINT_SIZE = 34;
const BGP_EVENT_SYMBOL_SIZE = 60;
const BGP_COLLECTOR_RENDER_ORDER = 4.4;
@@ -42,6 +44,7 @@ const BGP_COLLECTOR_ICON_SOURCE = new URL("../assets/icons/bgp-broadcast-pin.svg
const BGP_COLLECTOR_HOVER_SCALE = 1.08;
const BGP_COLLECTOR_LOCKED_SCALE = 1.12;
const BGP_COLLECTOR_PULSE_AMPLITUDE = 0.14;
const bgpCameraLocalScratch = new THREE.Vector3();
bgpOverlayGroup.name = "bgp-overlay-root";
bgpEventOverlayGroup.name = "bgp-event-overlay-layer";
@@ -1119,12 +1122,16 @@ function createEventRingSprite(marker) {
transparent: true,
opacity: 0,
depthWrite: false,
depthTest: true,
// The ring is a billboard around a surface anchor. Testing every pixel
// against Earth slices its sides at the limb even when the anchor is
// visible; visibility is decided once for the whole billboard below.
depthTest: false,
blending: THREE.AdditiveBlending,
}),
);
ring.position.copy(marker.position);
ring.renderOrder = BGP_EVENT_RENDER_ORDER - 0.05;
ring.renderOrder = BGP_EVENT_RING_RENDER_ORDER;
marker.userData.bgp_event_anchor_visible = true;
ring.visible = showBGP && !marker.userData.legend_filtered && !marker.userData.icon_presentation_hidden;
return ring;
}
@@ -1361,6 +1368,11 @@ export async function loadBGPAnomalies(scene, earth) {
export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cruiseMarker = null) {
const now = performance.now();
const earth = bgpGroup.parent;
if (earth && camera?.position) {
bgpCameraLocalScratch.copy(camera.position);
earth.worldToLocal(bgpCameraLocalScratch);
}
updateCollectorOverlayScan(lockedObjectType, lockedObject);
syncBGPLegendEffects();
const hasLockedLayer = Boolean(
@@ -1527,6 +1539,10 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
}
const ringPhaseA = (now * BGP_CONFIG.ring.speed + marker.userData.pulseOffset) % 1;
const ringAnchorVisible = !earth || !camera?.position
? true
: isSurfaceAnchorVisible(marker.position, bgpCameraLocalScratch, CONFIG.earthRadius);
marker.userData.bgp_event_anchor_visible = ringAnchorVisible;
const applyRingState = (ring, phase, maxScale) => {
if (!ring) return;
const progress = Math.max(0, Math.min(1, phase));
@@ -1540,8 +1556,8 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
ring.scale.setScalar(desiredWorldScale);
ring.material.color.setHex(markerColor);
ring.material.opacity = showBGP ? ringBaseOpacity * visibility : 0;
ring.visible = showBGP && !marker.userData.legend_filtered && !marker.userData.icon_presentation_hidden;
ring.renderOrder = isActive ? BGP_EVENT_RENDER_ORDER + 0.15 : BGP_EVENT_RENDER_ORDER - 0.05;
ring.visible = showBGP && ringAnchorVisible && !marker.userData.legend_filtered && !marker.userData.icon_presentation_hidden;
ring.renderOrder = isActive ? BGP_EVENT_RENDER_ORDER + 0.15 : BGP_EVENT_RING_RENDER_ORDER;
};
applyRingState(marker.userData.ringA, ringPhaseA, BGP_CONFIG.ring.scaleA);
@@ -1922,6 +1938,6 @@ export function applyBGPLegendFilter() {
bgpCollectorIconLayer.refreshFilter();
syncBGPLegendEffects();
anomalyMarkers.forEach(marker => {
if (marker.userData.ringA) marker.userData.ringA.visible = showBGP && !marker.userData.legend_filtered && !marker.userData.icon_presentation_hidden;
if (marker.userData.ringA) marker.userData.ringA.visible = showBGP && marker.userData.bgp_event_anchor_visible !== false && !marker.userData.legend_filtered && !marker.userData.icon_presentation_hidden;
});
}