/** * T30 进入引导与视角预设 纯逻辑检查。 * * 覆盖:`map.home` / `map.intro` 的归一化与默认值;预设视角解析;归位判定 `isAtHome()` 的容差与 * 经度环绕;以及「配置里那组沅陵坐标」确实被解析成预期值(防止手抄坐标时写错)。 * * 运行:node .scratch/cockpit-shell-panel-layout/verify/t30-view-presets.mjs */ import assert from 'node:assert/strict' import { existsSync, readFileSync } from 'node:fs' import { HOME_DEFAULTS, INTRO_DEFAULTS, homeLabel, isAtHome, planHome, planIntro, } from '../../../src/modules/cockpit/domain/viewPresets.js' import { builtinConfig, normalizeConfig } from '../../../src/modules/cockpit/services/cockpitConfig.js' // ---------------------------------------------------------------- 1. 默认值 const defaults = planHome({}) assert.equal(defaults.lon, HOME_DEFAULTS.lon, '缺省经度应回落到默认(沅陵)') assert.equal(defaults.lat, HOME_DEFAULTS.lat, '缺省纬度应回落到默认') assert.ok(defaults.range > 1000, '默认距离应是县域级(>1km)') assert.ok(defaults.pitch < 0, '默认俯角应为负(从上往下看)') const introDefaults = planIntro({}) assert.equal(introDefaults.enabled, true, '默认开启引导') assert.equal(introDefaults.title, INTRO_DEFAULTS.title, '默认文案') assert.ok(introDefaults.spinSeconds > 0, '默认开启自转') // ---------------------------------------------------------------- 2. 归一化 const normalized = normalizeConfig({ map: { home: { lon: 110.39028164, lat: 28.4436787, pitch: -55, range: 12000, label: '沅陵县城' }, intro: { enabled: false, title: '进入', spinSeconds: 0 }, }, }) assert.equal(normalized.config.map.home.lon, 110.39028164, '经度应透传(不丢精度)') assert.equal(normalized.config.map.home.lat, 28.4436787, '纬度应透传') assert.equal(normalized.config.map.home.label, '沅陵县城', '标签应透传') assert.equal(normalized.config.map.intro.enabled, false, '引导开关应透传') assert.equal(normalized.config.map.intro.spinSeconds, 0, '自转秒数应透传') const badTypes = normalizeConfig({ map: { home: { lon: '东经110', pitch: {} }, intro: { enabled: 'yes' } } }) assert.equal(badTypes.config.map.home.lon, null, '非数字经度应为 null(回落默认)') assert.equal(badTypes.config.map.home.pitch, null, '非数字俯角应为 null') assert.equal(badTypes.config.map.intro.enabled, true, '非布尔开关应回落默认 true') assert.equal(badTypes.issues.length, 3, `三个错字段各记一条 issue,实际 ${JSON.stringify(badTypes.issues)}`) // ---------------------------------------------------------------- 3. 计划层 const home = planHome({ home: { lon: 110.5, lat: 28.5, heading: 30, pitch: -40, range: 5000 } }) assert.equal(home.lon, 110.5, '配置优先') assert.equal(home.heading, 30, 'heading 透传') assert.equal(home.range, 5000, 'range 透传') assert.equal(home.label, '目标区域', '没给 label 时有默认文案') const introOff = planIntro({ intro: { enabled: false } }) assert.equal(introOff.enabled, false, '可关闭引导(走查自隔离用)') assert.equal(planIntro({ intro: { spinSeconds: -5 } }).spinSeconds, 0, '负数自转秒数应夹到 0') // label 文案:归位按钮的 tooltip 用它,必须带上坐标 const label = homeLabel({ lon: 110.39028164, lat: 28.4436787, label: '沅陵县城' }) assert.match(label, /沅陵县城/, '标签应含地名') assert.match(label, /110\.3903/, '标签应含经度(4 位小数)') assert.match(label, /28\.4437/, '标签应含纬度(4 位小数)') // ---------------------------------------------------------------- 4. 归位判定 const homeRef = { lon: 110.39028164, lat: 28.4436787, heading: 0, pitch: -55, range: 12000 } const atHomeState = { pivot: { lon: 110.39028164, lat: 28.4436787, height: 0 }, hpr: { heading: 0, pitch: -55, range: 12000 } } assert.equal(isAtHome(atHomeState, homeRef), true, '同一视角应判为在家') assert.equal( isAtHome({ pivot: { lon: 110.4, lat: 28.44 }, hpr: { heading: 0, pitch: -55, range: 12000 } }, homeRef), false, '中心偏出容差(>1e-4°)不应判为在家', ) assert.equal( isAtHome({ pivot: atHomeState.pivot, hpr: { heading: 0, pitch: -55, range: 30000 } }, homeRef), false, '距离差太多不应判为在家', ) assert.equal( isAtHome({ pivot: atHomeState.pivot, hpr: { heading: 0, pitch: -20, range: 12000 } }, homeRef), false, '俯角差太多不应判为在家', ) // 经度环绕:heading 0 与 360 是同一个朝向 assert.equal( isAtHome({ pivot: atHomeState.pivot, hpr: { heading: 360, pitch: -55, range: 12000 } }, homeRef), true, 'heading 360 等价于 0(经度/朝向环绕)', ) assert.equal(isAtHome(null, homeRef), false, '没有相机状态时不应判为在家') // ---------------------------------------------------------------- 5. 与真实配置对齐(本机) const configPath = new URL('../../../public/cockpit/config.json', import.meta.url) if (existsSync(configPath)) { const raw = JSON.parse(readFileSync(configPath, 'utf8')) const { config } = normalizeConfig(raw) const actual = planHome(config.map) assert.equal(actual.lon, 110.39028164, '配置里的经度应等于维护者给的沅陵坐标') assert.equal(actual.lat, 28.4436787, '配置里的纬度应等于维护者给的沅陵坐标') assert.ok(config.map.intro.enabled !== false, '本机配置应开启进入引导') console.log(` (已按本机配置核对:home=${actual.lon}, ${actual.lat} label=${actual.label} intro=${config.map.intro.enabled})`) } console.log( 'T30 检查通过:home/intro 默认值与归一化(含错字段 issue)、预设解析与 label 文案、归位判定(容差 + heading 环绕 + 无状态),并与本机配置里的沅陵坐标对齐核对。', )