Threejs 框选功能

Threejs 框选功能

在threejs中模型交互主要是通过射线检测选中单个物体或者在这条射线上的物体,如果要选择多个物体就比较麻烦了,框选功能比较适合这种。
threejs版本为 0.124.0
框选模型节点效果

原理概述

鼠标按下和抬起时的两个位置就是一个矩形,再利用相机位置可以生成一个四棱台,类似视椎体,然后遍历各个节点的包围盒中心是否在这个视椎体内。

部分细节说明

  • 初始化
    constructor(viewer) { //传递过来的viewer里定义了webGLRenderer,Scene,camera等
        this.viewer = viewer;
        this.renderer = viewer.renderer;
        this.camera = viewer.activeCamera;
        this.controls = viewer.controls;//框选时需要把控制器(orbit)禁用
        this.scene = viewer.scene;
        this.useSelect = false;//这个值为true时才能框选物体。可以在按下ctrl时设为true,松开时设为false
        this.currentIsPersectiveCamera = true; //只有透视相机才能框选
    
        this.selectionShape = null;//框选时在屏幕上出现的矩形区域
    
        this.startX = - Infinity;
        this.startY = - Infinity;
        this.startZ = -Infinity;
    
        this.prevX = - Infinity;
        this.prevY = - Infinity;
        this.prevZ = - Infinity;
    
        this.selectionPoints = [];//保存鼠标按下和松开时的坐标
        this.dragging = false;
        this.selectionShapeNeedsUpdate = false;
        this.init(); //用于初始化显示在屏幕上的矩形和绑定鼠标事件的函数
    }
    
  • 鼠标事件
    这里用到的鼠标事件有pointerdown,pointerup,pointermove。鼠标按下时记录按下的位置及将一些变量置空,鼠标移动中需要更新矩形的另一个点(第一个点是按下的位置),更新这个点后后续才能更新屏幕上的矩形。鼠标松开时构建四棱台并进行模型遍历。
		//鼠标按下事件
        this.renderer.domElement.addEventListener('pointerdown', e => {
            if (!this.useSelect) return;
            this.prevX = e.clientX;
            this.prevY = e.clientY;
            var rect = this.renderer.domElement.getBoundingClientRect();
            this.startX = ((e.clientX  - rect.left) / rect.width) * 2 - 1;
            this.startY =(-(e.clientY - rect.top) / rect.height) * 2 + 1;

            this.startZ = 0

            this.selectionPoints.length = [];
            this.dragging = true;
        });

        //鼠标松开事件
        this.renderer.domElement.addEventListener('pointerup', () => {
            if (!this.useSelect) return;
            if(!this.currentIsPersectiveCamera) message.info("请使用透视相机框选!");
            this.shape();
            this.selectionShape.visible = false;
            this.selectionPoints.length = [];
            this.selectionShapeNeedsUpdate = true;
            this.dragging = false;
        });

        //鼠标移动事件
        this.renderer.domElement.addEventListener('pointermove', e => {
            if (!this.useSelect) return;
            // If the left mouse button is not pressed
            if ((1 & e.buttons) === 0) {
                return;
            }

            const ex = e.clientX;
            const ey = e.clientY;

            var rect = this.renderer.domElement.getBoundingClientRect();
            let nx =((e.clientX  - rect.left) / rect.width) * 2 - 1;// (e.clientX / window.innerWidth) * 2 - 1;
            let ny = (-(e.clientY - rect.top) / rect.height) * 2 + 1;//- ((e.clientY / window.innerHeight) * 2 - 1);

            // set points for the corner of the box
            this.selectionPoints.length = 3 * 5;

            this.selectionPoints[0] = this.startX;
            this.selectionPoints[1] = this.startY;
            this.selectionPoints[2] = this.startZ;

            this.selectionPoints[3] = nx;
            this.selectionPoints[4] = this.startY;
            this.selectionPoints[5] = this.startZ;

            this.selectionPoints[6] = nx;
            this.selectionPoints[7] = ny;
            this.selectionPoints[8] = this.startZ;

            this.selectionPoints[9] = this.startX;
            this.selectionPoints[10] = ny;
            this.selectionPoints[11] = this.startZ;

            this.selectionPoints[12] = this.startX;
            this.selectionPoints[13] = this.startY;
            this.selectionPoints[14] = this.startZ;


            if (ex !== this.prevX || ey !== this.prevY) {
                this.selectionShapeNeedsUpdate = true;
                if(!this.selectionShape.visible)
                    this.selectionShape.visible = true;
            }
            this.prevX = ex;
            this.prevY = ey;
            this.selectionShape.visible = true;
        });
  • 更新框选矩形
    这个函数需要时时调用,也就是需要放到animate函数(调用了requestAnimationFrame的函数)中调用
	/**
     *更新框选形状
     */
    update() {
        if (!this.selectionShape) return;
        // Update the selection lasso lines
        if (this.selectionShapeNeedsUpdate) {
            this.selectionShape.geometry.setAttribute(
                'position',
                new Float32BufferAttribute(this.selectionPoints, 3, false)
            );

            this.selectionShape.frustumCulled = false;
            this.selectionShapeNeedsUpdate = false;
        }

        //根据相机设置框选形状的位置,让它显示在屏幕上  很重要!
        const yScale = Math.tan(MathUtils.DEG2RAD * this.camera.fov / 2) * this.selectionShape.position.z;
        this.selectionShape.scale.set(- yScale * this.camera.aspect, - yScale, 1);
    }
  • 生成虚拟四棱台并遍历模型
    这个阶段会生成一个虚拟的四棱台(为什么是四棱台?因为透视相机的视野范围就是一个四棱台),也就是把框选区域从屏幕上的2D转到世界空间的3D。检测比较算法具体看代码
/**
     * 松开鼠标后开始比较、隐藏mesh
     */
    shape(){
        if(this.selectionPoints.length<12) return;
        //根据框选矩形4个点和相机位置求出当前视景体的八个点,用视景体比较mesh是否在框选区域内
        this.selectionShape.updateMatrixWorld();
        let p1 = new Vector3(this.selectionPoints[0],this.selectionPoints[1],this.selectionPoints[2]).applyMatrix4(this.selectionShape.matrixWorld);
        let p2 = new Vector3(this.selectionPoints[3],this.selectionPoints[4],this.selectionPoints[5]).applyMatrix4(this.selectionShape.matrixWorld);
        let p3 = new Vector3(this.selectionPoints[6],this.selectionPoints[7],this.selectionPoints[8]).applyMatrix4(this.selectionShape.matrixWorld);
        let p4 = new Vector3(this.selectionPoints[9],this.selectionPoints[10],this.selectionPoints[11]).applyMatrix4(this.selectionShape.matrixWorld);
        let cameraPos = this.camera.getWorldPosition(new Vector3());
        let dir1 = p1.clone().sub(cameraPos).normalize();
        let dir2 = p2.clone().sub(cameraPos).normalize();
        let dir3 = p3.clone().sub(cameraPos).normalize();
        let dir4 = p4.clone().sub(cameraPos).normalize();

        let scale = 20;//可以理解为视景体最大深度,需要确保所有物体都在这范围内,根据需要修改
        let newPos1 = cameraPos.clone().add(dir1.clone().multiplyScalar(scale));
        let newPos2 = cameraPos.clone().add(dir2.clone().multiplyScalar(scale));
        let newPos3 = cameraPos.clone().add(dir3.clone().multiplyScalar(scale));

        let center = newPos1.clone().add(newPos3).multiplyScalar(0.5);
        let centerDir = center.clone().sub(cameraPos).normalize();
        let centerDis = center.clone().distanceTo(cameraPos);
        let top = newPos1.clone().add(newPos2).multiplyScalar(0.5);
        let topDir = top.clone().sub(cameraPos).normalize();
        let topScale = top.clone().distanceTo(cameraPos)/centerDis;
        let right = newPos2.clone().add(newPos3).multiplyScalar(0.5);
        let rightDir = right.clone().sub(cameraPos).normalize();
        let rightScale = right.clone().distanceTo(cameraPos)/centerDis;

        let highlightObj = [];
        let vertices = [];

        //遍历所有模型的所有mesh,查看这个mesh的包围盒中心是否在框选区域内
        for(var i=0;i<this.models.length;i++){
            this.models[i].traverse(node=>{
                if(node.isMesh || (node.type == "Sprite" && node._type != "measure" && node._type != "file")){
                    if(!node.visible) return;
                    let boxCenter = null;
                    if(node.geometry.boundingBox){
                        boxCenter = node.geometry.boundingBox.getCenter(new Vector3());
                        boxCenter.applyMatrix4(node.matrixWorld);
                    }
                     else{
                        let box = new Box3().expandByObject(node);
                        boxCenter = box.getCenter(new Vector3());
                    }
                    let centerDis = this.projectVector(boxCenter,cameraPos,centerDir);
                    let topDis = centerDis * topScale;
                    let rightDis = centerDis * rightScale;

                    let centerPos = cameraPos.clone().add(centerDir.clone().multiplyScalar(centerDis));
                    let topPos = cameraPos.clone().add(topDir.clone().multiplyScalar(topDis));
                    let rightPos = cameraPos.clone().add(rightDir.clone().multiplyScalar(rightDis));
 

                    let tempTopDir = topPos.clone().sub(centerPos).normalize();
                    let tempRightDir = rightPos.clone().sub(centerPos).normalize();

                    let X = rightPos.clone().distanceTo(centerPos);
                    let Y = topPos.clone().distanceTo(centerPos);

                    let x = this.projectVector(boxCenter,centerPos,tempRightDir.clone());
                    let y = this.projectVector(boxCenter,centerPos,tempTopDir.clone());

                    //在框选范围内
                    if(x<= X && y <= Y){
                        //node.visible = false;
                        highlightObj.push(node); //将区域内的节点缓存
                    }
                }
            })
         }
         if(this.highlightCB) this.highlightCB(highlightObj); //其它脚本定义的高亮函数,将选定的节点高亮显示
    }
    
	/**
     * 计算向量vec1在向量vec2上的投影长度
     */
    projectVector(pos1,pos2,dir){
        let angle = pos1.clone().sub(pos2).angleTo(dir);
        let dis = pos1.clone().distanceTo(pos2) * Math.cos(angle);
        return Math.abs(dis);
    }

完整代码

import {
    Line,
    Float32BufferAttribute,
    MathUtils,
    Vector3,
    Box3,
} from 'three'

/**
 * 本脚本用于框选物体,并将框选区域内的mesh隐藏,针对的是mesh,不是整个模型
 * 先使用setModel()将模型加入数组
 * 再使用changeMode()打开框选
 */
class SelectArea {
    constructor(viewer) {
        this.viewer = viewer;
        this.renderer = viewer.renderer;
        this.camera = viewer.activeCamera;
        this.controls = viewer.controls;//框选时需要把控制器(orbit)禁用
        this.scene = viewer.scene;
        this.useSelect = false;//这个值为true时才能框选物体
        this.currentIsPersectiveCamera = true; //只有透视相机才能框选

        this.selectionShape = null;//框选时在屏幕上出现的矩形区域

        this.startX = - Infinity;
        this.startY = - Infinity;
        this.startZ = -Infinity;

        this.prevX = - Infinity;
        this.prevY = - Infinity;
        this.prevZ = - Infinity;

        this.selectionPoints = [];
        this.dragging = false;
        this.selectionShapeNeedsUpdate = false;
        this.init();
    }

    /**
     * 将模型加入数组,方便后续比较,外部调用
     * @param {*} model 
     */
    setModel(model){
        if(model){
            this.models.push(model)
        }
    }

    /**
     * 切换是否框选物体
     */
    changeMode(enabled) {
        this.useSelect = enabled;
        if (this.controls)  //如果有orbit控制器,在框选时需要禁用
            this.controls.enabled = !this.useSelect;
    }

    init() {

        // selection shape
        this.selectionShape = new Line();
        this.selectionShape.material.color.set(0xFFFF00).convertSRGBToLinear();
        this.selectionShape.renderOrder = 1;
        this.selectionShape.position.z = - 0.2;
        this.selectionShape.depthTest = false;
        this.selectionShape.scale.setScalar(1);
        this.camera.add(this.selectionShape);

        //鼠标按下事件
        this.renderer.domElement.addEventListener('pointerdown', e => {
            if (!this.useSelect) return;
            this.prevX = e.clientX;
            this.prevY = e.clientY;
            var rect = this.renderer.domElement.getBoundingClientRect();
            this.startX = ((e.clientX  - rect.left) / rect.width) * 2 - 1;
            this.startY =(-(e.clientY - rect.top) / rect.height) * 2 + 1;

            this.startZ = 0

            this.selectionPoints.length = [];
            this.dragging = true;
        });

        //鼠标松开事件
        this.renderer.domElement.addEventListener('pointerup', () => {
            if (!this.useSelect) return;
            if(!this.currentIsPersectiveCamera) console.log("请使用透视相机框选!");
            this.shape();
            this.selectionShape.visible = false;
            this.selectionPoints.length = [];
            this.selectionShapeNeedsUpdate = true;
            this.dragging = false;
        });

        //鼠标移动事件
        this.renderer.domElement.addEventListener('pointermove', e => {
            if (!this.useSelect) return;
            // If the left mouse button is not pressed
            if ((1 & e.buttons) === 0) {
                return;
            }

            const ex = e.clientX;
            const ey = e.clientY;

            var rect = this.renderer.domElement.getBoundingClientRect();
            let nx =((e.clientX  - rect.left) / rect.width) * 2 - 1;// (e.clientX / window.innerWidth) * 2 - 1;
            let ny = (-(e.clientY - rect.top) / rect.height) * 2 + 1;//- ((e.clientY / window.innerHeight) * 2 - 1);

            // set points for the corner of the box
            this.selectionPoints.length = 3 * 5;

            this.selectionPoints[0] = this.startX;
            this.selectionPoints[1] = this.startY;
            this.selectionPoints[2] = this.startZ;

            this.selectionPoints[3] = nx;
            this.selectionPoints[4] = this.startY;
            this.selectionPoints[5] = this.startZ;

            this.selectionPoints[6] = nx;
            this.selectionPoints[7] = ny;
            this.selectionPoints[8] = this.startZ;

            this.selectionPoints[9] = this.startX;
            this.selectionPoints[10] = ny;
            this.selectionPoints[11] = this.startZ;

            this.selectionPoints[12] = this.startX;
            this.selectionPoints[13] = this.startY;
            this.selectionPoints[14] = this.startZ;


            if (ex !== this.prevX || ey !== this.prevY) {
                this.selectionShapeNeedsUpdate = true;
                if(!this.selectionShape.visible)
                    this.selectionShape.visible = true;
            }
            this.prevX = ex;
            this.prevY = ey;
            this.selectionShape.visible = true;
        });

    }

    /**
     *更新框选形状
     */
    update() {
        if (!this.selectionShape) return;
        // Update the selection lasso lines
        if (this.selectionShapeNeedsUpdate) {
            this.selectionShape.geometry.setAttribute(
                'position',
                new Float32BufferAttribute(this.selectionPoints, 3, false)
            );

            this.selectionShape.frustumCulled = false;
            this.selectionShapeNeedsUpdate = false;
        }

        //根据相机设置框选形状的位置,让它显示在屏幕上
        const yScale = Math.tan(MathUtils.DEG2RAD * this.camera.fov / 2) * this.selectionShape.position.z;
        this.selectionShape.scale.set(- yScale * this.camera.aspect, - yScale, 1);
    }

    setHighlightCB(cb){
        this.highlightCB = cb;
    }

    /**
     * 松开鼠标后开始比较、隐藏mesh
     */
    shape(){
        if(this.selectionPoints.length<12) return;
        //根据框选矩形4个点和相机位置求出当前视景体的八个点,用视景体比较mesh是否在框选区域内
        this.selectionShape.updateMatrixWorld();
        let p1 = new Vector3(this.selectionPoints[0],this.selectionPoints[1],this.selectionPoints[2]).applyMatrix4(this.selectionShape.matrixWorld);
        let p2 = new Vector3(this.selectionPoints[3],this.selectionPoints[4],this.selectionPoints[5]).applyMatrix4(this.selectionShape.matrixWorld);
        let p3 = new Vector3(this.selectionPoints[6],this.selectionPoints[7],this.selectionPoints[8]).applyMatrix4(this.selectionShape.matrixWorld);
        let p4 = new Vector3(this.selectionPoints[9],this.selectionPoints[10],this.selectionPoints[11]).applyMatrix4(this.selectionShape.matrixWorld);
        let cameraPos = this.camera.getWorldPosition(new Vector3());
        let dir1 = p1.clone().sub(cameraPos).normalize();
        let dir2 = p2.clone().sub(cameraPos).normalize();
        let dir3 = p3.clone().sub(cameraPos).normalize();
        let dir4 = p4.clone().sub(cameraPos).normalize();

        let scale = 20;//可以理解为视景体最大深度,需要确保所有物体都在这范围内,外面的物体不会进行比较
        let newPos1 = cameraPos.clone().add(dir1.clone().multiplyScalar(scale));
        let newPos2 = cameraPos.clone().add(dir2.clone().multiplyScalar(scale));
        let newPos3 = cameraPos.clone().add(dir3.clone().multiplyScalar(scale));

        let center = newPos1.clone().add(newPos3).multiplyScalar(0.5);
        let centerDir = center.clone().sub(cameraPos).normalize();
        let centerDis = center.clone().distanceTo(cameraPos);
        let top = newPos1.clone().add(newPos2).multiplyScalar(0.5);
        let topDir = top.clone().sub(cameraPos).normalize();
        let topScale = top.clone().distanceTo(cameraPos)/centerDis;
        let right = newPos2.clone().add(newPos3).multiplyScalar(0.5);
        let rightDir = right.clone().sub(cameraPos).normalize();
        let rightScale = right.clone().distanceTo(cameraPos)/centerDis;

        let highlightObj = [];
        let vertices = [];

        //遍历所有模型的所有mesh,查看这个mesh的包围盒中心是否在框选区域内
        for(var i=0;i<this.models.length;i++){
            this.models[i].traverse(node=>{
                if(node.isMesh || (node.type == "Sprite" && node._type != "measure" && node._type != "file")){
                    if(!node.visible) return;
                    let boxCenter = null;
                    if(node.geometry.boundingBox){
                        boxCenter = node.geometry.boundingBox.getCenter(new Vector3());
                        boxCenter.applyMatrix4(node.matrixWorld);
                    }
                     else{
                        let box = new Box3().expandByObject(node);
                        boxCenter = box.getCenter(new Vector3());
                    }
                    let centerDis = this.projectVector(boxCenter,cameraPos,centerDir);
                    let topDis = centerDis * topScale;
                    let rightDis = centerDis * rightScale;

                    let centerPos = cameraPos.clone().add(centerDir.clone().multiplyScalar(centerDis));
                    let topPos = cameraPos.clone().add(topDir.clone().multiplyScalar(topDis));
                    let rightPos = cameraPos.clone().add(rightDir.clone().multiplyScalar(rightDis));
 

                    let tempTopDir = topPos.clone().sub(centerPos).normalize();
                    let tempRightDir = rightPos.clone().sub(centerPos).normalize();

                    let X = rightPos.clone().distanceTo(centerPos);
                    let Y = topPos.clone().distanceTo(centerPos);

                    let x = this.projectVector(boxCenter,centerPos,tempRightDir.clone());
                    let y = this.projectVector(boxCenter,centerPos,tempTopDir.clone());

                    //在框选范围内
                    if(x<= X && y <= Y){
                        //node.visible = false;
                        highlightObj.push(node);
                    }
                }
            })
        }
        if(this.highlightCB) this.highlightCB(highlightObj);
    }


    /**
     * 计算向量vec1在向量vec2上的投影长度
     */
    projectVector(pos1,pos2,dir){
        let angle = pos1.clone().sub(pos2).angleTo(dir);
        let dis = pos1.clone().distanceTo(pos2) * Math.cos(angle);
        return Math.abs(dis);
    }

}
export { SelectArea }

2022.10.28 更新

1、优化检测mesh方式
2、增加正交相机框选,使用setCamera函数切换相机
最新代码如下

import { Line, Float32BufferAttribute, MathUtils, Vector3, Box3, BufferGeometry } from 'three'

/**
 * 本脚本用于框选物体,并将框选区域内的mesh隐藏,针对的是mesh,不是整个模型
 * 先使用setModel()将模型加入数组
 * 再使用changeMode()打开框选
 */
class SelectArea {
  constructor(viewer) {
    this.viewer = viewer
    this.renderer = viewer.renderer
    this.camera = viewer.activeCamera
    this.controls = viewer.controls //框选时需要把控制器(orbit)禁用
    this.scene = viewer.scene
    this.useSelect = false //这个值为true时才能框选物体
    this.models = []

    this.selectionShape = null //框选时在屏幕上出现的矩形区域

    this.startX = -Infinity
    this.startY = -Infinity
    this.startZ = 0

    this.prevX = -Infinity
    this.prevY = -Infinity

    this.startPageX = 0
    this.startPageY = 0
    this.endPageX = 0
    this.endPageY = 0

    this.selectionPoints = []
    this.rect = null
    this.init()
  }

  /**
   * 将模型加入数组,方便后续比较,外部调用
   * @param {*} model
   */
  setModel(model) {
    if (model) {
      this.models.push(model)
    }
  }

  /**
   * 切换相机 可以正交、透视相互切换,也可以同种类型的不同相机切换
   * @param {*} camera
   */
  setCamera(camera) {
    this.camera = camera
    if (camera.isPerspectiveCamera) {
      this.camera.add(this.selectionShape)
      this.selectionShape.position.z = -1
    } else {
      this.scene.add(this.selectionShape)
      this.selectionShape.position.set(0, 0, 0)
      this.selectionShape.rotation.set(0, 0, 0)
      this.selectionShape.scale.set(1, 1, 1)
    }
  }

  /**
   * 切换是否框选物体
   */
  changeMode(enabled) {
    if (this.useSelect == enabled) return
    this.useSelect = enabled
    if (this.controls)
      //如果有orbit控制器,在框选时需要禁用
      this.controls.enabled = !this.useSelect
  }

  init() {
    // selection shape
    this.selectionShape = new Line()
    this.selectionShape.material.color.set(0xffff00).convertSRGBToLinear()
    this.selectionShape.renderOrder = 1
    this.selectionShape.position.z = -1
    this.selectionShape.material.depthTest = false
    this.selectionShape.scale.setScalar(1)
    this.selectionShape.frustumCulled = false
    this.camera.add(this.selectionShape)

    //鼠标按下事件
    this.renderer.domElement.addEventListener('pointerdown', e => {
      if (!this.useSelect) return
      this.prevX = e.clientX
      this.prevY = e.clientY

      this.rect = this.renderer.domElement.getBoundingClientRect()
      this.startX = ((e.clientX - this.rect.left) / this.rect.width) * 2 - 1
      this.startY = (-(e.clientY - this.rect.top) / this.rect.height) * 2 + 1

      this.startPageX = e.clientX
      this.startPageY = e.clientY

      this.selectionPoints.length = []
    })

    //鼠标松开事件
    this.renderer.domElement.addEventListener('pointerup', () => {
      if (!this.useSelect) return

      this.shape()
      this.selectionShape.visible = false
      this.selectionPoints.length = []
    })

    //鼠标移动事件
    this.renderer.domElement.addEventListener('pointermove', e => {
      if (!this.useSelect) return
      // If the left mouse button is not pressed
      if ((1 & e.buttons) === 0) {
        return
      }

      const ex = e.clientX
      const ey = e.clientY

      this.rect = this.renderer.domElement.getBoundingClientRect()
      let nx = ((e.clientX - this.rect.left) / this.rect.width) * 2 - 1 // (e.clientX / window.innerWidth) * 2 - 1;
      let ny = (-(e.clientY - this.rect.top) / this.rect.height) * 2 + 1 //- ((e.clientY / window.innerHeight) * 2 - 1);

      // set points for the corner of the box
      this.selectionPoints.length = 3 * 5

      this.selectionPoints[0] = this.startX
      this.selectionPoints[1] = this.startY
      this.selectionPoints[2] = this.startZ

      this.selectionPoints[3] = nx
      this.selectionPoints[4] = this.startY
      this.selectionPoints[5] = this.startZ

      this.selectionPoints[6] = nx
      this.selectionPoints[7] = ny
      this.selectionPoints[8] = this.startZ

      this.selectionPoints[9] = this.startX
      this.selectionPoints[10] = ny
      this.selectionPoints[11] = this.startZ

      this.selectionPoints[12] = this.startX
      this.selectionPoints[13] = this.startY
      this.selectionPoints[14] = this.startZ

      if (ex !== this.prevX || ey !== this.prevY) {
        this.endPageX = e.clientX
        this.endPageY = e.clientY
        this.update()
        if (!this.selectionShape.visible) this.selectionShape.visible = true
      }
      this.prevX = ex
      this.prevY = ey
    })
  }

  /**
   *更新框选形状
   */
  update() {
    if (!this.selectionShape || !this.useSelect) return

    //根据相机设置框选形状的位置,让它显示在屏幕上
    if (this.camera.isPerspectiveCamera) {
      this.selectionShape.geometry.setAttribute('position', new Float32BufferAttribute(this.selectionPoints, 3, false))

      const yScale = Math.tan((MathUtils.DEG2RAD * this.camera.fov) / 2) * this.selectionShape.position.z
      this.selectionShape.scale.set(-yScale * this.camera.aspect, -yScale, 1)
    } else {
      this.rect = this.renderer.domElement.getBoundingClientRect()
      const startX = (this.startPageX - this.rect.width / 2) / (this.rect.width / 2)
      const startY = (this.rect.height / 2 - this.startPageY) / (this.rect.height / 2)
      const endX = (this.endPageX - this.rect.width / 2) / (this.rect.width / 2)
      const endY = (this.rect.height / 2 - this.endPageY) / (this.rect.height / 2)

      this.camera.updateProjectionMatrix()
      let vec1 = new Vector3(startX, startY, -1).unproject(this.camera)
      let vec2 = new Vector3(endX, startY, -1).unproject(this.camera)
      let vec3 = new Vector3(endX, endY, -1).unproject(this.camera)
      let vec4 = new Vector3(startX, endY, -1).unproject(this.camera)
      let vec5 = vec1.clone()

      let dir = this.camera.getWorldDirection(new Vector3()).normalize()
      vec1.add(dir.clone().multiplyScalar(1.1))
      vec2.add(dir.clone().multiplyScalar(1.1))
      vec3.add(dir.clone().multiplyScalar(1.1))
      vec4.add(dir.clone().multiplyScalar(1.1))
      vec5.add(dir.clone().multiplyScalar(1.1))

      let points = [vec1, vec2, vec3, vec4, vec5]
      this.selectionShape.geometry.dispose()
      this.selectionShape.geometry = new BufferGeometry().setFromPoints(points)
    }
  }

  setHighlightCB(cb) {
    this.highlightCB = cb
  }

  /**
   * 松开鼠标后开始比较、隐藏mesh
   */
  shape() {
    let highlightObj = []
    if (this.selectionPoints.length < 12) return []
    //根据框选矩形4个点和相机位置求出当前视景体的八个点,用视景体比较mesh是否在框选区域内
    let p1, p2, p3, p4
    if (this.camera.isPerspectiveCamera) {
      this.selectionShape.updateMatrixWorld()
      p1 = new Vector3(this.selectionPoints[0], this.selectionPoints[1], this.selectionPoints[2]).applyMatrix4(
        this.selectionShape.matrixWorld
      )
      p2 = new Vector3(this.selectionPoints[3], this.selectionPoints[4], this.selectionPoints[5]).applyMatrix4(
        this.selectionShape.matrixWorld
      )
      p3 = new Vector3(this.selectionPoints[6], this.selectionPoints[7], this.selectionPoints[8]).applyMatrix4(
        this.selectionShape.matrixWorld
      )
      p4 = new Vector3(this.selectionPoints[9], this.selectionPoints[10], this.selectionPoints[11]).applyMatrix4(
        this.selectionShape.matrixWorld
      )
    } else {
      const position = this.selectionShape.geometry.getAttribute('position')
      p1 = new Vector3(position.getX(0), position.getY(0), position.getZ(0))
      p2 = new Vector3(position.getX(1), position.getY(1), position.getZ(1))
      p3 = new Vector3(position.getX(2), position.getY(2), position.getZ(2))
      p4 = new Vector3(position.getX(3), position.getY(3), position.getZ(3))
    }
    //求出4个点投影在相机上的位置  ,符合笛卡尔坐标系,范围[-1,1]
    p1 = p1.project(this.camera)
    p2 = p2.project(this.camera)
    p3 = p3.project(this.camera)
    p4 = p4.project(this.camera)

    let maxX = Math.max(p1.x, p2.x, p3.x, p4.x)
    let maxY = Math.max(p1.y, p2.y, p3.y, p4.y)
    let minX = Math.min(p1.x, p2.x, p3.x, p4.x)
    let minY = Math.min(p1.y, p2.y, p3.y, p4.y)

    //遍历所有模型的所有mesh,查看这个mesh的包围盒中心是否在框选区域内
    for (var i = 0; i < this.models.length; i++) {
      this.models[i].traverse(node => {
        if (node.isMesh || (node.type == 'Sprite' && node._type != 'measure' && node._type != 'file')) {
          if (!node.visible) return
          let boxCenter = null
          if (node.geometry.boundingBox) {
            boxCenter = node.geometry.boundingBox.getCenter(new Vector3())
            boxCenter.applyMatrix4(node.matrixWorld)
          } else {
            let box = new Box3().expandByObject(node)
            boxCenter = box.getCenter(new Vector3())
          }
          //求出包围盒中心投影在相机上的位置
          let pro = boxCenter.project(this.camera)
          //判断这个点是否在4个点范围内
          if (pro.x > minX && pro.x < maxX && pro.y > minY && pro.y < maxY) {
            highlightObj.push(node)
          }
        }
      })
    }
    if (this.highlightCB) this.highlightCB(highlightObj)
  }
}
export { SelectArea }

参考

Threejs 官方BVH里的一个案例

PS:后面才发现threejs官方出了一个框选的案例。。。

  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 19
    评论
three.js中实现框选功能可以通过以下步骤来实现: 1. 创建一个框选区域:通过鼠标事件监听用户的鼠标按下、移动和释放动作,记录下鼠标按下的位置和移动的位置,根据这两个位置计算出框选区域的位置和大小。 2. 射线检测:使用three.js提供的射线检测功能,将框选区域转换为一个射线,然后与场景中的物体进行相交检测。可以使用`Raycaster`类来进行射线检测。 3. 获取选中的物体:根据射线检测的结果,可以获取到与框选区域相交的物体。可以通过遍历射线检测的结果,获取到选中的物体。 下面是一个示例代码,演示了如何在three.js中实现框选功能: ```javascript // 创建一个框选区域 var boxSelect = document.createElement('div'); boxSelect.style.position = 'absolute'; boxSelect.style.border = '1px dashed red'; boxSelect.style.pointerEvents = 'none'; document.body.appendChild(boxSelect); var mouseDown = false; var startMousePos = new THREE.Vector2(); var endMousePos = new THREE.Vector2(); // 监听鼠标按下事件 document.addEventListener('mousedown', function(event) { mouseDown = true; startMousePos.x = (event.clientX / window.innerWidth) * 2 - 1; startMousePos.y = -(event.clientY / window.innerHeight) * 2 + 1; }); // 监听鼠标移动事件 document.addEventListener('mousemove', function(event) { if (mouseDown) { endMousePos.x = (event.clientX / window.innerWidth) * 2 - 1; endMousePos.y = -(event.clientY / window.innerHeight) * 2 + 1; // 更新框选区域的位置和大小 var boxWidth = Math.abs(endMousePos.x - startMousePos.x); var boxHeight = Math.abs(endMousePos.y - startMousePos.y); boxSelect.style.left = Math.min(startMousePos.x, endMousePos.x) * 100 + '%'; boxSelect.style.top = Math.min(startMousePos.y, endMousePos.y) * 100 + '%'; boxSelect.style.width = boxWidth * 100 + '%'; boxSelect.style.height = boxHeight * 100 + '%'; } }); // 监听鼠标释放事件 document.addEventListener('mouseup', function(event) { mouseDown = false; // 将框选区域转换为射线 var raycaster = new THREE.Raycaster(); raycaster.setFromCamera(startMousePos, camera); // 进行射线检测 var intersects = raycaster.intersectObjects(scene.children, true); // 获取选中的物体 var selectedObjects = []; for (var i = 0; i < intersects.length; i++) { selectedObjects.push(intersects[i].object); } // 处理选中的物体 // ... }); ``` 请注意,上述代码中的`camera`和`scene`是需要根据你的具体场景进行替换的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值