我会用 "零基础能看懂的人话 + 完整操作步骤 + 代码逐行对应" 的方式,从 "用户看到坐标轴" 到 "拖拽完成多边形平移",彻底拆解沿 Z 轴(红色高度轴)编辑的全流程(X/Y 轴逻辑完全一致,仅轴方向不同),保证每一步都讲清 "做了什么、为什么做、代码怎么实现"。
轴编辑(沿 Z 轴平移)完整流程拆解
前置认知:先搞懂 3 个核心概念(用 "桌子上的笔筒" 举例)
| 代码概念 | 生活对应 | 人话解释 |
|---|---|---|
| 多边形中心点 | 桌子正中央 | 所有操作的基准点,平移 / 旋转都围绕这个点进行 |
| Z 轴向量(ENU) | 笔筒竖直向上的方向 | 贴合桌面(地形)的垂直方向,不是 "地球正上方",保证拖拽沿桌面高低移动 |
| 约束平面 | 穿过笔筒的竖直玻璃面 | 笔筒只能在这个玻璃面内沿自身方向上下移动,不能左右 / 前后跑偏 |
完整流程:共 8 步(从可视化到拖拽完成)
第一步:初始化渲染 ------ 在多边形中心显示 Z 轴(用户能看到的操作把手)
做什么?
在多边形中心点画出红色的 Z 轴(带箭头的线段),让用户知道 "可以拖这个轴改高度"。
代码实现(对应你代码里的addAxisByCenter):
javascript
// 1. 计算多边形中心点(比如桌子正中央)
const center = this.getCenter(this.positions.positions);
// 2. 调用getAxisVector获取Z轴向量(笔筒向上的方向)
const zAxis = this.getAxisVector(center, 'z');
// 3. 计算Z轴末端点(比如从中心点向上20米)
const zEnd = Cesium.Cartesian3.add(center, Cesium.Cartesian3.multiplyByScalar(zAxis, 20, new Cesium.Cartesian3()), new Cesium.Cartesian3());
// 4. 绘制Z轴(红色带箭头的线段),命名为edit-axis-z(用于后续识别)
axisCollection.entities.add({
id: 'edit-axis-z',
polyline: {
positions: [center, zEnd],
width: 4,
material: new Cesium.PolylineArrowMaterialProperty(Cesium.Color.RED)
}
});
关键:getAxisVector 怎么拿到 Z 轴向量?
javascript
private getAxisVector(center: Cesium.Cartesian3, axis: 'z') {
// 1. 生成中心点的ENU坐标系矩阵(贴合桌面/地形的坐标系)
const enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(center);
// 2. 从矩阵第2列提取Z轴向量(ENU的天方向=桌面垂直向上)
const up = Cesium.Matrix4.getColumn(enuTransform, 2, new Cesium.Cartesian3());
// 3. 归一化:把向量长度转为1(比如原长度5,转1后偏移1米就是1米,无误差)
return Cesium.Cartesian3.normalize(up, up);
}
第二步:用户操作 ------ 左键点击 Z 轴(触发拖拽初始化)
做什么?
代码识别用户点击的是 Z 轴,标记 "轴拖拽中",并初始化拖拽所需的所有参数。
代码实现(对应你代码里的 LEFT_DOWN 事件):
javascript
this.handler.setInputAction(async (movement) => {
// 1. 拾取用户点击的实体(比如点到了edit-axis-z)
const picks = this.viewer.scene.drillPick(movement.position);
const entityId = picks[0].id.id || picks[0].id.name;
// 2. 识别是Z轴(edit-axis-z)
if (entityId === 'edit-axis-z') {
this.isAxisDragging = true; // 标记:正在拖轴
this.activeAxis = 'z'; // 标记:拖的是Z轴
this.disableMapMove(); // 禁用地图平移(视角不晃)
this.prepareAxisDrag(movement.position); // 核心:初始化拖拽参数
}
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
第三步:初始化拖拽参数 ------prepareAxisDrag(给拖拽 "定规矩")
做什么?
计算拖拽需要的 "基准点、轴方向、约束平面、起始位置",相当于 "告诉代码:只能沿 Z 轴拖,只能在这个平面内拖"。
代码实现(逐行拆解):
javascript
private prepareAxisDrag(screenPosition: Cesium.Cartesian2) {
// 1. 拿到多边形顶点,计算中心点(桌子中央)
const points = this.getNormalizedPoints(this.positions.positions);
const center = this.getCenter(points);
// 2. 拿到Z轴向量(笔筒向上的方向)
const zAxis = this.getAxisVector(center, 'z');
// 3. 生成约束平面(穿过笔筒的竖直玻璃面)→ 这就是dragPlane的由来!
const plane = this.getAxisDragPlane(center, zAxis);
// 4. 计算鼠标点击位置在约束平面上的投影点(玻璃面上的起始点)
const ray = this.viewer.camera.getPickRay(screenPosition); // 鼠标点到3D空间的射线
const startPoint = Cesium.IntersectionTests.rayPlane(ray, plane); // 射线和玻璃面的交点
// 5. 计算起始点相对于中心点的Z轴偏移(比如向上5米)
const v = Cesium.Cartesian3.subtract(startPoint, center, new Cesium.Cartesian3());
const startScalar = Cesium.Cartesian3.dot(zAxis, v); // 点乘:只保留Z轴偏移
// 6. 保存这些参数(供鼠标移动时用)
this.dragCenter = center; // 基准点:桌子中央
this.dragAxis = zAxis; // 方向:笔筒向上
this.dragPlane = plane; // 范围:玻璃面(核心!)
this.dragStartScalar = startScalar; // 起始偏移:向上5米
}
关键:getAxisDragPlane 怎么生成约束平面(玻璃面)?
javascript
private getAxisDragPlane(center: Cesium.Cartesian3, zAxis: Cesium.Cartesian3) {
// 1. 拿到相机朝向(你看桌子的视线方向)
const cameraDir = this.viewer.camera.direction;
// 2. 叉乘Z轴和相机朝向,得到平面法向量(玻璃面的垂直方向)
let normal = Cesium.Cartesian3.cross(zAxis, cameraDir, new Cesium.Cartesian3());
// 3. 极端情况:你正对笔筒顶部看,法向量无效→改用相机up向量(屏幕向上)
if (Cesium.Cartesian3.magnitude(normal) < 1e-6) {
normal = Cesium.Cartesian3.cross(zAxis, this.viewer.camera.up, new Cesium.Cartesian3());
}
// 4. 归一化法向量(长度转1)
normal = Cesium.Cartesian3.normalize(normal, normal);
// 5. 创建平面(过桌子中央、法向量为normal的玻璃面)
return new Cesium.Plane(normal, -Cesium.Cartesian3.dot(normal, center));
}
第四步:用户操作 ------ 鼠标移动(拖 Z 轴)
做什么?
用户按住 Z 轴拖动鼠标,代码实时计算偏移量,让多边形跟着沿 Z 轴移动。
代码实现(对应你代码里的 MOUSE_MOVE 事件):
javascript
this.handler.setInputAction((movement) => {
if (this.isAxisDragging) { // 只处理轴拖拽
this.handleAxisDrag(movement.endPosition); // 计算偏移+平移
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
第五步:计算偏移并平移 ------handleAxisDrag(核心执行逻辑)
做什么?
计算鼠标移动的 Z 轴偏移量,让多边形(含孔洞)沿 Z 轴平移,保证 "只改高度,不改位置"。
代码实现(逐行拆解):
javascript
private handleAxisDrag(screenPosition: Cesium.Cartesian2) {
// 1. 校验参数(少一个都不执行,避免崩溃)
if (!this.dragCenter || !this.dragAxis || !this.dragPlane || this.dragStartScalar === null) return;
// 2. 拿到鼠标当前位置的射线
const ray = this.viewer.camera.getPickRay(screenPosition);
// 3. 计算鼠标当前点在约束平面(玻璃面)上的投影点
const currentPoint = Cesium.IntersectionTests.rayPlane(ray, this.dragPlane);
// 4. 计算当前点相对于中心点的Z轴偏移(比如向上8米)
const v = Cesium.Cartesian3.subtract(currentPoint, this.dragCenter, new Cesium.Cartesian3());
const currentScalar = Cesium.Cartesian3.dot(this.dragAxis, v);
// 5. 计算本次拖拽的增量(8米 - 5米 = 3米)
const delta = currentScalar - this.dragStartScalar;
// 6. 过滤微小偏移(鼠标抖0.0001米不算移动,避免闪屏)
if (Math.abs(delta) < 0.0001) return;
// 7. 计算平移向量(Z轴方向 × 3米)
const translation = Cesium.Cartesian3.multiplyByScalar(this.dragAxis, delta, new Cesium.Cartesian3());
// 8. 执行平移:多边形(含孔洞)整体向上移3米
this.applyTranslation(translation);
// 9. 更新起始偏移(下次基于8米计算,实现连续拖拽)
this.dragStartScalar = currentScalar;
// 10. 更新渲染:重新画坐标轴、更新标签
this.updateAfterTransform();
}
第六步:执行平移 ------applyTranslation(移动多边形)
做什么?
遍历多边形的外轮廓和内环孔洞顶点,给每个顶点加上平移向量,实现 "整体平移"。
代码实现:
javascript
private applyTranslation(translation: Cesium.Cartesian3) {
// 1. 平移外轮廓顶点(比如桌子四条边)
this.positions.positions = this.positions.positions.map((p) =>
Cesium.Cartesian3.add(p, translation, new Cesium.Cartesian3())
);
// 2. 平移内环孔洞顶点(比如桌子中间的洞)
if (this.positions.holes) {
this.positions.holes.forEach((hole) => {
hole.positions = hole.positions.map((p) =>
Cesium.Cartesian3.add(p, translation, new Cesium.Cartesian3())
);
});
}
}
第七步:更新渲染 ------updateAfterTransform(收尾)
做什么?
平移后重新渲染坐标轴(跟着多边形移)、更新距离标签、刷新视图,让用户看到最新的多边形位置。
第八步:结束拖拽 ------ 鼠标松开(隐含逻辑)
做什么?
用户松开左键,代码标记isAxisDragging = false,启用地图平移,轴编辑流程结束。
核心关键点总结(必记)
- dragPlane 的核心作用:是轴拖拽的 "隐形轨道",保证多边形只能沿轴移动,不跑偏;
- ENU 轴向量的意义:贴合地形(山坡上拖 Z 轴沿坡面高低移,不是地球正上方);
- 点乘的作用:只保留轴方向的偏移(拖 Z 轴只算高度,过滤左右 / 前后偏移);
- 连续拖拽的关键 :每次移动后更新
dragStartScalar,基于当前位置计算,不是初始位置; - 鲁棒性保障:参数校验、微小偏移过滤、极端情况处理,避免代码崩溃。
简单说:轴编辑的本质是 "给多边形加一个沿指定轴的'移动轨道',让鼠标只能在轨道上拖,拖多少就移多少"。
轴代码:
javascript
private addAxisByCenter(activeShapePoints: Cesium.Cartesian3[]) {
if (!Array.isArray(activeShapePoints) || activeShapePoints.length < 2) return;
this.cleanEntityCollection('editTbAxisEntityCollection');
const axisCollection = new Cesium.CustomDataSource('editTbAxisEntityCollection');
this.viewer.dataSources.add(axisCollection);
// 处理闭合点(首尾相同)
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);
// ENU 基向量
const enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(center);
const east = Cesium.Matrix4.getColumn(enuTransform, 0, new Cesium.Cartesian3());
const north = Cesium.Matrix4.getColumn(enuTransform, 1, new Cesium.Cartesian3());
const up = Cesium.Matrix4.getColumn(enuTransform, 2, new Cesium.Cartesian3());
const axisLength = 20; // 单位:米
const eastEnd = Cesium.Cartesian3.add(center, Cesium.Cartesian3.multiplyByScalar(east, axisLength, new Cesium.Cartesian3()), new Cesium.Cartesian3());
const northEnd = Cesium.Cartesian3.add(center, Cesium.Cartesian3.multiplyByScalar(north, axisLength, new Cesium.Cartesian3()), new Cesium.Cartesian3());
const upEnd = Cesium.Cartesian3.add(center, Cesium.Cartesian3.multiplyByScalar(up, axisLength, new Cesium.Cartesian3()), new Cesium.Cartesian3());
// X 轴(东/西)蓝色
axisCollection.entities.add(
new Cesium.Entity({
id: 'edit-axis-x',
name: 'edit-axis-x',
polyline: {
positions: [center, eastEnd],
width: 10,
material: new Cesium.PolylineArrowMaterialProperty(Cesium.Color.BLUE),
clampToGround: false,
},
})
);
// Y 轴(北/南)绿色
axisCollection.entities.add(
new Cesium.Entity({
id: 'edit-axis-y',
name: 'edit-axis-y',
polyline: {
positions: [center, northEnd],
width: 10,
material: new Cesium.PolylineArrowMaterialProperty(Cesium.Color.GREEN),
clampToGround: false,
},
})
);
// Z 轴(高度)红色
axisCollection.entities.add(
new Cesium.Entity({
id: 'edit-axis-z',
name: 'edit-axis-z',
polyline: {
positions: [center, upEnd],
width: 10,
material: new Cesium.PolylineArrowMaterialProperty(Cesium.Color.RED),
clampToGround: false,
},
})
);
}