Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd41588527 | ||
|
|
45604a1ebb | ||
|
|
06c122bade | ||
|
|
96e9b15fda | ||
|
|
a8713e58fd |
@@ -8,6 +8,34 @@ This project follows the repository versioning rule:
|
||||
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
||||
- `bugfix` -> `+0.0.1`
|
||||
|
||||
## [0.76.2] — 2026-10-01
|
||||
|
||||
Released: 2026-10-01
|
||||
|
||||
### Highlights
|
||||
- 优化光缆密集区的卫星点拾取,并将点状交互物件优先级泛化到其他密集图层。
|
||||
|
||||
### Added / Fixed / Improved
|
||||
- 屏幕命中先统一比较卫星、BGP、算力和其他点状对象的距离,海缆线作为兜底候选,保护穿插在密集线缆中的小面积对象。
|
||||
- hover 与 click 共用同一候选排序,避免鼠标提示和最终选中对象不一致。
|
||||
- 补充密集区域拾取回归测试,清理中间态代码并同步渲染层说明。
|
||||
|
||||
---
|
||||
|
||||
## [0.76.1] — 2026-10-01
|
||||
|
||||
Released: 2026-10-01
|
||||
|
||||
### Highlights
|
||||
- 修复选中 BGP 事件静态光晕在地表边缘被切断或陷入地表的问题。
|
||||
|
||||
### Added / Fixed / Improved
|
||||
- 抽取通用整体锚点遮挡能力,BGP 事件保持原有图标样式并复用圆角图标的前后显隐规则。
|
||||
- 选中事件光晕与事件图标共享地表锚点、前景层级和前后显隐路径,清理中间态代码与资源。
|
||||
- 补充中英文渲染层说明、回归测试和浏览器验证记录。
|
||||
|
||||
---
|
||||
|
||||
## [0.76.0] — 2026-09-30
|
||||
|
||||
Released: 2026-09-30
|
||||
|
||||
@@ -253,7 +253,7 @@ All material, layer, satellite, BGP, cable, terrain, celestial, and other style
|
||||
|
||||
## Render hot paths
|
||||
|
||||
`cable-batches.js` batches the complete cable and landing-point sets. `cables.js` still owns original business objects, picking, occlusion, and selection state. Do not make these interaction proxies draw individually again or return a render batch as the detail-card business object.
|
||||
`cable-batches.js` batches the complete cable and landing-point sets. `cables.js` still owns original business objects, picking, occlusion, and selection state. Do not make these interaction proxies draw individually again or return a render batch as the detail-card business object. In dense cable areas, screen-distance picking compares satellites, BGP, compute and other point objects before using cable lines as the fallback; hover and click share the same ordering so small objects threaded through a line bundle remain selectable.
|
||||
|
||||
Satellite breathing runs in vertex shaders. `satellite-position-worker.js` moves full SGP4 position and initial trail calculations off the main thread; the main thread applies snapshots to interaction coordinates and render buffers. `satellite-propagation.js` supplies the same mathematics to the Worker, predicted orbits, and synchronous fallback. Reload, clear, and altitude changes must terminate prior work so stale snapshots cannot resurrect cleared layers.
|
||||
|
||||
|
||||
@@ -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. The selected BGP event's static overlay halo uses the event icon's R+0.48 anchor, foreground order and whole-anchor front/rear visibility; the existing animated BGP event ring remains a separate effect with its original behavior.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -233,7 +233,7 @@ TV 预览需要尽量复用 Earth 运行时的直播卡片结构和状态标签
|
||||
|
||||
## 渲染热路径
|
||||
|
||||
`cable-batches.js` 将全部海缆线段与全部登陆点分别合批绘制;`cables.js` 继续拥有原始业务对象、拾取、遮挡和选择状态。避免在后续功能中把这些代理对象重新加入逐对象绘制,或把绘制批次当作业务对象返回给详情卡。
|
||||
`cable-batches.js` 将全部海缆线段与全部登陆点分别合批绘制;`cables.js` 继续拥有原始业务对象、拾取、遮挡和选择状态。避免在后续功能中把这些代理对象重新加入逐对象绘制,或把绘制批次当作业务对象返回给详情卡。密集线缆区的拾取先在屏幕距离上比较卫星、BGP、算力和其他点状对象,海缆线作为没有更近点状对象时的兜底;hover 与 click 共用该候选顺序,保护穿插在线缆中的小面积物件。
|
||||
|
||||
卫星的逐点呼吸在 GPU 中计算。全量 SGP4 位置和初始轨迹计算通过 `satellite-position-worker.js` 离开主线程;主线程只接收位置快照、更新共享交互坐标及绘制缓冲。轨道数学由 `satellite-propagation.js` 同时供 Worker、预测轨道和同步降级路径使用,避免多套公式漂移。数据重载、清空和高度切换必须同步终止旧计算,不能让旧快照恢复已清空的图层。
|
||||
|
||||
|
||||
@@ -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 事件的静态覆盖光晕使用事件图标相同的 R+0.48 锚点、前景顺序和整块前后显隐;BGP 发散圆环仍保留原有独立动画语义。
|
||||
|
||||
|
||||
聚合投影层 `.map-clusters` 的 z-index 为 8,低于 HUD;以真实地表锚点做整标遮挡。普通点仍使用原 Three.js 图层,过渡中的成员只在投影层显示一次,避免双图标。Spiderfy 引线有透明加宽命中区域,悬停联动高亮引线与子点,点击返回同一业务对象;滚轮在图标与引线上继续缩放地球。
|
||||
|
||||
@@ -16,12 +16,14 @@
|
||||
## Current Version
|
||||
|
||||
- `main` 当前主线历史推导到:`0.16.5`
|
||||
- `dev` 当前开发分支历史推导到:`0.76.0`
|
||||
- `dev` 当前开发分支历史推导到:`0.76.2`
|
||||
|
||||
## Timeline
|
||||
|
||||
| Version | Type | Branch | Commit | Summary |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `0.76.2` | bugfix | `dev` | `v0.76.2` | 优化光缆密集区卫星点拾取,统一点状对象与线缆候选排序并补齐回归验证 |
|
||||
| `0.76.1` | bugfix | `dev` | `v0.76.1` | 修复选中 BGP 事件静态光晕地表遮挡,统一事件锚点显隐与前景偏移,清理中间态逻辑并补齐验证 |
|
||||
| `0.76.0` | feature | `dev` | `v0.76.0` | 详情卡默认配图与后台配置、五类算力中心、统一地表拾取与选中置顶、卡片交互修复及 Earth 性能优化 |
|
||||
| `0.75.4` | improvement | `dev` | `v0.75.4` | 图例统一筛选、卫星 Points 点径与性能优化、FPS、菜单 DPI/hover、星空 sRGB、地表对齐及新闻编码修复,清理废弃代码文档 |
|
||||
| `0.75.3` | improvement | `dev` | `v0.75.3` | 地球海陆材质与可读夜侧、连续拖拽、现代徽标、卫星平滑开放轨道及流光,修复 CelesTrak 缓存与任务历史并清理废弃实现 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.76.0",
|
||||
"version": "0.76.2",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"dependencies": {
|
||||
|
||||
@@ -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();
|
||||
@@ -22,7 +23,6 @@ let totalAnomalyCount = 0;
|
||||
let totalIncidentCount = 0;
|
||||
let textureCache = null;
|
||||
let eventRingTextureCache = null;
|
||||
let activeEventOverlay = null;
|
||||
let activeCollectorOverlayContext = null;
|
||||
const relativeTimeFormatter = new Intl.RelativeTimeFormat("zh-CN", {
|
||||
numeric: "auto",
|
||||
@@ -42,6 +42,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";
|
||||
@@ -279,6 +280,7 @@ const bgpEventIconLayer = createInteractableLayer({
|
||||
coordinates: "canvas",
|
||||
draw: drawBGPEventIcon,
|
||||
},
|
||||
surfaceAnchorOcclusion: true,
|
||||
getPosition: (item) => ({
|
||||
latitude: item.latitude,
|
||||
longitude: item.longitude,
|
||||
@@ -905,6 +907,16 @@ function createOverlaySprite({ color, opacity, scale }) {
|
||||
return sprite;
|
||||
}
|
||||
|
||||
function updateEventHaloVisibility(camera) {
|
||||
const earth = bgpEventOverlayGroup.parent?.parent;
|
||||
if (!earth || !camera || bgpEventOverlayGroup.children.length === 0) return;
|
||||
bgpCameraLocalScratch.copy(camera.position);
|
||||
earth.worldToLocal(bgpCameraLocalScratch);
|
||||
bgpEventOverlayGroup.children.forEach((halo) => {
|
||||
halo.visible = isSurfaceAnchorVisible(halo.position, bgpCameraLocalScratch, CONFIG.earthRadius);
|
||||
});
|
||||
}
|
||||
|
||||
function projectLatLon(lat, lon, bearingDeg, distanceDeg) {
|
||||
const latRad = (lat * Math.PI) / 180;
|
||||
const lonRad = (lon * Math.PI) / 180;
|
||||
@@ -1361,6 +1373,7 @@ export async function loadBGPAnomalies(scene, earth) {
|
||||
|
||||
export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cruiseMarker = null) {
|
||||
const now = performance.now();
|
||||
updateEventHaloVisibility(camera);
|
||||
updateCollectorOverlayScan(lockedObjectType, lockedObject);
|
||||
syncBGPLegendEffects();
|
||||
const hasLockedLayer = Boolean(
|
||||
@@ -1686,8 +1699,6 @@ export function showBGPEventOverlay(marker, earth) {
|
||||
typeof region?.longitude === "number",
|
||||
);
|
||||
if (validRegions.length === 0) return;
|
||||
const overlayItems = [];
|
||||
|
||||
validRegions.forEach((region) => {
|
||||
const eventBaseColor = marker.userData.baseColor || getSeverityColor(marker.userData.severity);
|
||||
const halo = createOverlaySprite({
|
||||
@@ -1695,19 +1706,19 @@ export function showBGPEventOverlay(marker, earth) {
|
||||
opacity: 0.24,
|
||||
scale: BGP_CONFIG.regionScale,
|
||||
});
|
||||
halo.position.copy(
|
||||
latLonToVector3(
|
||||
region.latitude,
|
||||
region.longitude,
|
||||
CONFIG.earthRadius + BGP_COLLECTOR_ALTITUDE_OFFSET - 0.1,
|
||||
),
|
||||
const anchor = latLonToVector3(
|
||||
region.latitude,
|
||||
region.longitude,
|
||||
CONFIG.earthRadius + BGP_CONFIG.altitudeOffset,
|
||||
);
|
||||
halo.renderOrder = 2;
|
||||
halo.position.copy(anchor);
|
||||
// Like marker badges, a visible anchor owns the entire billboard. Per-pixel
|
||||
// Earth depth would cut this camera-facing gradient through the surface.
|
||||
halo.material.depthTest = false;
|
||||
halo.renderOrder = BGP_EVENT_RENDER_ORDER;
|
||||
bgpEventOverlayGroup.add(halo);
|
||||
overlayItems.push(halo);
|
||||
});
|
||||
|
||||
activeEventOverlay = overlayItems;
|
||||
activeCollectorOverlayContext = null;
|
||||
bgpOverlayGroup.visible = showBGP;
|
||||
}
|
||||
@@ -1767,7 +1778,6 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
innerRing.renderOrder = 3;
|
||||
bgpCollectorRadarGroup.add(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);
|
||||
@@ -1823,7 +1833,6 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
);
|
||||
sectorFill.renderOrder = 2;
|
||||
bgpCollectorRadarGroup.add(sectorFill);
|
||||
overlayItems.push(sectorFill);
|
||||
|
||||
const outerArc = createCoverageBoundaryLine(
|
||||
outerArcPoints,
|
||||
@@ -1832,7 +1841,6 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
);
|
||||
outerArc.renderOrder = 3;
|
||||
bgpCollectorRadarGroup.add(outerArc);
|
||||
overlayItems.push(outerArc);
|
||||
|
||||
const leftBoundary = createCoverageBoundaryLine(
|
||||
leftBoundaryPoints,
|
||||
@@ -1841,7 +1849,6 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
);
|
||||
leftBoundary.renderOrder = 3;
|
||||
bgpCollectorRadarGroup.add(leftBoundary);
|
||||
overlayItems.push(leftBoundary);
|
||||
|
||||
const rightBoundary = createCoverageBoundaryLine(
|
||||
rightBoundaryPoints,
|
||||
@@ -1850,9 +1857,7 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
);
|
||||
rightBoundary.renderOrder = 3;
|
||||
bgpCollectorRadarGroup.add(rightBoundary);
|
||||
overlayItems.push(rightBoundary);
|
||||
|
||||
activeEventOverlay = overlayItems;
|
||||
activeCollectorOverlayContext = {
|
||||
marker,
|
||||
earth,
|
||||
@@ -1863,7 +1868,6 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
}
|
||||
|
||||
export function clearBGPEventOverlay() {
|
||||
activeEventOverlay = null;
|
||||
activeCollectorOverlayContext = null;
|
||||
clearGroup(bgpEventOverlayGroup);
|
||||
clearGroup(bgpCollectorRadarGroup);
|
||||
|
||||
@@ -490,6 +490,7 @@ export function createInteractableLayer(options = {}) {
|
||||
avoidance = false,
|
||||
cluster = undefined,
|
||||
icon,
|
||||
surfaceAnchorOcclusion = false,
|
||||
getPosition,
|
||||
getKind = (item) => item?.type || "default",
|
||||
getRotationBin = () => 0,
|
||||
@@ -548,7 +549,9 @@ export function createInteractableLayer(options = {}) {
|
||||
const hoverScale = stateScale.hover ?? 1;
|
||||
const lockedScale = stateScale.locked ?? 1;
|
||||
const dimmedScale = stateScale.dimmed ?? 1;
|
||||
const usesSurfaceAnchorOcclusion = Boolean(icon.badge);
|
||||
// Rounded badges enable this implicitly, while other icon styles can opt
|
||||
// into the same whole-billboard surface behavior without changing artwork.
|
||||
const usesSurfaceAnchorOcclusion = Boolean(surfaceAnchorOcclusion || icon.badge);
|
||||
const depthTest = usesSurfaceAnchorOcclusion ? false : material.depthTest ?? true;
|
||||
const depthWrite = material.depthWrite ?? false;
|
||||
const alphaTest = material.alphaTest ?? 0.01;
|
||||
|
||||
@@ -27,3 +27,9 @@ export function pickNearestInteractiveHit(groups = []) {
|
||||
);
|
||||
return candidates[0] || null;
|
||||
}
|
||||
|
||||
export function pickNearestPointCandidate(candidates = []) {
|
||||
return candidates
|
||||
.filter(candidate => candidate?.object && Number.isFinite(candidate.distancePxSq))
|
||||
.sort((left, right) => left.distancePxSq - right.distancePxSq)[0] || null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { pickNearestInteractiveHit } from "./interactive-picking.js";
|
||||
import { pickNearestInteractiveHit, pickNearestPointCandidate } from "./interactive-picking.js";
|
||||
|
||||
const hit = (object, distancePxSq, cluster = false) => ({ object, distancePxSq, cluster });
|
||||
|
||||
@@ -23,3 +23,10 @@ test("cluster hits are ignored while pointer eligibility remains true for real m
|
||||
{ type: "bgp_collector", hits: [hit("rrc22", 9)] },
|
||||
]).object).toBe("rrc22");
|
||||
});
|
||||
|
||||
test("nearest point candidate is resolved before the line fallback", () => {
|
||||
expect(pickNearestPointCandidate([
|
||||
{ type: "compute_center", object: "compute", distancePxSq: 16 },
|
||||
{ type: "satellite", object: "satellite", distancePxSq: 0 },
|
||||
]).type).toBe("satellite");
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { applyBGPLegendFilter } from "./bgp.js";
|
||||
import { applyCountryBoundaryLegendFilter } from "./country-boundaries.js";
|
||||
import { isSatelliteCategoryVisible, updateSatelliteLabels } from "./satellites.js";
|
||||
import { beginFrameStats, finishFrameStats } from "./frame-stats.js";
|
||||
import { pickNearestInteractiveHit } from "./interactive-picking.js";
|
||||
import { pickNearestInteractiveHit, pickNearestPointCandidate } from "./interactive-picking.js";
|
||||
import { createScreenSpaceDrag, globeDragRotationFactor } from "./globe-navigation.js";
|
||||
import * as THREE from "three";
|
||||
|
||||
@@ -5816,23 +5816,33 @@ function onMouseMove(event) {
|
||||
vesselIntersects,
|
||||
earthInteractableIntersects,
|
||||
});
|
||||
const satelliteCandidate = satIntersects.length > 0
|
||||
? {
|
||||
type: "satellite",
|
||||
index: satIntersects[0].index,
|
||||
object: getSatelliteAt(satIntersects[0].index),
|
||||
screen: { x: event.clientX, y: event.clientY },
|
||||
distancePxSq: satIntersects[0].distanceSq,
|
||||
}
|
||||
: null;
|
||||
const pointPick = pickNearestPointCandidate([interactivePick, satelliteCandidate]);
|
||||
if (renderer?.domElement) {
|
||||
renderer.domElement.style.cursor =
|
||||
interactivePick || satIntersects?.length || cableIntersects?.length
|
||||
pointPick || cableIntersects?.length
|
||||
? "pointer"
|
||||
: "default";
|
||||
}
|
||||
|
||||
let hoveredSat = null;
|
||||
let hoveredSatIndexFromIntersect = null;
|
||||
if (satIntersects.length > 0) {
|
||||
const satIndex = satIntersects[0].index;
|
||||
if (pointPick?.type === "satellite") {
|
||||
const satIndex = pointPick.index;
|
||||
hoveredSatIndexFromIntersect = satIndex;
|
||||
hoveredSat = selectSatellite(satIndex);
|
||||
}
|
||||
|
||||
const hoveredBGPMarker =
|
||||
interactivePick?.type === "bgp" || interactivePick?.type === "bgp_collector"
|
||||
pointPick?.type === "bgp" || pointPick?.type === "bgp_collector"
|
||||
? getPrimaryBGPHoverTarget(bgpAnomalyIntersects, bgpCollectorIntersects)
|
||||
: null;
|
||||
|
||||
@@ -5842,11 +5852,11 @@ function onMouseMove(event) {
|
||||
}
|
||||
|
||||
const hoveredComputeCenterMarker =
|
||||
interactivePick?.type === "compute_center" ? interactivePick.object : null;
|
||||
pointPick?.type === "compute_center" ? pointPick.object : null;
|
||||
const hoveredVesselMarker =
|
||||
interactivePick?.type === "vessel" && vesselPick.checked ? interactivePick.object : null;
|
||||
pointPick?.type === "vessel" && vesselPick.checked ? pointPick.object : null;
|
||||
const hoveredEarthInteractableMarker =
|
||||
interactivePick?.type === "earth_interactable" ? interactivePick.object : null;
|
||||
pointPick?.type === "earth_interactable" ? pointPick.object : null;
|
||||
const earthPoint = screenToEarthCoords(
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
@@ -5962,15 +5972,7 @@ function onMouseMove(event) {
|
||||
getEarthInteractableBriefHtml(hoveredEarthInteractableMarker),
|
||||
);
|
||||
objectTooltipShown = true;
|
||||
} else if (cableIntersects.length > 0 && getShowCables()) {
|
||||
const cable = cableIntersects[0].object;
|
||||
hoveredCable = cable;
|
||||
if (!isSameCable(cable, lockedObject)) {
|
||||
setCableState(cable.userData.cableId, CABLE_STATE.HOVERED);
|
||||
}
|
||||
showTooltip(event.clientX + TOOLTIP_CURSOR_OFFSET, event.clientY + TOOLTIP_CURSOR_OFFSET, getCableBriefHtml(cable));
|
||||
objectTooltipShown = true;
|
||||
} else if (hoveredSat?.properties) {
|
||||
} else if (hoveredSat?.properties && pointPick?.type === "satellite") {
|
||||
hoveredSatellite = hoveredSat;
|
||||
hoveredSatelliteIndex = hoveredSatIndexFromIntersect;
|
||||
if (hoveredSatelliteIndex !== lockedSatelliteIndex) {
|
||||
@@ -5986,6 +5988,14 @@ function onMouseMove(event) {
|
||||
setHoveredSatelliteIndex(hoveredSatelliteIndex);
|
||||
showTooltip(event.clientX + TOOLTIP_CURSOR_OFFSET, event.clientY + TOOLTIP_CURSOR_OFFSET, getSatelliteBriefHtml(hoveredSat.properties));
|
||||
objectTooltipShown = true;
|
||||
} else if (cableIntersects.length > 0 && getShowCables()) {
|
||||
const cable = cableIntersects[0].object;
|
||||
hoveredCable = cable;
|
||||
if (!isSameCable(cable, lockedObject)) {
|
||||
setCableState(cable.userData.cableId, CABLE_STATE.HOVERED);
|
||||
}
|
||||
showTooltip(event.clientX + TOOLTIP_CURSOR_OFFSET, event.clientY + TOOLTIP_CURSOR_OFFSET, getCableBriefHtml(cable));
|
||||
objectTooltipShown = true;
|
||||
} else if (lockedObjectType === "bgp" && lockedObject) {
|
||||
applyBGPHoverState(lockedObject);
|
||||
} else if (lockedObjectType === "bgp_collector" && lockedObject) {
|
||||
@@ -6239,6 +6249,23 @@ function onClick(event) {
|
||||
vesselIntersects,
|
||||
earthInteractableIntersects,
|
||||
});
|
||||
const satelliteCandidate = satIntersects.length > 0
|
||||
? {
|
||||
type: "satellite",
|
||||
index: satIntersects[0].index,
|
||||
object: getSatelliteAt(satIntersects[0].index),
|
||||
screen: { x: event.clientX, y: event.clientY },
|
||||
distancePxSq: satIntersects[0].distanceSq,
|
||||
}
|
||||
: null;
|
||||
const pointPick = pickNearestPointCandidate([interactivePick, satelliteCandidate]);
|
||||
|
||||
if (pointPick?.type === "satellite") {
|
||||
if (confirmInteractionCandidate(pointPick, { showConfirmationFeedback: false })) {
|
||||
showSelectionStatus(getInteractionCandidateLabel(pointPick));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (interactivePick) {
|
||||
let marker = interactivePick.object;
|
||||
@@ -6272,21 +6299,6 @@ function onClick(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (satIntersects.length > 0) {
|
||||
const hit = satIntersects[0];
|
||||
const candidate = {
|
||||
type: "satellite",
|
||||
index: hit.index,
|
||||
object: getSatelliteAt(hit.index),
|
||||
screen: { x: event.clientX, y: event.clientY },
|
||||
distancePxSq: hit.distancePxSq,
|
||||
};
|
||||
if (confirmInteractionCandidate(candidate, { showConfirmationFeedback: false })) {
|
||||
showSelectionStatus(getInteractionCandidateLabel(candidate));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCruiseModeActive()) interruptCruisePresentation({ resetLoop: true });
|
||||
clearLockedObject();
|
||||
hideInfoCard();
|
||||
|
||||
@@ -82,6 +82,25 @@ test('projected BGP symbols bake the same category color as GPU vertex colors',
|
||||
} finally {layer?.clearData();globalThis.document=oldDocument;globalThis.window=oldWindow;}
|
||||
});
|
||||
|
||||
test('surface anchor occlusion can preserve a custom icon drawing style', () => {
|
||||
const oldDocument = globalThis.document, oldWindow = globalThis.window;
|
||||
globalThis.window = { innerWidth: 1280, innerHeight: 720, devicePixelRatio: 1 };
|
||||
globalThis.document = { createElement: () => ({ width: 128, height: 128, getContext: () => new Proxy({}, { get: () => () => {} }) }) };
|
||||
let layer;
|
||||
try {
|
||||
layer = createInteractableLayer({ id: 'surface-anchor-style-test', cluster: false,
|
||||
surfaceAnchorOcclusion: true, getPosition: item => item,
|
||||
icon: { draw() {} } });
|
||||
const earth = new THREE.Object3D(), camera = new THREE.PerspectiveCamera(75, 1, .1, 5000);
|
||||
camera.position.z = 350; camera.updateMatrixWorld();
|
||||
layer.attach(earth); layer.setVisible(true); layer.setData([{ id: 'custom', latitude: 0, longitude: -90 }]);
|
||||
layer.updateVisualState(null, null, camera);
|
||||
const points = layer.group.children.find(group => group.children.some(child => child.isPoints)).children.find(child => child.isPoints);
|
||||
expect(points.material.depthTest).toBe(false);
|
||||
expect(points.material.defines.USE_SURFACE_ANCHOR_OCCLUSION).toBe('');
|
||||
} finally { layer?.clearData(); globalThis.document = oldDocument; globalThis.window = oldWindow; }
|
||||
});
|
||||
|
||||
|
||||
test('selected pulses do not refresh unchanged batches; state changes still do', () => {
|
||||
const oldDocument=globalThis.document,oldWindow=globalThis.window;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "planet"
|
||||
version = "0.76.0"
|
||||
version = "0.76.2"
|
||||
description = "智能星球计划 - 态势感知系统"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user