three.js 在dwg图纸中动态绘制rect几何对象

实现动态绘制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下,并对外提供接口,由业务端组装使用功能。

5.效果

相关推荐
黑客老陈9 分钟前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安14 分钟前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
暴富的Tdy41 分钟前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se42 分钟前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235611 小时前
web 渗透学习指南——初学者防入狱篇
前端
z千鑫1 小时前
【前端】入门指南:Vue中使用Node.js进行数据库CRUD操作的详细步骤
前端·vue.js·node.js
m0_748250742 小时前
Web入门常用标签、属性、属性值
前端
m0_748230442 小时前
SSE(Server-Sent Events)返回n ,前端接收数据时被错误的截断【如何避免SSE消息中的换行符或回车符被解释为事件消息的结束】
前端
生产队队长3 小时前
项目练习:element-ui的valid表单验证功能用法
前端·vue.js·ui