What to build: 点「进入系统」或「归位」后进入主场景区并锁定:中心点被约束在区内(拖到边界软回弹)、滚轮最远只到归位默认视角(本部署 12,000 m)、相机不钻地形;「返回全球」解除限位并飞回自由全景;全球态闲置 10 秒(2026-09-30 由 5 秒改定)回到程序初始态。这是需求①「限高限低,不许超过限制范围」的落地。
Blocked by: 36
Status: resolved Labels: ready-for-agent
cockpit-primary-area-guardrails/spec.md。cockpit-primary-area-guardrails/issues/03-camera-contract-composition-and-entry-state.md(已 resolved);决策记录 docs/adr/0009-primary-area-camera-limits-and-entry-state.md。areaState: 'locked' | 'global' 成为与 ground/pipeline 同层的全局视图条件;切一级模式不重置。map.home.range(本部署 12,000 m);与管线视图 [400, 6000]、RANGE_LIMITS.min 三者取交集,区内管线视图因此与现状一致、无需额外收紧。primaryArea.bounds 内:拖拽时允许最多框短边 5% 的越界,松手按既有阻尼弹回;shown.pivot 最终回到框内,越界只出现在拖拽过程。map.intro.enabled=false(走查自隔离)时按 global 自由视角启动、不弹引导页。已完成(2026-09-28)。 本票是限位与状态机的落地;雾气(票 38)仍由下游票负责,
它消费本票的 areaState 与 range_max。
验收第 3 条「中心点约束在 bounds 内」与「点选定位」在契约里没有交界:
cockpit-primary-area-guardrails/spec.md 自己把它记为「联动风险,未裁决,留给平行图 03」,
而平行图 03 收口时只裁了 range/高度(range ∈ [120, range_max]、h ≥ 地形+10 m),
没有裁中心点。这个缺口是实质性的:监测点散布全县(影像 105×122 km),
主场景区只框了县城 13.4×11 km,硬夹会把绝大多数点位挡在区外、点选后镜头停在框边而不是点位上
(mock 数据 lon 110.141、本部署框西界 110.329,直接落到框外十几公里)。
维护者 2026-09-28 裁定:
两条都已落进 docs/adr/0009(ADR 与实现一致)与本票实现。附带约束:
拖拽从框外起手时不回弹(否则刚定位到的点位一按鼠标就被拽回框边)——这条是第 1 条的必然推论。
domain/areaLimits.js:限位解析(区域 × 视图 × 全局三处取交集)、软区/硬框夹取、
防入地夹取、状态转移(只有「返回全球」能解除)、顶栏形态、闲置判定。纯函数、不 import cesium。domain/camera.js:把程序初始态全景提成 INITIAL_PANORAMA 常量(返回全球与首帧占位共用一份)。services/cameraControl.js:新增 setRangeLimits(与既有 setPitchLimits 对称)、
getRangeLimits、clampToLimits;滚轮/setHpr/applyViewPreset 全部改走生效区间(含区域上限与防入地);
拖拽经软区夹取、松手走 springBackIntoBounds();定位豁免靠「按下时是否已在框外」判定。services/mapService.js:applyAreaLimits() 是唯一的合成点;setAreaState('enter'|'returnGlobal')
是唯一的转移入口(归位即进区);宿主读数 data-map-area-state / -area-locked / -range-max;
getAreaLimits() / getAreaState() 只读出口;DEV 把手 enterArea / returnGlobal。services/cesiumEngine.js:groundHeightBelowCamera() / cameraHeight() —— 防入地要的是
相机正下方的地形(相机与 pivot 可能隔着一个山脊,用 pivot 的地形会在斜坡上误判)。stores/filterStore.js:areaState 进 store(全局视图条件,与 viewMode 同层,切一级模式不重置)。CockpitShell.vue:顶栏两形态 chip、区内状态读数、闲置计时(只在 global 态、任何交互重置、
监听可拆)、intro.enabled=false 时按 global 启动;顺带修掉一处被本票放大的布局缺陷(见下)。docs/cockpit-deployment.md 新增 2.10 进区锁定与相机区域限位;ADR 0009 补两处口径。shown 仍由既有阻尼收敛 —— 手感与票 03/21 一致。applyAreaLimits()):视图切换与进区都调它,不各算一套。clampToLimits()):否则「进区时 range 已在 12 km 以外」要等用户滚一次才被夹
—— 这正是决策票 03 记的 4.7× 突跳来源。blocked 原因并如实提示),而不是锁进一个不存在的框。verify/t37-area-limits.mjs 通过:限位解析(区内 max=home.range、管线 min(6000,12000)=6000、
区外回落到全局)、状态转移(只有返回全球能解除;重复 enter 仍锁定 → 判定与相机位置无关;
可用性不足时进不去且带原因)、软区 ≈550 m 与松手回弹落点、防入地只夹 range、顶栏两形态、
闲置 10 秒且区内不计时、以及假引擎驱动的 7 组输入链(含"定位到框外不被夹、紧接着手动拖不瞬移"与
"框内起手仍有软回弹")。viewMode 赋值挪到 applyAreaLimits() 之后 → 顺序守卫红。@puppeteer/browsers
是空目录(既有环境缺陷,t28 等同样失败),改用 DSH 自带浏览器驱动同一条验收链,
走真实运行时配置(data-source=file):
| # | 用例 | 实测 |
| --- | --- | --- |
| A1 | 区外自由 | area-state=global、无「返回全球」、按钮「归位/进入」、上限 3×10⁷ |
| A2 | 进入系统 → 锁定 | locked、range-max=12000、出现「返回全球」、按钮变「归位」、落到 home(110.390, 28.444) 且在框内 |
| A3 | 区内滚轮拉不远 | 先拉近到 120(证明链路通),再连滚 60 次夹在 12000(非全局 3×10⁷) |
| A4 | 管线取交集 | min=400, max=6000(min(6000,12000),与现状一致) |
| A5 | 顶栏不压模式按钮 | 状态区 512px 装进 521px 列,模式区终于 841 < chip 起 853,0 冲突 |
| A6 | 拖到边界回弹 | 拖到 110.47140(软区边)、松手 target 回到 110.465779226663(硬框边);setCameraDamping(0) 同步路径下 shown 同样在框内 |
| A7 | 返回全球 | locked→global、上限回 3×10⁷、目标 hpr 精确=heading 0 / pitch −60 / range 2×10⁷、按钮变回 |
| A8 | 全球态闲置 | 12 秒无交互后引导层带「点击进入系统」回来 |
| A9 | 区内闲置不回引导 | locked 闲置 12 秒,引导层仍为 false |
| A10 | 无页面级报错 | enter/return 往返三次,error/unhandledrejection 监听零记录 |areaLimits.js,230→233)。
t28-fill-container.mjs 是 puppeteer 脚本,因上述依赖缺失而 ERR_MODULE_NOT_FOUND,与本票无关。AGENTS.md:本会话未跑 build(浏览器走查是维护者本次明确要求才执行)。applyAreaLimits() 与 viewMode 的赋值顺序错了(我自己写出来又抓到的):先算限位、后落
viewMode,于是切到管线时区域上限仍按地面算(读数 12000 而非 6000)。domain 闸门与静态守卫
都发现不了(它们只测纯函数、只看"有没有调用"),只有真跑一遍才暴露;已修并把"顺序"做成断言(变异 ⑥)。justify-content: flex-end 向左溢出 46px,
实测「归位」(x 855–897)压住「预警处置」(x 799–898)。
修法:状态列由 minmax(240px, 1fr) 改为 minmax(min-content, 1fr)(按内容最小宽度兜底)。
走查加了 A5 按真实几何断言这条,避免以后再插 chip 时悄悄压上去。areaState 与 range_max 是可靠输入。range 500/150)属平行图 cockpit-point-locate-and-device-link 票 03,
本票只保证它们落在限位内(两档都远低于 12,000 m,任何画布比例下不会被夹)。document.hidden===true,rAF 被完全冻结(实测 1 秒 0 帧),
因此"阻尼收敛过程"的断言改用 setCameraDamping(0) 的同步路径验证(同样是既有契约的正常入口);
阻尼本身的手感验收仍由票 03 的真机走查覆盖,本票不重复断言。37 就位,38(区内视距雾气)解除阻塞 —— 门控读 getAreaState()(locked 生效、global/引导态关闭),
上限读 getAreaLimits().max(本部署 12,000 m),不要再自己解析主场景区配置。
39(粗→精影像交接)依赖 38;40(全程质量收口)留到最后。