module BABYLON { /** * A behavior that when attached to a mesh will allow the mesh to be dragged around the screen based on pointer events */ export class PointerDragBehavior implements Behavior { private _attachedNode: Node; private _dragPlane: Mesh; private _scene:Scene; private _pointerObserver:Nullable>; private static planeScene:Scene; private _draggingID = -1; /** * Fires each time the attached mesh is dragged with the pointer */ public onDragObservable = new Observable<{delta:Vector3, dragPlanePoint:Vector3}>() /** * Fires each time a drag begins (eg. mouse down on mesh) */ public onDragStartObservable = new Observable<{dragPlanePoint:Vector3}>() /** * Fires each time a drag ends (eg. mouse release after drag) */ public onDragEndObservable = new Observable<{dragPlanePoint:Vector3}>() /** * If the attached mesh should be moved when dragged */ public moveAttached = true; /** * Mesh with the position where the drag plane should be placed */ public _dragPlaneParent:Nullable=null; /** * Creates a pointer drag behavior that can be attached to a mesh * @param options The drag axis or normal of the plane that will be dragged accross */ constructor(private options:{dragAxis?:Vector3, dragPlaneNormal?:Vector3}){ var optionCount = 0; if(options.dragAxis){ optionCount++; } if(options.dragPlaneNormal){ optionCount++; } if(optionCount > 1){ throw "Multiple drag modes specified in dragBehavior options. Only one expected"; } if(optionCount < 1){ throw "At least one drag mode option must be specified"; } } /** * The name of the behavior */ public get name(): string { return "PointerDrag"; } /** * Initializes the behavior */ public init() {} /** * Attaches the drag behavior the passed in mesh * @param ownerNode The mesh that will be dragged around once attached */ public attach(ownerNode: Mesh): void { this._scene = ownerNode.getScene(); this._attachedNode = ownerNode; // Initialize drag plane to not interfere with existing scene if(!PointerDragBehavior.planeScene){ PointerDragBehavior.planeScene = new BABYLON.Scene(this._scene.getEngine()) this._scene.getEngine().scenes.pop(); } this._dragPlane = BABYLON.Mesh.CreatePlane("pointerDragPlane", 1000, PointerDragBehavior.planeScene, false, BABYLON.Mesh.DOUBLESIDE); // State of the drag var dragging = false var lastPosition = new BABYLON.Vector3(0,0,0); var delta = new BABYLON.Vector3(0,0,0); this._pointerObserver = this._scene.onPointerObservable.add((pointerInfo)=>{ if (pointerInfo.type == BABYLON.PointerEventTypes.POINTERDOWN) { if(!dragging && pointerInfo.pickInfo && pointerInfo.pickInfo.hit && pointerInfo.pickInfo.pickedMesh && pointerInfo.pickInfo.ray){ if(this._attachedNode == pointerInfo.pickInfo.pickedMesh){ this._updateDragPlanePosition(pointerInfo.pickInfo.ray); var pickedPoint = this._pickWithRayOnDragPlane(pointerInfo.pickInfo.ray) if(pickedPoint){ dragging = true; this._draggingID = (pointerInfo.event).pointerId; lastPosition.copyFrom(pickedPoint); this.onDragStartObservable.notifyObservers({dragPlanePoint: pickedPoint}); } } } }else if(pointerInfo.type == BABYLON.PointerEventTypes.POINTERUP){ if(this._draggingID == (pointerInfo.event).pointerId){ dragging = false; this._draggingID = -1; this.onDragEndObservable.notifyObservers({dragPlanePoint: lastPosition}); } }else if(pointerInfo.type == BABYLON.PointerEventTypes.POINTERMOVE){ if(this._draggingID == (pointerInfo.event).pointerId && dragging && pointerInfo.pickInfo && pointerInfo.pickInfo.ray){ var pickedPoint = this._pickWithRayOnDragPlane(pointerInfo.pickInfo.ray) this._updateDragPlanePosition(pointerInfo.pickInfo.ray); if (pickedPoint) { // depending on the drag mode option drag accordingly if(this.options.dragAxis){ //get the closest point on the dragaxis from the selected mesh to the picked point location // https://www.opengl.org/discussion_boards/showthread.php/159717-Closest-point-on-a-Vector-to-a-point this.options.dragAxis.scaleToRef(BABYLON.Vector3.Dot(pickedPoint.subtract(lastPosition), this.options.dragAxis), delta); }else{ pickedPoint.subtractToRef(lastPosition, delta); } if(this.moveAttached){ (this._attachedNode).position.addInPlace(delta); } this.onDragObservable.notifyObservers({delta: delta, dragPlanePoint: pickedPoint}); lastPosition.copyFrom(pickedPoint); } } } }); } private _pickWithRayOnDragPlane(ray:Nullable){ if(!ray){ return null; } var pickResult = PointerDragBehavior.planeScene.pickWithRay(ray, (m)=>{return m == this._dragPlane}) if (pickResult && pickResult.hit && pickResult.pickedMesh && pickResult.pickedPoint) { return pickResult.pickedPoint; }else{ return null; } } // Position the drag plane based on the attached mesh position, for single axis rotate the plane along the axis to face the camera private _updateDragPlanePosition(ray:Ray){ var pointA = this._dragPlaneParent ? this._dragPlaneParent.position : (this._attachedNode).position // center if(this.options.dragAxis){ var camPos = ray.origin; // Calculate plane normal in direction of camera but perpendicular to drag axis var pointB = pointA.add(this.options.dragAxis); // towards drag axis var pointC = pointA.add(camPos.subtract(pointA).normalize()); // towards camera // Get perpendicular line from direction to camera and drag axis var lineA = pointB.subtract(pointA); var lineB = pointC.subtract(pointA); var perpLine = BABYLON.Vector3.Cross(lineA, lineB); // Get perpendicular line from previous result and drag axis to adjust lineB to be perpendiculat to camera var norm = BABYLON.Vector3.Cross(lineA, perpLine).normalize(); this._dragPlane.position.copyFrom(pointA); this._dragPlane.lookAt(pointA.add(norm)); }else if(this.options.dragPlaneNormal){ this._dragPlane.position.copyFrom(pointA); this._dragPlane.lookAt(pointA.add(this.options.dragPlaneNormal)); } this._dragPlane.computeWorldMatrix(true); } /** * Detaches the behavior from the mesh */ public detach(): void { if(this._pointerObserver){ this._scene.onPointerObservable.remove(this._pointerObserver); } } } }