|
|
@@ -43,6 +43,8 @@ import {
|
|
|
import { planDeviceAsset } from '../domain/deviceAssets.js'
|
|
|
// 批次 3:位置符号合成计划层(规格 10 的唯一新接缝)与母版合成渲染。
|
|
|
import { planMarker, planToDataset } from '../domain/markerPlan.js'
|
|
|
+// 票 101:管线构件点位标记行(符号/标签/行业色)由领域层单源给出,服务层不拼视觉。
|
|
|
+import { pipeComponentMarkers } from '../domain/pipeNetwork.js'
|
|
|
// 票 82:标记几何单源(本体宽 / pad / 绘制宽),服务层不再持有尺寸字面量。
|
|
|
import { MARKER_BODY_WIDTH, markerDevKnobs, markerMetricsFor, resetMarkerDevKnobs, setMarkerDevKnobs } from '../domain/markerMetrics.js'
|
|
|
// 票 76(C3):强调量由服务层直接产出给 DOM —— 判据复用引擎同一份 domain 单源,
|
|
|
@@ -224,6 +226,16 @@ let markerSkipped = 0
|
|
|
// 票 42:关联设备临时图层的最近一次计划与结果(与摘要标记**分开**计数,互不污染)。
|
|
|
let deviceRows = []
|
|
|
|
|
|
+// 票 68:设施点(水厂/泵站/水源地/用水户)的最近一次计划行。与摘要标记/关联设备**分开**持有:
|
|
|
+// 设施点不进摘要序列、不进设备图例,也不参与设备的动效/重叠判定。
|
|
|
+let facilityMarkerRows = []
|
|
|
+let selectedFacilityKey = ''
|
|
|
+let facilitySelectionIdentity = null
|
|
|
+
|
|
|
+// 票 101:管线构件点位(阀门/窨井/特殊管点等)的最近一次标记行。同样独立持有:
|
|
|
+// 构件点击走**管线拾取链**(局部增强),不进设施详情,也不进设备/设施图例。
|
|
|
+let pipeComponentRows = []
|
|
|
+
|
|
|
/**
|
|
|
* DOM overlay 每帧取数的复用槽(票 35 卡顿整改)。
|
|
|
*
|
|
|
@@ -240,11 +252,43 @@ const overlayFrameCache = {
|
|
|
// 票 85:跨图层合计门控与收敛系数(降级时环仍画,只是更小更淡)。
|
|
|
combinedRef: null, converge: null,
|
|
|
// 静态帧闸门:镜头位姿/宿主矩形/输入计划都没变时,66ms 内的重复帧直接跳过。
|
|
|
- pose: '', rectKey: '', at: 0, lastPlan: null, lastRows: null, lastEffects: null,
|
|
|
+ pose: '', rectKey: '', at: 0, lastPlan: null, lastRows: null, lastFacilityRows: null, lastComponentRows: null, lastViewMode: '', lastEffects: null,
|
|
|
lastDeviceEffects: null, lastCombined: null,
|
|
|
}
|
|
|
const overlayPlanMarkerCache = new Map()
|
|
|
|
|
|
+// 票 68:设施点合成计划的记忆化包装(离散组合只有 type|status|tone 三类,数量极小)。
|
|
|
+const overlayFacilityPlanCache = new Map()
|
|
|
+
|
|
|
+// 票 101:构件点合成计划(离散组合只有 componentKind|industry 两类)。
|
|
|
+const overlayComponentPlanCache = new Map()
|
|
|
+
|
|
|
+function overlayComponentPlanMarkerCached(marker) {
|
|
|
+ const key = `${marker.componentKind || ''}|${marker.industry || ''}`
|
|
|
+ let plan = overlayComponentPlanCache.get(key)
|
|
|
+ if (!plan) {
|
|
|
+ plan = planMarker({ kind: 'pipe-component', componentKind: marker.componentKind, industry: marker.industry })
|
|
|
+ if (overlayComponentPlanCache.size > 64) overlayComponentPlanCache.clear()
|
|
|
+ overlayComponentPlanCache.set(key, plan)
|
|
|
+ }
|
|
|
+ return plan
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 设施点合成计划(票 68):走 `planMarker` 的新 kind,**不复用** `overlayPlanMarkerCached` ——
|
|
|
+ * 后者把「非监测点」强转成监测设备,设施点会丢设施类型图标并被降成设备档。
|
|
|
+ */
|
|
|
+function overlayFacilityPlanMarkerCached(marker) {
|
|
|
+ const key = `${marker.type || ''}|${marker.status || ''}|${marker.tone || ''}`
|
|
|
+ let plan = overlayFacilityPlanCache.get(key)
|
|
|
+ if (!plan) {
|
|
|
+ plan = planMarker({ kind: 'facility-point', status: marker.status, facilityType: marker.type, tone: marker.tone })
|
|
|
+ if (overlayFacilityPlanCache.size > 64) overlayFacilityPlanCache.clear()
|
|
|
+ overlayFacilityPlanCache.set(key, plan)
|
|
|
+ }
|
|
|
+ return plan
|
|
|
+}
|
|
|
+
|
|
|
/** `planMarker` 的记忆化包装:输入是少量离散组合(kind|status|industry|tone|unassociated)。 */
|
|
|
function overlayPlanMarkerCached(marker) {
|
|
|
const kind = marker.kind === 'monitoring-point' ? 'monitoring-point' : 'monitoring-device'
|
|
|
@@ -299,15 +343,23 @@ export function getMapCanvasRect() {
|
|
|
return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom }
|
|
|
}
|
|
|
|
|
|
-export function hasMapMarker(kind, code, pointCode = '') {
|
|
|
+export function hasMapMarker(kind, code, pointCode = '', key = '') {
|
|
|
if (!instance || !instance.engine || !instance.viewer) return false
|
|
|
+ if (kind === 'facility-point') return Boolean(facilityMarkerFor(code, key))
|
|
|
return kind === DEVICE_LAYER_KIND
|
|
|
? instance.engine.hasDeviceMarker(instance.viewer, code, pointCode)
|
|
|
: instance.engine.hasPointMarker(instance.viewer, kind, code)
|
|
|
}
|
|
|
|
|
|
-export function getMapMarkerAnchor(kind, code, pointCode = '') {
|
|
|
+export function getMapMarkerAnchor(kind, code, pointCode = '', key = '') {
|
|
|
if (!instance || !instance.engine || !instance.viewer || !code) return null
|
|
|
+ if (kind === 'facility-point') {
|
|
|
+ const marker = facilityMarkerFor(code, key)
|
|
|
+ const at = marker && instance.projectMapPoint?.(marker.longitude, marker.latitude)
|
|
|
+ if (!at) return null
|
|
|
+ const plan = overlayFacilityPlanMarkerCached(marker)
|
|
|
+ return { ...at, iconSize: Math.max(plan.draw.width, plan.draw.height), panelGap: 12 }
|
|
|
+ }
|
|
|
const anchor = kind === DEVICE_LAYER_KIND
|
|
|
? instance.engine.projectDeviceMarker(instance.viewer, code, pointCode)
|
|
|
: instance.engine.projectPointMarker(instance.viewer, kind, code)
|
|
|
@@ -348,8 +400,11 @@ export function cancelPointLocate() {
|
|
|
* · `TERRAIN` / `CAMERA` —— 地形采样取不到可靠高程,或该视角构造不出「相机高于地表」,
|
|
|
* 由 `planTerrainLocateTarget` 纯函数守卫裁决(不再用椭球 0 或旧高度伪造成功)。
|
|
|
* 迟到采样/实例被换走时作废,不覆盖新目标(`isStaleLocate` 的竞态纪律)。
|
|
|
+ *
|
|
|
+ * 票 101:抽成 `runTerrainLocate(共享管线)`,准入判据由调用方给 ——
|
|
|
+ * 点位/设备只认后端 `renderable`(票 07),设施点按票 68 口径「有坐标即可上图」。
|
|
|
*/
|
|
|
-export async function locateMonitoringPoint(object, { tier = 'point', centerOnly = false, onFailure = null } = {}) {
|
|
|
+async function runTerrainLocate(object, { tier = 'point', centerOnly = false, onFailure = null, isCurrent = null } = {}, admitted) {
|
|
|
const token = ++pointLocateToken
|
|
|
const record = instance
|
|
|
const fail = (reason) => {
|
|
|
@@ -357,14 +412,15 @@ export async function locateMonitoringPoint(object, { tier = 'point', centerOnly
|
|
|
return false
|
|
|
}
|
|
|
if (!record || !record.cameraControl || !record.engine || !record.viewer) return fail(TERRAIN_LOCATE_REASON.MAP)
|
|
|
- if (!isRenderable(object) || !Number.isFinite(object.longitude) || !Number.isFinite(object.latitude)) {
|
|
|
+ if (!admitted(object)) {
|
|
|
return fail(TERRAIN_LOCATE_REASON.COORDINATE)
|
|
|
}
|
|
|
|
|
|
let height = await record.engine.sampleTerrainHeight(record.viewer, object.longitude, object.latitude)
|
|
|
- if (!Number.isFinite(height)) height = record.engine.groundHeightAt(record.viewer, object.longitude, object.latitude)
|
|
|
- // 被更新的相机指令顶掉 / 实例已销毁:作废这次定位,归「地图未就绪」口径(与旧行为同文案)。
|
|
|
+ // 在读取旧 Viewer 或提交相机副作用之前,确认原选择与实例仍然有效。
|
|
|
if (isStaleLocate(token, pointLocateToken) || instance !== record) return fail(TERRAIN_LOCATE_REASON.MAP)
|
|
|
+ if (isCurrent && !isCurrent()) return false
|
|
|
+ if (!Number.isFinite(height)) height = record.engine.groundHeightAt(record.viewer, object.longitude, object.latitude)
|
|
|
const view = planLocateView(getCockpitConfig().map, tier)
|
|
|
// Terrain is required for point/device locating. Height 0 is never a success fallback.
|
|
|
const targetPlan = planTerrainLocateTarget(object, height, view)
|
|
|
@@ -378,6 +434,26 @@ export async function locateMonitoringPoint(object, { tier = 'point', centerOnly
|
|
|
return applied
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 点位/设备定位:准入只认后端 `renderable`(票 07 的唯一判据,不由前端重算范围)。
|
|
|
+ */
|
|
|
+export async function locateMonitoringPoint(object, options = {}) {
|
|
|
+ return runTerrainLocate(object, options, (item) =>
|
|
|
+ isRenderable(item) && Number.isFinite(item.longitude) && Number.isFinite(item.latitude))
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 票 101:设施点的定位跳转 —— 与点位/设备**同一条**地形感知飞行管线(同一份
|
|
|
+ * 地形采样 / 视角计划 / 相机指令 / 竞态令牌),唯一差别是准入判据:
|
|
|
+ * 设施坐标来自设施档案(票 68 的既定口径「有坐标即可上图」,档案坐标不经
|
|
|
+ * 几何图层那套 `coordinateValidation`),所以这里只要求经纬度是有限数字。
|
|
|
+ * @param {object} row 设施点标记行(`facilityPointMarkers()` 的输出,带 longitude/latitude)
|
|
|
+ */
|
|
|
+export async function locateFacilityPoint(row, options = {}) {
|
|
|
+ return runTerrainLocate(row, options, (item) =>
|
|
|
+ Number.isFinite(item && item.longitude) && Number.isFinite(item.latitude))
|
|
|
+}
|
|
|
+
|
|
|
/** 只有真元素才做 DOM / 引擎操作;单测传普通对象时整条引擎链路都不启动。 */
|
|
|
function isDomHost(host) {
|
|
|
return !!host && typeof host === 'object' && host.nodeType === 1 && typeof host.appendChild === 'function'
|
|
|
@@ -919,6 +995,11 @@ async function bootEngine(record) {
|
|
|
|
|
|
// 票 03:相机契约落在服务层 —— 引擎就绪后才挂,默认输入已在 createViewer 里全关。
|
|
|
const cameraEngine = engine.createCameraEngine(viewer)
|
|
|
+ // 设施锚点和 DOM 标记同帧共用地形投影,不读上一帧节点位置。
|
|
|
+ record.projectMapPoint = (lon, lat) => {
|
|
|
+ const ground = engine.groundHeightAt(viewer, lon, lat)
|
|
|
+ return cameraEngine.projectPivot({ lon, lat, height: Number.isFinite(ground) ? ground : 0 }, { visibleOnly: true })
|
|
|
+ }
|
|
|
record.cameraControl = createCameraControl({
|
|
|
engine: cameraEngine,
|
|
|
onStateChange: (state) => {
|
|
|
@@ -1037,11 +1118,11 @@ async function bootEngine(record) {
|
|
|
// 也不再有「关掉 DOM 露出 billboard」的第二态(那种态下点选只能靠 GPU 命中盒)。
|
|
|
if (mountMarkerOverlay(record.host, {
|
|
|
// 票 76(C6):hover 身份由 overlay 按**节点身份**回传(DOM 是唯一命中盒)。
|
|
|
- onHover: ({ kind, code }) => setDomHoverTarget(kind, code),
|
|
|
+ onHover: hit => setDomHoverTarget(hit?.kind || '', hit?.code || ''),
|
|
|
// 票 76(C6):点选**唯一**入口 —— 节点身份 → `pickMarkerByIdentity` → `dispatchPickResult`。
|
|
|
// GPU 拾取兜底已退役:billboard 不再渲染,命中盒与视觉同一份几何(DOM 节点),
|
|
|
// 不存在「DOM 认不出、坐标能拾到」的第三种情况。
|
|
|
- onPick: ({ kind, code }) => pickMarkerByIdentity(kind, code),
|
|
|
+ onPick: ({ kind, code, key }) => pickMarkerByIdentity(kind, code, key),
|
|
|
})) {
|
|
|
record.detachOverlay = attachOverlayRenderer(instance.viewer, () => {
|
|
|
if (!markerPlan || !instance || !instance.viewer) return null
|
|
|
@@ -1068,6 +1149,10 @@ async function bootEngine(record) {
|
|
|
const rectKey = overlayFrameCache.rectKey || `${hostRect.left},${hostRect.top},${hostRect.width},${hostRect.height}`
|
|
|
const inputsChanged = overlayFrameCache.lastPlan !== markerPlan
|
|
|
|| overlayFrameCache.lastRows !== deviceRows
|
|
|
+ || overlayFrameCache.lastFacilityRows !== facilityMarkerRows
|
|
|
+ || overlayFrameCache.lastFacilitySelection !== selectedFacilityKey
|
|
|
+ || overlayFrameCache.lastComponentRows !== pipeComponentRows
|
|
|
+ || overlayFrameCache.lastViewMode !== viewMode
|
|
|
|| overlayFrameCache.lastEffects !== markerEffectsPlan
|
|
|
|| overlayFrameCache.lastDeviceEffects !== deviceEffectsPlan
|
|
|
|| overlayFrameCache.lastCombined !== combinedEffectState
|
|
|
@@ -1083,6 +1168,10 @@ async function bootEngine(record) {
|
|
|
overlayFrameCache.at = nowMs
|
|
|
overlayFrameCache.lastPlan = markerPlan
|
|
|
overlayFrameCache.lastRows = deviceRows
|
|
|
+ overlayFrameCache.lastFacilityRows = facilityMarkerRows
|
|
|
+ overlayFrameCache.lastFacilitySelection = selectedFacilityKey
|
|
|
+ overlayFrameCache.lastComponentRows = pipeComponentRows
|
|
|
+ overlayFrameCache.lastViewMode = viewMode
|
|
|
overlayFrameCache.lastEffects = markerEffectsPlan
|
|
|
overlayFrameCache.lastDeviceEffects = deviceEffectsPlan
|
|
|
overlayFrameCache.lastCombined = combinedEffectState
|
|
|
@@ -1110,6 +1199,14 @@ async function bootEngine(record) {
|
|
|
}
|
|
|
return {
|
|
|
markers: markerPlan.markers,
|
|
|
+ // 票 68:设施点计划行(DOM overlay 第四条循环消费;计划/引用一变就重建帧状态)。
|
|
|
+ facilities: facilityMarkerRows,
|
|
|
+ facilityPlanOf: overlayFacilityPlanMarkerCached,
|
|
|
+ selectedFacilityKey,
|
|
|
+ // 票 101:管线构件点位行 + 地面视图可见性(管线视图交给 3D 模型/Cesium 兜底点)。
|
|
|
+ pipeComponents: pipeComponentRows,
|
|
|
+ pipeComponentsVisible: viewMode === 'ground',
|
|
|
+ componentPlanOf: overlayComponentPlanMarkerCached,
|
|
|
// 票 76(C1):重叠聚合符号每帧投影进 DOM(票 47 遗留:聚合此前只活在 billboard)。
|
|
|
// 只有 group 身份/计数/组内状态词,没有设备身份 —— 聚合符号不冒充设备。
|
|
|
aggregates: instance.engine.projectOverlapSymbols(instance.viewer),
|
|
|
@@ -1144,8 +1241,7 @@ async function bootEngine(record) {
|
|
|
// 首版按椭球 0 米投影,地形区里 DOM 标记恒落在 billboard 标记的**地面以下**。
|
|
|
// 高度取值与 `projectMarkerEntity` 一致:地形瓦片实测高程,取不到回 0(椭球面)。
|
|
|
projectToWindow: (lon, lat) => {
|
|
|
- const ground = engine.groundHeightAt(instance.viewer, lon, lat)
|
|
|
- const at = cameraEngine.projectPivot({ lon, lat, height: Number.isFinite(ground) ? ground : 0 })
|
|
|
+ const at = record.projectMapPoint(lon, lat)
|
|
|
if (!at) return null
|
|
|
// projectPivot 回**窗口坐标**;overlay 宿主 inset:0 铺在 host 内,
|
|
|
// 节点坐标原点 = host 左上角 → 减 host 的窗口原点(每帧只读一次布局)。
|
|
|
@@ -1173,6 +1269,10 @@ async function bootEngine(record) {
|
|
|
}
|
|
|
|
|
|
setEngineState(record, 'ready')
|
|
|
+ // 管线在引擎启动前收到的计划必须重放;不能等待一次偶然的摘要变化。
|
|
|
+ if (pendingPipeLayerPlan) void syncPipeLayerForPlan(pendingPipeLayerPlan).catch(() => {
|
|
|
+ if (instance === record && record.host?.dataset) record.host.dataset.mapPipeSync = 'failed'
|
|
|
+ })
|
|
|
publishDevHook()
|
|
|
} catch (error) {
|
|
|
if (instance !== record) return
|
|
|
@@ -1242,12 +1342,22 @@ export function resizeMap() {
|
|
|
|
|
|
/** 只在离开 /cockpit 时销毁。 */
|
|
|
export function unmountMap() {
|
|
|
+ // 数据可能早于延迟挂载到达:没有 Viewer 也必须清掉旧路由的计划和选择。
|
|
|
+ cancelPointLocate()
|
|
|
+ pendingPipeLayerPlan = null
|
|
|
+ lastPipeLayerRefs = { pipes: null, components: null, viewMode: null, viewer: null, overlayActive: null }
|
|
|
+ facilityMarkerRows = []
|
|
|
+ selectedFacilityKey = ''
|
|
|
+ facilitySelectionIdentity = null
|
|
|
+ pipeComponentRows = []
|
|
|
+ pipeSyncDeferred = false
|
|
|
+ pipeSyncDeferredPlan = null
|
|
|
if (!instance) {
|
|
|
+ unmountMarkerOverlay()
|
|
|
return
|
|
|
}
|
|
|
|
|
|
const record = instance
|
|
|
- cancelPointLocate()
|
|
|
instance = null
|
|
|
destroyedCount += 1
|
|
|
|
|
|
@@ -2800,7 +2910,8 @@ export function invalidateToneColors() {
|
|
|
// 合并语义:同一类落地任务只保留最后一次(连续翻页/多次摘要到达不会叠加执行)。
|
|
|
|
|
|
/** 管线图层最近一次落地的计划引用(同引用短路用)与延迟重试状态。 */
|
|
|
-let lastPipeLayerRefs = { pipes: null, components: null, viewMode: null }
|
|
|
+let lastPipeLayerRefs = { pipes: null, components: null, viewMode: null, viewer: null, overlayActive: null }
|
|
|
+let pendingPipeLayerPlan = null
|
|
|
let pipeSyncDeferred = false
|
|
|
let pipeSyncDeferredAt = 0
|
|
|
let pipeSyncDeferredPlan = null
|
|
|
@@ -3011,28 +3122,45 @@ export function syncPointMarkersForPlan(plan, legendSource) {
|
|
|
* @returns {Promise<boolean>} 是否已画到地图上
|
|
|
*/
|
|
|
export async function syncPipeLayerForPlan(plan) {
|
|
|
+ pendingPipeLayerPlan = plan
|
|
|
const layer = {
|
|
|
pipes: plan && Array.isArray(plan.pipelines) ? plan.pipelines : [],
|
|
|
components: plan && Array.isArray(plan.facilities) ? plan.facilities : [],
|
|
|
}
|
|
|
|
|
|
+ // 半启动 Viewer 也能先画兜底;DOM 挂载后的同数组计划必须重新发布接管状态。
|
|
|
+ const overlayActive = billboardVisualHidden()
|
|
|
+
|
|
|
+ // 移动期只保留一个最新计划。B 到达后不能继续排队 A,再由旧定时器回画 A。
|
|
|
+ if (pipeSyncDeferred) {
|
|
|
+ pipeComponentRows = pipeComponentMarkers(layer.components)
|
|
|
+ pipeSyncDeferredPlan = plan
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
// 票 35 第六轮(基础设施模式反而更卡):
|
|
|
// 1) **同引用短路**:计划层对同一批数据是原样引用的(见 modeLayerPlan 注释),
|
|
|
// 30s freshness tick 会让 watcher 再触发一次;引用没变、视图没变就不必重新指纹比较
|
|
|
// 几百条管段(每条一次 JSON.stringify)。基础设施模式管子最多,这一步最值钱。
|
|
|
// 2) **落地让路**:镜头在动时把这次同步排到落定后(最多 700ms),别和移动帧抢主线程。
|
|
|
if (layer.pipes === lastPipeLayerRefs.pipes && layer.components === lastPipeLayerRefs.components
|
|
|
- && lastPipeLayerRefs.viewMode === viewMode) {
|
|
|
+ && lastPipeLayerRefs.viewMode === viewMode && instance?.viewer
|
|
|
+ && lastPipeLayerRefs.viewer === instance.viewer
|
|
|
+ && lastPipeLayerRefs.overlayActive === overlayActive) {
|
|
|
return true
|
|
|
}
|
|
|
- lastPipeLayerRefs = { pipes: layer.pipes, components: layer.components, viewMode }
|
|
|
+ // 票 101:构件在地面视图升级为「点位标记」(DOM overlay)。行由领域层给出
|
|
|
+ // (符号/标签/行业色),这里只随图层计划同步;管线视图的 3D 模型不受影响。
|
|
|
+ pipeComponentRows = pipeComponentMarkers(layer.components)
|
|
|
if (cameraInMotion() && !pipeSyncDeferred) {
|
|
|
// 排队到镜头落定:保留最新一次计划,重试期间不被短路挡住。
|
|
|
+ const owner = instance
|
|
|
pipeSyncDeferred = true
|
|
|
pipeSyncDeferredAt = engineNow()
|
|
|
pipeSyncDeferredPlan = plan
|
|
|
lastPipeLayerRefs = { pipes: null, components: null, viewMode: null }
|
|
|
const retry = () => {
|
|
|
+ if (instance !== owner) return
|
|
|
if (!instance || !instance.viewer) { pipeSyncDeferred = false; return }
|
|
|
if (cameraInMotion() && engineNow() - pipeSyncDeferredAt < SCENE_APPLY_MAX_DEFER_MS) {
|
|
|
setTimeout(retry, 120)
|
|
|
@@ -3041,19 +3169,22 @@ export async function syncPipeLayerForPlan(plan) {
|
|
|
pipeSyncDeferred = false
|
|
|
const queued = pipeSyncDeferredPlan
|
|
|
pipeSyncDeferredPlan = null
|
|
|
- if (queued) void syncPipeLayerForPlan(queued)
|
|
|
+ if (queued && queued === pendingPipeLayerPlan) void syncPipeLayerForPlan(queued)
|
|
|
}
|
|
|
setTimeout(retry, 120)
|
|
|
- return true
|
|
|
+ return false
|
|
|
}
|
|
|
if (!instance || !instance.viewer) return false
|
|
|
|
|
|
const record = instance
|
|
|
+ const appliedViewMode = viewMode
|
|
|
const engine = await import('./cesiumPipeLayer.js')
|
|
|
if (instance !== record || !record.viewer) return false
|
|
|
record.pipeLayer = engine
|
|
|
engine.setPipeViewMode(record.viewer, viewMode)
|
|
|
- await engine.syncPipeLayer(record.viewer, layer, { onModelChange: (state) => {
|
|
|
+ const result = await engine.syncPipeLayer(record.viewer, layer, {
|
|
|
+ componentOverlayActive: billboardVisualHidden,
|
|
|
+ onModelChange: (state) => {
|
|
|
if (instance !== record || !record.host) return
|
|
|
const host = record.host
|
|
|
if (host.dataset) {
|
|
|
@@ -3067,7 +3198,8 @@ export async function syncPipeLayerForPlan(plan) {
|
|
|
}
|
|
|
host.dispatchEvent(new CustomEvent('cockpit-pipe-model-state', { detail: state }))
|
|
|
} })
|
|
|
- if (instance !== record) return false
|
|
|
+ if (instance !== record || pendingPipeLayerPlan !== plan || result.rendered !== true) return false
|
|
|
+ lastPipeLayerRefs = { pipes: layer.pipes, components: layer.components, viewMode: appliedViewMode, viewer: record.viewer, overlayActive }
|
|
|
const host = record.host
|
|
|
if (host && host.dataset) {
|
|
|
host.dataset.mapPipes = String(layer.pipes.length)
|
|
|
@@ -3076,6 +3208,54 @@ export async function syncPipeLayerForPlan(plan) {
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 同步**设施点图层**(票 68)。
|
|
|
+ *
|
|
|
+ * 与摘要标记/关联设备分开是结构性的:设施点不是监测点、也不是设备,不进摘要序列、
|
|
|
+ * 不进设备图例与动效/重叠判定。渲染由 DOM overlay 的设施点循环承担;本函数只持有本轮
|
|
|
+ * 计划行并写宿主读数(计数口径与图例的「可上图 N」同源,都是本轮可画的行数)。
|
|
|
+ *
|
|
|
+ * `rows` 已由 store 按设施类型 + 全局行业/片区筛好,计划层只按一级模式显隐,这里不再过滤。
|
|
|
+ *
|
|
|
+ * @param {Array} rows 设施点标记行(`domain/facilityPoints.js` 的 `facilityPointMarkers()` 输出)
|
|
|
+ * @returns {number} 本轮交给渲染层的设施点数
|
|
|
+ */
|
|
|
+export function syncFacilityMarkersForPlan(rows) {
|
|
|
+ facilityMarkerRows = Array.isArray(rows) ? rows : []
|
|
|
+ // watcher 可能早于合并后的 marker 同步;以新行重算可见选择,避免保留已筛掉的高亮。
|
|
|
+ setFacilitySelection(facilitySelectionIdentity)
|
|
|
+ const host = instance && isDomHost(instance.host) ? instance.host : null
|
|
|
+ if (host && host.dataset) {
|
|
|
+ host.dataset.mapFacilityPoints = String(facilityMarkerRows.length)
|
|
|
+ const byType = {}
|
|
|
+ for (const row of facilityMarkerRows) {
|
|
|
+ if (!row || !row.type) continue
|
|
|
+ byType[row.type] = (byType[row.type] || 0) + 1
|
|
|
+ }
|
|
|
+ host.dataset.mapFacilityTypes = Object.entries(byType).map(([type, count]) => `${type}:${count}`).join(',')
|
|
|
+ }
|
|
|
+ return facilityMarkerRows.length
|
|
|
+}
|
|
|
+
|
|
|
+/** 设施选中视觉只镜像实体详情 store 的身份;筛掉或关详情就清空,不自建第二套业务选择。 */
|
|
|
+export function setFacilitySelection(identity) {
|
|
|
+ facilitySelectionIdentity = identity || null
|
|
|
+ const row = identity?.kind === 'facility' ? facilityMarkerRows.find(item =>
|
|
|
+ item.entityId === identity.id && item.detailCategory === identity.category) : null
|
|
|
+ selectedFacilityKey = row?.key || ''
|
|
|
+ if (instance?.host?.dataset) instance.host.dataset.mapFacilityPicked = selectedFacilityKey
|
|
|
+}
|
|
|
+
|
|
|
+function facilityMarkerFor(code, key = '') {
|
|
|
+ const matches = facilityMarkerRows.filter(item => item.code === code && (!key || item.key === key))
|
|
|
+ return matches.length === 1 ? matches[0] : null
|
|
|
+}
|
|
|
+
|
|
|
+/** 本轮设施点计划行(走查/单测读;与宿主 `data-map-facility-*` 同源)。 */
|
|
|
+export function getFacilityMarkerRows() {
|
|
|
+ return facilityMarkerRows
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 同步**关联监测设备临时图层**(票 42)。
|
|
|
*
|
|
|
@@ -3714,7 +3894,7 @@ export function pickDeviceOrPointAt(clientX, clientY) {
|
|
|
// 保留本入口是因为 DEV 走查仍按「屏幕坐标」驱动同一条用户链路;实现里不再有 Cesium 拾取。
|
|
|
const target = domMarkerAt(clientX, clientY)
|
|
|
if (!target || !target.kind) return dispatchPickResult({ target: 'none', pick: null })
|
|
|
- return pickMarkerByIdentity(target.kind, target.code)
|
|
|
+ return pickMarkerByIdentity(target.kind, target.code, target.key)
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -3776,8 +3956,46 @@ function dispatchPickResult(resolved) {
|
|
|
* @param {string} code 节点上的 `data-marker-overlay-code`
|
|
|
* @returns {{target:string, pick:object|null}}
|
|
|
*/
|
|
|
-export function pickMarkerByIdentity(kind, code) {
|
|
|
+export function pickMarkerByIdentity(kind, code, key = '') {
|
|
|
if (kind === 'overlap-aggregate') return toggleOverlap(code)
|
|
|
+ // 票 101:设施点点击 —— 与独立设备**同一条派发链**(target 'point' → paintPicked +
|
|
|
+ // pointPickHandlers),由订阅方(MapCanvas)打开设施详情并做镜头定位;
|
|
|
+ // 详情与「设施台账」卡片点击同源(entityDetailStore + entity-detail 契约)。
|
|
|
+ // 缺主键/类别的行不可点(领域层 `clickable` 已判),这里再兜一层不把脏身份发出去。
|
|
|
+ if (kind === 'facility-point') {
|
|
|
+ // 节点 key 包含类别+主键;旧的 code-only 入口仅接受唯一匹配,不能猜第一条。
|
|
|
+ const row = facilityMarkerFor(code, key)
|
|
|
+ if (!row || !row.clickable) return { target: 'none', pick: null }
|
|
|
+ return dispatchPickResult({ target: 'point', pick: {
|
|
|
+ kind: 'facility-point',
|
|
|
+ key: row.key,
|
|
|
+ code: row.code,
|
|
|
+ name: row.name,
|
|
|
+ status: row.status,
|
|
|
+ id: row.entityId,
|
|
|
+ category: row.detailCategory,
|
|
|
+ industry: row.industry,
|
|
|
+ facilityType: row.type,
|
|
|
+ longitude: row.longitude,
|
|
|
+ latitude: row.latitude,
|
|
|
+ } })
|
|
|
+ }
|
|
|
+ // 票 101:管线构件点位点击 —— 与点构件实体/3D 模型同一条**管线拾取链**(局部增强),
|
|
|
+ // 不进设施详情。pick 形状与引擎 `pickPipeObjectAt` 的构件命中一致(objectKind:'component')。
|
|
|
+ if (kind === 'pipe-component') {
|
|
|
+ const row = (pipeComponentRows || []).find((item) => item.code === code) || null
|
|
|
+ if (!row) return { target: 'none', pick: null }
|
|
|
+ const pick = {
|
|
|
+ kind: 'pipeline',
|
|
|
+ code: row.code,
|
|
|
+ objectKind: 'component',
|
|
|
+ name: row.name,
|
|
|
+ componentKind: row.componentKind,
|
|
|
+ industry: row.industry,
|
|
|
+ }
|
|
|
+ for (const handler of [...pipelinePickHandlers]) handler(pick)
|
|
|
+ return { target: 'pipeline', pick }
|
|
|
+ }
|
|
|
if (!code) return { target: 'none', pick: null }
|
|
|
if (kind === 'associated-device') {
|
|
|
// 票 76(四态规格 2026-10-26 修)点击设备点:**始终走同一条派发链**。
|
|
|
@@ -3913,8 +4131,8 @@ function domMarkerAt(clientX, clientY) {
|
|
|
if (!doc || typeof doc.elementFromPoint !== 'function') return null
|
|
|
const element = doc.elementFromPoint(clientX, clientY)
|
|
|
const node = element && element.closest ? element.closest('.marker-overlay-node') : null
|
|
|
- if (!node || !node.dataset) return null
|
|
|
- return { kind: node.dataset.markerOverlayKind || '', code: node.dataset.markerOverlayCode || '' }
|
|
|
+ if (!node || !node.dataset || !instance.host.contains(node)) return null
|
|
|
+ return { kind: node.dataset.markerOverlayKind || '', code: node.dataset.markerOverlayCode || '', key: node.dataset.markerOverlayKey || '' }
|
|
|
}
|
|
|
|
|
|
/** DOM hover 身份 → 与点选同形状的设备命中行(只认关联设备;点位有 DOM tooltip)。 */
|