| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808 |
- /**
- * 地图标记 DOM overlay(票 47)——billboard 发糊与底板生硬的结构性解决。
- *
- * 渲染分工(维护者 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)。
- *
- * 与既有链路的关系(维护者裁定:DOM 完整复刻 billboard 功能后才替换它):
- * · 合成计划层 `domain/markerPlan.js` **原样复用**(归属/几何/锚点/降饱和不换媒介);
- * · `syncPointMarkersForPlan`(billboard 链路)**保留不动**——聚合、特效、Canvas 拾取仍依赖它;
- * 内层素材加载失败时 SVG 只有母版 = 底下 billboard 双层照常显示(降级可见);
- * · 拾取:overlay 节点挂**显式 click 触发器**,回调直接复用 `pickDeviceOrPointAt`
- * (与 Canvas 点选链同一个功能函数,维护者原话「照搬原本的功能函数」);
- * · `data-*` 断言面全部挂到 DOM 节点,走查不读内部实现。
- *
- * 性能纪律:节点池复用(不每帧重建);计划读数指纹变化才重建;postRender 只做 transform 更新。
- */
- import { renderInnerIconUrl } from './markerAssets.js'
- // 票 76(C1 → 统一渲染):聚合符号不再用旧形状表(`markerSvg(shape)` 的三角形/圆形图标已废弃),
- // 与标记/设备一样走自绘水滴母版;domain 的 `markerSvg` 导入随之移除。
- // 票 45:特效随标记 DOM 化 —— 周期/层数/放大倍数与 billboard 特效同一份 domain 常量(节奏单源)。
- // 票 70(重做):本行导入的每一枚都必须有消费者(不留死导入,票面 Acceptance 第 3 条)。
- import { DEVICE_EFFECT, effectProfileFor, RING_PERIOD_MS, GLOW_PERIOD_MS, EFFECT_PERIOD_MS } from '../domain/deviceEffects.js'
- // 票 76(2026-10-26):设备点的色键取设备状态词条(domain 单源),DOM 不自己映射状态→颜色。
- import { deviceVisualForStatus } from '../domain/deviceStatus.js'
- /**
- * 类名单源:注入的特效 CSS(下方模板串)与 DOM 构建(buildNode/mount)共用同一组常量。
- * 必须声明在 `FX_CSS` 之前——模板串在模块求值期就要读这些值(`const` 有 TDZ)。
- * 前三枚为票 47 既有值(原样保留,勿改:走查断言与样式钩子读这些类名)。
- */
- const OVERLAY_HOST_CLASS = 'marker-overlay-host'
- const NODE_CLASS = 'marker-overlay-node'
- const LABEL_CLASS = 'marker-overlay-label'
- const FX_CLASS = 'marker-overlay-fx'
- const FX_RING_CLASS = 'marker-overlay-fx-ring'
- const FX_GLOW_CLASS = 'marker-overlay-fx-glow'
- /**
- * 特效层样式(票 45):keyframes 无法用行内 cssText 表达,所以注入一次 <style>。
- * 环带 = 径向渐变(透明度 52% → 高亮 70% → 渐隐),对比更强烈;阴影更小但扩散节奏更快。
- * 锚点是**节点原点**即图标尖端落地位置(不是帽心),这样辐射环直接标记地面目标点。
- *
- * 票 70 走查反馈②(2026-10-21):扩散/旋转峰值统一读 `--fx-profile-scale`
- * (domain EFFECT_PROFILE 单源——预警此前 1.22 峰值探不出图标 = 「看不出动态」的根因);
- * 齿轮环 keyframes 的 scale 改为 profile 驱动(此前 0.9/1.12 写死,profile 的 scale/alpha 无人消费);
- * 中心辉光尺寸/亮度配合报警存在感提升。
- */
- const FX_CSS = `
- /* 票 76(标签规格,2026-10-26 终版):标签**常态显示**(监测点 / 普通设备点 / 中心设备 / 聚合点)。
- 圈内设备(--fan)**一律不显示顶部标签** —— 维护者裁定:
- · 环内设备密集,顶部标签互相压叠、还会被图案与特效淹没看不清;
- · 它们的完整名称由**右下角 tooltip**(paintMarkerTooltip,跟随鼠标)承担,一份就够。
- 因此这里不是「hover 才显示」,而是**根本没有**(连 hover 也不显示)。 */
- .${NODE_CLASS}--fan .${LABEL_CLASS} { display:none !important; }
- /* 选择态标识(2026-10-26 二次修正):发光用**点位本体颜色语义**(--marker-tone = 状态色),
- 不再用白光。两处都生效、两种进入方式共用:
- ① 聚合点被点开(选择态);
- ② 选中设备替换中心后,中心设备本身也是选择态(可再点回选择态)。
- --marker-tone 由节点构建时写入(见 buildNode),不靠 currentColor 兜底 ——
- 母版白色描边会让 currentColor 落成白色,这正是上一版「还是白光」的原因。 */
- .${NODE_CLASS}--active { z-index:2; }
- /* 票 76(四态,2026-10-26 修「第 3 步点不动」):聚合节点在设备节点**之后**追加到 DOM,
- 天然盖在中心设备上方 —— 聚合节点的 pointer-events:auto 会把对中心设备的点击全部吃掉。
- ③④ 的中心设备必须压过聚合节点,否则「点中心设备进 ④」永远不触发。
- 中心设备给比 --active 更高的层级,且聚合节点在中心存在时降为 pointer-events:none。 */
- .${NODE_CLASS}--center { z-index:3; }
- .${NODE_CLASS}--aggregate.${NODE_CLASS}--replaced { pointer-events:none; }
- .${NODE_CLASS}--active svg {
- filter:drop-shadow(0 0 6px var(--marker-tone, currentColor)) drop-shadow(0 0 14px var(--marker-tone, currentColor)) brightness(1.18);
- }
- /* 票 76(四态规格 2026-10-26):② 与 ④ 都是「被选择态」 —— 发光 + **隐藏自身标签**
- (② 隐藏聚合标签、④ 隐藏中心设备标签),hover 圈内设备才出全名。 */
- .${NODE_CLASS}--active .${LABEL_CLASS} { display:none; }
- .${NODE_CLASS}--active:hover .${LABEL_CLASS} { display:block; }
- .${FX_CLASS} { position:absolute; pointer-events:none; transform-origin:50% 50%; }
- .${FX_RING_CLASS} {
- position:absolute; left:50%; top:50%;
- border-radius:50%;
- background:radial-gradient(circle, rgba(0,0,0,0) 52%, currentColor 70%, rgba(0,0,0,0) 98%);
- transform:translate(-50%,-50%);
- animation:markerFxExpand var(--fx-period,1800ms) linear infinite;
- /* 减小静态光环,但增强扩散对比 */
- box-shadow:0 0 6px currentColor, 0 0 16px currentColor;
- will-change:transform,opacity;
- }
- .${FX_GLOW_CLASS} {
- position:absolute; left:50%; top:50%;
- border-radius:50%;
- background:radial-gradient(circle, currentColor 0%, color-mix(in srgb, currentColor 42%, transparent) 30%, rgba(0,0,0,0) 72%);
- box-shadow:0 0 18px currentColor, 0 0 40px currentColor;
- transform:translate(-50%,-50%);
- animation:markerFxGlow var(--fx-glow-period,2400ms) ease-in-out infinite;
- will-change:transform,opacity;
- }
- .${FX_CLASS}--blink .${FX_RING_CLASS} {
- animation:markerFxExpand var(--fx-blink-period,1400ms) linear infinite,
- markerFxFlicker ${EFFECT_PERIOD_MS.BLINK}ms steps(2, jump-none) infinite;
- }
- .${FX_CLASS}--spin .${FX_RING_CLASS} {
- background:none;
- border:2px dashed currentColor;
- animation:markerFxSpin var(--fx-spin-period,3700ms) linear infinite;
- }
- @keyframes markerFxExpand {
- 0% { transform:translate(-50%,-50%) scale(0.1); opacity:1; filter:brightness(2); }
- 35% { opacity:0.85; filter:brightness(1.6); }
- 70% { opacity:0.4; filter:brightness(1.1); }
- 100% { transform:translate(-50%,-50%) scale(var(--fx-profile-scale, 1)); opacity:0; filter:brightness(0.8); }
- }
- @keyframes markerFxGlow {
- 0%, 100% { transform:translate(-50%,-50%) scale(0.75); opacity:0.32; filter:brightness(1.2); }
- 50% { transform:translate(-50%,-50%) scale(1.45); opacity:0.85; filter:brightness(1.9); }
- }
- @keyframes markerFxFlicker {
- 0%, 100% { opacity:1; }
- 50% { opacity:0.28; }
- }
- @keyframes markerFxSpin {
- from { transform:translate(-50%,-50%) rotate(0deg) scale(calc(var(--fx-profile-scale, 1) * 0.9)); opacity:0.95; }
- to { transform:translate(-50%,-50%) rotate(360deg) scale(calc(var(--fx-profile-scale, 1) * 1.12)); opacity:0.68; }
- }
- @media (prefers-reduced-motion: reduce) { .${FX_CLASS} { display:none; } }
- `
- const FX_STYLE_ID = 'marker-overlay-fx-styles'
- /** 注入一次特效样式(幂等:已存在就跳过)。 */
- function ensureFxStyles() {
- if (typeof document === 'undefined') return
- if (document.getElementById(FX_STYLE_ID)) return
- const style = document.createElement('style')
- style.id = FX_STYLE_ID
- style.textContent = FX_CSS
- document.head.appendChild(style)
- }
- /** 每帧由 `syncMarkerOverlay` 注入的门控查询(mapService 从 domain 动效计划产出)。 */
- let frameEffectOf = () => ''
- /** 状态 → 特效 profile(domain 单源;DOM 层数/亮度/辉光只消费这一份表,不另造)。 */
- function markerFxProfile(status) {
- return effectProfileFor(status)
- }
- /**
- * 标记动效查询(票 70 重做):**双条件**才出特效——
- * ① 门控放行(`effectOf`:开且未降级,判定在 domain,渲染层不自行发明);
- * ② 状态有 profile 且 profile 的动效与门控放行的动效一致(防串键)。
- * @returns {{effect:string, profile:object}|null}
- */
- function fxOf(marker) {
- const effect = frameEffectOf(marker)
- if (!effect) return null
- const profile = markerFxProfile(marker.status)
- if (!profile || profile.effect !== effect) return null
- return { effect, profile }
- }
- /**
- * 环层数=状态 profile 的 layers(报警 4 / 预警 3 / 工单 2;辉光不计入层读数,只给报警)。
- * C3f 口径:层数只从 profile 消费,DOM 侧不许写死。
- */
- function markerEffectLayers(profile) {
- return profile.layers
- }
- /**
- * 特效 DOM(票 70 重做):固定 `markerEffectLayers(profile)`(+报警辉光 1)个节点,
- * CSS 动画驱动、JS 不逐帧更新。容器几何:`left:${-box / 2}px` 使其**中心 = 节点原点**
- * (图标尖端落地点),辐射环直接标记地面目标;`currentColor` 由 tone 染色(与标记本体同色)。
- * 周期变量全部来自 domain 常量(节奏单源):环 `RING_PERIOD_MS`、辉光 `GLOW_PERIOD_MS`。
- */
- function buildFxLayers(fx, plan, toneHex) {
- const { effect, profile } = fx
- const period = RING_PERIOD_MS[effect] || RING_PERIOD_MS[DEVICE_EFFECT.PULSE]
- const box = plan.draw.width
- const host = document.createElement('div')
- host.className = `${FX_CLASS} ${FX_CLASS}--${effect}`
- host.style.cssText = `position:absolute;left:${-box / 2}px;top:${-box / 2}px;width:${box}px;height:${box}px;color:${toneHex};`
- host.style.setProperty('--fx-period', `${period}ms`)
- host.style.setProperty('--fx-glow-period', `${GLOW_PERIOD_MS}ms`)
- host.style.setProperty('--fx-profile-scale', String(profile.scale))
- if (effect === DEVICE_EFFECT.BLINK) host.style.setProperty('--fx-blink-period', `${period}ms`)
- if (effect === DEVICE_EFFECT.SPIN) host.style.setProperty('--fx-spin-period', `${period}ms`)
- // 均分错相:第 i 层延迟 -period*i/layers(负延迟=开播即处于各自相位,形成连续外扩)。
- for (let layer = 0; layer < markerEffectLayers(profile); layer += 1) {
- const ring = document.createElement('div')
- ring.className = FX_RING_CLASS
- ring.style.width = `${box}px`
- ring.style.height = `${box}px`
- ring.style.animationDelay = `${-(period / markerEffectLayers(profile)) * layer}ms`
- host.appendChild(ring)
- }
- // 中心辉光只给报警(EFFECT_PROFILE.glow;票 07 优先级语义,不给预警/工单)。
- if (profile.glow) {
- const glow = document.createElement('div')
- glow.className = FX_GLOW_CLASS
- const g = Math.round(box * 0.7)
- glow.style.width = `${g}px`
- glow.style.height = `${g}px`
- host.appendChild(glow)
- }
- return host
- }
- /**
- * 聚合符号节点(票 76 / C1):重叠设备的聚合符号此前只活在 billboard 层(票 47 遗留),
- * DOM 侧一个都没有。节点形态与 billboard 版对齐:
- * · 形状/色键 = 引擎读数(`overlapShape` / `overlapTone`),形状生成器复用 domain `markerSvg`
- * (billboard 的 `imageOf` 就是它)——同源,不另画一套;
- * · 尺寸 = billboard 宽真值(`size * 1.5` 的产物,票 58 的聚合符号档),DOM 不自己算;
- * · 锚点 = 投影点**居中**(对应 billboard `verticalOrigin: CENTER` + pixelOffset 0),
- * 与标记的尖端锚定不同;
- * · 标签文案与 billboard 版同式样(「聚合 K 台 · 组内<状态词>」);
- * · **不含设备身份**:`data-marker-overlay-code` 放 group key(点击派发用),不放 deviceCode。
- */
- function buildAggregateNode(key, row, toneHex, plan) {
- const root = document.createElement('div')
- // `--aggregate` 修饰类:让聚合标签走「常显」规则(设备标签只在 hover 时显示)。
- root.className = `${NODE_CLASS} ${NODE_CLASS}--aggregate`
- root.dataset.markerOverlayKey = key
- root.dataset.markerOverlayKind = 'overlap-aggregate'
- root.dataset.markerOverlayCode = row.group
- root.dataset.markerOverlayAggregate = String(row.k)
- root.dataset.markerOverlayPointCode = row.pointCode || ''
- root.style.cssText = [
- 'position:absolute',
- 'left:0', 'top:0',
- 'width:0', 'height:0',
- 'pointer-events:auto',
- 'cursor:pointer',
- 'will-change:transform',
- ].join(';')
- // 票 76(选择态发光):状态色写成 CSS 变量(**必须在 cssText 之后**——cssText 会整段覆盖内联样式,
- // 先设置等于刚写就抹掉,这是上一版「还是白光」的第二层原因)。
- root.style.setProperty('--marker-tone', toneHex)
- // 票 76(统一设备点位渲染):聚合符号也走**自绘水滴母版**(与标记/设备同一套几何与渲染路径),
- // 不再用旧的形状表(三角形/圆形图标)——那是废弃的渲染模式。
- const w = plan.size.width
- const h = plan.size.height
- const pad = plan.draw.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)' : '',
- ].join(';')
- 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 && innerEl) {
- const diameter = plan.inner.diameterRatio * 378
- const cx = 378 * plan.anchor.x
- 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))
- renderInnerIconUrl(plan.inner.icon)
- .then((dataUrl) => { innerEl.setAttribute('href', dataUrl) })
- .catch(() => {})
- }
- // 票 76(新需求 2026-10-26):聚合点图标内加**聚合符号**(不是行业图标 —— 聚合表达"一组",
- // 不是某一种行业)。用内联 SVG 画三层同心「堆叠」符号,颜色取描边白,与母版同一条渲染路径。
- {
- const diameter = 0.62 * 378
- const cx = 378 * plan.anchor.x
- const cy = plan.inner.center.y * 475
- const group = document.createElementNS('http://www.w3.org/2000/svg', 'g')
- group.setAttribute('data-marker-overlay-aggregate-symbol', '')
- const r = diameter / 2
- for (let i = 0; i < 3; i += 1) {
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
- circle.setAttribute('cx', String(cx - r * 0.45 + i * r * 0.45))
- circle.setAttribute('cy', String(cy + r * 0.18 - i * r * 0.18))
- circle.setAttribute('r', String(r * 0.42))
- circle.setAttribute('fill', '#ffffff')
- circle.setAttribute('fill-opacity', i === 2 ? '1' : '0.55')
- group.appendChild(circle)
- }
- svg.appendChild(group)
- }
- const label = document.createElement('div')
- label.className = LABEL_CLASS
- label.textContent = `聚合 ${row.k} 台${row.label ? ` · 组内${row.label}` : ''}`
- label.title = label.textContent
- label.style.cssText = [
- 'position:absolute',
- `bottom:${h + pad + 3}px`,
- 'left:0',
- 'transform:translateX(-50%)',
- 'box-sizing:content-box',
- 'max-width:170px',
- 'overflow:hidden',
- 'white-space:nowrap',
- 'text-overflow:ellipsis',
- 'padding:4px 9px',
- 'border-radius:8px',
- 'border:1px solid rgba(1,171,234,0.55)',
- 'background:rgba(2,20,40,0.82)',
- 'color:#d7ecff',
- 'font:600 12px "Microsoft YaHei UI","Microsoft YaHei","PingFang SC","Noto Sans SC",sans-serif',
- 'pointer-events:none',
- ].join(';')
- root.appendChild(label)
- return { root, icon: svg }
- }
- /**
- * 自绘水滴母版(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 不被裁)。
- */
- 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"'
- 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()
- /** 帧内复用的"本轮在计划里"集合(票 35:原先每帧 new Set)。 */
- const wantedKeys = new Set()
- /** 指纹分隔符:用不可打印字符避免与业务字符串拼接歧义。 */
- const FP_SEP = '\u0001'
- let hostNode = null
- let hostRecord = null
- let postRenderRemove = null
- /** 临时 A/B 开关(票 47 走查):false = 隐藏 DOM 层、露出底下 billboard(之前版本)。 */
- // 票 76(C6):A/B 显隐开关已退役 —— overlay 挂载即生效,billboard 视觉不再有「对照组」。
- /**
- * 单个标记节点(特效层 + 标记 SVG + 可选标签),一次构建、每帧只更新 transform。
- * @param {object|null} fx `fxOf(marker)` 的产出(null = 无动效;关联设备恒 null——
- * 它们的特效留在 billboard 层,DOM 不画第二套环,同位双环会糊,票 72 遗留决策一约束)。
- */
- function buildNode(key, marker, plan, toneHex, fx) {
- const root = document.createElement('div')
- root.className = NODE_CLASS
- root.dataset.markerOverlayKey = key
- root.dataset.markerOverlayKind = marker.kind
- root.dataset.markerOverlayCode = marker.code
- // 宿主 data-* 断言面(规格 10 读数在 DOM 节点上延续)
- root.dataset.markerMaster = plan.master
- root.dataset.markerInner = plan.inner.enabled ? plan.inner.icon : ''
- root.dataset.markerAnchor = `${plan.anchor.x},${plan.anchor.y}`
- root.dataset.markerDesaturate = plan.desaturate ? 'yes' : 'no'
- root.dataset.markerTone = toneHex
- // 特效读数面(票 70 重做 / t45 C3d):无动效不挂键;挂 = 动效键 + profile 层数 + alpha 真值。
- if (fx) {
- root.dataset.markerEffect = fx.effect
- root.dataset.markerEffectLayers = String(markerEffectLayers(fx.profile))
- root.dataset.markerEffectAlpha = String(fx.profile.alpha)
- }
- const w = plan.size.width
- const h = plan.size.height
- const pad = plan.draw.pad
- root.style.cssText = [
- 'position:absolute',
- 'left:0', 'top:0',
- 'width:0', 'height:0',
- 'pointer-events:auto',
- 'cursor:pointer',
- 'will-change:transform',
- ].join(';')
- // 票 76(选择态发光):状态色写成 CSS 变量 —— **必须在 cssText 之后**(cssText 整段覆盖内联样式,
- // 先写等于刚写就抹掉;这是「还是白光」的第二层原因)。不能靠 `currentColor`:母版带白色描边,
- // currentColor 会落成白色(第一层原因)。
- root.style.setProperty('--marker-tone', toneHex)
- // 特效层在标记本体之下(先挂 fx、SVG 后画盖住环心,环带在图标外扩散可见)。
- // 锚点是节点原点即尖端落地点(buildFxLayers 的 -box/2 偏移保证中心=原点)。
- if (fx) {
- root.appendChild(buildFxLayers(fx, plan, toneHex))
- }
- 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)' : '',
- ].join(';')
- // 反应量缩放的**原点 = 尖端**(尖端固定在地面坐标上,标记围绕尖端放大/缩小,
- // 与 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。pointer-events:none —— 不拦截事件,相机拖拽与 Canvas 拾取照常穿过。
- // 限宽取计划层 `labelMaxWidth`(宽 × 2.6,billboard 同一单源)——浏览器走查修正:
- // 首版硬编码 220px,比 billboard 截断宽 3 倍,两层标签并现时一长一短明显不一致。
- if (marker.name) {
- const label = document.createElement('div')
- label.className = LABEL_CLASS
- label.textContent = marker.name
- label.title = marker.name // hover 读全(与 DOM 侧 EllipsisText 同语义)
- label.style.cssText = [
- 'position:absolute',
- `bottom:${h + pad + 3}px`,
- 'left:0',
- 'transform:translateX(-50%)',
- // content-box:max-width 只管文字,与 Cesium label 的截断宽度同语义
- 'box-sizing:content-box',
- `max-width:${plan.labelMaxWidth}px`,
- 'overflow:hidden',
- 'white-space:nowrap',
- 'text-overflow:ellipsis',
- 'padding:4px 9px',
- 'border-radius:8px',
- 'border:1px solid rgba(1,171,234,0.55)',
- 'background:rgba(2,20,40,0.82)',
- 'color:#d7ecff',
- 'font:600 12px "Microsoft YaHei UI","Microsoft YaHei","PingFang SC","Noto Sans SC",sans-serif',
- 'pointer-events:none',
- ].join(';')
- root.appendChild(label)
- }
- return { root, inner: innerEl, svg }
- }
- /**
- * 初始化 overlay 宿主(挂在地图容器内,z-index 在 canvas 之上、面板之下)。
- * @param {Element} mapHost 地图宿主(.map-canvas)
- * @param {object} [options] `{ onPick }` 显式点击触发器回调(窗口坐标 + 节点身份)
- * @returns {boolean} 是否初始化成功(失败时调用方回退 billboard 链路)
- */
- export function mountMarkerOverlay(mapHost, { onPick, onHover } = {}) {
- try {
- if (!mapHost || typeof mapHost.appendChild !== 'function') return false
- if (hostNode && hostNode.isConnected) return true
- hostNode = document.createElement('div')
- hostNode.className = OVERLAY_HOST_CLASS
- hostNode.dataset.markerOverlay = ''
- hostNode.style.cssText = [
- 'position:absolute', 'inset:0',
- 'pointer-events:none', // 宿主不拦截;节点自身 pointer-events:auto
- 'overflow:hidden',
- 'z-index:1',
- ].join(';')
- hostNode.style.display = ''
- // 票 76(C4 parity 现场把手):DOM 单层比 billboard 双层暗(镂空孔/边缘不再叠亮层)。
- // `?pinBrightness=1.2` 只做整体亮度补偿(不碰色相/饱和度);定档后写成默认常量并撤掉把手。
- try {
- if (import.meta.env && import.meta.env.DEV && typeof window !== 'undefined') {
- const hashQuery = String(window.location.hash || '').split('?')[1] || ''
- const raw = new URLSearchParams(window.location.search).get('pinBrightness')
- || new URLSearchParams(hashQuery).get('pinBrightness')
- || ''
- const value = Number(raw)
- if (Number.isFinite(value) && value > 0 && value !== 1) hostNode.style.filter = `brightness(${value})`
- }
- } catch (error) {
- // 走查把手:解析失败不影响挂载。
- }
- // 特效样式注入(票 70 重做 / t45 C3b):幂等一次;卸载时移除。
- ensureFxStyles()
- // 显式点击触发器(票 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)
- // 票 76(C6):DOM 节点 hover 派发 —— billboard 视觉让位后 GPU 拾取没有命中对象,
- // hover(设备反应量/面板高亮/tooltip)改由 overlay 按**节点身份**通知服务层。
- // 指针不在任何节点上时发 `null`,服务层据此清空 hover 态。
- const emitHover = (event) => {
- if (typeof onHover !== 'function') return
- const node = event.target && event.target.closest ? event.target.closest(`.${NODE_CLASS}`) : null
- if (!node) {
- onHover(null)
- return
- }
- onHover({
- kind: node.dataset.markerOverlayKind || '',
- code: node.dataset.markerOverlayCode || '',
- })
- }
- hostNode.addEventListener('pointermove', emitHover)
- hostNode.addEventListener('pointerleave', () => {
- if (typeof onHover === 'function') onHover(null)
- })
- mapHost.appendChild(hostNode)
- hostRecord = { host: mapHost }
- return true
- } catch {
- return false
- }
- }
- /** overlay 是否已挂载。 */
- export function isMarkerOverlayMounted() {
- return !!(hostNode && hostNode.isConnected)
- }
- // 票 76(C6):`setMarkerOverlayVisible()` 与 `?overlay=0` 已退役(票面「非目标」的收口项)——
- // 载体终裁 DOM 胜出后不再需要 billboard 对照组,第 ⑤ 条验收「点选统一走 overlay click 触发器」
- // 也因此不再有需要 GPU 拾取的第二态。
- /** overlay 是否在生效(票 76 / C6:挂载即生效,不再有 A/B 显隐态)。 */
- export function isMarkerOverlayVisible() {
- return isMarkerOverlayMounted()
- }
- /**
- * 每帧同步(由 mapService 的 postRender 驱动):
- * 投影 + transform 更新 + 屏外隐藏 + 池复用。**不重建节点**(指纹变化才重建)。
- * @param {object} params
- * @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` → 强调 scale(票 76 / C3:由**服务层**产出,
- * 1 = 常态、`EMPHASIS_STYLE.markerScale` = 强调;不再回读 billboard 实体)
- * @param {object} params.tones token 色表(tone 键 → CSS 颜色)
- * @param {Function} params.planOf marker → plan(markerPlan.planMarker 的包装)
- * @param {Function} [params.effectOf] marker → 门控后的动效键('' = 不动画;mapService 从
- * domain `planDeviceEffects` 计划产出,**只给摘要标记行**——关联设备的特效仍在 billboard 层)
- * @param {Array} [params.aggregates] 重叠聚合符号行(票 76 / C1:`projectOverlapSymbols()` 产出;
- * group 身份/计数/组内状态词/形状/色键/屏幕坐标 —— 唯一没有设备身份的一类节点)
- * @param {Function} params.projectToWindow (lon, lat) → 宿主相对窗口坐标(贴地口径由调用方保证)
- */
- export function syncMarkerOverlay({ markers, devices, aggregates, pointScales, tones, planOf, effectOf, projectToWindow }) {
- if (!hostNode || !hostNode.isConnected || typeof projectToWindow !== 'function') return
- // 每帧刷新门控查询(mapService 在计划变化时给新闭包;这里只搬运、不判定)。
- frameEffectOf = typeof effectOf === 'function' ? effectOf : () => ''
- // 票 35 卡顿整改:Set 复用(原先每帧 new 一个);指纹改成廉价拼接(原先每标记
- // JSON.stringify 一次,纯分配)。DOM 写入也改成"只有真的变了才写"。
- wantedKeys.clear()
- const wanted = wantedKeys
- // 设备行坐标是**窗口坐标**(引擎投影真值):转宿主相对要减宿主(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 || []) {
- const key = `${marker.kind}|${marker.code}`
- wanted.add(key)
- if (!Number.isFinite(marker.longitude) || !Number.isFinite(marker.latitude)) continue
- const windowPos = projectToWindow(marker.longitude, marker.latitude)
- if (!windowPos || !Number.isFinite(windowPos.x) || !Number.isFinite(windowPos.y)) continue
- const plan = planOf(marker)
- const toneHex = tones[marker.tone] || tones.neutral
- // 票 70:fxOf 的键取自 effectOf 门控查询(domain 计划已判开关/降级);profile 取状态。
- const fx = fxOf(marker)
- // 计划读数指纹:状态/行业/名称/母版/内层/降饱和/色值/**动效键与层数**任一变化就重建节点。
- const fp = [marker.status, marker.industry, marker.name,
- plan.master, plan.inner.enabled, plan.inner.icon, plan.inner.diameterRatio,
- plan.desaturate, toneHex, fx ? fx.effect : '', fx ? fx.profile.layers : 0].join(FP_SEP)
- 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, fx), 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(() => {
- // 内层素材失败:只有母版水滴(降级可见),不阻塞其余标记。
- })
- }
- }
- // 坐标只在真的变了才写(镜头静止时零 DOM 写入)。
- const transform = `translate(${windowPos.x}px, ${windowPos.y}px)`
- if (record.root.style.transform !== transform) record.root.style.transform = transform
- // 强调/还原的缩放(票 76 / C3):量由服务层产出(不再回读 billboard.scale),
- // DOM 围绕尖端缩放该量(transformOrigin=尖端,见 buildNode)。
- const markerScale = pointScales instanceof Map ? pointScales.get(key) : undefined
- const scaleTransform = markerScale && markerScale !== 1 ? `scale(${markerScale})` : ''
- if (record.svg.style.transform !== scaleTransform) record.svg.style.transform = scaleTransform
- if (record.root.style.display !== 'block') 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
- // 票 76(标签规格 2026-10-26 终版,维护者裁定):**圈内设备不建顶部标签**。
- // 理由:环内设备密集,顶部标签互相压叠且会被图案/特效淹没;它们的全名由右下角
- // tooltip(`paintMarkerTooltip`)承担,一份就够。
- // 实现:`fan` 行直接把 name 置空 → `buildNode` 不创建 label 节点(比 CSS 隐藏更彻底,
- // 也不会因为后续 `--fan` 类切换而重新冒出来)。
- const marker = {
- kind: 'associated-device',
- code: device.code,
- name: device.fan === true ? '' : (device.group ? (device.name || '') : ''),
- status: device.status || '',
- industry: device.industry || '',
- }
- const plan = planOf(marker)
- // 票 76(2026-10-26):色键由**设备状态**决定(domain 单源)——设备行不单独携带 tone,
- // 否则状态色与摘要标记两套口径。
- const toneHex = tones[deviceVisualForStatus(device.status).tone] || tones.neutral
- // 票 70(C3c 语义):关联设备循环**恒不画 DOM 环**(fx 恒 null)——它们的特效留在
- // billboard 层(ensureRingEntities 链路),同位双环会糊。
- // 票 76(特效归属,维护者裁定 A):聚合本身不挂环;**只有中心(被选中)设备**按自身状态挂环。
- // 2026-10-26 修:`DEVICE_EFFECT` 是**动效键名表**(PULSE/BLINK/SPIN),不是「状态 → 动效」映射 ——
- // 此前写成 `DEVICE_EFFECT[device.status]` 恒为 undefined,中心设备因此一直没有特效。
- // 状态 → profile 的唯一入口是 `effectProfileFor()`(domain 单源)。
- const profile = device.selected ? effectProfileFor(device.status) : null
- const fx = profile ? { effect: profile.effect, profile } : null
- const fp = [marker.status, marker.industry, marker.name,
- plan.master, plan.inner.enabled, plan.inner.icon, plan.inner.diameterRatio,
- plan.desaturate, toneHex, fx ? fx.effect : '', fx ? fx.profile.layers : 0].join(FP_SEP)
- 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, fx), 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 尖端补偿),只差宿主原点一层。
- const deviceTransform = `translate(${device.x - hostOffset.x}px, ${device.y - hostOffset.y}px)`
- if (record.root.style.transform !== deviceTransform) record.root.style.transform = deviceTransform
- // 反应量镜像:scale 读的是 billboard 实体真值(选中 1.6/hover 1.3/弱化 0.75),
- // 弱化的压暗同理镜像 alpha(只写透明度,不动色相——色键语义归设备状态)。
- // 中心设备与聚合点**同尺寸**(走查:选中设备后一直保持放大、且丢失选择态);其余设备照旧镜像。
- const effScale = device.selected ? 1 : device.scale
- const deviceScale = effScale && effScale !== 1 ? `scale(${effScale})` : ''
- if (record.svg.style.transform !== deviceScale) record.svg.style.transform = deviceScale
- const deviceOpacity = device.alpha !== undefined && device.alpha !== 1 ? String(device.alpha) : ''
- if (record.root.style.opacity !== deviceOpacity) record.root.style.opacity = deviceOpacity
- if (record.root.style.display !== 'block') record.root.style.display = 'block'
- // 票 76(标签规格):圈内设备(`fan`)标签改 hover-only;中心设备与普通设备点维持常态标签。
- record.root.classList.toggle(`${NODE_CLASS}--fan`, device.fan === true)
- // 票 76(四态规格 2026-10-26)设备节点:
- // ③ 设备常态 中心=设备点,无发光,环只在 hover 预览(`fan` 标签 hover-only)
- // ④ 设备被选择态 与聚合点被选择态相同:状态色发光 + 隐藏自身标签
- record.root.classList.toggle(`${NODE_CLASS}--active`, device.pinned === true)
- // 票 76(四态,2026-10-26):③④ 的中心设备要**压过聚合节点**(聚合节点后追加、会吃掉点击),
- // 否则「点中心设备进 ④」永远触发不了(走查第 3 步完全失效的直接原因)。
- record.root.classList.toggle(`${NODE_CLASS}--center`, device.selected === true)
- }
- // 重叠聚合符号(票 76 / C1:billboard 版只在 billboard 层,DOM 此前完全没有——票 47 遗留)。
- // 坐标同为引擎投影的窗口坐标,只差宿主原点一层。点击由宿主既有触发器派发(kind=overlap-aggregate)。
- for (const row of aggregates || []) {
- const key = row.key || `overlap|${row.pointCode}|${row.group}`
- wanted.add(key)
- if (!Number.isFinite(row.x) || !Number.isFinite(row.y)) continue
- const toneHex = tones[row.tone] || tones.neutral
- // 票 76(统一设备点位渲染):聚合符号也走 `planOf` 合成计划(母版/内层/几何与设备点同源),
- // 不再用旧的形状表(三角形图标 = 废弃渲染模式);计划读数纳入指纹,变化才重建。
- const plan = planOf({ kind: 'monitoring-device', status: row.status, industry: '', tone: row.tone })
- const fp = [
- row.k, row.label, row.status, row.state, toneHex,
- plan.master, plan.inner.enabled, plan.inner.icon, plan.inner.diameterRatio, plan.desaturate,
- ].join(FP_SEP)
- let record = pool.get(key)
- if (!record || record.fp !== fp) {
- if (record && record.root.parentNode) record.root.parentNode.removeChild(record.root)
- record = { ...buildAggregateNode(key, row, toneHex, plan), fp }
- pool.set(key, record)
- hostNode.appendChild(record.root)
- }
- const aggTransform = `translate(${row.x - hostOffset.x}px, ${row.y - hostOffset.y}px)`
- if (record.root.style.transform !== aggTransform) record.root.style.transform = aggTransform
- // 状态强调镜像(billboard `scale` / `outlineWidth` 真值)。
- // 票 76(四态):② 被选择态 = `pinned`(聚合点固定环 + 发光 + 略放大);
- // ① 的 hover 预览**不算**选择态,不放大也不发光。
- const active = row.pinned === true
- const activeBoost = active ? 1.12 : 1
- const effAggScale = (row.scale && row.scale > 0 ? row.scale : 1) * activeBoost
- const aggScale = effAggScale !== 1 ? `scale(${effAggScale})` : ''
- if (record.icon.style.transform !== aggScale) record.icon.style.transform = aggScale
- const aggFilter = row.outline ? 'drop-shadow(0 0 2px rgba(255,255,255,0.9))' : ''
- if (record.icon.style.filter !== aggFilter) record.icon.style.filter = aggFilter
- record.root.classList.toggle(`${NODE_CLASS}--active`, active)
- // 票 76(四态):① 的 hover 预览与 ② 的固定态都隐藏聚合标签(让出设备环);
- // ③④ 中心已是设备点 → 整枚聚合节点让位。
- const labelHidden = row.pinned === true || row.preview === true
- const label = record.root.querySelector(`.${LABEL_CLASS}`)
- if (label) {
- const wantedDisplay = labelHidden ? 'none' : 'block'
- if (label.style.display !== wantedDisplay) label.style.display = wantedDisplay
- }
- record.root.style.display = row.replaced ? 'none' : 'block'
- // ③④ 中心已是设备点:聚合节点虽然隐藏,但保险起见再降掉 pointer-events,
- // 避免它(或其子元素)在过渡帧里吃掉对中心设备的点击。
- record.root.classList.toggle(`${NODE_CLASS}--replaced`, row.replaced === true)
- }
- // 本轮不在计划里的:隐藏(池复用,不销毁)
- for (const [key, record] of pool.entries()) {
- if (!wanted.has(key) && record.root.style.display !== 'none') record.root.style.display = 'none'
- }
- }
- /**
- * 挂 postRender 驱动(mapService 调用;返回卸载函数)。
- * `getFrameState` 每帧由 mapService 提供最新标记计划与投影函数。
- */
- export function attachOverlayRenderer(viewer, getFrameState) {
- if (!viewer || !viewer.scene) return () => {}
- const onFrame = () => {
- const state = getFrameState()
- if (state) syncMarkerOverlay(state)
- }
- viewer.scene.postRender.addEventListener(onFrame)
- postRenderRemove = () => viewer.scene.postRender.removeEventListener(onFrame)
- return postRenderRemove
- }
- /** overlay 节点池(调试/断言用)。 */
- export function overlayPoolSize() {
- return pool.size
- }
- /** 卸载(离开 /cockpit 用):摘 postRender、清节点池与宿主、移除特效样式(t45 C3b 生命周期)。 */
- export function unmountMarkerOverlay() {
- if (postRenderRemove) {
- postRenderRemove()
- postRenderRemove = null
- }
- if (typeof document !== 'undefined') {
- const style = document.getElementById(FX_STYLE_ID)
- if (style && style.parentNode) style.parentNode.removeChild(style)
- }
- if (hostNode && hostNode.parentNode) hostNode.parentNode.removeChild(hostNode)
- hostNode = null
- hostRecord = null
- pool.clear()
- }
|