release: bump version to 0.31.3
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -20,3 +20,5 @@
|
||||
- [x] 在 activity layer 之后继续补 `route leak` 和 `path instability / flap` detector
|
||||
- [ ] 对 [frontend/public/earth/js/bgp.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp.js) 做按职责拆分的小重构,拆成 data / markers / overlays / animation,降低后续维护复杂度
|
||||
- [ ] 可选优化(非必做):将 BGP incident/collector 标点改为 HTML marker(参考 worldmonitor 的 `htmlElementsData` 思路),实现近乎固定屏幕尺寸与更高密度可点击性
|
||||
- [ ] 保持 Earth 当前这批纯个人偏好设置继续走本地持久化:`旋转模式`、HUD 面板显示/隐藏、`地形透明度` 暂不升级到后端系统设置,避免把设备级偏好过早做成全局配置
|
||||
- [ ] 如果后续明确需要“账号级同步 Earth 偏好”,再单独设计 `Earth user preferences`:优先按用户维度而不是全局系统设置保存,并规划 `localStorage -> backend` 的平滑迁移策略
|
||||
|
||||
@@ -8,6 +8,27 @@ This project follows the repository versioning rule:
|
||||
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
||||
- `bugfix` -> `+0.0.1`
|
||||
|
||||
## [0.31.3] — 2026-04-22
|
||||
|
||||
### ✨ Highlights
|
||||
- Earth 图层注册表和启动任务框架继续收口,启动顺序、启动模式、启动提示和任务注册现在都能从统一入口扩展
|
||||
- 修复 Earth 普通旋转模式与巡航模式切换时的一组交互回归,同时让卫星/地形/昼夜模式的表现更稳定
|
||||
|
||||
### 🔧 Improvements
|
||||
- 新增 [layer-startup-tasks.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/layer-startup-tasks.js) 启动任务注册表,支持 `registerLayerStartupTask(id, taskFactory)`,并拆成海缆 / 卫星 / BGP 独立注册函数
|
||||
- Earth 图层控制改成注册表驱动,统一承载 `startupPriority`、`startupMode`、`startupLabel`、`startupMessage` 与图层持久化元信息
|
||||
- Earth 设置支持持久化图层开关、旋转模式、HUD 面板显示状态、地形透明度与日夜模式,并提供一键重置
|
||||
- 更新 [earth-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/earth-frontend-context.md) 记录图层注册表、启动任务、设置持久化与巡航适配边界
|
||||
|
||||
### 🐛 Fixes
|
||||
- 修复普通旋转模式下点击海缆 / 卫星 / BGP 后卡片和选中表现会被异常清空的问题
|
||||
- 修复巡航模式切回旋转再切回巡航后无法继续自动巡航的问题
|
||||
- 修复开启地形后卫星选中反馈层被高海拔区域吞掉的问题,并恢复轨道只在地球前半侧可见
|
||||
- 修复关闭日夜模式后地球照明仍沿真实昼夜切换、亮部过曝和偏色的问题,改成更中性的 inspection lighting
|
||||
- 修复 toolbar 收起态仍挡住地球交互,以及首帧短暂展开闪现的问题
|
||||
|
||||
---
|
||||
|
||||
## [0.31.0] — 2026-04-21
|
||||
|
||||
## [0.31.2] — 2026-04-21
|
||||
|
||||
@@ -67,6 +67,7 @@ React 路由入口:
|
||||
- 旋转/缩放/布局
|
||||
- HUD 面板拖拽
|
||||
- 图层开关状态机
|
||||
- Earth 设置读取、持久化与重置
|
||||
|
||||
这份文件是 Earth 前端当前最核心的 UI 控制入口。
|
||||
|
||||
@@ -99,6 +100,7 @@ React 路由入口:
|
||||
- [bgp-cruise-adapter.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp-cruise-adapter.js)
|
||||
- [news.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/news.js)
|
||||
- [tv.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/tv.js)
|
||||
- [layer-startup-tasks.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/layer-startup-tasks.js)
|
||||
- [cruise-sequencer.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/cruise-sequencer.js)
|
||||
- [callout-connector.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/callout-connector.js)
|
||||
|
||||
@@ -109,6 +111,16 @@ React 路由入口:
|
||||
- 面板内容
|
||||
- hover/lock/selection 语义
|
||||
|
||||
其中 Earth 启动加载链现在也拆成了两层:
|
||||
|
||||
- `controls.js`
|
||||
- 提供图层注册表与启动元信息
|
||||
- `layer-startup-tasks.js`
|
||||
- 提供图层启动任务注册表
|
||||
- 通过 `registerLayerStartupTask(id, taskFactory)` 扩展启动任务
|
||||
- `main.js`
|
||||
- 只负责读取排序后的启动图层,再按映射执行队列
|
||||
|
||||
其中巡航模式现在已经拆成两层:
|
||||
|
||||
- `cruise-sequencer.js`
|
||||
@@ -169,6 +181,57 @@ Earth 图层按钮现在不应再只有“开/关”两态,而应支持:
|
||||
|
||||
因此后续如果别的图层也需要异步启用,应该直接走这套状态机,而不是再手写一套临时 loading class。
|
||||
|
||||
另外,Earth 图层控制现在已经收成“注册表驱动”:
|
||||
|
||||
- 图层元数据
|
||||
- `id`
|
||||
- `icon`
|
||||
- `label`
|
||||
- `meta`
|
||||
- `buttonId`
|
||||
- `persist`
|
||||
- `startupPriority`
|
||||
- `startupMode`
|
||||
- `startupLabel`
|
||||
- `startupMessage`
|
||||
- 图层行为
|
||||
- `getVisible()`
|
||||
- `setVisible(next, options)`
|
||||
|
||||
当前入口仍在 [controls.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js)。
|
||||
|
||||
这意味着后续新增图层时,优先应补一条图层注册定义,而不是同时去改:
|
||||
|
||||
- 图层面板 HTML
|
||||
- 持久化快照
|
||||
- 初始化恢复
|
||||
- click 绑定
|
||||
|
||||
这四处现在都应该由注册表派生。
|
||||
|
||||
其中:
|
||||
|
||||
- `startupPriority`
|
||||
- 描述图层参与启动加载时的顺序
|
||||
- `startupMode`
|
||||
- `visible`
|
||||
- 仅当前图层处于启用/可见状态时,才加入启动加载队列
|
||||
- `preload`
|
||||
- 即使当前图层未显示,也会参与启动预加载
|
||||
|
||||
当前 `main.js` 会通过注册表读取排序后的启动图层列表,再动态拼装启动加载队列,而不是手写一串固定步骤。像 BGP 这类需要尽早准备数据、但不一定默认显示的图层,应该优先走 `startupMode: "preload"`,而不是在启动流程里写隐式特判。
|
||||
|
||||
此外,启动阶段给用户看的提示文案也应尽量从注册表派生:
|
||||
|
||||
- `startupLabel`
|
||||
- 用于描述当前启动任务的业务名称
|
||||
- `startupMessage`
|
||||
- 用于描述启动中的提示文案
|
||||
- 可以是字符串
|
||||
- 也可以是对象,用于像海缆这种“准备阶段 / 主加载阶段”两段式文案
|
||||
|
||||
这样后续新增会参与启动加载的图层时,顺序、模式和提示文案都在同一处定义,不需要再去 `main.js` 里补第二套常量。
|
||||
|
||||
### `data-status-target`
|
||||
|
||||
图层按钮可以通过:
|
||||
@@ -182,6 +245,25 @@ Earth 图层按钮现在不应再只有“开/关”两态,而应支持:
|
||||
|
||||
以后别的异步图层也可以沿用这套约定。
|
||||
|
||||
## 当前设置持久化
|
||||
|
||||
Earth 设置面板当前由 [controls.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js) 统一负责:
|
||||
|
||||
- 捕获默认值
|
||||
- 从 `localStorage` 读取上次设置
|
||||
- 初始化应用当前设置
|
||||
- 用户变更后即时持久化
|
||||
- 一键重置回默认值
|
||||
|
||||
当前持久化的范围是:
|
||||
|
||||
- 旋转模式
|
||||
- HUD 面板显示/隐藏
|
||||
- 图层控制开关:`地形 / 卫星 / 轨迹 / 海缆 / BGP`
|
||||
- 地形透明度
|
||||
|
||||
也就是说,Earth 设置不是一次性 UI 状态了,而是本地设备级偏好。后续如果再加入新的设置项,应优先接入同一条持久化链,而不是各自散着写 `localStorage`。
|
||||
|
||||
## 当前地形链路
|
||||
|
||||
真实地形首次启用会慢,原因不只是一个:
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
## Current Version
|
||||
|
||||
- `main` 当前主线历史推导到:`0.16.5`
|
||||
- `dev` 当前开发分支历史推导到:`0.31.2`
|
||||
- `dev` 当前开发分支历史推导到:`0.31.3`
|
||||
|
||||
## Timeline
|
||||
|
||||
| Version | Type | Branch | Commit | Summary |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `0.31.3` | bugfix | `dev` | `pending` | 收口 Earth 图层注册表与启动任务框架,修复旋转/巡航切换、卫星地形遮挡与日夜关闭照明回归 |
|
||||
| `0.31.2` | bugfix | `dev` | `pending` | 将 Earth 巡航模式拆成通用 sequencer、通用连线和 BGP 巡航适配层,并修复空白点击推进与连线动画回归 |
|
||||
| `0.31.1` | bugfix | `dev` | `pending` | Earth 图层开关统一 loading 状态机,卫星首次加载可见化,并将文档按 technical / plans / deprecated 重构归档 |
|
||||
| `0.31.0` | feature | `dev` | `pending` | Earth 巡航展示模式:自动轮播 BGP 事件,连线逐帧追踪,卫星/海缆联动高亮,视觉状态全面统一 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.31.2",
|
||||
"version": "0.31.3",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"dependencies": {
|
||||
|
||||
@@ -538,7 +538,7 @@
|
||||
|
||||
.earth-settings-kicker {
|
||||
color: var(--hud-text-soft);
|
||||
font-size: var(--hud-panel-header-title-size);
|
||||
font-size: calc(0.72rem * var(--hud-scale));
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -548,6 +548,22 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.earth-settings-reset {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: auto;
|
||||
padding-inline: calc(10px * var(--hud-scale));
|
||||
color: var(--hud-text-soft);
|
||||
font-size: calc(0.68rem * var(--hud-scale));
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.earth-settings-reset .material-symbols-rounded {
|
||||
font-size: calc(0.86rem * var(--hud-scale));
|
||||
}
|
||||
|
||||
.earth-settings-content {
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
@@ -575,7 +591,7 @@
|
||||
.earth-settings-section-title {
|
||||
margin-bottom: 12px;
|
||||
color: var(--hud-text-soft);
|
||||
font-size: var(--hud-kicker-size);
|
||||
font-size: calc(0.62rem * var(--hud-scale));
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -590,9 +606,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 15px 16px;
|
||||
border-radius: 16px;
|
||||
gap: 12px;
|
||||
padding: 11px 14px;
|
||||
border-radius: 14px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent),
|
||||
rgba(255, 255, 255, 0.025);
|
||||
@@ -648,10 +664,10 @@
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hud-text-soft);
|
||||
padding: 8px 14px;
|
||||
padding: calc(5px * var(--hud-scale)) calc(10px * var(--hud-scale));
|
||||
border-radius: 999px;
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
font-size: calc(0.7rem * var(--hud-scale));
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
cursor: pointer;
|
||||
@@ -680,18 +696,23 @@
|
||||
.earth-settings-slider {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
height: calc(4px * var(--hud-scale));
|
||||
appearance: none;
|
||||
background: linear-gradient(90deg, rgba(132, 164, 204, 0.32), rgba(94, 130, 172, 0.5));
|
||||
border-radius: 999px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease;
|
||||
}
|
||||
|
||||
.earth-settings-slider:hover {
|
||||
background: linear-gradient(90deg, rgba(152, 184, 224, 0.44), rgba(114, 155, 202, 0.64));
|
||||
}
|
||||
|
||||
.earth-settings-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: calc(14px * var(--hud-scale));
|
||||
height: calc(14px * var(--hud-scale));
|
||||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.22) 55%, transparent 70%),
|
||||
@@ -699,26 +720,62 @@
|
||||
border: 1px solid rgba(222, 236, 252, 0.4);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06),
|
||||
0 6px 16px rgba(0, 0, 0, 0.24);
|
||||
0 4px 10px rgba(0, 0, 0, 0.24);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
|
||||
}
|
||||
|
||||
.earth-settings-slider:hover::-webkit-slider-thumb {
|
||||
transform: scale(1.22);
|
||||
background:
|
||||
radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.32) 55%, transparent 70%),
|
||||
linear-gradient(180deg, rgba(188, 216, 252, 1), rgba(108, 155, 210, 0.98));
|
||||
border-color: rgba(232, 244, 255, 0.72);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(145, 186, 255, 0.22),
|
||||
0 6px 14px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.earth-settings-slider:active::-webkit-slider-thumb {
|
||||
transform: scale(1.08);
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(145, 186, 255, 0.32),
|
||||
0 4px 10px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.earth-settings-slider::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: calc(14px * var(--hud-scale));
|
||||
height: calc(14px * var(--hud-scale));
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, rgba(164, 196, 236, 0.95), rgba(85, 127, 181, 0.92));
|
||||
border: 1px solid rgba(222, 236, 252, 0.4);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06),
|
||||
0 6px 16px rgba(0, 0, 0, 0.24);
|
||||
0 4px 10px rgba(0, 0, 0, 0.24);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.earth-settings-slider:hover::-moz-range-thumb {
|
||||
transform: scale(1.22);
|
||||
background: linear-gradient(180deg, rgba(188, 216, 252, 1), rgba(108, 155, 210, 0.98));
|
||||
border-color: rgba(232, 244, 255, 0.72);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(145, 186, 255, 0.22),
|
||||
0 6px 14px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.earth-settings-slider:active::-moz-range-thumb {
|
||||
transform: scale(1.08);
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(145, 186, 255, 0.32),
|
||||
0 4px 10px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.earth-settings-slider-value {
|
||||
flex: 0 0 auto;
|
||||
min-width: 46px;
|
||||
min-width: calc(34px * var(--hud-scale));
|
||||
text-align: right;
|
||||
color: var(--hud-text-soft);
|
||||
font-size: 0.78rem;
|
||||
font-size: calc(0.66rem * var(--hud-scale));
|
||||
letter-spacing: 0.04em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@@ -731,13 +788,13 @@
|
||||
|
||||
.earth-settings-item-title {
|
||||
color: var(--hud-text);
|
||||
font-size: calc(0.98rem * var(--hud-scale));
|
||||
font-size: calc(0.76rem * var(--hud-scale));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.earth-settings-item-subtitle {
|
||||
color: var(--hud-text-muted);
|
||||
font-size: 0.82rem;
|
||||
font-size: calc(0.67rem * var(--hud-scale));
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -750,11 +807,11 @@
|
||||
}
|
||||
|
||||
.earth-settings-link-meta .material-symbols-rounded:first-child {
|
||||
font-size: 1.1rem;
|
||||
font-size: calc(0.9rem * var(--hud-scale));
|
||||
}
|
||||
|
||||
.earth-settings-link-meta .material-symbols-rounded:last-child {
|
||||
font-size: 1rem;
|
||||
font-size: calc(0.82rem * var(--hud-scale));
|
||||
}
|
||||
|
||||
.earth-settings-switch {
|
||||
@@ -770,8 +827,8 @@
|
||||
}
|
||||
|
||||
.earth-settings-switch-track {
|
||||
width: 48px;
|
||||
height: 30px;
|
||||
width: calc(38px * var(--hud-scale));
|
||||
height: calc(22px * var(--hud-scale));
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(215, 229, 242, 0.12);
|
||||
@@ -782,10 +839,10 @@
|
||||
.earth-settings-switch-track::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
top: calc(3px * var(--hud-scale));
|
||||
left: calc(3px * var(--hud-scale));
|
||||
width: calc(16px * var(--hud-scale));
|
||||
height: calc(16px * var(--hud-scale));
|
||||
border-radius: 50%;
|
||||
background: #edf4fc;
|
||||
box-shadow: 0 6px 14px rgba(1, 8, 18, 0.26);
|
||||
@@ -798,7 +855,7 @@
|
||||
}
|
||||
|
||||
.earth-settings-switch input:checked + .earth-settings-switch-track::after {
|
||||
transform: translateX(18px);
|
||||
transform: translateX(calc(16px * var(--hud-scale)));
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.earth-toolbar-group,
|
||||
@@ -40,6 +41,7 @@
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.earth-toolbar-cluster {
|
||||
@@ -103,6 +105,14 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.earth-toolbar-cluster.is-collapsed .earth-toolbar-orb > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.earth-toolbar-hub > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.earth-toolbar-orb > .liquid-glass-surface {
|
||||
animation: floatDock 4.6s ease-in-out infinite;
|
||||
animation-delay: var(--orb-delay, 0s);
|
||||
@@ -120,7 +130,7 @@
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--hud-text-soft);
|
||||
font-size: 14px;
|
||||
font-size: calc(14px * var(--toolbar-scale));
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -361,7 +371,7 @@
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, 8px);
|
||||
transform: translate(-50%, calc(8px * var(--toolbar-scale)));
|
||||
}
|
||||
|
||||
.earth-toolbar-popover::before {
|
||||
@@ -381,7 +391,7 @@
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: calc(100% + (12px * var(--toolbar-scale)));
|
||||
transform: translate(-50%, 10px);
|
||||
transform: translate(-50%, calc(10px * var(--toolbar-scale)));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -398,16 +408,16 @@
|
||||
|
||||
.earth-zoom-toolbar .earth-zoom-btn,
|
||||
.earth-zoom-toolbar .earth-zoom-value {
|
||||
width: 42px;
|
||||
min-width: 42px;
|
||||
width: calc(42px * var(--toolbar-scale));
|
||||
min-width: calc(42px * var(--toolbar-scale));
|
||||
border-radius: 50%;
|
||||
color: var(--hud-text-soft);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.earth-zoom-toolbar .earth-zoom-btn {
|
||||
height: 42px;
|
||||
font-size: 20px;
|
||||
height: calc(42px * var(--toolbar-scale));
|
||||
font-size: calc(20px * var(--toolbar-scale));
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -416,9 +426,9 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 42px;
|
||||
height: calc(42px * var(--toolbar-scale));
|
||||
padding: 0;
|
||||
font-size: 0.68rem;
|
||||
font-size: calc(11px * var(--toolbar-scale));
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
@@ -429,15 +439,15 @@
|
||||
|
||||
.earth-toolbar-btn .earth-toolbar-tooltip {
|
||||
position: absolute;
|
||||
bottom: 56px;
|
||||
bottom: calc(56px * var(--toolbar-scale));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(18, 31, 52, 0.96), rgba(8, 18, 32, 0.95));
|
||||
color: var(--hud-text);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
padding: calc(6px * var(--toolbar-scale)) calc(12px * var(--toolbar-scale));
|
||||
border-radius: calc(6px * var(--toolbar-scale));
|
||||
font-size: calc(12px * var(--toolbar-scale));
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
@@ -453,7 +463,7 @@
|
||||
.earth-toolbar-popover:focus-within > .earth-toolbar-btn .earth-toolbar-tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
bottom: 58px;
|
||||
bottom: calc(58px * var(--toolbar-scale));
|
||||
}
|
||||
|
||||
.earth-toolbar-btn .earth-toolbar-tooltip::after {
|
||||
@@ -462,6 +472,6 @@
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 6px solid transparent;
|
||||
border: calc(6px * var(--toolbar-scale)) solid transparent;
|
||||
border-top-color: rgba(18, 31, 52, 0.96);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
|
||||
<div id="right-toolbar-group" class="earth-toolbar-group">
|
||||
<div id="control-toolbar" class="earth-toolbar">
|
||||
<div id="toolbar-cluster" class="earth-toolbar-cluster is-expanded">
|
||||
<div id="toolbar-cluster" class="earth-toolbar-cluster is-collapsed">
|
||||
<div class="earth-toolbar-orb" data-orb-index="0" style="--orb-delay: 0s;">
|
||||
<button id="search-action" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="搜索功能(待开发)">
|
||||
<span class="icon" aria-hidden="true">
|
||||
@@ -419,6 +419,10 @@
|
||||
<div class="hud-panel__title-group">
|
||||
<div class="earth-settings-kicker">设置</div>
|
||||
</div>
|
||||
<button id="settings-reset" class="earth-settings-reset hud-panel__action" type="button" aria-label="重置设置">
|
||||
<span class="material-symbols-rounded">restart_alt</span>
|
||||
<span>重置</span>
|
||||
</button>
|
||||
<button id="settings-close" class="earth-settings-close hud-panel__action hud-panel__action--close" type="button" aria-label="关闭设置">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
@@ -456,6 +460,16 @@
|
||||
<section class="earth-settings-section">
|
||||
<div class="earth-settings-section-title">视图</div>
|
||||
<div class="earth-settings-list">
|
||||
<label class="earth-settings-item" for="toggle-daynight">
|
||||
<div class="earth-settings-copy">
|
||||
<span class="earth-settings-item-title">日夜模式</span>
|
||||
<span class="earth-settings-item-subtitle">按真实太阳位置区分地球昼夜明暗,关闭后全球均匀照亮</span>
|
||||
</div>
|
||||
<span class="earth-settings-switch">
|
||||
<input id="toggle-daynight" type="checkbox" checked>
|
||||
<span class="earth-settings-switch-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="earth-settings-item" for="toggle-view-layers">
|
||||
<div class="earth-settings-copy">
|
||||
<span class="earth-settings-item-title">图层控制</span>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as THREE from "three";
|
||||
import * as Astronomy from "astronomy-engine";
|
||||
|
||||
import { CELESTIAL_CONFIG, EARTH_CONFIG } from "./constants.js";
|
||||
import {
|
||||
CELESTIAL_CONFIG,
|
||||
EARTH_CONFIG,
|
||||
SCENE_LIGHT_CONFIG,
|
||||
} from "./constants.js";
|
||||
import { latLonToVector3 } from "./utils.js";
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
@@ -21,7 +25,11 @@ let moonDirection = defaultMoonDirection.clone();
|
||||
let lastUpdatedAt = 0;
|
||||
let linkedSunLight = null;
|
||||
let linkedBackLight = null;
|
||||
let linkedAmbientLight = null;
|
||||
let linkedPointLight = null;
|
||||
let linkedCamera = null;
|
||||
let linkedEarth = null;
|
||||
let dayNightLightingEnabled = true;
|
||||
let brightStarSprites = [];
|
||||
let celestialRotationQuaternion = new THREE.Quaternion();
|
||||
let celestialViewQuaternion = new THREE.Quaternion();
|
||||
@@ -333,10 +341,71 @@ function updateSpritePositions() {
|
||||
}
|
||||
|
||||
function updateLighting() {
|
||||
if (!dayNightLightingEnabled && CELESTIAL_CONFIG.inspectionLighting?.enabled) {
|
||||
const inspection = CELESTIAL_CONFIG.inspectionLighting;
|
||||
const cameraDirection = linkedCamera
|
||||
? linkedCamera.position.clone().normalize()
|
||||
: defaultSunDirection.clone();
|
||||
const worldUp = new THREE.Vector3(0, 1, 0);
|
||||
const right = new THREE.Vector3().crossVectors(worldUp, cameraDirection);
|
||||
if (right.lengthSq() < 1e-6) {
|
||||
right.set(1, 0, 0);
|
||||
} else {
|
||||
right.normalize();
|
||||
}
|
||||
const adjustedUp = new THREE.Vector3()
|
||||
.crossVectors(cameraDirection, right)
|
||||
.normalize();
|
||||
|
||||
const resolveInspectionDirection = (offset) =>
|
||||
cameraDirection
|
||||
.clone()
|
||||
.multiplyScalar(offset.z)
|
||||
.add(right.clone().multiplyScalar(offset.x))
|
||||
.add(adjustedUp.clone().multiplyScalar(offset.y))
|
||||
.normalize();
|
||||
|
||||
if (linkedAmbientLight) {
|
||||
linkedAmbientLight.color.setHex(inspection.ambientColor);
|
||||
linkedAmbientLight.intensity = inspection.ambientIntensity;
|
||||
}
|
||||
|
||||
if (linkedSunLight) {
|
||||
linkedSunLight.color.setHex(inspection.keyLightColor);
|
||||
linkedSunLight.intensity = inspection.keyLightIntensity;
|
||||
linkedSunLight.position
|
||||
.copy(resolveInspectionDirection(inspection.keyLightOffset))
|
||||
.multiplyScalar(inspection.keyLightDistance);
|
||||
}
|
||||
|
||||
if (linkedBackLight) {
|
||||
linkedBackLight.color.setHex(inspection.backLightColor);
|
||||
linkedBackLight.intensity = inspection.backLightIntensity;
|
||||
linkedBackLight.position
|
||||
.copy(resolveInspectionDirection(inspection.backLightOffset))
|
||||
.multiplyScalar(inspection.backLightDistance);
|
||||
}
|
||||
|
||||
if (linkedPointLight) {
|
||||
linkedPointLight.color.setHex(inspection.pointLightColor);
|
||||
linkedPointLight.intensity = inspection.pointLightIntensity;
|
||||
linkedPointLight.position
|
||||
.copy(resolveInspectionDirection(inspection.pointLightOffset))
|
||||
.multiplyScalar(inspection.pointLightDistance);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const physicalSunDirection = getPhysicalSunDirection(
|
||||
new Date(lastUpdatedAt || Date.now()),
|
||||
);
|
||||
|
||||
if (linkedAmbientLight) {
|
||||
linkedAmbientLight.color.setHex(SCENE_LIGHT_CONFIG.ambient.color);
|
||||
linkedAmbientLight.intensity = SCENE_LIGHT_CONFIG.ambient.intensity;
|
||||
}
|
||||
|
||||
if (linkedSunLight) {
|
||||
linkedSunLight.color.setHex(CELESTIAL_CONFIG.sunLightColor);
|
||||
linkedSunLight.intensity = CELESTIAL_CONFIG.sunLightIntensity;
|
||||
@@ -352,6 +421,16 @@ function updateLighting() {
|
||||
.copy(physicalSunDirection)
|
||||
.multiplyScalar(-CELESTIAL_CONFIG.sunLightDistance * 0.7);
|
||||
}
|
||||
|
||||
if (linkedPointLight) {
|
||||
linkedPointLight.color.setHex(SCENE_LIGHT_CONFIG.point.color);
|
||||
linkedPointLight.intensity = SCENE_LIGHT_CONFIG.point.intensity;
|
||||
linkedPointLight.position.set(
|
||||
SCENE_LIGHT_CONFIG.point.position.x,
|
||||
SCENE_LIGHT_CONFIG.point.position.y,
|
||||
SCENE_LIGHT_CONFIG.point.position.z,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function computeCelestialState(date = new Date()) {
|
||||
@@ -364,14 +443,24 @@ function computeCelestialState(date = new Date()) {
|
||||
|
||||
export function initCelestialLayer(
|
||||
scene,
|
||||
{ camera = null, sunLight = null, backLight = null, earth = null } = {},
|
||||
{
|
||||
camera = null,
|
||||
sunLight = null,
|
||||
backLight = null,
|
||||
ambientLight = null,
|
||||
pointLight = null,
|
||||
earth = null,
|
||||
} = {},
|
||||
) {
|
||||
if (!scene || !CELESTIAL_CONFIG.enabled) return null;
|
||||
|
||||
disposeCelestialLayer();
|
||||
|
||||
linkedCamera = camera;
|
||||
linkedSunLight = sunLight;
|
||||
linkedBackLight = backLight;
|
||||
linkedAmbientLight = ambientLight;
|
||||
linkedPointLight = pointLight;
|
||||
linkedEarth = earth;
|
||||
|
||||
celestialRoot = new THREE.Group();
|
||||
@@ -457,6 +546,10 @@ export function initCelestialLayer(
|
||||
export function updateCelestialLayer(date = new Date(), camera = null) {
|
||||
if (!celestialRoot) return;
|
||||
|
||||
if (camera) {
|
||||
linkedCamera = camera;
|
||||
}
|
||||
|
||||
refreshCelestialView();
|
||||
|
||||
const now = date.getTime();
|
||||
@@ -503,6 +596,11 @@ export function setCelestialFollow(nextFollow = {}) {
|
||||
return getCelestialDebugState();
|
||||
}
|
||||
|
||||
export function setCelestialDayNightEnabled(enabled) {
|
||||
dayNightLightingEnabled = enabled;
|
||||
updateLighting();
|
||||
}
|
||||
|
||||
export function disposeCelestialLayer() {
|
||||
if (celestialRoot?.parent) {
|
||||
celestialRoot.parent.remove(celestialRoot);
|
||||
@@ -531,7 +629,11 @@ export function disposeCelestialLayer() {
|
||||
lastUpdatedAt = 0;
|
||||
linkedSunLight = null;
|
||||
linkedBackLight = null;
|
||||
linkedAmbientLight = null;
|
||||
linkedPointLight = null;
|
||||
linkedCamera = null;
|
||||
linkedEarth = null;
|
||||
dayNightLightingEnabled = true;
|
||||
sunDirection.copy(defaultSunDirection);
|
||||
moonDirection.copy(defaultMoonDirection);
|
||||
runtimeOrientationEuler = {
|
||||
|
||||
@@ -92,6 +92,45 @@ export const CELESTIAL_CONFIG = {
|
||||
sunLightColor: 0xfff4df,
|
||||
backLightIntensity: 0.3,
|
||||
backLightColor: 0x2b4c78,
|
||||
inspectionLighting: {
|
||||
enabled: true,
|
||||
ambientIntensity: 0.64,
|
||||
ambientColor: 0x707070,
|
||||
keyLightIntensity: 0.92,
|
||||
keyLightColor: 0xfcfcfb,
|
||||
keyLightDistance: 380,
|
||||
keyLightOffset: { x: 0.42, y: 0.34, z: 0.84 },
|
||||
backLightIntensity: 0.26,
|
||||
backLightColor: 0x8f96a0,
|
||||
backLightDistance: 260,
|
||||
backLightOffset: { x: -0.52, y: -0.1, z: -0.62 },
|
||||
pointLightIntensity: 0.36,
|
||||
pointLightColor: 0xfafcff,
|
||||
pointLightDistance: 320,
|
||||
pointLightOffset: { x: 0.18, y: 0.52, z: 0.62 },
|
||||
},
|
||||
};
|
||||
|
||||
export const SCENE_LIGHT_CONFIG = {
|
||||
ambient: {
|
||||
color: 0x404060,
|
||||
intensity: 1,
|
||||
},
|
||||
sun: {
|
||||
color: 0xffffff,
|
||||
intensity: 1.2,
|
||||
position: { x: 5, y: 3, z: 5 },
|
||||
},
|
||||
back: {
|
||||
color: 0x446688,
|
||||
intensity: 0.3,
|
||||
position: { x: -5, y: 0, z: -5 },
|
||||
},
|
||||
point: {
|
||||
color: 0xffffff,
|
||||
intensity: 0.4,
|
||||
position: { x: 10, y: 10, z: 10 },
|
||||
},
|
||||
};
|
||||
|
||||
export const TERRAIN_CONFIG = {
|
||||
@@ -192,6 +231,9 @@ export const SATELLITE_CONFIG = {
|
||||
initialLoadCount: 2400,
|
||||
hydrateFullAfterInitialLoad: true,
|
||||
trailLength: 10,
|
||||
displayAltitudeOffset: 8,
|
||||
frontFacingDotThreshold: 0.015,
|
||||
overlayRenderOrder: 12,
|
||||
dotSize: 4,
|
||||
ringSize: 0.07,
|
||||
apiPath: '/api/v1/visualization/geo/satellites',
|
||||
|
||||
879
frontend/public/earth/js/controls.js
vendored
879
frontend/public/earth/js/controls.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ export let terrain = null;
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
let _earthMaterial = null;
|
||||
let _earthShader = null;
|
||||
let _dayNightEnabled = true;
|
||||
const _earthSunDirection = new THREE.Vector3(
|
||||
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.x,
|
||||
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.y,
|
||||
@@ -33,6 +34,7 @@ function applyEarthDayNightShader(material) {
|
||||
shader.uniforms.uTwilightColor = { value: twilightColor };
|
||||
shader.uniforms.uNightTintColor = { value: nightTintColor };
|
||||
shader.uniforms.uNightTintIntensity = { value: EARTH_MATERIAL_CONFIG.dayNight.nightTintIntensity };
|
||||
shader.uniforms.uDayNightEnabled = { value: _dayNightEnabled ? 1.0 : 0.0 };
|
||||
|
||||
shader.vertexShader = shader.vertexShader.replace(
|
||||
"#include <common>",
|
||||
@@ -55,7 +57,8 @@ uniform float uTwilightWidth;
|
||||
uniform float uTwilightIntensity;
|
||||
uniform vec3 uTwilightColor;
|
||||
uniform vec3 uNightTintColor;
|
||||
uniform float uNightTintIntensity;`,
|
||||
uniform float uNightTintIntensity;
|
||||
uniform float uDayNightEnabled;`,
|
||||
).replace(
|
||||
"#include <output_fragment>",
|
||||
`
|
||||
@@ -65,16 +68,27 @@ uniform float uNightTintIntensity;`,
|
||||
float daylight = smoothstep(-uTwilightWidth, uTwilightWidth, sunFacing);
|
||||
float twilight = 1.0 - smoothstep(0.0, uTwilightWidth, abs(sunFacing));
|
||||
|
||||
outgoingLight *= mix(uNightFloor, uDayBoost, daylight);
|
||||
outgoingLight += uTwilightColor * twilight * uTwilightIntensity;
|
||||
outgoingLight += uNightTintColor * (1.0 - daylight) * uNightTintIntensity;
|
||||
// Camera-facing diffuse: vNormal and vViewPosition are both in view space.
|
||||
// N·V gives 1.0 at center-facing, 0 at limb — creates depth cue regardless of earth rotation.
|
||||
float nDotV = max(0.0, dot(normalize(vNormal), normalize(vViewPosition)));
|
||||
float cameraBoost = mix(0.62, 1.08, nDotV);
|
||||
|
||||
float dn = uDayNightEnabled;
|
||||
vec3 dnLight = outgoingLight;
|
||||
dnLight *= mix(uNightFloor, uDayBoost, daylight);
|
||||
dnLight += uTwilightColor * twilight * uTwilightIntensity;
|
||||
dnLight += uNightTintColor * (1.0 - daylight) * uNightTintIntensity;
|
||||
|
||||
// dn=0: emissive base (from material, set in JS) * camera-facing boost → always readable
|
||||
// dn=1: full day/night solar lighting
|
||||
outgoingLight = mix(outgoingLight * cameraBoost, dnLight, dn);
|
||||
|
||||
#include <output_fragment>
|
||||
`,
|
||||
);
|
||||
};
|
||||
|
||||
material.customProgramCacheKey = () => "earth-day-night-v1";
|
||||
material.customProgramCacheKey = () => "earth-day-night-v5";
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
|
||||
@@ -348,6 +362,28 @@ export function setEarthSunDirection(direction) {
|
||||
}
|
||||
}
|
||||
|
||||
export function setDayNightEnabled(enabled) {
|
||||
_dayNightEnabled = enabled;
|
||||
if (_earthShader?.uniforms?.uDayNightEnabled) {
|
||||
_earthShader.uniforms.uDayNightEnabled.value = enabled ? 1.0 : 0.0;
|
||||
}
|
||||
if (_earthMaterial) {
|
||||
if (enabled) {
|
||||
// Restore normal Phong lighting + custom day/night shader
|
||||
_earthMaterial.color.setHex(EARTH_MATERIAL_CONFIG.color);
|
||||
_earthMaterial.emissive.setHex(EARTH_MATERIAL_CONFIG.emissive);
|
||||
_earthMaterial.emissiveMap = null;
|
||||
} else {
|
||||
// Full bright: zero diffuse so directional light has no effect;
|
||||
// use original color as emissive map to show texture uniformly.
|
||||
_earthMaterial.color.setRGB(0, 0, 0);
|
||||
_earthMaterial.emissive.setHex(EARTH_MATERIAL_CONFIG.color);
|
||||
_earthMaterial.emissiveMap = _earthMaterial.map;
|
||||
}
|
||||
_earthMaterial.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadEarthTexture() {
|
||||
return new Promise((resolve) => {
|
||||
if (!_earthMaterial) { resolve(); return; }
|
||||
@@ -368,6 +404,10 @@ export function loadEarthTexture() {
|
||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
_earthMaterial.map = texture;
|
||||
// If day/night is currently disabled, sync emissiveMap to the newly loaded texture
|
||||
if (!_dayNightEnabled) {
|
||||
_earthMaterial.emissiveMap = texture;
|
||||
}
|
||||
_earthMaterial.needsUpdate = true;
|
||||
resolve();
|
||||
},
|
||||
|
||||
174
frontend/public/earth/js/layer-startup-tasks.js
Normal file
174
frontend/public/earth/js/layer-startup-tasks.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import {
|
||||
loadGeoJSONFromPath,
|
||||
loadLandingPoints,
|
||||
getCableLegendItems,
|
||||
toggleCables,
|
||||
} from "./cables.js";
|
||||
import {
|
||||
clearSatelliteData,
|
||||
getSatelliteLegendItems,
|
||||
loadSatellites,
|
||||
toggleSatellites,
|
||||
} from "./satellites.js";
|
||||
import {
|
||||
loadBGPAnomalies,
|
||||
toggleBGP,
|
||||
} from "./bgp.js";
|
||||
|
||||
/**
|
||||
* Layer startup task registry.
|
||||
*
|
||||
* This module is the startup-task counterpart to the layer registry in controls.js:
|
||||
* - controls.js owns layer metadata such as startupPriority/startupMode/startupMessage
|
||||
* - this file owns the executable startup task factory for each layer id
|
||||
*
|
||||
* A startup task is registered via registerLayerStartupTask(id, taskFactory).
|
||||
* The taskFactory receives a startup context from main.js and must return an async
|
||||
* function with the signature async (layerDefinition) => void.
|
||||
*
|
||||
* Put a task here only when a layer needs dedicated startup loading work:
|
||||
* - preloading data at boot
|
||||
* - staged loading with progress/loading messages
|
||||
* - post-load UI refresh or warmup
|
||||
*
|
||||
* Do not put plain visibility toggles or persistent UI state here; those still belong
|
||||
* to the layer registry/state flow in controls.js.
|
||||
*/
|
||||
const startupTaskRegistry = new Map();
|
||||
|
||||
export function resolveStartupMessage(layer, key, fallback) {
|
||||
const message = layer?.startupMessage;
|
||||
if (message && typeof message === "object" && key in message) {
|
||||
return message[key];
|
||||
}
|
||||
if (typeof message === "string" && message.trim()) {
|
||||
return message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function createLayerStartupTaskMap(context) {
|
||||
return Object.fromEntries(
|
||||
Array.from(startupTaskRegistry.entries()).map(([id, taskFactory]) => [
|
||||
id,
|
||||
taskFactory(context),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export function registerLayerStartupTask(id, taskFactory) {
|
||||
if (typeof id !== "string" || !id.trim()) {
|
||||
throw new Error("registerLayerStartupTask 需要有效的图层 id");
|
||||
}
|
||||
if (typeof taskFactory !== "function") {
|
||||
throw new Error("registerLayerStartupTask 需要可调用的任务工厂");
|
||||
}
|
||||
startupTaskRegistry.set(id, taskFactory);
|
||||
}
|
||||
|
||||
function registerBuiltinLayerStartupTasks() {
|
||||
startupTaskRegistry.clear();
|
||||
registerCableStartupTask();
|
||||
registerSatelliteStartupTask();
|
||||
registerBGPStartupTask();
|
||||
}
|
||||
|
||||
function registerCableStartupTask() {
|
||||
registerLayerStartupTask("cables", (context) => async (layer) => {
|
||||
if (!context.isCablesEnabled()) return;
|
||||
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "prepare", "正在加载登陆点..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
await loadLandingPoints(context.scene, context.earth, { silent: true });
|
||||
} catch (error) {
|
||||
context.reportError("登陆点", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "load", "正在加载海缆..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
await loadGeoJSONFromPath(context.scene, context.earth, { silent: true });
|
||||
if (!context.isCancelled() && context.isCablesEnabled()) {
|
||||
toggleCables(true);
|
||||
context.updateCableToggleUi(true);
|
||||
context.setLegendItems("cables", getCableLegendItems());
|
||||
context.refreshLegend();
|
||||
}
|
||||
} catch (error) {
|
||||
context.reportError(layer?.startupLabel || layer?.label || "海缆", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
});
|
||||
}
|
||||
|
||||
function registerSatelliteStartupTask() {
|
||||
registerLayerStartupTask("satellites", (context) => async (layer) => {
|
||||
if (!context.isSatellitesEnabled()) return;
|
||||
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "load", "正在加载卫星..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
clearSatelliteData();
|
||||
const loadResult = await loadSatellites({
|
||||
limit: context.getInitialSatelliteLoadLimit(),
|
||||
});
|
||||
if (!context.isCancelled() && context.isSatellitesEnabled()) {
|
||||
context.updateSatelliteToggleUi(true, loadResult.count);
|
||||
context.setLegendItems("satellites", getSatelliteLegendItems());
|
||||
context.refreshLegend();
|
||||
context.scheduleSatellitePositionWarmup(() => {
|
||||
if (!context.isCancelled() && context.isSatellitesEnabled()) {
|
||||
toggleSatellites(true);
|
||||
}
|
||||
});
|
||||
|
||||
if (context.shouldHydrateFullSatelliteSet(loadResult)) {
|
||||
const hydrationToken = context.nextSatelliteHydrationToken();
|
||||
context.hydrateAllSatellitesInBackground(
|
||||
() =>
|
||||
hydrationToken === context.getSatelliteHydrationToken() &&
|
||||
!context.isCancelled() &&
|
||||
context.isSatellitesEnabled(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
context.reportError(layer?.startupLabel || layer?.label || "卫星", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
});
|
||||
}
|
||||
|
||||
function registerBGPStartupTask() {
|
||||
registerLayerStartupTask("bgp", (context) => async (layer) => {
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "load", "正在加载BGP态势..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
const bgpResult = await loadBGPAnomalies(context.scene, context.earth);
|
||||
if (!context.isCancelled()) {
|
||||
toggleBGP(context.getShowBGP());
|
||||
context.updateBGPHud(bgpResult);
|
||||
context.syncBGPKnownEventIds();
|
||||
}
|
||||
} catch (error) {
|
||||
context.reportError(layer?.startupLabel || layer?.label || "BGP态势", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
});
|
||||
}
|
||||
|
||||
registerBuiltinLayerStartupTasks();
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
BGP_CONFIG,
|
||||
CRUISE_CONFIG,
|
||||
ROTATION_MODE,
|
||||
SCENE_LIGHT_CONFIG,
|
||||
} from "./constants.js";
|
||||
import { vector3ToLatLon, screenToEarthCoords } from "./utils.js";
|
||||
import {
|
||||
@@ -36,7 +37,7 @@ import {
|
||||
clearEarthTexture,
|
||||
setEarthSunDirection,
|
||||
} from "./earth.js";
|
||||
import { registerTerrainMesh, clearTerrainData } from "./terrain.js";
|
||||
import { registerTerrainMesh, clearTerrainData, sampleElevationAt } from "./terrain.js";
|
||||
import {
|
||||
initCelestialLayer,
|
||||
updateCelestialLayer,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
getSunDirection,
|
||||
setCelestialOrientation,
|
||||
setCelestialFollow,
|
||||
setCelestialDayNightEnabled,
|
||||
} from "./celestial.js";
|
||||
import {
|
||||
loadGeoJSONFromPath,
|
||||
@@ -129,12 +131,17 @@ import {
|
||||
getAutoRotate,
|
||||
getRotationMode,
|
||||
getShowTerrain,
|
||||
getStartupLoadLayers,
|
||||
setAutoRotate,
|
||||
applyImmediateView,
|
||||
focusEarthView,
|
||||
getZoomLevel,
|
||||
teardownControls,
|
||||
} from "./controls.js";
|
||||
import {
|
||||
createLayerStartupTaskMap,
|
||||
resolveStartupMessage,
|
||||
} from "./layer-startup-tasks.js";
|
||||
import {
|
||||
setLayerButtonState,
|
||||
} from "./layer-button-state.js";
|
||||
@@ -1224,10 +1231,13 @@ export function init() {
|
||||
registerTerrainMesh(createTerrain(earthObj));
|
||||
initCelestialLayer(scene, {
|
||||
camera,
|
||||
ambientLight: sceneLights?.ambientLight ?? null,
|
||||
sunLight: sceneLights?.sunLight ?? null,
|
||||
backLight: sceneLights?.backLight ?? null,
|
||||
pointLight: sceneLights?.pointLight ?? null,
|
||||
earth: earthObj,
|
||||
});
|
||||
setCelestialDayNightEnabled(true);
|
||||
createGridLines(scene, earthObj);
|
||||
createSatellites(scene, earthObj);
|
||||
|
||||
@@ -1258,21 +1268,43 @@ function registerGlobalApi() {
|
||||
}
|
||||
|
||||
function addLights() {
|
||||
const ambientLight = new THREE.AmbientLight(0x404060);
|
||||
const ambientLight = new THREE.AmbientLight(SCENE_LIGHT_CONFIG.ambient.color);
|
||||
ambientLight.intensity = SCENE_LIGHT_CONFIG.ambient.intensity;
|
||||
scene.add(ambientLight);
|
||||
|
||||
const sunLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
sunLight.position.set(5, 3, 5);
|
||||
const sunLight = new THREE.DirectionalLight(
|
||||
SCENE_LIGHT_CONFIG.sun.color,
|
||||
SCENE_LIGHT_CONFIG.sun.intensity,
|
||||
);
|
||||
sunLight.position.set(
|
||||
SCENE_LIGHT_CONFIG.sun.position.x,
|
||||
SCENE_LIGHT_CONFIG.sun.position.y,
|
||||
SCENE_LIGHT_CONFIG.sun.position.z,
|
||||
);
|
||||
sunLight.target.position.set(0, 0, 0);
|
||||
scene.add(sunLight);
|
||||
scene.add(sunLight.target);
|
||||
|
||||
const backLight = new THREE.DirectionalLight(0x446688, 0.3);
|
||||
backLight.position.set(-5, 0, -5);
|
||||
const backLight = new THREE.DirectionalLight(
|
||||
SCENE_LIGHT_CONFIG.back.color,
|
||||
SCENE_LIGHT_CONFIG.back.intensity,
|
||||
);
|
||||
backLight.position.set(
|
||||
SCENE_LIGHT_CONFIG.back.position.x,
|
||||
SCENE_LIGHT_CONFIG.back.position.y,
|
||||
SCENE_LIGHT_CONFIG.back.position.z,
|
||||
);
|
||||
scene.add(backLight);
|
||||
|
||||
const pointLight = new THREE.PointLight(0xffffff, 0.4);
|
||||
pointLight.position.set(10, 10, 10);
|
||||
const pointLight = new THREE.PointLight(
|
||||
SCENE_LIGHT_CONFIG.point.color,
|
||||
SCENE_LIGHT_CONFIG.point.intensity,
|
||||
);
|
||||
pointLight.position.set(
|
||||
SCENE_LIGHT_CONFIG.point.position.x,
|
||||
SCENE_LIGHT_CONFIG.point.position.y,
|
||||
SCENE_LIGHT_CONFIG.point.position.z,
|
||||
);
|
||||
scene.add(pointLight);
|
||||
|
||||
return {
|
||||
@@ -1364,100 +1396,54 @@ async function loadData() {
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
|
||||
// Step 2 — Landing points
|
||||
if (cablesEnabled) {
|
||||
setLoadingMessage("正在加载登陆点...");
|
||||
await yieldFrame(12);
|
||||
try {
|
||||
await loadLandingPoints(scene, earth, { silent: true });
|
||||
} catch (err) {
|
||||
errors.push({ label: "登陆点", reason: err });
|
||||
}
|
||||
const startupLoaders = createLayerStartupTaskMap({
|
||||
scene,
|
||||
earth,
|
||||
setLoadingMessage,
|
||||
yieldFrame,
|
||||
refreshLegend,
|
||||
setLegendItems,
|
||||
updateCableToggleUi,
|
||||
updateSatelliteToggleUi,
|
||||
updateBGPHud,
|
||||
getShowBGP,
|
||||
getInitialSatelliteLoadLimit,
|
||||
shouldHydrateFullSatelliteSet,
|
||||
scheduleSatellitePositionWarmup,
|
||||
hydrateAllSatellitesInBackground,
|
||||
syncBGPKnownEventIds: () => ensureBGPCruiseAdapter().syncKnownEventIds(),
|
||||
isCancelled: () => loadToken !== currentLoadToken || destroyed,
|
||||
isCablesEnabled: () => cablesEnabled,
|
||||
isSatellitesEnabled: () => satellitesEnabled,
|
||||
nextSatelliteHydrationToken: () => ++satelliteHydrationToken,
|
||||
getSatelliteHydrationToken: () => satelliteHydrationToken,
|
||||
reportError: (label, reason) => {
|
||||
errors.push({ label, reason });
|
||||
},
|
||||
});
|
||||
|
||||
const startupLayers = getStartupLoadLayers();
|
||||
const startupLoadQueue = startupLayers
|
||||
.map((layer) => ({
|
||||
layer,
|
||||
run: startupLoaders[layer.id],
|
||||
}))
|
||||
.filter((entry) => typeof entry.run === "function");
|
||||
|
||||
for (const entry of startupLoadQueue) {
|
||||
await entry.run(entry.layer);
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
}
|
||||
|
||||
// Step 3 — Cables
|
||||
if (cablesEnabled) {
|
||||
setLoadingMessage("正在加载海缆...");
|
||||
await yieldFrame(12);
|
||||
try {
|
||||
const cableCount = await loadGeoJSONFromPath(scene, earth, {
|
||||
silent: true,
|
||||
});
|
||||
if (loadToken === currentLoadToken && cablesEnabled) {
|
||||
toggleCables(true);
|
||||
updateCableToggleUi(true);
|
||||
setLegendItems("cables", getCableLegendItems());
|
||||
refreshLegend();
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ label: "海缆", reason: err });
|
||||
}
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
}
|
||||
|
||||
// Step 4 — Satellites
|
||||
if (satellitesEnabled) {
|
||||
setLoadingMessage("正在加载卫星...");
|
||||
await yieldFrame(12);
|
||||
try {
|
||||
clearSatelliteData();
|
||||
const loadResult = await loadSatellites({
|
||||
limit: getInitialSatelliteLoadLimit(),
|
||||
});
|
||||
if (loadToken === currentLoadToken && satellitesEnabled) {
|
||||
updateSatelliteToggleUi(true, loadResult.count);
|
||||
setLegendItems("satellites", getSatelliteLegendItems());
|
||||
refreshLegend();
|
||||
scheduleSatellitePositionWarmup(() => {
|
||||
if (
|
||||
loadToken === currentLoadToken &&
|
||||
satellitesEnabled &&
|
||||
!destroyed
|
||||
) {
|
||||
toggleSatellites(true);
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldHydrateFullSatelliteSet(loadResult)) {
|
||||
const hydrationToken = ++satelliteHydrationToken;
|
||||
hydrateAllSatellitesInBackground(
|
||||
() =>
|
||||
hydrationToken === satelliteHydrationToken &&
|
||||
loadToken === currentLoadToken &&
|
||||
satellitesEnabled &&
|
||||
!destroyed,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ label: "卫星", reason: err });
|
||||
}
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
}
|
||||
|
||||
// Step 5 — BGP
|
||||
setLoadingMessage("正在加载BGP态势...");
|
||||
await yieldFrame(12);
|
||||
try {
|
||||
const bgpResult = await loadBGPAnomalies(scene, earth);
|
||||
if (loadToken === currentLoadToken) {
|
||||
toggleBGP(true);
|
||||
updateBGPHud(bgpResult);
|
||||
ensureBGPCruiseAdapter().syncKnownEventIds();
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ label: "BGP态势", reason: err });
|
||||
}
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
|
||||
// Step 6 — Terrain (if enabled)
|
||||
if (getShowTerrain()) {
|
||||
setLoadingMessage("正在渲染地形...");
|
||||
const terrainLayer = startupLayers.find((layer) => layer.id === "terrain");
|
||||
const terrainMessage = resolveStartupMessage(
|
||||
terrainLayer,
|
||||
"load",
|
||||
"正在渲染地形...",
|
||||
);
|
||||
setLoadingMessage(terrainMessage);
|
||||
await yieldFrame(24);
|
||||
}
|
||||
|
||||
@@ -1493,7 +1479,14 @@ export async function reloadData() {
|
||||
await loadData();
|
||||
}
|
||||
|
||||
export async function setCablesEnabled(enabled) {
|
||||
export function getSatellitesEnabled() {
|
||||
return satellitesEnabled;
|
||||
}
|
||||
|
||||
export async function setCablesEnabled(
|
||||
enabled,
|
||||
{ suppressStatus = false, suppressLoadingUi = false } = {},
|
||||
) {
|
||||
if (enabled === cablesEnabled) {
|
||||
updateCableToggleUi(enabled);
|
||||
return getCableLines().length;
|
||||
@@ -1502,32 +1495,47 @@ export async function setCablesEnabled(enabled) {
|
||||
if (!enabled) {
|
||||
clearSelectionAndInfo();
|
||||
disableCables();
|
||||
showStatusMessage("线缆已隐藏", "info");
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage("线缆已隐藏", "info");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
setLoadingMessage("正在加载线缆数据...");
|
||||
setLoading(true);
|
||||
hideError();
|
||||
if (!suppressLoadingUi) {
|
||||
setLoadingMessage("正在加载线缆数据...");
|
||||
setLoading(true);
|
||||
hideError();
|
||||
}
|
||||
|
||||
try {
|
||||
const cableCount = await ensureCablesEnabled();
|
||||
showStatusMessage("线缆已显示", "info");
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage("线缆已显示", "info");
|
||||
}
|
||||
return cableCount;
|
||||
} catch (error) {
|
||||
cablesEnabled = false;
|
||||
clearCableData(getEarth());
|
||||
updateCableToggleUi(false);
|
||||
const message = `线缆加载失败: ${error?.message || String(error)}`;
|
||||
showError(message);
|
||||
showStatusMessage(message, "error");
|
||||
if (!suppressLoadingUi) {
|
||||
showError(message);
|
||||
}
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage(message, "error");
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!suppressLoadingUi) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setSatellitesEnabled(enabled) {
|
||||
export async function setSatellitesEnabled(
|
||||
enabled,
|
||||
{ suppressStatus = false, suppressLoadingUi = false } = {},
|
||||
) {
|
||||
if (enabled === satellitesEnabled) {
|
||||
updateSatelliteToggleUi(enabled);
|
||||
return getSatelliteCount();
|
||||
@@ -1539,24 +1547,34 @@ export async function setSatellitesEnabled(enabled) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
setLoadingMessage("正在加载卫星数据...");
|
||||
setLoading(true);
|
||||
hideError();
|
||||
if (!suppressLoadingUi) {
|
||||
setLoadingMessage("正在加载卫星数据...");
|
||||
setLoading(true);
|
||||
hideError();
|
||||
}
|
||||
|
||||
try {
|
||||
const satelliteCount = await ensureSatellitesEnabled();
|
||||
showStatusMessage("卫星已显示", "info");
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage("卫星已显示", "info");
|
||||
}
|
||||
return satelliteCount;
|
||||
} catch (error) {
|
||||
satellitesEnabled = false;
|
||||
resetSatelliteState();
|
||||
updateSatelliteToggleUi(false, 0);
|
||||
const message = `卫星加载失败: ${error?.message || String(error)}`;
|
||||
showError(message);
|
||||
showStatusMessage(message, "error");
|
||||
if (!suppressLoadingUi) {
|
||||
showError(message);
|
||||
}
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage(message, "error");
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!suppressLoadingUi) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1618,7 +1636,10 @@ function getFrontFacingCables(cableLines) {
|
||||
scratchCableDirection
|
||||
.subVectors(scratchCableCenter, earth.position)
|
||||
.normalize();
|
||||
return scratchCameraToEarth.dot(scratchCableDirection) > 0;
|
||||
return (
|
||||
scratchCameraToEarth.dot(scratchCableDirection) >
|
||||
SATELLITE_CONFIG.frontFacingDotThreshold
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1634,7 +1655,10 @@ function getFrontFacingBGPMarkers(markers) {
|
||||
scratchBGPDirection
|
||||
.subVectors(scratchBGPWorldPosition, earth.position)
|
||||
.normalize();
|
||||
return scratchCameraToEarth.dot(scratchBGPDirection) > 0;
|
||||
return (
|
||||
scratchCameraToEarth.dot(scratchBGPDirection) >
|
||||
SATELLITE_CONFIG.frontFacingDotThreshold
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1793,10 +1817,16 @@ function onMouseMove(event) {
|
||||
if (earthPoint) {
|
||||
const coords = vector3ToLatLon(earthPoint);
|
||||
updateCoordinatesDisplay(coords.lat, coords.lon, coords.alt);
|
||||
const elevMeters = sampleElevationAt(coords.lat, coords.lon);
|
||||
const elevText = elevMeters !== null
|
||||
? elevMeters >= 1000
|
||||
? `${(elevMeters / 1000).toFixed(2)} km`
|
||||
: `${Math.round(elevMeters)} m`
|
||||
: "—";
|
||||
showTooltip(
|
||||
event.clientX + TOOLTIP_COORDS_OFFSET,
|
||||
event.clientY + TOOLTIP_COORDS_OFFSET,
|
||||
`纬度: ${coords.lat}°<br>经度: ${coords.lon}°<br>海拔: ${coords.alt.toFixed(1)} km`,
|
||||
`纬度: ${coords.lat}°<br>经度: ${coords.lon}°<br>海拔: ${elevText}`,
|
||||
);
|
||||
} else {
|
||||
hideTooltip();
|
||||
|
||||
@@ -490,7 +490,8 @@ function computeSatellitePosition(satellite, time) {
|
||||
}
|
||||
|
||||
const r = Math.sqrt(x * x + y * y + z * z);
|
||||
const displayRadius = CONFIG.earthRadius * 1.05;
|
||||
const displayRadius =
|
||||
CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
|
||||
const scale = displayRadius / r;
|
||||
|
||||
return new THREE.Vector3(x * scale, y * scale, z * scale);
|
||||
@@ -637,7 +638,7 @@ function buildTleLinesFromElements(props, fallbackTime) {
|
||||
}
|
||||
|
||||
function generateFallbackPosition(satellite, index, total) {
|
||||
const radius = CONFIG.earthRadius + 5;
|
||||
const radius = CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
|
||||
|
||||
const noradId = satellite.properties?.norad_cat_id || index;
|
||||
const inclination = satellite.properties?.inclination || 53;
|
||||
@@ -843,6 +844,10 @@ export function toggleTrails(visible) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getShowTrails() {
|
||||
return showTrails;
|
||||
}
|
||||
|
||||
export function getShowSatellites() {
|
||||
return showSatellites;
|
||||
}
|
||||
@@ -902,7 +907,10 @@ export function isSatelliteFrontFacing(index, camera = cameraRef) {
|
||||
.subVectors(scratchWorldSatellitePosition, earthObjRef.position)
|
||||
.normalize();
|
||||
|
||||
return scratchToCamera.dot(scratchToSatellite) > 0;
|
||||
return (
|
||||
scratchToCamera.dot(scratchToSatellite) >
|
||||
SATELLITE_CONFIG.frontFacingDotThreshold
|
||||
);
|
||||
}
|
||||
|
||||
function createBrighterDotCanvas() {
|
||||
@@ -948,6 +956,7 @@ function createRingSprite(position, isLocked = false) {
|
||||
const sprite = new THREE.Sprite(spriteMaterial);
|
||||
sprite.position.copy(position);
|
||||
sprite.scale.set(SATELLITE_CONFIG.ringSize, SATELLITE_CONFIG.ringSize, 1);
|
||||
sprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
|
||||
earthObjRef.add(sprite);
|
||||
return sprite;
|
||||
}
|
||||
@@ -967,6 +976,7 @@ function createRelatedSatelliteSprite(position, color = "#7dd3fc") {
|
||||
const sprite = new THREE.Sprite(spriteMaterial);
|
||||
sprite.position.copy(position);
|
||||
sprite.scale.set(SATELLITE_CONFIG.ringSize * 0.8, SATELLITE_CONFIG.ringSize * 0.8, 1);
|
||||
sprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
|
||||
earthObjRef.add(sprite);
|
||||
return sprite;
|
||||
}
|
||||
@@ -989,6 +999,7 @@ export function showHoverRing(position, isLocked = false) {
|
||||
lockedDotSprite = new THREE.Sprite(dotMaterial);
|
||||
lockedDotSprite.position.copy(position);
|
||||
lockedDotSprite.scale.set(4, 4, 1);
|
||||
lockedDotSprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder + 1;
|
||||
earthObjRef.add(lockedDotSprite);
|
||||
return lockedRingSprite;
|
||||
}
|
||||
@@ -1199,7 +1210,8 @@ function calculatePredictedOrbit(
|
||||
|
||||
if (points.length < samples * 0.5) {
|
||||
points.length = 0;
|
||||
const radius = CONFIG.earthRadius + 5;
|
||||
const radius =
|
||||
CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
|
||||
const inclination = satellite.properties?.inclination || 53;
|
||||
const raan = satellite.properties?.raan || 0;
|
||||
|
||||
@@ -1250,9 +1262,12 @@ export function showPredictedOrbit(satellite) {
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthTest: true,
|
||||
depthWrite: false,
|
||||
});
|
||||
|
||||
predictedOrbitLine = new THREE.Line(geometry, material);
|
||||
predictedOrbitLine.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
|
||||
earthObjRef.add(predictedOrbitLine);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ let terrainLoadPromise = null;
|
||||
let terrainReady = false;
|
||||
let terrainFailed = false;
|
||||
let terrainTileCache = new Map();
|
||||
let resolvedTileCache = new Map();
|
||||
let terrainVertexSamples = null;
|
||||
let terrainOpacity = TERRAIN_CONFIG.opacity;
|
||||
|
||||
@@ -67,7 +68,9 @@ async function decodeTerrainTile(z, x, y) {
|
||||
TERRAIN_CONFIG.tileSize,
|
||||
TERRAIN_CONFIG.tileSize,
|
||||
);
|
||||
return { data, width, height };
|
||||
const tileData = { data, width, height };
|
||||
resolvedTileCache.set(cacheKey, tileData);
|
||||
return tileData;
|
||||
})();
|
||||
|
||||
terrainTileCache.set(cacheKey, tilePromise);
|
||||
@@ -236,6 +239,7 @@ export function registerTerrainMesh(mesh) {
|
||||
terrainFailed = false;
|
||||
terrainLoadPromise = null;
|
||||
terrainTileCache = new Map();
|
||||
resolvedTileCache = new Map();
|
||||
terrainOpacity = TERRAIN_CONFIG.opacity;
|
||||
if (terrainMesh?.material) {
|
||||
terrainMesh.material.opacity = terrainOpacity;
|
||||
@@ -287,9 +291,19 @@ export function clearTerrainData() {
|
||||
terrainFailed = false;
|
||||
terrainVertexSamples = null;
|
||||
terrainTileCache = new Map();
|
||||
resolvedTileCache = new Map();
|
||||
terrainOpacity = TERRAIN_CONFIG.opacity;
|
||||
}
|
||||
|
||||
export function sampleElevationAt(lat, lon) {
|
||||
if (!terrainReady) return null;
|
||||
const z = TERRAIN_CONFIG.baseZoom;
|
||||
const { tileX, tileY, pixelX, pixelY } = latLonToTileSample(lat, lon, z, TERRAIN_CONFIG.tileSize);
|
||||
const tile = resolvedTileCache.get(`${z}/${tileX}/${tileY}`);
|
||||
if (!tile) return null;
|
||||
return Math.max(0, decodeTerrariumHeight(tile, pixelX, pixelY));
|
||||
}
|
||||
|
||||
export function setTerrainOpacity(nextOpacity) {
|
||||
terrainOpacity = THREE.MathUtils.clamp(nextOpacity, 0.05, 1);
|
||||
if (terrainMesh?.material) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "planet"
|
||||
version = "0.31.2"
|
||||
version = "0.31.3"
|
||||
description = "智能星球计划 - 态势感知系统"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user