Forráskód Böngészése

fix(cockpit): T03 极点保护 —— 中心永不进入极点带,消除极点附近异常旋转

维护者反馈:视角移动到南北极点时整个地球出现异常旋转。

实测复现(新增 `verify/t03-pole-probe.mjs`,真机 + dev 把手读取):
把中心送到 ±89.5° 附近后继续朝极点拖,中心纬度会跑到 -96 / -168 这类**非法值**
(`Cartesian3.fromDegrees` 不校验纬度),并且经度在 0 与 -180 之间**无限来回**
—— 后者就是「整个地球异常旋转」的直接来源。

根因(两层):
1. 平移用等距圆柱近似(`lon += east/(111320·cos lat)`)时,`cos(lat)→0` 让经度增量爆走,
   纬度 89.5° 处 40px 拖拽能换出 40°+ 经度;
2. 换成精确球面大圆后又出现新问题:它的 `bearing` 是方位角,极点附近「向北」会绕到对面,
   经度翻转 180° 并逐帧来回——为它叠加「跨极点折回 / 夹到界上」两种补救都会留下
   2 周期环(实测界内 83.6° ↔ 界上 85.0° 反复跳)。

**最终方案:承认奇点、从根上避开它**(维护者指出「纬线在极点收敛,东/西在极点失去意义」
正是这个意思):
- `POLAR_LAT_LIMIT = 85`:中心纬度硬限在 ±85°,此处 ENU 局部系完全良态(奇点只在 ±90°),
  引擎侧不再退化;
- 平移回到简单映射(纬度线性 + 夹限、经度按 `cos(min(|lat|,60°))` 换算),
  两个方向都**单调且有界**,不可能出现符号翻转;代价是极点带内不再严格等距
  (东西向偏快),但那里不是驾驶舱观察区;
- `sanitizePivot()` 统一收口:地面拾取点、选中对象、外部送进来的点都要过这道闸,
  否则一个落在极点带内的起点会让之后每一步平移都被夹住而「拖不动」;
- 引擎侧 `frameAt()` 再夹一次纬度(第二道防线,防其它调用方直接喂端点值)。

验证:
- `verify/t03-camera-contract.mjs` **20/20**,其中 9e 三组直接钉这个 bug:
  界上朝极点拖 8 次落点恒为 `12.5000,85.0000`;朝极点推进 60 步停在界上且最大经度漂移
  **0.000000000°**;跨 ±180 经线经度归一。
- 真机 `verify/t03-pole-probe.mjs`:南极段 12 次向南拖经度恒 0.000、纬度恒 -85.000;
  北极段单调下降、无经度跳变。
- 回归:`t01-panel-catalog`、`t06-assets` 15/15。

新增诊断脚本 `verify/t03-pole-probe.mjs`(真机)、`verify/t03-pole-domain-probe.mjs` 与
`verify/t03-pan-direction-probe.mjs`(纯 domain,用来量清 dy↔南北 的符号方向)。
Kazerin 1 hónapja
szülő
commit
d129c6a9ac

+ 4 - 1
.scratch/cockpit-shell-panel-layout/issues/03-camera-control-contract.md

@@ -33,6 +33,8 @@ Labels: ready-for-agent
 3. **手感现场可调**:DEV 把手加 `__cockpitMap.setCameraDamping(毫秒)`(`0` = 不做阻尼、直接跟手),τ 越小越跟手、越大越飘 —— 「丝滑」是主观量,控制台拖调比改代码快。
 4. **平移松手有惯性**(2026-09-17 第二次复验反馈):原实现里 `endDrag` 会把目标改写为「屏幕中心的地面拾取点」,而阻尼攒下的滞后正好等于这段位移——目标被拽回当前实际位置,滞后当场清零,表现为**松手即停、没有惯性**;滚轮与右键没有这一步改写,所以会继续滑到目标,三种交互手感不一致。修法:松手只做**条件重锚**——偏差 ≤ `range × REANCHOR_LIMIT_RATIO`(1%,即肉眼不可见)时才把中心重锚到拾取点(顺手清掉等距近似的累积误差),偏差过大就保持推算值,宁可不重锚也不让画面跳。于是松手后目标不再被改写,惯性由阻尼自然延续。
 
+5. **极点保护**(2026-09-17 第三次复验反馈:视角移到南北极点时整个地球异常旋转)。实测复现(新增真机探针 `verify/t03-pole-probe.mjs`):中心送到 ±89.5° 附近再朝极点拖,纬度会跑到 -96 / -168 这类**非法值**(`Cartesian3.fromDegrees` 不校验纬度),且经度在 0 与 -180 之间**无限来回** —— 后者正是异常旋转的直接来源。根因有两层:等距圆柱近似的 `cos(lat)→0` 让经度增量爆走;改成精确球面大圆后,其 `bearing` 是方位角,极点附近「向北」会绕到对面、经度翻转 180° 并逐帧来回(叠加「折回 / 夹到界上」两种补救都残留 2 周期环)。**最终方案是承认奇点并从根上避开**(维护者指出「纬线在极点收敛,东/西在极点失去意义」):`POLAR_LAT_LIMIT = 85` 硬限中心纬度(±85° 处 ENU 系完全良态,奇点只在 ±90°),平移回到「纬度线性 + 夹限、经度按 `cos(min(|lat|,60°))` 换算」的单调有界映射;`sanitizePivot()` 统一收口所有入口(拾取点/选中点/外部点),引擎侧 `frameAt()` 再夹一次作第二道防线。代价:极点带内不再严格等距(东西向偏快),但那里不是驾驶舱观察区。
+
 改动文件:`src/modules/cockpit/domain/camera.js`、`src/modules/cockpit/services/cameraControl.js`、`src/modules/cockpit/services/mapService.js`、`docs/adr/0008-web-cockpit-camera-controls.md`、`verify/t03-camera-contract.mjs`、`verify/t03-camera-walkthrough.mjs`。
 
 提交情况(2026-09-17):方向反转 + 阻尼一次提交,平移惯性修正紧随其后一次(`git log` 里搜「松手有惯性」)。`mapService.js` 里那一行 `__cockpitMap.setCameraDamping` 把手**故意没随这两次提交** —— 该文件当时还带着并发会话「底图/配置」票的未提交改动(引用尚未提交的 `domain/basemap.js`、`services/cockpitConfig.js` 等),单独提交会产出引用缺失模块的树。维护者裁定:**等那一路落地后随它一起提交**(那行改动内容极小,落地前 `setCameraDamping` 只影响 DEV 调参便利性,不影响契约本身)。
@@ -70,7 +72,8 @@ Labels: ready-for-agent
 
 ### 验收证据
 
-- 逻辑:`node .scratch/cockpit-shell-panel-layout/verify/t03-camera-contract.mjs` → **17/17 通过**(左键只平移且方向为「抓着地图拖」、平移方向在 heading=0/90 两点验证、右键只旋转且 range/中心不动、pitch 夹限、拖拽中锁定中心且松手后才换中心、滚轮只改距离并夹限、中心三规则、换中心 8 帧单调且 h/r 全程不变、**阻尼渐入不超调 87 帧停稳并吸附**、**阻尼与帧率无关(60fps 83.8% vs 120fps 83.8%)**、**平移松手有惯性(松手时滞后 0.0383°,松手后又滑行 0.0329°、85 帧停稳)**、**滚轮与右键同样有余韵(range 滞后 4410m / heading 滞后 48.1°)**、**条件重锚(1% 内才重锚、大偏差不跳画面)**、用户操作打断飞行、**阻尼可拖调(τ=40 比 τ=400 跟手 9 倍,τ=0 一帧贴齐)**、DOM 绑定与默认行为抑制、状态串)。
+- 逻辑:`node .scratch/cockpit-shell-panel-layout/verify/t03-camera-contract.mjs` → **20/20 通过**(左键只平移且方向为「抓着地图拖」、平移方向在 heading=0/90 两点验证、右键只旋转且 range/中心不动、pitch 夹限、拖拽中锁定中心且松手后才换中心、滚轮只改距离并夹限、中心三规则、换中心 8 帧单调且 h/r 全程不变、**阻尼渐入不超调 87 帧停稳并吸附**、**阻尼与帧率无关(60fps 83.8% vs 120fps 83.8%)**、**平移松手有惯性(松手时滞后 0.0383°,松手后又滑行 0.0329°、85 帧停稳)**、**滚轮与右键同样有余韵(range 滞后 4410m / heading 滞后 48.1°)**、**条件重锚(1% 内才重锚、大偏差不跳画面)**、**极点保护三组(界上朝极点拖落点恒为 12.5000,85.0000;朝极点推进 60 步停在界上且最大经度漂移 0.000000000°;跨 ±180 经线归一)**、用户操作打断飞行、**阻尼可拖调(τ=40 比 τ=400 跟手 9 倍,τ=0 一帧贴齐)**、DOM 绑定与默认行为抑制、状态串)。
+- 真机极点复验:`node .scratch/cockpit-shell-panel-layout/verify/t03-pole-probe.mjs` → 南极段 12 次向南拖经度恒 0.000、纬度恒 -85.000;北极段单调下降、无经度跳变(修复前经度在 0 与 -180 之间无限来回)。
 - 回归:`t01-panel-catalog.mjs` 通过(`getLifecycleStats` 新增 `camera` 字段,原字段语义未变)、`t06-assets.mjs` 15/15。
 - 真机:**待跑**(见下一步)。脚本已按新方向与阻尼更新:A2 改为断言中心沿**相机左手**方向移动、新增 A10「阻尼在真机上生效」(实际态滞后目标态且缺口单调收敛),并在量几何前统一等 `settled`(有阻尼后手势结束不等于画面停稳)。