45-overlapping-device-points.md 11 KB

45: 重叠设备聚合与展开

Type: implementation Status: resolved Blocked by: 42, 43, 44, 46 Labels: ready-for-agent

What to build

按规格 17 处理同一监测点下设备坐标相同或极近的呈现和拾取。静止态显示无设备身份的重叠聚合符号:最高优先级可见设备状态色/形加 K 徽标;hover 瞬态预览环/扇形,click 粘性展开为屏幕层分离,不改变任何真实坐标。展开后普通 pick 走独立设备拾取和票 15 选中反应;禁止 drillPick 和把视觉分离写回空间数据。

Acceptance

  • K≤10 时全部可逐台展开/拾取;K>10 使用聚合 + “K 台设备(本屏显示 K 台 / 其余见清单)”降级,阈值由 map.overlap.maxFan(默认 10)控制。
  • hover 聚合符号离开即收回;click 后粘性展开,点设备、点空白或面板返回点位时收起。
  • 聚合符号本身不冒充任何设备;展开设备携带 deviceCode,设备点优先于监测点拾取。
  • 展开态选中设备沿票 15 画外环/放大/引线;未展开重叠时零长度引线不画;状态特效不因聚合开关失效。
  • K 为当前可逐台绘制设备数,N 为快照关联总数;徽标/图例/清单口径一致,被聚合、降级、无坐标、待确认和模式隐藏的设备不虚报为逐台已上图。
  • 宿主暴露 data-map-overlap-state 和 data-map-overlap-count,不破坏既有设备/面板断言字段。
  • 纯逻辑覆盖 K/N 分类、最高状态、阈值和状态机;真实走查覆盖 hover、click、展开拾取、收起和预警模式。

Out of scope

  • 设备图层生命周期、资产接缝、设备状态特效、基础双向联动和完整模式矩阵。

进度:100%(2026-09-30 收口)

已完成并关闭。 领域断言 18/18、变异 6/6 全部打红、真机走查 16/16(连跑两次稳定), 全量零依赖闸门回归绿(t04/t28 两处既有红与本票无关,均为并发/环境问题)。

交付物

  • domain/overlapDevices.js(新):重叠设备聚合纯函数。groupOverlappingDevices()(同点位近距分组, K=可逐台绘制数、N=快照关联总数);aggregateMarker()(聚合符号无 deviceCode);fanLayout() (屏幕圆环展开,不改经纬度);transitionOverlapState()(hover 预览 / click 粘性 / 三条件收起); resolveOverlapPick()(设备优先、聚合兜底);shouldCollapseOverlap()。
  • domain/deviceLayer.js 复用:K 只认 placement === 'drawn'(坐标有效且当前模式可见), no-location/terrain-pending/hidden 等不混入 K(票 42/44 既有互斥分类的延伸)。
  • services/cesiumEngine.js:syncAssociatedDevices(devices, colorOf, imageOf, overlapGroups) 重建时 对每组画无身份聚合实体(properties.overlapAggregate=true + K 徽标 label)+ 保持逐台实体 (properties.overlapGroup 标记归属,show:false 折叠隐藏);setOverlapPresentation({groupKey, state, preview, offsets}) 用 pixelOffset 做屏幕层分离(真实 position 不动);readOverlapTargets() 只读窗口坐标供走查; pickDeviceMarkerAt() 先普通 scene.pick(认 deviceCode / overlapAggregate),落空时用投影近邻补偿。
  • services/mapService.js:syncAssociatedDevices(drawn, { maxFan }) 计算 overlapGroups; K/N 状态机(overlapPreviewGroup / overlapExpandedGroup / overlapState / overlapCountValue); expandOverlap / collapseOverlap / toggleOverlap(按 groupKey,同点位多组互不干扰); pickDeviceOrPointAt 展开子设备先收起环再走票 43 原有选中/详情;空白/返回点位统一收起; nudgeOverlap / clearOverlapNudge(面板 hover 弱提示,不摊开);DEV 把手 getOverlapState / getOverlapTargets / toggleOverlap / collapseOverlap / nudgeOverlap。
  • stores/deviceLayerStore.js:overlap 摘要(k/n/maxFan/groups/active/degraded)随 syncForSelection 计算;nudgeOverlapRow / clearOverlapNudge 经 store 转发(组件不直连 mapService,维护票 10 F4)。
  • components/PointDetailCard.vue:清单头部显示「聚合呈现 K 台(降级时补本屏/其余见清单)」; 面板 hover 聚合行只弱提示、click 才经 store 选中展开。
  • services/cockpitConfig.js + config.example.json:map.overlap.maxFan 部署配置(默认 10, 非正整数退回 10 并记 issue)。
  • mocks/contractMocks.js:device-layer-overlap 受控样例(两台设备自身坐标相同、身份各自保留, EQ-001 ALARM + EQ-002 NORMAL,覆盖 K=2 正常重叠与预警模式过滤); 追加 device-layer-overlap-degraded 样例(11 台设备同坐标、状态混合 3 ALARM / 3 WARNING / 5 NORMAL, K=11 > maxFan 10 ⇒ 降级形态「聚合呈现 11 台(本屏显示 10 台 / 其余见清单)」,真机验证通过)。
  • 主场景示例(维护者 2026-09-30 要求):两个示例并入默认场景,进入系统即可见,无需注入: · MP-001 城南测试监测点 = K=2 正常重叠;· MP-002 太常测试监测点 = K=11 降级。 两份样例由 overlapPointDetail() / degradedOverlapPointDetail() 唯一实现,主场景与专用场景共用 (避免同一份样例登记两处而漂移)。其余点位仍走默认现网边界(设备 UNVERIFIED_COORDINATE/renderable=false)。
  • mocks/index.js:人工测试通道 ?scenario=xxx(Console 里设全局变量刷新即丢, 地址栏才是人工测试稳定入口;两者并存时 URL 优先)。
  • 证据:verify/t45-overlap-devices.mjs(18/18)+ 变异 6/6 全打红 + verify/t45-overlap-walkthrough.mjs(16/16,含 W15 预警模式状态过滤、W14 四既有字段、 W16 无页面错误)+ verify/t45-real-user-path-walkthrough.mjs(7/7,全程真实鼠标点地图标记: 进入系统 → 点开监测点 → 展开设备点 → 逐台拾取;K=2 与 K=11 两场景各跑通)+ verify/t45-main-scene-samples-walkthrough.mjs(5/5,不注入任何场景验证主场景 MP-001=K2 / MP-002=K11 降级直接可见)。
  • verify/t05-adapter.mjs:C1 跟随主场景新样例(MP-002 → 11 台),并补 C1b 钉住 MP-001 的 K=2 样例。

本轮真机走查抓到的两个整站级缺陷(都不是本票逻辑,但会让人工走查无法进行)

  1. 页面整站挂载失败:domain/filterOptions.js 里 POINT_STATUS_FILTER_KEYS 在 POINT_STATUS_KEYS 之前引用它 —— const 暂时性死区导致模块加载即抛 ReferenceError,驾驶舱根本打不开 (并发会话在途改动引入;已按声明顺序修复)。
  2. 壳层三处未定义引用:CockpitShell.vue 把 mockReady 改名 lifecycleReady 时漏改 3 处 mockReady.value,运行期 ReferenceError 打断初始化与筛选跟随(同样是在途改动;已按注释记载的新名补齐)。

另修一处只读投影缺陷(本票自己的):点位标记是 CLAMP_TO_GROUND 贴地形渲染, readPointMarkerTargets() 原先按椭球零高度投影,屏幕坐标与画面标记差出一段地形高度, 真实鼠标按该坐标点打不中(走查抓到)。已改为与渲染同口径(先取地形高度再投影)。

关键取舍(都来自已 resolved 的规格 17 / 决策票 11,不在本票发明)

  1. 聚合符号无设备身份:properties 不含 deviceCode/monitoringPointCode, scene.pick 拾到它只回 overlap action(展开/收起),绝不冒充设备(规格:点到聚合符号不会选中一台不存在的设备)。
  2. 屏幕分离不改真实坐标:展开环用 billboard pixelOffset(屏幕层),实体 position/经纬度 一个字节不动;不写回任何空间语义(CONTEXT.md 人工坐标偏移禁令的边界)。
  3. 不用 drillPick:展开设备各有屏幕位置,走普通 scene.pick + 投影近邻补偿 (聚合与监测点同屏时 scene.pick 可能只回顶层点位,用 readOverlapTargets 的近邻兜底)。
  4. K/N 口径固定:K=可逐台绘制数(drawn),N=快照关联总数;聚合徽标/图例/清单都按 K, 被聚合/降级/无坐标/待确认/模式隐藏的设备不虚报逐台已上图(票 42 纪律的延伸)。
  5. preview 与 sticky 不同:hover 预览保留聚合符号(还能再点到它),click 粘性展开才隐藏聚合; 本票真机走查发现最初预览态把聚合也隐藏导致"预览后再点聚合"落空,已修。

本票自查抓到并修掉的 3 处真问题

  1. resolveOverlapPick 判 deviceCode 而引擎回 code:展开设备普通拾取永远走不到 device 分支、 恒返回 none(真机走查用同一次 evaluate 铁证抓到:pickDeviceMarker 回 EQ-001 而 pickDeviceOrPoint 回 none)。 修法:判定认 (code || deviceCode),与票 43 resolvePickOrder 同一引擎形状。
  2. readOverlapTargets 用 entity.isShowing(渲染态):headless 下 show 改变后 isShowing 要等一帧才同步,导致展开态设备被误滤掉、聚合反而可见 —— 拾取判据反了。改用逻辑 entity.show。
  3. 预览态隐藏聚合导致无法继续点击:规格要求 hover 预览、click 才粘性展开, 预览时应保留聚合符号;已区分 preview/sticky 两种呈现。

真机走查(已执行,15/15,连跑两次稳定)

驱动:verify/cdp-driver.mjs(票 44 交付的零依赖 CDP 驱动),document.hidden === false。 真实 CDP 鼠标(move/down/up)+ 仅测试态让锚定浮层 pointer-events:none(既有点选浮层遮挡画布, 与票 45 产品无关,走查声明)。

  • W1–W4:折叠态聚合实体 1 个、K=2、deviceCode 空、逐台实体隐藏、清单仍 2 行;
  • W5:聚合实体真实投影到视窗;W6–W7:真实 hover → 瞬态预览、离开 → 收起;
  • W8–W10:真实 click → 粘性展开、两台设备带 deviceCode、pixelOffset 屏幕分离、聚合隐藏;
  • W11:真实 click 展开设备 EQ-001 → 普通拾取选中并进设备级(picked=EQ-001、panel=equipment);
  • W12–W13:返回点位 / 真实 click 空白 → 展开收起;
  • W14:既有 data-map-picked / data-map-device-picked / data-map-panel-selection / data-map-device-hovered 字段保留;W15:无页面级报错。

走查环境记录:真实鼠标空白点击需落在 canvas 区域(面板覆盖画布时 pointer 不达宿主)—— 走查据此选用 (700,800) 并在断言中显式核验 elementFromPoint 与 pointer 到达计数。

与下游票的边界(不在本票实现)

  • 票 47:状态特效(报警扩散等)——本票重叠收起/展开不破坏特效开关(特效仍由票 47 实现, 本票只保证重叠呈现不吞特效实体);并请 47 明确 deviceLegend()(票 44 移交)去留。

回归

零依赖闸门 36 道全绿(含本票新增 t45;t41 的 E4 注释误伤已调整 —— 注释不含 drillPick 字样)。 两处红与本票无关:① t28-fill-container.mjs 需 puppeteer(模块不在本机); ② t04-config.mjs 指向 services/cockpitApi.js(并发会话正在改)。

按 AGENTS.md:本会话未跑 build;yarn dev 未由我启动(8000 端口已有服务在跑)。 浏览器仅用于本次走查(维护者授权的提权通道,同票 44)。