36 KiB
Earth 前端结构
本文件描述当前 Earth 大屏前端的真实结构,重点是帮助后续继续改 HUD、图层、媒体面板、真实地形、BGP 可视化时,不再重复踩结构和状态同步上的坑。
相关规则建议一起参考:
当前目标
Earth 前端不是普通管理页,它是独立的大屏展示前端。当前产品目标是:
- 维持地球视图的空间感和可读性
- 让 HUD、图层、媒体面板、BGP、卫星、海缆等保持统一交互
- 把加载中、已启用、已隐藏、锁定中这类状态做清楚
当前入口
React 路由入口:
当前做法很简单:
- React 页面只负责提供一个全屏
iframe - 真正的 Earth 应用运行在:
所以 Earth 前端本质上是 public/earth 下的一套独立静态应用。
当前文件分层
1. 页面入口与结构
职责:
- HUD 基础 DOM
- 图层面板
- 媒体面板
- 工具栏
- 设置弹窗
- 兼容旧元素 id
2. 主运行时
职责:
- 地球初始化
- Three.js 场景组装
- 数据加载与刷新
- 各图层集成
- Earth 级别状态同步
3. 地球控制层
职责:
- 工具栏交互
- 图层面板交互
- 旋转/缩放/布局
- HUD 面板拖拽
- 图层开关状态机
- Earth 设置读取、持久化与重置
这份文件是 Earth 前端当前最核心的 UI 控制入口。
Earth 设置面板现在按 data-settings-tab 和 data-settings-tab-panel 分类组织。桌面端和移动端使用同一组分类语义:运行、显示、面板、动捕、快捷键、系统。新增设置项时应先判断它属于哪个分类,再补 DOM、持久化字段和恢复逻辑;不要把所有控件继续堆到一个长面板里。
快捷键配置属于设备本地偏好,由 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_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 负责。它监听 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 的 recognizeGesture(),按以下顺序匹配,前者命中即返回:
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 抢先误判。getZoomHoldPose(姿态 zoom):动作停止后,只要两腕仍保持在胸口及以上、且 span >ZOOM_HOLD_SPREAD_FACTOR × shoulderWidth(默认 1.30)就持续派发zoom_in;两肘明显外展且 span <ZOOM_HOLD_CLOSE_FACTOR × shoulderWidth(默认 0.85)就持续派发zoom_out。- layer / focus:左手抬起 + 上下移动派发
layer_prev/next;头部左右倾斜派发focus_prev/next。 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 层。第一阶段只接入 Motion:motion-cruise-adapter.js 通过 persistent presentation 复用巡航固定卡片位置和 connector,但不会让鼠标移动触发自动隐藏;connector 每帧重算 source/target anchor,让卡片拖动、地球旋转和目标移动时端点继续跟随。BGP/News 仍保持原有 CruiseSequencer 自动轮播路径,避免改变既有巡航体验。
6. 地球与地形
职责:
- 地球球体、云层、大气
- 真实地形 mesh
- terrain tile 拉取、解码、位移、着色
- 海陆基座与国界底图的整球 overlay
Earth 地表是多层近似同心球,不是单一 mesh。earth.js 的基座球、高清材质 overlay、云层/大气,以及 country-boundaries.js 的海陆基座都需要明确半径间距。远距视图下 GPU 深度精度会下降,相邻 shell 过近会 z-fighting,表现为黑色闪烁块或雪花。当前稳定策略是让海陆基座使用 landAltitudeOffset = 0.32,高清材质使用 textureOverlayAltitudeOffset = 0.48;后续新增或调整整球地表 overlay 时,必须同步检查 Earth 渲染图层顺序,并在 50% 缩放视图验证。
7. 图层模块
- satellites.js
- cables.js
- bgp.js
- bgp-cruise-adapter.js
- vessels.js
- news.js
- tv.js
- layer-startup-tasks.js
- cruise-sequencer.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,不会覆盖用户偏好。
brand.js 管理 Earth HUD 品牌资源。默认品牌来自静态资源,运行时覆盖值来自 /api/v1/earth/brand,上传的图片通过 /earth-brand-assets/... 读取。前端必须把 logo/title 图片和文本 fallback 分开处理:图片加载失败时显示文本标题,文本字段为空时使用后端默认值,避免 HUD 品牌区空白。控制台的 Earth 内容页负责保存和重置品牌配置,Earth 前端只消费结果。
about.js 管理 Earth 设置里的“关于”卡片。默认内容仍保留在前端作为兜底,运行时优先读取 /api/v1/earth/about。接口失败或字段缺失时必须回退默认值,避免设置页出现空白。Admin Next 的 Earth 内容页提供“关于”tab,保存走 PUT /api/v1/earth/about,恢复默认走 DELETE /api/v1/earth/about。
oobe.js 管理 Earth 首次初始化引导。是否显示 OOBE 必须由 /api/v1/earth/oobe-status 的 ready 字段决定,不能依赖 localStorage 判断系统是否初始化。localStorage 只允许记录“本浏览器暂时跳过”的短时状态;如果后端已经认为 ready: true,退出登录、清空本地缓存或换浏览器都不应再次弹出 OOBE。桌面端使用深色星空遮罩和毛玻璃启动面板,移动端改为底部 sheet,并尊重 prefers-reduced-motion。
Admin Next 的 Earth 内容页必须按运行时语义组织这些配置:
品牌标识:品牌预览应使用与 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 事件展示”混写在同一个状态机里。
新闻巡航摘要计划见:
当前样式分层
Earth 的 CSS 不是一份大样式表,而是分层管理:
- base.css
- hud.css
- toolbar.css
- layer-panel.css
- info-panel.css
- legend.css
- earth-stats.css
- coordinates-display.css
- tv-panel.css
当前建议:
- 通用 HUD 壳层写进
hud.css - 单一面板特性写进各自子文件
- 不要把业务状态样式再散回
index.html
当前图层开关状态语义
Earth 图层按钮现在不应再只有“开/关”两态,而应支持:
inactiveactiveloading
当前入口在:
关键函数:
updateLayerButtonState(button, isActive)setLayerButtonState(button, options)
setLayerButtonState 负责:
loading样式aria-busy- 按钮禁用
- tooltip 更新
- 绑定状态文本更新
- 可选同步
active
因此后续如果别的图层也需要异步启用,应该直接走这套状态机,而不是再手写一套临时 loading class。
另外,Earth 图层控制现在已经收成“注册表驱动”:
- 图层元数据
idiconlabelmetabuttonIdpersiststartupPrioritystartupModestartupLabelstartupMessage
- 图层行为
getVisible()setVisible(next, options)
当前入口仍在 controls.js。
这意味着后续新增图层时,优先应补一条图层注册定义,而不是同时去改:
- 图层面板 HTML
- 持久化快照
- 初始化恢复
- click 绑定
这四处现在都应该由注册表派生。
其中:
startupPriority- 描述图层参与启动加载时的顺序
startupModevisible- 仅当前图层处于启用/可见状态时,才加入启动加载队列
preload- 即使当前图层未显示,也会参与启动预加载
当前 main.js 会通过注册表读取排序后的启动图层列表,再动态拼装启动加载队列,而不是手写一串固定步骤。像 BGP 这类需要尽早准备数据、但不一定默认显示的图层,应该优先走 startupMode: "preload",而不是在启动流程里写隐式特判。
此外,启动阶段给用户看的提示文案也应尽量从注册表派生:
startupLabel- 用于描述当前启动任务的业务名称
startupMessage- 用于描述启动中的提示文案
- 可以是字符串
- 也可以是对象,用于像海缆这种“准备阶段 / 主加载阶段”两段式文案
这样后续新增会参与启动加载的图层时,顺序、模式和提示文案都在同一处定义,不需要再去 main.js 里补第二套常量。
船只图层与图例
AIS 船只图层入口:
船只图层当前负责:
- 请求
/api/v1/vessels/snapshot获取当前视口初始快照;请求必须携带bbox、zoom,并传入受控limit - 通过
/ws的vesselschannel 订阅后续增量;订阅 payload 同样必须携带当前视口bbox、zoom、limit - 将聚合后的 AIS GeoJSON 转为地球局部坐标 marker 数据;后端默认
limit=1000,最大limit=5000 - 通过
createInteractableLayer()注册 Interactable 图标层 - 用按航向分桶的
THREE.Points批量渲染普通船只 marker - 按船型映射颜色;
vessels.js会用vessel_type_name和 AISvessel_type数字共同归一化船型 - 根据航行/停泊状态绘制三角形或圆点纹理
- 用单点
THREE.Pointsoverlay 承载 hover / locked glow - 支持 hover、lock、轨迹加载和视觉聚焦
船只图层不再是“每艘船一个 THREE.Sprite”。原始 Sprite 方案在拖动地球时会把透明对象排序、draw call 和对象级 raycast 成本全部放到主交互路径上;即使 BarentsWatch 免费 AIS 当前只覆盖挪威周边,也会让地球拖动明显不跟手。
当前设计把普通船只拆成少量批次:
- moving / anchored 分开。
- moving 船只按
VESSEL_COURSE_BINS做航向分桶。 - 每个批次是一组
THREE.PointsMaterial,位置和颜色写入BufferGeometryattribute。 - 普通态不带 glow;hover / 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 的图标层接口完成:
- 拖动地球或惯性旋转时跳过 hover picking。
- 对 hover picking 做轻量节流。
- 只保留正面船只作为候选。
- 将候选船只投影到屏幕坐标。
- 用
VESSEL_POINTER_RADIUS_PX做像素距离命中,并取最近船只。
这样 picking 位置和用户看到的屏幕 marker 对齐,也避免 Points 自带 raycaster 在固定屏幕尺寸图标上的命中半径错位。
图例系统已经注册 vessels 模式:
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、状态增量更新和屏幕空间 picking;BGP 事件的向外扩散圈、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 按 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 接管成功 toast,refresh 自身报错只走 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 事件重叠”这类场景。
接口细节、生命周期和接入示例见:
视角控制反馈
controls.js 统一维护 Earth 缩放状态。滚轮缩放、缩放按钮和触屏双指捏合最终都会更新 zoomLevel,并通过 showZoomStatusCapsule() 显示当前缩放比例:
showGestureStatusMessage(`缩放 ${Math.round(zoomLevel * 100)}%`, "info");
该提示每 90ms 最多更新一次,显示 760ms 后淡出。它是视角反馈,不是数据加载进度,也不应该写进图层 loading 状态。
main.js 只负责在双指捏合缩放时调用 setZoomLevel() 和 showZoomStatusCapsule()。鼠标滚轮与缩放按钮的胶囊提示应继续放在 controls.js,避免同一种缩放反馈散落在多个模块。
拖拽地球的旋转灵敏度会根据当前缩放连续衰减,而不是按某个缩放阈值分段:
const scale = THREE.MathUtils.clamp(
Math.pow(zoom, -CONFIG.dragRotationZoomExponent),
CONFIG.dragRotationScaleMin,
CONFIG.dragRotationScaleMax,
);
调参入口在 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 统一负责:
- 捕获默认值
- 从
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 等 interactable,tooltip 显示 interactable 信息,但国界高亮不应闪烁。
当前地形链路
真实地形首次启用会慢,原因不只是一个:
- 需要拉取 Terrarium 瓦片
- 需要解码图片
- 需要按顶点采样高程
- 需要重新写入 geometry 和 color
- 需要重新计算法线与包围体
当前入口在:
当前已经做了两层体验优化:
- 图层开关 loading 状态持续可见
- 页面空闲时会预热
ensureTerrainReady()
当前巡航链路
当前巡航边界:
- 通用巡航层包含:
- 当前目标
- 队列顺序
- 相机 focus
- 停留 / 隐藏 / 切换
- 业务模块提供:
- 提供目标队列
- 提供 focus 坐标
- 提供卡片内容
- 提供高亮/图层副作用
巡航模块的结构文件: