
“绘图工具”在 Cesium 项目里几乎是标配但真把模块拆开裁剪时很多团队会发现画点、画线容易画矩形也容易偏偏“椭圆”这个看似简单的图形落进 Cesium 之后会遇到一堆坐标换算和交互设计问题。这次我们就聚焦Cesium绘图工具 - Ellipse讲清楚两件事一个椭圆实体是怎么画出来的以及一个能交给同事、能接业务数据的椭圆绘制工具该怎么封装。先不聊概念。直接说结论在 Cesium 里绘制椭圆最有性价比的方案是使用 Entity API。长半轴和短半轴分别设置为semiMajorAxis和semiMinorAxis位置用Cesium.Cartesian3.fromDegrees给出再补上材质、轮廓、高度这几个属性一个可用椭圆就能显示。若要做到“点一个点、拖一下鼠标、右键结束”这种地图语义化绘制则要自己基于ScreenSpaceEventHandler做交互状态管理Cesium 没有内置一套开箱即用的“绘图工具”官方更多是提供基础实体和几何计算能力。本文会按“核心能力 - 场景边界 - 环境准备 - 静态实体 - 交互绘制 - 数据批量封装 - 常见问题 → 工程建议”的顺序展开所有代码都以 CesiumJS 为基准。如果你正在做地理围栏、缓冲区分析、范围标注、可视域范围模拟或者仅仅是想把项目里“椭圆绘制”这个功能从能用做到可维护这篇文章可以直接收藏。1. 核心能力速览能力项说明项目归属Cesium 绘图模块中的 Ellipse 图形绘制能力底层依赖CesiumJS Entity API、ScreenSpaceEventHandler、Transforms、Ellipsoid主要功能静态椭圆创建、鼠标交互绘制、椭圆参数读取、批量椭圆图层加载可配置参数中心坐标、半长轴、半短轴、旋转角、高度、材质、轮廓、是否贴地支持平台现代浏览器支持 Vite / Webpack / CDN 引入 CesiumJS启动方式npm 启动前端或直接在 HTML 中引入 CesiumJSAPI 能力可封装为drawEllipse()、createEllipseLayer()等多业务接口批量能力椭圆参数数据具有循环调用能力量大时建议切换 Primitive 渲染操作复杂度中等。需要理解坐标拾取、经纬度与地面距离之间的换算适合读者Cesium 场景初始化还能跑通但绘图模块尚未落地的前端开发者这个模块的定位很清晰它不是“原理研究”而是“能直接写进工程的功能沉淀”。所以下面所有内容都偏向可用、可测、可复用的实现方式。2. 适用场景与使用边界2.1 典型适用场景椭圆在 Cesium 里的表现形式通常是“贴地平面或指定高度的面状体”不是一个立体球体。所以它的典型使用场景包括地图范围内的圆形缓冲简化显示比如地震影响半径、管线安全范围用椭圆示意某些形状不规则的关联区域例如临空管制区局部范围地理围栏叠加在中心点位置不动、径向不同时用椭圆区分不同影响半径辅助可视域分析、雷达扫描范围、水面波动影响区等需要“以中心点向外扩散”的表达只不过这些场景往往还需要配合动态材质或额外标绘。2.2 不适合什么场景先划清边界后面写代码才不容易返工。如果你要表达的是“一个真正沿椭球面高度起伏、或跟随地形的曲面椭圆”Entity 的ellipse不够需要结合GroundPrimitive和几何实例还要处理地形分类开发量会明显变大。如果你要的是“像三维建模软件里那样的空间椭圆环”也不是 Entity ellipse 的活要用EllipseOutlineGeometry或者自定义几何体。如果你有大量椭圆需要同时渲染比如超过几千个直接用 Entity 会产生大量 DrawCommand不建议硬扛优先考虑分类、聚合或切换到 Primitive。3. 环境准备与前置条件3.1 推荐项目结构这篇内容以 JavaScript 示例为主。用 Vite 还是 Webpack 不关键但建议项目结构至少分三块“地图视图”、“绘图工具”、“业务参数解析”。不要把所有代码都堆到一个App.vue或index.js里。我建议的目录结构是cesium-ellipse-demo/ ├─ index.html ├─ package.json ├─ src/ │ ├─ main.js │ └─ map/ │ ├─ viewer.js │ ├─ ellipseStatic.js │ └─ ellipseDrawTool.jsviewer.js负责 Cesium.Viewer 初始化ellipseStatic.js负责直接根据业务参数生成椭圆ellipseDrawTool.js负责鼠标交互绘制。3.2 CesiumJS 引入如果使用 npm 安装 CesiumJS基础安装命令如下npm install cesium然后在入口文件里引入 Cesium 和样式import * as Cesium from cesium; import cesium/Build/Cesium/Widgets/widgets.css;如果你的项目还在用比较老的 Webpack 配置需要额外处理 Cesium 静态资源和 worker如果使用 Vite建议配合社区常见的 Cesium 插件处理资源路径。更简短的验证方式是直接用 HTML 引入打包后的 JS但不建议长期用于工程化项目。3.3 初始化 Viewer无论静态绘制还是交互绘制都需要一个已经初始化成功的 Viewerconst viewer new Cesium.Viewer(cesiumContainer, { baseLayerPicker: false, geocoder: false, animation: false, timeline: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false }); viewer.scene.globe.enableLighting false;这里把默认组件关掉一部分是为了让后续表单和绘图交互更清晰。如果只是快速验证保持默认全部开启也没问题。4. 静态椭圆绘制与环境验证4.1 基础代码先从一个最直接的 Entity 椭圆开始。假设椭圆中心点在北京附近经度 116.39、纬度 39.9长半轴 300 米短半轴 120 米const ellipseEntity viewer.entities.add({ name: 业务椭圆示例, position: Cesium.Cartesian3.fromDegrees(116.39, 39.9), ellipse: { semiMajorAxis: 300.0, semiMinorAxis: 120.0, height: 0, material: Cesium.Color.fromCssColorString(#00b4ff).withAlpha(0.4), outline: true, outlineColor: Cesium.Color.WHITE, outlineWidth: 2 } }); viewer.flyTo(ellipseEntity);这段代码跑通后页面会自动飞行到椭圆附近并显示一个半透明的蓝色椭圆面。重点说明几个参数semiMajorAxis和semiMinorAxis的单位是米。注意它们不是经纬度差值而是真实空间距离。height: 0表示椭圆画在地球椭球表面高度 0 米处。如果中心位置较高或需要贴现状地形需要用heightReference。rotation如果不设置默认按坐标方向排布通常接近“东西方向为长半轴”的表现。4.2 贴地模式在三维场景中如果业务需要椭圆贴合地形表面最直接的做法是设置ellipse: { semiMajorAxis: 300.0, semiMinorAxis: 120.0, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, classificationType: Cesium.ClassificationType.BOTH, material: Cesium.Color.fromCssColorString(#00b4ff).withAlpha(0.35), outline: true, outlineColor: Cesium.Color.WHITE }贴地椭圆在原理上属于“分类”绘制它依赖地形和纹理分类机制不能只是把height改成0。如果中心区域当前地形是山地使用CLAMP_TO_GROUND能得到贴合地表的结果但要注意这种情况下无法再像普通 Entity 那样随意设置高度。4.3 视觉表现中的常见组合在 Cesium 绘图工具里椭圆往往要和中心点标记、动态边框同步。常用组合是viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0), point: { pixelSize: 10, color: Cesium.Color.YELLOW } }); viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0), ellipse: { semiMajorAxis: 300, semiMinorAxis: 120, material: Cesium.Color.CYAN.withAlpha(0.2), outline: true, outlineColor: Cesium.Color.CYAN } });这种点加面的结构比单独显示一个面更贴近“可标绘、可测量”的工具形态。后面的交互绘制也是基于这种组合来设计。5. 交互式椭圆绘制工具静态 Entity 只是“数据展示”而标题里强调“绘图工具”必须解决“用户如何通过鼠标画出一个椭圆”的问题。Cesium 官方没有提供大家默认统一的drawEllipse方法因此此处需要自行封装交互状态机。5.1 简单但有效的交互协议很多研发团队在封装椭圆交互时习惯把流程定成三次左键点击第一次点击确定中心点第二次点击确定长半轴方向与长度第三次点击确定短半轴长度。这样做旋转角度自然但代码实现起来要维护较多临时状态。考虑到项目要快速落地以下给出一个更常用的协议参数面板里输入长半轴、短半轴、旋转角度鼠标只需要确定中心点。地图上第一次左键点击就把椭圆创建出来下一次点击继续创建下一个椭圆。这样既避免鼠标移动过程中对“半轴方向”的歧义也方便后续接入业务表单。5.2 交互工具代码骨架在ellipseDrawTool.js中实现import * as Cesium from cesium; export class EllipseDrawTool { constructor(viewer, options {}) { this.viewer viewer; this.handler null; this.enabled false; this.options Object.assign({ semiMajorAxis: 200, semiMinorAxis: 80, rotation: 0, height: 0, material: Cesium.Color.fromCssColorString(#00b4ff).withAlpha(0.35), outlineColor: Cesium.Color.WHITE, onDrawEnd: null }, options); } enable() { if (this.enabled || !this.viewer) return; this.enabled true; this.handler new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas); this.handler.setInputAction((movement) { const position this.pickGlobePosition(movement.position); if (!position) return; this.createEllipseAt(position); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); // ESC 取消激活状态 document.addEventListener(keydown, this.onKeyDown); } disable() { if (!this.enabled) return; this.enabled false; if (this.handler) { this.handler.destroy(); this.handler null; } document.removeEventListener(keydown, this.onKeyDown); } onKeyDown (e) { if (e.key Escape) { this.disable(); } }; pickGlobePosition(screenPos) { const ray this.viewer.camera.getPickRay(screenPos); if (!ray) return null; const target this.viewer.scene.globe.pick(ray, this.viewer.scene); return target || this.viewer.camera.pickEllipsoid(screenPos, this.viewer.scene.globe.ellipsoid); } createEllipseAt(position) { const entity this.viewer.entities.add({ position, ellipse: { semiMajorAxis: this.options.semiMajorAxis, semiMinorAxis: this.options.semiMinorAxis, rotation: Cesium.Math.toRadians(this.options.rotation), height: this.options.height, material: this.options.material, outline: true, outlineColor: this.options.outlineColor } }); if (typeof this.options.onDrawEnd function) { this.options.onDrawEnd(entity, position); } return entity; } }这个实现的关键点有两个。第一是坐标拾取。scene.globe.pick返回的是“射线与当前地形或 3D Tiles 表面的交点”适合点击地面画图camera.pickEllipsoid则适合在无地形或海洋区域作为兜底。两者结合能减少空白处点击无效的情况。第二是状态管理。enable/disable必须成对出现。很多项目踩坑不是椭圆画不出来而是绘图模式激活后没有关闭ScreenSpaceEventHandler导致鼠标左键一直被吞其他工具无法使用。5.3 实时预览思路上面的版本是“点击一下立刻生成椭圆”。如果产品经理要求“鼠标移动时能看到预览”那么需要引入额外一个临时 Entity并在MOUSE_MOVE事件里更新它的半轴参数。核心结构如下let tempEntity null; let startCartesian null; handler.setInputAction((movement) { const cartesian pickGlobePosition(movement.position); if (!cartesian) return; if (!startCartesian) { startCartesian cartesian; tempEntity viewer.entities.add({ position: cartesian, ellipse: { semiMajorAxis: 1, semiMinorAxis: 1, material: Cesium.Color.CYAN.withAlpha(0.3), outline: true } }); } else { // 更新预览半径 const distance Cesium.Cartesian3.distance(startCartesian, cartesian); const major Math.max(distance * 0.6, 1); const minor Math.max(distance * 0.3, 1); tempEntity.ellipse.semiMajorAxis major; tempEntity.ellipse.semiMinorAxis minor; } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);这里把鼠标移动距离简化映射为长半轴的 0.6 倍、短半轴的 0.3 倍只是一个方便产品体验的预览方案。真实业务里如果需要精确还原鼠标拖拽位置不能直接拿 3D 距离除一个系数应该把鼠标点投影到以中心点为原点的局部切平面坐标系中再用局部坐标的x和y去赋值两个半轴。这是从“演示代码”走向“业务可用代码”的关键一步。5.4 封装对外接口实际工程中绘图功能不应该直接开放viewer.entities.add给业务页面。更好的做法是把工具类收缩成方法export function drawEllipseByMouse(viewer, ellipseParams) { const tool new EllipseDrawTool(viewer, { semiMajorAxis: ellipseParams.semiMajorAxis, semiMinorAxis: ellipseParams.semiMinorAxis, rotation: ellipseParams.rotation, onDrawEnd: (entity, position) { const carto Cesium.Cartographic.fromCartesian(position); console.log({ lng: Cesium.Math.toDegrees(carto.longitude), lat: Cesium.Math.toDegrees(carto.latitude), semiMajorAxis: ellipseParams.semiMajorAxis, semiMinorAxis: ellipseParams.semiMinorAxis, rotation: ellipseParams.rotation }); } }); tool.enable(); return tool; }这么做的价值在于业务页面只需要调用drawEllipseByMouse不关心地图内部是 Entity 还是 Primitive不关心坐标拾取如何实现后续要换底图、换投影或增加权限控制也只在工具内部改。6. 基于数据的批量椭圆生成绘图工具不只服务人工鼠标点击更多场景是读取后端数据后批量生成一批椭圆。例如服务器返回多个区域的“中心点 长半轴 短半轴 旋转角”前端需要在地图上一次性展示。6.1 定义一个特征数据结构建议和后端约定如下结构{ features: [ { center: [116.39, 39.9], semiMajorAxis: 300, semiMinorAxis: 120, rotation: 30, height: 0, color: #00b4ff }, { center: [116.41, 39.92], semiMajorAxis: 500, semiMinorAxis: 180, rotation: 60, height: 0, color: #ff8800 } ] }这个结构把几何参数和颜色放在一起前端不需要为每个字段单独解析循环生成即可。6.2 批量生成代码export function addEllipseFeatureLayer(viewer, featureData) { const entities []; featureData.features.forEach((item) { const entity viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(item.center[0], item.center[1]), ellipse: { semiMajorAxis: item.semiMajorAxis, semiMinorAxis: item.semiMinorAxis, rotation: Cesium.Math.toRadians(item.rotation || 0), height: item.height || 0, material: Cesium.Color.fromCssColorString(item.color).withAlpha(0.35), outline: true, outlineColor: Cesium.Color.WHITE } }); entities.push(entity); }); if (entities.length 0) { viewer.flyTo(entities); } return entities; }6.3 批量数据的渲染优化如果一个图层里的椭圆数量只有几十个Entity 方案没有任何问题。但如果是上千个甚至更多就要评估是否改用 Primitive 渲染原因是 Entity 本身带有数据管理和事件能力开销相对大。从工程经验看更稳妥的优化顺序是先确认数量级。使用场景中 100 个以内的椭圆优先使用 Entity代码可读性好排查问题快。如果数量很多且图形不发变化考虑把椭圆构造成几何实例一次性提交给PrimitiveCollection。如果椭圆需要按帧更新位置或尺寸避免重建几何体最好把更新对象缩小到可变化参数而不是直接销毁后重新添加。7. 性能观察与资源占用这部分是很多同学习惯性忽略的点。文字说“观察”但实际开发时具体资源占用需要使用浏览器 DevTools 验证。打开浏览器开发者工具选择 Performance 面板后在地图上旋转、平移可以看到每一帧的渲染耗时。临时实体在交互过程中的高频更新会导致 CPU 持续计算如果明显卡顿优先检查MOUSE_MOVE回调里是否做了太多坐标拾取和对象创建。ScreenSpaceEventHandler本身不是性能瓶颈真正的性能瓶颈通常在“每个鼠标移动事件都要重新计算半轴、重新创建 Entity”。应只更新一个预览 Entity 的参数而不是新建 Entity。显存和显卡资源这里无法给出固定值因为 Cesium 的渲染压力与地形、影像、模型节点、屏幕分辨率强相关。但可以确定的是单个平面椭圆面片由 GPU 绘制面数不高绝大多数普通商务本都能跑会带来明显压力的往往是地图底图、倾斜摄影、模型节点数量而不是椭圆本身。在实际测试环境里要重点观察三点绘制过程中拖动是否顺滑、切换工具后鼠标事件是否仍然遗留、清除椭圆后viewer.entities长度是否归零。后两者往往比帧率更容易暴露问题。8. 常见问题与排查方法问题现象可能原因排查方式解决方案地图只有天空看不到椭圆中心点坐标或高度设置错误视图未飞到目标范围用console.log输出椭圆 Entity 参数检查 position 是否正常调整viewer.flyTo(entity)或用官方坐标查询工具确认坐标范围椭圆半轴无效semiMajorAxis/semiMinorAxis传了字符串或 0打印参数类型检查后端返回字段使用Number()强转并设置大于 0 的默认值椭圆没有贴地仍浮在 0 高度只设置了height没有设置heightReference和classificationType查看 Entity 对应 graphics 属性改为CLAMP_TO_GROUND并设置classificationType绘图工具开启后其他地图工具无法点击ScreenSpaceEventHandler未销毁或事件重复注册查看是否有多个 handler 同时监听左键在disable中调用handler.destroy()点击地貌后位置偏差大使用了camera.pickEllipsoid没有使用globe.pick拾取地形在点击事件中打印拾取结果的高度改成射线拾取与球体拾取结合的方案旋转角度没有生效直接把业务字段里的角度数值传给rotation但 Cesium 需要弧度检查渲染效果与参数值调用Cesium.Math.toRadians(angle)后再赋值批量生成的椭圆颜色相互遮挡半透明材质叠加后视觉不可控降低透明度或关闭轮廓统一使用同色系透明度重要椭圆单独高亮输出到 GeoJSON 后经纬度丢失只保存了 Entity 对象没有转成 Cartographic检查保存逻辑从position中获取 Cartesian3再用Cartographic.fromCartesian转为经纬度9. 最佳实践与使用建议9.1 先定义数据模型再写绘图逻辑绘图工具的底层是交互但交互结束后产物是什么一定是数据。建议在写地图代码前先定义椭圆参数对象const ellipseFeature { lng: 0, lat: 0, semiMajorAxis: 0, semiMinorAxis: 0, rotationDegree: 0, height: 0, metadata: {} };这样无论鼠标画、表格导入、还是接口返回都能在同一个数据管线里处理。9.2 给临时绘制的 Entity 做统一分组如果把绘图结果直接加到viewer.entities全局集合里后期删除、隐藏、导出时都要遍历全量实体容易误删底图相关对象。建议使用CustomDataSourceconst dataSource new Cesium.CustomDataSource(ellipse-layer); viewer.dataSources.add(dataSource); function addEllipseToLayer(entityOptions) { return dataSource.entities.add(entityOptions); } function clearEllipseLayer() { dataSource.entities.removeAll(); }这样“清空椭圆图层”的操作不会影响其他标注或模型。9.3 属性合法性校验要前置在 Cesium 的绘图工具中最容易出现“前端画好了、保存数据后后端检查失败”的情况。因此交互结束弹窗或编辑框内应至少校验长半轴、短半轴是否大于 0中心点经度是否在-180 到 180范围纬度是否在-90 到 90范围旋转角度是否为合法数值可选高度是否在合理范围内。9.4 权限与数据安全边界椭圆绘制常用于地理围栏和影响范围模拟这类业务往往涉及场所位置、管线路径或人员活动范围等敏感信息。演示环境可以使用公开测试坐标生产环境则必须从服务端按权限下发中心点数据和半轴参数不能把地图上所有标绘结果直接暴露给无权限用户。涉及地面影像、倾斜摄影模型或其他带有版权标识的底图时不要在公开页面随意展示未经授权的数据图层。10. 总结与下一步回到这个Cesium绘图工具 - Ellipse的能力本身静态绘制用 Entity 的 ellipse交互绘制需要封装 ScreenSpaceEventHandler批量数据用 CustomDataSource 统一管理数量巨大的场景再考虑 Primitive。整个过程并不复杂也没有必须使用特定版本的特殊 API。建议第一次动手时先跑通“静态 Entity 椭圆”确认坐标显示正常后再增加鼠标点击创建机制。最容易踩的坑集中在两个地方一个是没有把rotation从角度转成弧度另一个是点击地形时没有正确拾取到地面交点导致椭圆位置漂移。下一步可以继续扩展的方向很多比如把椭圆绘制工具与可视域分析、地理围栏编辑、图层权限校验、动态材质显示结合。如果这篇 Cesium 绘图工具实战对你有帮助建议收藏备用下一篇可以继续展开矩形、圆柱体或动态光照相关的 Cesium 绘图模块。