You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 08:25:12