现状:Threejs官方demo提供了DragControls.js平面拖拽控件,但只能拖拽Object在垂直于相机法线的平面上移动
目标场景:在六面体空间盒子中沿着六面体平面拖拽物体,并且需要限制在盒子内部
已知:空间盒子大小,对象所在的平面
方案设计:
- 将Object拖拽依赖的信息放在userData中,示例如下
- 基于拖拽控件,改造部分关键代码,实现目标
// 模型对象部分参数
{
// ...
userData: {
type: 'tm', // Object类型,按需定义
mesh: {
// ...
position3D: {
// ...
d: 'bottom', // Object 所在的平面,取值bottom,up,back,front,left,right
limit: {
x: {
min: -500,
max: 500,
},
y: {
min: 0,
max: 1000,
},
z: {
min: -500,
max: 500,
},
}
}
}
}
}
源码分析
/** DragControls.js插件关键代码展示 */
// let _selected = null; // 选中的对象
const _plane = new Plane(); // 移动平面
const _raycaster = new Raycaster(); // 基于鼠标和相机位置确定的射线
function onPointerDown( event ) {
// 此处省略...
if ( _intersections.length > 0 ) {
// 赋值选中对象
_selected = ( scope.transformGroup === true ) ? _objects[ 0 ] : _intersections[ 0 ].object;
// 根据选中对象位置和相机的法线确定可拖拽平面
_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
}
// 此处省略...
}
function onPointerMove( event ) {
// 此处省略...
if ( _selected ) {
// 判断射线和平面相交,赋值Object新的位置
if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
_selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
}
}
// 此处省略...
}
let _selected = null; // 选中的对象
const _plane = new Plane(); // 移动平面
const _raycaster = new Raycaster(); // 基于鼠标和相机位置确定的射线
function onPointerDown( event ) {
// 此处省略...
if ( _intersections.length > 0 ) {
// 赋值选中对象
_selected = ( scope.transformGroup === true ) ? _objects[ 0 ] : _intersections[ 0 ].object;
// 根据选中对象位置和相机的法线确定可拖拽平面
_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
}
// 此处省略...
}
function onPointerMove( event ) {
// 此处省略...
if ( _selected ) {
// 判断射线和平面相交,赋值Object新的位置
if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
_selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
}
}
// 此处省略...
}
代码改造
/** newDragControls.js 改造后关键代码 */
// 假设空间盒子的底面位于xz平面,y正方向放置
// 六面体的法线方向
const planeNormal = {
'bottom': new Vector3(0, 1, 0),
'up': new Vector3(0, 1, 0),
'back': new Vector3(0, 0, 1),
'front': new Vector3(0, 0, 1),
'left': new Vector3(1, 0, 0),
'right': new Vector3(1, 0, 0),
}
function onPointerDown( event ) {
// 此处省略...
if ( _intersections.length > 0 ) {
// 获取当前对象所在平面的法线
const normal = planeNormal[object.userData?.mesh?.position3D?.d] || _camera.getWorldDirection( _plane.normal )
// 中心点默认取用对象的位置
let point = _worldPosition.setFromMatrixPosition( object.matrixWorld )
_plane.setFromNormalAndCoplanarPoint( normal, point);
}
// 此处省略...
}
function onPointerMove( event ) {
// 此处省略...
if ( _selected ) {
if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
const direction = new Vector3()
direction.copy(_intersection).sub(_raycaster.ray.origin).normalize()
const angle = _raycaster.ray.direction.angleTo(direction)
const distance = _raycaster.ray.origin.distanceTo(_intersection)
const limit = _selected.userData?.mesh?.position3D?.limit
const max = Math.max(limit?.x?.max * 2, limit?.y?.max * 2, limit?.z?.max * 2) * 2 || 0
// 优化拖拽体验,避免闪现移动
if (angle < 1 && (distance < max || !max)) {
// 限制移动范围
const newPosition = _intersection.sub( _offset ).applyMatrix4( _inverseMatrix )
if (limit) {
newPosition.x = calPosition(newPosition.x, limit.x)
newPosition.y = calPosition(newPosition.y, limit.y)
newPosition.z = calPosition(newPosition.z, limit.z)
}
}
}
}
// 此处省略...
}
// p - 某个方向的坐标 limit - 大小限制 {min: 0, max: 0}
function calPosition (p, limit) {
let r = p
if (limit) {
if (r > limit.max) {
r = limit.max
}
if (r < limit.min) {
r = limit.min
}
}
return r
}
onPointerDown函数中根据Object所在平面确定了_plane参数,然后在onPointerMove函数中根据拖拽位置确定了Object的目标位置,并根据limit参数限制确定了最终的位置信息,同理可推广到任意平面。
优化彩蛋
如改造代码所示,拖拽点是Object的位置,当移动距离较大时,视觉上物体移动的位置和鼠标的位置存在偏差,给人一种错觉,以底面和顶面尤为显著,故在特定场景下取用户点击的实际位置
function onPointerDown( event ) {
// 此处省略...
if ( _intersections.length > 0 ) {
// 获取当前对象所在平面的法线
const normal = planeNormal[object.userData?.mesh?.position3D?.d] || _camera.getWorldDirection( _plane.normal )
// 中心点默认取用对象的位置
let point = _worldPosition.setFromMatrixPosition( object.matrixWorld )
// 上下平面,取用射线和对象相交点的平面
if (['bottom', 'up'].includes(object.userData?.mesh?.position3D?.d)) {
point = intersection.point
}
_plane.setFromNormalAndCoplanarPoint( normal, point);
}
// 此处省略...
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。