在学习cesium中,有这么一个需求,在三维场景中,在加载的实体billbord后,点击billbord,能在三维场景中进行旋转拖拽和平移.
- 利用fabric.js Fabric.js Javascript Canvas Library (fabricjs.com),, 一个能够让你轻而易举操作canvas的神奇的库. Fabric 不仅提供了一个虚拟canvas对象, 还有svg渲染器, 交互层, 还有一整套十分有用的工具. 这是一个完全开源的项目, MIT协议, 多年以来依靠许多贡献者共同维护
- 在对实体billbord进行平移时,要对平移的点进行判断,是在地形上还是模型,对点位进行位置拾取和坐标转换
- 点击实体billbord获取出来的属性中rotation是弧度,而fabric.js接受的是度,所有需要进行弧度与度之间的转换
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="Cesium-1.91//Cesium.js"></script>
<link href="Cesium-1.91/Widgets/widgets.css" rel="stylesheet">
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
}
#con {
position: absolute;
z-index: 999;
float: left;
font-size: 18px;
color: #fff;
}
#cesiumContainer {
width: 100vw;
height: 100vh;
position: relative;
}
#editorcanvas {
position: absolute;
z-index: 999;
right: 0;
top: 100px;
}
.container {
position: absolute !important;
z-index: 999 !important;
left: 50% !important;
top: 50% !important;
transform: translate(-50%, -50%) !important;
display: none !important;
}
</style>
</head>
<body>
<div id="con">
<div><button onclick="addBillboard()">图标</button></div>
</div>
<div id="cesiumContainer">
</div>
<div class="container" style="position: relative;">
<div id="CanvasContainer" style="width: 100vw;height: 100vh;">
<canvas id="Canvas" width="800" height="800"></canvas>
</div>
</div>
<script src="libs/fabric.min.js"></script>
<script type="module">
import { EditBoard } from './utils/zeditBoard.js';
Cesium.Ion.defaultAccessToken ='xxxxx';
// 实例化cesium
let viewer = new Cesium.Viewer('cesiumContainer', {
infoBox: false,
});
window.viewer = viewer;
</div>
</body>
</html>
import { EditBoard } from './utils/zeditBoard.js';
if (EditBoard) {
const Canvas = document.getElementById('Canvas');
window.editBoard = new EditBoard(viewer, Canvas);
}
handler.setInputAction(movement => {
if (editBoard) {
editBoard.update(true);
}
var pick = viewer.scene.pick(movement.position);
if (pick.id && pick.id._billboard){
let obj: any = JSON.parse(boardToolsClick); //判断浏览器是否存在了编辑好的点,目的是为了如果第一次编辑完后,第二次继续在第一次基础上继续编辑
let targetPosition = obj ? obj.position : cartesian;//如果浏览器obj存在,使用obj.position(笛卡尔坐标),不存在,使用cartesian
pick.id.show = false;
// 将Cartesian3坐标转换为屏幕坐标
const windowCoordinates =
Cesium.SceneTransforms.wgs84ToWindowCoordinates(
window.map.viewer.scene,
targetPosition
);
const ele = document.getElementsByClassName('container')[0];
ele.style.cssText = `display: block !important;`;
const top = windowCoordinates.y
const left = windowCoordinates.x
if (editBoardRef) {
if (obj) {
editBoardRef.editBillboardImage(
image,//实体billboard是一个图片
width,//实体billboard宽度
height,//实体billboard高度
left,
top,
feature.entity,//实体billboard
styles,//实体billboard央视
windowCoordinates,//Cartesian3坐标转换为屏幕坐标
obj.graphic.style.rotation,//已存在的角度
obj.graphic.style.scale//已存在的缩放大小
);
} else {
editBoardRef.editBillboardImage(
image,
width,
height,
left,
top,
feature.entity,
styles,
windowCoordinates,
);
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
zeditBoard.js
import {
getImgURL, setFlagToolsClick,
setBoardClick, getBoardClick
} from "@/utils/common";
import * as Cesium from "cesium";
import { displayCanvasContainer, radiansToDegrees, degreesToRadians } from "./const";
const fabric = window.fabric;
// fabric 默认在html 引入,如果需要可以通过npm 管理 import
export class EditBoard {
/**
*
* @param {*} viewer -cesium 实例
* @param {*} canvas -编辑 实例
*/
constructor(viewer, canvas, cancontainerCanvasEl) {
this.viewer = viewer;
this.changePosition = {};
this.fabricCanvas = null;
this.currentPick = null;
this.windowCoordinates = null;
this.cancontainerCanvasEl = cancontainerCanvasEl;
this.rotating = null;
this.initEdit(canvas);
}
// 初始化
initEdit(Canvas) {
// 自定义旋转鼠标样式
fabric.util.addListener(fabric.window, "load", function () {
fabric.freeDrawingBrush = new fabric.PencilBrush(
fabric.Canvas.prototype.freeDrawingBrush
);
fabric.freeDrawingBrush.rotateMouseCursor = `${getImgURL(
"rotate.png"
)},auto`; // 自定义旋转光标的URL
});
// 初始化Fabric.js画布
this.fabricCanvas = new fabric.Canvas(Canvas);
this.fabricCanvas.setWidth(this.viewer.canvas.clientWidth);
this.fabricCanvas.setHeight(this.viewer.canvas.clientHeight);
// 设置最小缩放比例
this.fabricCanvas.minScale = 0.1;
this.changeRotateIcon();
}
// 编辑图像的函数
editBillboardImage(
imageUrl,
width = 200,
height = 50,
left,
top,
item,
styles,
windowCoordinates,
objRotation,
objScale
) {
this.currentPick = item;
this.windowCoordinates = windowCoordinates
const { scale, rotation = 0 } = styles;
const angle = objRotation ? radiansToDegrees(objRotation) : radiansToDegrees(rotation || 0);
this.rotating = 360 - angle;
// 清空Fabric.js画布
const { fabricCanvas } = this;
fabricCanvas.clear();
// 加载广告牌图像
const _this = this;
fabric.Image.fromURL(imageUrl, function (img) {
img.set({
scaleX: objScale ? (width * objScale) / img.width : (width * scale) / img.width,
scaleY: objScale ? (height * objScale) / img.height : (height * scale) / img.height,
// scaleX:scale,
// scaleY:scale,
left, // 设置图片左上角的x坐标
top, // 设置图片左上角的y坐标
transparentCorners: false,
cornerColor: "#fff",
cornerStrokeColor: "#fff",
borderColor: "blue",
cornerSize: 6,
// padding: 10,
cornerStyle: "rect",
angle: 360 - angle,
originalAngle: 360 - angle,
originalFlipX: img.flipX,
originalFlipY: img.flipY,
originX: 'center',
originY: 'center',
});
// 控制是否旋转显示按钮
img.setControlsVisibility({
mt: false,
mb: false,
// ml: false,
mr: false,
// tr: false,
// tl: false,
// br: false,
// bl: false,
// mtr: false, // 为true则middle-top-rotate控制器可用,false则不可用。
});
fabricCanvas.add(img);
fabricCanvas.setActiveObject(img);
fabricCanvas.renderAll();
});
// 监听函数 可自行在这里监听调用
fabricCanvas.on("selection:created", function (e) {
// 创建自定义事件 这一步可不需参考
let myEvent = new CustomEvent("editBillboardCreate", {
detail: {
editBoard: _this,
}, // 可选,传递一些数据
});
// 触发自定义事件
document.dispatchEvent(myEvent);
});
fabricCanvas.on("selection:updated", function (e) { });
fabricCanvas.on("selection:cleared", function (e) {
_this.update(true);
setFlagToolsClick(false) //存在浏览器的方法
});
fabricCanvas.on("mouse:down", function (e) { });
fabricCanvas.on("object:modified", function (e) { });
fabricCanvas.on("object:scaling", function (e) {
});
fabricCanvas.on("object:moving", function (e) {
_this.changePosition = e.pointer;
});
fabricCanvas.on("object:rotating", function (e) {
_this.rotating = e.target.angle;
});
fabricCanvas.on("mouse:wheel", function (e) { });
}
//屏幕坐标转三维坐标
/**
*
* @param {object} px
* @returns {object} cartesian三维坐标或者 false
* 该方法根据给定的屏幕坐标px,计算出对应的三维世界坐标。该三维世界坐标可以代表一个具体的点在地图上的位置。
使用drillPick方法来获取屏幕坐标点上所有的对象。如果该点上有一个或多个对象,方法会尝试从3D模型上拾取坐标。
如果拾取不在3D模型上,并且地形存在,则从地形上拾取坐标。
如果既不在3D模型上也不在地形上,则从地球椭球体上拾取坐标。
最后返回这个点的三维世界坐标,或者在无法确定时返回false。
transformWGS84ToCartesian方法说明:
该方法根据给定的地理坐标(WGS84格式)计算出相应的三维世界坐标(笛卡尔坐标)。
使用Cesium的Cartesian3.fromDegrees方法从给定的经纬度和高度计算出三维坐标。
transformCartesianToWGS84方法说明:
该方法根据给定的三维世界坐标(笛卡尔坐标)计算出相应的地理坐标(WGS84格式)。
使用Cesium的Ellipsoid.WGS84和cartesianToCartographic方法将三维世界坐标转换为地理坐标。
*/
getCatesian3FromPX(px) {
if (window.map.viewer && px) {
//获取场景中坐标点上所有的对象
var picks = window.map.viewer.scene.drillPick(px);
var cartesian = null;
var isOn3dtiles = false,
isOnTerrain = false;
// drillPick
for (let i in picks) {
let pick = picks[i];
if (
(pick && pick.primitive instanceof Cesium.Cesium3DTileFeature) ||
(pick && pick.primitive instanceof Cesium.Cesium3DTileset) ||
(pick && pick.primitive instanceof Cesium.Model)
) {
//模型上拾取
isOn3dtiles = true;
}
// 3dtilset
if (isOn3dtiles) {
window.map.viewer.scene.pick(px); // pick
cartesian = window.map.viewer.scene.pickPosition(px);
if (cartesian) {
let cartographic = Cesium.Cartographic.fromCartesian(cartesian);
if (cartographic.height < 0) cartographic.height = 0;
let lon = Cesium.Math.toDegrees(cartographic.longitude),
lat = Cesium.Math.toDegrees(cartographic.latitude),
height = cartographic.height;
cartesian = this.transformWGS84ToCartesian({
lng: lon,
lat: lat,
alt: height
});
}
}
}
// 地形
let boolTerrain =
window.map.viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider;
// Terrain
if (!isOn3dtiles && !boolTerrain) {
var ray = window.map.viewer.scene.camera.getPickRay(px);
if (!ray) return null;
cartesian = window.map.viewer.scene.globe.pick(ray, window.map.viewer.scene);
isOnTerrain = true;
}
// 地球
if (!isOn3dtiles && !isOnTerrain && boolTerrain) {
cartesian = window.map.viewer.scene.camera.pickEllipsoid(
px,
window.map.viewer.scene.globe.ellipsoid
);
}
if (cartesian) {
let position = this.transformCartesianToWGS84(cartesian);
if (position.alt < 0) {
cartesian = this.transformWGS84ToCartesian(position, 0.1);
}
return cartesian;
}
return false;
}
}
/***
* 坐标转换 84转笛卡尔
* @param {Object} {lng,lat,alt} 地理坐标
* @return {Object} Cartesian3 三维位置坐标
*/
transformWGS84ToCartesian(position, alt) {
if (window.map.viewer) {
return position
? Cesium.Cartesian3.fromDegrees(
position.lng || position.lon,
position.lat,
(position.alt = alt || position.alt),
Cesium.Ellipsoid.WGS84
)
: Cesium.Cartesian3.ZERO;
}
}
/***
* 坐标转换 笛卡尔转84
* @param {Object} Cartesian3 三维位置坐标
* @return {Object} {lng,lat,alt} 地理坐标
*/
transformCartesianToWGS84(cartesian) {
if (window.map.viewer && cartesian) {
var ellipsoid = Cesium.Ellipsoid.WGS84;
var cartographic = ellipsoid.cartesianToCartographic(cartesian);
return {
lng: Cesium.Math.toDegrees(cartographic.longitude),
lat: Cesium.Math.toDegrees(cartographic.latitude),
alt: cartographic.height
};
}
}
update(val) {
// 保存且写入信息
const { fabricCanvas } = this;
if (val) {
if (!fabricCanvas._objects[0]) return;
const dataURL = fabricCanvas._objects[0].toDataURL();
// 写入数据
const item = fabricCanvas._objects[0];
const width = item.getScaledWidth();
const height = item.getScaledHeight();
if (!this.currentPick) return;
this.currentPick._billboard._image._value = dataURL;
// 大小
// this.currentPick._billboard._width._value = width;
// this.currentPick._billboard._height._value = height;
// 屏幕坐标转经纬度
// var pick1 = new Cesium.Cartesian2(item.left + width, item.top + height);
// let pick1 = new Cesium.Cartesian2(
// item.left + width / 2,
// item.top + height
// );
let cartesian1 = this.getCatesian3FromPX(
this.windowCoordinates
);
let cartesian2 = this.getCatesian3FromPX(
new Cesium.Cartesian2(
this.changePosition.x,
this.changePosition.y)
);
// 判断是否移动了
if (this.changePosition.x || this.changePosition.y) {
this.currentPick._position._value = cartesian2;
}
// 需要存的数据(方便入库)
const obj = {
width,
height,
position: this.changePosition.x || this.changePosition.y ? cartesian2 : cartesian1,
image: dataURL,
angle: 360 - this.rotating,
};
// 改变的数据存在这个字段
this.currentPick.changeData = obj;
// 创建自定义事件
let myEvent = new CustomEvent("editBillboard", {
detail: {
objInfo: obj,
editBoard: this,
}, // 可选,传递一些数据
});
// 触发自定义事件
document.dispatchEvent(myEvent);
this.destroyEdit();
// 更新广告牌的图像
} else {
// 退出编辑状态
this.destroyEdit();
}
}
// 结束
destroyEdit() {
if (this.fabricCanvas) {
// const node = fabricCanvas.getElement()
// fabricCanvas.dispose() // 释放资源
// fabricCanvas = null
// node.remove()
displayCanvasContainer(false);
this.currentPick.show = true;
this.currentPick = null;
this.changePosition = {};
this.rotating = null;
}
}
// 设置旋转图标
changeRotateIcon() {
const iconURL = getImgURL("rotate.png");
const callback = (image, isError) => {
if (!isError) {
fabric.Object.prototype.controls.ml = new fabric.Control({
x: 0,
y: -0.5,
offsetY: -40,
cursorStyle: "help",
actionHandler: fabric.controlsUtils.rotationWithSnapping,
cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,
// 渲染图标
render: this.renderIcon(image._element, 20),
// 设置控制点大小
cornerSize: 30,
});
}
};
fabric.Image.fromURL(iconURL, callback);
}
renderIcon(image, initialAngle) {
return function (ctx, left, top, styleOverride, fabricObject) {
let size = this.cornerSize;
ctx.save();
ctx.translate(left, top);
ctx.rotate(
fabric.util.degreesToRadians(fabricObject.angle + initialAngle)
);
ctx.drawImage(image, -size / 2, -size / 2, size, size);
ctx.restore();
};
}
}
export const getFlagToolsClick = () => {
return localStorage.getItem("flagToolsClick") === "true"
? true
: false
};
export const setFlagToolsClick = (value) => {
localStorage.setItem("flagToolsClick", value);
};
/**
*弧度转度
*
* @export
* @param {number} radians
* @return {*}
*/
export function radiansToDegrees(radians:number) {
return radians * (180 / Math.PI);
}