🌏【cesium系列】Cesium实现鼠标绘制几何图形

238 阅读3分钟

前言

img

​ 之前我们绘制点线面都是通过固定坐标的方式进行绘制的,接下来将介绍怎么监听鼠标事件,从而绘制点线面。

实现方式参考网上已有的代码,并进行封装。

相关的示例代码地址: github.com/leixq1024/v…

实现思路

deepseek_mermaid_20250507_f5f8d2

完整实现代码

import * as Cesium from 'cesium'
// 定义绘制模式类型
type DrawingMode = 'Point' | 'Polyline' | 'Polygon' | 'Circle' | 'Rectangle'

// 扩展 Entity 类型以支持自定义属性
interface CustomEntity extends Cesium.Entity {
  positionData?: Cesium.Cartesian3 | Cesium.Cartesian3[]
  radius?: number
}

// 绘制配置参数接口
interface DrawOptions {
  removeLast?: boolean // 是否移除上一次绘制的图形,默认 true
}

// 全局变量声明
let drawHandler: Cesium.ScreenSpaceEventHandler | null = null
let activeShapePoints: Cesium.Cartesian3[] = []
let floatingPoint: CustomEntity | undefined | any
let activeShape: CustomEntity | undefined | any
let lastFeature: CustomEntity | undefined | any
const drawLayers: CustomEntity[] = []

/**
 * 手动绘制几何图形
 * @param viewer - Cesium 地图视图对象
 * @param drawingMode - 绘制模式:点/线/面/圆/矩形
 * @param options.removeLast - 是否清除上一个绘制
 * @param callback - 绘制完成回调函数
 * @returns 屏幕空间事件处理器
 */
export const draw = (
  viewer: any,
  drawingMode: DrawingMode,
  options: DrawOptions = { removeLast: true },
  callback?: (entity: CustomEntity) => void
): Cesium.ScreenSpaceEventHandler | undefined => {
  if (!viewer) return

  const { removeLast } = options
  // 配置场景参数
  viewer.scene.globe.depthTestAgainstTerrain = false
  viewer.enableCursorStyle = false
  viewer._element.style.cursor = 'crosshair'

  // 清理现有处理器
  if (drawHandler && !drawHandler.isDestroyed()) {
    drawHandler.destroy()
  }

  terminateShape()

  // 初始化事件处理器
  drawHandler = new Cesium.ScreenSpaceEventHandler(viewer.canvas)

  // 处理左键点击事件
  drawHandler.setInputAction((event: Cesium.ScreenSpaceEventHandler.PositionedEvent) => {
    const earthPosition = viewer.scene.pickPosition(event.position)

    if (Cesium.defined(earthPosition)) {
      if (drawingMode === 'Point') {
        // 点要素处理逻辑
        const finalPoint = createPoint(earthPosition)
        drawLayers.push(finalPoint)
        cleanupHandler()
        callback?.(finalPoint)
        return
      }

      if (!activeShapePoints.length) {
        // 初始化动态图形
        floatingPoint = createPoint(earthPosition, false)
        activeShapePoints.push(earthPosition)

        const dynamicPositions = new Cesium.CallbackProperty(() => {
          return drawingMode === 'Polygon' ? new Cesium.PolygonHierarchy(activeShapePoints) : activeShapePoints
        }, false)

        activeShape = drawShape(dynamicPositions)
      } else if (['Rectangle', 'Circle'].includes(drawingMode)) {
        // 矩形和圆特殊处理
        cleanupHandler()
        terminateShape(removeLast)
        return
      }

      activeShapePoints.push(earthPosition)
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)

  // 处理鼠标移动事件
  drawHandler.setInputAction((event: Cesium.ScreenSpaceEventHandler.MotionEvent) => {
    if (floatingPoint) {
      const newPosition: any = viewer.scene.pickPosition(event.endPosition)
      if (Cesium.defined(newPosition)) {
        floatingPoint.position?.setValue(newPosition)
        activeShapePoints.pop()
        activeShapePoints.push(newPosition)
      }
    }
  }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)

  // 处理双击事件
  drawHandler.setInputAction(() => {
    cleanupHandler()
    activeShapePoints = activeShapePoints.slice(0, -2)
    terminateShape(removeLast)
  }, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK)

  /** 清理事件处理器 */
  const cleanupHandler = () => {
    drawHandler?.destroy()
    viewer._element.style.cursor = 'default'
  }

  /** 终止当前图形绘制 */
  function terminateShape(removeLast?: boolean): void {
    removeLast && viewer.entities.remove(lastFeature!)

    if (activeShapePoints.length) {
      const finalShape = drawShape(activeShapePoints, true)
      drawLayers.push(finalShape)
      callback?.(finalShape)
    }

    viewer.entities.remove(floatingPoint!)
    viewer.entities.remove(activeShape!)
    floatingPoint = undefined
    activeShape = undefined
    activeShapePoints = []
  }

  /** 创建点要素 */
  function createPoint(worldPosition: Cesium.Cartesian3, isPoint = true): CustomEntity {
    const point = viewer.entities.add({
      position: worldPosition,
      point: {
        outlineWidth: isPoint ? 2 : 0,
        outlineColor: Cesium.Color.fromBytes(51, 153, 204),
        color: isPoint ? Cesium.Color.WHITE.withAlpha(0.5) : Cesium.Color.TRANSPARENT,
        pixelSize: 10
      }
    }) as CustomEntity

    point.positionData = worldPosition
    return point
  }

  /** 计算两点间距离(平面坐标系) */
  function calcRadius(point1: Cesium.Cartesian3, point2: Cesium.Cartesian3): number {
    return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2))
  }

  /** 绘制几何图形 */
  function drawShape(positionData: Cesium.Cartesian3[] | Cesium.CallbackProperty, final = false): CustomEntity {
    let shape: any

    switch (drawingMode) {
      case 'Polyline':
        shape = viewer.entities.add({
          polyline: {
            positions: positionData as Cesium.Property,
            material: Cesium.Color.fromBytes(51, 153, 204)
          }
        }) as CustomEntity
        shape.positionData = positionData
        break

      case 'Polygon':
        shape = viewer.entities.add({
          polygon: {
            hierarchy: positionData as Cesium.Property,
            perPositionHeight: true,
            material: Cesium.Color.WHITE.withAlpha(0.5),
            outline: true,
            outlineColor: Cesium.Color.fromBytes(51, 153, 204)
          }
        }) as CustomEntity
        shape.positionData = positionData
        break

      case 'Circle': {
        const positions =
          positionData instanceof Cesium.CallbackProperty
            ? positionData.getValue(Cesium.JulianDate.now())
            : positionData

        const radius = calcRadius(positions[0], positions[positions.length - 1])
        const callbackRadius = new Cesium.CallbackProperty(
          () => calcRadius(positions[0], positions[positions.length - 1]),
          false
        )

        const cartographic = Cesium.Cartographic.fromCartesian(positions[0])!

        shape = viewer.entities.add({
          position: activeShapePoints[0],
          name: 'Circle',
          ellipse: {
            semiMinorAxis: callbackRadius,
            semiMajorAxis: callbackRadius,
            height: cartographic.height,
            material: Cesium.Color.WHITE.withAlpha(0.5),
            outline: true,
            outlineColor: Cesium.Color.fromBytes(51, 153, 204),
            outlineWidth: 1
          }
        }) as CustomEntity

        Object.assign(shape, { positionData: [positions[0]], radius })
        break
      }

      case 'Rectangle': {
        const positions =
          positionData instanceof Cesium.CallbackProperty
            ? positionData.getValue(Cesium.JulianDate.now())
            : positionData

        const cartographic = Cesium.Cartographic.fromCartesian(positions[0])!

        shape = viewer.entities.add({
          name: 'Rectangle',
          rectangle: {
            coordinates: new Cesium.CallbackProperty(() => Cesium.Rectangle.fromCartesianArray(positions), false),
            height: cartographic.height,
            material: Cesium.Color.WHITE.withAlpha(0.5),
            outline: true,
            outlineColor: Cesium.Color.fromBytes(51, 153, 204),
            outlineWidth: 1
          }
        }) as CustomEntity

        shape.positionData = positions
        break
      }
    }

    if (final) lastFeature = shape!
    return shape!
  }

  return drawHandler
}

参数介绍

参数名称描述是否必传
viewer地图对象
drawingMode绘制的图形类型(点、线、面、矩形、圆)
options拓展选项,里面有一个参数 removeLast表示是否清除上次绘制内容
callback回调函数,将绘制好的图形对象返回回来

使用方法

将这段代码放到一个ts文件里面,引用即可。

注:绘制点和绘制线条的都是以双击结束绘制

绘制点

import { draw } from '@/utils/cesium/pointLineAndSurface.ts' // 我的ts文件
// 绘制点
const drawPoint = () => {
  const view: any = getMap() // 你的地图对象
  draw(view, 'Point', { continueDraw: false, removeLast: false }, (entity: any) => {
    console.log('👉 ~ 点实体:', entity)
  })
}

绘制点

绘制线

import { draw } from '@/utils/cesium/pointLineAndSurface.ts' // 我的ts文件
// 绘制线
const drawLine = () => {
  const view: any = getMap()
  draw(view, 'Polyline', { removeLast: true }, (entity: any) => {
    console.log('👉 ~ 线实体:', entity)
  })
}

这里因为我传的removeLasttrue,并且线段的结束绘制是以双击触发,所以会把上一次的绘制的清除掉

image-20250507215533515

绘制面

import { draw } from '@/utils/cesium/pointLineAndSurface.ts' // 我的ts文件
// 绘制面
const drawPolygon = () => {
  const view: any = getMap()
  draw(view, 'Polygon', { removeLast: true }, (entity: any) => {
    console.log('👉 ~ 面实体:', entity)
  })
}

面

绘制圆

import { draw } from '@/utils/cesium/pointLineAndSurface.ts' // 我的ts文件
// 绘制圆
const drawCircle = () => {
  const view: any = getMap()
  draw(view, 'Circle', { removeLast: true }, (entity: any) => {
    console.log('👉 ~ 圆实体:', entity)
  })
}

圆

绘制矩形

import { draw } from '@/utils/cesium/pointLineAndSurface.ts' // 我的ts文件
// 绘制矩形
const drawRect = () => {
  const view: any = getMap()
  draw(view, 'Rectangle', { removeLast: true }, (entity: any) => {
    console.log('👉 ~ 矩形实体:', entity)
  })
}

image-20250507215628158