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