实现动态绘制rect几何对象
1.基本架构设计
ts
class RectGeo {
app: any;
private _onMouseDownHand: (event: any) => void;
private _onMouseUpHand: (event: any) => void;
private _onMouseMoveHand: (event: any) => void;
private _drawSuccess: any;
constructor(app: any) {
this.app = app;
this._onMouseDownHand = this._onMouseDown.bind(this);
this._onMouseUpHand = this._onMouseUp.bind(this);
this._onMouseMoveHand = this._onMouseMove.bind(this);
}
private _onMouseDown(e: any) {}
private _onMouseMove(e: any) {}
private _onMouseUp(e: any) {}
public startDrawing(successCallback: Function) {
this.endDrawing();
this._drawSuccess = successCallback;
this.app.bimViewer.dom.addEventListener('mousedown', this._onMouseDownHand, true);
this.app.bimViewer.dom.addEventListener('mousemove', this._onMouseMoveHand, true);
this.app.bimViewer.dom.addEventListener('mouseup', this._onMouseUpHand, true);
}
public endDrawing() {
this.app.bimViewer.dom.removeEventListener('mousedown', this._onMouseDownHand, true);
this.app.bimViewer.dom.removeEventListener('mousemove', this._onMouseMoveHand, true);
this.app.bimViewer.dom.removeEventListener('mouseup', this._onMouseUpHand, true);
}
}
以上是基础鼠标交互的代码框架实现,也可以写成一个基类供其他地方有类似交互的代码进行继承,这里为了文章方便就直接写在一起了
2.关键代码实现
ts
private _onMouseDown(e: any) {
if (e.button === 0) {
e.stopPropagation();
} else if (e.button === 1 || e.button === 2) {
return;
}
const offsetX = e.offsetX;
const offsetY = e.offsetY;
const renderDomRect = this.app.bimViewer.getRenderDom().getBoundingClientRect();
const camera = this.app.bimViewer.getMainCamera();
this._startPoint.set((offsetX / renderDomRect.width) * 2 - 1, (offsetY / renderDomRect.height) * -2 + 1, 0.0);
this._startPoint.unproject(camera);
this._startPoint.z = 0.0;
this._endPoint.set((offsetX / renderDomRect.width) * 2 - 1, (offsetY / renderDomRect.height) * -2 + 1, 0.0);
this._endPoint.unproject(camera);
this._endPoint.z = 0.0;
this._drawShape(this._startPoint, this._endPoint);
}
private _onMouseMove(e: any) {
const offsetX = e.offsetX;
const offsetY = e.offsetY;
if (this._drawShapeMesh) {
const camera = this.app.bimViewer.getMainCamera();
const renderDomRect = this.app.bimViewer.getRenderDom().getBoundingClientRect();
this._endPoint.set((offsetX / renderDomRect.width) * 2 - 1, (offsetY / renderDomRect.height) * -2 + 1, 0.0);
this._endPoint.unproject(camera);
this._endPoint.z = 0.0;
// @ts-ignore
const corePositions = this._drawShapeMesh.geometry.attributes.position.array;
corePositions[3] = this._endPoint.x;
corePositions[7] = this._endPoint.y;
corePositions[9] = this._endPoint.x;
corePositions[10] = this._endPoint.y;
// @ts-ignore
this._drawShapeMesh.geometry.attributes.position.needsUpdate = true;
this._drawShapeMesh.geometry.computeBoundingSphere();
this.app.getViewer().cameraChanged();
}
}
private _onMouseUp(e: any) {
if (e.button === 0) {
e.stopPropagation();
} else if (e.button === 1 || e.button === 2) {
return;
}
let scene = this.app.bimViewer.getIncrementalScene();
scene.remove(this._drawShapeMesh);
this._drawShapeMesh = null;
this.app.getViewer().cameraChanged();
if (this._startPoint.equals(this._endPoint)) {
return;
}
if (this._drawSuccess) {
// 更新起点终点为左上,右下(解决不同方向问题)
const startP = new Vector3(
Math.min(this._startPoint.x, this._endPoint.x),
Math.max(this._startPoint.y, this._endPoint.y),
0
);
const endP = new Vector3(
Math.max(this._startPoint.x, this._endPoint.x),
Math.min(this._startPoint.y, this._endPoint.y),
0
);
this._drawSuccess({
startPoint: startP,
endPoint: endP
});
this._startPoint.set(0, 0, 0);
this._endPoint.set(0, 0, 0);
}
}
private _drawShape(startPoint: Vector3, endPoint: Vector3) {
const geometry = new BufferGeometry();
const vertices = new Float32Array([
startPoint.x,
startPoint.y,
0.0,
endPoint.x,
startPoint.y,
0.0,
startPoint.x,
endPoint.y,
0.0,
endPoint.x,
endPoint.y,
0.0
]);
geometry.setAttribute('position', new BufferAttribute(vertices, 3));
const indices = [0, 2, 1, 1, 2, 3];
geometry.setIndex(indices);
const material = new MeshBasicMaterial({ color: 'yellow', transparent: true, opacity: 0.5 });
this._drawShapeMesh = new Mesh(geometry, material);
let scene = this.app.bimViewer.getIncrementalScene();
scene.add(this._drawShapeMesh);
}
这里实现了鼠标按下,移动,抬起的绘制矩形交互。
1.当鼠标按下时通过屏幕上二维空间的坐标转换成三维空间的世界坐标(这个公式是怎么推演的,后续有机会再写篇文章),获取到三维坐标后就可以用这个数据进行顶点设置。
2.这里使用BufferGeometry并自定义设置positon,矩形有四个点组成,但webGL中最多只能到绘制三角形,所以矩形拆成两个三角形绘制,那就是六个点。为了复用顶点,这里有了index索引,索引值就是position数组中的点位置下标,这样position数组长度是4,index数组长度是6。
3.当鼠标移动时计算移动中的三维空间中的世界坐标,然后动态修改position,就能实时更新矩形。
4.当鼠标抬起时绘制结束,可以做一些返回数据等业务操作了。
3.业务代码实现
ts
/**
* 根据已知数据添加一个矩形
* @param config 矩形的配置项
*/
add(config: RectGeoConfig) {
const configCopy: RectGeoConfig = cloneDeep(config);
const group = new Group();
group.name = configCopy.id;
const color = configCopy.style.color;
const opacity = configCopy.style.opacity;
for (let i = 0; i < configCopy.positions.length; i++) {
const sp = configCopy.positions[i].startPoint;
const ep = configCopy.positions[i].endPoint;
const startPoint = new Vector3(sp.x, sp.y, sp.z);
const endPoint = new Vector3(ep.x, ep.y, ep.z);
// 进行偏移量转换
const viewTargetCurrent = new Vector3(
this.app.dwgDefaultView.Target[0],
this.app.dwgDefaultView.Target[1],
this.app.dwgDefaultView.Target[2]
);
startPoint.sub(viewTargetCurrent);
endPoint.sub(viewTargetCurrent);
const geometry = new BufferGeometry();
const vertices = new Float32Array([
startPoint.x,
startPoint.y,
0.0,
endPoint.x,
startPoint.y,
0.0,
startPoint.x,
endPoint.y,
0.0,
endPoint.x,
endPoint.y,
0.0
]);
geometry.setAttribute('position', new BufferAttribute(vertices, 3));
const indices = [0, 2, 1, 1, 2, 3];
geometry.setIndex(indices);
const material = new MeshBasicMaterial({ color: color, transparent: true, opacity: opacity });
const mesh = new Mesh(geometry, material);
mesh.name = configCopy.id + '_splitRect_' + i;
const geo = new EdgesGeometry(mesh.geometry);
const mat = new LineBasicMaterial({ color: '#fff' });
const wireframe = new LineSegments(geo, mat);
mesh.add(wireframe);
group.add(mesh);
}
const scene = this.app.bimViewer.getIncrementalScene();
scene.add(group);
}
/**
* 根据id删除对应的矩形
* @param id
* @returns
*/
remove(id: string) {
const scene = this.app.bimViewer.getIncrementalScene();
const mesh = scene.getObjectByName(id);
if (!mesh) return false;
scene.remove(mesh);
this.selectRectGeoId = null;
return true;
}
/**
* 根据矩形的起点终点,分割的长宽,进行矩形网格化分割
* @param startPoint
* @param endPoint
* @param smallRectWidth
* @param smallRectHeight
* @returns
*/
splitGrid(
startPoint: Vector3,
endPoint: Vector3,
smallRectWidth: number,
smallRectHeight: number
): Array<{ startPoint: Vector3; endPoint: Vector3 }> {
let arr: Array<{ startPoint: Vector3; endPoint: Vector3 }> = [];
const rightTop = new Vector3(endPoint.x, startPoint.y, 0.0);
const leftBottom = new Vector3(startPoint.x, endPoint.y, 0.0);
const bigRectWidth = startPoint.distanceTo(rightTop);
const bigRectHeight = startPoint.distanceTo(leftBottom);
const numRectsWide = Math.ceil(bigRectWidth / smallRectWidth);
const numRectsHigh = Math.ceil(bigRectHeight / smallRectHeight);
let rectWidth = smallRectWidth;
let rectHeight = smallRectHeight;
for (let i = 0; i < numRectsHigh; i++) {
for (let j = 0; j < numRectsWide; j++) {
rectWidth = smallRectWidth;
rectHeight = smallRectHeight;
// 如果这是右边的小矩形,调整它的宽度
if (j === numRectsWide - 1) {
rectWidth = bigRectWidth - j * smallRectWidth;
}
// 如果这是底部的小矩形,调整它的高度
if (i === numRectsHigh - 1) {
rectHeight = bigRectHeight - i * smallRectHeight;
}
const x = j * smallRectWidth;
const y = i * smallRectHeight;
const rect = {
startPoint: new Vector3(startPoint.x + x, startPoint.y - y, 0.0),
endPoint: new Vector3(startPoint.x + x + rectWidth, startPoint.y - y - rectHeight, 0.0)
};
arr.push(rect);
}
}
return arr;
}
4.封装RectGeoConfig 矩形的配置项
ts
class RectGeoConfig {
/** id区分矩形 */
id: string;
/** 矩形的起点终点位置 */
positions: Array<{
startPoint: Vector3;
endPoint: Vector3;
}>;
/** 矩形的样式 */
style: StyleInterface;
constructor() {
this.id = '';
this.positions = [];
this.style = {
color: 'yellow',
opacity: 0.5,
edges: true
};
}
}
interface StyleInterface {
/** 矩形颜色 */
color: string;
/** 矩形透明度 */
opacity: number;
/** 是否显示矩形边框 */
edges: boolean;
}
4.业务端调用
ts
let id = 0;
let rectGeo = new DrawGeometry.Rect.RectGeo(dwgApp);
let rectMap = new Map();
window.startDraw = function () {
rectGeo.startDrawing((data) => {
id++;
const config = new DrawGeometry.Rect.RectGeoConfig();
config.id = id;
config.positions.push({
startPoint: data.startPoint,
endPoint: data.endPoint
});
rectGeo.add(config);
rectMap.set(id, { id: id, startPoint: data.startPoint, endPoint: data.endPoint, splitState: false });
});
};
window.endDraw = function () {
rectGeo.endDrawing();
};
window.remove = function () {
const rectId = rectGeo.getSelectRectGeoId();
if (!rectId) return;
const data = rectMap.get(rectId);
if (!data) return;
console.log(rectId, data);
rectGeo.remove(rectId);
rectMap.delete(rectId);
};
window.split = function () {
const rectId = rectGeo.getSelectRectGeoId();
if (!rectId) return;
const data = rectMap.get(rectId);
if (!data) return;
console.log(rectId, rectMap, data);
let splitArr = rectGeo.splitGrid(
data.startPoint,
data.endPoint,
Math.ceil(Math.random() * 10) * 1000,
Math.ceil(Math.random() * 10) * 1000
);
window.remove();
id++;
const config = new DrawGeometry.Rect.RectGeoConfig();
config.id = id;
for (let i = 0; i < splitArr.length; i++) {
config.positions.push({
startPoint: splitArr[i].startPoint,
endPoint: splitArr[i].endPoint
});
}
rectGeo.add(config);
rectMap.set(id, {
id: id,
startPoint: data.startPoint,
endPoint: data.endPoint,
splitState: true,
splitWidth: 1000,
splitHeight: 1000,
splitArr: splitArr
});
};
window.getSelectRectGeoId = function () {
const rectId = rectGeo.getSelectRectGeoId();
console.log(rectId);
};
这里的设计,将Rect模块放在DrawGeometry下,并对外提供接口,由业务端组装使用功能。