Pārlūkot izejas kodu

Merge pull request #1141 from nockawa/engine2d

Canvas2D: Inteaction mode available + some bug fixes + Scene prePointer obs
David Catuhe 9 gadi atpakaļ
vecāks
revīzija
dce8ceb651

+ 38 - 24
src/Canvas2d/babylon.bounding2d.ts

@@ -28,38 +28,46 @@
             this.extent = Vector2.Zero();
             this.extent = Vector2.Zero();
         }
         }
 
 
-        public static CreateFromSize(size: Size): BoundingInfo2D {
+        public static CreateFromSize(size: Size, origin?: Vector2): BoundingInfo2D {
             let r = new BoundingInfo2D();
             let r = new BoundingInfo2D();
-            BoundingInfo2D.CreateFromSizeToRef(size, r);
+            BoundingInfo2D.CreateFromSizeToRef(size, r, origin);
             return r;
             return r;
         }
         }
 
 
-        public static CreateFromRadius(radius: number): BoundingInfo2D {
+        public static CreateFromRadius(radius: number, origin?: Vector2): BoundingInfo2D {
             let r = new BoundingInfo2D();
             let r = new BoundingInfo2D();
-            BoundingInfo2D.CreateFromRadiusToRef(radius, r);
+            BoundingInfo2D.CreateFromRadiusToRef(radius, r, origin);
             return r;
             return r;
         }
         }
 
 
-        public static CreateFromPoints(points: Vector2[]): BoundingInfo2D {
+        public static CreateFromPoints(points: Vector2[], origin?: Vector2): BoundingInfo2D {
             let r = new BoundingInfo2D();
             let r = new BoundingInfo2D();
-            BoundingInfo2D.CreateFromPointsToRef(points, r);
+            BoundingInfo2D.CreateFromPointsToRef(points, r, origin);
 
 
             return r;
             return r;
         }
         }
 
 
-        public static CreateFromSizeToRef(size: Size, b: BoundingInfo2D) {
+        public static CreateFromSizeToRef(size: Size, b: BoundingInfo2D, origin?: Vector2) {
             b.center = new Vector2(size.width / 2, size.height / 2);
             b.center = new Vector2(size.width / 2, size.height / 2);
             b.extent = b.center.clone();
             b.extent = b.center.clone();
+            if (origin) {
+                b.center.x -= size.width * origin.x;
+                b.center.y -= size.height * origin.y;
+            }
             b.radius = b.extent.length();
             b.radius = b.extent.length();
         }
         }
 
 
-        public static CreateFromRadiusToRef(radius: number, b: BoundingInfo2D) {
+        public static CreateFromRadiusToRef(radius: number, b: BoundingInfo2D, origin?: Vector2) {
             b.center = Vector2.Zero();
             b.center = Vector2.Zero();
+            if (origin) {
+                b.center.x -= radius * origin.x;
+                b.center.y -= radius * origin.y;
+            }
             b.extent = new Vector2(radius, radius);
             b.extent = new Vector2(radius, radius);
             b.radius = radius;
             b.radius = radius;
         }
         }
 
 
-        public static CreateFromPointsToRef(points: Vector2[], b: BoundingInfo2D) {
+        public static CreateFromPointsToRef(points: Vector2[], b: BoundingInfo2D, origin?: Vector2) {
             let xmin = Number.MAX_VALUE, ymin = Number.MAX_VALUE, xmax = Number.MIN_VALUE, ymax = Number.MIN_VALUE;
             let xmin = Number.MAX_VALUE, ymin = Number.MAX_VALUE, xmax = Number.MIN_VALUE, ymax = Number.MIN_VALUE;
             for (let p of points) {
             for (let p of points) {
                 xmin = Math.min(p.x, xmin);
                 xmin = Math.min(p.x, xmin);
@@ -67,12 +75,17 @@
                 ymin = Math.min(p.y, ymin);
                 ymin = Math.min(p.y, ymin);
                 ymax = Math.max(p.y, ymax);
                 ymax = Math.max(p.y, ymax);
             }
             }
-            BoundingInfo2D.CreateFromMinMaxToRef(xmin, xmax, ymin, ymax, b);
+            BoundingInfo2D.CreateFromMinMaxToRef(xmin, xmax, ymin, ymax, b, origin);
         }
         }
 
 
-
-        public static CreateFromMinMaxToRef(xmin: number, xmax: number, ymin: number, ymax: number, b: BoundingInfo2D) {
-            b.center = new Vector2(xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2);
+        public static CreateFromMinMaxToRef(xmin: number, xmax: number, ymin: number, ymax: number, b: BoundingInfo2D, origin?: Vector2) {
+            let w = xmax - xmin;
+            let h = ymax - ymin;
+            b.center = new Vector2(xmin + w / 2, ymin + h / 2);
+            if (origin) {
+                b.center.x -= w * origin.x;
+                b.center.y -= h * origin.y;
+            }
             b.extent = new Vector2(xmax - b.center.x, ymax - b.center.y);
             b.extent = new Vector2(xmax - b.center.x, ymax - b.center.y);
             b.radius = b.extent.length();
             b.radius = b.extent.length();
         }
         }
@@ -105,9 +118,9 @@
          * @param matrix the transformation matrix to apply
          * @param matrix the transformation matrix to apply
          * @return the new instance containing the result of the transformation applied on this BoundingInfo2D
          * @return the new instance containing the result of the transformation applied on this BoundingInfo2D
          */
          */
-        public transform(matrix: Matrix, origin: Vector2=null): BoundingInfo2D {
+        public transform(matrix: Matrix): BoundingInfo2D {
             var r = new BoundingInfo2D();
             var r = new BoundingInfo2D();
-            this.transformToRef(matrix, origin, r);
+            this.transformToRef(matrix, r);
             return r;
             return r;
         }
         }
 
 
@@ -125,11 +138,10 @@
         /**
         /**
          * Transform this BoundingInfo2D with a given matrix and store the result in an existing BoundingInfo2D instance.
          * Transform this BoundingInfo2D with a given matrix and store the result in an existing BoundingInfo2D instance.
          * This is a GC friendly version, try to use it as much as possible, specially if your transformation is inside a loop, allocate the result object once for good outside of the loop and use it every time.
          * This is a GC friendly version, try to use it as much as possible, specially if your transformation is inside a loop, allocate the result object once for good outside of the loop and use it every time.
-         * @param origin An optional normalized origin to apply before the transformation. 0;0 is top/left, 0.5;0.5 is center, etc.
          * @param matrix The matrix to use to compute the transformation
          * @param matrix The matrix to use to compute the transformation
          * @param result A VALID (i.e. allocated) BoundingInfo2D object where the result will be stored
          * @param result A VALID (i.e. allocated) BoundingInfo2D object where the result will be stored
          */
          */
-        public transformToRef(matrix: Matrix, origin: Vector2, result: BoundingInfo2D) {
+        public transformToRef(matrix: Matrix, result: BoundingInfo2D) {
             // Construct a bounding box based on the extent values
             // Construct a bounding box based on the extent values
             let p = new Array<Vector2>(4);
             let p = new Array<Vector2>(4);
             p[0] = new Vector2(this.center.x + this.extent.x, this.center.y + this.extent.y);
             p[0] = new Vector2(this.center.x + this.extent.x, this.center.y + this.extent.y);
@@ -137,13 +149,6 @@
             p[2] = new Vector2(this.center.x - this.extent.x, this.center.y - this.extent.y);
             p[2] = new Vector2(this.center.x - this.extent.x, this.center.y - this.extent.y);
             p[3] = new Vector2(this.center.x - this.extent.x, this.center.y + this.extent.y);
             p[3] = new Vector2(this.center.x - this.extent.x, this.center.y + this.extent.y);
 
 
-            //if (origin) {
-            //    let off = new Vector2((p[0].x - p[2].x) * origin.x, (p[0].y - p[2].y) * origin.y);
-            //    for (let j = 0; j < 4; j++) {
-            //        p[j].subtractInPlace(off);
-            //    }
-            //}
-
             // Transform the four points of the bounding box with the matrix
             // Transform the four points of the bounding box with the matrix
             for (let i = 0; i < 4; i++) {
             for (let i = 0; i < 4; i++) {
                 Vector2.TransformToRef(p[i], matrix, p[i]);
                 Vector2.TransformToRef(p[i], matrix, p[i]);
@@ -165,5 +170,14 @@
             BoundingInfo2D.CreateFromMinMaxToRef(xmin, xmax, ymin, ymax, result);
             BoundingInfo2D.CreateFromMinMaxToRef(xmin, xmax, ymin, ymax, result);
         }
         }
 
 
+        doesIntersect(pickPosition: Vector2): boolean {
+            // is it inside the radius?
+            let pickLocal = pickPosition.subtract(this.center);
+            if (pickLocal.lengthSquared() <= (this.radius * this.radius)) {
+                // is it inside the rectangle?
+                return ((Math.abs(pickLocal.x) <= this.extent.x) && (Math.abs(pickLocal.y) <= this.extent.y));
+            }
+            return false;
+        }
     }
     }
 }
 }

+ 387 - 11
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.
          * 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.
          * 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.
          * 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 scene the Scene that owns the Canvas
          * @param name the name of the Canvas, for information purpose only
          * @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 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 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.
          * @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();
             let c = new Canvas2D();
-            c.setupCanvas(scene, name, size, true, cachingStrategy);
+            c.setupCanvas(scene, name, size, true, cachingStrategy, enableInteraction);
             c.position = pos;
             c.position = pos;
+            c.origin = Vector2.Zero();
 
 
             return c;
             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.
          * 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.
          * 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 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
          * @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) {
             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!");
                 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();
             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 plane = new WorldSpaceCanvas2d(name, scene, c);
             let vertexData = VertexData.CreatePlane({ width: size.width/2, height: size.height/2, sideOrientation: sideOrientation });
             let vertexData = VertexData.CreatePlane({ width: size.width/2, height: size.height/2, sideOrientation: sideOrientation });
@@ -114,7 +115,7 @@
             return c;
             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();
             let engine = scene.getEngine();
             this._fitRenderingDevice = !size;
             this._fitRenderingDevice = !size;
             if (!size) {
             if (!size) {
@@ -127,6 +128,8 @@
             this._hierarchyLevelZFactor = 1 / this._hierarchyMaxDepth;
             this._hierarchyLevelZFactor = 1 / this._hierarchyMaxDepth;
             this._hierarchyLevelMaxSiblingCount = 1000;
             this._hierarchyLevelMaxSiblingCount = 1000;
             this._hierarchySiblingZDelta = this._hierarchyLevelZFactor / this._hierarchyLevelMaxSiblingCount;
             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);
             this.setupGroup2D(this, null, name, Vector2.Zero(), size, this._cachingStrategy===Canvas2D.CACHESTRATEGY_ALLGROUPS ? Group2D.GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE : Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY);
 
 
@@ -141,6 +144,7 @@
 
 
             if (cachingstrategy !== Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
             if (cachingstrategy !== Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
                 this._background = Rectangle2D.Create(this, "###CANVAS BACKGROUND###", 0, 0, size.width, size.height);
                 this._background = Rectangle2D.Create(this, "###CANVAS BACKGROUND###", 0, 0, size.width, size.height);
+                this._background.isPickable = false;
                 this._background.origin = Vector2.Zero();
                 this._background.origin = Vector2.Zero();
                 this._background.levelVisible = false;
                 this._background.levelVisible = false;
             }
             }
@@ -159,6 +163,332 @@
 
 
             this._supprtInstancedArray = this._engine.getCaps().instancedArrays !== null;
             this._supprtInstancedArray = this._engine.getCaps().instancedArrays !== null;
 //            this._supprtInstancedArray = false; // TODO REMOVE!!!
 //            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));
+        }
+
+        /**
+         * Internal method, you should use the Prim2DBase version instead
+         */
+        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;
+        }
+
+        /**
+         * Internal method, you should use the Prim2DBase version instead
+         */
+        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;
+        }
+
+        /**
+         * Determine if the given pointer is captured or not
+         * @param pointerId the Id of the pointer
+         * @return true if it's captured, false otherwise
+         */
+        public isPointerCaptured(pointerId: number): boolean {
+            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 = false;
+        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);
+                }
+            });
         }
         }
 
 
         /**
         /**
@@ -170,6 +500,10 @@
                 return false;
                 return false;
             }
             }
 
 
+            if (this.interactionEnabled) {
+                this._setupInteraction(false);
+            }
+
             if (this._beforeRenderObserver) {
             if (this._beforeRenderObserver) {
                 this._scene.onBeforeRenderObservable.remove(this._beforeRenderObserver);
                 this._scene.onBeforeRenderObservable.remove(this._beforeRenderObserver);
                 this._beforeRenderObserver = null;
                 this._beforeRenderObserver = null;
@@ -293,6 +627,18 @@
             this._background.levelVisible = true;
             this._background.levelVisible = true;
         }
         }
 
 
+        /**
+         * Enable/Disable interaction for this Canvas
+         * When enabled the Prim2DBase.pointerEventObservable property will notified when appropriate events occur
+         */
+        public get interactionEnabled(): boolean {
+            return this._interactionEnabled;
+        }
+
+        public set interactionEnabled(enable: boolean) {
+            this._setupInteraction(enable);
+        }
+
         public get _engineData(): Canvas2DEngineBoundData {
         public get _engineData(): Canvas2DEngineBoundData {
             return this.__engineData;
             return this.__engineData;
         }
         }
@@ -322,6 +668,17 @@
         }
         }
 
 
         private __engineData: Canvas2DEngineBoundData;
         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 _worldSpaceNode: WorldSpaceCanvas2d;
         private _mapCounter = 0;
         private _mapCounter = 0;
         private _background: Rectangle2D;
         private _background: Rectangle2D;
@@ -342,10 +699,12 @@
 
 
         public _renderingSize: Size;
         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.width = this.engine.getRenderWidth();
             this._renderingSize.height = this.engine.getRenderHeight();
             this._renderingSize.height = this.engine.getRenderHeight();
 
 
@@ -363,6 +722,23 @@
             this.updateGlobalTransVis(false);
             this.updateGlobalTransVis(false);
 
 
             this._prepareGroupRender(context);
             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);
             this._groupRender(context);
 
 
             // If the canvas is cached at canvas level, we must manually render the sprite that will display its content
             // If the canvas is cached at canvas level, we must manually render the sprite that will display its content
@@ -434,7 +810,7 @@
                 // Create a Sprite that will be used to render this cache, the "__cachedSpriteOfGroup__" starting id is a hack to bypass exception throwing in case of the Canvas doesn't normally allows direct primitives
                 // Create a Sprite that will be used to render this cache, the "__cachedSpriteOfGroup__" starting id is a hack to bypass exception throwing in case of the Canvas doesn't normally allows direct primitives
                 else {
                 else {
                     let sprite = Sprite2D.Create(parent, `__cachedSpriteOfGroup__${group.id}`, group.position.x, group.position.y, map, node.contentSize, node.pos, false);
                     let sprite = Sprite2D.Create(parent, `__cachedSpriteOfGroup__${group.id}`, group.position.x, group.position.y, map, node.contentSize, node.pos, false);
-                    sprite.origin = Vector2.Zero();
+                    sprite.origin = group.origin.clone();
                     res.sprite = sprite;
                     res.sprite = sprite;
                 }
                 }
             }
             }

+ 9 - 2
src/Canvas2d/babylon.group2d.ts

@@ -43,7 +43,7 @@
         static _createCachedCanvasGroup(owner: Canvas2D): Group2D {
         static _createCachedCanvasGroup(owner: Canvas2D): Group2D {
             var g = new Group2D();
             var g = new Group2D();
             g.setupGroup2D(owner, null, "__cachedCanvasGroup__", Vector2.Zero());
             g.setupGroup2D(owner, null, "__cachedCanvasGroup__", Vector2.Zero());
-
+            g.origin = Vector2.Zero();
             return g;
             return g;
             
             
         }
         }
@@ -184,6 +184,11 @@
             this._groupRender(context);
             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() {
         protected updateLevelBoundingInfo() {
             let size: Size;
             let size: Size;
 
 
@@ -267,7 +272,7 @@
 
 
                         // We need to check if prepare is needed because even if the primitive is in the dirtyList, its parent primitive may also have been modified, then prepared, then recurse on its children primitives (this one for instance) if the changes where impacting them.
                         // We need to check if prepare is needed because even if the primitive is in the dirtyList, its parent primitive may also have been modified, then prepared, then recurse on its children primitives (this one for instance) if the changes where impacting them.
                         // For instance: a Rect's position change, the position of its children primitives will also change so a prepare will be call on them. If a child was in the dirtyList we will avoid a second prepare by making this check.
                         // For instance: a Rect's position change, the position of its children primitives will also change so a prepare will be call on them. If a child was in the dirtyList we will avoid a second prepare by making this check.
-                        if (!p.isDisposed && p.needPrepare()) {
+                        if (!p.isDisposed && p._needPrepare()) {
                             p._prepareRender(context);
                             p._prepareRender(context);
                         }
                         }
                     });
                     });
@@ -412,6 +417,8 @@
                 this._cacheRenderSprite.rotation = this.rotation;
                 this._cacheRenderSprite.rotation = this.rotation;
             } else if (prop.id === Prim2DBase.scaleProperty.id) {
             } else if (prop.id === Prim2DBase.scaleProperty.id) {
                 this._cacheRenderSprite.scale = this.scale;
                 this._cacheRenderSprite.scale = this.scale;
+            } else if (prop.id === Prim2DBase.originProperty.id) {
+                this._cacheRenderSprite.origin = this.origin.clone();
             } else if (prop.id === Group2D.actualSizeProperty.id) {
             } else if (prop.id === Group2D.actualSizeProperty.id) {
                 this._cacheRenderSprite.spriteSize = this.actualSize.clone();
                 this._cacheRenderSprite.spriteSize = this.actualSize.clone();
                 //console.log(`[${this._globalTransformProcessStep}] Sync Sprite ${this.id}, width: ${this.actualSize.width}, height: ${this.actualSize.height}`);
                 //console.log(`[${this._globalTransformProcessStep}] Sync Sprite ${this.id}, width: ${this.actualSize.width}, height: ${this.actualSize.height}`);

+ 506 - 15
src/Canvas2d/babylon.prim2dBase.ts

@@ -4,7 +4,286 @@
         forceRefreshPrimitive: boolean;
         forceRefreshPrimitive: boolean;
     }
     }
 
 
+    /**
+     * This class store information for the pointerEventObservable Observable.
+     * The Observable is divided into many sub events (using the Mask feature of the Observable pattern): PointerOver, PointerEnter, PointerDown, PointerMouseWheel, PointerMove, PointerUp, PointerDown, PointerLeave, PointerGotCapture and PointerLostCapture.
+     */
+    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;
+
+        /**
+         * True if the Control keyboard key is down
+         */
+        ctrlKey: boolean;
+
+        /**
+         * true if the Shift keyboard key is down
+         */
+        shiftKey: boolean;
+
+        /**
+         * true if the Alt keyboard key is down
+         */
+        altKey: boolean;
+
+        /**
+         * true if the Meta keyboard key is down
+         */
+        metaKey: boolean;
+
+        /**
+         * For button, buttons, refer to https://www.w3.org/TR/pointerevents/#button-states
+         */
+        button: number;
+        /**
+         * For button, buttons, refer to https://www.w3.org/TR/pointerevents/#button-states
+         */
+        buttons: number;
+
+        /**
+         * The amount of mouse wheel rolled
+         */
+        mouseWheelDelta: number;
+
+        /**
+         * Id of the Pointer involved in the event
+         */
+        pointerId: number;
+        width: number;
+        height: number;
+        presssure: number;
+        tilt: Vector2;
+
+        /**
+         * true if the involved pointer is captured for a particular primitive, false otherwise.
+         */
+        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";
+            }
+        }
+    }
+
+    /**
+     * Stores information about a Primitive that was intersected
+     */
+    export class PrimitiveIntersectedInfo {
+        constructor(public prim: Prim2DBase, public intersectionLocation: Vector2) {
+            
+        }
+    }
+
+    /**
+     * Main class used for the Primitive Intersection API
+     */
+    export class IntersectInfo2D {
+        constructor() {
+            this.findFirstOnly = false;
+            this.intersectHidden = false;
+            this.pickPosition = Vector2.Zero();
+        }
+
+        // Input settings, to setup before calling an intersection related method
+
+        /**
+         * Set the pick position, relative to the primitive where the intersection test is made
+         */
+        public pickPosition: Vector2;
+
+        /**
+         * If true the intersection will stop at the first hit, if false all primitives will be tested and the intersectedPrimitives array will be filled accordingly (false default)
+         */
+        public findFirstOnly: boolean;
+
+        /**
+         * If true the intersection test will also be made on hidden primitive (false default)
+         */
+        public intersectHidden: boolean;
+
+        // Intermediate data, don't use!
+        public _globalPickPosition: Vector2;
+        public _localPickPosition: Vector2;
+
+        // Output settings, up to date in return of a call to an intersection related method
+
+        /**
+         * The topmost intersected primitive
+         */
+        public topMostIntersectedPrimitive: PrimitiveIntersectedInfo;
+
+        /**
+         * The array containing all intersected primitive, in no particular order.
+         */
+        public intersectedPrimitives: Array<PrimitiveIntersectedInfo>;
+
+        /**
+         * true if at least one primitive intersected during the test
+         */
+        public get isIntersected(): boolean {
+            return this.intersectedPrimitives && this.intersectedPrimitives.length > 0;
+        }
+
+        // Internals, don't use
+        public _exit(firstLevel: boolean) {
+            if (firstLevel) {
+                this._globalPickPosition = null;
+            }
+        }
+    }
+
     @className("Prim2DBase")
     @className("Prim2DBase")
+    /**
+     * Base class for a Primitive of the Canvas2D feature
+     */
     export class Prim2DBase extends SmartPropertyPrim {
     export class Prim2DBase extends SmartPropertyPrim {
         static PRIM2DBASE_PROPCOUNT: number = 10;
         static PRIM2DBASE_PROPCOUNT: number = 10;
 
 
@@ -14,6 +293,9 @@
             }
             }
 
 
             this.setupSmartPropertyPrim();
             this.setupSmartPropertyPrim();
+            this._pointerEventObservable = new Observable<PrimitivePointerInfo>();
+            this._isPickable = true;
+            this._siblingDepthOffset = this._hierarchyDepthOffset = 0;
             this._boundingInfoDirty = true;
             this._boundingInfoDirty = true;
             this._boundingInfo = new BoundingInfo2D();
             this._boundingInfo = new BoundingInfo2D();
             this._owner = owner;
             this._owner = owner;
@@ -45,6 +327,11 @@
             this.origin = new Vector2(0.5, 0.5);
             this.origin = new Vector2(0.5, 0.5);
         }
         }
 
 
+        /**
+         * From 'this' primitive, traverse up (from parent to parent) until the given predicate is true
+         * @param predicate the predicate to test on each parent
+         * @return the first primitive where the predicate was successful
+         */
         public traverseUp(predicate: (p: Prim2DBase) => boolean): Prim2DBase {
         public traverseUp(predicate: (p: Prim2DBase) => boolean): Prim2DBase {
             let p: Prim2DBase = this;
             let p: Prim2DBase = this;
             while (p != null) {
             while (p != null) {
@@ -56,14 +343,30 @@
             return null;
             return null;
         }
         }
 
 
+        /**
+         * Retrieve the owner Canvas2D
+         */
         public get owner(): Canvas2D {
         public get owner(): Canvas2D {
             return this._owner;
             return this._owner;
         }
         }
 
 
+        /**
+         * Get the parent primitive (can be the Canvas, only the Canvas has no parent)
+         */
         public get parent(): Prim2DBase {
         public get parent(): Prim2DBase {
             return this._parent;
             return this._parent;
         }
         }
 
 
+        /**
+         * The array of direct children primitives
+         */
+        public get children(): Prim2DBase[] {
+            return this._children;
+        }
+
+        /**
+         * The identifier of this primitive, may not be unique, it's for information purpose only
+         */
         public get id(): string {
         public get id(): string {
             return this._id;
             return this._id;
         }
         }
@@ -77,6 +380,9 @@
         public static zOrderProperty: Prim2DPropInfo;
         public static zOrderProperty: Prim2DPropInfo;
 
 
         @instanceLevelProperty(1, pi => Prim2DBase.positionProperty = pi, false, true)
         @instanceLevelProperty(1, pi => Prim2DBase.positionProperty = pi, false, true)
+        /**
+         * Position of the primitive, relative to its parent.
+         */
         public get position(): Vector2 {
         public get position(): Vector2 {
             return this._position;
             return this._position;
         }
         }
@@ -86,6 +392,10 @@
         }
         }
 
 
         @instanceLevelProperty(2, pi => Prim2DBase.rotationProperty = pi, false, true)
         @instanceLevelProperty(2, pi => Prim2DBase.rotationProperty = pi, false, true)
+        /**
+         * Rotation of the primitive, in radian, along the Z axis
+         * @returns {} 
+         */
         public get rotation(): number {
         public get rotation(): number {
             return this._rotation;
             return this._rotation;
         }
         }
@@ -95,6 +405,9 @@
         }
         }
 
 
         @instanceLevelProperty(3, pi => Prim2DBase.scaleProperty = pi, false, true)
         @instanceLevelProperty(3, pi => Prim2DBase.scaleProperty = pi, false, true)
+        /**
+         * Uniform scale applied on the primitive
+         */
         public set scale(value: number) {
         public set scale(value: number) {
             this._scale = value;
             this._scale = value;
         }
         }
@@ -103,6 +416,14 @@
             return this._scale;
             return this._scale;
         }
         }
 
 
+        /**
+         * this method must be implemented by the primitive type to return its size
+         * @returns The size of the primitive
+         */
+        public get actualSize(): Size {
+            return undefined;
+        }
+
         @instanceLevelProperty(4, pi => Prim2DBase.originProperty = pi, false, true)
         @instanceLevelProperty(4, pi => Prim2DBase.originProperty = pi, false, true)
         public set origin(value: Vector2) {
         public set origin(value: Vector2) {
             this._origin = value;
             this._origin = value;
@@ -112,9 +433,9 @@
          * The origin defines the normalized coordinate of the center of the primitive, from the top/left corner.
          * The origin defines the normalized coordinate of the center of the primitive, from the top/left corner.
          * The origin is used only to compute transformation of the primitive, it has no meaning in the primitive local frame of reference
          * The origin is used only to compute transformation of the primitive, it has no meaning in the primitive local frame of reference
          * For instance:
          * For instance:
-         * 0,0 means the center is top/left
-         * 0.5,0.5 means the center is at the center of the primitive
-         * 0,1 means the center is bottom/left
+         * 0,0 means the center is bottom/left. Which is the default for Canvas2D instances
+         * 0.5,0.5 means the center is at the center of the primitive, which is default of all types of Primitives
+         * 0,1 means the center is top/left
          * @returns The normalized center.
          * @returns The normalized center.
          */
          */
         public get origin(): Vector2 {
         public get origin(): Vector2 {
@@ -122,6 +443,10 @@
         }
         }
 
 
         @dynamicLevelProperty(5, pi => Prim2DBase.levelVisibleProperty = pi)
         @dynamicLevelProperty(5, pi => Prim2DBase.levelVisibleProperty = pi)
+        /**
+         * Let the user defines if the Primitive is hidden or not at its level. As Primitives inherit the hidden status from their parent, only the isVisible property give properly the real visible state.
+         * Default is true, setting to false will hide this primitive and its children.
+         */
         public get levelVisible(): boolean {
         public get levelVisible(): boolean {
             return this._levelVisible;
             return this._levelVisible;
         }
         }
@@ -131,6 +456,10 @@
         }
         }
 
 
         @instanceLevelProperty(6, pi => Prim2DBase.isVisibleProperty = pi)
         @instanceLevelProperty(6, pi => Prim2DBase.isVisibleProperty = pi)
+        /**
+         * Use ONLY THE GETTER to determine if the primitive is visible or not.
+         * The Setter is for internal purpose only!
+         */
         public get isVisible(): boolean {
         public get isVisible(): boolean {
             return this._isVisible;
             return this._isVisible;
         }
         }
@@ -140,6 +469,10 @@
         }
         }
 
 
         @instanceLevelProperty(7, pi => Prim2DBase.zOrderProperty = pi)
         @instanceLevelProperty(7, pi => Prim2DBase.zOrderProperty = pi)
+        /**
+         * You can override the default Z Order through this property, but most of the time the default behavior is acceptable
+         * @returns {} 
+         */
         public get zOrder(): number {
         public get zOrder(): number {
             return this._zOrder;
             return this._zOrder;
         }
         }
@@ -148,22 +481,60 @@
             this._zOrder = value;
             this._zOrder = value;
         }
         }
 
 
+        /**
+         * Define if the Primitive can be subject to intersection test or not (default is true)
+         */
+        public get isPickable(): boolean {
+            return this._isPickable;
+        }
+
+        public set isPickable(value: boolean) {
+            this._isPickable = value;
+        }
+
+        /**
+         * Return the depth level of the Primitive into the Canvas' Graph. A Canvas will be 0, its direct children 1, and so on.
+         * @returns {} 
+         */
         public get hierarchyDepth(): number {
         public get hierarchyDepth(): number {
             return this._hierarchyDepth;
             return this._hierarchyDepth;
         }
         }
 
 
+        /**
+         * Retrieve the Group that is responsible to render this primitive
+         * @returns {} 
+         */
         public get renderGroup(): Group2D {
         public get renderGroup(): Group2D {
             return this._renderGroup;
             return this._renderGroup;
         }
         }
 
 
+        /**
+         * Get the global transformation matrix of the primitive
+         */
         public get globalTransform(): Matrix {
         public get globalTransform(): Matrix {
             return this._globalTransform;
             return this._globalTransform;
         }
         }
 
 
+        /**
+         * Get invert of the global transformation matrix of the primitive
+         * @returns {} 
+         */
         public get invGlobalTransform(): Matrix {
         public get invGlobalTransform(): Matrix {
             return this._invGlobalTransform;
             return this._invGlobalTransform;
         }
         }
 
 
+        /**
+         * Get the local transformation of the primitive
+         */
+        public get localTransform(): Matrix {
+            this._updateLocalTransform();
+            return this._localTransform;
+        }
+
+        /**
+         * Get the boundingInfo associated to the primitive.
+         * The value is supposed to be always up to date
+         */
         public get boundingInfo(): BoundingInfo2D {
         public get boundingInfo(): BoundingInfo2D {
             if (this._boundingInfoDirty) {
             if (this._boundingInfoDirty) {
                 this._boundingInfo = this.levelBoundingInfo.clone();
                 this._boundingInfo = this.levelBoundingInfo.clone();
@@ -171,8 +542,7 @@
 
 
                 var tps = new BoundingInfo2D();
                 var tps = new BoundingInfo2D();
                 for (let curChild of this._children) {
                 for (let curChild of this._children) {
-                    let t = curChild.globalTransform.multiply(this.invGlobalTransform);
-                    curChild.boundingInfo.transformToRef(t, curChild.origin, tps);
+                    curChild.boundingInfo.transformToRef(curChild.localTransform, tps);
                     bi.unionToRef(tps, bi);
                     bi.unionToRef(tps, bi);
                 }
                 }
 
 
@@ -181,6 +551,105 @@
             return this._boundingInfo;
             return this._boundingInfo;
         }
         }
 
 
+        /**
+         * Interaction with the primitive can be create using this Observable. See the PrimitivePointerInfo class for more information
+         */
+        public get pointerEventObservable(): Observable<PrimitivePointerInfo> {
+            return this._pointerEventObservable;
+        }
+
+        protected levelIntersect(intersectInfo: IntersectInfo2D): boolean {
+
+            return false;
+        }
+
+        /**
+         * Capture all the Events of the given PointerId for this primitive.
+         * Don't forget to call releasePointerEventsCapture when done.
+         * @param pointerId the Id of the pointer to capture the events from.
+         */
+        public setPointerEventCapture(pointerId: number): boolean {
+            return this.owner._setPointerCapture(pointerId, this);
+        }
+
+        /**
+         * Release a captured pointer made with setPointerEventCapture.
+         * @param pointerId the Id of the pointer to release the capture from.
+         */
+        public releasePointerEventsCapture(pointerId: number): boolean {
+            return this.owner._releasePointerCapture(pointerId, this);
+        }
+
+        /**
+         * Make an intersection test with the primitive, all inputs/outputs are stored in the IntersectInfo2D class, see its documentation for more information.
+         * @param intersectInfo contains the settings of the intersection to perform, to setup before calling this method as well as the result, available after a call to this method.
+         */
+        public intersect(intersectInfo: IntersectInfo2D): boolean {
+            if (!intersectInfo) {
+                return false;
+            }
+
+            // If this is null it means this method is call for the first level, initialize stuffs
+            let firstLevel = !intersectInfo._globalPickPosition;
+            if (firstLevel) {
+                // Compute the pickPosition in global space and use it to find the local position for each level down, always relative from the world to get the maximum accuracy (and speed). The other way would have been to compute in local every level down relative to its parent's local, which wouldn't be as accurate (even if javascript number is 80bits accurate).
+                intersectInfo._globalPickPosition = Vector2.Zero();
+                Vector2.TransformToRef(intersectInfo.pickPosition, this.globalTransform, intersectInfo._globalPickPosition);
+                intersectInfo._localPickPosition = intersectInfo.pickPosition.clone();
+                intersectInfo.intersectedPrimitives = new Array<PrimitiveIntersectedInfo>();
+                intersectInfo.topMostIntersectedPrimitive = null;
+            }
+
+            if (!intersectInfo.intersectHidden && !this.isVisible) {
+                return false;
+            }
+
+            // Fast rejection test with boundingInfo
+            if (!this.boundingInfo.doesIntersect(intersectInfo._localPickPosition)) {
+                // Important to call this before each return to allow a good recursion next time this intersectInfo is reused
+                intersectInfo._exit(firstLevel);
+                return false;
+            }
+
+            // 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) {
+                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) {
+                    intersectInfo._exit(firstLevel);
+                    return true;
+                }
+            }
+
+            // Recurse to children if needed
+            if (!levelIntersectRes || !intersectInfo.findFirstOnly) {
+                for (let curChild of this._children) {
+                    // Don't test primitive not pick able or if it's hidden and we don't test hidden ones
+                    if (!curChild.isPickable || (!intersectInfo.intersectHidden && !curChild.isVisible)) {
+                        continue;
+                    }
+
+                    // Must compute the localPickLocation for the children level
+                    Vector2.TransformToRef(intersectInfo._globalPickPosition, curChild.invGlobalTransform, intersectInfo._localPickPosition);
+
+                    // If we got an intersection with the child and we only need to find the first one, quit!
+                    if (curChild.intersect(intersectInfo) && intersectInfo.findFirstOnly) {
+                        intersectInfo._exit(firstLevel);
+                        return true;
+                    }
+                }
+            }
+
+            intersectInfo._exit(firstLevel);
+            return intersectInfo.isIntersected;
+        }
+
         public moveChild(child: Prim2DBase, previous: Prim2DBase): boolean {
         public moveChild(child: Prim2DBase, previous: Prim2DBase): boolean {
             if (child.parent !== this) {
             if (child.parent !== this) {
                 return false;
                 return false;
@@ -236,7 +705,7 @@
             return true;
             return true;
         }
         }
 
 
-        protected getActualZOffset(): number {
+        public getActualZOffset(): number {
             return this._zOrder || 1 - (this._siblingDepthOffset + this._hierarchyDepthOffset);
             return this._zOrder || 1 - (this._siblingDepthOffset + this._hierarchyDepthOffset);
         }
         }
 
 
@@ -246,7 +715,7 @@
             }
             }
         }
         }
 
 
-        public needPrepare(): boolean {
+        public _needPrepare(): boolean {
             return (this.isVisible || this._visibilityChanged) && (this._modelDirty || (this._instanceDirtyFlags !== 0) || (this._globalTransformProcessStep !== this._globalTransformStep));
             return (this.isVisible || this._visibilityChanged) && (this._modelDirty || (this._instanceDirtyFlags !== 0) || (this._globalTransformProcessStep !== this._globalTransformStep));
         }
         }
 
 
@@ -299,6 +768,28 @@
             }
             }
         }
         }
 
 
+        private _updateLocalTransform(): boolean {
+            let tflags = Prim2DBase.positionProperty.flagId | Prim2DBase.rotationProperty.flagId | Prim2DBase.scaleProperty.flagId;
+            if (this.checkPropertiesDirty(tflags)) {
+                var rot = Quaternion.RotationAxis(new Vector3(0, 0, 1), this._rotation);
+                var local = Matrix.Compose(new Vector3(this._scale, this._scale, this._scale), rot, new Vector3(this._position.x, this._position.y, 0));
+
+                this._localTransform = local;
+                this.clearPropertiesDirty(tflags);
+
+                // this is important to access actualSize AFTER fetching a first version of the local transform and reset the dirty flag, because accessing actualSize on a Group2D which actualSize is built from its content will trigger a call to this very method on this very object. We won't mind about the origin offset not being computed, as long as we return a local transform based on the position/rotation/scale
+                var actualSize = this.actualSize;
+                if (!actualSize) {
+                    throw new Error(`The primitive type: ${Tools.getClassName(this)} must implement the actualSize get property!`);
+                }
+
+                local.m[12] -= (actualSize.width * this.origin.x) * local.m[0] + (actualSize.height * this.origin.y) * local.m[4];
+                local.m[13] -= (actualSize.width * this.origin.x) * local.m[1] + (actualSize.height * this.origin.y) * local.m[5];
+                return true;
+            }
+            return false;
+        }
+
         protected updateGlobalTransVis(recurse: boolean) {
         protected updateGlobalTransVis(recurse: boolean) {
             if (this.isDisposed) {
             if (this.isDisposed) {
                 return;
                 return;
@@ -317,19 +808,16 @@
                 // Detect a change of visibility
                 // Detect a change of visibility
                 this._visibilityChanged = (curVisibleState !== undefined) && curVisibleState !== this.isVisible;
                 this._visibilityChanged = (curVisibleState !== undefined) && curVisibleState !== this.isVisible;
 
 
-                // Detect if either the parent or this node changed
-                let tflags = Prim2DBase.positionProperty.flagId | Prim2DBase.rotationProperty.flagId | Prim2DBase.scaleProperty.flagId;
-                if (this.isVisible && (this._parent && this._parent._globalTransformStep !== this._parentTransformStep) || this.checkPropertiesDirty(tflags)) {
-                    var rot = Quaternion.RotationAxis(new Vector3(0, 0, 1), this._rotation);
-                    var local = Matrix.Compose(new Vector3(this._scale, this._scale, this._scale), rot, new Vector3(this._position.x, this._position.y, 0));
+                // Get/compute the localTransform
+                let localDirty = this._updateLocalTransform();
 
 
-                    this._globalTransform = this._parent ? local.multiply(this._parent._globalTransform) : local;
+                // Check if we have to update the globalTransform
+                if (!this._globalTransform || localDirty || (this._parent && this._parent._globalTransformStep !== this._parentTransformStep)) {
+                    this._globalTransform = this._parent ? this._localTransform.multiply(this._parent._globalTransform) : this._localTransform;
                     this._invGlobalTransform = Matrix.Invert(this._globalTransform);
                     this._invGlobalTransform = Matrix.Invert(this._globalTransform);
 
 
                     this._globalTransformStep = this.owner._globalTransformProcessStep + 1;
                     this._globalTransformStep = this.owner._globalTransformProcessStep + 1;
                     this._parentTransformStep = this._parent ? this._parent._globalTransformStep : 0;
                     this._parentTransformStep = this._parent ? this._parent._globalTransformStep : 0;
-
-                    this.clearPropertiesDirty(tflags);
                 }
                 }
                 this._globalTransformProcessStep = this.owner._globalTransformProcessStep;
                 this._globalTransformProcessStep = this.owner._globalTransformProcessStep;
             }
             }
@@ -351,8 +839,10 @@
         private _siblingDepthOffset: number;
         private _siblingDepthOffset: number;
         private _zOrder: number;
         private _zOrder: number;
         private _levelVisible: boolean;
         private _levelVisible: boolean;
+        public _pointerEventObservable: Observable<PrimitivePointerInfo>;
         public _boundingInfoDirty: boolean;
         public _boundingInfoDirty: boolean;
         protected _visibilityChanged;
         protected _visibilityChanged;
+        private _isPickable;
         private _isVisible: boolean;
         private _isVisible: boolean;
         private _id: string;
         private _id: string;
         private _position: Vector2;
         private _position: Vector2;
@@ -370,6 +860,7 @@
 
 
         // Stores the previous 
         // Stores the previous 
         protected _globalTransformProcessStep: number;
         protected _globalTransformProcessStep: number;
+        protected _localTransform: Matrix;
         protected _globalTransform: Matrix;
         protected _globalTransform: Matrix;
         protected _invGlobalTransform: Matrix;
         protected _invGlobalTransform: Matrix;
     }
     }

+ 16 - 1
src/Canvas2d/babylon.rectangle2d.ts

@@ -148,6 +148,10 @@
         public static notRoundedProperty: Prim2DPropInfo;
         public static notRoundedProperty: Prim2DPropInfo;
         public static roundRadiusProperty: Prim2DPropInfo;
         public static roundRadiusProperty: Prim2DPropInfo;
 
 
+        public get actualSize(): Size {
+            return this.size;
+        }
+
         @instanceLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 1, pi => Rectangle2D.sizeProperty = pi, false, true)
         @instanceLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 1, pi => Rectangle2D.sizeProperty = pi, false, true)
         public get size(): Size {
         public get size(): Size {
             return this._size;
             return this._size;
@@ -176,8 +180,19 @@
             this.notRounded = value === 0;
             this.notRounded = value === 0;
         }
         }
 
 
+        protected levelIntersect(intersectInfo: IntersectInfo2D): boolean {
+            // If we got there it mean the boundingInfo intersection succeed, if the rectangle has not roundRadius, it means it succeed!
+            if (this.notRounded) {
+                return true;
+            }
+
+            // Well, for now we neglect the area where the pickPosition could be outside due to the roundRadius...
+            // TODO make REAL intersection test here!
+            return true;
+        }
+
         protected updateLevelBoundingInfo() {
         protected updateLevelBoundingInfo() {
-            BoundingInfo2D.CreateFromSizeToRef(this.size, this._levelBoundingInfo);
+            BoundingInfo2D.CreateFromSizeToRef(this.size, this._levelBoundingInfo, this.origin);
         }
         }
 
 
         protected setupRectangle2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, size: Size, roundRadius = 0, fill?: IBrush2D, border?: IBrush2D, borderThickness: number = 1) {
         protected setupRectangle2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, size: Size, roundRadius = 0, fill?: IBrush2D, border?: IBrush2D, borderThickness: number = 1) {

+ 5 - 1
src/Canvas2d/babylon.sprite2d.ts

@@ -121,6 +121,10 @@
             this._texture = value;
             this._texture = value;
         }
         }
 
 
+        public get actualSize(): Size {
+            return this.spriteSize;
+        }
+
         @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, pi => Sprite2D.spriteSizeProperty = pi, false, true)
         @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, pi => Sprite2D.spriteSizeProperty = pi, false, true)
         public get spriteSize(): Size {
         public get spriteSize(): Size {
             return this._size;
             return this._size;
@@ -158,7 +162,7 @@
         }
         }
 
 
         protected updateLevelBoundingInfo() {
         protected updateLevelBoundingInfo() {
-            BoundingInfo2D.CreateFromSizeToRef(this.spriteSize, this._levelBoundingInfo);
+            BoundingInfo2D.CreateFromSizeToRef(this.spriteSize, this._levelBoundingInfo, this.origin);
         }
         }
 
 
         protected setupSprite2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, texture: Texture, spriteSize: Size, spriteLocation: Vector2, invertY: boolean) {
         protected setupSprite2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, texture: Texture, spriteSize: Size, spriteLocation: Vector2, invertY: boolean) {

+ 8 - 8
src/Canvas2d/babylon.text2d.ts

@@ -145,7 +145,7 @@
 
 
         public set text(value: string) {
         public set text(value: string) {
             this._text = value;
             this._text = value;
-            this._actualAreaSize = null;    // A change of text will reset the Actual Area Size which will be recomputed next time it's used
+            this._actualSize = null;    // A change of text will reset the Actual Area Size which will be recomputed next time it's used
             this._updateCharCount();
             this._updateCharCount();
         }
         }
 
 
@@ -176,18 +176,18 @@
             this._hAlign = value;
             this._hAlign = value;
         }
         }
 
 
-        public get actualAreaSize(): Size {
+        public get actualSize(): Size {
             if (this.areaSize) {
             if (this.areaSize) {
                 return this.areaSize;
                 return this.areaSize;
             }
             }
 
 
-            if (this._actualAreaSize) {
-                return this._actualAreaSize;
+            if (this._actualSize) {
+                return this._actualSize;
             }
             }
 
 
-            this._actualAreaSize = this.fontTexture.measureText(this._text, this._tabulationSize);
+            this._actualSize = this.fontTexture.measureText(this._text, this._tabulationSize);
 
 
-            return this._actualAreaSize;
+            return this._actualSize;
         }
         }
 
 
         protected get fontTexture(): FontTexture {
         protected get fontTexture(): FontTexture {
@@ -213,7 +213,7 @@
         }
         }
 
 
         protected updateLevelBoundingInfo() {
         protected updateLevelBoundingInfo() {
-            BoundingInfo2D.CreateFromSizeToRef(this.actualAreaSize, this._levelBoundingInfo);
+            BoundingInfo2D.CreateFromSizeToRef(this.actualSize, this._levelBoundingInfo, this.origin);
         }
         }
 
 
         protected setupText2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, fontName: string, text: string, areaSize: Size, defaultFontColor: Color4, vAlign, hAlign, tabulationSize: number) {
         protected setupText2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, fontName: string, text: string, areaSize: Size, defaultFontColor: Color4, vAlign, hAlign, tabulationSize: number) {
@@ -349,7 +349,7 @@
         private _defaultFontColor: Color4;
         private _defaultFontColor: Color4;
         private _text: string;
         private _text: string;
         private _areaSize: Size;
         private _areaSize: Size;
-        private _actualAreaSize: Size;
+        private _actualSize: Size;
         private _vAlign: number;
         private _vAlign: number;
         private _hAlign: number;
         private _hAlign: number;
     }
     }

+ 1 - 1
src/Shaders/sprite2d.vertex.fx

@@ -60,7 +60,7 @@ void main(void) {
 	}
 	}
 
 
 	vec4 pos;
 	vec4 pos;
-	pos.xy = (pos2.xy - origin) * sizeUV * textureSize;
+	pos.xy = (pos2.xy * sizeUV * textureSize) - origin;
 	pos.z = 1.0;
 	pos.z = 1.0;
 	pos.w = 1.0;
 	pos.w = 1.0;
 	gl_Position = vec4(dot(pos, transformX), dot(pos, transformY), zBias.x, zBias.y);
 	gl_Position = vec4(dot(pos, transformX), dot(pos, transformY), zBias.x, zBias.y);

+ 25 - 3
src/Tools/babylon.observable.ts

@@ -8,22 +8,44 @@
         /**
         /**
         * If the callback of a given Observer set skipNextObservers to true the following observers will be ignored
         * If the callback of a given Observer set skipNextObservers to true the following observers will be ignored
         */
         */
-        constructor(public skipNextObservers = false) {
+        constructor(mask: number, skipNextObservers = false) {
+            this.mask = mask;
+            this.skipNextObservers = skipNextObservers;
         }
         }
+
+        /**
+         * An Observer can set this property to true to prevent subsequent observers of being notified
+         */
+        public skipNextObservers: boolean;
+
+        /**
+         * Get the mask value that were used to trigger the event corresponding to this EventState object
+         */
+        public mask: number;
     }
     }
 
 
+    /**
+     * Represent an Observer registered to a given Observable object.
+     */
     export class Observer<T> {
     export class Observer<T> {
         constructor(public callback: (eventData: T, eventState: EventState) => void, public mask: number) {
         constructor(public callback: (eventData: T, eventState: EventState) => void, public mask: number) {
         }
         }
     }
     }
 
 
+    /**
+     * The Observable class is a simple implementation of the Observable pattern.
+     * There's one slight particularity though: a given Observable can notify its observer using a particular mask value, only the Observers registered with this mask value will be notified.
+     * This enable a more fine grained execution without having to rely on multiple different Observable objects.
+     * For instance you may have a given Observable that have four different types of notifications: Move (mask = 0x01), Stop (mask = 0x02), Turn Right (mask = 0X04), Turn Left (mask = 0X08).
+     * A given observer can register itself with only Move and Stop (mask = 0x03), then it will only be notified when one of these two occurs and will never be for Turn Left/Right.
+     */
     export class Observable<T> {
     export class Observable<T> {
         _observers = new Array<Observer<T>>();
         _observers = new Array<Observer<T>>();
 
 
         /**
         /**
          * Create a new Observer with the specified callback
          * Create a new Observer with the specified callback
          * @param callback the callback that will be executed for that Observer
          * @param callback the callback that will be executed for that Observer
-         * @param mash the mask used to filter observers
+         * @param mask the mask used to filter observers
          * @param insertFirst if true the callback will be inserted at the first position, hence executed before the others ones. If false (default behavior) the callback will be inserted at the last position, executed after all the others already present.
          * @param insertFirst if true the callback will be inserted at the first position, hence executed before the others ones. If false (default behavior) the callback will be inserted at the last position, executed after all the others already present.
          */
          */
         public add(callback: (eventData: T, eventState: EventState) => void, mask: number = -1, insertFirst = false): Observer<T> {
         public add(callback: (eventData: T, eventState: EventState) => void, mask: number = -1, insertFirst = false): Observer<T> {
@@ -81,7 +103,7 @@
          * @param mask
          * @param mask
          */
          */
         public notifyObservers(eventData: T, mask: number = -1): void {
         public notifyObservers(eventData: T, mask: number = -1): void {
-            var state = new EventState();
+            var state = new EventState(mask);
 
 
             for (var obs of this._observers) {
             for (var obs of this._observers) {
                 if (obs.mask & mask) {
                 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.
      * 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 {
     export class PointerInfo {
         constructor(public type: number, public event: PointerEvent | MouseWheelEvent, public pickInfo: PickingInfo) {
         constructor(public type: number, public event: PointerEvent | MouseWheelEvent, public pickInfo: PickingInfo) {
@@ -238,10 +252,20 @@
         public onPointerPick: (evt: PointerEvent, pickInfo: PickingInfo) => void;
         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
          * Observable event triggered each time an input event is received from the rendering canvas
          */
          */
         public onPointerObservable = new Observable<PointerInfo>();
         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
         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 _pointerX: number;
         private _pointerY: number;
         private _pointerY: number;
@@ -637,14 +661,25 @@
             };
             };
 
 
             this._onPointerMove = (evt: PointerEvent) => {
             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) {
                 if (!this.cameraToUseForPointers && !this.activeCamera) {
                     return;
                     return;
                 }
                 }
 
 
                 var canvas = this._engine.getRenderingCanvas();
                 var canvas = this._engine.getRenderingCanvas();
 
 
-                this._updatePointerPosition(evt);
-
                 if (!this.pointerMovePredicate) {
                 if (!this.pointerMovePredicate) {
                     this.pointerMovePredicate = (mesh: AbstractMesh): boolean => mesh.isPickable && mesh.isVisible && mesh.isReady() && (this.constantlyUpdateMeshUnderPointer || mesh.actionManager !== null && mesh.actionManager !== undefined);
                     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._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) {
                 if (!this.cameraToUseForPointers && !this.activeCamera) {
                     return;
                     return;
                 }
                 }
 
 
-                this._updatePointerPosition(evt);
                 this._startingPointerPosition.x = this._pointerX;
                 this._startingPointerPosition.x = this._pointerX;
                 this._startingPointerPosition.y = this._pointerY;
                 this._startingPointerPosition.y = this._pointerY;
                 this._startingPointerTime = new Date().getTime();
                 this._startingPointerTime = new Date().getTime();
@@ -782,12 +828,22 @@
             };
             };
 
 
             this._onPointerUp = (evt: PointerEvent) => {
             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) {
                 if (!this.cameraToUseForPointers && !this.activeCamera) {
                     return;
                     return;
                 }
                 }
 
 
-                this._updatePointerPosition(evt);
-
                 if (!this.pointerUpPredicate) {
                 if (!this.pointerUpPredicate) {
                     this.pointerUpPredicate = (mesh: AbstractMesh): boolean => {
                     this.pointerUpPredicate = (mesh: AbstractMesh): boolean => {
                         return mesh.isPickable && mesh.isVisible && mesh.isReady() && (!mesh.actionManager || (mesh.actionManager.hasPickTriggers || mesh.actionManager.hasSpecificTrigger(ActionManager.OnLongPressTrigger)));
                         return mesh.isPickable && mesh.isVisible && mesh.isReady() && (!mesh.actionManager || (mesh.actionManager.hasPickTriggers || mesh.actionManager.hasSpecificTrigger(ActionManager.OnLongPressTrigger)));