# 41: 监测点定位与面板锚定接入 ## 进度 Type: implementation Status: ready-for-agent Blocked by: 37 Labels: ready-for-agent ## What to build 把决策地图票 03 的点位/设备两档定位行为与票 04/12 的面板锚定跟随接入现有 `/cockpit` 外壳。监测点点击和台账选择必须进入同一选择语义:点位级定位使用 `map.locate.point`(缺省 500m),设备级由票 43 消费;面板锚定跟随监测点标记中心,四向避让,完全离开画布才隐藏;无地图位置时显示注明原因的固定详情,不伪造定位或锚点。 ## Prerequisites - 复用已关闭的 03 相机控制、36/37 主场景区配置与限位、30 引导/归位语义。 - 不重建 Viewer,不改 ADR 0008 三键相机语义,不改点位状态词表。 - 以 14/15/16/17/18 的外壳接缝为后续输入,不在本票实现设备图层或设备状态特效。 ## Acceptance - [x] 地图点击和监测点台账选择使用同一监测点选择入口。 - [x] 有效点位坐标触发点位级完整视角定位;定位完成以 `settled` 和配置 hpr 为准。 - [x] 详情面板锚定跟随监测点标记中心,四向避让生效;标记完全离开画布时隐藏,回到可见范围时恢复。 - [x] 关闭/取消不回退镜头;无地图位置时保留固定详情与非空原因,不创建虚假锚点。 - [x] 进入引导、归位、模式切换和区域限位回归不破坏既有契约。 - [x] 添加纯逻辑断言和外壳可观察走查证据,禁止读取 Cesium 私有实体。 ## Out of scope - 关联设备临时图层、设备点位资产、状态特效、重叠展开、完整模式矩阵。 ## 进度:100%(2026-09-29 收口) **已完成并关闭。** 逻辑闸门 **46/46**、变异测试 **20/20 全部打红**、**真机走查已执行**(见下), 全量零依赖闸门回归绿。 **维护者本轮另指定了两处测试点位坐标**(走查用):`MP-001` 更名「城南测试监测点」 (`110.39832789395186, 28.434456975811777`)、`MP-002` 更名「太常测试监测点」 (`110.36299369455823, 28.452196938060194`)。两处坐标源(`map-snapshot` 的 `MAP_POINTS` 与台账 `/monitoring/distribution` 的 `distributionPoint`)**同时改**——只改一处会让 "点同一个点飞到两个地方"。两个坐标都落在主场景区内,县域默认视角下可见。 ### 交付物 - `domain/pointLocate.js`(新):两档定位视角(`map.locate.point` 缺省 `0/−55/500`、`device` 缺省 `150`) + 竞态判据 `isStaleLocate(mine, current)`。纯函数,配置逐项兜底、负 range 不生效。 - `domain/pointAnchor.js`(新):锚定布局(四向顺序右→左→下→上、12px 间距不压图标、 边缘留白、**拥挤时允许裁切而不消失**、只在完全离屏时隐藏)。 - `pointSelection.js`(新):**统一选择入口**。放在接线模块而不是 `detailStore`,因为两侧都不许自己长出对方: `detailStore` 不得依赖 `mapService`(票 10 的 F4 守卫),`mapService` 不依赖 Vue/pinia(票 02 的层次纪律)。 - `services/cesiumEngine.js`:新增 `groundHeightAt()` / `projectPointMarker()` / `hasPointMarker()` —— 只用 Cesium **公开**实体与公开拾取,不读私有成员(验收第 6 条)。 - `services/mapService.js`:`locateMonitoringPoint()`(完整视角预设 + 地形采样 + 序号防竞态)、 `cancelPointLocate()`、`onMapFrame()`(渲染帧触发)、`onCameraStateChange()`(相机变化触发)、 `getMapCanvasRect()` / `getMapMarkerAnchor()` / `hasMapMarker()`。 - `domain/coordinate.js`:新增 `locationVerdict(objects, code)` —— 把「**不在已加载快照里**」与 「**后端判不可上图**」分开(判定在 domain,store 只喂已加载对象)。 - `stores/mapStore.js`:`locationVerdict()` 出口(把已加载对象喂给 domain 判据)。 - `stores/detailStore.js`:`closeDetail` 时作废在途采样(**不回退镜头**,ADR 0008 不变)。 - `CockpitShell.vue`:锚定跟随、归属短线、暂隐时的顶栏入口(对象编码 + 原因 + 关闭 + **仅标记有效时**的重新定位)、 固定详情退化、`z-index: 1`(**低于**四个详情窗,规格 R-3)。 - `components/PointDetailCard.vue`:`locationReason` 属性 —— 无地图位置时一行如实说明原因。 - 配置:`cockpitConfig.js` 归一化 `map.locate.*`(逐项校验 + issue 记录),`config.example.json` 同步。 - 证据:`verify/t41-point-locate-anchor.mjs`(**46/46**)+ 变异测试 **20/20 全部打红** + `verify/t41-point-locate-anchor-walkthrough.mjs`(真机走查脚本,供复跑)。 ### 真机走查(已执行):抓出并修掉 6 处真缺陷 本机 `@puppeteer/browsers` 是空目录(票 37 记录的既有环境缺陷),故按票 36/37 成例 **改用 DSH 自带浏览器真实驱动同一条验收链**(`yarn dev` @8000,真实 `public/cockpit/config.json`)。 **6 处缺陷全部是纯逻辑闸门不可能发现的**: 1. **详情端点永远回默认名称**(mock 缺陷,被本次改名暴露):`/monitoring-points/*` 只把请求的 **编码**写回,**名称**永远取默认载荷那一个 —— 走查点 MP-001 却显示「太常测试监测点」。 修:按请求编码回显名称(只回名称,状态/原因仍是样例手工构造的那一份,不制造 "状态正常、原因却是压力超高"的自相矛盾)。 2. **锚定在页面不可见时完全不动**:`refreshAnchor()` 把 `document.hidden` 写成无条件早退, 于是标签页在后台时**连首次布局都不做**,面板永远停在初值 `canvas-missing`。 修:只有**逐帧**重算跳过,**显式**重算(选择/尺寸/相机变化/挂载)照常执行。 3. **投影要求"配了地形就必须取到高度"**:相机一飞到瓦片尚未加载的区域,`globe.getHeight` 返回 undefined → 被误判成"标记离屏"、面板整块消失,而画面上那个标记明明还看得见。 修:取不到就回退**椭球面**——那正是 Cesium 的 `CLAMP_TO_GROUND` 在无地形数据时的画法, 锚点判据不能比"可见性"本身更严。 4. **暂隐的面板仍拦截地图点击**:只设 `visibility:hidden`,而该浮层是 `pointer-events:auto` 的 绝对定位元素 —— 看不见却吃掉覆盖区域的交互。修:暂隐时同时 `pointerEvents: 'none'`。 5. **程序化下发视角不通知锚定**:`applyViewPreset`(定位/归位/返回全球/自转)不经过相机输入回调, 只靠渲染帧时,隐藏标签页里点「重新定位」会**相机已回到点位、面板仍显示隐藏**。 修:下发成功后通知一次相机状态变化。 6. **相机变化缺第二条触发路径**:锚定原先只挂 `scene.postRender`,不渲染就完全停更。 修:新增 `onCameraStateChange()` 订阅(相机输入与程序化下发都走它)。 **实测证据(真实浏览器)**:进入引导 → 落家视角(中心高程 111.58 m 实测地形)、进区锁定; 点/选 MP-001 → `range=500`、`pitch=−55`、`heading=0`、中心=点位坐标、中心高程 120.22 m(实测地形); 面板 `anchorState=visible`、`side=right`、`z-index=1`、**距标记中心 21 px**(= 9+12 规格下限)、 **不覆盖图标**、完全在画布内、归属短线在位;名称显示「城南测试监测点」(缺陷 1 修复后); 真实拖拽把标记拖出画面 → `anchorState=hidden`、`reason=marker-offscreen`、`visibility:hidden`、 `pointer-events:none`、详情仍在 DOM(保留选择),顶栏出现 「MP-001 · 点位标记已离开地图画面 重新定位 关闭」;点「重新定位」→ 相机回到点位(range=500、settled)。 **未覆盖项(如实记录)**:① 走查时浏览器标签 `document.hidden===true`,rAF 被冻结、阻尼循环不推进 (票 37 记录过同一环境事实),因此"四向翻转"的真机观感未逐向走查 —— 四向顺序与避让判据由 t41 闸门 D1/D3/D5/D6 纯逻辑覆盖;② 固定详情(`renderable=false` 的对象)需该对象**已被分页加载**, 而冻结环境下分页不推进,故真机只验到"未加载 → 暂隐(**不是**固定详情)"这条更易错的语义, 固定详情本身由 t41 闸门 E2b 纯逻辑覆盖;③ 建议维护者在**前台标签页**复跑一次 `t41-point-locate-anchor-walkthrough.mjs` 补这两条观感。 ### 三条关键取舍(都来自已 resolved 的决策/规格,不在本票发明) 1. **定位走完整视角预设**(`applyViewPreset` 连 hpr 一起换),**不是** `setCameraFocus` 的"只换中心"—— 后者会把 range 留在县域 12000 m,正是决策票 03 要解决的问题。闸门 B1/B2 用同一台假引擎把两者对照钉住。 2. **锚定只避开画布边缘**:按规格票 12 的修订节,左右业务栏/图例/底部摘要**不再是避让矩形**, `no-space` / `icon-covered` 两种暂隐原因**在本票实现里不存在**(规格要求移除,不留死代码); 唯一隐藏条件是"窗口完全离开画面"。代价(极端拥挤时被裁切/被侧栏盖住)维护者已明确接受。 3. **无地图位置只认后端判定**:对象**不在已加载快照里**(分页没翻到 / 引擎未就绪)**不算**无地图位置 —— 那是"暂时无法锚定",走暂隐并保留选择;只有后端 `renderable=false` 才转固定详情。 `/monitoring/distribution`(台账数据源)**不逐对象给坐标校验**,所以台账入口在快照确认前一律走暂隐路径, 不伪造固定卡。 ### 被本票改写的既有断言(两处,均已在原地注明理由) - `t18-monitoring-panels.mjs` 的 **H12**:原文要求台账函数体出现 `detailStore.openPoint` 且**不移动相机**; 本票验收第 1、2 条改为「同一入口 + 触发点位级定位」,判据换成"台账调统一入口且不自己碰相机", 并保留 `locateCode` 的对照组(证明不是"全文没有 setCameraFocus 就绿")。 - `t18-monitoring-panels-walkthrough.mjs` 的 **C2**:原文「点台账卡片不动相机」→ 改为 「点卡片触发点位级定位(range < 2000)」,并保留点击前 range 作对照。 (成例:t01 的 A5 在票 17 被改写。) ### 回归 **零依赖逻辑闸门全绿**(t01/t03/t04/t05 **248**/t06/t07 **39**/t08/t09/t10 **57**/t11/t12/t13/t14/t15/t16/ t17/t18 **71**/t19/t21/t21-view/t29/t30/t36/t37/t38/t39/t41 **46**)。 **唯一一处红是既有环境缺陷、与本票无关**:`t28-fill-container.mjs` 需要 puppeteer, 而本机 `@puppeteer/browsers` 是空目录(票 37 已记录同一缺陷)。 **按 `AGENTS.md`:本会话未跑 build**(真机走查是维护者本轮明确要求才执行的)。 ### 下一步 1. 维护者在**前台标签页**复跑 `t41-point-locate-anchor-walkthrough.mjs`,补"四向翻转观感"与 "固定详情"两条(其余项已在本次走查覆盖)。 2. 若 1080p/4K 实测发现 300×280 CSS px 下限或帧耗不合适 → **回决策票 04 重议数值** (规格明确不许实现票自行改掉"贴点/暂隐"优先级)。 3. 本票已 close,`42`(关联设备临时图层)解除阻塞。 ## Comments ### local — 2026-10-10T07:10:36.628Z 整体验收审查发现无位置监测点固定详情被modeLayerPlan.selectionVisible的marker判据阻止挂载(overview也受影响)。对应规格要求台账打开固定详情。详见../verify/artifacts/t48/acceptance-status.md;尚未修复/真实回验,当前关闭状态仅代表历史交付,不代表新整体验收通过。 补充:本阶段已修复固定无位置点详情门控,t48-linkage-invariants回归通过;真实台账目前无样本,未按本阶段再关闭本票。