Files
planet/docs/technical/zh/earth-frontend-context.md
linkong 899e3bce43
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
release: bump version to 0.71.0
2026-06-11 16:47:24 +08:00

39 KiB
Raw Blame History

智能星球前端结构

本文件描述当前智能星球前端的真实结构,重点是帮助后续继续改 HUD、图层、媒体面板、真实地形、BGP 可视化时,不再重复踩结构和状态同步上的坑。

相关规则建议一起参考:

当前目标

Earth 前端不是普通管理页,它是独立的大屏展示前端。当前产品目标是:

  • 维持地球视图的空间感和可读性
  • 让 HUD、图层、媒体面板、BGP、卫星、海缆等保持统一交互
  • 把加载中、已启用、已隐藏、锁定中这类状态做清楚

当前入口

React 路由入口:

当前做法很简单:

  • React 页面只负责提供一个全屏 iframe
  • 真正的 Earth 应用运行在:

所以 Earth 前端本质上是 public/earth 下的一套独立静态应用。

当前文件分层

1. 页面入口与结构

职责:

  • HUD 基础 DOM
  • 图层面板
  • 媒体面板
  • 工具栏
  • 设置弹窗
  • 兼容旧元素 id

2. 主运行时

职责:

  • 地球初始化
  • Three.js 场景组装
  • 数据加载与刷新
  • 各图层集成
  • Earth 级别状态同步

Earth 收到 /wsearth_updates 时只把它当作刷新提示,真实数据仍通过 /api/v1/visualization/... 接口重新 GET。数据库驱动的刷新由后端 listener 直接清理缓存再广播,不再默认经过 earth_refresh 作业队列;前端收到 database_changed 后会按 layer 读取 clear_then_reloadreloaddelta 策略。clear_then_reload 必须先清 Three.js 对象再 no-store 重拉summary 只做一致性校验,不能用 0 作为跳过图层重拉的理由。技术链路见 数据作业与 Outbox 技术架构,业务数据流见 业务架构与数据流转

3. 地球控制层

职责:

  • 工具栏交互
  • 图层面板交互
  • 旋转/缩放/布局
  • HUD 面板拖拽
  • 图层开关状态机
  • Earth 设置读取、持久化与重置

这份文件是 Earth 前端当前最核心的 UI 控制入口。

Earth 设置面板现在按 data-settings-tabdata-settings-tab-panel 分类组织。桌面端和移动端使用同一组分类语义:运行、显示、面板、动捕、快捷键、系统。新增设置项时应先判断它属于哪个分类,再补 DOM、持久化字段和恢复逻辑不要把所有控件继续堆到一个长面板里。

显示 分类里的新闻类型选择复用巡航模块的 chip 选择器形态只控制星球端当前浏览器的新闻分类显示。它不会打开或关闭图层、底图、边界、TV、数据点、BGP、船舶、卫星或算力中心这些仍由图层面板、媒体面板和控制台配置各自负责。controls.js 只持久化 shared.newsCategoryFilters 并广播 earth:news-category-filters-changenews.js 会把选中的类型拼到 /api/v1/news/earth-feed?categories=...&locale=zh-CN,让 Web 和 UE 走同一套后端类型过滤。

快捷键配置属于设备本地偏好,由 controls.js 负责读取、捕获、启用/禁用和重置。它不应写入后端用户设置,也不应影响其它浏览器。后续新增快捷键时,必须同时提供默认键、显示标签、可禁用状态和重置路径,避免只在 keydown handler 中硬编码。

4. UI 与状态消息

职责:

  • loading 面板
  • status message
  • tooltip / error / 清理逻辑

当前 status message 有两类短提示:

  • 普通业务提示:通过 showStatusMessage() 入队显示。
  • 手势提示:通过 showGestureStatusMessage() 直接短暂显示,用于缩放视角时的 缩放 N%

手势提示不会抢占 loading 状态。对应样式是 hud.css 中的 .earth-status-message.gesture

5. 动作捕捉控制适配层

职责:

  • 作为 Motion Provider manager统一接入 browser_cameramotion_agent
  • 默认使用浏览器 getUserMedia + 本地 MediaPipe 识别;高级模式可连接本地 Motion Capture Agent WebSocket。
  • 处理浏览器摄像头权限/安全上下文错误,以及 Agent 断线重连和 status / heartbeat
  • 过滤低置信度和过快重复的手势事件。
  • rotate_leftrotate_rightrotate_uprotate_downzoom_inzoom_outfocus_prevfocus_nextlayer_prevlayer_nextconfirm 映射到 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。shared.motionEnabledGestures 保存用户允许识别的动作浏览器端先过滤Motion Agent 模式还会通过 set_enabled_gestures 同步给服务端。

动捕调试面板由 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.v2shared.motionDebugEnabledshared.motionProvidershared.motionDebugSkeletonOnlyshared.motionEnabledGesturesswitch、输入源和动作白名单控件都预留 data-gatekeeper-permission="earth.motion_debug"

Browser Camera provider 的手势识别管线在 motion-browser-provider.jsrecognizeGesture(),按以下顺序匹配,前者命中即返回:

  1. getZoomTrend(趋势 zoom:基于上一帧到当前帧两腕 x 间距的变化量(Math.abs(rightWrist.x - leftWrist.x) 的 delta。两腕都越过噪声地板ZOOM_TREND_MIN_WRIST_DELTA = 0.010)且高度差不超过 ZOOM_TREND_HEIGHT_TOLERANCEspread 增加 → zoom_inspread 减少 → 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 是新的 Presentation 层。第一阶段只接入 Motionmotion-cruise-adapter.js 通过 persistent presentation 复用巡航固定卡片位置和 connector但不会让鼠标移动触发自动隐藏connector 每帧重算 source/target anchor让卡片拖动、地球旋转和目标移动时端点继续跟随。BGP/News 仍保持原有 CruiseSequencer 自动轮播路径,避免改变既有巡航体验。

6. 地球与地形

职责:

  • 地球球体、云层、大气
  • 真实地形 mesh
  • terrain tile 拉取、解码、位移、着色
  • 海陆基座与国界底图的整球 overlay

智能星球地表是多层近似同心球,不是单一 mesh。earth.js 的基座球、高清材质 overlay、云层/大气,以及 country-boundaries.js 的海陆基座都需要明确半径间距。远距视图下 GPU 深度精度会下降,相邻 shell 过近会 z-fighting表现为黑色闪烁块或雪花。当前稳定策略是让海陆基座使用 landAltitudeOffset = 0.32,高清材质使用 EARTH_SURFACE_TEXTURE_ALTITUDE_OFFSET = 0.48国界线、coastline、claim 线和国界 hover 线也必须使用同一个高清材质壳半径,避免转动地球时相对高清贴图产生视差漂浮感。后续新增或调整整球地表 overlay 时,必须同步检查 智能星球渲染图层顺序,并在 50% 缩放视图验证。

7. 图层模块

职责:

  • 各自的数据层
  • 开关行为
  • 面板内容
  • hover/lock/selection 语义

tv.js 管理 media-panel 里的直播 / 态势新闻 tab。toolbar 打开或切换 TV/新闻时,会通过 earth:tv-visibility-changeearth:tv-tab-change 回写 Earth 设置:面板可见性仍按 desktop/mobile viewport 存在 views.<scope>.panelVisibility.media-panel,当前 tab 存在 shared.mediaPanelActiveTab,因此刷新页面后能恢复用户上次打开的直播或新闻状态。closeTransientMobileOverlays() 这类临时收起会带 persist:false,不会覆盖用户偏好。

brand.js 管理智能星球 HUD 品牌资源。默认品牌来自静态资源,运行时覆盖值来自 /api/v1/earth/brand,上传的图片通过 /earth-brand-assets/... 读取。前端必须把 logo/title 图片和文本 fallback 分开处理:图片加载失败时显示文本标题,文本字段为空时使用后端默认值,避免 HUD 品牌区空白。控制台的智能星球内容页负责保存和重置品牌配置,智能星球前端只消费结果。

about.js 管理智能星球设置里的“关于”卡片。默认内容仍保留在前端作为兜底,运行时优先读取 /api/v1/earth/about。接口失败或字段缺失时必须回退默认值避免设置页出现空白。控制台的智能星球内容页提供“关于”tab保存走 PUT /api/v1/earth/about,恢复默认走 DELETE /api/v1/earth/about

oobe.js 管理 Earth 首次初始化引导。是否显示 OOBE 必须由 /api/v1/earth/oobe-statusready 字段决定,不能依赖 localStorage 判断系统是否初始化。localStorage 只允许记录“本浏览器暂时跳过”的短时状态;如果后端已经认为 ready: true,退出登录、清空本地缓存或换浏览器都不应再次弹出 OOBE。桌面端使用深色星空遮罩和毛玻璃启动面板移动端改为底部 sheet并尊重 prefers-reduced-motion

控制台的智能星球内容页必须按运行时语义组织这些配置:

  • 品牌标识:品牌预览应使用与 Earth HUD 左上角一致的深色星空背景、尺寸、间距、logo/title 渲染和文本 fallback而不是普通表单预览。
  • 关于:配置 Earth 设置里的 About 卡片,包括 logo、眉标、标题、版本、描述和元信息条目Earth 运行时从 /earth/about 读取,失败时回退默认内容。
  • 国界精度:构建边界、刷新状态和恢复默认属于这个分区内部动作,不应放在页面全局工具栏。
  • 电视直播:列表同时区分内置源、采集源和自定义源;卡片状态表达启用、停用、新建或错误。内置源不能删除,采集源和自定义源可以删除。新增直播源在点击加号后才进入草稿状态,保存后固化到列表,取消则销毁草稿。
  • 底图资源图层资源3D 模型新闻锚点策略:如果后端能力未接入,控制台应明确显示待接入空态,不应混入 TV 或品牌配置。

TV 预览需要尽量复用 Earth 运行时的直播卡片结构和状态标签,这样控制台看到的内置、直播加载中、播放源、地区、语言等信息与 Earth 中实际显示保持一致。

其中 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 事件展示”混写在同一个状态机里。

新闻巡航摘要的未来计划保存在仓库路径 docs/plans/earth-news-cruise-summary-plan.md,不作为公开 Docs 页面入口。

当前样式分层

Earth 的 CSS 不是一份大样式表,而是分层管理:

当前建议:

  • 通用 HUD 壳层写进 hud.css
  • 单一面板特性写进各自子文件
  • 不要把业务状态样式再散回 index.html

当前图层开关状态语义

Earth 图层按钮现在不应再只有“开/关”两态,而应支持:

  • inactive
  • active
  • loading

当前入口在:

关键函数:

  • 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

这意味着后续新增图层时,优先应补一条图层注册定义,而不是同时去改:

  • 图层面板 HTML
  • 持久化快照
  • 初始化恢复
  • click 绑定

这四处现在都应该由注册表派生。

其中:

  • startupPriority
    • 描述图层参与启动加载时的顺序
  • startupMode
    • visible
      • 仅当前图层处于启用/可见状态时,才加入启动加载队列
    • preload
      • 即使当前图层未显示,也会参与启动预加载

当前 main.js 会通过注册表读取排序后的启动图层列表,再动态拼装启动加载队列,而不是手写一串固定步骤。像 BGP 这类需要尽早准备数据、但不一定默认显示的图层,应该优先走 startupMode: "preload",而不是在启动流程里写隐式特判。

此外,启动阶段给用户看的提示文案也应尽量从注册表派生:

  • startupLabel
    • 用于描述当前启动任务的业务名称
  • startupMessage
    • 用于描述启动中的提示文案
    • 可以是字符串
    • 也可以是对象,用于像海缆这种“准备阶段 / 主加载阶段”两段式文案

这样后续新增会参与启动加载的图层时,顺序、模式和提示文案都在同一处定义,不需要再去 main.js 里补第二套常量。

船只图层与图例

AIS 船只图层入口:

船只图层当前负责:

  • 请求 /api/v1/vessels/snapshot 获取全局当前状态快照Earth 前端统一传全球 bbox、当前 zoomlimit=3000
  • vessel_current_state 当前状态 GeoJSON 转为地球局部坐标 marker 数据;历史 AIS 原始观测只用于轨迹、审计和态势分析
  • 通过 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。
  • cluster: falseavoidance: false,密集海域允许重叠,不参与动态屏幕聚类。
  • 地球旋转和缩放不会重新请求船只,也不会按当前镜头 bbox 重连 WebSocket。

方向标准以 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/snapshotAPI 参数里的 bbox 是后端接口约束Earth 运行时传全球范围,不表示当前镜头视口。/wsvessels channel 如启用,只作为低频 reload/dirty 提示,不能把每条 AIS delta 直接变成整层重建。后端通过 diagnostics.source == "vessel_current_state" 暴露当前状态链路。

新的图层接口族是 /api/v1/layers/*,用于把地图渲染数据和聚合面板统计分开。地图层请求必须带 bboxzoom 和受控 limit,响应会返回 visible_countreturned_countdiagnostics,其中 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 模式:

getVesselLegendItems() 返回带 shape 的图例项:

  • shape: "vessel":三角形,表示航行船只。
  • shape: "dot":圆点,表示停泊或低速状态。

图例项颜色来自 VESSEL_CONFIG.colors,不要在 legend.css 里重新定义业务颜色。新增船型时,应优先改 vessels.jsconstants.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 生成的黄色扁平球纹理。它的 altitudeOffsetrenderOrder 与海缆线一致避免漂在海缆之上Sprite 本体关闭 depthTest 保持球完整,背面可见性由 isFacingCamera() 的球体遮挡判断控制。

图标资源可以继续用 canvas draw也可以放到 frontend/public/earth/assets/icons/ 后由 Interactable 预加载。asset 路径不会在每帧读取;图层加载阶段通过 preloadAssets() 只加载一次 SVG / 图片,之后按 icon source + state + bucket + color 生成 CanvasTexture 并复用。当前算力中心已经从 assets/icons/compute-supercomputer.svgassets/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.jsentityType:entityId 缓存在模块内存中。用户关闭详情卡或待定位列表后再次打开同一个算力中心 / BGP 观测站,已经采集到的候选和状态文案会恢复;一键采用 会优先使用缓存候选,避免重复调用在线地理编码或 LLM factcheck。保存成功后该实体的候选列表会清空为“正在刷新图层”状态避免旧候选在刷新后继续误导用户。

候选行的 预览 / 保存 按钮采用单一的事件委托模型:每个候选根(详情卡里的 [data-collect-cache-key] 块,或待定位列表里的 [data-unresolved-item])只挂一个 click 监听,由 data-candidate-actions-bound 幂等标记,不再混用 pointerup / click 直绑或重复委托。候选对象不再以 JSON 字符串塞进 HTML 属性后再 JSON.parse,按钮只携带 data-candidate-indexhandler 通过 cache-key 在模块内存的 Map 里取出原对象,避开 HTML 实体转义对 & / < / " 的破坏。点击 预览 会派发 earth:preview-location-candidate,由 main.jspreviewLocationCandidate() 调用 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() 只在场景就绪时重新拉取后端数据,本身不再吐 已保存 toasthandleComputeCenterLocationSaved() 在 spawn 成功路径让 refresh 静默后台运行,只在 spawn 返回 null(场景未就绪)或抛错时才让 refresh 接管成功 toastrefresh 自身报错只走 console.warn,绝不冒泡成 保存失败 文案——保存请求本身已经成功,刷新失败属于后续同步问题。

asset 图标大小由 Interactableicon.fitSize 控制。SVG / 图片文件应尽量保持原始 viewBox 和路径,不要为了在地球上显示成 60x60 而手写 transformdrawAssetIcon() 会把资源等比 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 的同坐标关系也在公共层记录,但真实位置必须始终以 icon_base_position 为准。缩放、避让、聚合和后续 spiderfy 展开都只能改变屏幕表现,不能写回 marker.positionTHREE.Points 里的业务锚点;巡航定位、详情卡、搜索定位和 picking 返回对象都必须落回真实经纬度。多个图标归入同一个经纬度 key 时,公共层只写 icon_avoidance_* 元数据,供业务层弱化 halo 或显示聚合提示;真正的低缩放聚合应通过独立 cluster glyph / screen layout 层实现,而不是把对象沿地表切平面挪开。

Interactable 的单点显示只由全局地图缩放决定170% 及以下强制显示小圆点,超过 170% 显示原图标。cluster 现在由 cluster.strategy 决定:stable-spherical 使用离散 zoom band 和 3D 球面分桶BGP、算力中心和 Earth interactable 在同一 band 内旋转或细微缩放时不会重新计算聚合拓扑;dynamic-screen 保留屏幕空间聚类,适合船只这类实时高频图层;none 关闭聚类。稳定球面聚类的 cluster 圆点刚性落在成员 3D 质心投影上,不参与 2D 避让避免缩放时被推离真实地理位置。cluster 圆点大小随包含对象数量增长,数量过多时按稳定地理顺序拆成多个较小圆点;数量默认只在 hover tooltip 中显示。这个过程只设置 icon_cluster_* 展示元数据和重建渲染 Points不改变每个 marker 的真实经纬度。

接口细节、生命周期和接入示例见:

视角控制反馈

controls.js 统一维护智能星球缩放状态,所有入口最终都必须调用 setZoomLevel() 写相机距离。不要在其他模块直接写 camera.position.z,否则缩放百分比、拖拽灵敏度和 Interactable 聚合阈值会再次分叉。

滚轮输入分两类处理:传统鼠标滚轮保留 10% 档位和短动画,并用 wheelZoomTarget 作为连续滚动的逻辑基准;触控板 / 高精度滚轮按 pixel delta 连续缩放,直接调用 setZoomLevel(),不走 10% 档位动画。触控板路径还会过滤反向后的短窗口旧方向小 residual delta避免惯性尾巴把用户刚反向的缩放又拉回旧方向。

该提示每 90ms 最多更新一次,显示 760ms 后淡出。它是视角反馈,不是数据加载进度,也不应该写进图层 loading 状态。

main.js 只负责在双指捏合缩放和动捕缩放时调用 setZoomLevel()showZoomStatusCapsule()。鼠标滚轮与缩放按钮的胶囊提示应继续放在 controls.js,避免同一种缩放反馈散落在多个模块。

拖拽地球的旋转灵敏度会根据当前缩放连续衰减,而不是按某个缩放阈值分段:

const scale = THREE.MathUtils.clamp(
  Math.pow(zoom, -CONFIG.dragRotationZoomExponent),
  CONFIG.dragRotationScaleMin,
  CONFIG.dragRotationScaleMax,
);

调参入口在 constants.jsdragRotationFactorBase 控制基础速度,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 统一负责:

  • 捕获默认值
  • 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.jsplanet.earth.boundaries.highPrecisionEnabled。未开启高精时,即使本机已经有高精 manifest/PMTiles也继续加载低精 countries-admin0.min.geojson fallback开启高精但高精产物缺失时智能星球工具栏设置会调用 /api/v1/earth/boundaries/build 启动后台构建并轮询进度。构建成功后调用 reloadCountryBoundaries() 热切换,不再刷新整个页面。国界 hover 与 tooltip 解耦:只要地表坐标落在国界 polygon 内就保持高亮如果鼠标同时命中卫星、船只、BGP 等 interactabletooltip 显示 interactable 信息,但国界高亮不应闪烁。

当前地形链路

真实地形首次启用会慢,原因不只是一个:

  1. 需要拉取 Terrarium 瓦片
  2. 需要解码图片
  3. 需要按顶点采样高程
  4. 需要重新写入 geometry 和 color
  5. 需要重新计算法线与包围体

当前入口在:

当前已经做了两层体验优化:

  1. 图层开关 loading 状态持续可见
  2. 页面空闲时会预热 ensureTerrainReady()

当前巡航链路

当前巡航边界:

  • 通用巡航层包含:
    • 当前目标
    • 队列顺序
    • 相机 focus
    • 停留 / 隐藏 / 切换
  • 业务模块提供:
    • 提供目标队列
    • 提供 focus 坐标
    • 提供卡片内容
    • 提供高亮/图层副作用

巡航模块的结构文件: