t21-view-modes.mjs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. /**
  2. * T21 地面/管线视图条件 纯逻辑检查。
  3. *
  4. * 决策地图票 05 锁死了三条硬约束,这里逐条断言:
  5. * ① 管线视图压低相机 + 加大 pitch,且 pitch 上限放宽到允许平视;
  6. * ② **切换视图不重置旋转中心**(`viewHprFor()` 的签名里根本没有中心参数,从构造上杜绝);
  7. * ③ 三键分工不变 —— 管线视图只换 limits,不新增输入绑定(契约层面由 t03 覆盖,这里断言 limits 取值)。
  8. * 另外覆盖:地面视图还原(用进管线前记下的倾角/距离)、图层透明度、配置覆盖与脏值兜底。
  9. *
  10. * 运行:node .scratch/cockpit-shell-panel-layout/verify/t21-view-modes.mjs
  11. */
  12. import assert from 'node:assert/strict'
  13. import { PITCH_LIMITS } from '../../../src/modules/cockpit/domain/camera.js'
  14. import {
  15. DEFAULT_VIEW_MODE,
  16. GROUND_ALPHA,
  17. GROUND_PITCH,
  18. PIPELINE_ALPHA,
  19. PIPELINE_CAMERA,
  20. VIEW_MODES,
  21. isKnownViewMode,
  22. planViewMode,
  23. viewHprFor,
  24. } from '../../../src/modules/cockpit/domain/viewModes.js'
  25. // ---------------------------------------------------------------- 1. 词汇
  26. assert.equal(DEFAULT_VIEW_MODE, VIEW_MODES.GROUND, '默认是地面视图')
  27. assert.equal(isKnownViewMode('ground'), true, 'ground 合法')
  28. assert.equal(isKnownViewMode('pipeline'), true, 'pipeline 合法')
  29. assert.equal(isKnownViewMode('管线'), false, '中文不是合法键(脏值要挡住)')
  30. // ---------------------------------------------------------------- 2. 地面视图
  31. const ground = planViewMode(VIEW_MODES.GROUND)
  32. assert.equal(ground.mode, VIEW_MODES.GROUND, '地面视图模式')
  33. assert.deepEqual(ground.limits, PITCH_LIMITS.ground, '地面视图用常规俯视上下限')
  34. assert.equal(ground.terrainAlpha, 1, '地面视图地形不透明(还原)')
  35. assert.equal(ground.tilesetAlpha, 1, '地面视图模型不透明(还原)')
  36. assert.equal(ground.rangeFactor, 1, '地面视图不改距离')
  37. // ---------------------------------------------------------------- 3. 管线视图
  38. const pipeline = planViewMode(VIEW_MODES.PIPELINE)
  39. assert.equal(pipeline.mode, VIEW_MODES.PIPELINE, '管线视图模式')
  40. assert.deepEqual(pipeline.limits, PITCH_LIMITS.pipeline, '管线视图放宽 pitch 上限(允许平视/轻微仰视)')
  41. assert.ok(pipeline.limits.max > PITCH_LIMITS.ground.max, '管线视图的 pitch 上限必须比地面视图更宽')
  42. assert.equal(pipeline.pitch, PIPELINE_CAMERA.pitch, '管线视图用预设倾角')
  43. assert.ok(pipeline.pitch > GROUND_PITCH, '管线视图的 pitch 要更"平"(比地面视图大)')
  44. assert.ok(pipeline.rangeFactor < 1, '管线视图要压低弹簧臂(距离变小)')
  45. assert.equal(pipeline.terrainAlpha, PIPELINE_ALPHA.terrain, '管线视图地形半透明')
  46. assert.equal(pipeline.tilesetAlpha, PIPELINE_ALPHA.tileset, '管线视图模型半透明')
  47. assert.ok(pipeline.terrainAlpha > 0 && pipeline.terrainAlpha < 1, '半透明而非完全隐藏(保留空间参照)')
  48. assert.ok(pipeline.tilesetAlpha > 0 && pipeline.tilesetAlpha < 1, '模型也是半透明而非隐藏')
  49. // 未知模式退回地面视图,不抛错
  50. assert.equal(planViewMode('nope').mode, VIEW_MODES.GROUND, '未知模式应退回地面视图')
  51. // 配置覆盖:只认有限数字
  52. const overridden = planViewMode(VIEW_MODES.PIPELINE, { pitch: -30, rangeFactor: 0.5, terrainAlpha: 0.7, tilesetAlpha: 0.6 })
  53. assert.equal(overridden.pitch, -30, '倾角可被配置覆盖')
  54. assert.equal(overridden.rangeFactor, 0.5, '距离系数可被配置覆盖')
  55. assert.equal(overridden.terrainAlpha, 0.7, '地形透明度可被配置覆盖')
  56. assert.equal(overridden.tilesetAlpha, 0.6, '模型透明度可被配置覆盖')
  57. const dirty = planViewMode(VIEW_MODES.PIPELINE, { pitch: 'x', rangeFactor: NaN, terrainAlpha: null })
  58. assert.equal(dirty.pitch, PIPELINE_CAMERA.pitch, '脏倾角应退回预设')
  59. assert.equal(dirty.rangeFactor, PIPELINE_CAMERA.rangeFactor, '脏系数应退回预设')
  60. assert.equal(dirty.terrainAlpha, PIPELINE_ALPHA.terrain, '脏透明度应退回预设')
  61. // ---------------------------------------------------------------- 4. 切换时的相机变换(不重置中心)
  62. const current = { heading: 42, pitch: -55, range: 12000 }
  63. const toPipeline = viewHprFor(current, pipeline, null)
  64. assert.equal(toPipeline.heading, 42, 'heading 原样带走(切换视图不转向)')
  65. assert.equal(toPipeline.pitch, PIPELINE_CAMERA.pitch, '倾角按管线预设')
  66. assert.equal(toPipeline.range, 12000 * PIPELINE_CAMERA.rangeFactor, '距离按系数压低')
  67. // 关键:返回值里没有 pivot/中心字段 —— 从构造上就不可能重置中心
  68. assert.equal('pivot' in toPipeline, false, '切换视图不得产出中心字段(不重置中心的构造性保证)')
  69. assert.equal('pivot' in pipeline, false, '视图计划里也不该有中心')
  70. // 管线视图的距离被夹在合理区间(贴着地面看,但不至于钻进地表)
  71. const tooFar = viewHprFor({ heading: 0, pitch: -55, range: 500000 }, pipeline, null)
  72. assert.equal(tooFar.range, PIPELINE_CAMERA.maxRange, '超远的距离应被夹到管线视图上限')
  73. const tooNear = viewHprFor({ heading: 0, pitch: -55, range: 200 }, pipeline, null)
  74. assert.equal(tooNear.range, PIPELINE_CAMERA.minRange, '过近的距离应被夹到管线视图下限')
  75. // 回地面:用进管线前记下的倾角/距离还原(决策地图「地面视图还原」)
  76. const baseline = { pitch: -62, range: 9000 }
  77. const backToGround = viewHprFor({ heading: 42, pitch: -18, range: 3000 }, ground, baseline)
  78. assert.equal(backToGround.heading, 42, '回地面也不改朝向')
  79. assert.equal(backToGround.pitch, -62, '回地面应还原进管线前的倾角')
  80. assert.equal(backToGround.range, 9000, '回地面应还原进管线前的距离')
  81. // 没有基线时用默认地面倾角兜底(比如直接以管线视角进系统)
  82. const groundNoBaseline = viewHprFor({ heading: 0, pitch: -18, range: 3000 }, ground, null)
  83. assert.equal(groundNoBaseline.pitch, GROUND_PITCH, '没有基线时回落到默认地面倾角')
  84. // 中心不变是行为契约:同一次切换,heading 与中心都不在变换范围内
  85. const stateBefore = { pivot: { lon: 110.39, lat: 28.44, height: 111.6 }, hpr: { heading: 90, pitch: -55, range: 8000 } }
  86. const afterSwitch = { ...stateBefore, hpr: { ...stateBefore.hpr, ...viewHprFor(stateBefore.hpr, pipeline, null) } }
  87. assert.deepEqual(afterSwitch.pivot, stateBefore.pivot, '切换视图后中心必须一模一样')
  88. console.log(
  89. `T21 视图条件检查通过:地面/管线计划(管线 pitch ${PIPELINE_CAMERA.pitch}° / 距离 ×${PIPELINE_CAMERA.rangeFactor} / 地形 α${PIPELINE_ALPHA.terrain} / 模型 α${PIPELINE_ALPHA.tileset})、pitch 上限放宽到 ${PITCH_LIMITS.pipeline.max}°、切换不产中心字段(构造性不重置)、地面视图按基线还原、脏配置兜底。`,
  90. )