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 }
2025.02.28 更新
正交相机的框选形状似乎会错位,如下图,红色为实际框选范围,黄色为显示范围,修改update函数,其他不变
/**
*更新框选形状
*/
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 {
const startX =
((this.startPageX - this.rect.left) / this.rect.width) * 2 - 1;
const startY =
(-(this.startPageY - this.rect.top) / this.rect.height) * 2 + 1;
const endX = ((this.endPageX - this.rect.left) / this.rect.width) * 2 - 1;
const endY =
(-(this.endPageY - this.rect.top) / this.rect.height) * 2 + 1;
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);
}
}
参考
PS:后面才发现threejs官方出了一个框选的案例。。。