如何点击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

