浏览代码

Scene + Canvas2D improvments

Scene.onPrePointerObservable added for internal sake only
Canvas2D: interaction in progress...
nockawa 9 年之前
父节点
当前提交
9dd9daf61e

+ 370 - 10
src/Canvas2d/babylon.canvas2d.ts

@@ -51,21 +51,22 @@
          * Create a new 2D ScreenSpace Rendering Canvas, it is a 2D rectangle that has a size (width/height) and a position relative to the top/left corner of the screen.
          * ScreenSpace Canvas will be drawn in the Viewport as a 2D Layer lying to the top of the 3D Scene. Typically used for traditional UI.
          * All caching strategies will be available.
+         * PLEASE NOTE: the origin of a Screen Space Canvas is set to [0;0] (bottom/left) which is different than the default origin of a Primitive which is centered [0.5;0.5]
          * @param scene the Scene that owns the Canvas
          * @param name the name of the Canvas, for information purpose only
          * @param pos the position of the canvas, relative from the bottom/left of the scene's viewport
          * @param size the Size of the canvas. If null two behaviors depend on the cachingStrategy: if it's CACHESTRATEGY_CACHECANVAS then it will always auto-fit the rendering device, in all the other modes it will fit the content of the Canvas
          * @param cachingStrategy either CACHESTRATEGY_TOPLEVELGROUPS, CACHESTRATEGY_ALLGROUPS, CACHESTRATEGY_CANVAS, CACHESTRATEGY_DONTCACHE. Please refer to their respective documentation for more information.
          */
-        static CreateScreenSpace(scene: Scene, name: string, pos: Vector2, size: Size, cachingStrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS): Canvas2D {
+        static CreateScreenSpace(scene: Scene, name: string, pos: Vector2, size: Size, cachingStrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS, enableInteraction: boolean = true): Canvas2D {
             let c = new Canvas2D();
-            c.setupCanvas(scene, name, size, true, cachingStrategy);
+            c.setupCanvas(scene, name, size, true, cachingStrategy, enableInteraction);
             c.position = pos;
+            c.origin = Vector2.Zero();
 
             return c;
         }
 
-
         /**
          * Create a new 2D WorldSpace Rendering Canvas, it is a 2D rectangle that has a size (width/height) and a world transformation information to place it in the world space.
          * This kind of canvas can't have its Primitives directly drawn in the Viewport, they need to be cached in a bitmap at some point, as a consequence the DONT_CACHE strategy is unavailable. For now only CACHESTRATEGY_CANVAS is supported, but the remaining strategies will be soon.
@@ -80,7 +81,7 @@
          * @param sideOrientation Unexpected behavior occur if the value is different from Mesh.DEFAULTSIDE right now, so please use this one.
          * @param cachingStrategy Must be CACHESTRATEGY_CANVAS for now
          */
-        static CreateWorldSpace(scene: Scene, name: string, position: Vector3, rotation: Quaternion, size: Size, renderScaleFactor: number=1, sideOrientation?: number, cachingStrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS): Canvas2D {
+        static CreateWorldSpace(scene: Scene, name: string, position: Vector3, rotation: Quaternion, size: Size, renderScaleFactor: number = 1, sideOrientation?: number, cachingStrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS, enableInteraction: boolean = true): Canvas2D {
             if (cachingStrategy !== Canvas2D.CACHESTRATEGY_CANVAS) {
                 throw new Error("Right now only the CACHESTRATEGY_CANVAS cache Strategy is supported for WorldSpace Canvas. More will come soon!");
             }
@@ -94,7 +95,7 @@
             }
 
             let c = new Canvas2D();
-            c.setupCanvas(scene, name, new Size(size.width*renderScaleFactor, size.height*renderScaleFactor), false, cachingStrategy);
+            c.setupCanvas(scene, name, new Size(size.width*renderScaleFactor, size.height*renderScaleFactor), false, cachingStrategy, enableInteraction);
 
             let plane = new WorldSpaceCanvas2d(name, scene, c);
             let vertexData = VertexData.CreatePlane({ width: size.width/2, height: size.height/2, sideOrientation: sideOrientation });
@@ -114,7 +115,7 @@
             return c;
         }
 
-        protected setupCanvas(scene: Scene, name: string, size: Size, isScreenSpace: boolean = true, cachingstrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
+        protected setupCanvas(scene: Scene, name: string, size: Size, isScreenSpace: boolean, cachingstrategy: number, enableInteraction: boolean) {
             let engine = scene.getEngine();
             this._fitRenderingDevice = !size;
             if (!size) {
@@ -127,6 +128,8 @@
             this._hierarchyLevelZFactor = 1 / this._hierarchyMaxDepth;
             this._hierarchyLevelMaxSiblingCount = 1000;
             this._hierarchySiblingZDelta = this._hierarchyLevelZFactor / this._hierarchyLevelMaxSiblingCount;
+            this._primPointerInfo = new PrimitivePointerInfo();
+            this._capturedPointers = new StringDictionary<Prim2DBase>();
 
             this.setupGroup2D(this, null, name, Vector2.Zero(), size, this._cachingStrategy===Canvas2D.CACHESTRATEGY_ALLGROUPS ? Group2D.GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE : Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY);
 
@@ -160,6 +163,321 @@
 
             this._supprtInstancedArray = this._engine.getCaps().instancedArrays !== null;
 //            this._supprtInstancedArray = false; // TODO REMOVE!!!
+
+            this._setupInteraction(enableInteraction);
+        }
+
+        private _setupInteraction(enable: boolean) {
+            // No change detection
+            if (enable === this._interactionEnabled) {
+                return;
+            }
+
+            // Set the new state
+            this._interactionEnabled = enable;
+
+            // Disable interaction
+            if (!enable) {
+                if (this._scenePrePointerObserver) {
+                    this.scene.onPrePointerObservable.remove(this._scenePrePointerObserver);
+                    this._scenePrePointerObserver = null;
+                }
+
+                return;
+            }
+
+            // Enable Interaction
+
+            // Register the observable
+            this.scene.onPrePointerObservable.add((e, s) => this._handlePointerEventForInteraction(e, s));
+        }
+
+        public setPointerCapture(pointerId: number, primitive: Prim2DBase): boolean {
+            if (this.isPointerCaptured(pointerId)) {
+                return false;
+            }
+
+            // Try to capture the pointer on the HTML side
+            try {
+                this.engine.getRenderingCanvas().setPointerCapture(pointerId);
+            } catch (e) {
+                //Nothing to do with the error. Execution will continue.
+            }
+
+            this._primPointerInfo.updateRelatedTarget(primitive, Vector2.Zero());
+            this._bubbleNotifyPrimPointerObserver(primitive, PrimitivePointerInfo.PointerGotCapture);
+
+            this._capturedPointers.add(pointerId.toString(), primitive);
+            return true;
+        }
+
+        public releasePointerCapture(pointerId: number, primitive: Prim2DBase): boolean {
+            if (this._capturedPointers.get(pointerId.toString()) !== primitive) {
+                return false;
+            }
+
+            // Try to release the pointer on the HTML side
+            try {
+                this.engine.getRenderingCanvas().releasePointerCapture(pointerId);
+            } catch (e) {
+                //Nothing to do with the error. Execution will continue.
+            }
+
+            this._primPointerInfo.updateRelatedTarget(primitive, Vector2.Zero());
+            this._bubbleNotifyPrimPointerObserver(primitive, PrimitivePointerInfo.PointerLostCapture);
+            this._capturedPointers.remove(pointerId.toString());
+            return true;
+        }
+
+        public isPointerCaptured(pointerId: number) {
+            return this._capturedPointers.contains(pointerId.toString());
+        }
+
+        private getCapturedPrimitive(pointerId: number): Prim2DBase {
+            // Avoid unnecessary lookup
+            if (this._capturedPointers.count === 0) {
+                return null;
+            }
+            return this._capturedPointers.get(pointerId.toString());
+        }
+           
+        private static _interInfo = new IntersectInfo2D();
+        private _handlePointerEventForInteraction(eventData: PointerInfoPre, eventState: EventState) {
+            // Update the this._primPointerInfo structure we'll send to observers using the PointerEvent data
+            this._updatePointerInfo(eventData);
+
+            let capturedPrim = this.getCapturedPrimitive(this._primPointerInfo.pointerId);
+
+            // Make sure the intersection list is up to date, we maintain this list either in response of a mouse event (here) or before rendering the canvas.
+            // Why before rendering the canvas? because some primitives may move and get away/under the mouse cursor (which is not moving). So we need to update at both location in order to always have an accurate list, which is needed for the hover state change.
+            this._updateIntersectionList(this._primPointerInfo.canvasPointerPos, capturedPrim!==null);
+
+            // Update the over status, same as above, it's could be done here or during rendering, but will be performed only once per render frame
+            this._updateOverStatus();
+
+            // Check if we have nothing to raise
+            if (!this._actualOverPrimitive && !capturedPrim) {
+                return;
+            }
+
+            // Update the relatedTarget info with the over primitive or the captured one (if any)
+            let targetPrim = capturedPrim || this._actualOverPrimitive.prim;
+
+            let targetPointerPos = capturedPrim ? this._primPointerInfo.canvasPointerPos.subtract(new Vector2(targetPrim.globalTransform.m[12], targetPrim.globalTransform.m[13])) : this._actualOverPrimitive.intersectionLocation;
+
+            this._primPointerInfo.updateRelatedTarget(targetPrim, targetPointerPos);
+
+            // Analyze the pointer event type and fire proper events on the primitive
+
+            if (eventData.type === PointerEventTypes.POINTERWHEEL) {
+                this._bubbleNotifyPrimPointerObserver(targetPrim, PrimitivePointerInfo.PointerMouseWheel);
+            } else if (eventData.type === PointerEventTypes.POINTERMOVE) {
+                this._bubbleNotifyPrimPointerObserver(targetPrim, PrimitivePointerInfo.PointerMove);
+            } else if (eventData.type === PointerEventTypes.POINTERDOWN) {
+                this._bubbleNotifyPrimPointerObserver(targetPrim, PrimitivePointerInfo.PointerDown);
+            } else if (eventData.type === PointerEventTypes.POINTERUP) {
+                this._bubbleNotifyPrimPointerObserver(targetPrim, PrimitivePointerInfo.PointerUp);
+            }
+        }
+
+        private _updatePointerInfo(eventData: PointerInfoPre) {
+            let pii = this._primPointerInfo;
+            if (!pii.canvasPointerPos) {
+                pii.canvasPointerPos = Vector2.Zero();
+            }
+            pii.canvasPointerPos.x = eventData.localPosition.x - this.position.x;
+            pii.canvasPointerPos.y = (this.engine.getRenderHeight() - eventData.localPosition.y) - this.position.y;
+            pii.mouseWheelDelta = 0;
+
+            if (eventData.type === PointerEventTypes.POINTERWHEEL) {
+                var event = <MouseWheelEvent>eventData.event;
+                if (event.wheelDelta) {
+                    pii.mouseWheelDelta = event.wheelDelta / (PrimitivePointerInfo.MouseWheelPrecision * 40);
+                } else if (event.detail) {
+                    pii.mouseWheelDelta = -event.detail / PrimitivePointerInfo.MouseWheelPrecision;
+                }
+            } else {
+                var pe         = <PointerEvent>eventData.event;
+                pii.ctrlKey    = pe.ctrlKey;
+                pii.altKey     = pe.altKey;
+                pii.shiftKey   = pe.shiftKey;
+                pii.metaKey    = pe.metaKey;
+                pii.button     = pe.button;
+                pii.buttons    = pe.buttons;
+                pii.pointerId  = pe.pointerId;
+                pii.width      = pe.width;
+                pii.height     = pe.height;
+                pii.presssure  = pe.pressure;
+                pii.tilt.x     = pe.tiltX;
+                pii.tilt.y     = pe.tiltY;
+                pii.isCaptured = this.getCapturedPrimitive(pe.pointerId)!==null;
+            }
+        }
+
+        private _updateIntersectionList(mouseLocalPos: Vector2, isCapture: boolean) {
+            if (this.scene.getRenderId() === this._intersectionRenderId) {
+                return;
+            }
+
+            let ii = Canvas2D._interInfo;
+            ii.pickPosition.x = mouseLocalPos.x;
+            ii.pickPosition.y = mouseLocalPos.y;
+            ii.findFirstOnly = false;
+
+            // Fast rejection: test if the mouse pointer is outside the canvas's bounding Info
+            if (!isCapture && !this.boundingInfo.doesIntersect(ii.pickPosition)) {
+                this._previousIntersectionList = this._actualIntersectionList;
+                this._actualIntersectionList   = null;
+                this._previousOverPrimitive    = this._actualOverPrimitive;
+                this._actualOverPrimitive      = null;
+                return;
+            }
+
+            this._updateCanvasState();
+
+            this.intersect(ii);
+
+            this._previousIntersectionList = this._actualIntersectionList;
+            this._actualIntersectionList   = ii.intersectedPrimitives;
+            this._previousOverPrimitive    = this._actualOverPrimitive;
+            this._actualOverPrimitive      = ii.topMostIntersectedPrimitive;
+
+            this._intersectionRenderId = this.scene.getRenderId();
+        }
+
+        // Based on the previousIntersectionList and the actualInstersectionList we can determined which primitives are being hover state or loosing it
+        private _updateOverStatus() {
+            if ((this.scene.getRenderId() === this._hoverStatusRenderId) || !this._previousIntersectionList || !this._actualIntersectionList) {
+                return;
+            }
+
+            // Detect a change of over
+            let prevPrim = this._previousOverPrimitive ? this._previousOverPrimitive.prim : null;
+            let actualPrim = this._actualOverPrimitive ? this._actualOverPrimitive.prim   : null;
+
+            if (prevPrim !== actualPrim) {
+                // Detect if the current pointer is captured, only fire event if they belong to the capture primitive
+                let capturedPrim = this.getCapturedPrimitive(this._primPointerInfo.pointerId);
+
+                // Notify the previous "over" prim that the pointer is no longer over it
+                if ((capturedPrim && capturedPrim===prevPrim) || (!capturedPrim && prevPrim)) {
+                    this._primPointerInfo.updateRelatedTarget(prevPrim, this._previousOverPrimitive.intersectionLocation);
+                    this._bubbleNotifyPrimPointerObserver(prevPrim, PrimitivePointerInfo.PointerOut);
+                }
+
+                // Notify the new "over" prim that the pointer is over it
+                if ((capturedPrim && capturedPrim === actualPrim) || (!capturedPrim && actualPrim)) {
+                    this._primPointerInfo.updateRelatedTarget(actualPrim, this._actualOverPrimitive.intersectionLocation);
+                    this._bubbleNotifyPrimPointerObserver(actualPrim, PrimitivePointerInfo.PointerOver);
+                }
+            }
+
+            this._hoverStatusRenderId = this.scene.getRenderId();
+        }
+
+        private _updatePrimPointerPos(prim: Prim2DBase) {
+            if (this._primPointerInfo.isCaptured) {
+                this._primPointerInfo.primitivePointerPos = this._primPointerInfo.relatedTargetPointerPos;
+            } else {
+                for (let pii of this._actualIntersectionList) {
+                    if (pii.prim === prim) {
+                        this._primPointerInfo.primitivePointerPos = pii.intersectionLocation;
+                        return;
+                    }
+                }
+            }
+        }
+
+        private _notifDebugMode = true;
+        private _debugExecObserver(prim: Prim2DBase, mask: number) {
+            if (!this._notifDebugMode) {
+                return;
+            }
+
+            let debug = "";
+            for (let i = 0; i < prim.hierarchyDepth; i++) {
+                debug += "  ";
+            }
+
+            let pii = this._primPointerInfo;
+            debug += `[RID:${this.scene.getRenderId()}] [${prim.hierarchyDepth}] event:${PrimitivePointerInfo.getEventTypeName(mask)}, id: ${prim.id} (${Tools.getClassName(prim)}), primPos: ${pii.primitivePointerPos.toString()}, canvasPos: ${pii.canvasPointerPos.toString()}`;
+            console.log(debug);
+        }
+
+        private _bubbleNotifyPrimPointerObserver(prim: Prim2DBase, mask: number) {
+            let pii = this._primPointerInfo;
+
+            // In case of PointerOver/Out we will first notify the children (but the deepest to the closest) with PointerEnter/Leave
+            if ((mask & (PrimitivePointerInfo.PointerOver | PrimitivePointerInfo.PointerOut)) !== 0) {
+                this._notifChildren(prim, mask);
+            }
+
+            let bubbleCancelled = false;
+            let cur = prim;
+            while (cur) {
+                // Only trigger the observers if the primitive is intersected (except for out)
+                if (!bubbleCancelled) {
+                    this._updatePrimPointerPos(cur);
+
+                    // Exec the observers
+                    this._debugExecObserver(cur, mask);
+                    cur._pointerEventObservable.notifyObservers(pii, mask);
+
+                    // Bubble canceled? If we're not executing PointerOver or PointerOut, quit immediately
+                    // If it's PointerOver/Out we have to trigger PointerEnter/Leave no matter what
+                    if (pii.cancelBubble) {
+                        if ((mask & (PrimitivePointerInfo.PointerOver | PrimitivePointerInfo.PointerOut)) === 0) {
+                            return;
+                        }
+
+                        // We're dealing with PointerOver/Out, let's keep looping to fire PointerEnter/Leave, but not Over/Out anymore
+                        bubbleCancelled = true;
+                    }
+                }
+
+                // If bubble is cancel we didn't update the Primitive Pointer Pos yet, let's do it
+                if (bubbleCancelled) {
+                    this._updatePrimPointerPos(cur);
+                }
+
+                // Trigger a PointerEnter corresponding to the PointerOver
+                if (mask === PrimitivePointerInfo.PointerOver) {
+                    this._debugExecObserver(cur, PrimitivePointerInfo.PointerEnter);
+                    cur._pointerEventObservable.notifyObservers(pii, PrimitivePointerInfo.PointerEnter);
+                }
+
+                // Trigger a PointerLeave corresponding to the PointerOut
+                else if (mask === PrimitivePointerInfo.PointerOut) {
+                    this._debugExecObserver(cur, PrimitivePointerInfo.PointerLeave);
+                    cur._pointerEventObservable.notifyObservers(pii, PrimitivePointerInfo.PointerLeave);
+                }
+
+                // Loop to the parent
+                cur = cur.parent;
+            }
+        }
+
+        _notifChildren(prim: Prim2DBase, mask: number) {
+            let pii = this._primPointerInfo;
+
+            prim.children.forEach(curChild => {
+                // Recurse first, we want the deepest to be notified first
+                this._notifChildren(curChild, mask);
+
+                this._updatePrimPointerPos(curChild);
+
+                // Fire the proper notification
+                if (mask === PrimitivePointerInfo.PointerOver) {
+                    this._debugExecObserver(curChild, PrimitivePointerInfo.PointerEnter);
+                    curChild._pointerEventObservable.notifyObservers(pii, PrimitivePointerInfo.PointerEnter);
+                }
+
+                // Trigger a PointerLeave corresponding to the PointerOut
+                else if (mask === PrimitivePointerInfo.PointerOut) {
+                    this._debugExecObserver(curChild, PrimitivePointerInfo.PointerLeave);
+                    curChild._pointerEventObservable.notifyObservers(pii, PrimitivePointerInfo.PointerLeave);
+                }
+            });
         }
 
         /**
@@ -171,6 +489,10 @@
                 return false;
             }
 
+            if (this.interactionEnabled) {
+                this._setupInteraction(false);
+            }
+
             if (this._beforeRenderObserver) {
                 this._scene.onBeforeRenderObservable.remove(this._beforeRenderObserver);
                 this._beforeRenderObserver = null;
@@ -294,6 +616,14 @@
             this._background.levelVisible = true;
         }
 
+        public get interactionEnabled(): boolean {
+            return this._interactionEnabled;
+        }
+
+        public set interactionEnabled(enable: boolean) {
+            this._setupInteraction(enable);
+        }
+
         public get _engineData(): Canvas2DEngineBoundData {
             return this.__engineData;
         }
@@ -323,6 +653,17 @@
         }
 
         private __engineData: Canvas2DEngineBoundData;
+        private _interactionEnabled: boolean;
+        private _primPointerInfo: PrimitivePointerInfo;
+        private _updateRenderId: number;
+        private _intersectionRenderId: number;
+        private _hoverStatusRenderId: number;
+        private _previousIntersectionList: Array<PrimitiveIntersectedInfo>;
+        private _actualIntersectionList: Array<PrimitiveIntersectedInfo>;
+        private _previousOverPrimitive: PrimitiveIntersectedInfo;
+        private _actualOverPrimitive: PrimitiveIntersectedInfo;
+        private _capturedPointers: StringDictionary<Prim2DBase>;
+        private _scenePrePointerObserver: Observer<PointerInfoPre>;
         private _worldSpaceNode: WorldSpaceCanvas2d;
         private _mapCounter = 0;
         private _background: Rectangle2D;
@@ -343,10 +684,12 @@
 
         public _renderingSize: Size;
 
-        /**
-         * Method that renders the Canvas, you should not invoke
-         */
-        private _render() {
+        private _updateCanvasState() {
+            // Check if the update has already been made for this render Frame
+            if (this.scene.getRenderId() === this._updateRenderId) {
+                return;
+            }
+
             this._renderingSize.width = this.engine.getRenderWidth();
             this._renderingSize.height = this.engine.getRenderHeight();
 
@@ -364,6 +707,23 @@
             this.updateGlobalTransVis(false);
 
             this._prepareGroupRender(context);
+
+            this._updateRenderId = this.scene.getRenderId();
+        }
+
+        /**
+         * Method that renders the Canvas, you should not invoke
+         */
+        private _render() {
+
+            this._updateCanvasState();
+
+            if (this._primPointerInfo.canvasPointerPos) {
+                this._updateIntersectionList(this._primPointerInfo.canvasPointerPos, false);
+                this._updateOverStatus();   // TODO this._primPointerInfo may not be up to date!
+            }
+
+            var context = new Render2DContext();
             this._groupRender(context);
 
             // If the canvas is cached at canvas level, we must manually render the sprite that will display its content

+ 5 - 0
src/Canvas2d/babylon.group2d.ts

@@ -184,6 +184,11 @@
             this._groupRender(context);
         }
 
+        protected levelIntersect(intersectInfo: IntersectInfo2D): boolean {
+            // If we've made it so far it means the boundingInfo intersection test succeed, the Group2D is shaped the same, so we always return true
+            return true;
+        }
+
         protected updateLevelBoundingInfo() {
             let size: Size;
 

+ 200 - 5
src/Canvas2d/babylon.prim2dBase.ts

@@ -4,10 +4,188 @@
         forceRefreshPrimitive: boolean;
     }
 
+    export class PrimitivePointerInfo {
+        private static _pointerOver        = 0x0001;
+        private static _pointerEnter       = 0x0002;
+        private static _pointerDown        = 0x0004;
+        private static _pointerMouseWheel  = 0x0008;
+        private static _pointerMove        = 0x0010;
+        private static _pointerUp          = 0x0020;
+        private static _pointerOut         = 0x0040;
+        private static _pointerLeave       = 0x0080;
+        private static _pointerGotCapture  = 0x0100;
+        private static _pointerLostCapture = 0x0200;
+
+        private static _mouseWheelPrecision = 3.0;
+
+        // The behavior is based on the HTML specifications of the Pointer Events (https://www.w3.org/TR/pointerevents/#list-of-pointer-events). This is not 100% compliant and not meant to be, but still, it's based on these specs for most use cases to be programmed the same way (as closest as possible) as it would have been in HTML.
+
+        /**
+         * This event type is raised when a pointing device is moved into the hit test boundaries of a primitive.
+         * Bubbles: yes
+         */
+        public static get PointerOver(): number {
+            return PrimitivePointerInfo._pointerOver;
+        }
+
+        /**
+         * This event type is raised when a pointing device is moved into the hit test boundaries of a primitive or one of its descendants.
+         * Bubbles: no
+         */
+        public static get PointerEnter(): number {
+            return PrimitivePointerInfo._pointerEnter;
+        }
+
+        /**
+         * This event type is raised when a pointer enters the active button state (non-zero value in the buttons property). For mouse it's when the device transitions from no buttons depressed to at least one button depressed. For touch/pen this is when a physical contact is made.
+         * Bubbles: yes
+         */
+        public static get PointerDown(): number {
+            return PrimitivePointerInfo._pointerDown;
+        }
+
+        /**
+         * This event type is raised when the pointer is a mouse and it's wheel is rolling
+         * Bubbles: yes
+         */
+        public static get PointerMouseWheel(): number {
+            return PrimitivePointerInfo._pointerMouseWheel;
+        }
+
+        /**
+         * This event type is raised when a pointer change coordinates or when a pointer changes button state, pressure, tilt, or contact geometry and the circumstances produce no other pointers events.
+         * Bubbles: yes
+         */
+        public static get PointerMove(): number {
+            return PrimitivePointerInfo._pointerMove;
+        }
+
+        /**
+         * This event type is raised when the pointer leaves the active buttons states (zero value in the buttons property). For mouse, this is when the device transitions from at least one button depressed to no buttons depressed. For touch/pen, this is when physical contact is removed.
+         * Bubbles: yes
+         */
+        public static get PointerUp(): number {
+            return PrimitivePointerInfo._pointerUp;
+        }
+
+        /**
+         * This event type is raised when a pointing device is moved out of the hit test the boundaries of a primitive.
+         * Bubbles: yes
+         */
+        public static get PointerOut(): number {
+            return PrimitivePointerInfo._pointerOut;
+        }
+
+        /**
+         * This event type is raised when a pointing device is moved out of the hit test boundaries of a primitive and all its descendants.
+         * Bubbles: no
+         */
+        public static get PointerLeave(): number {
+            return PrimitivePointerInfo._pointerLeave;
+        }
+
+        /**
+         * This event type is raised when a primitive receives the pointer capture. This event is fired at the element that is receiving pointer capture. Subsequent events for that pointer will be fired at this element.
+         * Bubbles: yes
+         */
+        public static get PointerGotCapture(): number {
+            return PrimitivePointerInfo._pointerGotCapture;
+        }
+
+        /**
+         * This event type is raised after pointer capture is released for a pointer.
+         * Bubbles: yes
+         */
+        public static get PointerLostCapture(): number {
+            return PrimitivePointerInfo._pointerLostCapture;
+        }
+
+        public static get MouseWheelPrecision(): number {
+            return PrimitivePointerInfo._mouseWheelPrecision;
+        }
+
+        /**
+         * Event Type, one of the static PointerXXXX property defined above (PrimitivePointerInfo.PointerOver to PrimitivePointerInfo.PointerLostCapture)
+         */
+        eventType: number;
+
+        /**
+         * Position of the pointer relative to the bottom/left of the Canvas
+         */
+        canvasPointerPos: Vector2;
+
+        /**
+         * Position of the pointer relative to the bottom/left of the primitive that registered the Observer
+         */
+        primitivePointerPos: Vector2;
+
+        /**
+         * The primitive where the event was initiated first (in case of bubbling)
+         */
+        relatedTarget: Prim2DBase;
+
+        /**
+         * Position of the pointer relative to the bottom/left of the relatedTarget
+         */
+        relatedTargetPointerPos: Vector2;
+
+        /**
+         * An observable can set this property to true to stop bubbling on the upper levels
+         */
+        cancelBubble: boolean;
+
+        ctrlKey: boolean;
+        shiftKey: boolean;
+        altKey: boolean;
+        metaKey: boolean;
+        button: number;
+        buttons: number;
+        mouseWheelDelta: number;
+        pointerId: number;
+        width: number;
+        height: number;
+        presssure: number;
+        tilt: Vector2;
+        isCaptured: boolean;
+
+        constructor() {
+            this.primitivePointerPos = Vector2.Zero();
+            this.tilt = Vector2.Zero();
+            this.cancelBubble = false;
+        }
+
+        updateRelatedTarget(prim: Prim2DBase, primPointerPos: Vector2) {
+            this.relatedTarget = prim;
+            this.relatedTargetPointerPos = primPointerPos;
+        }
+
+        public static getEventTypeName(mask: number): string {
+            switch (mask) {
+                case PrimitivePointerInfo.PointerOver:        return "PointerOver";
+                case PrimitivePointerInfo.PointerEnter:       return "PointerEnter";
+                case PrimitivePointerInfo.PointerDown:        return "PointerDown";
+                case PrimitivePointerInfo.PointerMouseWheel:  return "PointerMouseWheel";
+                case PrimitivePointerInfo.PointerMove:        return "PointerMove";
+                case PrimitivePointerInfo.PointerUp:          return "PointerUp";
+                case PrimitivePointerInfo.PointerOut:         return "PointerOut";
+                case PrimitivePointerInfo.PointerLeave:       return "PointerLeave";
+                case PrimitivePointerInfo.PointerGotCapture:  return "PointerGotCapture";
+                case PrimitivePointerInfo.PointerLostCapture: return "PointerLostCapture";
+            }
+        }
+    }
+
+    export class PrimitiveIntersectedInfo {
+        constructor(public prim: Prim2DBase, public intersectionLocation: Vector2) {
+            
+        }
+    }
+
     export class IntersectInfo2D {
         constructor() {
             this.findFirstOnly = false;
             this.intersectHidden = false;
+            this.pickPosition = Vector2.Zero();
         }
 
         // Input settings, to setup before calling an intersection related method
@@ -20,7 +198,8 @@
         public _localPickPosition: Vector2;
 
         // Output settings, up to date in return of a call to an intersection related method
-        public intersectedPrimitives: Array<{ prim: Prim2DBase, intersectionLocation: Vector2 }>;
+        public topMostIntersectedPrimitive: PrimitiveIntersectedInfo;
+        public intersectedPrimitives: Array<PrimitiveIntersectedInfo>;
         public get isIntersected(): boolean {
             return this.intersectedPrimitives && this.intersectedPrimitives.length > 0;
         }
@@ -43,7 +222,9 @@
             }
 
             this.setupSmartPropertyPrim();
+            this._pointerEventObservable = new Observable<PrimitivePointerInfo>();
             this._isPickable = true;
+            this._siblingDepthOffset = this._hierarchyDepthOffset = 0;
             this._boundingInfoDirty = true;
             this._boundingInfo = new BoundingInfo2D();
             this._owner = owner;
@@ -94,6 +275,10 @@
             return this._parent;
         }
 
+        public get children(): Prim2DBase[] {
+            return this._children;
+        }
+
         public get id(): string {
             return this._id;
         }
@@ -222,7 +407,7 @@
 
                 var tps = new BoundingInfo2D();
                 for (let curChild of this._children) {
-                    curChild.boundingInfo.transformToRef(this.localTransform, tps);
+                    curChild.boundingInfo.transformToRef(curChild.localTransform, tps);
                     bi.unionToRef(tps, bi);
                 }
 
@@ -231,6 +416,10 @@
             return this._boundingInfo;
         }
 
+        public get pointerEventObservable(): Observable<PrimitivePointerInfo> {
+            return this._pointerEventObservable;
+        }
+
         protected levelIntersect(intersectInfo: IntersectInfo2D): boolean {
 
             return false;
@@ -248,7 +437,8 @@
                 intersectInfo._globalPickPosition = Vector2.Zero();
                 Vector2.TransformToRef(intersectInfo.pickPosition, this.globalTransform, intersectInfo._globalPickPosition);
                 intersectInfo._localPickPosition = intersectInfo.pickPosition.clone();
-                intersectInfo.intersectedPrimitives = new Array<{ prim: Prim2DBase, intersectionLocation: Vector2 }>();
+                intersectInfo.intersectedPrimitives = new Array<PrimitiveIntersectedInfo>();
+                intersectInfo.topMostIntersectedPrimitive = null;
             }
 
             if (!intersectInfo.intersectHidden && !this.isVisible) {
@@ -265,7 +455,11 @@
             // We hit the boundingInfo that bounds this primitive and its children, now we have to test on the primitive of this level
             let levelIntersectRes = this.levelIntersect(intersectInfo);
             if (levelIntersectRes) {
-                intersectInfo.intersectedPrimitives.push({ prim: this, intersectionLocation: intersectInfo._localPickPosition});
+                let pii = new PrimitiveIntersectedInfo(this, intersectInfo._localPickPosition.clone());
+                intersectInfo.intersectedPrimitives.push(pii);
+                if (!intersectInfo.topMostIntersectedPrimitive || (intersectInfo.topMostIntersectedPrimitive.prim.getActualZOffset() > pii.prim.getActualZOffset())) {
+                    intersectInfo.topMostIntersectedPrimitive = pii;
+                }
 
                 // If we must stop at the first intersection, we're done, quit!
                 if (intersectInfo.findFirstOnly) {
@@ -352,7 +546,7 @@
             return true;
         }
 
-        protected getActualZOffset(): number {
+        public getActualZOffset(): number {
             return this._zOrder || 1 - (this._siblingDepthOffset + this._hierarchyDepthOffset);
         }
 
@@ -486,6 +680,7 @@
         private _siblingDepthOffset: number;
         private _zOrder: number;
         private _levelVisible: boolean;
+        public _pointerEventObservable: Observable<PrimitivePointerInfo>;
         public _boundingInfoDirty: boolean;
         protected _visibilityChanged;
         private _isPickable;

+ 2 - 2
src/Tools/babylon.observable.ts

@@ -8,7 +8,7 @@
         /**
         * If the callback of a given Observer set skipNextObservers to true the following observers will be ignored
         */
-        constructor(public skipNextObservers = false) {
+        constructor(public mask: number, public skipNextObservers = false) {
         }
     }
 
@@ -81,7 +81,7 @@
          * @param mask
          */
         public notifyObservers(eventData: T, mask: number = -1): void {
-            var state = new EventState();
+            var state = new EventState(mask);
 
             for (var obs of this._observers) {
                 if (obs.mask & mask) {

+ 62 - 6
src/babylon.scene.ts

@@ -32,8 +32,22 @@
     }
 
     /**
+     * This class is used to store pointer related info for the onPrePointerObservable event.
+     * Set the skipOnPointerObservable property to true if you want the engine to stop any process after this event is triggered, even not calling onPointerObservable
+     */
+    export class PointerInfoPre {
+        constructor(public type: number, public event: PointerEvent | MouseWheelEvent, localX, localY) {
+            this.skipOnPointerObservable = false;
+            this.localPosition = new Vector2(localX, localY);
+        }
+
+        public localPosition: Vector2;
+        public skipOnPointerObservable: boolean;
+    }
+
+    /**
      * This type contains all the data related to a pointer event in Babylon.js.
-     * The event member is an instance of PointerEvent for all types except PointerWheel and is of type MouseWheelEvent when type equals PointerWheel. The differents event types can be found in the PointerEventTypes class.
+     * The event member is an instance of PointerEvent for all types except PointerWheel and is of type MouseWheelEvent when type equals PointerWheel. The different event types can be found in the PointerEventTypes class.
      */
     export class PointerInfo {
         constructor(public type: number, public event: PointerEvent | MouseWheelEvent, public pickInfo: PickingInfo) {
@@ -238,10 +252,20 @@
         public onPointerPick: (evt: PointerEvent, pickInfo: PickingInfo) => void;
 
         /**
+         * This observable event is triggered when any mouse event registered during Scene.attach() is called BEFORE the 3D engine to process anything (mesh/sprite picking for instance).
+         * You have the possibility to skip the 3D Engine process and the call to onPointerObservable by setting PointerInfoBase.skipOnPointerObservable to true
+         */
+        public onPrePointerObservable = new Observable<PointerInfoPre>();
+
+        /**
          * Observable event triggered each time an input event is received from the rendering canvas
          */
         public onPointerObservable = new Observable<PointerInfo>();
 
+        public get unTranslatedPointer(): Vector2 {
+            return new Vector2(this._unTranslatedPointerX, this._unTranslatedPointerY);
+        }
+
         public cameraToUseForPointers: Camera = null; // Define this parameter if you are using multiple cameras and you want to specify which one should be used for pointer position
         private _pointerX: number;
         private _pointerY: number;
@@ -637,14 +661,25 @@
             };
 
             this._onPointerMove = (evt: PointerEvent) => {
+
+                this._updatePointerPosition(evt);
+
+                // PreObservable support
+                if (this.onPrePointerObservable.hasObservers()) {
+                    let type = evt.type === "mousewheel" || evt.type === "DOMMouseScroll" ? PointerEventTypes.POINTERWHEEL : PointerEventTypes.POINTERMOVE;
+                    let pi = new PointerInfoPre(type, evt, this._unTranslatedPointerX, this._unTranslatedPointerY);
+                    this.onPrePointerObservable.notifyObservers(pi, type);
+                    if (pi.skipOnPointerObservable) {
+                        return;
+                    }
+                }
+
                 if (!this.cameraToUseForPointers && !this.activeCamera) {
                     return;
                 }
 
                 var canvas = this._engine.getRenderingCanvas();
 
-                this._updatePointerPosition(evt);
-
                 if (!this.pointerMovePredicate) {
                     this.pointerMovePredicate = (mesh: AbstractMesh): boolean => mesh.isPickable && mesh.isVisible && mesh.isReady() && (this.constantlyUpdateMeshUnderPointer || mesh.actionManager !== null && mesh.actionManager !== undefined);
                 }
@@ -689,11 +724,22 @@
             };
 
             this._onPointerDown = (evt: PointerEvent) => {
+                this._updatePointerPosition(evt);
+
+                // PreObservable support
+                if (this.onPrePointerObservable.hasObservers()) {
+                    let type = PointerEventTypes.POINTERDOWN;
+                    let pi = new PointerInfoPre(type, evt, this._unTranslatedPointerX, this._unTranslatedPointerY);
+                    this.onPrePointerObservable.notifyObservers(pi, type);
+                    if (pi.skipOnPointerObservable) {
+                        return;
+                    }
+                }
+
                 if (!this.cameraToUseForPointers && !this.activeCamera) {
                     return;
                 }
 
-                this._updatePointerPosition(evt);
                 this._startingPointerPosition.x = this._pointerX;
                 this._startingPointerPosition.y = this._pointerY;
                 this._startingPointerTime = new Date().getTime();
@@ -782,12 +828,22 @@
             };
 
             this._onPointerUp = (evt: PointerEvent) => {
+                this._updatePointerPosition(evt);
+
+                // PreObservable support
+                if (this.onPrePointerObservable.hasObservers()) {
+                    let type = PointerEventTypes.POINTERUP;
+                    let pi = new PointerInfoPre(type, evt, this._unTranslatedPointerX, this._unTranslatedPointerY);
+                    this.onPrePointerObservable.notifyObservers(pi, type);
+                    if (pi.skipOnPointerObservable) {
+                        return;
+                    }
+                }
+
                 if (!this.cameraToUseForPointers && !this.activeCamera) {
                     return;
                 }
 
-                this._updatePointerPosition(evt);
-
                 if (!this.pointerUpPredicate) {
                     this.pointerUpPredicate = (mesh: AbstractMesh): boolean => {
                         return mesh.isPickable && mesh.isVisible && mesh.isReady() && (!mesh.actionManager || (mesh.actionManager.hasPickTriggers || mesh.actionManager.hasSpecificTrigger(ActionManager.OnLongPressTrigger)));