标记便签改为 DOM 覆盖层,修复近距离缩放漂移

This commit is contained in:
2026-09-19 02:25:05 +08:00
parent 41d68e087a
commit fade9eedc2
2 changed files with 70 additions and 39 deletions
+56 -39
View File
@@ -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) {