# 主场景区的相机限位与进出状态机 spec Status: ready-for-agent ## Problem Statement 驾驶舱进入主场景区(沅陵县城框定范围)后,相机没有区域限位:用户可以把弹簧臂拉远到全球尺度,看到高精度精细数据以外的低精度区域与底图接缝("露馅")。同时,进入主场景区时相机从全球粗略底图飞到精细影像矩形,过渡突兀。本 spec 固化第 1 项(相机限位与进出状态机)的实现契约;雾与过渡分别由同 effort 的 07、08 另行固化为规格(待 resolved)。 决策来源:`.scratch/cockpit-primary-area-guardrails/issues/03`(已 resolved,维护者 Q1–Q5 + R2-1…R2-6 全部确认),决策记录在 `docs/adr/0009-primary-area-camera-limits-and-entry-state.md`。 ## Solution 进入主场景区后,相机被显式模式锁限定:**最远视野 = 归位默认视角**(`range_max = 12,000 m`),中心点被约束在 `map.primaryArea.bounds` 内(边界软回弹),相机不钻入地形;只有「返回全球」能解除限位,回到自由全球视角;全球态闲置 5 秒未操作回到程序初始态(星球自转 + 屏幕中央闪烁「点击进入系统」),不重置一级模式与筛选。与既有三键相机契约(ADR 0008)、地面/管线视图(取交集)合成,全部落在服务层。 ## User Stories 1. 作为值班人员,我希望进入主场景区后滚轮不能把镜头拉远到超出归位默认视角,以便看不到高精度数据以外的区域而"露馅"。 2. 作为值班人员,我希望点「进入系统」或「归位」后进入主场景区并自动锁定限位,以便无需手动设置即可获得保护。 3. 作为值班人员,我希望「归位」把我带回沅陵县城默认视角(12 km 县域级),以便快速回到工作位置。 4. 作为值班人员,我希望点击「返回全球」后限位解除、镜头阻尼飞到自由全球全景,以便查看整体态势或离开主场景区。 5. 作为值班人员,我希望在自由全球视角闲置 5 秒后回到程序初始态(星球自转 + 「点击进入系统」闪烁),以便长时间离开后回来仍是熟悉入口。 6. 作为值班人员,我希望回到引导页时不重置当前一级模式与行业/区域筛选,以便回来后续接之前的工作上下文。 7. 作为值班人员,我希望在区内平移到边界时有软回弹手感(短暂越界后松手弹回),以便知道"到头了"而不是被硬生生挡住。 8. 作为值班人员,我希望镜头贴近地面或设备时不会钻进地形(防入地),以便始终能看到目标。 9. 作为值班人员,我希望在区内切换地面/管线视图时两组约束取交集、行为与现状一致,以便管线巡查不被区域限位破坏。 10. 作为值班人员,我希望在区外顶栏按钮显示为「归位/进入」,以便一步回到主场景区。 11. 作为维护者,我希望未配置 `map.primaryArea` 时区域限位与自建雾自动关闭并如实提示,以便不误导部署。 12. 作为维护者,我希望限位的数值(`range_max`、软区、防入地 margin)可配置或可微调,以便按现场观感校准。 13. 作为测试人员,我希望限位与状态机的纯逻辑可直接断言(脱离浏览器),以便快速回归。 14. 作为验收人员,我希望通过既有走查脚本验证"进入→锁定→返回→闲置回引导"全过程,以便确认无跳变与露馅。 ## Implementation Decisions - **`range_max = home.range = 12,000 m`**:最远视野 = 归位默认视角(`map.home` pitch −55 / range 12000,home 预设不改)。取代 `issues/02` 的 `range_max = 1651/(2·tan(fovY/2))`(≈2,542 m)与"15% 垂直覆盖预算"锚点;`isAtHome` 的 2% 相对容差(240 m)足够识别"已归位"。 - **中心点 ∈ `primaryArea.bounds`(软回弹)**:拖拽时 `target.pivot` 允许越出最多框短边的 5%(≈550 m,可微调);越界部分在**松手时按既有阻尼弹回**框内(非拖拽中持续回弹);`shown.pivot` 最终回到框内,越界只出现在拖拽过程。 - **显式模式锁**:`areaState: 'locked' | 'global'`;点「进入系统」/「归位」进区并锁定;**只有「返回全球」能解除**;判定与相机位置无关,不随跨界自动切换(避免贴边抖动)。 - **返回全球**:解除限位 + 阻尼飞到程序初始态全景(heading 0 / pitch −60 / range 20,000,000,与 `INITIAL_HPR` 一致),之后完全自由。 - **全球态闲置 5 秒**:回到程序初始态(星球自转恢复 + 屏幕中央闪烁「点击进入系统」);**不重置** `mode`/筛选;任何交互即重置计时。`map.intro.enabled=false`(走查自隔离)时按 `global` 自由视角启动、不弹引导页。 - **与视图模式合成(取交集)**:区内管线视图 `range ∈ [400, min(6000, range_max)] = [400, 6,000]`(因 `range_max=12,000 > 6,000`,与现状一致,无额外收紧);pitch 上下限沿用视图模式(区域不收紧 pitch)。 - **防入地(继承 02)**:相机离地高度 `h = range·sin|pitch|`;每次输入(滚轮/轨道)时取相机正下方地形(`globe.getHeight`),若 `h < 地形 + 10 m` 则**夹 range**(不夹 pitch、不动 pivot);瓦片未加载时退回固定 10 m 绝对下限。输入时 range 同时受 `[120, range_max]`、管线视图 `[400, 6000]`、防入地下限三处夹取,最终生效 = 三者交集。 - **状态归属**:`areaState` 进 `filterStore`(全局视图条件,与 `viewMode` 同层,切一级模式不重置);`mapService` 负责状态转移(进入/返回/归位);`cameraControl` 按状态换 limits(新增 `setRangeLimits` 一类,与既有 `setPitchLimits` 对称);5 秒闲置计时器与引导页显隐归 `CockpitShell`(组件内或 `uiStore`)。 - **顶栏按钮**:区内显示「归位」+「返回全球」两个 chip(归位沿用现有);区外只显示一个「归位/进入」chip。 - 三键分工与阻尼(ADR 0008)不变;限位只在输入时生效(现状 clamp 行为),无逐帧主动回拉。 ## Testing Decisions - **什么是好测试**:只断言外部行为 —— 限位生效后的可达 range 区间、状态机转移(进入/返回/闲置回引导)、软回弹的最终落点、防入地夹取、`isAtHome` 识别;不断言内部变量名或组件结构。 - **逻辑 seam(主)**:`domain/camera.js` 及同层新增纯函数(限位解析 / 状态机转移 / 软区夹取)—— 与既有 `verify/t03-camera-contract.mjs`(相机契约)、`t21-view-modes.mjs`(视图模式计划)同一模式:脱浏览器断言。 - **组件 seam**:`CockpitShell` 是唯一公共组件 seam —— 走查脚本断言顶栏 chip 的显隐/文案(`data-*`)、引导页显隐、`dataset.camera` 状态读数。 - **集成 seam**:`mapService.getCameraState()` + `filterStore.areaState` 为只读观测出口;走查断言"进入→锁定→滚轮拉远被夹→返回→闲置回引导"全过程,沿用 `t30-intro-walkthrough.mjs` 的做法。 - **先例**:`verify/t03-camera-contract.mjs`、`t30-intro-walkthrough.mjs`、`t30-view-presets.mjs`。按 `AGENTS.md`,改代码后不自动跑 build/dev,验证由维护者明确要求时执行。 ## Out of Scope - 视距雾气(曲线/配色/门控)—— 同 effort 07,`areaState` 与 `range_max` 是其输入契约。 - 低精度→高精度过渡手段 —— 同 effort 08。 - 镜头定位到监测点/设备的视角契约 —— 平行图 `cockpit-point-locate-and-device-link` 03(其 range 上限受 `range_max = 12,000` 约束)。 - 重做进入引导动画本身(票 30 已交付并验收)。 - 让 Three.js 接管场景(ADR 0004 / 0007 锁死;06 已裁定废止深度互通备选)。 - 主场景区的配置字段形状与兜底(01 已锁)、数值口径(02 已锁,锚点被本 spec 取代)。 - 改 `mode` / `panelId` / 面板语义与布局。 ## Further Notes - **联动风险(已确认归属,未裁决,留给平行图 03)**:主场景区只覆盖县城(13.4×11 km),而监测点散布全县(影像 105×122 km)。若区外监测点被选中/定位,中心点 ∈ `primaryArea.bounds` 的限位会挡住它 —— 该裁决归 `cockpit-point-locate-and-device-link/issues/03`(claimed);若裁定允许区外定位,则需在本契约开例外或收紧范围定义。 - **边缘口径(2026-09-18 维护者确认)**:`intro.enabled=false` 时"全球态闲置 5 秒回引导页"**不发生**(无引导页可回,保持 global 自由视角)。 - **软区与防入地 margin 均标注"可微调"**:实现与走查按观感校准,数值改动只涉及 domain 层常量,不改变契约形状。 - 本 spec 与 `docs/adr/0009-primary-area-camera-limits-and-entry-state.md` 一致;实现票按 Q6=(a)+(c) 接在 `.scratch/cockpit-shell-panel-layout/issues/` 30 之后(31+)。