03-camera-control-contract.md 7.0 KB

03: 相机控制契约

What to build: 实现驾驶舱三维地图的显式相机控制:左键拖拽平移、右键绕中心旋转、滚轮调整弹簧臂距离,并替换 Cesium 默认交互。

Blocked by: 02, 27

依赖说明(2026-09-16 维护者确认):三键相机契约必须落在真实引擎上,故新增票 27(接入 Cesium 常驻 Viewer)作为本票前置。

Status: claimed Labels: ready-for-agent

契约来源

ADR 0008 与决策地图票 Spring-arm orbit pivot:三键分工、旋转中心 = 选中对象地表点(未选中时 = 屏幕中心的地面拾取点)、右键按下锁定中心、取消选中不回退、只有左键平移才把中心交回地面拾取点、换中心时保持 heading 与 range。弹簧臂原语锁定为 camera.lookAtTransform(中心东北天局部系, HeadingPitchRange),不用 Tres OrbitControls、不用 vue-cesium 的鼠标绑定。

验收

  • 鼠标左键拖拽只执行摄像机平移,不旋转、不缩放。 —— 逻辑 1/2 绿;真机 A2 待跑。
  • 鼠标右键拖拽绕旋转中心执行轨道旋转;右键按下时锁定中心,拖拽中不重新拾取。 —— 逻辑 3/5 绿;真机 A3 待跑。
  • 滚轮只改变摄像机与旋转中心的距离。 —— 逻辑 6 绿;真机 A4 待跑。
  • 选中监测点/设备时中心为该对象地表点;未选中时为地面拾取点;取消选中不回退中心。 —— 逻辑 7 绿;真机 A5/A6/A7/A8 待跑。
  • 从无选中到选中时镜头平滑换中心,保持 heading 与 range。 —— 逻辑 8 绿;真机 A6 待跑。
  • Cesium 默认左键旋转、滚轮缩放等行为被替换。 —— createViewer() 里逐项关闭(enableRotate/Zoom/Tilt/Look/Translate),左键平移/滚轮距离由本票接管;真机 A2/A4 待跑。
  • 相机控制集中在地图服务层。 —— domain/camera.js(纯数学)+ services/cameraControl.js(状态机与 DOM 绑定)+ services/cesiumEngine.js(Cesium 方言翻译)+ services/mapService.js(生命周期与对外 API);组件与外壳未改。
  • 三类交互各有可重复演示路径。 —— 见下面「演示路径」一节。

进度:90%

交付物

  • src/modules/cockpit/domain/camera.js:三键的纯逻辑 —— orbit()(右键)、zoomByWheel()(滚轮)、metersPerPixel() + pivotOffsetMeters() + shiftPivot()(平移)、flightProgress() + lerpPivot()(换中心动画)、pitch 上下限与 range 上下限、formatCameraState()(状态串)。不 import cesium、不碰 DOM。
  • src/modules/cockpit/services/cameraControl.js:状态机(中心、弹簧臂、拖拽态、锁定中心、飞行)+ attachCameraInput() 事件绑定(左/右键 pointer、滚轮 passive:false + preventDefault、右键菜单 preventDefault、setPointerCapture)。引擎以接口注入,所以能脱离浏览器断言。
  • src/modules/cockpit/services/cesiumEngine.js:新增 createCameraEngine(viewer),把契约用到的 Cesium 方言包出来 —— applyView(lookAtTransform + HeadingPitchRange)、readView(反读当前相机相对中心的 HPR)、pickGroundAtCenter(屏幕中心椭球求交;地形归票 21)、projectPivot(中心点投回画布)、inputTarget/viewport/fovY/now/requestFrame。本文件仍是全仓唯一 import 'cesium' 的地方。
  • src/modules/cockpit/services/mapService.js:引擎就绪后挂相机控制,卸载时先摘事件再销毁实例;对外只暴露 setCameraFocus(point|null) / clearCameraFocus() / getCameraState();宿主写 data-camera(状态串,走查与人工排查都读它);DEV 下挂 window.__cockpitMap 把手(演示与走查用,生产构建不含)。
  • verify/t03-camera-contract.mjs:10 项逻辑断言(假引擎)。
  • verify/t03-camera-walkthrough.mjs:9 项真机断言(真 Cesium + page.mouse 真实输入事件)。

设计要点

  • 平移按「中心点所在距离上的垂直可视高度」换算像素→米(2·range·tan(fovY/2)/viewportH),与 pitch 无关,4K/窗口尺寸变化都不用改灵敏度。
  • ADR 0008 那句「拖拽方向映射为摄像机移动方向」落地为「相机跟着拖」(拖右 → 相机右移 → 画面左移)。若维护者要改成「抓着地图拖」的手感,改 PAN_FOLLOWS_DRAG 一个常量即可,逻辑与走查都跟着它走。
  • pitch 上下限先给地面视图 [-85, -5];ADR 里「管线视图允许平视或轻微仰视」的放宽([-85, 5])已在常量里备好,等视图预设那一票接(不属本票)。
  • 初始视角不设预设:本票只管控制语义,挂载时按当前真实相机反读 HPR 对齐状态,因此第一次交互不会跳变;「默认全球视角」观感收口仍是维护者待定项(票 27 遗留里也记着)。
  • 中心缺失(看向天空、拾不到椭球)时不动相机,等下一次可拾取再建立中心。

演示路径(三类交互,均可重复)

前置:yarn dev → 打开 /#/cockpit,引擎就绪后宿主上会有 data-camera="h=… p=… r=… pivot=… focus=ground drag=none"。

  1. 左键拖拽(平移):在中央一张图上按住左键横向拖 —— pivot 变化、h/p/r 不变,画面随之平移(不再旋转)。
  2. 右键拖拽(轨道旋转):按住右键横向/纵向拖 —— h 随横向像素、p 随纵向像素变化,r 与 pivot 不动;拖拽中松开前不会有任何拾取。
  3. 滚轮(弹簧臂):滚轮上下 —— 只有 r 变化,h/p/pivot 不动。
  4. 选中换中心(等监测点/设备详情票接 UI 前可用控制台):__cockpitMap.setCameraFocus({ lon: 110.396, lat: 28.455 }) —— 中心平滑飞到该点,全程 h/r 不变;__cockpitMap.clearCameraFocus() 中心不回退。

验收证据

  • 逻辑:node .scratch/cockpit-shell-panel-layout/verify/t03-camera-contract.mjs → 10/10 通过(左键只平移并按像素比例移动中心、平移方向 = 相机移动方向、右键只旋转且 range/中心不动、pitch 夹限、拖拽中锁定中心且松手后才换中心、滚轮只改距离并夹限、中心三规则、换中心 10 帧单调且 h/r 全程不变、DOM 绑定与默认行为抑制、状态串)。
  • 回归:t01-panel-catalog.mjs 通过(getLifecycleStats 新增 camera 字段,原字段语义未变)、t06-assets.mjs 15/15。
  • 真机:待跑(见下一步)。

下一步(待确认什么)

真机走查 9 项还没跑 —— 需要 http://localhost:8000 上的 yarn dev(维护者自行起服务,项目规则下我不自动起)。服务起来后执行:

node .scratch/cockpit-shell-panel-layout/verify/t03-camera-walkthrough.mjs

把结果补进「验收证据」并把进度提到 100% 再 close。若真机暴露出与逻辑不一致的地方(例如 Cesium 事件被上游吞掉、中心投影偏移),据实回调进度并写明根因。