|
@@ -0,0 +1,177 @@
|
|
|
|
+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<Mesh> {
|
|
|
|
+ private _attachedNode: Node;
|
|
|
|
+ private _dragPlane: Mesh;
|
|
|
|
+ private _scene:Scene;
|
|
|
|
+ private _pointerObserver:Nullable<Observer<PointerInfo>>;
|
|
|
|
+ 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<Mesh>=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 = (<PointerEvent>pointerInfo.event).pointerId;
|
|
|
|
+ lastPosition.copyFrom(pickedPoint);
|
|
|
|
+ this.onDragStartObservable.notifyObservers({dragPlanePoint: pickedPoint});
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }else if(pointerInfo.type == BABYLON.PointerEventTypes.POINTERUP){
|
|
|
|
+ if(this._draggingID == (<PointerEvent>pointerInfo.event).pointerId){
|
|
|
|
+ dragging = false;
|
|
|
|
+ this._draggingID = -1;
|
|
|
|
+ this.onDragEndObservable.notifyObservers({dragPlanePoint: lastPosition});
|
|
|
|
+ }
|
|
|
|
+ }else if(pointerInfo.type == BABYLON.PointerEventTypes.POINTERMOVE){
|
|
|
|
+ if(this._draggingID == (<PointerEvent>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){
|
|
|
|
+ (<Mesh>this._attachedNode).position.addInPlace(delta);
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ this.onDragObservable.notifyObservers({delta: delta, dragPlanePoint: pickedPoint});
|
|
|
|
+ lastPosition.copyFrom(pickedPoint);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private _pickWithRayOnDragPlane(ray:Nullable<Ray>){
|
|
|
|
+ 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 : (<Mesh>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);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|