Files
planet/docs/technical/zh/earth-frontend-context.md
rayd1o 9b913a3b83
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.59.0
2026-05-16 05:02:05 +08:00

481 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Earth 前端结构
本文件描述当前 Earth 大屏前端的真实结构,重点是帮助后续继续改 HUD、图层、媒体面板、真实地形、BGP 可视化时,不再重复踩结构和状态同步上的坑。
相关规则建议一起参考:
- [项目规则](/home/ray/dev/linkong/planet/rules.md)
- [前端布局指南](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-layout-guidelines.md)
## 当前目标
Earth 前端不是普通管理页,它是独立的大屏展示前端。当前产品目标是:
- 维持地球视图的空间感和可读性
- 让 HUD、图层、媒体面板、BGP、卫星、海缆等保持统一交互
- 把加载中、已启用、已隐藏、锁定中这类状态做清楚
## 当前入口
React 路由入口:
- [Earth.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Earth/Earth.tsx)
当前做法很简单:
- React 页面只负责提供一个全屏 `iframe`
- 真正的 Earth 应用运行在:
- [index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html)
所以 Earth 前端本质上是 `public/earth` 下的一套独立静态应用。
## 当前文件分层
### 1. 页面入口与结构
- [index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html)
职责:
- HUD 基础 DOM
- 图层面板
- 媒体面板
- 工具栏
- 设置弹窗
- 兼容旧元素 id
### 2. 主运行时
- [main.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/main.js)
职责:
- 地球初始化
- Three.js 场景组装
- 数据加载与刷新
- 各图层集成
- Earth 级别状态同步
### 3. 地球控制层
- [controls.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js)
职责:
- 工具栏交互
- 图层面板交互
- 旋转/缩放/布局
- HUD 面板拖拽
- 图层开关状态机
- Earth 设置读取、持久化与重置
这份文件是 Earth 前端当前最核心的 UI 控制入口。
### 4. UI 与状态消息
- [ui.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/ui.js)
职责:
- loading 面板
- status message
- tooltip / error / 清理逻辑
当前 status message 有两类短提示:
- 普通业务提示:通过 `showStatusMessage()` 入队显示。
- 手势提示:通过 `showGestureStatusMessage()` 直接短暂显示,用于缩放视角时的 `缩放 N%`
手势提示不会抢占 loading 状态。对应样式是 [hud.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/hud.css) 中的 `.earth-status-message.gesture`
### 5. 动作捕捉控制适配层
- [motion-control.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/motion-control.js)
职责:
- 作为 Motion Provider manager统一接入 `browser_camera``motion_agent`
- 默认使用浏览器 `getUserMedia` + 本地 MediaPipe 识别;高级模式可连接本地 Motion Capture Agent WebSocket。
- 处理浏览器摄像头权限/安全上下文错误,以及 Agent 断线重连和 `status` / `heartbeat`
- 过滤低置信度和过快重复的手势事件。
-`rotate_left``rotate_right``rotate_up``rotate_down``zoom_in``zoom_out``focus_prev``focus_next``layer_prev``layer_next``confirm` 映射到 `main.js` 暴露的动作入口。
- 解析 `skeleton` 调试事件并派发 `earth:motion-debug-frame`
动作捕捉识别可以在浏览器本地执行,也可以在本地 Agent 中执行,但两者都不会把实时视频帧发给 SaaS 云端。`main.js` 暴露旋转、缩放、目标切换、图层切换和确认入口,并通过 `window.__planetEarth.motion` 提供调试入口。默认只有 URL 参数 `?motion=1`、本地存储 `planet-earth-motion-control-enabled=true`,或 Earth 设置中的“动捕调试模式”打开时才启动当前 provider。
动捕调试面板由 [motion-debug-panel.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/motion-debug-panel.js) 负责。它监听 `earth:motion-debug-frame`,用 canvas 绘制归一化骨架点和连线Browser Camera provider 会额外通过 `earth:motion-debug-video-source` 提供本机 `<video>` 作为调试预览底图,`shared.motionDebugSkeletonOnly` 可切换为只显示骨骼。`停止匹配动作` 通过 `earth:motion-recognition-pause` 暂停 gesture 执行,但继续显示视频和骨架。未匹配动作为红色,匹配后变绿并显示动作名。设置项持久化在 `planet.earth.settings.v2``shared.motionDebugEnabled``shared.motionProvider``shared.motionDebugSkeletonOnly`switch 和输入源控件都预留 `data-gatekeeper-permission="earth.motion_debug"`
Browser Camera provider 的手势识别管线在 [motion-browser-provider.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/motion-browser-provider.js) 的 `recognizeGesture()`,按以下顺序匹配,前者命中即返回:
1. **`getZoomTrend`(趋势 zoom**:基于上一帧到当前帧两腕 x 间距的变化量(`Math.abs(rightWrist.x - leftWrist.x)` 的 delta。两腕都越过噪声地板`ZOOM_TREND_MIN_WRIST_DELTA = 0.010`)且高度差不超过 `ZOOM_TREND_HEIGHT_TOLERANCE`spread 增加 → `zoom_in`spread 减少 → `zoom_out`。趋势优先级最高,避免中间帧被 layer/focus/rotate 抢先误判。
2. **`getZoomHoldPose`(姿态 zoom**:动作停止后,只要两腕仍保持在胸口及以上、且 span > `ZOOM_HOLD_SPREAD_FACTOR × shoulderWidth`(默认 1.30)就持续派发 `zoom_in`;两肘明显外展且 span < `ZOOM_HOLD_CLOSE_FACTOR × shoulderWidth`(默认 0.85)就持续派发 `zoom_out`
3. **layer / focus**:左手抬起 + 上下移动派发 `layer_prev/next`;头部左右倾斜派发 `focus_prev/next`
4. **`getRightArmPattern`(单臂 rotate**:仅当 `!isZoomCandidatePose(...) && isLeftArmAtRest(...)` 同时成立时才考虑。`isLeftArmAtRest` 要求左腕明显垂在肩下 13% 以下且左肘/左腕都不外伸,把「张臂中间帧」「单手举起」「左手扶在胸前」等所有模糊状态都判为非静止 —— 单臂 rotate 严格要求另一只手处于静止。
两个关键设计:
- **mirror-safe**:所有 zoom 检测都基于 `Math.abs(rightWrist.x - leftWrist.x)`,不依赖单侧 x 方向。无论摄像头是否做镜像翻转(浏览器默认不翻转,`getUserMedia` 返回的就是原始帧),张臂始终 → zoom_in合手始终 → zoom_out。早期基于「左腕往左 + 右腕往右」的判定会在非镜像视图里把方向判反。
- **连续 vs 离散**rotate / layer / focus / confirm 都过 `applyPoseLatch`同一手势只发一次必须先回到中性位才能再发挥一下转一格。zoom 故意 bypass latch每帧匹配都返回 → 下游 `GESTURE_POLICIES.zoom_in/out.cooldownMs = 120` 节流到 ~8 次/秒,张臂保持就一直放大直到姿势变化。这是 zoom 跟其它手势在交互语义上的本质区别,不要复用 latch 给 zoom。
[presentation-controller.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/presentation-controller.js) 是新的 Presentation 层。第一阶段只接入 Motion`motion-cruise-adapter.js` 通过 persistent presentation 复用巡航固定卡片位置和 connector但不会让鼠标移动触发自动隐藏connector 每帧重算 source/target anchor让卡片拖动、地球旋转和目标移动时端点继续跟随。BGP/News 仍保持原有 `CruiseSequencer` 自动轮播路径,避免改变既有巡航体验。
### 6. 地球与地形
- [earth.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/earth.js)
- [terrain.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/terrain.js)
- [country-boundaries.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/country-boundaries.js)
职责:
- 地球球体、云层、大气
- 真实地形 mesh
- terrain tile 拉取、解码、位移、着色
- 海陆基座与国界底图的整球 overlay
Earth 地表是多层近似同心球,不是单一 mesh。`earth.js` 的基座球、高清材质 overlay、云层/大气,以及 `country-boundaries.js` 的海陆基座都需要明确半径间距。远距视图下 GPU 深度精度会下降,相邻 shell 过近会 z-fighting表现为黑色闪烁块或雪花。当前稳定策略是让海陆基座使用 `landAltitudeOffset = 0.32`,高清材质使用 `textureOverlayAltitudeOffset = 0.48`;后续新增或调整整球地表 overlay 时,必须同步检查 [Earth 渲染图层顺序](/home/ray/dev/linkong/planet/docs/technical/zh/earth-render-layer-order.md),并在 50% 缩放视图验证。
### 7. 图层模块
- [satellites.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/satellites.js)
- [cables.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/cables.js)
- [bgp.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp.js)
- [bgp-cruise-adapter.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp-cruise-adapter.js)
- [vessels.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/vessels.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)
职责:
- 各自的数据层
- 开关行为
- 面板内容
- hover/lock/selection 语义
`tv.js` 管理 `media-panel` 里的直播 / 态势新闻 tab。toolbar 打开或切换 TV/新闻时,会通过 `earth:tv-visibility-change``earth:tv-tab-change` 回写 Earth 设置:面板可见性仍按 desktop/mobile viewport 存在 `views.<scope>.panelVisibility.media-panel`,当前 tab 存在 `shared.mediaPanelActiveTab`,因此刷新页面后能恢复用户上次打开的直播或新闻状态。`closeTransientMobileOverlays()` 这类临时收起会带 `persist:false`,不会覆盖用户偏好。
其中 Earth 启动加载链现在也拆成了两层:
- `controls.js`
- 提供图层注册表与启动元信息
- `layer-startup-tasks.js`
- 提供图层启动任务注册表
- 通过 `registerLayerStartupTask(id, taskFactory)` 扩展启动任务
- `main.js`
- 只负责读取排序后的启动图层,再按映射执行队列
其中巡航模式现在已经拆成两层:
- `cruise-sequencer.js`
- 负责目标队列顺序、停留时长、切换节奏、打断与恢复
- `callout-connector.js`
- 负责卡片连线 SVG、路径计算与绘制动画
- `bgp-cruise-adapter.js`
- 负责 BGP 巡航展示适配目标排序、卡片落点、连线路径、focus/overlay/info-card 时序
当前 BGP 巡航只是这套能力的一个调用方不应再把“按队列巡航”和“BGP 事件展示”混写在同一个状态机里。
新闻巡航摘要计划见:
- [Earth 新闻巡航摘要计划](/home/ray/dev/linkong/planet/docs/plans/earth-news-cruise-summary-plan.md)
## 当前样式分层
Earth 的 CSS 不是一份大样式表,而是分层管理:
- [base.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/base.css)
- [hud.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/hud.css)
- [toolbar.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/toolbar.css)
- [layer-panel.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/layer-panel.css)
- [info-panel.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/info-panel.css)
- [legend.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/legend.css)
- [earth-stats.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/earth-stats.css)
- [coordinates-display.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/coordinates-display.css)
- [tv-panel.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/tv-panel.css)
当前建议:
- 通用 HUD 壳层写进 `hud.css`
- 单一面板特性写进各自子文件
- 不要把业务状态样式再散回 `index.html`
## 当前图层开关状态语义
Earth 图层按钮现在不应再只有“开/关”两态,而应支持:
- `inactive`
- `active`
- `loading`
当前入口在:
- [controls.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js)
- [layer-button-state.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/layer-button-state.js)
关键函数:
- `updateLayerButtonState(button, isActive)`
- `setLayerButtonState(button, options)`
`setLayerButtonState` 负责:
- `loading` 样式
- `aria-busy`
- 按钮禁用
- tooltip 更新
- 绑定状态文本更新
- 可选同步 `active`
因此后续如果别的图层也需要异步启用,应该直接走这套状态机,而不是再手写一套临时 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` 里补第二套常量。
### 船只图层与图例
AIS 船只图层入口:
- [vessels.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/vessels.js)
- [interactable.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/interactable.js)
船只图层当前负责:
- 请求 `/api/v1/vessels/snapshot` 获取当前视口初始快照;请求必须携带 `bbox``zoom`,并传入受控 `limit`
- 通过 `/ws``vessels` channel 订阅后续增量;订阅 payload 同样必须携带当前视口 `bbox``zoom``limit`
- 将聚合后的 AIS GeoJSON 转为地球局部坐标 marker 数据;后端默认 `limit=1000`,最大 `limit=5000`
- 通过 `createInteractableLayer()` 注册 Interactable 图标层
- 用按航向分桶的 `THREE.Points` 批量渲染普通船只 marker
- 按船型映射颜色;`vessels.js` 会用 `vessel_type_name` 和 AIS `vessel_type` 数字共同归一化船型
- 根据航行/停泊状态绘制三角形或圆点纹理
- 用单点 `THREE.Points` overlay 承载 hover / locked glow
- 支持 hover、lock、轨迹加载和视觉聚焦
船只图层不再是“每艘船一个 `THREE.Sprite`”。原始 Sprite 方案在拖动地球时会把透明对象排序、draw call 和对象级 raycast 成本全部放到主交互路径上;即使 BarentsWatch 免费 AIS 当前只覆盖挪威周边,也会让地球拖动明显不跟手。
当前设计把普通船只拆成少量批次:
- moving / anchored 分开。
- moving 船只按 `VESSEL_COURSE_BINS` 做航向分桶。
- 每个批次是一组 `THREE.PointsMaterial`,位置和颜色写入 `BufferGeometry` attribute。
- 普通态不带 glowhover / locked 时才在相同点位叠加带 glow 的单点 overlay。
方向标准以 AIS `course / cog` 为准:从正北开始顺时针。普通态和交互态都通过同一套 canvas 旋转规则生成纹理,避免 hover 后箭头方向和原 marker 不一致。
船型展示也必须复用同一套归一化结果。`buildVesselMarkerData()` 会把后端的 `vessel_type_name` 和 AIS 数字类型码归一化为 `type`,用于 marker 颜色;同时生成 `vessel_type_display`供详情卡、hover 简述和搜索结果显示。不要让详情卡直接只读原始 `vessel_type_name`,否则会出现 marker 已按 Cargo/Tanker 等颜色显示、卡片仍写 `Other` 的不一致。
AISStream 的 `PositionReport` 常带实时位置和 `MetaData.ShipName`,但船型通常来自低频 `ShipStaticData.Type`。后端会把 `MetaData.ShipName` 补进船名,并将类型码映射为 Cargo / Tanker / Passenger / Fishing / Military仍缺失的船型需要等待静态 AIS 消息或后续船舶资料 enrichment不能在前端凭颜色之外的信息臆造细分类。
`/api/v1/visualization/geo/vessels` 路由已移除。前端打开船只图层时应先按当前视口拉一次 `/api/v1/vessels/snapshot`,再用 WebSocket 接收同一视口内的 upsert 增量;地图拖动或缩放后应重新拉取 snapshot 并重发 vessels 订阅。后端只在当前 raw 窗口为空时受控回退到 legacy `vessel_position` / `vessel_static`,前端可通过 `diagnostics.legacy_fallback_used` 识别该状态。
新的图层接口族是 `/api/v1/layers/*`,用于把地图渲染数据和聚合面板统计分开。地图层请求必须带 `bbox``zoom` 和受控 `limit`,响应会返回 `visible_count``returned_count``diagnostics`,其中 `degraded/truncated/limit_clamped` 用于前端提示降级。右侧聚合统计不要从图层响应累加,应读取 `/api/v1/data-products``/api/v1/data-products/{product_id}/status`,因为这些统计保持全量/全局口径,不随当前视口变化。
船只 hover / click 也不再对渲染对象做 `raycaster.intersectObjects()``main.js` 只负责传入当前 Earth、camera、pointer 和命中半径,实际命中计算由 `interactable.js` 的图标层接口完成:
1. 拖动地球或惯性旋转时跳过 hover picking。
2. 对 hover picking 做轻量节流。
3. 只保留正面船只作为候选。
4. 将候选船只投影到屏幕坐标。
5.`VESSEL_POINTER_RADIUS_PX` 做像素距离命中,并取最近船只。
这样 picking 位置和用户看到的屏幕 marker 对齐,也避免 `Points` 自带 raycaster 在固定屏幕尺寸图标上的命中半径错位。
图例系统已经注册 `vessels` 模式:
- [legend.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/legend.js)
- [legend.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/legend.css)
`getVesselLegendItems()` 返回带 `shape` 的图例项:
- `shape: "vessel"`:三角形,表示航行船只。
- `shape: "dot"`:圆点,表示停泊或低速状态。
图例项颜色来自 `VESSEL_CONFIG.colors`,不要在 `legend.css` 里重新定义业务颜色。新增船型时,应优先改 `vessels.js``constants.js` 的船型映射,再同步图例项。
`interactable.js` 是后续地表图标类图层的共用入口。它当前已经承载船只、BGP 事件、BGP 观测站和算力中心图层的批量 `Points`、texture cache、hover / locked overlay、默认 glow、状态增量更新和屏幕空间 pickingBGP 事件的向外扩散圈、BGP 观测站的 halo / 覆盖扇形仍由 `bgp.js` 保留业务动画,但图标本体和 pointer 命中已经接入通用层。新增小型、中心对齐、可以参与深度测试的图标类元素时,应优先复用这个接口,而不是再次复制船只渲染逻辑。
登陆点是当前明确保留的例外:它曾接入 `Interactable`,但 pin 类 SVG 在地球边缘会被 `THREE.Points` 的深度测试裁切成碎片;关闭 depthTest 又会破坏背面遮挡语义。因此登陆点退回 `cables.js` 内的专用 `THREE.Sprite` 路径,并改为 canvas 生成的黄色扁平球纹理。它的 `altitudeOffset``renderOrder` 与海缆线一致避免漂在海缆之上Sprite 本体关闭 `depthTest` 保持球完整,背面可见性由 `isFacingCamera()` 的球体遮挡判断控制。
图标资源可以继续用 canvas draw也可以放到 `frontend/public/earth/assets/icons/` 后由 `Interactable` 预加载。asset 路径不会在每帧读取;图层加载阶段通过 `preloadAssets()` 只加载一次 SVG / 图片,之后按 `icon source + state + bucket + color` 生成 `CanvasTexture` 并复用。当前算力中心已经从 `assets/icons/compute-supercomputer.svg``assets/icons/compute-gpu-cluster.svg` 和备用 `assets/icons/compute-hdd-network.svg` 读取图标,再在 canvas 上叠加未确认位置的 `?` badge。算力中心后端在启动链路只渲染源数据自带坐标或 `compute_center_locations` 维表坐标;手动候选采集会调用 ROR 和 Nominatim/OpenStreetMap并在 GeoJSON 或候选响应中返回位置精度、置信度、来源说明和核验时间;前端详情卡展示这些字段。
算力中心图层行左上角的通知气泡显示 GeoJSON `unresolved` 数量。这个数字表示“完全没有可信坐标、不能渲染到地球上”的记录,不等同于地图上带 `?` 的已定位待确认点。点击气泡会在图层面板右侧打开固定信息卡,信息卡内容区内部滚动,不随鼠标 hover 消失。列表中的单条 `采集` 只展示候选;顶部 `一键采用` 会按当前列表顺序逐条采集、保存最高置信候选,成功一条就移除一条、重新编号,并通过 `earth:compute-center-unresolved-count-change` 同步气泡数量。批量结束后再触发 `earth:compute-center-location-saved` 刷新真实图层。
详情卡里的坐标候选状态由 [info-card.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/info-card.js) 按 `entityType:entityId` 缓存在模块内存中。用户关闭详情卡或待定位列表后再次打开同一个算力中心 / BGP 观测站,已经采集到的候选和状态文案会恢复;`一键采用` 会优先使用缓存候选,避免重复调用在线地理编码或 LLM factcheck。保存成功后该实体的候选列表会清空为“正在刷新图层”状态避免旧候选在刷新后继续误导用户。
候选行的 `预览 / 保存` 按钮采用单一的事件委托模型:每个候选根(详情卡里的 `[data-collect-cache-key]` 块,或待定位列表里的 `[data-unresolved-item]`)只挂一个 `click` 监听,由 `data-candidate-actions-bound` 幂等标记,不再混用 `pointerup` / `click` 直绑或重复委托。候选对象不再以 JSON 字符串塞进 HTML 属性后再 `JSON.parse`,按钮只携带 `data-candidate-index`handler 通过 cache-key 在模块内存的 `Map` 里取出原对象,避开 HTML 实体转义对 `&` / `<` / `"` 的破坏。点击 `预览` 会派发 `earth:preview-location-candidate`,由 `main.js``previewLocationCandidate()` 调用 `showComputeCenterLocationPreview()`:在候选经纬度上挂双层空心呼吸 sprite视觉参考 BGP 事件 ring并把视角聚焦到候选坐标切换到另一个候选会替换为新呼吸圈保存时立即清除并由 `spawnSavedComputeCenterLocation()` 即时生成正式算力中心交互图标。注意 `main.js` 没有模块级 `earth` 变量,所有 location-save / preview 处理函数必须先 `const earth = getEarth();`,否则会在事件 handler 里抛 `ReferenceError``.catch` 静默掉,外观上等同于按钮“没有反应”。
`earth:compute-center-location-saved` 之后的图层校准链路对后台刷新失败保持沉默:`spawnComputeCenterAfterLocationSave()` 已经把 toast 和 locked 状态都给了用户,`refreshComputeCentersAfterLocationSave()` 只在场景就绪时重新拉取后端数据,本身不再吐 `已保存` toast`handleComputeCenterLocationSaved()` 在 spawn 成功路径让 refresh 静默后台运行,只在 spawn 返回 `null`(场景未就绪)或抛错时才让 refresh 接管成功 toastrefresh 自身报错只走 `console.warn`,绝不冒泡成 `保存失败` 文案——保存请求本身已经成功,刷新失败属于后续同步问题。
asset 图标大小由 `Interactable``icon.fitSize` 控制。SVG / 图片文件应尽量保持原始 viewBox 和路径,不要为了在地球上显示成 60x60 而手写 `transform``drawAssetIcon()` 会把资源等比 contain 到指定尺寸并居中绘制到 atlas canvas。
`Interactable` 默认使用固定屏幕像素尺寸适合船只、BGP 事件、BGP 观测站、算力中心这类需要稳定识别的图标。如果某类图标需要跟随相机距离缩放,可以把 `sizeMode` 设为非 `"fixed"`,并用 `sizeScale.min / max / referenceFov` 控制缩放范围;单个 marker 的业务尺寸差异可以通过 `getPointSizeMultiplier()` 表达,例如 BGP 事件按严重级别调整点大小BGP 观测站按活跃度调整点大小。
`Interactable` 不再把图标本体额外抬离业务高度。`altitudeOffset` 就是 marker、hover glow、locked glow 和 picking 共同使用的地表高度;这样船只图标会继续贴着船只轨迹线,不会因为单独抬高显示位置而显得漂浮。后续如果要解决边缘 glow 裁切,应优先考虑 glow 纹理、overlay 尺寸或图层专属特效,而不是把通用图标层整体抬高。
跨 Interactable 的同坐标避让也在公共层处理。每个 marker 会保留 `icon_base_position` 作为业务原始位置;当多个 Interactable marker 归入同一个经纬度 key 时,公共层会把它们沿地表切平面排成小圈,并刷新已创建的 `THREE.Points` geometry。这样视觉位置和屏幕空间 picking 位置一致,不需要业务层再单独判断“算力中心和 BGP 事件重叠”这类场景。
接口细节、生命周期和接入示例见:
- [Earth 可交互图标接入](/home/ray/dev/linkong/planet/docs/technical/zh/earth-interactable-usage.md)
### 视角控制反馈
[controls.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js) 统一维护 Earth 缩放状态。滚轮缩放、缩放按钮和触屏双指捏合最终都会更新 `zoomLevel`,并通过 `showZoomStatusCapsule()` 显示当前缩放比例:
```javascript
showGestureStatusMessage(`缩放 ${Math.round(zoomLevel * 100)}%`, "info");
```
该提示每 90ms 最多更新一次,显示 760ms 后淡出。它是视角反馈,不是数据加载进度,也不应该写进图层 loading 状态。
[main.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/main.js) 只负责在双指捏合缩放时调用 `setZoomLevel()``showZoomStatusCapsule()`。鼠标滚轮与缩放按钮的胶囊提示应继续放在 `controls.js`,避免同一种缩放反馈散落在多个模块。
拖拽地球的旋转灵敏度会根据当前缩放连续衰减,而不是按某个缩放阈值分段:
```javascript
const scale = THREE.MathUtils.clamp(
Math.pow(zoom, -CONFIG.dragRotationZoomExponent),
CONFIG.dragRotationScaleMin,
CONFIG.dragRotationScaleMax,
);
```
调参入口在 [constants.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/constants.js)`dragRotationFactorBase` 控制基础速度,`dragRotationZoomExponent` 控制放大后的衰减曲线,`dragRotationScaleMin` / `dragRotationScaleMax` 控制上下限。
### `data-status-target`
图层按钮可以通过:
- `data-status-target`
指向一个状态文本节点。当前 terrain 已接入:
- 按钮:`#toggle-terrain`
- 状态节点:`#terrain-status`
地形不是默认可见图层时,启动期不会立即阻塞加载地形瓦片。`controls.js` 会在图层可见性恢复完成后才调度 `scheduleTerrainPrefetch()`,并且只在高清材质可用、地形尚未 ready、预取未开始时执行。预取使用 `setTimeout` + `requestIdleCallback`,避免和首屏云图、高清材质、图层启动队列抢主线程。
地形瓦片请求也不再逐个散发大量单 tile 请求。`terrain.js` 会把需要的 Terrarium tile 去重后按 `TERRAIN_CONFIG.batchRequestSize` 分批请求 `/api/v1/visualization/terrain/terrarium/batch`;后端用 LRU 内存缓存、批次去重和并发限制代理 S3 Terrarium tile。单 tile endpoint 仍保留给回退路径和浏览器缓存语义。
以后别的异步图层也可以沿用这套约定。
## 当前设置持久化
Earth 设置面板当前由 [controls.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js) 统一负责:
- 捕获默认值
-`localStorage` 读取上次设置
- 初始化应用当前设置
- 用户变更后即时持久化
- 一键重置回默认值
当前持久化的范围是:
- 旋转模式
- 地球默认大小(作为重置视角、缩放重置和巡航视图的默认 zoom 真源)
- HUD 面板显示/隐藏
- 图层控制开关:`地形 / 卫星 / 海缆 / BGP`
- 卫星显示偏好:`卫星显示风格 / 卫星呼吸闪烁 / 真实卫星高度 / 轨迹显示`
- 地表 hover 提示偏好:`国家 / 位置 / 完整`
- 国界精度偏好:低精 fallback / 高精 PMTiles
- 地形透明度
也就是说Earth 设置不是一次性 UI 状态了,而是本地设备级偏好。后续如果再加入新的设置项,应优先接入同一条持久化链,而不是各自散着写 `localStorage`
地表 hover 提示由 `controls.js` 持久化为 `shared.surfaceHoverInfoMode`,实际 tooltip 在 `main.js` 的地表 hover 分支组合。`国家` 模式只在命中国家时显示国家信息,海洋区域不显示地表 tooltip`位置` 模式只显示纬度、经度和地形采样海拔,并清除国家边界 hover`完整` 模式在陆地显示国家 + 位置,在海洋显示位置。
卫星真实高度开关由 `controls.js` 持久化,实际渲染状态在 `satellites.js`。开启时SGP4 得到的真实半径会按对数压缩到当前 Earth 视觉半径范围;关闭时,卫星点、轨迹和预测轨道都回到旧版同层球面。切换时必须刷新卫星位置并清理轨迹缓存,避免同一条轨迹混入两个高度模型。`maxRealAltitudeOffset = 25` 是当前相机和 `earthRadius = 100` 下的视觉上限:它让 GEO / MEO 比 LEO 明显更高,但把最高轨道控制在地球半径外约 25%,避免选择点、红色轨迹和主体地球之间出现过大的空场。
SGP4 传播输出是惯性系位置,不能直接当成 Earth 的经纬度固定坐标使用。`satellites.js` 会用当前时间的 `gstime` 把 ECI/TEME 位置转换到 ECF再映射到 `latLonToVector3()` 使用的 Three.js 坐标轴。卫星点和短尾迹使用随采样时间变化的地固坐标,表示相对当前地球表面的实际位置;锁定后的预测轨道线使用锁定时刻固定的 `gstime`把未来一圈惯性轨道投到当前地球姿态上显示因此会闭合并且轨道面倾角应与详情卡一致。fallback 预测轨道也必须使用真正的 RAAN + inclination 轨道平面公式,不能把 inclination 当成恒定纬度。
国界精度偏好独立存储在 `country-boundaries.js``planet.earth.boundaries.highPrecisionEnabled`。未开启高精时,即使本机已经有高精 manifest/PMTiles也继续加载低精 `countries-admin0.min.geojson` fallback开启高精但高精产物缺失时Earth 工具栏设置会调用 `/api/v1/earth/boundaries/build` 启动后台构建并轮询进度。构建成功后调用 `reloadCountryBoundaries()` 热切换,不再刷新整个页面。国界 hover 与 tooltip 解耦:只要地表坐标落在国界 polygon 内就保持高亮如果鼠标同时命中卫星、船只、BGP 等 interactabletooltip 显示 interactable 信息,但国界高亮不应闪烁。
## 当前地形链路
真实地形首次启用会慢,原因不只是一个:
1. 需要拉取 Terrarium 瓦片
2. 需要解码图片
3. 需要按顶点采样高程
4. 需要重新写入 geometry 和 color
5. 需要重新计算法线与包围体
当前入口在:
- [terrain.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/terrain.js)
当前已经做了两层体验优化:
1. 图层开关 loading 状态持续可见
2. 页面空闲时会预热 `ensureTerrainReady()`
## 当前巡航链路
当前巡航边界:
- 通用巡航层包含:
- 当前目标
- 队列顺序
- 相机 focus
- 停留 / 隐藏 / 切换
- 业务模块提供:
- 提供目标队列
- 提供 focus 坐标
- 提供卡片内容
- 提供高亮/图层副作用
巡航模块的结构文件:
- [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)
- [bgp-cruise-adapter.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp-cruise-adapter.js)