# 41: 监测点定位与面板锚定接入 ## 进度 本轮已认领并完成收口。修复EMPTY合法监测点详情身份丢失(`services/detailApi.js`不再把pointStatus=EMPTY当整分区无数据,保留名称/编码/状态/零设备统计/坐标原因);公开台账入口→生产浮层模板组件SSR9/9,旧marker门控与旧EMPTY丢弃两处变异分别触发4/1条行为失败、还原9/9;定位锚定46/46、两级详情/模式矩阵/联动不变量通过。真实画面复验(real数据源,扩展恢复后):台账点「供水监测点001」(GS001)→浮动固定详情显示名称/编码/无设备/关联0台、无归属引线;关闭后详情从DOM消失、镜头未回退。本票六项验收全部通过,resolved关闭。 Type: implementation Status: resolved 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] 关闭/取消不回退镜头;无地图位置时保留固定详情与非空原因,不创建虚假锚点(源码+SSR+真实画面均通过)。 - [x] 进入引导、归位、模式切换和区域限位回归不破坏既有契约。 - [x] 添加纯逻辑断言和外壳可观察走查证据,禁止读取 Cesium 私有实体。 ## Out of scope - 关联设备临时图层、设备点位资产、状态特效、重叠展开、完整模式矩阵。 ## 本轮复验交付与下一步(优先于历史记录) - `services/detailApi.js`不再把pointStatus=EMPTY当成整个详情分区无数据;无关联设备仍是合法监测点,保留编码、名称、业务状态、零设备统计及coordinateValidation,不修改坐标准入判据。 - 新增[公开台账入口与固定浮层SSR回归](../verify/t41-fixed-detail-render.mjs):真实monitoringStore.openLedgerPoint→统一选择入口→Pinia详情→模式/坐标领域判据→生产浮层模板与真实PointDetailCard,9/9。通过原有定位出口观测:不可上图点不下发locateMonitoringPoint。EMPTY载荷丢失先红后绿。 - 旧marker门控变异导致4项渲染断言失败;旧EMPTY载荷丢弃导致1项身份断言失败;还原正常源码9/9。加载器仅在测试进程内变异,不改写生产文件。 - 当前定位/锚定46/46;两级详情、模式矩阵及联动不变量闸门通过;共享适配层282/283,失败D1-overviewStore-no-raw-fields来自其它概览store修改,未为绿灯改变判据。 - Standards:无硬性违规、无需要修复的smell。Spec:修复满足固定详情身份要求,无scope creep;SSR人为提供外壳输入,未执行真实外壳生命周期/watchers/CSS/hydration。零点位定位请求也不等于验证全部浏览器相机命令。详见[本轮证据记录](../verify/artifacts/t41/fixed-detail-reverification.md)。 - 维护者明确允许现有页面复验,不构建/不启停服务。browser启动超时后列表和tab状态不一致;随后qqco启动成功,snapshot报extension not responding,未取得本轮截图。此前停止操作错误地同时传session与空requestId多次失败;不报告Agent Window已清理,不换其它浏览器进程或debug脚本绕过。 下一步:扩展响应恢复后,在现有real页面台账打开后端已加载的区外/无位置EMPTY点位,核对身份、原因、固定可见且无引线;关闭后核对镜头不回退。另补有效点的四向翻转/离屏恢复观感。后端空点台账已由真实联调票修复,本轮不再声称“当前无真实样本”,以最新请求和页面为准。画面证据齐全后再关闭本票并向父地图追加完成gist。当前不关闭,不追加完成gist。 ## 历史进度: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回归通过;真实台账目前无样本,未按本阶段再关闭本票。 ### local — 2026-10-13T04:07:27.005Z 本轮公开台账入口→真实PointDetailCard和生产浮层模板SSR9/9;新增EMPTY合法点位身份丢失修复。两处旧缺陷变异分别触发4/1条行为失败,还原9/9。双轴审查认可修复但SSR不覆盖完整外壳生命周期/CSS/相机。后端空点台账已修复,旧无样本结论不沿用;浏览器扩展未响应,无本轮画面证据。保持claimed/open,下一步扩展恢复后real台账固定详情与有效点锚定画面复验。