在 Cesium 应用里,圆形绘制是标绘功能里最基础的单元,也是最容易踩坑的单元。点一下出圆心、再点一下定半径,看上去只有两步,但真写到生产环境,还要处理椭球拾取、动态半径预览、右键取消、贴地高度、批量圆心数据接入等问题。一个 Circle 绘图工具远比想象中复杂。
这篇围绕“Cesium 绘图工具 - Circle”讲三件事:第一,用 Entity + ellipse 实现鼠标交互画圆;第二,把画圆封装成可复用的 CircleDrawTool 类,支持动态半径、右键取消、二次编辑;第三,补齐批量绘制、GeoJSON 导出、坐标精度和渲染性能这些容易被忽略的细节。
先说结论:如果只是临时给 Cesium Viewer 加一个画圆功能,几十行代码就能跑通;如果想做成生产可用的绘图工具,建议把状态机分成 idle / center / radius 三个阶段。这个交互模型和地图标绘里常见的“智能拉圆”一致,后续扩展矩形、多边形、箭头都基于同一套框架,能直接复用事件管理、撤销逻辑和图层管理能力。
1. Circle 圆形绘图工具核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | Cesium 前端绘图交互工具,不依赖第三方 GIS 库 |
| 核心接口 | Entity + EllipseGraphics,可选 Primitive / GroundPrimitive 批量渲染 |
| 交互方式 | 第一次点击定圆心,移动鼠标动态显示半径,第二次点击完成绘制 |
| 主要功能 | 圆形绘制、半径实时预览、右键取消、精确半径生成、批量圆心加载、GeoJSON 导出 |
| 运行环境 | 浏览器端运行,依赖 Cesium 库和 WebGL,与后端技术栈无关 |
| 图形模式 | 普通椭球面圆、贴地形圆、指定高度圆 |
| API 集成 | 可封装成 CircleDrawTool 类,对外提供 start / stop / addCircleFromLngLat / exportGeoJSON 方法 |
| 批量任务 | 支持传入多组经纬度和半径批量生成圆形,可统一显隐与清除 |
| 适用场景 | 三维标绘、二三维一体化标注、范围圈选、缓冲区示意、Cesium 插件开发 |
| 主要限制 | 精确测量需考虑 WGS84 椭球曲率,超大范围圆形建议用测地线计算而非平面距离 |
这套能力在 Cesium 内部并不算新特性,核心都是用Entity的ellipse属性渲染圆。真正拉开差距的是工程封装:事件是否及时销毁、坐标是否做椭球校正、绘制的圆能不能导出给后端、批量数据是否会卡掉 WebGL。后面几节会按这个顺序展开。
2. 适用场景与使用边界
圆形绘图工具最常见的落地场景是三维 GIS 标绘。应急指挥场景里,用户要在数字孪生底图上圈出事件影响范围;园区管理后台里,运营人员要在三维场景中标注某个设备的服务半径;车联网可视化平台里,需要根据车辆实时位置生成圆形缓冲区。这些场景通常只需要视觉化表达,对鼠标交互的流畅度和数据导出能力要求高于几何精度。
Circle 工具也适合做前端二三维一体化方案。同一组经纬度和半径数据,在二维高德底图上是一个圆,在 Cesium 三维场景里也应该是一个圆。把画圆逻辑从业务代码里抽出来,定义成 addCircleFromLngLat 这类统一方法,后端只需要存一组中心点坐标加半径,前端负责渲染,业务侧不需要感知二维还是三维。
不适合的场景也要明确。如果要做基于精确投影的工程测量,圆形边界必须由后端或桌面 GIS 按大地线计算,前端画出来的圆只能作为示意。在国内生产环境中,对底图服务的选择还涉及互联网地图服务合规要求,要把 Cesium 默认地形、在线影像更换为有合法授权的底图源,不能在生产环境直接引用来源不明的地图瓦片。涉及人员位置、敏感设施等坐标数据时,发布前要做脱敏和权限控制。
3. 实现方案与核心 API 选择
3.1 Entity + ellipse 交互方案
Entity 是 Cesium 面向业务开发者的高层封装。创建圆形实体时,position表示圆心,ellipse.semiMajorAxis和ellipse.semiMinorAxis表示半径。把两个轴设置成相同数值,椭圆就变成圆。
半径在绘制过程中是变化的,可以直接给semiMajorAxis赋一个数字,也可以传入CallbackProperty让半径属性始终保持响应式。对于动态预览,使用普通对象赋值最简单,每次鼠标移动更新值即可,Cesium 内部会触发图形更新。
const circleEntity = viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0), ellipse: { semiMajorAxis: 1000, semiMinorAxis: 1000, height: 0, material: Cesium.Color.RED.withAlpha(0.3), outline: true, outlineColor: Cesium.Color.RED, outlineWidth: 2 } });上面的代码是静态画圆。鼠标交互还需要ScreenSpaceEventHandler监听点击和移动事件,事件回调里拿到屏幕坐标后,用viewer.camera.pickEllipsoid把屏幕点还原成 WGS84 椭球面上的Cartesian3坐标。这一步决定了后续所有圆的圆心精度。
3.2 Primitive / GroundPrimitive 批量渲染方案
Entity 使用方便,但数据量大了之后,每个圆都会产生额外的管理和绘制开销。对于成百上千个静态圆形,更合适的方案是合并成Primitive或GroundPrimitive。Entity 适合交互绘制阶段,Primitive 适合一次性渲染已有数据阶段。
实际项目里,我建议把 Entity 当成“绘图交互层的解决方案”,把 Primitive 当成“大数据批量层的解决方案”。交互画圆时用一个 Entity 做预览,等用户确认后,再把结果数据交给批量图层渲染。这样既能保证交互流畅,又能避免长期积累大量 Entity 导致场景帧率下降。
4. 开发环境准备
4.1 初始化 Cesium 项目
Cesium 工具代码需要运行在支持 WebGL 的浏览器中,建议使用 Vite 作为工程化底座。实际接入 Cesium 时,可以通过 npm 安装官方最新版本,也可以按官方文档使用 CDN 方式。生产环境建议锁住一个验证过的版本,避免 Cesium 升级导致 API 变化。
npm init -y npm install cesium npm install -D vite4.2 初始化 Viewer
在 Vite 项目中引入 Cesium,需要同时引入 CSS 和核心库。Cesium 新版 API 对异步地形初始化更友好,可以先初始化 Viewer,再异步设置地形数据。
import * as Cesium from 'cesium'; import 'cesium/Build/Cesium/Widgets/widgets.css'; // 使用 Cesium Ion 时填入自己的 Token Cesium.Ion.defaultAccessToken = 'your-cesium-ion-token'; async function initCesium() { const viewer = new Cesium.Viewer('cesiumContainer', { animation: false, timeline: false, infoBox: false, selectionIndicator: false, baseLayerPicker: true, shouldAnimate: true }); // 生产环境请替换为有合法授权的底图和地形服务 try { viewer.terrainProvider = await Cesium.createWorldTerrainAsync(); } catch (e) { console.warn('地形服务加载失败,继续使用默认椭球体', e); } viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 20000) }); return viewer; } const viewer = await initCesium();如果不需要在线影像和真实地形,也可以不填 Ion Token,直接使用 Cesium 默认的空白地球,圆形绘制核心逻辑不受影响。这样本地开发门槛会低很多,只要有一个 HTML 容器和浏览器就能开始验证。
5. Circle 圆形绘图工具完整实现
5.1 交互状态设计
圆形绘制工具的核心是状态机。常见交互方式有几种,最符合地图用户习惯的是:第一次点击确定圆心,移动鼠标时出现半透明预览圆,第二次点击确定半径并完成绘制,右键取消当前绘制。
这三个状态定义成一个mode字段即可,不用引入复杂状态管理库。
mode: 'idle' // 空闲状态 mode: 'drawing' // 已点圆心,正在等待第二次点击代码逻辑上只需要判断两次分支。初次点击时创建 previewEntity,后续点击时提交实体并重置状态。如果还要支持连续绘制,可以在提交完成后把状态恢复为 idle。若要支持右键删除已经画好的圆,再加一个右键点击事件判断当前状态。
5.2 圆心拾取与半径计算
圆心坐标使用pickEllipsoid获取。这里要注意的是,pickEllipsoid返回的是三维世界坐标,不是经纬度,因此需要利用Cartographic.fromCartesian转成经纬度和高度。半径如果直接使用Cartesian3.distance计算,得到的是圆心到目标点的空间直线距离;范围小时误差不大,范围较大时建议用EllipsoidGeodesic计算椭球面上的测地距离,结果更接近真实地表距离。
_calcRadius(cartesian) { const cartographic = Cesium.Cartographic.fromCartesian(cartesian); const geodesic = new Cesium.EllipsoidGeodesic(this.centerCartographic, cartographic); return geodesic.surfaceDistance; }EllipsoidGeodesic.surfaceDistance得到的是沿椭球面的距离,单位是米。对于几千公里的跨区域圆形标注,这个误差控制远比平面公式可靠。如果只是半径 500 米到 5 公里的城市级圆形,用哪种算法差异已经非常小,可以在注释里说明。
5.3 动态预览实现
预览圆的实现方法是:第一次点击后添加一个半透明圆实体,圆心已经固定,半径初始为 0;鼠标移动事件触发时,不断用当前鼠标位置与圆心计算距离,并更新椭圆半轴。
_updatePreview(radius) { if (!this.previewEntity) return; this.previewEntity.ellipse.semiMajorAxis = Math.max(this.minRadius, radius); this.previewEntity.ellipse.semiMinorAxis = Math.max(this.minRadius, radius); }5.4 完整 CircleDrawTool 类
把绘制逻辑封装成一个类,内部管理事件和数据,外部只需要调用构造器和 start 方法。这样在 Vue、React 等框架里都能复用。
import * as Cesium from 'cesium'; export class CircleDrawTool { constructor(viewer, options = {}) { this.viewer = viewer; this.handler = null; this.previewEntity = null; this.centerCartesian = null; this.centerCartographic = null; this.radiusMeters = 0; this.mode = 'idle'; this.minRadius = options.minRadius || 1; this.material = options.material || Cesium.Color.RED.withAlpha(0.3); this.outlineColor = options.outlineColor || Cesium.Color.RED; this.outlineWidth = options.outlineWidth || 2; this.onDrawEnd = options.onDrawEnd || null; } start() { if (this.handler) return; const handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas); handler.setInputAction((movement) => { this._handleLeftClick(movement); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); handler.setInputAction((movement) => { this._handleMouseMove(movement); }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); handler.setInputAction(() => { this.cancel(); }, Cesium.ScreenSpaceEventType.RIGHT_CLICK); this.handler = handler; } stop() { if (!this.handler) return; this.handler.destroy(); this.handler = null; this.cancel(); } cancel() { if (this.previewEntity) { this.viewer.entities.remove(this.previewEntity); this.previewEntity = null; } this.mode = 'idle'; this.centerCartesian = null; this.centerCartographic = null; this.radiusMeters = 0; } _pickCartesian(screenPosition) { if (!Cesium.defined(screenPosition)) return null; return this.viewer.camera.pickEllipsoid(screenPosition, Cesium.Ellipsoid.WGS84); } _handleLeftClick(movement) { const cartesian = this._pickCartesian(movement.position); if (!cartesian) return; const cartographic = Cesium.Cartographic.fromCartesian(cartesian); if (this.mode === 'idle') { this.mode = 'drawing'; this.centerCartesian = cartesian; this.centerCartographic = cartographic; this.radiusMeters = 0; this.previewEntity = this._createPreviewEntity(cartographic.height); return; } if (this.mode === 'drawing') { this.radiusMeters = Math.max(this.minRadius, this._calcRadius(cartesian)); this._updatePreview(this.radiusMeters); this._commit(); } } _handleMouseMove(movement) { if (this.mode !== 'drawing') return; const cartesian = this._pickCartesian(movement.endPosition); if (!cartesian) return; this.radiusMeters = Math.max(this.minRadius, this._calcRadius(cartesian)); this._updatePreview(this.radiusMeters); } _calcRadius(cartesian) { const cartographic = Cesium.Cartographic.fromCartesian(cartesian); const geodesic = new Cesium.EllipsoidGeodesic(this.centerCartographic, cartographic); return geodesic.surfaceDistance; } _createPreviewEntity(height) { const ellipseOptions = { semiMajorAxis: 0, semiMinorAxis: 0, material: this.material, outline: true, outlineColor: this.outlineColor, outlineWidth: this.outlineWidth }; if (height !== undefined) { ellipseOptions.height = height; } return this.viewer.entities.add({ position: this.centerCartesian, ellipse: ellipseOptions }); } _commit() { const entity = this.previewEntity; this.previewEntity = null; const center = this.centerCartographic; const lng = Cesium.Math.toDegrees(center.longitude); const lat = Cesium.Math.toDegrees(center.latitude); const result = { entity, lng, lat, radius: this.radiusMeters, centerCartesian: this.centerCartesian }; this.mode = 'idle'; this.centerCartesian = null; this.centerCartographic = null; if (this.onDrawEnd) { this.onDrawEnd(result); } } }调用方式也很直接:
const circleTool = new CircleDrawTool(viewer, { material: Cesium.Color.ORANGE.withAlpha(0.25), outlineColor: Cesium.Color.ORANGE, onDrawEnd: (result) => { console.log('圆形完成', result.lng, result.lat, result.radius); } }); circleTool.start();此时在浏览器里可以看到,第一次点击出现圆心,鼠标拖动时圆形会跟随鼠标实时改变大小,第二次点击后圆形成型。右键点击可以取消当前未完成的绘制。
6. 交互增强与功能扩展
6.1 精确半径绘制
鼠标拖拽画出的圆半径不够精确,生产系统通常需要支持手动输入半径。增加一个addCircleFromLngLat方法即可,它能根据经纬度和半径直接生成圆实体,适合做双击定位、经纬度输入框联动和批量数据回显。
function addCircleFromLngLat(viewer, lng, lat, radiusMeters, options = {}) { const height = options.height !== undefined ? options.height : 0; const entity = viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(lng, lat, height), ellipse: { semiMajorAxis: radiusMeters, semiMinorAxis: radiusMeters, height, material: options.material || Cesium.Color.BLUE.withAlpha(0.25), outline: options.outline !== false, outlineColor: options.outlineColor || Cesium.Color.BLUE, outlineWidth: options.outlineWidth || 2 } }); return entity; }判断成功的标准是:在页面上输入经纬度 116.39、39.9 和半径 2000,能在对应位置看到半径为 2 公里的圆;如果使用底图比例尺对比,半径应该与地图测量结果基本一致。
6.2 图层管理
画完一个圆后,最怕用户在场景里找不到删除入口。工具类应该维护一个数组或 CustomDataSource,记录所有绘制完成的实体。CustomDataSource的好处是,圆可以按业务逻辑分组,支持统一显隐、删除和排序。
const circleDataSource = new Cesium.CustomDataSource('circleLayer'); viewer.dataSources.add(circleDataSource); function addCircleToLayer(lng, lat, radiusMeters) { return circleDataSource.entities.add({ position: Cesium.Cartesian3.fromDegrees(lng, lat), ellipse: { semiMajorAxis: radiusMeters, semiMinorAxis: radiusMeters, material: Cesium.Color.GREEN.withAlpha(0.3), outline: true, outlineColor: Cesium.Color.GREEN } }); } function clearCircleLayer() { circleDataSource.entities.removeAll(); }这样工具模块与业务模块解耦,CircleDrawTool 只负责画圆和修改临时状态,真正管理已绘制圆的是数据源。后面做图层树开关、删除当前选中圆、导出全部圆都变得很容易。
6.3 与 Vue3 / React 项目的集成
在 Vue3 项目里,CircleDrawTool 不应该放在组件内部每次重建。更稳妥的做法是:初始化 Cesium Viewer 之后,把 viewer 实例保存在一个单例模块中,CircleDrawTool 只在组件挂载后创建一次。
// circleStore.js export const circleState = { tool: null, viewer: null };组件卸载时,执行circleState.tool.stop(),销毁 ScreenSpaceEventHandler,避免事件监听污染。事件绑定过多次数最容易造成“点击一次后圆出现两三个”的问题,根因就是组件热更新或重复挂载没有执行 stop。
7. 批量绘制、数据接入与导出
7.1 批量圆心数据加载
实际项目不会只画一两个圆。后端接口返回一组业务对象后,前端需要把这些数据统一渲染到场景中。批量加载的套路是先创建统一数据源,再循环 add。
async function loadCircleData(viewer, apiData) { const dataSource = new Cesium.CustomDataSource('businessCircles'); viewer.dataSources.add(dataSource); apiData.forEach((item) => { dataSource.entities.add({ position: Cesium.Cartesian3.fromDegrees(item.lng, item.lat, item.height || 0), ellipse: { semiMajorAxis: item.radius, semiMinorAxis: item.radius, height: item.height || 0, material: Cesium.Color.fromCssColorString(item.color || '#3388ff').withAlpha(0.25), outline: true, outlineColor: Cesium.Color.fromCssColorString(item.color || '#3388ff'), outlineWidth: 2 } }); }); return dataSource; }接口返回的数据字段可以与后端约定为{ lng, lat, radius, color, height }。这里的radius单位是米,与 Cesium 的semiMajorAxis保持一致,避免出现单位换算错误。
7.2 圆形转 GeoJSON 导出
用户画完圆之后,如果只是停留在画面上,数据无法进入业务系统。让绘图工具具有导出能力,才能真正完成“标绘 - 存储 - 回显”闭环。圆的存储通常转成圆形边界多边形,后端保存这个多边形即可。
function exportCircleToGeoJSON(lng, lat, radiusMeters, segments = 64) { const coordinates = []; for (let i = 0; i <= segments; i++) { const bearing = (i / segments) * Math.PI * 2; const deltaLat = (radiusMeters * Math.cos(bearing)) / 111320.0; const deltaLng = (radiusMeters * Math.sin(bearing)) / (111320.0 * Math.cos((lat * Math.PI) / 180.0)); coordinates.push([lng + deltaLng, lat + deltaLat]); } coordinates.push(coordinates[0]); return { type: 'Feature', properties: { center: [lng, lat], radius: radiusMeters }, geometry: { type: 'Polygon', coordinates: [coordinates] } }; }上面的代码使用的是球面近似公式,适合百公里以内的圆形导出,注释必须标注精度适用边界。如果项目对精度要求很高,应改用 Cesium 的几何计算把圆采样成一组边界点,再落库。导出成功后,通常还需要一个“清除画布”按钮,避免重复画圆导致数据源膨胀。
7.3 从后端读取已保存圆形
读取已保存圆的核心是坐标解析。后端返回的如果是 GeoJSON,前端可以自己遍历 FeatureCollection,跳过非 Polygon 类型,把 center 和半径恢复成 Cesium 圆实体。如果后端只保存了多边形坐标,前端需要计算多边形外接圆圆心和半径,这一步更复杂,建议业务接口直接约定保存 center 和 radius,必要时同时保存 GeoJSON,以简化成本。
8. 渲染性能与资源占用观察
Cesium 是 WebGL 应用,浏览器显存占用会随着场景实体、纹理、地形变化而波动。圆形绘制工具本身只产生少量几何体,正常情况下不会成为性能瓶颈,但以下三个问题值得关注。
第一,动态预览更新频率。鼠标移动事件在绘制阶段会高频触发,每次移动都要更新椭圆半轴。鼠标在场景上快速滑动时,事件频率可能达到每秒几十次,这种频率下不能执行复杂计算。我在 CircleDrawTool 中的_calcRadius直接使用EllipsoidGeodesic计算,几十次计算完全可接受;但如果把大量 DOM 操作或请求逻辑放进鼠标移动事件,帧率会明显下降。
第二,实体数量。几十个圆形用 Entity 很轻松,几百个也基本流畅,上千个就需要观察帧率。鼠标拖拽场景、旋转视角时,如果实体调用次数过高,应该把静态圆切换到 Primitive。Cesium 官方性能分析里,Entity 适合中小数量,Primitive 适合大批量复用外观。
第三,图层清理。圆形的 Outline 和半透明材质都会产生额外的渲染指令。多次重复点击“绘制”却不清理已经提交的实体,会导致 Draw Call 不断增加。开发阶段可以在requestAnimationFrame回调里临时输出viewer.scene.primitives.length和viewer.entities.values.length,确认实体数量是否符合预期。
function checkSceneStatus(viewer) { console.log('entities:', viewer.entities.values.length); console.log('primitives:', viewer.scene.primitives.length); console.log('groundPrimitives:', viewer.scene.groundPrimitives.length); }浏览器 DevTools 的 Performance 面板可以用来录制鼠标移动时的耗时。如果某段脚本执行时间明显超过 16ms,说明绘制逻辑里出现了高耗时操作,要优先排查事件回调内是否做了多余的坐标反算或数据源查询。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 点击不画圆,也没有预览实体 | pickEllipsoid 返回 undefined,说明点击位置没有落在椭球表面 | 在点击回调内打印 movement.position 和 pickEllipsoid 返回值 | 先判断返回值,为空直接 return;关闭场景中鼠标选中干扰,或使用 scene.pickPosition 替代 |
| 圆形总是吸附到某个中心点 | 圆心坐标使用旧版本或重复点击后中心点没有清空 | 检查 mode 状态和中心点重置逻辑 | 在 cancel 和 stop 中统一重置 centerCartesian、centerCartographic |
| 圆不是正圆,而是椭圆 | semiMajorAxis 与 semiMinorAxis 不一致,或数据单位不是米 | 查看代码中 ellipse 两个半轴赋值 | 画圆时两个半轴都赋 radius,不要单独改其中一个 |
| 半径差很大,和底图比例尺对不上 | 使用 Cartesian3.distance 计算三维直线距离导致大范围误差 | 对半径 100 km 以上的圆做对比测试 | 改用 EllipsoidGeodesic.surfaceDistance 计算椭球表面距离 |
| 圆沉入地形或飘在空中 | height 设置不正确,或底图地形起伏较大 | 检查圆心 Cartographic.height 和 terrainProvider | 需要贴地时使用 heightReference: CLAMP_TO_GROUND,或先处理地形拾取坐标 |
| 连续点击出现多个重复圆 | ScreenSpaceEventHandler 未销毁或重复绑定 | 在控制台打印 handler 是否已存在 | start 方法里判断 handler 是否已创建,组件卸载时调用 stop 销毁事件 |
| 清除圆后页面出现残留半透明色块 | Entity 删除后 Primitive 或材质缓存未释放,或存在隐藏 entity | 使用 viewer.entities.removeAll 并检查 scene.primitives | 对非 Entity 对象,需要调用 primitive.destroy() 释放 WebGL 资源 |
| 高分辨率屏幕上拾取坐标偏移 | 没有适配 devicePixelRatio,或 Canvas 尺寸与 CSS 尺寸不一致 | 检查浏览器窗口大小和 Canvas 尺寸 | 使用 Cesium 默认的像素坐标处理,避免手动换算 window.innerWidth |
排查时最有效的方式是把事件回调里的关键数据打出来。第一次点击后打印 centerCartographic,移动鼠标时打印 radiusMeters,第二次点击后打印提交的实体。看到哪一步数据断掉,就能判断是事件问题、坐标问题还是渲染问题。
10. 最佳实践与下一步建议
从这个最基础的 Circle 绘制工具出发,后续可以按以下方向迭代。
第一,形成统一标绘工具链。把 CircleDrawTool 抽出来的状态管理、事件绑定、图层管理逻辑应用到矩形、多边形、折线、箭头等工具中。每个工具都只负责“绘制交互”,绘制完成的结果统一提交到同一个标绘图层,业务侧不感知当前是圆还是多边形。
第二,完善撤销与回显能力。给每个绘制完成的实体分配唯一 ID,维护一个已绘制数组;删除时不仅移除实体,还要从数组中移除。这样配合后端批量接口就能实现一次加载、多次回显。
第三,建立数据精度约定。前端绘图圆形的半径单位统一为米,圆心坐标统一为 WGS84 经纬度。后端存储时保留 center 和 radius 两个字段,同时提供 GeoJSON 导出接口给其他 GIS 系统使用。
第四,接入贴地和 WebGL 资源释放场景前,先做性能压测。不要直接在一个已经加载了大量倾斜摄影、BIM、点云数据的 Cesium Viewer 里裸跑包含上千个 Entity 圆形的业务,应该先用批量接口和CustomDataSource验证交互流程,确认帧率可接受再上线。
Circle 工具是整个 Cesium 标绘体系里的入门组件,但它涉及的问题非常完整:屏幕坐标转地理坐标、动态实体更新、角度和距离的单位、图层生命周期、事件销毁、数据导出。把这些坑提前摸一遍,后面开发更复杂的标绘功能会顺畅很多。建议在本地直接创建一个 Vite 项目,把上面的 CircleDrawTool 类粘贴进去跑一圈,先观察动态预览效果,再逐步加入精确半径和 GeoJSON 导出,就能形成一套可复用的圆形标绘模块。