From fade9eedc20c4eadc8281895bfaa1068fcaa5b45 Mon Sep 17 00:00:00 2001 From: fukunee Date: Sat, 19 Sep 2026 02:25:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=87=E8=AE=B0=E4=BE=BF=E7=AD=BE=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20DOM=20=E8=A6=86=E7=9B=96=E5=B1=82=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=BF=91=E8=B7=9D=E7=A6=BB=E7=BC=A9=E6=94=BE=E6=BC=82?= =?UTF-8?q?=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scene/CesiumGlobe.js | 95 +++++++++++++++++++++++----------------- src/styles.css | 14 ++++++ 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/src/scene/CesiumGlobe.js b/src/scene/CesiumGlobe.js index f9cda1e..da6e5ed 100644 --- a/src/scene/CesiumGlobe.js +++ b/src/scene/CesiumGlobe.js @@ -7,10 +7,6 @@ const IMAGERY_URL = const TERRAIN_URL = 'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer'; -// 标记便签的离地高度(米):抬到高空,使其 bounding 球比城市/国家名更靠相机, -// 从而在透明通道的深度排序中始终后绘制(盖在标签上层)。 -const MARKER_ALTITUDE = 50000; - // 计算多边形集合的球面面积(km²),用于按国土面积分级国家名字号 function sphericalAreaKm2(polys) { let area = 0; @@ -96,7 +92,7 @@ function makeStickyNote(text, fontSize, selected = false) { ctx.textAlign = 'center'; ctx.fillText(text, arrowW + bodyW / 2, h / 2 + 1); - return { canvas: c, scale: 1 / S }; + return { canvas: c, scale: 1 / S, width: w, height: h }; } export class CesiumGlobe { @@ -235,47 +231,69 @@ export class CesiumGlobe { } _buildMarkers() { - const viewer = this.viewer; - ENTRIES.forEach((entry, i) => { - // 便签:黄底黑字 + 左侧箭头指向坐标(整张图) + const position = Cesium.Cartesian3.fromDegrees(entry.lng, entry.lat); const noteNormal = makeStickyNote(entry.name, 19, false); const noteSelected = makeStickyNote(entry.name, 19, true); - - const entity = viewer.entities.add({ - position: Cesium.Cartesian3.fromDegrees(entry.lng, entry.lat, MARKER_ALTITUDE), - billboard: { - image: noteNormal.canvas, - scale: noteNormal.scale, - horizontalOrigin: Cesium.HorizontalOrigin.LEFT, - verticalOrigin: Cesium.VerticalOrigin.CENTER, - pixelOffset: new Cesium.Cartesian2(0, 0), - disableDepthTestDistance: Number.POSITIVE_INFINITY, - distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 1e7), - }, - }); - entity.entryIndex = i; - - this.markers.push({ entity, noteNormal, noteSelected, index: i }); + this.markers.push({ position, noteNormal, noteSelected, index: i }); }); + this._initMarkerLayer(); + } + + // 便签用 DOM 覆盖层渲染:始终在城市/国家名上层,且缩放到最近也不会漂移 + _initMarkerLayer() { + const container = document.getElementById('cesium-container'); + this._markerLayer = document.createElement('div'); + this._markerLayer.id = 'marker-layer'; + container.appendChild(this._markerLayer); + + for (const m of this.markers) { + const el = document.createElement('div'); + el.className = 'marker-note'; + el.style.width = `${m.noteNormal.width}px`; + el.style.height = `${m.noteNormal.height}px`; + el.dataset.index = m.index; + m.el = el; + m.normalUrl = m.noteNormal.canvas.toDataURL(); + m.selectedUrl = m.noteSelected.canvas.toDataURL(); + el.style.backgroundImage = `url("${m.normalUrl}")`; + el.addEventListener('click', (e) => { + e.stopPropagation(); + this.onPick?.(m.index); + }); + this._markerLayer.appendChild(el); + } + + this.viewer.scene.postRender.addEventListener(() => this._updateMarkerLayer()); + } + + // 每帧更新便签屏幕位置(相机移动/缩放/飞行时跟随) + _updateMarkerLayer() { + const scene = this.viewer.scene; + const camera = scene.camera; + const toPos = new Cesium.Cartesian3(); + for (const m of this.markers) { + const el = m.el; + if (!el) continue; + const win = Cesium.SceneTransforms.worldToWindowCoordinates(scene, m.position); + if (!win) { el.style.display = 'none'; continue; } + Cesium.Cartesian3.subtract(m.position, camera.positionWC, toPos); + if (Cesium.Cartesian3.dot(toPos, camera.directionWC) < 0) { + el.style.display = 'none'; // 背对相机不显示 + continue; + } + el.style.display = 'block'; + el.style.left = `${win.x}px`; + el.style.top = `${win.y}px`; + } } _setupPick() { const viewer = this.viewer; // 移除 Viewer 默认的单击选中与双击“追踪实体”:否则双击后会锁定相机(定住) + // 标记便签已改为 DOM 覆盖层,点击由 DOM 元素自行处理 viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK); viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); - - const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); - handler.setInputAction((movement) => { - const picked = viewer.scene.pick(movement.position); - if (Cesium.defined(picked) && picked.id && picked.id.entryIndex !== undefined) { - const index = picked.id.entryIndex; - this.onPick?.(index); - this.flyToEntry(index); - } - }, Cesium.ScreenSpaceEventType.LEFT_CLICK); - this._handler = handler; } // 相机平滑飞至某个标记上空并居中:回到与初始一致的高度与北朝上方向,左键拖拽手感不变 @@ -421,11 +439,10 @@ export class CesiumGlobe { setSelected(index) { this.selected = index; if (!this.viewer) return; - this.markers.forEach(({ entity, noteNormal, noteSelected, index: i }) => { - const on = i === index; - entity.billboard.image = on ? noteSelected.canvas : noteNormal.canvas; // 选中加黑边 + this.markers.forEach((m) => { + const on = m.index === index; + if (m.el) m.el.style.backgroundImage = `url("${on ? m.selectedUrl : m.normalUrl}")`; }); - this.viewer.scene.requestRender(); } setVisible(visible) { diff --git a/src/styles.css b/src/styles.css index 5d1280b..4d5e61d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -37,6 +37,20 @@ html, body { /* 隐藏左下角全部归属信息(Cesium ion logo + 影像文字) */ #cesium-container .cesium-widget-credits { display: none !important; } +/* 标记便签 DOM 覆盖层(在 Cesium 画布之上) */ +#marker-layer { + position: absolute; inset: 0; z-index: 20; + pointer-events: none; +} +.marker-note { + position: absolute; + pointer-events: auto; + background-repeat: no-repeat; + background-size: 100% 100%; + transform: translateY(-50%); + cursor: pointer; +} + /* 气氛遮罩 */ .vignette { position: absolute; inset: 0; pointer-events: none;