|
@@ -1,58 +1,65 @@
|
|
|
/**
|
|
/**
|
|
|
- * 地图标记 DOM overlay(票 20)——billboard 发糊与底板生硬的结构性解决。
|
|
|
|
|
|
|
+ * 地图标记 DOM overlay(票 47)——billboard 发糊与底板生硬的结构性解决。
|
|
|
*
|
|
*
|
|
|
- * 把 `planPointMarkers` 产出的标记从 GPU billboard 迁到 **UI 同层的 DOM 渲染**:
|
|
|
|
|
- * · 标记图形 = 内联 SVG(状态色母版 + 内层图标 + 白描边),矢量渲染**永不发糊**;
|
|
|
|
|
- * · 标签 = 真 DOM(圆角底板、装饰边、text-overflow、hover title 全部原生能力);
|
|
|
|
|
- * · 每帧由 `postRender` 把经纬度经 `wgs84ToWindowCoordinates` 投影成窗口坐标,
|
|
|
|
|
- * 只写 `transform`(合成层更新,不触发布局)。
|
|
|
|
|
|
|
+ * 渲染分工(维护者 2026-10-15 裁定):
|
|
|
|
|
+ * · **外层母版 = 自绘 SVG**(矢量,任何 DPR 都锐利;billboard 发糊的根治)。
|
|
|
|
|
+ * 水滴形按实测母版几何拟合:帽圆 (188.5,189) r189(逐行验证误差 <1px)+
|
|
|
|
|
+ * 下半收尖三次贝塞尔(对 512px 母版左缘 57 点最小二乘拟合,最大残差 4.75 源像素
|
|
|
|
|
+ * ≈ 0.34 屏显像素;拟合探针见 .scratch/cockpit-real-data-wayfinding/research/fit-pin-bezier.mjs)。
|
|
|
|
|
+ * 镂空孔 = 实测圆 (188.5,189) r75。viewBox 外扩 40 与 billboard 裁剪窗(OUTLINE_PAD)同形。
|
|
|
|
|
+ * · **内层行业图标 = 素材**(`markerAssets.renderInnerIconUrl`:与 billboard 同一
|
|
|
|
|
+ * `recolorInner` 去色重染规则,但**不下采样**——256px 原生分辨率 data URL,
|
|
|
|
|
+ * DOM 里按帽径向下采样,永远锐利)。
|
|
|
|
|
+ * · 标签 = 真 DOM(圆角底板、装饰边、text-overflow、hover title 原生能力)。
|
|
|
|
|
+ * · 每帧 `postRender` 投影:与 billboard `CLAMP_TO_GROUND` 同口径(地形实测高程,取不到回椭球 0)。
|
|
|
*
|
|
*
|
|
|
- * 与既有链路的关系:
|
|
|
|
|
- * · 合成计划层 `domain/markerPlan.js` **原样复用**(归属/几何/锚点/降饱和逻辑不换媒介);
|
|
|
|
|
- * · `syncPointMarkersForPlan`(billboard 链路)**保留不动**——overlay 初始化失败自动回退;
|
|
|
|
|
- * · 拾取:DOM 节点自身可点(直接派发给既有 onPointPick 订阅者),Canvas 拾取继续服务 3D 对象;
|
|
|
|
|
- * · `data-*` 断言面全部挂到 DOM 节点(`data-marker-overlay-key` 等),走查不读内部实现。
|
|
|
|
|
|
|
+ * 与既有链路的关系(维护者裁定:DOM 完整复刻 billboard 功能后才替换它):
|
|
|
|
|
+ * · 合成计划层 `domain/markerPlan.js` **原样复用**(归属/几何/锚点/降饱和不换媒介);
|
|
|
|
|
+ * · `syncPointMarkersForPlan`(billboard 链路)**保留不动**——聚合、特效、Canvas 拾取仍依赖它;
|
|
|
|
|
+ * 内层素材加载失败时 SVG 只有母版 = 底下 billboard 双层照常显示(降级可见);
|
|
|
|
|
+ * · 拾取:overlay 节点挂**显式 click 触发器**,回调直接复用 `pickDeviceOrPointAt`
|
|
|
|
|
+ * (与 Canvas 点选链同一个功能函数,维护者原话「照搬原本的功能函数」);
|
|
|
|
|
+ * · `data-*` 断言面全部挂到 DOM 节点,走查不读内部实现。
|
|
|
*
|
|
*
|
|
|
- * 性能纪律:节点池复用(不每帧重建);postRender 回调内只做 transform 与 visibility 更新。
|
|
|
|
|
|
|
+ * 性能纪律:节点池复用(不每帧重建);计划读数指纹变化才重建;postRender 只做 transform 更新。
|
|
|
*/
|
|
*/
|
|
|
|
|
+import { renderInnerIconUrl } from './markerAssets.js'
|
|
|
|
|
+
|
|
|
const OVERLAY_HOST_CLASS = 'marker-overlay-host'
|
|
const OVERLAY_HOST_CLASS = 'marker-overlay-host'
|
|
|
const NODE_CLASS = 'marker-overlay-node'
|
|
const NODE_CLASS = 'marker-overlay-node'
|
|
|
const LABEL_CLASS = 'marker-overlay-label'
|
|
const LABEL_CLASS = 'marker-overlay-label'
|
|
|
|
|
|
|
|
-/** 节点池:key → { root, label, lastX, lastY };标记消失时节点隐藏不销毁。 */
|
|
|
|
|
-const pool = new Map()
|
|
|
|
|
-let hostNode = null
|
|
|
|
|
-let hostRecord = null
|
|
|
|
|
-let postRenderRemove = null
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 水滴形 SVG(矢量母版,替代位图):状态色由调用方注入 `--tone` 变量。
|
|
|
|
|
- * viewBox 0 0 100 126(宽高比 ≈ 1.257,与白母版实测一致);尖端 (50,126)。
|
|
|
|
|
|
|
+ * 自绘水滴母版(bbox 坐标系:x∈[0,378]、y∈[0,475],尖端 (188.5,474))。
|
|
|
|
|
+ * viewBox 外扩 40(同 billboard OUTLINE_PAD 裁剪窗)→ "-40 -40 458 555"。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 描边(浏览器走查修正):billboard 的 `withWhiteOutline` 是**向外膨胀** 28 源像素、
|
|
|
|
|
+ * 母版本身保持 378 全宽;SVG `stroke` 默认沿路径**中心线**(外 14 / 内 14)会把彩色母版
|
|
|
|
|
+ * 蚀掉一圈、白环也只出一半——同尺寸下 DOM 标记明显比 billboard 小。
|
|
|
|
|
+ * 用 `paint-order="stroke fill"` 把描边压到填充**之下**:彩色母版全尺寸保留,
|
|
|
|
|
+ * 白色纯外扩 `stroke-width/2` = 28 → 与 billboard 的 OUTLINE_WIDTH 同值(外沿 28 < pad 40 不被裁)。
|
|
|
*/
|
|
*/
|
|
|
-function markerSvg(master) {
|
|
|
|
|
- const fill = master === 'hollow'
|
|
|
|
|
- ? '<path d="M50 4C25 4 6 24 6 50c0 37 44 66 44 76s-44-39-44-76C6 24 25 4 50 4Zm0 26a20 20 0 1 1 0 40 20 20 0 0 1 0-40Z" fill="var(--tone)" fill-rule="evenodd"/>'
|
|
|
|
|
- : '<path d="M50 4C25 4 6 24 6 50c0 37 44 66 44 76s-44-39-44-76C6 24 25 4 50 4Z" fill="var(--tone)"/>'
|
|
|
|
|
- return `<svg viewBox="0 0 100 126" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">${fill}</svg>`
|
|
|
|
|
-}
|
|
|
|
|
|
|
+const PIN_OUTLINE = 'M188.5 0A189 189 0 0 0 111.6 361.7C157.3 382 173.6 435.5 188.5 474C203.4 435.5 219.7 382 265.4 361.7A189 189 0 0 0 188.5 0Z'
|
|
|
|
|
+const PIN_HOLE = 'M263.5 189a75 75 0 1 0-150 0 75 75 0 1 0 150 0Z'
|
|
|
|
|
+const PIN_STROKE = 'stroke="#ffffff" stroke-width="56" stroke-linejoin="round" paint-order="stroke fill"'
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 内层行业图标:manifest 登记的彩色/重染切图以 <image> 嵌入 SVG(矢量母版内嵌位图,
|
|
|
|
|
- * 位图按 60% 帽径绘制在帽心,与 billboard 合成一致——但 SVG 缩放矢量无损,无发糊)。
|
|
|
|
|
- */
|
|
|
|
|
-function innerSvg(icon, capCenterX, capCenterY, diameter, innerUrls) {
|
|
|
|
|
- if (!icon || !innerUrls) return ''
|
|
|
|
|
- const asset = innerUrls[icon]
|
|
|
|
|
- if (!asset) return ''
|
|
|
|
|
- const half = diameter / 2
|
|
|
|
|
- // 染色规则(共识 §4.2):内层一律去色重染(亮部白、暗部浅蓝)——SVG <image> 不能逐像素
|
|
|
|
|
- // 重染,用 CSS filter 近似:去色 → 提亮反转(invert 让深色剪影变亮)→ 上浅蓝色调。
|
|
|
|
|
- return `<image href="${asset}" x="${capCenterX - half}" y="${capCenterY - half}" width="${diameter}" height="${diameter}"` +
|
|
|
|
|
- ` filter="brightness(0.35) invert(1) sepia(1) saturate(3) hue-rotate(160deg)" opacity="0.95"/>`
|
|
|
|
|
|
|
+function markerSvg(master, toneHex) {
|
|
|
|
|
+ const body = master === 'hollow'
|
|
|
|
|
+ ? `<path d="${PIN_OUTLINE}${PIN_HOLE}" fill="${toneHex}" fill-rule="evenodd" ${PIN_STROKE}/>`
|
|
|
|
|
+ : `<path d="${PIN_OUTLINE}" fill="${toneHex}" ${PIN_STROKE}/>`
|
|
|
|
|
+ return `<svg viewBox="-40 -40 458 555" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">${body}<image data-marker-overlay-inner="" x="0" y="0" width="0" height="0"/></svg>`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 节点池:key → { root, inner, fp };标记消失时节点隐藏不销毁,计划读数变化时重建。 */
|
|
|
|
|
+const pool = new Map()
|
|
|
|
|
+let hostNode = null
|
|
|
|
|
+let hostRecord = null
|
|
|
|
|
+let postRenderRemove = null
|
|
|
|
|
+/** 临时 A/B 开关(票 47 走查):false = 隐藏 DOM 层、露出底下 billboard(之前版本)。 */
|
|
|
|
|
+let overlayVisible = true
|
|
|
|
|
+
|
|
|
/** 单个标记节点(标记 SVG + 可选标签),一次构建、每帧只更新 transform。 */
|
|
/** 单个标记节点(标记 SVG + 可选标签),一次构建、每帧只更新 transform。 */
|
|
|
-function buildNode(key, marker, plan, toneHex, innerUrls) {
|
|
|
|
|
|
|
+function buildNode(key, marker, plan, toneHex) {
|
|
|
const root = document.createElement('div')
|
|
const root = document.createElement('div')
|
|
|
root.className = NODE_CLASS
|
|
root.className = NODE_CLASS
|
|
|
root.dataset.markerOverlayKey = key
|
|
root.dataset.markerOverlayKey = key
|
|
@@ -63,13 +70,11 @@ function buildNode(key, marker, plan, toneHex, innerUrls) {
|
|
|
root.dataset.markerInner = plan.inner.enabled ? plan.inner.icon : ''
|
|
root.dataset.markerInner = plan.inner.enabled ? plan.inner.icon : ''
|
|
|
root.dataset.markerAnchor = `${plan.anchor.x},${plan.anchor.y}`
|
|
root.dataset.markerAnchor = `${plan.anchor.x},${plan.anchor.y}`
|
|
|
root.dataset.markerDesaturate = plan.desaturate ? 'yes' : 'no'
|
|
root.dataset.markerDesaturate = plan.desaturate ? 'yes' : 'no'
|
|
|
|
|
+ root.dataset.markerTone = toneHex
|
|
|
|
|
|
|
|
const w = plan.size.width
|
|
const w = plan.size.width
|
|
|
const h = plan.size.height
|
|
const h = plan.size.height
|
|
|
- // SVG viewBox 100x126 → 屏显 w×h;帽心/内层按 MASTER_GEOMETRY 比例换算到 viewBox 坐标
|
|
|
|
|
- const capX = 50
|
|
|
|
|
- const capY = 47.6 // 0.397 × 120(bbox 高在 viewBox 中 ≈ 120)
|
|
|
|
|
- const diameter = 60 // 60% × 100(viewBox 宽)
|
|
|
|
|
|
|
+ const pad = plan.draw.pad
|
|
|
|
|
|
|
|
root.style.cssText = [
|
|
root.style.cssText = [
|
|
|
'position:absolute',
|
|
'position:absolute',
|
|
@@ -80,21 +85,41 @@ function buildNode(key, marker, plan, toneHex, innerUrls) {
|
|
|
'will-change:transform',
|
|
'will-change:transform',
|
|
|
].join(';')
|
|
].join(';')
|
|
|
|
|
|
|
|
- const markerLayer = document.createElement('div')
|
|
|
|
|
- markerLayer.style.cssText = [
|
|
|
|
|
- `position:absolute`,
|
|
|
|
|
- `width:${w}px`,
|
|
|
|
|
- `height:${h}px`,
|
|
|
|
|
- // BOTTOM 锚定:节点 transform 原点是标记尖端,图形向上画
|
|
|
|
|
- `left:${-w / 2}px`,
|
|
|
|
|
- `top:${-h}px`,
|
|
|
|
|
- `--tone:${toneHex}`,
|
|
|
|
|
|
|
+ // 标记本体:自绘 SVG(外层母版)+ 异步嵌入的内层素材 <image>(原生分辨率,向下采样)。
|
|
|
|
|
+ // 摆放:尖端(锚点)落在 root 原点——合成窗含 pad,尖端距输出底边/左缘的换算与
|
|
|
|
|
+ // billboard `verticalOrigin:BOTTOM + pixelOffset(0,pad)` 同一补偿。
|
|
|
|
|
+ const wrap = document.createElement('div')
|
|
|
|
|
+ wrap.innerHTML = markerSvg(plan.master, toneHex)
|
|
|
|
|
+ const svg = wrap.firstChild
|
|
|
|
|
+ svg.style.cssText = [
|
|
|
|
|
+ 'position:absolute',
|
|
|
|
|
+ `left:${-(pad + w * plan.anchor.x)}px`,
|
|
|
|
|
+ `top:${-(h + pad)}px`,
|
|
|
|
|
+ `width:${plan.draw.width}px`,
|
|
|
|
|
+ `height:${plan.draw.height}px`,
|
|
|
plan.desaturate ? 'filter:saturate(0.55) brightness(0.72)' : '',
|
|
plan.desaturate ? 'filter:saturate(0.55) brightness(0.72)' : '',
|
|
|
].join(';')
|
|
].join(';')
|
|
|
- markerLayer.innerHTML = markerSvg(plan.master) + innerSvg(plan.inner.icon, capX, capY, diameter * (100 / w), innerUrls)
|
|
|
|
|
- root.appendChild(markerLayer)
|
|
|
|
|
|
|
+ // 反应量缩放的**原点 = 尖端**(尖端固定在地面坐标上,标记围绕尖端放大/缩小,
|
|
|
|
|
+ // 与 billboard verticalOrigin:BOTTOM + scale 同一行为)。
|
|
|
|
|
+ svg.style.transformOrigin = `${pad + w * plan.anchor.x}px ${h + pad}px`
|
|
|
|
|
+ root.appendChild(svg)
|
|
|
|
|
+
|
|
|
|
|
+ const innerEl = svg.querySelector('[data-marker-overlay-inner]')
|
|
|
|
|
+ if (plan.inner.enabled) {
|
|
|
|
|
+ // 直径/圆心 = 计划层几何(80% × 母版宽、帽心对齐)。viewBox 起点 -40 只是外扩描边
|
|
|
|
|
+ // 余量,bbox 坐标即 SVG 用户坐标——圆心直接用母版帽心 (188.5, 0.397×475)。
|
|
|
|
|
+ const diameter = plan.inner.diameterRatio * 378
|
|
|
|
|
+ const cx = 378 * plan.anchor.x // 帽心 x = 尖端 x = 188.5
|
|
|
|
|
+ const cy = plan.inner.center.y * 475
|
|
|
|
|
+ innerEl.setAttribute('x', String(cx - diameter / 2))
|
|
|
|
|
+ innerEl.setAttribute('y', String(cy - diameter / 2))
|
|
|
|
|
+ innerEl.setAttribute('width', String(diameter))
|
|
|
|
|
+ innerEl.setAttribute('height', String(diameter))
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 标签:真 DOM(圆角底板 + 装饰边 + text-overflow + hover title 原生能力)
|
|
|
|
|
|
|
+ // 标签:真 DOM。pointer-events:none —— 不拦截事件,相机拖拽与 Canvas 拾取照常穿过。
|
|
|
|
|
+ // 限宽取计划层 `labelMaxWidth`(宽 × 2.6,billboard 同一单源)——浏览器走查修正:
|
|
|
|
|
+ // 首版硬编码 220px,比 billboard 截断宽 3 倍,两层标签并现时一长一短明显不一致。
|
|
|
if (marker.name) {
|
|
if (marker.name) {
|
|
|
const label = document.createElement('div')
|
|
const label = document.createElement('div')
|
|
|
label.className = LABEL_CLASS
|
|
label.className = LABEL_CLASS
|
|
@@ -102,11 +127,12 @@ function buildNode(key, marker, plan, toneHex, innerUrls) {
|
|
|
label.title = marker.name // hover 读全(与 DOM 侧 EllipsisText 同语义)
|
|
label.title = marker.name // hover 读全(与 DOM 侧 EllipsisText 同语义)
|
|
|
label.style.cssText = [
|
|
label.style.cssText = [
|
|
|
'position:absolute',
|
|
'position:absolute',
|
|
|
- // 标签底边在标记上方:-(标记高 + 2px 间距)
|
|
|
|
|
- `bottom:${h + 2}px`,
|
|
|
|
|
|
|
+ `bottom:${h + pad + 3}px`,
|
|
|
'left:0',
|
|
'left:0',
|
|
|
'transform:translateX(-50%)',
|
|
'transform:translateX(-50%)',
|
|
|
- 'max-width:220px',
|
|
|
|
|
|
|
+ // content-box:max-width 只管文字,与 Cesium label 的截断宽度同语义
|
|
|
|
|
+ 'box-sizing:content-box',
|
|
|
|
|
+ `max-width:${plan.labelMaxWidth}px`,
|
|
|
'overflow:hidden',
|
|
'overflow:hidden',
|
|
|
'white-space:nowrap',
|
|
'white-space:nowrap',
|
|
|
'text-overflow:ellipsis',
|
|
'text-overflow:ellipsis',
|
|
@@ -116,19 +142,21 @@ function buildNode(key, marker, plan, toneHex, innerUrls) {
|
|
|
'background:rgba(2,20,40,0.82)',
|
|
'background:rgba(2,20,40,0.82)',
|
|
|
'color:#d7ecff',
|
|
'color:#d7ecff',
|
|
|
'font:600 12px "Microsoft YaHei UI","Microsoft YaHei","PingFang SC","Noto Sans SC",sans-serif',
|
|
'font:600 12px "Microsoft YaHei UI","Microsoft YaHei","PingFang SC","Noto Sans SC",sans-serif',
|
|
|
- 'pointer-events:auto',
|
|
|
|
|
|
|
+ 'pointer-events:none',
|
|
|
].join(';')
|
|
].join(';')
|
|
|
root.appendChild(label)
|
|
root.appendChild(label)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return root
|
|
|
|
|
|
|
+ return { root, inner: innerEl, svg }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 初始化 overlay 宿主(挂在地图容器内,z-index 在 canvas 之上、面板之下)。
|
|
* 初始化 overlay 宿主(挂在地图容器内,z-index 在 canvas 之上、面板之下)。
|
|
|
|
|
+ * @param {Element} mapHost 地图宿主(.map-canvas)
|
|
|
|
|
+ * @param {object} [options] `{ onPick }` 显式点击触发器回调(窗口坐标 + 节点身份)
|
|
|
* @returns {boolean} 是否初始化成功(失败时调用方回退 billboard 链路)
|
|
* @returns {boolean} 是否初始化成功(失败时调用方回退 billboard 链路)
|
|
|
*/
|
|
*/
|
|
|
-export function mountMarkerOverlay(mapHost) {
|
|
|
|
|
|
|
+export function mountMarkerOverlay(mapHost, { onPick } = {}) {
|
|
|
try {
|
|
try {
|
|
|
if (!mapHost || typeof mapHost.appendChild !== 'function') return false
|
|
if (!mapHost || typeof mapHost.appendChild !== 'function') return false
|
|
|
if (hostNode && hostNode.isConnected) return true
|
|
if (hostNode && hostNode.isConnected) return true
|
|
@@ -141,6 +169,30 @@ export function mountMarkerOverlay(mapHost) {
|
|
|
'overflow:hidden',
|
|
'overflow:hidden',
|
|
|
'z-index:1',
|
|
'z-index:1',
|
|
|
].join(';')
|
|
].join(';')
|
|
|
|
|
+ // 挂载时尊重当前 A/B 开关(`?overlay=0` 会在挂载前就置 false)。
|
|
|
|
|
+ hostNode.style.display = overlayVisible ? '' : 'none'
|
|
|
|
|
+ // 显式点击触发器(票 47 维护者裁定:给事件触发器、点击照搬原功能函数):
|
|
|
|
|
+ // 命中标记节点 → 交给注入的 onPick(mapService 传 pickDeviceOrPointAt,与 Canvas 点选同一函数)。
|
|
|
|
|
+ hostNode.addEventListener('click', (event) => {
|
|
|
|
|
+ if (typeof onPick !== 'function') return
|
|
|
|
|
+ const node = event.target && event.target.closest ? event.target.closest(`.${NODE_CLASS}`) : null
|
|
|
|
|
+ if (!node) return
|
|
|
|
|
+ onPick({
|
|
|
|
|
+ x: event.clientX,
|
|
|
|
|
+ y: event.clientY,
|
|
|
|
|
+ kind: node.dataset.markerOverlayKind,
|
|
|
|
|
+ code: node.dataset.markerOverlayCode,
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ // 标记节点上的按下/抬起不再冒泡到宿主:宿主的 attachPointPick 会对同一次点击**再拾一遍**
|
|
|
|
|
+ // (双派发)。点选统一走上面的显式触发器。pointermove 不拦——hover 高亮与 tooltip 链路照常。
|
|
|
|
|
+ const stopOnMarker = (event) => {
|
|
|
|
|
+ if (event.target && event.target.closest && event.target.closest(`.${NODE_CLASS}`)) {
|
|
|
|
|
+ event.stopPropagation()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ hostNode.addEventListener('pointerdown', stopOnMarker)
|
|
|
|
|
+ hostNode.addEventListener('pointerup', stopOnMarker)
|
|
|
mapHost.appendChild(hostNode)
|
|
mapHost.appendChild(hostNode)
|
|
|
hostRecord = { host: mapHost }
|
|
hostRecord = { host: mapHost }
|
|
|
return true
|
|
return true
|
|
@@ -154,40 +206,135 @@ export function isMarkerOverlayMounted() {
|
|
|
return !!(hostNode && hostNode.isConnected)
|
|
return !!(hostNode && hostNode.isConnected)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 临时 A/B 开关(票 47 走查用,维护者裁定「等功能完全实现后再替换 billboard」期间的对照工具):
|
|
|
|
|
+ * `false` 隐藏 DOM overlay 层、露出底下 billboard(= 之前版本);`true` 回到当前版本。
|
|
|
|
|
+ * 只切显示、不切数据链路——两层同位同素材,同一视角下互换即对照。
|
|
|
|
|
+ * 入口:DEV 把手 `__cockpitMap.setMarkerOverlay(bool)`;初始态 `?overlay=0`(mapService 读)。
|
|
|
|
|
+ */
|
|
|
|
|
+export function setMarkerOverlayVisible(visible) {
|
|
|
|
|
+ overlayVisible = visible !== false
|
|
|
|
|
+ if (hostNode) hostNode.style.display = overlayVisible ? '' : 'none'
|
|
|
|
|
+ return overlayVisible
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** A/B 开关当前读数(走查断言用)。 */
|
|
|
|
|
+export function isMarkerOverlayVisible() {
|
|
|
|
|
+ return overlayVisible
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 每帧同步(由 mapService 的 postRender 驱动):
|
|
* 每帧同步(由 mapService 的 postRender 驱动):
|
|
|
- * 投影 + transform 更新 + 屏外隐藏 + 池复用。**不重建节点**。
|
|
|
|
|
|
|
+ * 投影 + transform 更新 + 屏外隐藏 + 池复用。**不重建节点**(指纹变化才重建)。
|
|
|
* @param {object} params
|
|
* @param {object} params
|
|
|
- * @param {Viewer} params.viewer Cesium viewer(只取 scene 投影 API)
|
|
|
|
|
* @param {Array} params.markers `planPointMarkers().markers`(含 industry/tone/plan 消费字段)
|
|
* @param {Array} params.markers `planPointMarkers().markers`(含 industry/tone/plan 消费字段)
|
|
|
|
|
+ * @param {Array} [params.devices] 关联设备行(`projectDeviceMarkers()` 产出:屏幕坐标直供,
|
|
|
|
|
+ * scale/alpha 已含选中/hover/弱化反应量——DOM 镜像 billboard 实体真值,防「小一圈」)
|
|
|
|
|
+ * @param {Map} [params.pointScales] `kind|code` → billboard 当前 scale(强调 1.55 等),DOM 同步镜像
|
|
|
* @param {object} params.tones token 色表(tone 键 → CSS 颜色)
|
|
* @param {object} params.tones token 色表(tone 键 → CSS 颜色)
|
|
|
* @param {Function} params.planOf marker → plan(markerPlan.planMarker 的包装)
|
|
* @param {Function} params.planOf marker → plan(markerPlan.planMarker 的包装)
|
|
|
- * @param {object} params.innerUrls 内层图标语义名 → data URL(manifest 解析产物)
|
|
|
|
|
|
|
+ * @param {Function} params.projectToWindow (lon, lat) → 宿主相对窗口坐标(贴地口径由调用方保证)
|
|
|
*/
|
|
*/
|
|
|
-export function syncMarkerOverlay({ markers, tones, planOf, innerUrls, projectToWindow }) {
|
|
|
|
|
|
|
+export function syncMarkerOverlay({ markers, devices, pointScales, tones, planOf, projectToWindow }) {
|
|
|
if (!hostNode || !hostNode.isConnected || typeof projectToWindow !== 'function') return
|
|
if (!hostNode || !hostNode.isConnected || typeof projectToWindow !== 'function') return
|
|
|
const wanted = new Set()
|
|
const wanted = new Set()
|
|
|
|
|
+ // 设备行坐标是**窗口坐标**(引擎投影真值):转宿主相对要减宿主(overlay 挂载父级)的
|
|
|
|
|
+ // 窗口原点。每帧读一次——宿主可能随布局移动,不能在挂载时缓存。
|
|
|
|
|
+ const hostRect = hostNode.parentElement && hostNode.parentElement.getBoundingClientRect
|
|
|
|
|
+ ? hostNode.parentElement.getBoundingClientRect()
|
|
|
|
|
+ : { left: 0, top: 0 }
|
|
|
|
|
+ const hostOffset = { x: hostRect.left, y: hostRect.top }
|
|
|
|
|
|
|
|
for (const marker of markers || []) {
|
|
for (const marker of markers || []) {
|
|
|
const key = `${marker.kind}|${marker.code}`
|
|
const key = `${marker.kind}|${marker.code}`
|
|
|
wanted.add(key)
|
|
wanted.add(key)
|
|
|
if (!Number.isFinite(marker.longitude) || !Number.isFinite(marker.latitude)) continue
|
|
if (!Number.isFinite(marker.longitude) || !Number.isFinite(marker.latitude)) continue
|
|
|
|
|
|
|
|
- // 经纬度 → 窗口坐标:投影函数由调用方注入(cesiumEngine 的 SceneTransforms 包装,
|
|
|
|
|
- // overlay 不直接 import 'cesium'——与 mapService 的层次纪律一致)。
|
|
|
|
|
const windowPos = projectToWindow(marker.longitude, marker.latitude)
|
|
const windowPos = projectToWindow(marker.longitude, marker.latitude)
|
|
|
if (!windowPos || !Number.isFinite(windowPos.x) || !Number.isFinite(windowPos.y)) continue
|
|
if (!windowPos || !Number.isFinite(windowPos.x) || !Number.isFinite(windowPos.y)) continue
|
|
|
|
|
|
|
|
- let node = pool.get(key)
|
|
|
|
|
- if (!node) {
|
|
|
|
|
- const plan = planOf(marker)
|
|
|
|
|
- const toneHex = tones[marker.tone] || tones.neutral
|
|
|
|
|
- node = buildNode(key, marker, plan, toneHex, innerUrls)
|
|
|
|
|
- pool.set(key, { root: node })
|
|
|
|
|
- hostNode.appendChild(node)
|
|
|
|
|
|
|
+ const plan = planOf(marker)
|
|
|
|
|
+ const toneHex = tones[marker.tone] || tones.neutral
|
|
|
|
|
+ // 计划读数指纹:状态/行业/名称/母版/内层/降饱和/色值任一变化就重建节点。
|
|
|
|
|
+ const fp = JSON.stringify([
|
|
|
|
|
+ marker.status, marker.industry, marker.name,
|
|
|
|
|
+ plan.master, plan.inner.enabled, plan.inner.icon, plan.inner.diameterRatio,
|
|
|
|
|
+ plan.desaturate, toneHex,
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let record = pool.get(key)
|
|
|
|
|
+ if (!record || record.fp !== fp) {
|
|
|
|
|
+ if (record && record.root.parentNode) record.root.parentNode.removeChild(record.root)
|
|
|
|
|
+ record = { ...buildNode(key, marker, plan, toneHex), fp }
|
|
|
|
|
+ pool.set(key, record)
|
|
|
|
|
+ hostNode.appendChild(record.root)
|
|
|
|
|
+ if (plan.inner.enabled) {
|
|
|
|
|
+ // 内层素材:与 billboard 同一去色重染规则的原生分辨率 data URL(缓存复用)。
|
|
|
|
|
+ // 竞态防护:加载回来时池记录可能已被更新的状态重建——只在仍是本节点时写入。
|
|
|
|
|
+ const mine = record
|
|
|
|
|
+ renderInnerIconUrl(plan.inner.icon)
|
|
|
|
|
+ .then((dataUrl) => {
|
|
|
|
|
+ if (pool.get(key) === mine) mine.inner.setAttribute('href', dataUrl)
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(() => {
|
|
|
|
|
+ // 内层素材失败:只有母版水滴(降级可见),不阻塞其余标记。
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- const record = pool.get(key)
|
|
|
|
|
record.root.style.transform = `translate(${windowPos.x}px, ${windowPos.y}px)`
|
|
record.root.style.transform = `translate(${windowPos.x}px, ${windowPos.y}px)`
|
|
|
|
|
+ // 强调/还原的缩放镜像:billboard.scale 被 highlightEntities(1.55)等改写时,
|
|
|
|
|
+ // DOM 围绕尖端缩放同一个量(transformOrigin=尖端,见 buildNode)——两层不分离。
|
|
|
|
|
+ const markerScale = pointScales instanceof Map ? pointScales.get(key) : undefined
|
|
|
|
|
+ record.svg.style.transform = markerScale && markerScale !== 1 ? `scale(${markerScale})` : ''
|
|
|
|
|
+ record.root.style.display = 'block'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 关联设备(票 47 维护者反馈②根因:设备是独立 billboard 图层,此前 DOM 只渲染
|
|
|
|
|
+ // 摘要标记计划、关联设备根本没进 DOM)。行由引擎每帧投影产出(屏幕坐标真值 +
|
|
|
|
|
+ // 反应量真值),形态走与摘要未关联设备同一套合成计划(实心母版 + 内层行业图标)。
|
|
|
|
|
+ for (const device of devices || []) {
|
|
|
|
|
+ const key = device.key || `device|${device.code}`
|
|
|
|
|
+ wanted.add(key)
|
|
|
|
|
+ if (!Number.isFinite(device.x) || !Number.isFinite(device.y)) continue
|
|
|
|
|
+
|
|
|
|
|
+ // billboard 图层里个体设备**没有 label**(只有聚合符号有)——DOM 侧同口径不渲染
|
|
|
|
|
+ // 名字标签(name 置空即不建 label 节点),否则两层并现时 DOM 多出一排设备名牌。
|
|
|
|
|
+ const marker = {
|
|
|
|
|
+ kind: 'associated-device',
|
|
|
|
|
+ code: device.code,
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ status: device.status || '',
|
|
|
|
|
+ industry: device.industry || '',
|
|
|
|
|
+ }
|
|
|
|
|
+ const plan = planOf(marker)
|
|
|
|
|
+ const toneHex = tones[device.tone] || tones.neutral
|
|
|
|
|
+ const fp = JSON.stringify([
|
|
|
|
|
+ marker.status, marker.industry,
|
|
|
|
|
+ plan.master, plan.inner.enabled, plan.inner.icon, plan.inner.diameterRatio,
|
|
|
|
|
+ plan.desaturate, toneHex,
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let record = pool.get(key)
|
|
|
|
|
+ if (!record || record.fp !== fp) {
|
|
|
|
|
+ if (record && record.root.parentNode) record.root.parentNode.removeChild(record.root)
|
|
|
|
|
+ record = { ...buildNode(key, marker, plan, toneHex), fp }
|
|
|
|
|
+ pool.set(key, record)
|
|
|
|
|
+ hostNode.appendChild(record.root)
|
|
|
|
|
+ if (plan.inner.enabled) {
|
|
|
|
|
+ const mine = record
|
|
|
|
|
+ renderInnerIconUrl(plan.inner.icon)
|
|
|
|
|
+ .then((dataUrl) => {
|
|
|
|
|
+ if (pool.get(key) === mine) mine.inner.setAttribute('href', dataUrl)
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(() => {})
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 坐标 = 引擎投影的窗口坐标(含 pixelOffset 尖端补偿),只差宿主原点一层。
|
|
|
|
|
+ record.root.style.transform = `translate(${device.x - hostOffset.x}px, ${device.y - hostOffset.y}px)`
|
|
|
|
|
+ // 反应量镜像:scale 读的是 billboard 实体真值(选中 1.6/hover 1.3/弱化 0.75),
|
|
|
|
|
+ // 弱化的压暗同理镜像 alpha(只写透明度,不动色相——色键语义归设备状态)。
|
|
|
|
|
+ record.svg.style.transform = device.scale && device.scale !== 1 ? `scale(${device.scale})` : ''
|
|
|
|
|
+ record.root.style.opacity = device.alpha !== undefined && device.alpha !== 1 ? String(device.alpha) : ''
|
|
|
record.root.style.display = 'block'
|
|
record.root.style.display = 'block'
|
|
|
}
|
|
}
|
|
|
|
|
|