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

如何点击Three.js 3D画布选择并修改Fabric.js对象

问题描述

我有一个包含TextureCanvas对象的Three.js场景,该TextureCanvas内嵌入了Fabric.js画布,画布上有一个矩形对象。我希望点击这个3D场景中的对象后,能实现和直接在Fabric.js画布上操作一样的修改效果(比如缩放、移动等)。

我参考过两个示例,它们都是将3D画布和2D画布并列展示,用户可以在2D画布中修改对象;其中一个示例支持点击Three.js管理的3D画布来添加Fabric.js圆形对象,该对象会同步显示在2D画布中,还能在2D画布拖拽边角进行缩放。

我的场景里没有2D画布视图,自己做了一个示例,点击3D画布上的正方形可以选中它(显示边角)并缩放(2D画布仅作参考用),这说明我已经能通过3D画布和Fabric.js对象进行交互。

现在我想实现和2D画布操作完全一致的移动、修改功能,并且已经获取到了鼠标在2D画布中的对应坐标。请问有没有办法触发选择最近的边角控制点,然后模拟拖拽操作,达到和直接在2D画布上拖拽修改一样的效果?

附上我的点击事件代码:

function onMouseClick( evt ) {
  console.log('BEG renderer onMouseClick');
  evt.preventDefault();

  var array = getMousePosition( container, evt.clientX, evt.clientY );
  onClickPosition.fromArray( array );

  var intersects = getIntersects( onClickPosition, scene.children );

  if ( intersects.length > 0 && intersects[ 0 ].uv ) {
    var uv = intersects[ 0 ].uv;
    intersects[ 0 ].object.material.map.transformUv( uv );

    var x2 = getRealPosition( "x", uv.x );
    var y2 = getRealPosition( "x", uv.y );
    const pointer = {x: x2, y: y2}
    const target = canvas._searchPossibleTargets(canvas.getObjects(), pointer)
    let foundFabricjsObject = false;
    counter +=1;
    console.log('counter: ', counter);
    if (target) {
      console.log('Clicked on a fabricjs object');
      foundFabricjsObject = true;
      target.set('fill', 'red');
      if(counter%2 == 0) {
        console.log('scale the selected fabricjs object by 0.5');
        target.scale(0.5);
      }
      else {
        console.log('scale the selected fabricjs object by 2');
        target.scale(2.0);
      }
        
      canvas.setActiveObject(target).requestRenderAll()
    } else {
      console.log('Clicked NOT on a fabricjs object');
      canvas.discardActiveObject().requestRenderAll()
    }
    
    if(!foundFabricjsObject) {
      var circle = new fabric.Circle({
        radius: 3,
        left: getRealPosition( "x", uv.x ),
        top: getRealPosition( "y", uv.y ),
        fill: 'red'
      });
  
      canvas.add( circle );
    }
  }
}

解决方案

要模拟Fabric.js原生的边角拖拽操作,你可以直接复用Fabric.js内部的交互逻辑,通过模拟完整的鼠标事件流来触发控制点拖拽,无需手动修改对象属性。具体实现步骤如下:

1. 检测点击位置是否命中对象控制点

当你把3D坐标转换为Fabric.js画布上的pointer坐标后,除了检测是否命中对象本身,还要检查是否命中对象的缩放/旋转控制点。可以使用Fabric.js的内部方法_findTargetCorner来完成检测:

// 假设currentTarget是已选中的Fabric.js对象
const corner = currentTarget._findTargetCorner(pointer);

如果corner返回有效值(比如'tl'代表左上角、'br'代表右下角等),说明点击命中了对象的控制点。

2. 模拟完整的Fabric.js交互事件流

一旦确定命中控制点或对象本身,就需要在Fabric.js画布上模拟mouse:down、mouse:move、mouse:up这三个核心事件,还原原生交互逻辑:

// 全局变量记录拖拽状态
let isDragging = false;
let currentTarget = null;
let currentCorner = null;

// 鼠标按下事件
function onMouseDown(evt) {
  evt.preventDefault();
  // 3D坐标转Fabric.js画布坐标(复用你现有转换逻辑)
  const array = getMousePosition(container, evt.clientX, evt.clientY);
  const clickPos = new THREE.Vector2().fromArray(array);
  const intersects = getIntersects(clickPos, scene.children);
  
  if (intersects.length > 0 && intersects[0].uv) {
    const uv = intersects[0].uv;
    intersects[0].object.material.map.transformUv(uv);
    const pointer = {
      x: getRealPosition("x", uv.x),
      y: getRealPosition("y", uv.y)
    };

    currentTarget = canvas._searchPossibleTargets(canvas.getObjects(), pointer);
    if (currentTarget) {
      canvas.setActiveObject(currentTarget);
      // 检测是否命中控制点
      currentCorner = currentTarget._findTargetCorner(pointer);
      isDragging = true;
      
      // 触发Fabric.js的mouse:down事件
      canvas.fire('mouse:down', {
        pointer: pointer,
        target: currentTarget,
        corner: currentCorner
      });
      canvas.requestRenderAll();
    }
  }
}

// 鼠标移动事件
function onMouseMove(evt) {
  if (!isDragging || !currentTarget) return;
  
  evt.preventDefault();
  // 坐标转换
  const array = getMousePosition(container, evt.clientX, evt.clientY);
  const movePos = new THREE.Vector2().fromArray(array);
  const intersects = getIntersects(movePos, scene.children);
  
  if (intersects.length > 0 && intersects[0].uv) {
    const uv = intersects[0].uv;
    intersects[0].object.material.map.transformUv(uv);
    const pointer = {
      x: getRealPosition("x", uv.x),
      y: getRealPosition("y", uv.y)
    };
    
    // 触发Fabric.js的mouse:move事件
    canvas.fire('mouse:move', {
      pointer: pointer,
      target: currentTarget,
      corner: currentCorner
    });
    canvas.requestRenderAll();
  }
}

// 鼠标松开事件
function onMouseUp(evt) {
  if (!isDragging || !currentTarget) return;
  
  evt.preventDefault();
  // 坐标转换
  const array = getMousePosition(container, evt.clientX, evt.clientY);
  const upPos = new THREE.Vector2().fromArray(array);
  const intersects = getIntersects(upPos, scene.children);
  
  const pointer = intersects.length > 0 && intersects[0].uv 
    ? {
        x: getRealPosition("x", intersects[0].uv.x),
        y: getRealPosition("y", intersects[0].uv.y)
      }
    : {x: 0, y: 0};

  // 触发Fabric.js的mouse:up事件
  canvas.fire('mouse:up', {
    pointer: pointer,
    target: currentTarget,
    corner: currentCorner
  });
  
  // 重置拖拽状态
  isDragging = false;
  currentTarget = null;
  currentCorner = null;
  canvas.requestRenderAll();
}

// 给Three.js容器绑定事件
container.addEventListener('mousedown', onMouseDown);
container.addEventListener('mousemove', onMouseMove);
container.addEventListener('mouseup', onMouseUp);
container.addEventListener('mouseleave', onMouseUp); // 防止鼠标移出容器后状态残留

3. 关键注意事项

  • 兼容性处理:_findTargetCorner是Fabric.js内部方法,若担心版本兼容性,可自行实现控制点检测:计算对象每个控制点的屏幕坐标,判断鼠标坐标是否在控制点的检测范围内(比如给每个控制点设置10px的检测半径)。
  • 坐标准确性:确保getRealPosition方法能精准将UV坐标映射到Fabric.js画布的实际像素坐标,否则交互会出现偏移。
  • 渲染同步:每次触发Fabric.js事件后调用canvas.requestRenderAll(),保证Three.js的TextureCanvas能同步更新渲染结果。

内容的提问来源于stack exchange,提问作者Avner Moshkovitz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 14:54:55