entity几何体旋转圈编辑(绕 Z 轴旋转)完整流程拆解

我会沿用「零基础人话 + 完整步骤 + 代码逐行拆解」的方式,以绕 Z 轴(红色旋转圈)旋转为例(X/Y 轴逻辑一致,仅旋转轴不同),从 "渲染旋转圈" 到 "拖拽完成多边形旋转",彻底拆解旋转圈编辑的全流程,讲清每一步 "做什么、为什么做、代码怎么实现"。


旋转圈编辑(绕 Z 轴旋转)完整流程拆解

前置认知:先搞懂 3 个核心概念(用 "转盘上的盘子" 举例)

代码概念 生活对应 人话解释
多边形中心点 转盘的中心轴 旋转的基准点,所有顶点都绕这个点转
Z 轴向量(ENU) 转盘中心的竖直轴 旋转轴,多边形绕这个轴做圆周运动(比如转盘绕中心轴转)
旋转约束平面 转盘的桌面(水平面) 旋转只能在这个平面内进行,保证绕轴旋转不跑偏
旋转圈 转盘边缘的环形把手 用户拖拽这个圈,带动盘子(多边形)绕中心轴旋转

完整流程:共 8 步(从可视化到拖拽完成)

第一步:初始化渲染 ------ 在多边形中心显示 Z 轴旋转圈(用户能看到的操作把手)

做什么?

在多边形中心点画出红色的 Z 轴旋转圈(XY 水平面的环形折线),让用户知道 "拖这个圈能绕 Z 轴旋转"。

代码实现(对应代码里的addRotateRingsByCenter):
javascript 复制代码
// 1. 计算多边形中心点(转盘中心)
const center = this.getCenter(this.positions.positions);
// 2. 定义旋转圈参数(半径、分段数,分段越多圈越圆)
const ringRadius = 20; // 旋转圈半径20米
const segment = 36;    // 分成36段(每10度一段)
// 3. 生成旋转圈的顶点(XY水平面的环形)
const positions = [];
for (let i = 0; i <= segment; i++) {
  const angle = (Cesium.Math.TWO_PI * i) / segment; // 计算每个点的角度
  // 生成局部坐标(XY平面,Z=0):绕Z轴的环形
  const localPoint = new Cesium.Cartesian3(
    ringRadius * Math.cos(angle), // X坐标(东方向)
    ringRadius * Math.sin(angle), // Y坐标(北方向)
    0                             // Z坐标=0(水平面)
  );
  // 转换为世界坐标(贴合地形)
  const enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(center);
  const worldPoint = Cesium.Matrix4.multiplyByPoint(enuTransform, localPoint, new Cesium.Cartesian3());
  positions.push(worldPoint);
}
// 4. 绘制Z轴旋转圈(红色环形折线),命名为edit-rotate-ring-z(用于识别)
ringCollection.entities.add({
  id: 'edit-rotate-ring-z',
  polyline: {
    positions: positions,
    width: 2,
    material: Cesium.Color.RED.withAlpha(0.8)
  }
});
关键:旋转圈的位置逻辑
  • Z 轴旋转圈在XY 水平面 (垂直于 Z 轴),Y 轴旋转圈在XZ 平面 ,X 轴旋转圈在YZ 平面
  • 所有旋转圈都以多边形中心点为圆心,保证旋转围绕中心进行。

第二步:用户操作 ------ 左键点击 Z 轴旋转圈(触发旋转初始化)

做什么?

代码识别用户点击的是 Z 轴旋转圈,标记 "旋转圈拖拽中",并初始化旋转所需的所有参数。

代码实现(对应你代码里的 LEFT_DOWN 事件):
javascript 复制代码
this.handler.setInputAction(async (movement) => {
  // 1. 拾取用户点击的实体(比如点到了edit-rotate-ring-z)
  const picks = this.viewer.scene.drillPick(movement.position);
  const entityId = picks[0].id.id || picks[0].id.name;
  
  // 2. 识别是Z轴旋转圈(edit-rotate-ring-z)
  if (entityId === 'edit-rotate-ring-z') {
    this.isRingDragging = true;    // 标记:正在拖旋转圈
    this.activeRing = 'z';         // 标记:绕Z轴旋转
    this.disableMapMove();         // 禁用地图平移(视角不晃)
    this.prepareRingDrag(movement.position); // 核心:初始化旋转参数
  }
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);

第三步:初始化旋转参数 ------prepareRingDrag(给旋转 "定规矩")

做什么?

计算旋转需要的 "旋转中心、旋转轴、约束平面、起始方向向量",相当于 "告诉代码:只能绕 Z 轴,在水平面内旋转"。

代码实现(逐行拆解):
javascript 复制代码
private prepareRingDrag(screenPosition: Cesium.Cartesian2) {
  // 1. 拿到多边形顶点,计算中心点(转盘中心)
  const points = this.getNormalizedPoints(this.positions.positions);
  if (points.length < 2) return;
  const center = this.getCenter(points);
  
  // 2. 拿到Z轴向量(转盘中心的竖直轴)
  const zAxis = this.getAxisVector(center, 'z');
  
  // 3. 生成旋转约束平面(转盘的桌面=XY水平面,垂直于Z轴)
  // 平面方程:Z轴向量为法向量,过中心点
  const plane = new Cesium.Plane(zAxis, -Cesium.Cartesian3.dot(zAxis, center));
  
  // 4. 计算鼠标点击位置在约束平面上的投影点(转盘边缘的起始点)
  const ray = this.viewer.camera.getPickRay(screenPosition); // 鼠标点到3D空间的射线
  const startPoint = Cesium.IntersectionTests.rayPlane(ray, plane); // 射线和桌面的交点
  if (!startPoint) return;
  
  // 5. 计算起始点相对于中心点的向量(从中心到起始点的方向)
  const v = Cesium.Cartesian3.subtract(startPoint, center, new Cesium.Cartesian3());
  // 6. 归一化:向量长度转1(方便计算角度,长度不影响旋转)
  const startVector = Cesium.Cartesian3.normalize(v, new Cesium.Cartesian3());
  
  // 7. 保存这些参数(供鼠标移动时用)
  this.dragCenter = center;        // 旋转中心:转盘中心
  this.dragAxis = zAxis;           // 旋转轴:Z轴
  this.dragPlane = plane;          // 旋转平面:桌面(XY水平面)
  this.dragStartPoint = startPoint; // 起始点:转盘边缘点击处
  this.dragStartVector = startVector; // 起始方向向量:中心→起始点
}
关键对比(和轴拖拽的区别):
  • 旋转约束平面:垂直于旋转轴(Z 轴旋转的平面是 XY 水平面);
  • 轴拖拽约束平面:包含轴向量(Z 轴拖拽的平面是竖直面);
  • 保存的核心参数:旋转圈保存 "起始方向向量",轴拖拽保存 "起始偏移标量"。

第四步:用户操作 ------ 鼠标移动(拖旋转圈)

做什么?

用户按住旋转圈拖动鼠标,代码实时计算旋转角度,让多边形跟着绕 Z 轴旋转。

代码实现(对应你代码里的 MOUSE_MOVE 事件):
javascript 复制代码
this.handler.setInputAction((movement) => {
  if (this.isRingDragging) { // 只处理旋转圈拖拽
    this.handleRingDrag(movement.endPosition); // 计算角度+旋转
  }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

第五步:计算角度并旋转 ------handleRingDrag(核心执行逻辑)

做什么?

计算鼠标移动的旋转角度,让多边形(含孔洞)绕 Z 轴旋转,保证 "只改朝向,不改位置"。

代码实现(逐行拆解):
javascript 复制代码
private handleRingDrag(screenPosition: Cesium.Cartesian2) {
  // 1. 校验参数(少一个都不执行,避免崩溃)
  if (!this.dragCenter || !this.dragAxis || !this.dragPlane || !this.dragStartVector) return;
  
  // 2. 拿到鼠标当前位置的射线
  const ray = this.viewer.camera.getPickRay(screenPosition);
  if (!ray) return;
  
  // 3. 计算鼠标当前点在旋转约束平面(桌面)上的投影点
  const currentPoint = Cesium.IntersectionTests.rayPlane(ray, this.dragPlane);
  if (!currentPoint) return;
  
  // 4. 计算当前点相对于中心点的向量,归一化(长度转1)
  const v = Cesium.Cartesian3.subtract(currentPoint, this.dragCenter, new Cesium.Cartesian3());
  const currentVector = Cesium.Cartesian3.normalize(v, new Cesium.Cartesian3());
  
  // 5. 计算旋转角度(核心!用叉乘+点乘求夹角)
  // 5.1 叉乘:起始向量 × 当前向量 → 得到垂直于两者的向量(判断旋转方向:顺时针/逆时针)
  const cross = Cesium.Cartesian3.cross(this.dragStartVector, currentVector, new Cesium.Cartesian3());
  // 5.2 点乘旋转轴:得到正弦值(sin)→ 旋转方向+角度大小
  const sin = Cesium.Cartesian3.dot(this.dragAxis, cross);
  // 5.3 点乘起始/当前向量:得到余弦值(cos)→ 角度大小
  const cos = Cesium.Cartesian3.dot(this.dragStartVector, currentVector);
  // 5.4 反正切求角度(弧度):范围 -π ~ π(-180° ~ 180°)
  const angle = Math.atan2(sin, cos);
  
  // 6. 过滤微小角度(鼠标抖0.0001弧度不算旋转,避免闪屏)
  if (Math.abs(angle) < 0.0001) return;
  
  // 7. 执行旋转:多边形(含孔洞)绕Z轴旋转angle弧度
  this.applyRotation(this.dragAxis, angle, this.dragCenter);
  
  // 8. 更新起始方向向量(下次基于当前方向计算,实现连续旋转)
  this.dragStartVector = currentVector;
  
  // 9. 更新渲染:重新画旋转圈、更新标签
  this.updateAfterTransform();
}
关键:旋转角度计算的人话解释
  • 比如起始向量是 "向右(0°)",当前向量是 "向上(90°)":
    • cos = 0(0° 和 90° 的余弦值),sin = 1(正弦值);
    • angle = Math.atan2(1, 0) = π/2(90°)→ 顺时针旋转 90°;
  • 若当前向量是 "向下(-90°)":
    • sin = -1angle = -π/2(-90°)→ 逆时针旋转 90°;
  • 核心:通过 sin/cos 同时确定 "旋转方向" 和 "旋转角度",比单纯用距离计算更精准。

第六步:执行旋转 ------applyRotation(旋转多边形)

做什么?

用四元数 + 旋转矩阵,将多边形的外轮廓和内环孔洞顶点绕指定轴、指定中心旋转指定角度,保证 "整体旋转且位置不变"。

代码实现(逐行拆解):
javascript 复制代码
private applyRotation(axis: Cesium.Cartesian3, angle: number, center: Cesium.Cartesian3) {
  if (!this.positions?.positions) return;
  
  // 1. 用轴+角度创建四元数(Cesium推荐的旋转方式,避免万向锁)
  const q = Cesium.Quaternion.fromAxisAngle(axis, angle);
  // 2. 四元数转旋转矩阵(方便计算顶点旋转)
  const m = Cesium.Matrix3.fromQuaternion(q);
  
  // 3. 定义单个顶点的旋转逻辑(核心!)
  const rotatePoint = (p: Cesium.Cartesian3) => {
    // 3.1 顶点 - 中心点 → 平移到原点(转盘中心移到坐标原点)
    const local = Cesium.Cartesian3.subtract(p, center, new Cesium.Cartesian3());
    // 3.2 应用旋转矩阵 → 绕原点旋转angle弧度
    const rotated = Cesium.Matrix3.multiplyByVector(m, local, new Cesium.Cartesian3());
    // 3.3 平移回原中心 → 完成旋转
    return Cesium.Cartesian3.add(rotated, center, new Cesium.Cartesian3());
  };
  
  // 4. 旋转外轮廓顶点(盘子边缘)
  this.positions.positions = this.positions.positions.map((p) => rotatePoint(p));
  
  // 5. 旋转内环孔洞顶点(盘子中间的洞)
  if (Array.isArray(this.positions.holes)) {
    this.positions.holes.forEach((hole) => {
      if (Array.isArray(hole.positions)) {
        hole.positions = hole.positions.map((p) => rotatePoint(p));
      }
    });
  }
}
关键:顶点旋转的三步法(人话)
  1. 平移到原点:把顶点从 "转盘边缘" 移到 "坐标原点"(中心对齐);
  2. 绕原点旋转:用旋转矩阵让顶点绕 Z 轴转指定角度;
  3. 平移回中心:把旋转后的顶点移回原转盘中心;→ 最终效果:顶点绕转盘中心旋转,位置不变,朝向改变。

第七步:更新渲染 ------updateAfterTransform(收尾)

做什么?

旋转后重新渲染旋转圈(跟着多边形转)、更新距离标签、刷新视图,让用户看到最新的多边形朝向。

第八步:结束旋转 ------ 鼠标松开(隐含逻辑)

做什么?

用户松开左键,代码标记isRingDragging = false,启用地图平移,旋转圈编辑流程结束。


旋转圈 vs 轴拖拽核心差异(对比记忆)

维度 旋转圈编辑(绕轴旋转) 轴拖拽编辑(沿轴平移)
核心目标 改朝向(绕中心转) 改位置(沿轴移)
约束平面 垂直于旋转轴(XY 水平面) 包含轴向量(竖直面)
核心参数 起始方向向量(中心→起始点) 起始偏移标量(轴方向距离)
核心运算 叉乘 + 点乘求角度(sin/cos/atan2) 点乘提取轴偏移(delta)
顶点处理 平移→旋转→平移(三步法) 直接加平移向量(一步法)
结果值 angle(弧度,-π~π) delta(米,距离增量)

核心关键点总结(必记)

  1. 旋转约束平面:垂直于旋转轴,保证旋转在平面内进行(绕 Z 轴只在水平面转);
  2. 角度计算核心:叉乘判方向、点乘判大小,atan2 转弧度,兼顾 "方向 + 角度";
  3. 顶点旋转三步法:平移到原点→旋转→平移回中心,是 3D 旋转的通用标准写法;
  4. 四元数的作用:避免 "万向锁"(绕多轴旋转时的角度丢失问题),Cesium 推荐用法;
  5. 连续旋转的关键 :每次移动后更新dragStartVector,基于当前方向计算,而非初始方向。

简单说:旋转圈编辑的本质是 "给多边形加一个'旋转轨道'(垂直于旋转轴的平面),让鼠标拖轨道边缘,带动多边形绕中心轴转,拖多少角度就转多少"。

圈代码

javascript 复制代码
  /**
   * 在中心点绘制旋转圈(Z=红色,Y=绿色,X=蓝色)
   */
  private addRotateRingsByCenter(activeShapePoints: Cesium.Cartesian3[]) {
    if (!Array.isArray(activeShapePoints) || activeShapePoints.length < 2) return;
    this.cleanEntityCollection('editTbRotateRingEntityCollection');
    const ringCollection = new Cesium.CustomDataSource('editTbRotateRingEntityCollection');
    this.viewer.dataSources.add(ringCollection);

    // 处理闭合点(首尾相同)
    const points =
      activeShapePoints.length > 2 && Cesium.Cartesian3.equals(activeShapePoints[0], activeShapePoints[activeShapePoints.length - 1])
        ? activeShapePoints.slice(0, -1)
        : activeShapePoints;

    // 计算中心点(笛卡尔坐标平均)
    const center = points.reduce((acc, cur) => Cesium.Cartesian3.add(acc, cur, acc), new Cesium.Cartesian3(0, 0, 0));
    Cesium.Cartesian3.multiplyByScalar(center, 1 / points.length, center);

    const ringRadius = 25; // 单位:米
    const segment = 64;

    const addRing = (plane: 'xy' | 'xz' | 'yz', color: Cesium.Color, id: string) => {
      const positions: Cesium.Cartesian3[] = [];
      for (let i = 0; i <= segment; i++) {
        const angle = (Cesium.Math.TWO_PI * i) / segment;
        let local = new Cesium.Cartesian3();
        if (plane === 'xy') {
          local = new Cesium.Cartesian3(ringRadius * Math.cos(angle), ringRadius * Math.sin(angle), 0);
        } else if (plane === 'xz') {
          local = new Cesium.Cartesian3(ringRadius * Math.cos(angle), 0, ringRadius * Math.sin(angle));
        } else {
          local = new Cesium.Cartesian3(0, ringRadius * Math.cos(angle), ringRadius * Math.sin(angle));
        }
        positions.push(local);
      }
      const matrix = Cesium.Transforms.eastNorthUpToFixedFrame(center);
      const worldPositions = positions.map((p) => Cesium.Matrix4.multiplyByPoint(matrix, p, new Cesium.Cartesian3()));

      ringCollection.entities.add(
        new Cesium.Entity({
          id,
          name: id,
          polyline: {
            positions: worldPositions,
            width: 2,
            material: color,
            clampToGround: false,
          },
        })
      );
    };

    // 🔴 绕 Z 轴(高度轴)旋转:水平面环
    addRing('xy', Cesium.Color.RED, 'edit-rotate-ring-z');
    // 🟢 绕 Y 轴(南北轴)旋转:XZ 环
    addRing('xz', Cesium.Color.GREEN, 'edit-rotate-ring-y');
    // 🔵 绕 X 轴(东西轴)旋转:YZ 环
    addRing('yz', Cesium.Color.BLUE, 'edit-rotate-ring-x');
  }
相关推荐
ct9782 天前
Cesium高级特效与着色器开发全指南
前端·gis·cesium·着色器
duansamve8 天前
Cesium 线段分割和删除
cesium
YAY_tyy8 天前
Cesium 基础:地球场景初始化与视角控制
前端·cesium
ct97815 天前
Cesium中的CZML
学习·gis·cesium
weipt18 天前
关于vue项目中cesium的地图显示问题
前端·javascript·vue.js·cesium·卫星影像·地形
YAY_tyy18 天前
综合实战:基于 Turfjs 的智慧园区空间管理系统
前端·3d·cesium·turfjs
haokan_Jia18 天前
【三、基于Cesium的三维智慧流域四预系统-轻量级搭建】
cesium
YAY_tyy18 天前
Turfjs 性能优化:大数据量地理要素处理技巧
前端·3d·arcgis·cesium·turfjs
Tiam-201618 天前
cesium使用cesium-plot-js标绘多种图形
javascript·vue.js·arcgis·es6·gis·cesium·cesium-plot-js