把星座「握」在手里:3D 交互与运营大屏的落地手记

十月下旬,M3 的下半场做完了两件事:让 3D 地球从「能看」变成「能操作」,让数据从「在电脑上看」变成「能挂到墙上」。

这篇不列任务清单,也不铺指标表,只回答三个问题:为什么这么做、用起来是什么手感、哪些地方我们故意留了白。文中所有界面图来自本地演示数据。

M3下半场-3D轨道与星下点.png


一、换个比喻:从「每两秒换一张照片」到「摄像机一直开着」

早期的 3D 页面有个说不出口的毛病:画面在抖。不是显卡的问题,而是写法的问题——每次拿到新数据就把整个场景重建一遍,地球重新挂载、材质重新加载、相机重新对焦。站在用户视角,就是「地球每隔两秒哆嗦一下」。

这次改造换了个比喻:不再每两秒换一张照片,而是把摄像机一直开着,只换演员。

落到代码上是三件事:

  • 一条 requestAnimationFrame 循环到底。整个引擎只有一个连续渲染循环,相机的阻尼、标签的屏幕坐标、测距读数的位置都在这条循环里更新。数据来了只是「换一帧演员的位置」,场景不重建,自然就不抖。
  • 把场景拆成一组独立图层。地球、星空、卫星、星间链路、最短路由、历史轨迹、轨道线、星下点轨迹、书签标记,各自是一个独立模块,各自有 dispose();面板上露出来的是其中七个开关。开关和透明度作用在图层上,而不是重建整个场景——这也是为什么图层面板上的滑条能「即时生效」而不会卡。
  • 把像素比按住。devicePixelRatio 最高取 2。4K 屏 + 大星座时,这条限制省下来的 GPU 比任何算法优化都直接。

顺手还清掉了几个容易漏的口子:容器尺寸用 ResizeObserver 跟,组件卸载时把渲染器、控制器、贴图、几何体、事件监听全部回收。3D 页面的内存泄漏几乎都出在「忘了 dispose」,这件事后面讲大屏时还会再遇到一次。


二、时间轴:真正难的不是画面,是「这一刻卫星在哪」

3D 页面这轮最有意思的功能是那条时间轴:滑块从「24 小时前」拖到「24 小时后」,地球上的卫星跟着走到那个时刻的位置。

看起来是个滑块,实际上藏着一个架构级的岔路口。

一个不能直接复用的接口

系统里本来就有一个「取当前帧」的接口 /simulation/snapshot。它返回所有卫星的经纬高、星间链路、最短路由——正好是 3D 渲染需要的数据。但它有一个副作用:每调用一次,服务端的仿真时钟就往前推一步。

这在实时模式下是特性,在拖动时间轴时就变成了事故:

  • 用户只是想「回看六小时前」,结果把整个系统的仿真时钟拖到了六小时前;
  • 实时刷新(每 2 秒一次)和拖动刷新同时在推时钟,两边互相打架;
  • 更麻烦的是,别的页面、别的用户看到的「现在」,已经不是真正的现在了。

所以这次新开了一个只读接口:/simulation/snapshot/seek?offsetSeconds=…。

只读的意思,是「什么都不改」

这个接口的契约写得很死:

  • 只把卫星传播到「基准历元 + 偏移」,返回经纬高;
  • 不推进 simTime,不改共享状态,不做故障注入;
  • 偏移钳制在 ±86400 秒(±24 小时),越界按边界算;
  • 因为它只回答「卫星在哪」,所以星间链路和路由直接返回空数组,不浪费带宽。

一句话:拖动只「看」,实时才「走」。

M3下半场-时间轴链路.png

前端只做两件事:少发请求、别抢主线程

拖动滑块会连续触发几十次事件。如果每次都发请求,后端会被自己人打穿。所以这里用的是尾部节流:150 毫秒内的连续输入合并成一次请求,最后一次一定落定——算下来最多每秒七次,手感是连贯的,压力是可控的。

还有一个更容易被忽略的细节:拖动模式下,轮询不再调用会推进时钟的那个接口,只刷新 KPI 面板。否则你一边拖,系统一边把时钟往前推,画面就会「往回跳」。

至于「谁来算位置」这个问题,我们选了后端算,而不是在前端塞 satellite.js + Web Worker。原因不是前端算不动,而是口径一致:后端的只读 seek、实时的当前帧、2D 轨道视图里的那条曲线,走的是同一个 SGP4 传播器、同一个坐标转换。三处显示如果各算各的,迟早会出现「3D 说在头顶、2D 说在地平线下」的尴尬。

轨道线:一圈 121 个点,只在需要时重建

打开「轨道线」图层,你会看到每颗卫星一条闭合的蓝色曲线;打开「星下点轨迹」,地球表面多出一条黄色曲线。

这两条线不是前端逐帧算出来的,而是后端顺着真实 TLE 采样一圈返回的:默认 120 段、也就是 121 个点,周期直接从 TLE 第二行的平均运动反推(解析失败退回默认值),一次最多取 20 颗星。

前端拿到这些点之后只做一件事:把经纬高换成场景坐标,重建一次折线几何体。低频操作,不做增量脏检查,也不参与逐帧计算——这是它能和 60 帧动画和平共处的原因。


三、点一下、量一下、做个记号

如果说时间轴解决的是「看什么时候」,那么工具解决的是「怎么看」。

点卫星,弹出一张「可信到数量级」的卡片

点击卫星会弹出详情卡:NORAD 编号、分类、是否故障、经纬高,以及两个稍显「越界」的字段——速度和周期。

这两个值不是查库来的,也不是额外调接口要的,而是用圆轨道近似(vis-viva)当场推的:半径 = 地球半径 + 高度,速度 ≈ 7.6 km/s,周期 ≈ 96 分钟(550 km 高度)。

为什么这么做?因为详情卡是「一眼确认」的场景:你要判断这颗星是不是在合理的高度、合理的速度上。为了一次性展示去加一个接口、加一张表,代价和收益完全不成比例。在演示和巡检场景里,「数量级对」比「小数点后六位对」更有价值——这处取舍在评审里专门说明过,避免后来人误以为是精确解算。

拾取用的是射线检测(Raycaster),没有引入任何引擎自带的选择器。好处是跟渲染解耦:想在哪个图层上加拾取,给那一层加个 pick() 就行。

测距:先找卫星,找不到就落到地面

测距工具的逻辑只有两句,但顺序很重要:

  1. 先判断点到的位置有没有卫星——有就取卫星的真实位置;
  2. 没有就取射线与地球表面的交点。

读数会跟着相机走:拖动视角时,距离数字会贴着两点连线的中点移动。这种「跟着相机每帧重投影」的做法,比在固定位置弹一个提示框更接近真实测绘软件的手感。

书签:存本地,比存数据库更合适

标记工具允许你在地球上点一个位置,起个名字(北京测控站、三亚站……),重启浏览器还在。

存储用的是 localStorage,按用户名分键:leo-sim-markers:<username>。为什么不上库表?

  • 它是个人视角的偏好,不是业务数据,别人不需要看到;
  • 它需要离线可用——演示环境没网、没后端也要能放标记;
  • 它不该为一个小功能引入一次数据库迁移、一套接口、一批测试。

风险也接受了:换一台机器、换一个浏览器就没了;存储配额不足或隐私模式时,静默退化成「只在本次会话有效」,不做任何阻断。

M3下半场-标记工具.png

图层面板的默认值,是设计出来的

七行开关看着简单,默认值其实都有理由:

  • 卫星、标签、星间链路、最短路由:默认开。这是「一眼看懂星座在干什么」的最小集合。
  • 历史轨迹、轨道线、星下点轨迹:默认关,三者的理由各不相同。轨迹是「跑一段时间才积累出来」的,打开后从当前帧开始记,切换编目或重置仿真会清空;轨道线要额外拉一次采样接口、重建 20 颗星的几何体;星下点同理。它们都属于「想深挖时再打开」的功能。

M3下半场-时间轴与图层面板.png


四、大屏:一面墙的阅读习惯,和桌面上不一样

3D 页面的服务对象是坐在椅子上、握着鼠标的人;大屏的服务对象是站在三米外、只看八秒的人。这两个前提几乎决定了所有设计选择。

它不是一个「更大的仪表盘」

大屏是一个独立路由:不套主布局、没有侧栏、没有顶部菜单。投到墙上时,屏幕上只应该有三样东西——结论、态势、异常。导航是给操作者用的,不是给观众用的。

配色也故意「叛变」了一次:大屏固定深空色,不跟随全局的亮/暗主题。理由很实际——投墙时环境光不可控,跟着主题切换反而会让某一档配色变得不可读。这处不一致被明确记为偏差,而不是偷偷埋在样式里。

八张卡片,回答八个问题

顶部一行八张大字卡,每张只回答一个问题:有多少颗星、多少个星座、多少个地面站、多少条链路在跑、在线率多少、覆盖率多少、多少人在接入、有多少条告警。

栅格是 12 列、每卡跨 3 列,所以 2560 宽是「两行四张」;1600 以下收字号、收紧主体比例,1280 以下切成「每行两张、主体单列」。数字用等宽数字字形(tabular-nums)——不是强迫症,是为了让数字变化时不要左右跳。

全球覆盖区:为什么最后没用 3D

最初的设计稿里,覆盖区想直接嵌一个 3D 地球。最后选了 ECharts 的 2D 世界地图,原因是三条:

  1. 投墙可读性:三米外看一个转动的球,你分不清哪条线连着哪颗星;平面地图上,一条弧线从欧洲连到北美,一眼就懂。
  2. 预算:3D 引擎是首页预算里最贵的一块,塞进大屏等于让每个进大屏的人先下载一遍 Three.js。
  3. 可控:地图是静态资源 + 几个 series,出问题好定位;3D 出问题只能看截图猜。

世界底图放在 public/maps/world.geo.json(217 个要素,约 1 MB),由 nginx 直供、可缓存、可离线;不进 JS 包,只在进入大屏时 fetch 一次再注册。万一地图加载失败,会自动降级成经纬度直角坐标上的散点加连线——不好看,但不会白屏。

地图上画三样东西:地面站(按状态着色,故障红、降级黄、正常青)、卫星(菱形,大小随高度变化,近似表达「覆盖圈」大小)、星间链路(线的透明度随链路质量变化)。同时渲染的卫星上限 160 颗、连线 240 条,这是用真实数据压过之后定的天花板。

这里要诚实一句:卫星大小表达的覆盖圈是「近似发光」,不是精确的星下点足迹(footprint)。真实的覆盖圈要考虑最小仰角、地形遮挡、天线方向图,那是一个独立课题。

右侧两条线:一条看链路,一条看告警

  • 实时链路状态取托管链路的列表,按状态判通断(ACTIVE / OPERATIONAL / ESTABLISHED 算通),用信号强度折算成一根质量条。没有托管链路时给空态,不硬编数据。
  • 告警跑马灯复用通知中心那个 WebSocket 单例。有个细节值得说:大屏是独立路由,主布局已经卸载,所以它可以安全「接管」这条连接,离开时再断开——不会和通知中心抢连接。超过五条时,前端把列表复制一份、用 CSS 位移做无缝上滚,鼠标悬停暂停。

推送是「尽力而为」:连不上就只有轮询,页面上会亮一个红点写「推送已断开」。我们宁可让它说实话,也不做一个假装在线的绿点。

挂两小时不崩,靠的是四个「收口」

大屏和普通页面的最大区别是没人关它。所以刷新和清理必须写死:

  • 一个 5 秒的定时器驱动全量刷新,六路请求并发、各自独立兜底,某一路挂了就保留旧值,不让整屏闪白;
  • document.hidden 时暂停轮询,切回来立刻刷一次再重启——投墙的机器可能整夜开着,但没人看的时候不该空转;
  • 离开页面时依次:清定时器 → 退订 WebSocket 回调 → dispose() 图表实例 → 移除 resize 监听;
  • 大屏代码走路由级动态引入,天然是一个独立 chunk(约 9.5 KB),不占首页预算。

这四条不是「优化」,是挂墙的前提。少一条,第二天早上回来就是一屏白。

M3下半场-运营大屏-2560.png


五、怎么证明它不是「演示专用」

演示能跑通,和「明天还能跑通」,中间隔着一整套验证。这次的做法是:能在 DOM 和网络层断言的,全部自动化;只能靠眼睛看的,老实写进人工联调清单。

自动化的部分:

  • 后端只读 seek 与轨道采样写了专门的单测,四个用例全绿:只读性(两次请求结果一致)、位置合法性(经纬度落在合理区间、高度接近标称值)、越界偏移被钳到 ±24 小时、合成模式下轨道采样返回空;
  • 前端 3D 交互四个用例:图层开关联动、工具按钮激活态、时间轴的「拖动触发 seek / 切回实时不再触发 seek」、开启轨道图层才去拉采样接口;
  • 大屏三个用例:八张卡片与口径、仪表盘入口跳转、离开大屏后不再产生轮询请求。

最后这条值得单独说。它验证的不是「显示对不对」,而是「走了以后有没有留下东西在跑」。这类用例平时没人愿意写,但它恰好对应最真实的事故形态:页面关了,定时器还在跑,内存一点点涨。

人工联调的部分也说清楚:WebGL 画布里的点击拾取、标记落点、轨道线渲染,在无头浏览器里截图断言意义不大(像素级比对脆弱、又验证不了「点得准」),所以这部分靠本地起后端、手动核对,并在文档里标明「需 WebGL 手动验证」。

体积上,3D 页面自己的 chunk 约 42 KB,大屏约 9.5 KB,都没有把重依赖塞回首页。性能预算是一道闸门,不是一句口号——上一轮刚把首页 JS 压下来,这一轮不能再放回去。


六、留白清单:我们没做的,和故意不做的

一份诚实的交付,应该能列出「边界」在哪里。这次明确留在门外的是:

  1. 精确覆盖圈。地图上的卫星大小是近似发光的视觉效果,不是测地线足迹。
  2. 遥测专用推送。大屏的实时指标目前靠轮询(KPI + 快照),没有为遥测单开一条 WebSocket topic。投墙场景下 5 秒粒度够用,但要做到「秒级曲线」就得补这条链路。
  3. 大屏跟随主题。固定深色是刻意的,代价是「白天在明亮的会议室看会偏暗」。
  4. 时间轴里的链路。只读 seek 只回答「卫星在哪」,不回星间链路和路由——因为拖动时观众关心的是轨迹,不是拓扑。
  5. 合成星座的轨道线。没有真实 TLE 就采不出真实轨道,所以合成模式下这个图层是空的,不做「假轨道」。
  6. 多屏拼接与自动轮播。先把「一面墙稳定跑两小时」做扎实,再谈「三面墙拼一张图」。

这些不是「TODO 列表」,而是主动划定的边界:每一条都知道上限在哪、为什么现在不做、以后要做需要补什么。


七、三句话总结

  • 画面不抖,靠的是一条渲染循环 + 数据换帧,而不是更贵的显卡。
  • 时间能拖,靠的是一个「只读」的接口和对时钟的敬畏——能不写状态就不写状态。
  • 墙能挂住,靠的是定时器、订阅、图表、监听四个收口,而不是一张好看的截图。

3D 和大屏是这一轮里最「好看」的两个交付,但它们真正的价值,恰恰在于那些看不见的部分:一个不推进时钟的接口、一次 150 毫秒的节流、一个卸载时就消失的定时器。

下一段路,M4 会把注意力放回「规模」——K8s、碰撞预警、任务规划。到那时,今天这些「看不见的部分」会变成地基。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP