ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Cesium椭圆绘图工具开发:从鼠标交互到参数计算

Cesium椭圆绘图工具开发:从鼠标交互到参数计算 Cesium 里画一个 Ellipse 并不难一句话就能创建一个椭圆实体。但真正做“绘图工具”时问题从来不是 API 调不出来而是用户用鼠标在地图上操作时中心点、长半轴、短半轴、旋转角这些数据应该怎么一步一步确定下来以及如何把最终结果交还给业务层。围绕“Cesium 绘图工具 - Ellipse”这个话题最值得先看的不是某一个接口而是一个能交互、能动态预览、能输出结构化管理数据的小工具应该怎样设计。这篇内容适合正在做 Cesium 标绘、地理围栏、空间分析选区、雷达覆盖范围等功能的开发者我会直接按可落地的方案拆开讲。先说结论把 Ellipse 做成绘图工具关键不是数据怎么画而是交互状态怎么管。绘图工具一般需要经过一个“点击开始、移动预览、再次点击完成”的过程期间要不断从前端鼠标位置换算成地球上的坐标再换算成椭圆的半轴参数。最容易踩的坑也在这里拾取球面坐标、区分地形高度、处理旋转方向、清理临时实体和事件监听器任意一个没做好工具就会变成“有时能画有时画歪有时报错”。1. 先搞清楚 Ellipse 工具在 Cesium 里要解决什么1.1 别把 API 使用等同于工具开发Cesium 中的椭圆并不是一张图片也不是一个贴图素材它是基于椭球体数学计算出的几何图形。你在代码里需要给 Cesium 至少提供几个信息一个中心点一条长半轴一条短半轴可能还有一个旋转角。Cesium 拿到这些参数后会在本地坐标系里生成椭圆边界并最终绘制在地球表面或指定高度上。这个机制本身很清晰。但用户使用绘图工具时并不会有耐心去控制台里填 semiMajorAxis 和 semiMinorAxis。用户的操作习惯往往是“我在地图上点一下鼠标往外拉看到形状差不多了再点一下完成”。所以工具层要做的最核心工作就是把鼠标动作实时翻译成 Cesium 能理解的一组椭圆参数。很多开发者第一次做 Ellipse 绘图时只写了一个静态 Entity 出来然后发现这根本不是工具只是一个“显示样例”。真正的工具需要有状态等待开始、绘制中、预览中、编辑中、已完成还要有回调和清理机制。1.2 一个绘图工具需要拆成三个能力如果要把 Ellipse 做成一个可复用的模块我建议先拆成三块输入采集处理鼠标点击、移动、右键取消等事件并把屏幕像素位置转换成地球上某个 Cartesian3 坐标。参数计算根据操作点计算中心点、半长轴长度、半短轴长度、旋转角并实时更新预览实体。结果输出用户在完成动作后把最终生成的椭圆参数统一交给外部回调由业务方决定是保存到数据库、添加到列表中还是继续叠加其他样式。这三个能力最好不要混在一个函数里。否则工具一复杂比如下一步要支持矩形、多边形、圆形等多图元叠加时代码会膨胀得非常快。2. Cesium 的椭圆从哪里来Entity、Geometry 和地面刷新选型2.1 三种实现方式分别是什么Cesium 中做椭圆通常有几个路径方式典型 API适合场景特点Entity 图形viewer.entities.addEllipseGraphics快速开发、需要编辑属性、需要和 popup 联动写法简单属性容易动态修改PrimitiveCesium.EllipseGeometryPrimitive大量数据、需要精细控制渲染性能好但代码量和内存管理成本高GroundPrimitiveEllipseGeometry配合地面几何体希望椭圆贴地并能参与地形遮罩或 3D Tiles 分类对数据和地形场景要求更高初始化逻辑容易出问题从“工具”这个角度讲大部分人更适合 Entity 方式。原因是绘图过程中需要反复修改半长轴、半短轴、旋转角Entity 的 Property 可以直接在帧循环中求值调试起来也直观。2.2 我推荐先按 Entity 方式做我一般不会一上来就用 Primitive 实现绘图工具。除非你的业务明确要求一次渲染几千个椭圆否则 Entity 方式的性能已经完全够用。Entity 的另一个好处是可以直接绑定属性状态。比如绘制过程中可以用一个临时 Entity 作为预览颜色半透明等到绘制完成再把参数传给另一个正式 Entity颜色加深加一个 outline。这个切换过程非常自然。Primitive 方式当然也能做到但需要手写 Geometry 更新、矩阵变换、实例销毁对工具类模块来说维护成本偏高。所以下面所有代码思路都先按 Entity EllipseGraphics 来实现。3. 先跑一个最小可运行场景手动指定参数的 Ellipse3.1 准备 Viewer 环境在写绘图工具之前先把基础的 Cesium 场景搭起来。无论你是用原生 HTML 页面还是基于 Vue、React 接入最终都需要一个 Cesium.Viewer 实例。const viewer new Cesium.Viewer(cesiumContainer, { animation: false, timeline: false, baseLayerPicker: false, geocoder: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false });如果只是本地演示不需要专门配置地形服务。但要注意不加载地形时默认地球是一个椭球体鼠标点击后可以很容易通过pickEllipsoid拿到球面坐标。如果加载了地形视觉上看到的山体并不是椭球表面这时还需要额外处理地形高度。建议前期先关掉地形把流程跑通再逐步接入地形场景。3.2 手动创建一个 Ellipse Entity下面这段代码会生成一个中心点在[114.3, 30.6]附近的椭圆。半长轴 1200 米半短轴 600 米方向默认对齐本地坐标轴。const ellipseEntity viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(114.3, 30.6), ellipse: { semiMajorAxis: 1200, semiMinorAxis: 600, material: Cesium.Color.RED.withAlpha(0.4), outline: true, outlineColor: Cesium.Color.WHITE, height: 0 } }); viewer.flyTo(ellipseEntity);这一步跑通之后你应该能看到一个扁平的地面椭圆。从代码可以看出所谓的“Ellipse 数据”本质上就是中心点加两个半径。那绘图工具要做的就是让用户通过鼠标把这个参数“画”出来。4. Ellipse 绘图工具的实现思路两点击外接矩形4.1 为什么选择“两点击外接矩形”方案地理绘图工具里椭圆的交互方式有很多种。最常见的是中心点加半径先点中心再拉一条半径。外接矩形先点一个角再拖出另一个对角形成一个外接矩形椭圆内切于矩形。中心点加长轴方向加短轴长度类似 CAD 绘图操作比较复杂。我建议新手工程先实现“两点击外接矩形”方案。这是一个很稳妥的折中方案一次点击确定起点移动鼠标时动态显示预览第二次点击完成。用户不需要理解半轴和旋转角这些概念只需要像画矩形一样拖动最终程序把矩形的宽高换算成半长轴和半短轴。这种交互对没有专业测绘背景的用户最友好。外接矩形方式还有另外一个好处后续如果要在同一个工具库中增加 Rectangle、Circle、Polygon 等图元交互模型可以保持一致不至于每个图形都维护一套完全独立的鼠标逻辑。4.2 屏幕坐标拾取地球坐标实现绘图工具时最容易出问题的是“鼠标点在地图上怎么拿到地理位置”。Cesium 中常用的是viewer.camera.pickEllipsoid。它接收一个窗口坐标返回鼠标射线与椭球体交点对应的 Cartesian3。如果鼠标指向天空或者射线没有和地球相交返回结果会是 undefined所以代码里一定要做判空。function pickEarthPosition(viewer, screenPos) { const cartesian viewer.camera.pickEllipsoid( screenPos, viewer.scene.globe.ellipsoid ); if (Cesium.defined(cartesian)) { return Cesium.Cartesian3.clone(cartesian); } return null; }这里不建议在裸球场景直接用scene.pickPosition因为如果没有深度缓冲pickPosition可能拿到空值或错误值。先要判断你的场景里是否有地形、要素、模型等产生深度信息。裸球数据用pickEllipsoid更稳定。4.3 外接矩形如何换算成椭圆半轴现在有一个起点startPoint和一个当前指针位置currentPoint。它们代表的是外接矩形的两个对角点。按常规理解外接矩形的水平宽度是起点和当前点在东西方向上的差高度是南北方向上的差中心就是两个点的中点。把这个矩形内切出一个椭圆后椭圆的半长轴就是矩形宽度的一半半短轴就是矩形高度的一半。不过这里不能简单用经度、纬度直接相减因为纬度一度和经度一度在地球上对应的地面长度不同越是高纬度地区偏差越大。所以要先把两个点转到中心点的本地东北天坐标系中再分别计算东西方向和南北方向的半轴长度。function computeEllipseParams(p0, p1) { const center new Cesium.Cartesian3(); Cesium.Cartesian3.add(p0, p1, center); Cesium.Cartesian3.multiplyByScalar(center, 0.5, center); const enuMatrix Cesium.Transforms.eastNorthUpToFixedFrame(center); const inverseMatrix new Cesium.Matrix4(); Cesium.Matrix4.inverse(enuMatrix, inverseMatrix); const localP0 new Cesium.Cartesian3(); Cesium.Matrix4.multiplyByPoint(inverseMatrix, p0, localP0); const semiMajor Math.max(Math.abs(localP0.x), 1.0); const semiMinor Math.max(Math.abs(localP0.y), 1.0); return { center: center, semiMajorAxis: semiMajor, semiMinorAxis: semiMinor }; }暂不讨论旋转角时这个函数已经足够生成一个标准椭圆。如果你希望绘制时让椭圆方向随起点到当前点的方位而变化那就需要在localP0.x、localP0.y的基础上再算一次旋转角。这一步可以放到后面扩展。5. 关键代码一个简单的 Ellipse 绘图工具类5.1 工具类的基本结构下面是一个轻量版 EllipseDrawer它覆盖了启动、动态预览、二次点击完成、清理这几个基本动作。实际工程项目里可以在这个基础上增加“右键取消”“finished 回调”“结果列表管理”等能力。class EllipseDrawer { constructor(viewer, options {}) { this.viewer viewer; this.onFinish options.onFinish || (() {}); this._handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); this._previewEntity null; this._startPoint null; this._active false; } activate() { this._active true; this._handler.setInputAction((movement) { if (!this._active) return; const position pickEarthPosition(this.viewer, movement.position); if (!position) return; if (!this._startPoint) { this._startPoint position; return; } // 第二次点击生成最终椭圆 this._finish(position); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); this._handler.setInputAction((movement) { if (!this._active || !this._startPoint) return; const position pickEarthPosition(this.viewer, movement.endPosition); if (!position) return; this._updatePreview(position); }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); } _updatePreview(currentPoint) { const params computeEllipseParams(this._startPoint, currentPoint); if (!this._preview
返回列表