Three.js拖拽框始终生成在原点,按下鼠标时不跟随光标位置怎么办?
Three.js拖拽框缩放功能异常问题
我尝试在Three.js渲染场景中实现拖拽框缩放功能,用户拖拽框选区域后相机会缩放至该区域,但拖拽框生成异常。我尝试使用mesh.position来调整位置,但似乎没有任何效果。以下是我的代码:
public dragZoom(value: boolean): void { const geometry = new THREE.PlaneGeometry(1, 1); const material = new THREE.MeshBasicMaterial({ color: 0x808080, opacity: 0.1, transparent: true, }); let mesh = new THREE.Mesh(geometry, material); let isDragging = false; let initialCursorPos = new THREE.Vector2(); let initialBoxPos = new THREE.Vector3(); const borderGeometry = new THREE.EdgesGeometry(geometry); const borderMaterial = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2 }); const borderLines = new THREE.LineSegments(borderGeometry, borderMaterial); mesh.add(borderLines); const onMouseDown = (event: MouseEvent) => { isDragging = true; initialCursorPos.x = event.clientX; initialCursorPos.y = event.clientY; initialBoxPos.x = (event.clientX / window.innerWidth) * 2 - 1; initialBoxPos.y = -(event.clientY / window.innerHeight) * 2 + 1; mesh.position.set(initialBoxPos.x, initialBoxPos.y, 0); this.scene.add(mesh); }; const onMouseMove = (event: MouseEvent) => { if (isDragging) { const deltaX = (event.clientX - initialCursorPos.x) / 40; // Divide by a smaller value for less aggressive scaling const deltaY = (event.clientY - initialCursorPos.y) / 40; // Divide by a smaller value for less aggressive scaling mesh.scale.set(deltaX, deltaY, 1); const boxSize = new THREE.Box3().setFromObject(mesh).getSize(new THREE.Vector3()); const width = boxSize.x; const height = boxSize.y; const newX = initialBoxPos.x + (width / 2) * Math.sign(deltaX); const newY = initialBoxPos.y - (height / 2) * Math.sign(deltaY); mesh.position.set(newX, newY, 0); } }; const onMouseUp = () => { if (isDragging) { isDragging = false; const boundingBox = new THREE.Box3().setFromObject(mesh); const boxSize = boundingBox.getSize(new THREE.Vector3()); const cameraAspect = this.camera.aspect; const horizontalPadding = (boxSize.x - boxSize.y * cameraAspect) / 2; const verticalPadding = (boxSize.y - boxSize.x / cameraAspect) / 2; this.controls.fitToBox(mesh, true, { paddingTop: verticalPadding, paddingBottom: verticalPadding, paddingLeft: horizontalPadding, paddingRight: horizontalPadding, }); this.scene.remove(mesh); mesh = null; } }; this.renderer.domElement.addEventListener('mousedown', onMouseDown); this.renderer.domElement.addEventListener('mousemove', onMouseMove); this.renderer.domElement.addEventListener('mouseup', onMouseUp); }
问题分析与修复方案
1. 核心问题:坐标转换逻辑错误
直接将屏幕坐标转成NDC坐标赋值给mesh.position,未通过相机的投影矩阵逆转换到世界空间,导致拖拽框位置完全偏离鼠标区域。
2. 缩放与位置计算逻辑混乱
用deltaX/40直接作为缩放值,会导致拖拽方向与框选区域方向错位,且位置调整公式未匹配世界坐标的逻辑。
3. 事件绑定重复风险
每次调用dragZoom都会重复绑定鼠标事件,触发多次回调,导致拖拽行为异常。
修改后的完整代码
public dragZoom(value: boolean): void { // 先移除旧事件,避免重复绑定 this.renderer.domElement.removeEventListener('mousedown', onMouseDown); this.renderer.domElement.removeEventListener('mousemove', onMouseMove); this.renderer.domElement.removeEventListener('mouseup', onMouseUp); if (!value) return; const geometry = new THREE.PlaneGeometry(1, 1); const material = new THREE.MeshBasicMaterial({ color: 0x808080, opacity: 0.1, transparent: true, depthTest: false, // 确保拖拽框显示在最上层 }); let mesh = new THREE.Mesh(geometry, material); mesh.visible = false; // 初始隐藏,拖拽时显示 const borderGeometry = new THREE.EdgesGeometry(geometry); const borderMaterial = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2 }); const borderLines = new THREE.LineSegments(borderGeometry, borderMaterial); mesh.add(borderLines); this.scene.add(mesh); let isDragging = false; let initialScreenPos = new THREE.Vector2(); let initialWorldPos = new THREE.Vector3(); // 屏幕坐标转世界坐标工具函数 const screenToWorld = (x: number, y: number): THREE.Vector3 => { const vector = new THREE.Vector3( (x / window.innerWidth) * 2 - 1, -(y / window.innerHeight) * 2 + 1, 0.5 ); vector.unproject(this.camera); const dir = vector.sub(this.camera.position).normalize(); const distance = -this.camera.position.z / dir.z; return this.camera.position.clone().add(dir.multiplyScalar(distance)); }; const onMouseDown = (event: MouseEvent) => { isDragging = true; initialScreenPos.set(event.clientX, event.clientY); initialWorldPos = screenToWorld(event.clientX, event.clientY); mesh.position.copy(initialWorldPos); mesh.scale.set(1, 1, 1); mesh.visible = true; }; const onMouseMove = (event: MouseEvent) => { if (!isDragging) return; const currentWorldPos = screenToWorld(event.clientX, event.clientY); const delta = currentWorldPos.clone().sub(initialWorldPos); // 计算框选宽高与中心位置 const width = Math.abs(delta.x); const height = Math.abs(delta.y); mesh.scale.set(width, height, 1); const centerX = initialWorldPos.x + delta.x / 2; const centerY = initialWorldPos.y + delta.y / 2; mesh.position.set(centerX, centerY, 0); }; const onMouseUp = () => { if (!isDragging) return; isDragging = false; mesh.visible = false; const boundingBox = new THREE.Box3().setFromObject(mesh); const boxSize = boundingBox.getSize(new THREE.Vector3()); // 过滤过小的拖拽框,避免误操作 if (boxSize.x < 0.1 || boxSize.y < 0.1) return; // 用固定padding替代动态计算,避免异常值 const padding = 0.1; this.controls.fitToBox(boundingBox, true, { paddingTop: padding, paddingBottom: padding, paddingLeft: padding, paddingRight: padding, }); }; this.renderer.domElement.addEventListener('mousedown', onMouseDown); this.renderer.domElement.addEventListener('mousemove', onMouseMove); this.renderer.domElement.addEventListener('mouseup', onMouseUp); }
关键修改说明
- 坐标转换:新增
screenToWorld函数,通过unproject将屏幕坐标转换为世界空间坐标,确保拖拽框位置与鼠标完全对齐。 - 拖拽逻辑优化:基于初始与当前世界坐标的差值计算框选区域,调整mesh的缩放与位置,实现自然的拖拽框选效果。
- 事件管理:添加事件移除逻辑,避免重复绑定导致的异常回调。
- 视觉与交互优化:设置
depthTest: false让拖拽框始终在最上层,初始隐藏拖拽框,添加最小尺寸过滤避免误触发。
内容的提问来源于stack exchange,提问作者SourBanana
相关产品推荐
相关产品推荐

