Vue3+Cesium学习教程系列(22)---一种更加实用的椭圆绘制与编辑方法

46 阅读3分钟

本学习系列将以Cesium + Vue3 + Vite +Typescript+elementplus作为主要技术栈展开,后续会循序渐进,持续探索Cesium的高级功能,详情请关注原文Cesium+Vue3学习教程系列(22)---一种更加实用的椭圆绘制与编辑方法

上篇文章Cesium+Vue3学习教程系列(21)---Cesium与turf.js结合,以椭圆形的绘制与编辑为例中使用的是两点法绘制椭圆,即通过矩形的角点确定椭圆的长半轴、短半轴及椭圆的圆心,再利用Turf.js库计算椭圆形的图形点,最后使用Cesium进行多边形的绘制。这种方式其实是有些缺陷的,那就是生成的椭圆形不经过控制点,形状不可控,而且都是正交的,无法绘制具有一定旋转角度的椭圆形。

本文给出一种更加实用的椭圆绘制与编辑方法,通过控制点完美控制椭圆形状,而且解决了旋转角度问题。先看效果:

本文采用的是三点法绘制椭圆(见下图,手绘有点丑,见谅哈 ☺☺☺)。即通过控制点A、B确定椭圆的长轴(或短轴)的端点,控制点A和控制点B的中点作为椭圆的圆心,然后计算控制点C到线段AB的距离确定短半轴的长度。

图片 当发生旋转时,只需要计算AB所在的直线与正东的夹角即可。注:这里可能有个坑,官方文档里定义的rotation是与正北的逆时针夹角,我实际代码里使用正东夹角才对。各位读者大佬们,如果知道是什么原因,烦请告知我一下,谢谢。

图片

本文使用Cesium中Entity自带的ellipse实现椭圆绘制,具体实现步骤如下:

1、新建CommonEllipseUseCesium类继承BaseDraw

import EventDispatcher from "@/system/EventDispatcher/EventDispatcher";
import { CallbackPositionProperty, CallbackProperty, Cartesian3, ClassificationType, Color, Entity, Viewer } from "cesium";
import { BaseDraw, GeometryType } from "../BaseDraw";
import { CreateEllipse2Points } from "./CreatePolygonPoints";
export default class CommonEllipseUseCesium extends BaseDraw {
    /** 临时点,用来实时跟随鼠标 */
    private tempCursor = new Cartesian3();
    protected get maxPointCount() { return 3; }
    constructor(viewer: Viewer, dispatcher: EventDispatcher) {
        super(viewer, dispatcher);
        this.geometryType = GeometryType.COMMON_ELLIPSE2;
        this.minPointCount = 3;
    }
    protected buildFinalEntity(): Entity {
        let positions = this.getPositions();
        return this.viewer.entities.add({
            position: Cartesian3.midpoint(positions[0], positions[1], new Cartesian3()),
            ellipse: {
                semiMajorAxis: CreateEllipse2Points(positions).semiMajorAxis,
                semiMinorAxis: CreateEllipse2Points(positions).semiMinorAxis,
                rotation: CreateEllipse2Points(positions).rotation,
                material: Color.BLUE.withAlpha(0.4),
                outline: true,
                outlineColor: Color.WHITE,
                outlineWidth: 2,
                classificationType: ClassificationType.BOTH
            }
        });
    }
    protected buildTempEntity(): Entity | undefined {
      
        if (this.getPositions().length < (this.minPointCount - 1)) return undefined;
        return this.viewer.entities.add({
            position: new CallbackPositionProperty(() => Cartesian3.midpoint(this.getPositions()[0], this.getPositions()[1], new Cartesian3()), false),
            ellipse: {
                semiMajorAxis: new CallbackProperty(() => CreateEllipse2Points([
                    ...this.getPositions(),
                    this.tempCursor,
                ]).semiMajorAxis, false),
                semiMinorAxis: new CallbackProperty(() => CreateEllipse2Points([
                    ...this.getPositions(),
                    this.tempCursor,
                ]).semiMinorAxis, false),
                rotation: new CallbackProperty(() => CreateEllipse2Points([
                    ...this.getPositions(),
                    this.tempCursor,
                ]).rotation, false),
                classificationType: ClassificationType.BOTH,// 贴地
                material: Color.CYAN.withAlpha(0.3),
                outline: true,
                outlineColor: Color.WHITE,
                outlineWidth: 2
            }
        });
    }
    protected updateTempEntity(cursor: Cartesian3): void {
        this.tempCursor = cursor;
    }
    /* -------------------- 鼠标事件 -------------------- */
    protected onLeftClick(e: any): void {
        super.onLeftClick(e);
        if (this.getPositions().length === this.maxPointCount) {
            this.finish();
        }
    }
    ;
}

2、在回调函数回调函数CreateEllipse2Points中通过前两个控制点确定长半轴长度及椭圆圆心,然后通过pointToLineDistance计算第三个点到长轴的距离作为短半轴长度,最后计算长轴与正东的夹角。

export const CreateEllipse2Points = (pos: Cartesian3[]) => {  
    // 1. 圆心
    const center = Cartesian3.midpoint(pos[0], pos[1], new Cartesian3());
    // 2. 长半轴
    let semiMajorAxis = Cartesian3.distance(pos[0], pos[1]) / 2;
    // 3. 短半轴
    let semiMinorAxis = pointToLineDistance(pos[2], pos[0], pos[1]);
    // 4. rotation
    // ENU变换
    const enuToFixed = Transforms.eastNorthUpToFixedFrame(center);
    const fixedToEnu = Matrix4.inverse(enuToFixed, new Matrix4());
    const p2ENU = Matrix4.multiplyByPoint(fixedToEnu, pos[1], new Cartesian3());
    const centerENU = new Cartesian3(0, 0, 0);
    const majorVecENU = Cartesian3.subtract(p2ENU, centerENU, new Cartesian3());
    // rotation是与正东(x轴)的夹角
    let rotation = Math.atan2(majorVecENU.y, majorVecENU.x);
    // 5. 保证semiMajorAxis >= semiMinorAxis
    if (semiMajorAxis < semiMinorAxis) {
        [semiMajorAxis, semiMinorAxis] = [semiMinorAxis, semiMajorAxis];
        rotation += Math.PI / 2;
    }
    // 归一化到[0, 2PI]
    rotation = (rotation + 2 * Math.PI) % (2 * Math.PI);
    return {      
        semiMajorAxis,
        semiMinorAxis,
        rotation
    };
}

3、然后在EditHelper.ts中添加新椭圆的绘制,从而完成编辑功能。


case GeometryType.COMMON_ELLIPSE2:  
    this.shapeEntity.position = new ConstantPositionProperty(Cartesian3.midpoint(this.positions[0], this.positions[1], new Cartesian3()));
    if (this.shapeEntity.ellipse) {
        this.shapeEntity.ellipse.semiMajorAxis = new CallbackProperty(() => CreateEllipse2Points(this.positions).semiMajorAxis, false);
        this.shapeEntity.ellipse.semiMinorAxis = new CallbackProperty(() => CreateEllipse2Points(this.positions).semiMinorAxis, false);
        this.shapeEntity.ellipse.rotation = new CallbackProperty(() => CreateEllipse2Points(this.positions).rotation, false);
    }
    break;

4、实现效果

图片

5、更多代码请关注原文Cesium+Vue3学习教程系列(22)---一种更加实用的椭圆绘制与编辑方法