// This THREEx helper makes it easy to handle the mouse events in your 3D scene // // * CHANGES NEEDED // * handle drag/drop // * notify events not object3D - like DOM // * so single object with property // * DONE bubling implement bubling/capturing // * DONE implement event.stopPropagation() // * DONE implement event.type = "click" and co // * DONE implement event.target // // # Lets get started // // First you include it in your page // // `````` // // # use the object oriented api // // You bind an event like this // // ```mesh.on('click', function(object3d){ ... })``` // // To unbind an event, just do // // ```mesh.off('click', function(object3d){ ... })``` // // As an alternative, there is another naming closer DOM events. // Pick the one you like, they are doing the same thing // // ```mesh.addEventListener('click', function(object3d){ ... })``` // ```mesh.removeEventListener('click', function(object3d){ ... })``` // // # Supported Events // // Always in a effort to stay close to usual pratices, the events name are the same as in DOM. // The semantic is the same too. // Currently, the available events are // [click, dblclick, mouseup, mousedown](http://www.quirksmode.org/dom/events/click.html), // [mouseover and mouse out](http://www.quirksmode.org/dom/events/mouseover.html). // // # use the standalone api // // The object-oriented api modifies THREE.Object3D class. // It is a global class, so it may be legitimatly considered unclean by some people. // If this bother you, simply do ```THREEx.DomEvent.noConflict()``` and use the // standalone API. In fact, the object oriented API is just a thin wrapper // on top of the standalone API. // // First, you instanciate the object // // ```var domEvent = new THREEx.DomEvent();``` // // Then you bind an event like this // // ```domEvent.bind(mesh, 'click', function(object3d){ object3d.scale.x *= 2; });``` // // To unbind an event, just do // // ```domEvent.unbind(mesh, 'click', callback);``` // // // # Code // /** @namespace */ var THREEx = THREEx || {}; // # Constructor THREEx.DomEvent = function(camera) { this._camera = camera || null; this._domElement= null; this._projector = new THREE.Projector(); this._selected = null; this._boundObjs = {}; this.setBoundContext('_'); this.mouseIsDown = false; this.mouseDragNotified = false; this.lastDownTime = 0; // Bind dom event for mouse and touch var _this = this; //this._$onClick = function(){ _this._onClick.apply(_this, arguments); }; //this._$onDblClick = function(){ _this._onDblClick.apply(_this, arguments); }; this._$onMouseMove = function(){ _this._onMouseMove.apply(_this, arguments); }; this._$onMouseDown = function(){ _this._onMouseDown.apply(_this, arguments); }; this._$onMouseUp = function(){ _this._onMouseUp.apply(_this, arguments); }; this._$onTouchMove = function(){ _this._onTouchMove.apply(_this, arguments); }; this._$onTouchStart = function(){ _this._onTouchStart.apply(_this, arguments); }; this._$onTouchEnd = function(){ _this._onTouchEnd.apply(_this, arguments); }; } THREEx.DomEvent.prototype.setDOMElement = function(domElement) { if(this._domElement) this.unsetDOMElement(); this._domElement=domElement; //this._domElement.addEventListener( 'click' , this._$onClick , false ); //this._domElement.addEventListener( 'dblclick' , this._$onDblClick , false ); this._domElement.addEventListener( 'mousemove' , this._$onMouseMove , false ); this._domElement.addEventListener( 'mousedown' , this._$onMouseDown , false ); this._domElement.addEventListener( 'mouseup' , this._$onMouseUp , false ); this._domElement.addEventListener( 'touchmove' , this._$onTouchMove , false ); this._domElement.addEventListener( 'touchstart' , this._$onTouchStart , false ); this._domElement.addEventListener( 'touchend' , this._$onTouchEnd , false ); } THREEx.DomEvent.prototype.unsetDOMElement = function() { if(this._domElement) { //this._domElement.removeEventListener( 'click' , this._$onClick , false ); //this._domElement.removeEventListener( 'dblclick' , this._$onDblClick , false ); this._domElement.removeEventListener( 'mousemove' , this._$onMouseMove , false ); this._domElement.removeEventListener( 'mousedown' , this._$onMouseDown , false ); this._domElement.removeEventListener( 'mouseup' , this._$onMouseUp , false ); this._domElement.removeEventListener( 'touchmove' , this._$onTouchMove , false ); this._domElement.removeEventListener( 'touchstart' , this._$onTouchStart , false ); this._domElement.removeEventListener( 'touchend' , this._$onTouchEnd , false ); this._domElement=null; } } THREEx.DomEvent.prototype.setBoundContext = function(boundContext) { this._boundContext=boundContext; if(this._boundObjs[boundContext]===undefined) this._boundObjs[boundContext]=[]; } THREEx.DomEvent.prototype.unsetBoundContext = function(boundContext) { if(this._boundObjs[boundContext]!==undefined) { var boundObjs=this._boundObjs[boundContext]; for(var i=0;i=0) $this._boundObjs[$this._boundContext].splice(index, 1); for(var i=0;i0) { x = event.touches[0].pageX; y = event.touches[0].pageY; } else if(event.changedTouches && event.changedTouches.length>0) { x = event.changedTouches[0].pageX; y = event.changedTouches[0].pageY; } else { console.warn("Unable to get event position"); return false; } var mouseX = +((x-offset.left) / domElement.width() ) * 2 - 1; var mouseY = -((y-offset.top) / domElement.height()) * 2 + 1; var vector = new THREE.Vector3( mouseX, mouseY, 1 ); vector.unproject( this._camera ); var worldPos = this._camera.getWorldPosition(); //vector.sub( this._camera.position ).normalize() vector.sub( worldPos ).normalize() var ray = new THREE.Raycaster( worldPos, vector ); var intersects = ray.intersectObjects( this._boundObjs[this._boundContext] ); this.objectLocked = intersects.length !== 0; this.enableDrag = this.objectLocked && enableDrag; return this.objectLocked; } /********************************************************************************/ /* onEvent */ /********************************************************************************/ // # handle click kind of events THREEx.DomEvent.prototype._onEvent = function(eventName, mouseX, mouseY, origDomEvent, eventX, eventY) { //console.log("_onEvent",eventName,mouseX,mouseY); if(eventName=="mouseup") { this.mouseIsDown = false; if(!this.objectLocked) return null; } else if(eventName=="mousedown") { this.mouseIsDown = true; this.mouseDownPos = [eventX, eventY]; this.mouseDragNotified = false; this.lastDownTime = Date.now(); } else if(eventName=="mousemove") { if(this.mouseIsDown) { var dx = this.mouseDownPos[0] - eventX; var dy = this.mouseDownPos[1] - eventY; var distSq = dx * dx + dy * dy; if(this.enableDrag) { if(distSq < 100) return null; if(!this.mouseDragNotified) { if(Date.now()-this.lastDownTime<50) return null; this.mouseDragNotified = true; return this._onEvent("mouseup", this.mouseEventPos[0], this.mouseEventPos[1], origDomEvent, eventX, eventY); } } else { if(distSq > 100) { this.objectLocked = false; return null; } } } } var vector = new THREE.Vector3( mouseX, mouseY, 1 ); vector.unproject( this._camera ); var worldPos = this._camera.getWorldPosition(); vector.sub( worldPos ).normalize() var ray = new THREE.Raycaster( worldPos, vector ); var intersects = ray.intersectObjects( this._boundObjs[this._boundContext] ); //console.log("camera",this._camera.position,"ray",ray,"bound",this._boundObjs) // if there are no intersections, return now if( intersects.length === 0 ) { //console.warn("THREEx",eventName,"No hit"); return null; } // init some vairables var intersect = intersects[0]; var object3d = this.getRootObject(intersect.object); var objectCtx = this._objectCtxGet(object3d); if( !objectCtx ) return null; // notify handlers this._notify(eventName, object3d, origDomEvent, intersect.point); } THREEx.DomEvent.prototype._notify = function(eventName, object3d, origDomEvent, point) { //console.log("notify",eventName,"to",object3d.id) var objectCtx = this._objectCtxGet(object3d); var handlers = objectCtx ? objectCtx[eventName+'Handlers'] : null; // do bubbling if( !objectCtx || !handlers || handlers.length === 0 ) { if(object3d.parent) this._notify(eventName, object3d.parent); return; } // notify all handlers var handlers = objectCtx[eventName+'Handlers']; for(var i = 0; i < handlers.length; i++){ var handler = handlers[i]; var toPropagate = true; handler.callback({ type : eventName, target : object3d, origDomEvent : origDomEvent, stopPropagation : function(){ toPropagate = false; }, point: point }); if( !toPropagate ) continue; // do bubbling if( handler.useCapture === false ){ object3d.parent && this._notify(eventName, object3d.parent, origDomEvent, point); } } } /********************************************************************************/ /* handle mouse events */ /********************************************************************************/ // # handle mouse events //THREEx.DomEvent.longClickTimer=null; THREEx.DomEvent.prototype._onMouseDown = function(event) { //console.log("_onMouseDown",event.type) var $this=this; if(event.button!==0) return; /* if(THREEx.DomEvent.longClickTimer) clearTimeout(THREEx.DomEvent.longClickTimer); THREEx.DomEvent.longClickTimer=setTimeout(function() { THREEx.DomEvent.longClickTimer=null; $this._onMouseEvent('holdclick', event); },500); */ return this._onMouseEvent('mousedown', event); } THREEx.DomEvent.prototype._onMouseUp = function(event) { if(event.button!==0) return; //console.log("_onMouseUp",event.type) /* if(THREEx.DomEvent.longClickTimer) { clearTimeout(THREEx.DomEvent.longClickTimer); THREEx.DomEvent.longClickTimer=null; } */ return this._onMouseEvent('mouseup' , event); } THREEx.DomEvent.prototype._onMouseMove = function(event) { //console.log("_onMouseMove",event.type) if(!this.mouseIsDown) return null; /* if(THREEx.DomEvent.longClickTimer) { clearTimeout(THREEx.DomEvent.longClickTimer); THREEx.DomEvent.longClickTimer = null; } */ return this._onMouseEvent('mousemove', event); } THREEx.DomEvent.prototype._onMouseEvent = function(eventName, domEvent) { var domElement=$(this._domElement); var offset=domElement.offset(); var mouseX = +((domEvent.clientX-offset.left) / domElement.width() ) * 2 - 1; var mouseY = -((domEvent.clientY-offset.top) / domElement.height()) * 2 + 1; this.mouseEventPos = [ mouseX, mouseY ]; return this._onEvent(eventName, mouseX, mouseY, domEvent, domEvent.clientX-offset.left, domEvent.clientY-offset.top); } /* THREEx.DomEvent.prototype._onClick = function(event) { // TODO handle touch ? return this._onMouseEvent('click' , event); } THREEx.DomEvent.prototype._onDblClick = function(event) { // TODO handle touch ? return this._onMouseEvent('dblclick' , event); } */ /********************************************************************************/ /* handle touch events */ /********************************************************************************/ // # handle touch events THREEx.DomEvent.prototype._onTouchStart = function(event){ return this._onTouchEvent('mousedown', event); } THREEx.DomEvent.prototype._onTouchEnd = function(event){ return this._onTouchEvent('mouseup' , event); } THREEx.DomEvent.prototype._onTouchMove = function(event){ if(!this.mouseIsDown) return null; else return this._onTouchEvent('mousemove', event); } /* THREEx.DomEvent.prototype._onTouchMove = function(domEvent) { if( domEvent.touches.length != 1 ) return undefined; domEvent.preventDefault(); var mouseX = +(domEvent.touches[ 0 ].pageX / window.innerWidth ) * 2 - 1; var mouseY = -(domEvent.touches[ 0 ].pageY / window.innerHeight) * 2 + 1; return this._onMove('mousemove', mouseX, mouseY, domEvent); } */ THREEx.DomEvent.prototype._onTouchEvent = function(eventName, domEvent) { var domElement=$(this._domElement); var offset=domElement.offset(); var mouseX=0, mouseY=0, eventX=0, eventY=0; if(domEvent.touches && domEvent.touches.length>0) { mouseX = +((domEvent.touches[0].clientX-offset.left) / domElement.width() ) * 2 - 1; mouseY = -((domEvent.touches[0].clientY-offset.top) / domElement.height()) * 2 + 1; eventX = domEvent.touches[0].clientX-offset.left; eventY = domEvent.touches[0].clientY-offset.top; } else if(domEvent.changedTouches && domEvent.changedTouches.length>0) { mouseX = +((domEvent.changedTouches[0].clientX-offset.left) / domElement.width() ) * 2 - 1; mouseY = -((domEvent.changedTouches[0].clientY-offset.top) / domElement.height()) * 2 + 1; eventX = domEvent.changedTouches[0].clientX-offset.left; eventY = domEvent.changedTouches[0].clientY-offset.top; } this.mouseEventPos = [ mouseX, mouseY ]; return this._onEvent(eventName, mouseX, mouseY, domEvent, eventX, eventY); }