41-monitoring-point-locate-and-panel-anchor.md 12 KB

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

  • 地图点击和监测点台账选择使用同一监测点选择入口。
  • 有效点位坐标触发点位级完整视角定位;定位完成以 settled 和配置 hpr 为准。
  • 详情面板锚定跟随监测点标记中心,四向避让生效;标记完全离开画布时隐藏,回到可见范围时恢复。
  • 关闭/取消不回退镜头;无地图位置时保留固定详情与非空原因,不创建虚假锚点。
  • 进入引导、归位、模式切换和区域限位回归不破坏既有契约。
  • 添加纯逻辑断言和外壳可观察走查证据,禁止读取 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回归通过;真实台账目前无样本,未按本阶段再关闭本票。