Browse Source

Merge pull request #1192 from nockawa/alignment

Canvas2D: ZE 1.0 (hopefully)
David Catuhe 9 years ago
parent
commit
b554696429

+ 1 - 1
Tools/Gulp/config.json

@@ -172,7 +172,7 @@
       "../../src/Canvas2d/babylon.text2d.js",
       "../../src/Canvas2d/babylon.lines2d.js",
       "../../src/Canvas2d/babylon.canvas2d.js",
-      "../../src/Canvas2d/babylon.worldSpaceCanvas2d.js",
+      "../../src/Canvas2d/babylon.worldSpaceCanvas2dNode.js",
       "../../src/Materials/babylon.shaderMaterial.js",
       "../../src/Tools/babylon.tools.dds.js",
       "../../src/Physics/Plugins/babylon.cannonJSPlugin.js",

+ 22 - 27
src/Canvas2d/babylon.bounding2d.ts

@@ -28,46 +28,45 @@
             this.extent = Vector2.Zero();
         }
 
-        public static CreateFromSize(size: Size, origin?: Vector2): BoundingInfo2D {
+        public static CreateFromSize(size: Size): BoundingInfo2D {
             let r = new BoundingInfo2D();
-            BoundingInfo2D.CreateFromSizeToRef(size, r, origin);
+            BoundingInfo2D.CreateFromSizeToRef(size, r);
             return r;
         }
 
-        public static CreateFromRadius(radius: number, origin?: Vector2): BoundingInfo2D {
+        public static CreateFromRadius(radius: number): BoundingInfo2D {
             let r = new BoundingInfo2D();
-            BoundingInfo2D.CreateFromRadiusToRef(radius, r, origin);
+            BoundingInfo2D.CreateFromRadiusToRef(radius, r);
             return r;
         }
 
-        public static CreateFromPoints(points: Vector2[], origin?: Vector2): BoundingInfo2D {
+        public static CreateFromPoints(points: Vector2[]): BoundingInfo2D {
             let r = new BoundingInfo2D();
-            BoundingInfo2D.CreateFromPointsToRef(points, r, origin);
+            BoundingInfo2D.CreateFromPointsToRef(points, r);
 
             return r;
         }
 
-        public static CreateFromSizeToRef(size: Size, b: BoundingInfo2D, origin?: Vector2) {
-            b.center = new Vector2(size.width / 2, size.height / 2);
-            b.extent = b.center.clone();
-            if (origin) {
-                b.center.x -= size.width * origin.x;
-                b.center.y -= size.height * origin.y;
+        public static CreateFromSizeToRef(size: Size, b: BoundingInfo2D) {
+            if (!size) {
+                size = Size.Zero();
             }
+            b.center.x = +size.width / 2;
+            b.center.y = +size.height / 2;
+            b.extent.x = b.center.x;
+            b.extent.y = b.center.y;
             b.radius = b.extent.length();
         }
 
-        public static CreateFromRadiusToRef(radius: number, b: BoundingInfo2D, origin?: Vector2) {
-            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.radius = radius;
+        public static CreateFromRadiusToRef(radius: number, b: BoundingInfo2D) {
+            b.center.x = b.center.y = 0;
+            let r = +radius;
+            b.extent.x = r;
+            b.extent.y = r;
+            b.radius = r;
         }
 
-        public static CreateFromPointsToRef(points: Vector2[], b: BoundingInfo2D, origin?: Vector2) {
+        public static CreateFromPointsToRef(points: Vector2[], b: BoundingInfo2D) {
             let xmin = Number.MAX_VALUE, ymin = Number.MAX_VALUE, xmax = Number.MIN_VALUE, ymax = Number.MIN_VALUE;
             for (let p of points) {
                 xmin = Math.min(p.x, xmin);
@@ -75,17 +74,13 @@
                 ymin = Math.min(p.y, ymin);
                 ymax = Math.max(p.y, ymax);
             }
-            BoundingInfo2D.CreateFromMinMaxToRef(xmin, xmax, ymin, ymax, b, origin);
+            BoundingInfo2D.CreateFromMinMaxToRef(xmin, xmax, ymin, ymax, b);
         }
 
-        public static CreateFromMinMaxToRef(xmin: number, xmax: number, ymin: number, ymax: number, b: BoundingInfo2D, origin?: Vector2) {
+        public static CreateFromMinMaxToRef(xmin: number, xmax: number, ymin: number, ymax: number, b: BoundingInfo2D) {
             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.radius = b.extent.length();
         }

+ 402 - 180
src/Canvas2d/babylon.canvas2d.ts

@@ -47,142 +47,87 @@
          */
         public static CACHESTRATEGY_DONTCACHE = 4;
 
-        /**
-         * Create a new 2D ScreenSpace Rendering Canvas, it is a 2D rectangle that has a size (width/height) and a position relative to the bottom/left corner of the screen.
-         * ScreenSpace Canvas will be drawn in the Viewport as a 2D Layer lying to the top of the 3D Scene. Typically used for traditional UI.
-         * All caching strategies will be available.
-         * PLEASE NOTE: the origin of a Screen Space Canvas is set to [0;0] (bottom/left) which is different than the default origin of a Primitive which is centered [0.5;0.5]
-         * @param scene the Scene that owns the Canvas
-         * Options:
-         *  - id: a text identifier, for information purpose only
-         *  - pos: the position of the canvas, relative from the bottom/left of the scene's viewport. Alternatively you can set the x and y properties directly. Default value is [0, 0]
-         *  - size: the Size of the canvas. Alternatively the width and height properties can be set. 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
-         *  - cachingStrategy: either CACHESTRATEGY_TOPLEVELGROUPS, CACHESTRATEGY_ALLGROUPS, CACHESTRATEGY_CANVAS, CACHESTRATEGY_DONTCACHE. Please refer to their respective documentation for more information. Default is Canvas2D.CACHESTRATEGY_DONTCACHE
-         *  - enableInteraction: if true the pointer events will be listened and rerouted to the appropriate primitives of the Canvas2D through the Prim2DBase.onPointerEventObservable observable property. Default is true.
-         *  - isVisible: true if the canvas must be visible, false for hidden. Default is true.
-         *  - marginTop/Left/Right/Bottom: define the margin for the corresponding edge, if all of them are null, margin is not used in layout computing. Default Value is null for each.
-         *  - hAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
-         *  - vAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
-         */
-        static CreateScreenSpace(scene: Scene, options: { id?: string, x?: number, y?: number, position?: Vector2, origin?: Vector2, width?: number, height?: number, size?: Size, cachingStrategy?: number, enableInteraction?: boolean, isVisible?: boolean, marginTop?: number, marginLeft?: number, marginRight?: number, marginBottom?: number, hAlignment?: number, vAlignment?: number }): Canvas2D {
-            let c = new Canvas2D();
-
-            if (!options) {
-                c.setupCanvas(scene, null, null, 1, true, Canvas2D.CACHESTRATEGY_DONTCACHE, true, Vector2.Zero(), true, null, null, null, null, null, null);
-                c.position = Vector2.Zero();
-            } else {
-                let pos = options.position || new Vector2(options.x || 0, options.y || 0);
-                let size = (!options.size && !options.width && !options.height) ? null : (options.size || (new Size(options.width || 0, options.height || 0)));
-
-                c.setupCanvas(scene, options.id || null, size, 1, true, options.cachingStrategy || Canvas2D.CACHESTRATEGY_DONTCACHE, options.enableInteraction || true, options.origin || Vector2.Zero(), options.isVisible || true, options.marginTop, options.marginLeft, options.marginRight, options.marginBottom, options.hAlignment || Prim2DBase.HAlignLeft, options.vAlignment || Prim2DBase.VAlignTop);
-                c.position = pos;
+        constructor(scene: Scene, settings?: {
+            id                        ?: string,
+            children                  ?: Array<Prim2DBase>,
+            size                      ?: Size,
+            renderScaleFactor         ?: number,
+            isScreenSpace             ?: boolean,
+            cachingStrategy           ?: number,
+            enableInteraction         ?: boolean,
+            origin                    ?: Vector2,
+            isVisible                 ?: boolean,
+            backgroundRoundRadius     ?: number,
+            backgroundFill            ?: IBrush2D | string,
+            backgroundBorder          ?: IBrush2D | string,
+            backgroundBorderThickNess ?: number,
+        }) {
+            super(settings);
+
+            Prim2DBase._isCanvasInit = false;
+
+            if (!settings) {
+                settings = {};
             }
 
-            return c;
-        }
+            let renderScaleFactor = (settings.renderScaleFactor == null) ? 1 : settings.renderScaleFactor;
+            if (this._cachingStrategy !== Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
+                this._background              = new Rectangle2D({ parent: this, id: "###CANVAS BACKGROUND###", size: settings.size }); //TODO CHECK when size is null
+                this._background.zOrder       = 1.0;
+                this._background.isPickable   = false;
+                this._background.origin       = Vector2.Zero();
+                this._background.levelVisible = false;
 
-        /**
-         * 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.
-         * @param scene the Scene that owns the Canvas
-         * @param size the dimension of the Canvas in World Space
-         * Options:
-         *  - id: a text identifier, for information purpose only, default is null.
-         *  - position the position of the Canvas in World Space, default is [0,0,0]
-         *  - rotation the rotation of the Canvas in World Space, default is Quaternion.Identity()
-         *  - renderScaleFactor A scale factor applied to create the rendering texture that will be mapped in the Scene Rectangle. If you set 2 for instance the texture will be twice large in width and height. A greater value will allow to achieve a better rendering quality. Default value is 1.
-         * BE AWARE that the Canvas true dimension will be size*renderScaleFactor, then all coordinates and size will have to be express regarding this size.
-         * TIPS: if you want a renderScaleFactor independent reference of frame, create a child Group2D in the Canvas with position 0,0 and size set to null, then set its scale property to the same amount than the renderScaleFactor, put all your primitive inside using coordinates regarding the size property you pick for the Canvas and you'll be fine.
-         * - sideOrientation: Unexpected behavior occur if the value is different from Mesh.DEFAULTSIDE right now, so please use this one, which is the default.
-         * - cachingStrategy Must be CACHESTRATEGY_CANVAS for now, which is the default.
-         *  - enableInteraction: if true the pointer events will be listened and rerouted to the appropriate primitives of the Canvas2D through the Prim2DBase.onPointerEventObservable observable property. Default is false (the opposite of ScreenSpace).
-         * - isVisible: true if the canvas must be visible, false for hidden. Default is true.
-         * - customWorldSpaceNode: if specified the Canvas will be rendered in this given Node. But it's the responsibility of the caller to set the "worldSpaceToNodeLocal" property to compute the hit of the mouse ray into the node (in world coordinate system) as well as rendering the cached bitmap in the node itself. The properties cachedRect and cachedTexture of Group2D will give you what you need to do that.
-         */
-        static CreateWorldSpace(scene: Scene, size: Size, options: { id?: string, position?: Vector3, rotation?: Quaternion, renderScaleFactor?: number, sideOrientation?: number, cachingStrategy?: number, enableInteraction?: boolean, isVisible?: boolean, customWorldSpaceNode?: Node }): Canvas2D {
+                if (settings.backgroundRoundRadius != null) {
+                    this.backgroundRoundRadius = settings.backgroundRoundRadius;
+                }
 
-            let cs = options && options.cachingStrategy || Canvas2D.CACHESTRATEGY_CANVAS;
+                if (settings.backgroundBorder != null) {
+                    this.backgroundBorder = <IBrush2D>settings.backgroundBorder;        // TOFIX
+                }
 
-            if (cs !== Canvas2D.CACHESTRATEGY_CANVAS) {
-                throw new Error("Right now only the CACHESTRATEGY_CANVAS cache Strategy is supported for WorldSpace Canvas. More will come soon!");
-            }
+                if (settings.backgroundBorderThickNess != null) {
+                    this.backgroundBorderThickness = settings.backgroundBorderThickNess;
+                }
 
-            //if (cachingStrategy === Canvas2D.CACHESTRATEGY_DONTCACHE) {
-            //    throw new Error("CACHESTRATEGY_DONTCACHE cache Strategy can't be used for WorldSpace Canvas");
-            //}
+                if (settings.backgroundFill != null) {
+                    this.backgroundFill = <IBrush2D>settings.backgroundFill;
+                }
 
-            let enableInteraction = options ? options.enableInteraction : true;
-            let createWorldSpaceNode = !options || (options.customWorldSpaceNode == null);
-            let isVisible = options ? options.isVisible || true : true;
-            let id = options ? options.id || null : null;
-            let rsf = options ? options.renderScaleFactor || 1 : 1;
+                this._background._patchHierarchy(this);
+            }
 
-            let c = new Canvas2D();
-            c.setupCanvas(scene, id, new Size(size.width, size.height), rsf, false, cs, enableInteraction, new Vector2(0.5, 0.5), isVisible, null, null, null, null, null, null);
+            let engine = scene.getEngine();
 
-            if (createWorldSpaceNode) {
-                let plane = new WorldSpaceCanvas2D(id, scene, c);
-                let vertexData = VertexData.CreatePlane({
-                    width: size.width,
-                    height: size.height,
-                    sideOrientation: options && options.sideOrientation || Mesh.DEFAULTSIDE
-                });
-                let mtl = new StandardMaterial(id + "_Material", scene);
+            this.__engineData                   = engine.getOrAddExternalDataWithFactory("__BJSCANVAS2D__", k => new Canvas2DEngineBoundData());
+            this._renderScaleFactor             = renderScaleFactor;
+            this._primPointerInfo               = new PrimitivePointerInfo();
+            this._capturedPointers              = new StringDictionary<Prim2DBase>();
+            this._pickStartingPosition          = Vector2.Zero();
+            this._hierarchyLevelMaxSiblingCount = 10;
+            this._hierarchyDepthOffset          = 0;
+            this._siblingDepthOffset            = 1 / this._hierarchyLevelMaxSiblingCount;
+            this._scene                         = scene;
+            this._engine                        = engine;
+            this._renderingSize                 = new Size(0, 0);
 
-                c.applyCachedTexture(vertexData, mtl);
-                vertexData.applyToMesh(plane, false);
+            this._patchHierarchy(this);
 
-                mtl.specularColor = new Color3(0, 0, 0);
-                mtl.disableLighting = true;
-                mtl.useAlphaFromDiffuseTexture = true;
-                plane.position = options && options.position || Vector3.Zero();
-                plane.rotationQuaternion = options && options.rotation || Quaternion.Identity();
-                plane.material = mtl;
-                c._worldSpaceNode = plane;
-            } else {
-                c._worldSpaceNode = options.customWorldSpaceNode;
-            }
-            return c;
-        }
+            let enableInteraction = (settings.enableInteraction == null) ? true : settings.enableInteraction;
 
-        protected setupCanvas(scene: Scene, name: string, size: Size, renderScaleFactor: number, isScreenSpace: boolean, cachingstrategy: number, enableInteraction: boolean, origin: Vector2, isVisible: boolean, marginTop: number, marginLeft: number, marginRight: number, marginBottom: number, hAlign: number, vAlign: number) {
-            let engine = scene.getEngine();
-            this._fitRenderingDevice = !size;
-            if (!size) {
-                size = new Size(engine.getRenderWidth(), engine.getRenderHeight());
+            this._fitRenderingDevice = !settings.size;
+            if (!settings.size) {
+                settings.size = new Size(engine.getRenderWidth(), engine.getRenderHeight());
             } else {
-                size.height *= renderScaleFactor;
-                size.width *= renderScaleFactor;
+                settings.size.height *= renderScaleFactor;
+                settings.size.width *= renderScaleFactor;
             }
-            this.__engineData = engine.getOrAddExternalDataWithFactory("__BJSCANVAS2D__", k => new Canvas2DEngineBoundData());
-            this._renderScaleFactor = renderScaleFactor;
-            this._cachingStrategy = cachingstrategy;
-            this._primPointerInfo = new PrimitivePointerInfo();
-            this._capturedPointers = new StringDictionary<Prim2DBase>();
-            this._pickStartingPosition = Vector2.Zero();
-
-            this.setupGroup2D(this, null, name, Vector2.Zero(), origin, size, isVisible, this._cachingStrategy === Canvas2D.CACHESTRATEGY_ALLGROUPS ? Group2D.GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE : Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY, marginTop, marginLeft, marginRight, marginBottom, hAlign, vAlign);
-
-            this._hierarchyLevelMaxSiblingCount = 10;
-            this._hierarchyDepthOffset = 0;
-            this._siblingDepthOffset = 1 / this._hierarchyLevelMaxSiblingCount;
-            this._scene = scene;
-            this._engine = engine;
-            this._renderingSize = new Size(0, 0);
 
             // Register scene dispose to also dispose the canvas when it'll happens
             scene.onDisposeObservable.add((d, s) => {
                 this.dispose();
             });
 
-            if (cachingstrategy !== Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
-                this._background = Rectangle2D.Create(this, { id: "###CANVAS BACKGROUND###", width: size.width, height: size.height });
-                this._background.isPickable = false;
-                this._background.origin = Vector2.Zero();
-                this._background.levelVisible = false;
-            }
-            this._isScreeSpace = isScreenSpace;
-
             if (this._isScreeSpace) {
                 this._afterRenderObserver = this._scene.onAfterRenderObservable.add((d, s) => {
                     this._engine.clear(null, false, true);
@@ -195,15 +140,19 @@
             }
 
             this._supprtInstancedArray = this._engine.getCaps().instancedArrays !== null;
-            //            this._supprtInstancedArray = false; // TODO REMOVE!!!
+//            this._supprtInstancedArray = false; // TODO REMOVE!!!
 
             this._setupInteraction(enableInteraction);
         }
 
-        public get hierarchyLevelMaxSiblingCount(): number {
-            return this._hierarchyLevelMaxSiblingCount;
+        protected _canvasPreInit(settings: any) {
+            let cachingStrategy   = (settings.cachingStrategy == null) ? Canvas2D.CACHESTRATEGY_DONTCACHE : settings.cachingStrategy;
+            this._cachingStrategy = cachingStrategy;
+            this._isScreeSpace = (settings.isScreenSpace == null) ? true : settings.isScreenSpace;
         }
 
+        public static hierarchyLevelMaxSiblingCount: number = 10;
+
         private _setupInteraction(enable: boolean) {
             // No change detection
             if (enable === this._interactionEnabled) {
@@ -229,7 +178,7 @@
 
                 // Register the observable
                 this._scenePrePointerObserver = this.scene.onPrePointerObservable.add((e, s) => {
-                    let hs = 1 / this.engine.getHardwareScalingLevel();
+                    let hs = 1/this.engine.getHardwareScalingLevel();
                     let localPos = e.localPosition.multiplyByFloats(hs, hs);
                     this._handlePointerEventForInteraction(e, localPos, s);
                 });
@@ -273,11 +222,11 @@
             mtx.invert();
             let v = Vector3.TransformCoordinates(worldPos, mtx);
             let rsf = this._renderScaleFactor;
-            let res = new Vector2(v.x * rsf, v.y * rsf);
+            let res = new Vector2(v.x*rsf, v.y*rsf);
             let size = this.actualSize;
             let o = this.origin;
-            res.x += size.width * o.x;
-            res.y += size.width * o.y;
+            res.x += size.width * 0.5;  // res is centered, make it relative to bottom/left
+            res.y += size.width * 0.5;
             return res;
         }
 
@@ -340,7 +289,7 @@
             }
             return this._capturedPointers.get(pointerId.toString());
         }
-
+           
         private static _interInfo = new IntersectInfo2D();
         private _handlePointerEventForInteraction(eventData: PointerInfoBase, localPosition: Vector2, eventState: EventState) {
             // Dispose check
@@ -355,7 +304,7 @@
 
             // 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);
+            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();
@@ -401,8 +350,8 @@
                 var x = localPosition.x - viewport.x;
                 var y = localPosition.y - viewport.y;
 
-                pii.canvasPointerPos.x = x - this.position.x;
-                pii.canvasPointerPos.y = engine.getRenderHeight() - y - this.position.y;
+                pii.canvasPointerPos.x = x - this.actualPosition.x;
+                pii.canvasPointerPos.y = engine.getRenderHeight() -y - this.actualPosition.y;
             } else {
                 pii.canvasPointerPos.x = localPosition.x;
                 pii.canvasPointerPos.y = localPosition.y;
@@ -417,20 +366,20 @@
                     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;
+                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;
             }
         }
 
@@ -439,6 +388,11 @@
                 return;
             }
 
+            // A little safe guard, it might happens than the event is triggered before the first render and nothing is computed, this simple check will make sure everything will be fine
+            if (!this._globalTransform) {
+                this.updateCachedStates(true);
+            }
+
             let ii = Canvas2D._interInfo;
             ii.pickPosition.x = mouseLocalPos.x;
             ii.pickPosition.y = mouseLocalPos.y;
@@ -447,18 +401,18 @@
             // 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;
+                this._actualIntersectionList   = null;
+                this._previousOverPrimitive    = this._actualOverPrimitive;
+                this._actualOverPrimitive      = null;
                 return;
             }
 
             this.intersect(ii);
 
             this._previousIntersectionList = this._actualIntersectionList;
-            this._actualIntersectionList = ii.intersectedPrimitives;
-            this._previousOverPrimitive = this._actualOverPrimitive;
-            this._actualOverPrimitive = ii.topMostIntersectedPrimitive;
+            this._actualIntersectionList   = ii.intersectedPrimitives;
+            this._previousOverPrimitive    = this._actualOverPrimitive;
+            this._actualOverPrimitive      = ii.topMostIntersectedPrimitive;
 
             this._intersectionRenderId = this.scene.getRenderId();
         }
@@ -471,14 +425,14 @@
 
             // Detect a change of over
             let prevPrim = this._previousOverPrimitive ? this._previousOverPrimitive.prim : null;
-            let actualPrim = this._actualOverPrimitive ? this._actualOverPrimitive.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)) {
+                if ((capturedPrim && capturedPrim===prevPrim) || (!capturedPrim && prevPrim)) {
                     this._primPointerInfo.updateRelatedTarget(prevPrim, this._previousOverPrimitive.intersectionLocation);
                     this._bubbleNotifyPrimPointerObserver(prevPrim, PrimitivePointerInfo.PointerOut, null);
                 }
@@ -525,9 +479,9 @@
         private _bubbleNotifyPrimPointerObserver(prim: Prim2DBase, mask: number, eventData: any) {
             let ppi = this._primPointerInfo;
 
-            // In case of PointerOver/Out we will first notify the children (but the deepest to the closest) with PointerEnter/Leave
+            // In case of PointerOver/Out we will first notify the parent with PointerEnter/Leave
             if ((mask & (PrimitivePointerInfo.PointerOver | PrimitivePointerInfo.PointerOut)) !== 0) {
-                this._notifChildren(prim, mask);
+                this._notifParents(prim, mask);
             }
 
             let bubbleCancelled = false;
@@ -578,6 +532,11 @@
 
         private _triggerActionManager(prim: Prim2DBase, ppi: PrimitivePointerInfo, mask: number, eventData) {
 
+            // A little safe guard, it might happens than the event is triggered before the first render and nothing is computed, this simple check will make sure everything will be fine
+            if (!this._globalTransform) {
+                this.updateCachedStates(true);
+            }
+
             // Process Trigger related to PointerDown
             if ((mask & PrimitivePointerInfo.PointerDown) !== 0) {
                 // On pointer down, record the current position and time to be able to trick PickTrigger and LongPressTrigger
@@ -591,15 +550,15 @@
                         let actionEvent = ActionEvent.CreateNewFromPrimitive(prim, ppi.primitivePointerPos, eventData);
 
                         switch (eventData.button) {
-                            case 0:
-                                prim.actionManager.processTrigger(ActionManager.OnLeftPickTrigger, actionEvent);
-                                break;
-                            case 1:
-                                prim.actionManager.processTrigger(ActionManager.OnCenterPickTrigger, actionEvent);
-                                break;
-                            case 2:
-                                prim.actionManager.processTrigger(ActionManager.OnRightPickTrigger, actionEvent);
-                                break;
+                        case 0:
+                            prim.actionManager.processTrigger(ActionManager.OnLeftPickTrigger, actionEvent);
+                            break;
+                        case 1:
+                            prim.actionManager.processTrigger(ActionManager.OnCenterPickTrigger, actionEvent);
+                            break;
+                        case 2:
+                            prim.actionManager.processTrigger(ActionManager.OnRightPickTrigger, actionEvent);
+                            break;
                         }
                         prim.actionManager.processTrigger(ActionManager.OnPickDownTrigger, actionEvent);
                     }
@@ -664,27 +623,27 @@
             }
         }
 
-        _notifChildren(prim: Prim2DBase, mask: number) {
+        _notifParents(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);
+            let curPrim: Prim2DBase = this;
 
-                this._updatePrimPointerPos(curChild);
+            while (curPrim) {
+                this._updatePrimPointerPos(curPrim);
 
                 // Fire the proper notification
                 if (mask === PrimitivePointerInfo.PointerOver) {
-                    this._debugExecObserver(curChild, PrimitivePointerInfo.PointerEnter);
-                    curChild._pointerEventObservable.notifyObservers(pii, PrimitivePointerInfo.PointerEnter);
+                    this._debugExecObserver(curPrim, PrimitivePointerInfo.PointerEnter);
+                    curPrim._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);
+                    this._debugExecObserver(curPrim, PrimitivePointerInfo.PointerLeave);
+                    curPrim._pointerEventObservable.notifyObservers(pii, PrimitivePointerInfo.PointerLeave);
                 }
-            });
+                curPrim = curPrim.parent;
+            }
         }
 
         /**
@@ -802,6 +761,27 @@
         }
 
         /**
+         * Property that defines the thickness of the border object used to draw the background of the Canvas.
+         * @returns If the background is not set, null will be returned, otherwise a valid number matching the thickness is returned.
+         */
+        public get backgroundBorderThickness(): number {
+            if (!this._background || !this._background.isVisible) {
+                return null;
+            }
+            return this._background.borderThickness;
+        }
+
+        public set backgroundBorderThickness(value: number) {
+            this.checkBackgroundAvailability();
+
+            if (value === this._background.borderThickness) {
+                return;
+            }
+
+            this._background.borderThickness = value;
+        }
+
+        /**
          * You can set the roundRadius of the background
          * @returns The current roundRadius
          */
@@ -863,7 +843,7 @@
         private _capturedPointers: StringDictionary<Prim2DBase>;
         private _scenePrePointerObserver: Observer<PointerInfoPre>;
         private _scenePointerObserver: Observer<PointerInfo>;
-        private _worldSpaceNode: Node;
+        protected _worldSpaceNode: Node;
         private _mapCounter = 0;
         private _background: Rectangle2D;
         private _scene: Scene;
@@ -876,7 +856,7 @@
         private _groupCacheMaps: MapTexture[];
         private _beforeRenderObserver: Observer<Scene>;
         private _afterRenderObserver: Observer<Scene>;
-        private _supprtInstancedArray: boolean;
+        private _supprtInstancedArray : boolean;
 
         public _renderingSize: Size;
 
@@ -890,20 +870,36 @@
                 return;
             }
 
-            this._renderingSize.width = this.engine.getRenderWidth();
-            this._renderingSize.height = this.engine.getRenderHeight();
+            // Detect a change of rendering size
+            let renderingSizeChanged = false;
+            let newWidth = this.engine.getRenderWidth();
+            if (newWidth !== this._renderingSize.width) {
+                renderingSizeChanged = true;
+            }
+            this._renderingSize.width = newWidth;
+
+            let newHeight = this.engine.getRenderHeight();
+            if (newHeight !== this._renderingSize.height) {
+                renderingSizeChanged = true;
+            }
+            this._renderingSize.height = newHeight;
+
 
-            if (this._fitRenderingDevice) {
+            // If the canvas fit the rendering size and it changed, update
+            if (renderingSizeChanged && this._fitRenderingDevice) {
                 this.size = this._renderingSize;
                 if (this._background) {
                     this._background.size = this.size;
                 }
+
+                // Dirty the Layout at the Canvas level to recompute as the size changed
+                this._setLayoutDirty();
             }
 
             var context = new PrepareRender2DContext();
 
             ++this._globalTransformProcessStep;
-            this.updateGlobalTransVis(false);
+            this.updateCachedStates(false);
 
             this._prepareGroupRender(context);
 
@@ -921,7 +917,8 @@
                 this._updateIntersectionList(this._primPointerInfo.canvasPointerPos, false);
                 this._updateOverStatus();   // TODO this._primPointerInfo may not be up to date!
             }
-            
+
+            this.engine.setState(false);
             this._groupRender();
 
             // If the canvas is cached at canvas level, we must manually render the sprite that will display its content
@@ -941,7 +938,7 @@
             let size = group.actualSize;
             size = new Size(Math.ceil(size.width), Math.ceil(size.height));
             if (minSize) {
-                size.width = Math.max(minSize.width, size.width);
+                size.width  = Math.max(minSize.width, size.width);
                 size.height = Math.max(minSize.height, size.height);
             }
 
@@ -985,14 +982,14 @@
                 // Special case if the canvas is entirely cached: create a group that will have a single sprite it will be rendered specifically at the very end of the rendering process
                 if (this._cachingStrategy === Canvas2D.CACHESTRATEGY_CANVAS) {
                     this._cachedCanvasGroup = Group2D._createCachedCanvasGroup(this);
-                    let sprite = Sprite2D.Create(this._cachedCanvasGroup, map, { id: "__cachedCanvasSprite__", spriteSize: node.contentSize, spriteLocation: node.pos });
+                    let sprite = new Sprite2D(map, { parent: this._cachedCanvasGroup, id: "__cachedCanvasSprite__", spriteSize:node.contentSize, spriteLocation:node.pos});
                     sprite.zOrder = 1;
                     sprite.origin = Vector2.Zero();
                 }
 
                 // 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 {
-                    let sprite = Sprite2D.Create(parent, map, { id: `__cachedSpriteOfGroup__${group.id}`, x: group.position.x, y: group.position.y, spriteSize: node.contentSize, spriteLocation: node.pos });
+                    let sprite = new Sprite2D(map, { parent: parent, id:`__cachedSpriteOfGroup__${group.id}`, x: group.actualPosition.x, y: group.actualPosition.y, spriteSize:node.contentSize, spriteLocation:node.pos});
                     sprite.origin = group.origin.clone();
                     res.sprite = sprite;
                 }
@@ -1028,7 +1025,232 @@
             return Canvas2D._gradientColorBrushes.getOrAddWithFactory(GradientColorBrush2D.BuildKey(color1, color2, translation, rotation, scale), () => new GradientColorBrush2D(color1, color2, translation, rotation, scale, true));
         }
 
+        public static GetBrushFromString(brushString: string): IBrush2D {
+            // Note: yes, I hate/don't know RegEx.. Feel free to add your contribution to the cause!
+
+            brushString = brushString.trim();
+            let split = brushString.split(",");
+
+            // Solid, formatted as: "[solid:]#FF808080"
+            if (split.length === 1) {
+                let value: string = null;
+                if (brushString.indexOf("solid:") === 0) {
+                    value = brushString.substr(6).trim();
+                } else if (brushString.indexOf("#") === 0) {
+                    value = brushString;
+                } else {
+                    return null;
+                }
+                return Canvas2D.GetSolidColorBrushFromHex(value);
+            }
+
+            // Gradient, formatted as: "[gradient:]#FF808080, #FFFFFFF[, [10:20], 180, 1]" [10:20] is a real formatting expected not a EBNF notation
+            // Order is: gradient start, gradient end, translation, rotation (degree), scale
+            else {
+                if (split[0].indexOf("gradient:") === 0) {
+                    split[0] = split[0].substr(9).trim();
+                }
+
+                try {
+                    let start = Color4.FromHexString(split[0].trim());
+                    let end = Color4.FromHexString(split[1].trim());
+
+                    let t: Vector2 = Vector2.Zero();
+                    if (split.length > 2) {
+                        let v = split[2].trim();
+                        if (v.charAt(0) !== "[" || v.charAt(v.length - 1) !== "]") {
+                            return null;
+                        }
+                        let sep = v.indexOf(":");
+                        let x = parseFloat(v.substr(1, sep));
+                        let y = parseFloat(v.substr(sep + 1, v.length - (sep + 1)));
+                        t = new Vector2(x, y);
+                    }
+
+                    let r: number = 0;
+                    if (split.length > 3) {
+                        r = Tools.ToRadians(parseFloat(split[3].trim()));
+                    }
+
+                    let s: number = 1;
+                    if (split.length > 4) {
+                        s = parseFloat(split[4].trim());
+                    }
+
+                    return Canvas2D.GetGradientColorBrush(start, end, t, r, s);
+                } catch (e) {
+                    return null;
+                } 
+            }
+        }
+
         private static _solidColorBrushes: StringDictionary<IBrush2D> = new StringDictionary<IBrush2D>();
         private static _gradientColorBrushes: StringDictionary<IBrush2D> = new StringDictionary<IBrush2D>();
     }
+    @className("WorldSpaceCanvas2D")
+    export class WorldSpaceCanvas2D extends Canvas2D {
+        /**
+         * 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.
+         * @param scene the Scene that owns the Canvas
+         * @param size the dimension of the Canvas in World Space
+         * Options:
+         *  - id: a text identifier, for information purpose only, default is null.
+         *  - position the position of the Canvas in World Space, default is [0,0,0]
+         *  - rotation the rotation of the Canvas in World Space, default is Quaternion.Identity()
+         *  - renderScaleFactor A scale factor applied to create the rendering texture that will be mapped in the Scene Rectangle. If you set 2 for instance the texture will be twice large in width and height. A greater value will allow to achieve a better rendering quality. Default value is 1.
+         * BE AWARE that the Canvas true dimension will be size*renderScaleFactor, then all coordinates and size will have to be express regarding this size.
+         * TIPS: if you want a renderScaleFactor independent reference of frame, create a child Group2D in the Canvas with position 0,0 and size set to null, then set its scale property to the same amount than the renderScaleFactor, put all your primitive inside using coordinates regarding the size property you pick for the Canvas and you'll be fine.
+         * - sideOrientation: Unexpected behavior occur if the value is different from Mesh.DEFAULTSIDE right now, so please use this one, which is the default.
+         * - cachingStrategy Must be CACHESTRATEGY_CANVAS for now, which is the default.
+         *  - enableInteraction: if true the pointer events will be listened and rerouted to the appropriate primitives of the Canvas2D through the Prim2DBase.onPointerEventObservable observable property. Default is false (the opposite of ScreenSpace).
+         * - isVisible: true if the canvas must be visible, false for hidden. Default is true.
+         * - customWorldSpaceNode: if specified the Canvas will be rendered in this given Node. But it's the responsibility of the caller to set the "worldSpaceToNodeLocal" property to compute the hit of the mouse ray into the node (in world coordinate system) as well as rendering the cached bitmap in the node itself. The properties cachedRect and cachedTexture of Group2D will give you what you need to do that.
+         */
+        constructor(scene: Scene, size: Size, settings?: {
+
+            children                 ?: Array<Prim2DBase>,
+            id                       ?: string,
+            worldPosition            ?: Vector3,
+            worldRotation            ?: Quaternion,
+            renderScaleFactor        ?: number,
+            sideOrientation          ?: number,
+            cachingStrategy          ?: number,
+            enableInteraction        ?: boolean,
+            isVisible                ?: boolean,
+            backgroundRoundRadius    ?: number,
+            backgroundFill           ?: IBrush2D | string,
+            backgroundBorder         ?: IBrush2D | string,
+            backgroundBorderThickNess?: number,
+            customWorldSpaceNode     ?: Node,
+            marginTop                ?: number | string,
+            marginLeft               ?: number | string,
+            marginRight              ?: number | string,
+            marginBottom             ?: number | string,
+            margin                   ?: number | string,
+            marginHAlignment         ?: number,
+            marginVAlignment         ?: number,
+            marginAlignment          ?: string,
+
+        }) {
+            Prim2DBase._isCanvasInit = true;
+            let s = <any>settings;
+            s.isScreenSpace = false;
+            s.size = size.clone();
+            settings.cachingStrategy = (settings.cachingStrategy==null) ? Canvas2D.CACHESTRATEGY_CANVAS : settings.cachingStrategy;
+
+            if (settings.cachingStrategy !== Canvas2D.CACHESTRATEGY_CANVAS) {
+                throw new Error("Right now only the CACHESTRATEGY_CANVAS cache Strategy is supported for WorldSpace Canvas. More will come soon!");
+            }
+
+            super(scene, settings);
+            Prim2DBase._isCanvasInit = false;
+
+
+            //if (cachingStrategy === Canvas2D.CACHESTRATEGY_DONTCACHE) {
+            //    throw new Error("CACHESTRATEGY_DONTCACHE cache Strategy can't be used for WorldSpace Canvas");
+            //}
+
+            //let enableInteraction = settings ? settings.enableInteraction : true;
+            let createWorldSpaceNode = !settings || (settings.customWorldSpaceNode == null);
+            //let isVisible = settings ? settings.isVisible || true : true;
+            let id = settings ? settings.id || null : null;
+            //let rsf = settings ? settings.renderScaleFactor || 1 : 1;
+
+            //let c = new Canvas2D();
+            //c.setupCanvas(scene, id, new Size(size.width, size.height), rsf, false, cs, enableInteraction, new Vector2(0.5, 0.5), isVisible, null, null, null, null, null, null);
+
+            if (createWorldSpaceNode) {
+                let plane = new WorldSpaceCanvas2DNode(id, scene, this);
+                let vertexData = VertexData.CreatePlane({
+                    width: size.width,
+                    height: size.height,
+                    sideOrientation: settings && settings.sideOrientation || Mesh.DEFAULTSIDE
+                });
+                let mtl = new StandardMaterial(id + "_Material", scene);
+
+                this.applyCachedTexture(vertexData, mtl);
+                vertexData.applyToMesh(plane, false);
+
+                mtl.specularColor = new Color3(0, 0, 0);
+                mtl.disableLighting = true;
+                mtl.useAlphaFromDiffuseTexture = true;
+                plane.position = settings && settings.worldPosition || Vector3.Zero();
+                plane.rotationQuaternion = settings && settings.worldRotation || Quaternion.Identity();
+                plane.material = mtl;
+                this._worldSpaceNode = plane;
+            } else {
+                this._worldSpaceNode = settings.customWorldSpaceNode;
+            }
+            //            return c;
+        }
+    }
+
+    @className("ScreenSpaceCanvas2D")
+    export class ScreenSpaceCanvas2D extends Canvas2D {
+        /**
+         * Create a new 2D ScreenSpace Rendering Canvas, it is a 2D rectangle that has a size (width/height) and a position relative to the bottom/left corner of the screen.
+         * ScreenSpace Canvas will be drawn in the Viewport as a 2D Layer lying to the top of the 3D Scene. Typically used for traditional UI.
+         * All caching strategies will be available.
+         * PLEASE NOTE: the origin of a Screen Space Canvas is set to [0;0] (bottom/left) which is different than the default origin of a Primitive which is centered [0.5;0.5]
+         * @param scene the Scene that owns the Canvas
+         * Options:
+         *  - id: a text identifier, for information purpose only
+         *  - pos: the position of the canvas, relative from the bottom/left of the scene's viewport. Alternatively you can set the x and y properties directly. Default value is [0, 0]
+         *  - size: the Size of the canvas. Alternatively the width and height properties can be set. 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
+         *  - cachingStrategy: either CACHESTRATEGY_TOPLEVELGROUPS, CACHESTRATEGY_ALLGROUPS, CACHESTRATEGY_CANVAS, CACHESTRATEGY_DONTCACHE. Please refer to their respective documentation for more information. Default is Canvas2D.CACHESTRATEGY_DONTCACHE
+         *  - enableInteraction: if true the pointer events will be listened and rerouted to the appropriate primitives of the Canvas2D through the Prim2DBase.onPointerEventObservable observable property. Default is true.
+         *  - isVisible: true if the canvas must be visible, false for hidden. Default is true.
+         *  - marginTop/Left/Right/Bottom: define the margin for the corresponding edge, if all of them are null, margin is not used in layout computing. Default Value is null for each.
+         *  - hAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
+         *  - vAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
+         */
+        constructor(scene: Scene, settings?: {
+
+            children                  ?: Array<Prim2DBase>,
+            id                        ?: string,
+            x                         ?: number,
+            y                         ?: number,
+            position                  ?: Vector2,
+            origin                    ?: Vector2,
+            width                     ?: number,
+            height                    ?: number,
+            size                      ?: Size,
+            cachingStrategy           ?: number,
+            enableInteraction         ?: boolean,
+            isVisible                 ?: boolean,
+            backgroundRoundRadius     ?: number,
+            backgroundFill            ?: IBrush2D | string,
+            backgroundBorder          ?: IBrush2D | string,
+            backgroundBorderThickNess ?: number,
+            marginTop                 ?: number | string,
+            marginLeft                ?: number | string,
+            marginRight               ?: number | string,
+            marginBottom              ?: number | string,
+            margin                    ?: string,
+            marginHAlignment          ?: number,
+            marginVAlignment          ?: number,
+            marginAlignment           ?: string,
+
+        }) {
+            Prim2DBase._isCanvasInit = true;
+            super(scene, settings);
+
+            //let c = new Canvas2D();
+
+            //if (!settings) {
+            //    c.setupCanvas(scene, null, null, 1, true, Canvas2D.CACHESTRATEGY_DONTCACHE, true, Vector2.Zero(), true, null, null, null, null, null, null);
+            //    c.position = Vector2.Zero();
+            //} else {
+            //    let pos = settings.position || new Vector2(settings.x || 0, settings.y || 0);
+            //    let size = (!settings.size && !settings.width && !settings.height) ? null : (settings.size || (new Size(settings.width || 0, settings.height || 0)));
+
+            //    c.setupCanvas(scene, settings.id || null, size, 1, true, settings.cachingStrategy || Canvas2D.CACHESTRATEGY_DONTCACHE, settings.enableInteraction || true, settings.origin || Vector2.Zero(), settings.isVisible || true, settings.marginTop, settings.marginLeft, settings.marginRight, settings.marginBottom, settings.hAlignment || PrimitiveAlignment.AlignLeft, settings.vAlignment || PrimitiveAlignment.AlignTop);
+            //    c.position = pos;
+            //}
+
+            //return c;
+        }
+    }
+
 }

+ 162 - 0
src/Canvas2d/babylon.canvas2dLayoutEngine.ts

@@ -0,0 +1,162 @@
+module BABYLON {
+
+    @className("LayoutEngineBase")
+    /**
+     * This is the base class you have to extend in order to implement your own Layout Engine.
+     * Note that for performance reason, each different Layout Engine type must be instanced only once, good practice is through a static `Singleton`property defined in the type itself.
+     * If data has to be associated to a given primitive you can use the SmartPropertyPrim.addExternalData API to do it.
+     */
+    export class LayoutEngineBase implements ILockable {
+        constructor() {
+            this.layoutDirtyOnPropertyChangedMask = 0;
+        }
+
+        public updateLayout(prim: Prim2DBase) {
+        }
+
+        public get isChildPositionAllowed(): boolean {
+            return false;
+        }
+
+        isLocked(): boolean {
+            return this._isLocked;
+        }
+
+        lock(): boolean {
+            if (this._isLocked) {
+                return false;
+            }
+            this._isLocked = true;
+            return true;
+        }
+
+        public layoutDirtyOnPropertyChangedMask;
+
+        private _isLocked: boolean;
+    }
+
+    @className("CanvasLayoutEngine")
+    export class CanvasLayoutEngine extends LayoutEngineBase {
+        public static Singleton: CanvasLayoutEngine = new CanvasLayoutEngine();
+
+        // A very simple (no) layout computing...
+        // The Canvas and its direct children gets the Canvas' size as Layout Area
+        // Indirect children have their Layout Area to the actualSize (margin area) of their parent
+        public updateLayout(prim: Prim2DBase) {
+
+            // If this prim is layoutDiry we update  its layoutArea and also the one of its direct children
+            if (prim._isFlagSet(SmartPropertyPrim.flagLayoutDirty)) {
+
+                for (let child of prim.children) {
+                    this._doUpdate(child);
+                }
+                prim._clearFlags(SmartPropertyPrim.flagLayoutDirty);
+            }
+
+        }
+
+        private _doUpdate(prim: Prim2DBase) {
+            // Canvas ?
+            if (prim instanceof Canvas2D) {
+                prim.layoutArea = prim.actualSize;
+            }
+
+            // Direct child of Canvas ?
+            else if (prim.parent instanceof Canvas2D) {
+                prim.layoutArea = prim.owner.actualSize;
+            }
+
+            // Indirect child of Canvas
+            else {
+                prim.layoutArea = prim.parent.contentArea;
+            }
+        }
+
+        get isChildPositionAllowed(): boolean {
+            return true;
+        }
+    }
+
+
+    @className("StackPanelLayoutEngine")
+    export class StackPanelLayoutEngine extends LayoutEngineBase {
+        constructor() {
+            super();
+            this.layoutDirtyOnPropertyChangedMask = Prim2DBase.sizeProperty.flagId;
+        }
+
+        public static get Horizontal(): StackPanelLayoutEngine {
+            if (!StackPanelLayoutEngine._horizontal) {
+                StackPanelLayoutEngine._horizontal = new StackPanelLayoutEngine();
+                StackPanelLayoutEngine._horizontal.isHorizontal = true;
+                StackPanelLayoutEngine._horizontal.lock();
+            }
+
+            return StackPanelLayoutEngine._horizontal;
+        }
+
+        public static get Vertical(): StackPanelLayoutEngine {
+            if (!StackPanelLayoutEngine._vertical) {
+                StackPanelLayoutEngine._vertical = new StackPanelLayoutEngine();
+                StackPanelLayoutEngine._vertical.isHorizontal = false;
+                StackPanelLayoutEngine._vertical.lock();
+            }
+
+            return StackPanelLayoutEngine._vertical;
+        }
+        private static _horizontal: StackPanelLayoutEngine = null;
+        private static _vertical: StackPanelLayoutEngine = null;
+
+
+        get isHorizontal(): boolean {
+            return this._isHorizontal;
+        }
+
+        set isHorizontal(val: boolean) {
+            if (this.isLocked()) {
+                return;
+            }
+            this._isHorizontal = val;
+        }
+
+        private _isHorizontal: boolean = true;
+
+        public updateLayout(prim: Prim2DBase) {
+            if (prim._isFlagSet(SmartPropertyPrim.flagLayoutDirty)) {
+
+                let x = 0;
+                let y = 0;
+                let h = this.isHorizontal;
+                let max = 0;
+
+                for (let child of prim.children) {
+                    let layoutArea = child.layoutArea;
+                    child.margin.computeArea(child.actualSize, layoutArea);
+
+                    max = Math.max(max, h ? layoutArea.height : layoutArea.width);
+
+                }
+
+                for (let child of prim.children) {
+                    child.layoutAreaPos = new Vector2(x, y);
+
+                    let layoutArea = child.layoutArea;
+
+                    if (h) {
+                        x += layoutArea.width;
+                        child.layoutArea = new Size(layoutArea.width, max);
+                    } else {
+                        y += layoutArea.height;
+                        child.layoutArea = new Size(max, layoutArea.height);
+                    }
+                }
+                prim._clearFlags(SmartPropertyPrim.flagLayoutDirty);
+            }
+
+        }
+
+        get isChildPositionAllowed(): boolean {
+            return false;
+        }
+    }
+}

+ 62 - 57
src/Canvas2d/babylon.ellipse2d.ts

@@ -166,20 +166,19 @@
     @className("Ellipse2D")
     export class Ellipse2D extends Shape2D {
 
-        public static sizeProperty: Prim2DPropInfo;
+        public static acutalSizeProperty: Prim2DPropInfo;
         public static subdivisionsProperty: Prim2DPropInfo;
 
+        @instanceLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 1, pi => Ellipse2D.acutalSizeProperty = pi, false, true)
         public get actualSize(): Size {
+            if (this._actualSize) {
+                return this._actualSize;
+            }
             return this.size;
         }
 
-        @instanceLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 1, pi => Ellipse2D.sizeProperty = pi, false, true)
-        public get size(): Size {
-            return this._size;
-        }
-
-        public set size(value: Size) {
-            this._size = value;
+        public set actualSize(value: Size) {
+            this._actualSize = value;
         }
 
         @modelLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 2, pi => Ellipse2D.subdivisionsProperty = pi)
@@ -192,21 +191,15 @@
         }
 
         protected levelIntersect(intersectInfo: IntersectInfo2D): boolean {
-            let x = intersectInfo._localPickPosition.x;
-            let y = intersectInfo._localPickPosition.y;
-            let w = this.size.width/2;
-            let h = this.size.height/2;
+            let w = this.size.width / 2;
+            let h = this.size.height / 2;
+            let x = intersectInfo._localPickPosition.x-w;
+            let y = intersectInfo._localPickPosition.y-h;
             return ((x * x) / (w * w) + (y * y) / (h * h)) <= 1;
         }
 
         protected updateLevelBoundingInfo() {
-            BoundingInfo2D.CreateFromSizeToRef(this.size, this._levelBoundingInfo, this.origin);
-        }
-
-        protected setupEllipse2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, origin: Vector2, size: Size, subdivisions: number, fill: IBrush2D, border: IBrush2D, borderThickness: number, isVisible: boolean, marginTop: number, marginLeft: number, marginRight: number, marginBottom: number, vAlignment: number, hAlignment: number) {
-            this.setupShape2D(owner, parent, id, position, origin, isVisible, fill, border, borderThickness, marginTop, marginLeft, marginRight, marginBottom, hAlignment, vAlignment);
-            this.size = size;
-            this.subdivisions = subdivisions;
+            BoundingInfo2D.CreateFromSizeToRef(this.actualSize, this._levelBoundingInfo);
         }
 
         /**
@@ -226,45 +219,58 @@
          *  - hAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
          *  - vAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
          */
-        public static Create(parent: Prim2DBase, options: { id?: string, position?: Vector2, x?: number, y?: number, origin?: Vector2, size?: Size, width?: number, height?: number, subdivisions?: number, fill?: IBrush2D, border?: IBrush2D, borderThickness?: number, isVisible?: boolean, marginTop?: number, marginLeft?: number, marginRight?: number, marginBottom?: number, vAlignment?: number, hAlignment?: number}): Ellipse2D {
-            Prim2DBase.CheckParent(parent);
+        constructor(settings?: {
+
+            parent            ?: Prim2DBase, 
+            children          ?: Array<Prim2DBase>,
+            id                ?: string,
+            position          ?: Vector2,
+            x                 ?: number,
+            y                 ?: number,
+            origin            ?: Vector2,
+            size              ?: Size,
+            width             ?: number,
+            height            ?: number,
+            subdivisions      ?: number,
+            fill              ?: IBrush2D | string,
+            border            ?: IBrush2D | string,
+            borderThickness   ?: number,
+            isVisible         ?: boolean,
+            marginTop         ?: number | string,
+            marginLeft        ?: number | string,
+            marginRight       ?: number | string,
+            marginBottom      ?: number | string,
+            margin            ?: number | string,
+            marginHAlignment  ?: number,
+            marginVAlignment  ?: number,
+            marginAlignment   ?: string,
+            paddingTop        ?: number | string,
+            paddingLeft       ?: number | string,
+            paddingRight      ?: number | string,
+            paddingBottom     ?: number | string,
+            padding           ?: string,
+            paddingHAlignment ?: number,
+            paddingVAlignment ?: number,
+
+        }) {
+
+            // Avoid checking every time if the object exists
+            if (settings == null) {
+                settings = {};
+            }
 
-            let ellipse = new Ellipse2D();
+            super(settings);
 
-            if (!options) {
-                ellipse.setupEllipse2D(parent.owner, parent, null, Vector2.Zero(), null, new Size(10, 10), 64, Canvas2D.GetSolidColorBrushFromHex("#FFFFFFFF"), null, 1, true, null, null, null, null, null, null);
-            } else {
-                let fill: IBrush2D;
-                if (options.fill === undefined) {
-                    fill = Canvas2D.GetSolidColorBrushFromHex("#FFFFFFFF");
-                } else {
-                    fill = options.fill;
-                }
-                let pos = options.position || new Vector2(options.x || 0, options.y || 0);
-                let size = options.size || (new Size(options.width || 10, options.height || 10));
-
-                ellipse.setupEllipse2D
-                (
-                    parent.owner,
-                    parent,
-                    options.id || null,
-                    pos,
-                    options.origin || null,
-                    size,
-                    (options.subdivisions == null) ? 64 : options.subdivisions,
-                    fill,
-                    options.border || null,
-                    (options.borderThickness == null) ? 1 : options.borderThickness,
-                    (options.isVisible == null) ? true : options.isVisible,
-                    options.marginTop || null,
-                    options.marginLeft || null,
-                    options.marginRight || null,
-                    options.marginBottom || null,
-                    options.vAlignment || null,
-                    options.hAlignment || null);
+            if (settings.size != null) {
+                this.size = settings.size;
+            }
+            else if (settings.width || settings.height) {
+                let size = new Size(settings.width, settings.height);
+                this.size = size;
             }
 
-            return ellipse;
+            let sub  = (settings.subdivisions == null) ? 64 : settings.subdivisions;
+            this.subdivisions = sub;
         }
 
         protected createModelRenderCache(modelKey: string): ModelRenderCache {
@@ -368,18 +374,17 @@
             }
             if (part.id === Shape2D.SHAPE2D_BORDERPARTID) {
                 let d = <Ellipse2DInstanceData>part;
-                let size = this.size;
+                let size = this.actualSize;
                 d.properties = new Vector3(size.width, size.height, this.subdivisions);
             }
             else if (part.id === Shape2D.SHAPE2D_FILLPARTID) {
                 let d = <Ellipse2DInstanceData>part;
-                let size = this.size;
+                let size = this.actualSize;
                 d.properties = new Vector3(size.width, size.height, this.subdivisions);
             }
             return true;
         }
 
-        private _size: Size;
         private _subdivisions: number;
     }
 }

+ 91 - 86
src/Canvas2d/babylon.group2d.ts

@@ -23,13 +23,6 @@
         public static GROUPCACHEBEHAVIOR_CACHEINPARENTGROUP = 2;
 
         /**
-         * Don't invoke directly, rely on Group2D.CreateXXX methods
-         */
-        constructor() {
-            super();
-        }
-
-        /**
          * Create an Logical or Renderable Group.
          * @param parent the parent primitive, must be a valid primitive (or the Canvas)
          * options:
@@ -43,45 +36,57 @@
          *  - hAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
          *  - vAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
          */
-        static CreateGroup2D(parent: Prim2DBase, options: { id?: string, position?: Vector2, x?: number, y?: number, origin?: Vector2, size?: Size, width?: number, height?: number, cacheBehavior?: number, isVisible?: boolean, marginTop?: number, marginLeft?: number, marginRight?: number, marginBottom?: number, vAlignment?: number, hAlignment?: number }): Group2D {
-            Prim2DBase.CheckParent(parent);
-
-
-            var g = new Group2D();
-
-            if (!options) {
-                g.setupGroup2D(parent.owner, parent, null, Vector2.Zero(), null, null, true, Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY, null, null, null, null, null, null);
-            } else {
-                let pos = options.position || new Vector2(options.x || 0, options.y || 0);
-                let size = (!options.size && !options.width && !options.height) ? null : (options.size || (new Size(options.width || 0, options.height || 0)));
-
-                g.setupGroup2D
-                    (
-                    parent.owner,
-                    parent,
-                    options.id || null,
-                    pos,
-                    options.origin || null,
-                    size,
-                    (options.isVisible == null) ? true : options.isVisible,
-                    (options.cacheBehavior == null) ? Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY : options.cacheBehavior,
-                    options.marginTop,
-                    options.marginLeft,
-                    options.marginRight,
-                    options.marginBottom,
-                    options.hAlignment || Prim2DBase.HAlignLeft,
-                    options.vAlignment || Prim2DBase.VAlignTop);
-            }
-
-            return g;
+        constructor(settings?: {
+
+            parent            ?: Prim2DBase, 
+            children          ?: Array<Prim2DBase>,
+            id                ?: string,
+            position          ?: Vector2,
+            x                 ?: number,
+            y                 ?: number,
+            origin            ?: Vector2,
+            size              ?: Size,
+            width             ?: number,
+            height            ?: number,
+            cacheBehavior     ?: number,
+            layoutEngine      ?: LayoutEngineBase | string,
+            isVisible         ?: boolean,
+            marginTop         ?: number | string,
+            marginLeft        ?: number | string,
+            marginRight       ?: number | string,
+            marginBottom      ?: number | string,
+            margin            ?: number | string,
+            marginHAlignment  ?: number,
+            marginVAlignment  ?: number,
+            marginAlignment   ?: string,
+            paddingTop        ?: number | string,
+            paddingLeft       ?: number | string,
+            paddingRight      ?: number | string,
+            paddingBottom     ?: number | string,
+            padding           ?: string,
+            paddingHAlignment ?: number,
+            paddingVAlignment ?: number,
+
+        }) {
+            if (settings == null) {
+                settings = {};
+            }
+            if (settings.origin == null) {
+                settings.origin = new Vector2(0, 0);
+            }
+            super(settings);
+ 
+            let size = (!settings.size && !settings.width && !settings.height) ? null : (settings.size || (new Size(settings.width || 0, settings.height || 0)));
+
+            this._cacheBehavior = (settings.cacheBehavior==null) ? Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY : settings.cacheBehavior;
+            this.size = size;
+            this._viewportPosition = Vector2.Zero();
         }
 
         static _createCachedCanvasGroup(owner: Canvas2D): Group2D {
-            var g = new Group2D();
-            g.setupGroup2D(owner, null, "__cachedCanvasGroup__", Vector2.Zero(), Vector2.Zero(), null, true, Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY, null, null, null, null, null, null);
-            g.origin = Vector2.Zero();
+            var g = new Group2D({ parent: owner, id: "__cachedCanvasGroup__", position: Vector2.Zero(), origin: Vector2.Zero(), size:null, isVisible:true});
             return g;
-
+            
         }
 
         protected applyCachedTexture(vertexData: VertexData, material: StandardMaterial) {
@@ -138,13 +143,6 @@
             return true;
         }
 
-        protected setupGroup2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, origin: Vector2, size: Size, isVisible: boolean, cacheBehavior: number, marginTop: number, marginLeft: number, marginRight: number, marginBottom: number, hAlign: number, vAlign: number) {
-            this._cacheBehavior = cacheBehavior;
-            this.setupPrim2DBase(owner, parent, id, position, origin, isVisible, marginTop, marginLeft, marginRight, marginBottom, hAlign, vAlign);
-            this.size = size;
-            this._viewportPosition = Vector2.Zero();
-        }
-
         /**
          * @returns Returns true if the Group render content, false if it's a logical group only
          */
@@ -196,9 +194,9 @@
                 actualSize = new Size(Math.ceil(m.x), Math.ceil(m.y));
             }
 
-            // Compare the size with the one we previously had, if it differ we set the property dirty and trigger a GroupChanged to synchronize a displaySprite (if any)
+            // Compare the size with the one we previously had, if it differs we set the property dirty and trigger a GroupChanged to synchronize a displaySprite (if any)
             if (!actualSize.equals(this._actualSize)) {
-                this._instanceDirtyFlags |= Group2D.actualSizeProperty.flagId;
+                this.onPrimitivePropertyDirty(Group2D.actualSizeProperty.flagId);
                 this._actualSize = actualSize;
                 this.handleGroupChanged(Group2D.actualSizeProperty);
             }
@@ -219,7 +217,7 @@
         }
 
         public _renderCachedCanvas() {
-            this.updateGlobalTransVis(true);
+            this.updateCachedStates(true);
             let context = new PrepareRender2DContext();
             this._prepareGroupRender(context);
             this._groupRender();
@@ -252,7 +250,7 @@
             // Update the Global Transformation and visibility status of the changed primitives
             if ((this._renderableData._primDirtyList.length > 0) || context.forceRefreshPrimitive) {
                 sortedDirtyList = this._renderableData._primDirtyList.sort((a, b) => a.hierarchyDepth - b.hierarchyDepth);
-                this.updateGlobalTransVisOf(sortedDirtyList, true);
+                this.updateCachedStatesOf(sortedDirtyList, true);
             }
 
             // Setup the size of the rendering viewport
@@ -347,11 +345,13 @@
                     var curVP = engine.setDirectViewport(this._viewportPosition.x, this._viewportPosition.y, this._viewportSize.width, this._viewportSize.height);
                 }
 
+                let curAlphaTest = engine.getAlphaTesting() === true;
+                let curDepthWrite = engine.getDepthWrite() === true;
+
                 // ===================================================================
                 // First pass, update the InstancedArray and render Opaque primitives
 
-                // Disable Culling, Alpha Testing, Enable Depth Write
-                engine.setState(false);
+                // Disable Alpha Testing, Enable Depth Write
                 engine.setAlphaTesting(false);
                 engine.setDepthWrite(true);
 
@@ -435,6 +435,10 @@
                         engine.setViewport(curVP);
                     }
                 }
+
+                // Restore saved states
+                engine.setAlphaTesting(curAlphaTest);
+                engine.setDepthWrite(curDepthWrite);
             }
         }
 
@@ -447,7 +451,7 @@
             }
 
             // Sort all the primitive from their depth, max (bottom) to min (top)
-            rd._transparentPrimitives.sort((a, b) => b._primitive.getActualZOffset() - a._primitive.getActualZOffset());
+            rd._transparentPrimitives.sort((a, b) => b._primitive.actualZOffset - a._primitive.actualZOffset);
 
             let checkAndAddPrimInSegment = (seg: TransparentSegment, tpiI: number): boolean => {
                 let tpi = rd._transparentPrimitives[tpiI];
@@ -457,7 +461,7 @@
                     return false;
                 }
 
-                let tpiZ = tpi._primitive.getActualZOffset();
+                let tpiZ = tpi._primitive.actualZOffset;
 
                 // We've made it so far, the tpi can be part of the segment, add it
                 tpi._transparentSegment = seg;
@@ -483,7 +487,7 @@
                 }
 
                 // Reset the segment, we have to create/rebuild it
-                tpi._transparentSegment = null;
+                tpi._transparentSegment = null;                
 
                 // If there's a previous valid segment, check if this prim can be part of it
                 if (prevSeg) {
@@ -495,7 +499,7 @@
                     let ts = new TransparentSegment();
                     ts.groupInsanceInfo = tpi._groupInstanceInfo;
                     let prim = tpi._primitive;
-                    ts.startZ = prim.getActualZOffset();
+                    ts.startZ = prim.actualZOffset;
                     ts.startDataIndex = prim._getFirstIndexInDataBuffer();
                     ts.endDataIndex = prim._getLastIndexInDataBuffer() + 1; // Make it exclusive, more natural to use in a for loop
                     ts.endZ = ts.startZ;
@@ -544,27 +548,27 @@
             // Render Mode specifics
             switch (context.renderMode) {
                 case Render2DContext.RenderModeOpaque:
-                    {
-                        if (!gii.hasOpaqueData) {
-                            return null;
-                        }
-                        setDirty = (dirty: boolean) => { gii.opaqueDirty = dirty; };
-                        getDirty = () => gii.opaqueDirty;
-                        context.groupInfoPartData = gii.opaqueData;
-                        gipd = gii.opaqueData;
-                        break;
+                {
+                    if (!gii.hasOpaqueData) {
+                        return null;
                     }
+                    setDirty = (dirty: boolean) => { gii.opaqueDirty = dirty; };
+                    getDirty = () => gii.opaqueDirty;
+                    context.groupInfoPartData = gii.opaqueData;
+                    gipd = gii.opaqueData;
+                    break;
+                }
                 case Render2DContext.RenderModeAlphaTest:
-                    {
-                        if (!gii.hasAlphaTestData) {
-                            return null;
-                        }
-                        setDirty = (dirty: boolean) => { gii.alphaTestDirty = dirty; };
-                        getDirty = () => gii.alphaTestDirty;
-                        context.groupInfoPartData = gii.alphaTestData;
-                        gipd = gii.alphaTestData;
-                        break;
+                {
+                    if (!gii.hasAlphaTestData) {
+                        return null;
                     }
+                    setDirty = (dirty: boolean) => { gii.alphaTestDirty = dirty; };
+                    getDirty = () => gii.alphaTestDirty;
+                    context.groupInfoPartData = gii.alphaTestData;
+                    gipd = gii.alphaTestData;
+                    break;
+                }
                 default:
                     throw new Error("_prepareContext is only for opaque or alphaTest");
             }
@@ -677,8 +681,8 @@
 
             // For now we only support these property changes
             // TODO: add more! :)
-            if (prop.id === Prim2DBase.positionProperty.id) {
-                rd._cacheRenderSprite.position = this.position.clone();
+            if (prop.id === Prim2DBase.actualPositionProperty.id) {
+                rd._cacheRenderSprite.actualPosition = this.actualPosition.clone();
             } else if (prop.id === Prim2DBase.rotationProperty.id) {
                 rd._cacheRenderSprite.rotation = this.rotation;
             } else if (prop.id === Prim2DBase.scaleProperty.id) {
@@ -686,7 +690,7 @@
             } else if (prop.id === Prim2DBase.originProperty.id) {
                 rd._cacheRenderSprite.origin = this.origin.clone();
             } else if (prop.id === Group2D.actualSizeProperty.id) {
-                rd._cacheRenderSprite.spriteSize = this.actualSize.clone();
+                rd._cacheRenderSprite.size = this.actualSize.clone();
                 //console.log(`[${this._globalTransformProcessStep}] Sync Sprite ${this.id}, width: ${this.actualSize.width}, height: ${this.actualSize.height}`);
             }
         }
@@ -744,7 +748,10 @@
 
 
             if (this._isRenderableGroup) {
-                this._renderableData = new RenderableGroupData();
+                // Yes, we do need that check, trust me, unfortunately we can call _detectGroupStates many time on the same object...
+                if (!this._renderableData) {
+                    this._renderableData = new RenderableGroupData();
+                }
             }
 
             // If the group is tagged as renderable we add it to the renderable tree
@@ -763,8 +770,6 @@
         protected _isRenderableGroup: boolean;
         protected _isCachedGroup: boolean;
         private _cacheGroupDirty: boolean;
-        private _size: Size;
-        private _actualSize: Size;
         private _cacheBehavior: number;
         private _viewportPosition: Vector2;
         private _viewportSize: Size;
@@ -840,8 +845,8 @@
 
         updateSmallestZChangedPrim(tpi: TransparentPrimitiveInfo) {
             if (tpi._primitive) {
-                let newZ = tpi._primitive.getActualZOffset();
-                let curZ = this._firstChangedPrim ? this._firstChangedPrim._primitive.getActualZOffset() : Number.MIN_VALUE;
+                let newZ = tpi._primitive.actualZOffset;
+                let curZ = this._firstChangedPrim ? this._firstChangedPrim._primitive.actualZOffset : Number.MIN_VALUE;
                 if (newZ > curZ) {
                     this._firstChangedPrim = tpi;
                 }
@@ -851,7 +856,7 @@
         _primDirtyList: Array<Prim2DBase>;
         _childrenRenderableGroups: Array<Group2D>;
         _renderGroupInstancesInfo: StringDictionary<GroupInstanceInfo>;
-
+        
         _cacheNode: PackedRect;
         _cacheTexture: MapTexture;
         _cacheRenderSprite: Sprite2D;

File diff suppressed because it is too large
+ 644 - 567
src/Canvas2d/babylon.lines2d.ts


File diff suppressed because it is too large
+ 1546 - 206
src/Canvas2d/babylon.prim2dBase.ts


+ 79 - 52
src/Canvas2d/babylon.rectangle2d.ts

@@ -167,21 +167,20 @@
     @className("Rectangle2D")
     export class Rectangle2D extends Shape2D {
 
-        public static sizeProperty: Prim2DPropInfo;
+        public static actualSizeProperty: Prim2DPropInfo;
         public static notRoundedProperty: Prim2DPropInfo;
         public static roundRadiusProperty: Prim2DPropInfo;
 
+        @instanceLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 1, pi => Rectangle2D.actualSizeProperty = pi, false, true)
         public get actualSize(): Size {
+            if (this._actualSize) {
+                return this._actualSize;
+            }
             return this.size;
         }
 
-        @instanceLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 1, pi => Rectangle2D.sizeProperty = pi, false, true)
-        public get size(): Size {
-            return this._size;
-        }
-
-        public set size(value: Size) {
-            this._size = value;
+        public set actualSize(value: Size) {
+            this._actualSize = value;
         }
 
         @modelLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 2, pi => Rectangle2D.notRoundedProperty = pi)
@@ -201,6 +200,7 @@
         public set roundRadius(value: number) {
             this._roundRadius = value;
             this.notRounded = value === 0;
+            this._positioningDirty();
         }
 
         private static _i0 = Vector2.Zero();
@@ -218,10 +218,9 @@
             // If it's the case for one, check if the mouse is located in the quarter that we care about (the one who is visible) then finally make a distance check with the roundRadius radius to see if it's inside the circle quarter or outside.
 
             // First let remove the origin out the equation, to have the rectangle with an origin at bottom/left
-            let o = this.origin;
             let size = this.size;
-            Rectangle2D._i0.x = intersectInfo._localPickPosition.x + (size.width * o.x);
-            Rectangle2D._i0.y = intersectInfo._localPickPosition.y + (size.height * o.y);
+            Rectangle2D._i0.x = intersectInfo._localPickPosition.x;
+            Rectangle2D._i0.y = intersectInfo._localPickPosition.y;
 
             let rr = this.roundRadius;
             let rrs = rr * rr;
@@ -281,14 +280,7 @@
         }
 
         protected updateLevelBoundingInfo() {
-            BoundingInfo2D.CreateFromSizeToRef(this.size, this._levelBoundingInfo, this.origin);
-        }
-
-        protected setupRectangle2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, origin: Vector2, size: Size, roundRadius, fill: IBrush2D, border: IBrush2D, borderThickness: number, isVisible: boolean, marginTop: number, marginLeft: number, marginRight: number, marginBottom: number, vAlignment: number, hAlignment: number) {
-            this.setupShape2D(owner, parent, id, position, origin, isVisible, fill, border, borderThickness, marginTop, marginLeft, marginRight, marginBottom, hAlignment, vAlignment);
-            this.size = size;
-            this.notRounded = !roundRadius;
-            this.roundRadius = roundRadius;
+            BoundingInfo2D.CreateFromSizeToRef(this.actualSize, this._levelBoundingInfo);
         }
 
         /**
@@ -308,39 +300,63 @@
          *  - hAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
          *  - vAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
          */
-        public static Create(parent: Prim2DBase, options: { id?: string, position?: Vector2, x?: number, y?: number, origin?: Vector2, size?: Size, width?: number, height?: number, roundRadius?: number, fill?: IBrush2D, border?: IBrush2D, borderThickness?: number, isVisible?: boolean, marginTop?: number, marginLeft?: number, marginRight?: number, marginBottom?: number, vAlignment?: number, hAlignment?: number}): Rectangle2D {
-            Prim2DBase.CheckParent(parent);
+        constructor(settings ?: {
+            parent           ?: Prim2DBase, 
+            children         ?: Array<Prim2DBase>,
+            id               ?: string,
+            position         ?: Vector2,
+            x                ?: number,
+            y                ?: number,
+            rotation         ?: number,
+            scale            ?: number,
+            origin           ?: Vector2,
+            size             ?: Size,
+            width            ?: number,
+            height           ?: number,
+            roundRadius      ?: number,
+            fill             ?: IBrush2D | string,
+            border           ?: IBrush2D | string,
+            borderThickness  ?: number,
+            isVisible        ?: boolean,
+            marginTop        ?: number | string,
+            marginLeft       ?: number | string,
+            marginRight      ?: number | string,
+            marginBottom     ?: number | string,
+            margin           ?: number | string,
+            marginHAlignment ?: number,
+            marginVAlignment ?: number,
+            marginAlignment  ?: string,
+            paddingTop       ?: number | string,
+            paddingLeft      ?: number | string,
+            paddingRight     ?: number | string,
+            paddingBottom    ?: number | string,
+            padding          ?: string,
+            paddingHAlignment?: number,
+            paddingVAlignment?: number,
+        }) {
+
+            // Avoid checking every time if the object exists
+            if (settings == null) {
+                settings = {};
+            }
 
-            let rect = new Rectangle2D();
+            super(settings);
 
-            if (!options) {
-                rect.setupRectangle2D(parent.owner, parent, null, Vector2.Zero(), null, new Size(10, 10), 0, Canvas2D.GetSolidColorBrushFromHex("#FFFFFFFF"), null, 1, true, null, null, null, null, null, null);
-            } else {
-                let pos = options.position || new Vector2(options.x || 0, options.y || 0);
-                let size = options.size || (new Size(options.width || 10, options.height || 10));
-                let fill = options.fill===undefined ? Canvas2D.GetSolidColorBrushFromHex("#FFFFFFFF") : options.fill;
-
-                rect.setupRectangle2D
-                (
-                    parent.owner,
-                    parent,
-                    options.id || null,
-                    pos,
-                    options.origin || null,
-                    size,
-                    (options.roundRadius == null) ? 0 : options.roundRadius,
-                    fill,
-                    options.border || null,
-                    (options.borderThickness==null) ? 1 : options.borderThickness,
-                    options.isVisible || true,
-                    options.marginTop || null,
-                    options.marginLeft || null,
-                    options.marginRight || null,
-                    options.marginBottom || null,
-                    options.vAlignment || null,
-                    options.hAlignment || null);
+            if (settings.size != null) {
+                this.size = settings.size;
             }
-            return rect;
+            else if (settings.width || settings.height) {
+                let size = new Size(settings.width, settings.height);
+                this.size = size;
+            }
+
+            //let size            = settings.size || (new Size((settings.width === null) ? null : (settings.width || 10), (settings.height === null) ? null : (settings.height || 10)));
+            let roundRadius     = (settings.roundRadius == null) ? 0 : settings.roundRadius;
+            let borderThickness = (settings.borderThickness == null) ? 1 : settings.borderThickness;
+
+            //this.size            = size;
+            this.roundRadius     = roundRadius;
+            this.borderThickness = borderThickness;
         }
 
         public static roundSubdivisions = 16;
@@ -428,6 +444,18 @@
             return renderCache;
         }
 
+        // We override this method because if there's a roundRadius set, we will reduce the initial Content Area to make sure the computed area won't intersect with the shape contour. The formula is simple: we shrink the incoming size by the amount of the roundRadius
+        protected _getInitialContentAreaToRef(primSize: Size, initialContentPosition: Vector2, initialContentArea: Size) {
+            // Fall back to default implementation if there's no round Radius
+            if (this._notRounded) {
+                super._getInitialContentAreaToRef(primSize, initialContentPosition, initialContentArea);
+            } else {
+                let rr = Math.round((this.roundRadius - (this.roundRadius/Math.sqrt(2))) * 1.3);
+                initialContentPosition.x = initialContentPosition.y = rr;
+                initialContentArea.width = Math.max(0, primSize.width - (rr * 2));
+                initialContentArea.height = Math.max(0, primSize.height - (rr * 2));
+            }
+        }
 
         protected createInstanceDataParts(): InstanceDataBase[] {
             var res = new Array<InstanceDataBase>();
@@ -446,18 +474,17 @@
             }
             if (part.id === Shape2D.SHAPE2D_BORDERPARTID) {
                 let d = <Rectangle2DInstanceData>part;
-                let size = this.size;
+                let size = this.actualSize;
                 d.properties = new Vector3(size.width, size.height, this.roundRadius || 0);
             }
             else if (part.id === Shape2D.SHAPE2D_FILLPARTID) {
                 let d = <Rectangle2DInstanceData>part;
-                let size = this.size;
+                let size = this.actualSize;
                 d.properties = new Vector3(size.width, size.height, this.roundRadius || 0);
             }
             return true;
         }
 
-        private _size: Size;
         private _notRounded: boolean;
         private _roundRadius: number;
     }

+ 32 - 24
src/Canvas2d/babylon.renderablePrim2d.ts

@@ -272,11 +272,6 @@
             return null;
         }
 
-        @instanceData()
-        get origin(): Vector2 {
-            return null;
-        }
-
         getClassTreeInfo(): ClassTreeInfo<InstanceClassInfo, InstancePropInfo> {
             if (!this.typeInfo) {
                 this.typeInfo = ClassTreeInfo.get<InstanceClassInfo, InstancePropInfo>(Object.getPrototypeOf(this));
@@ -353,10 +348,15 @@
             this._isTransparent = value;
         }
 
-        setupRenderablePrim2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, origin: Vector2, isVisible: boolean, marginTop: number, marginLeft: number, marginRight: number, marginBottom: number, hAlign: number, vAlign: number) {
-            this.setupPrim2DBase(owner, parent, id, position, origin, isVisible, marginTop, marginLeft, marginRight, marginBottom, hAlign, vAlign);
-            this._isTransparent = false;
-            this._isAlphaTest = false;
+        constructor(settings?: {
+            parent       ?: Prim2DBase, 
+            id           ?: string,
+            origin       ?: Vector2,
+            isVisible    ?: boolean,
+        }) {
+            super(settings);
+            this._isTransparent            = false;
+            this._isAlphaTest              = false;
             this._transparentPrimitiveInfo = null;
         }
 
@@ -365,6 +365,11 @@
                 return false;
             }
 
+            if (this._transparentPrimitiveInfo) {
+                this.renderGroup._renderableData.removeTransparentPrimitiveInfo(this._transparentPrimitiveInfo);
+                this._transparentPrimitiveInfo = null;
+            }
+
             if (this._modelRenderInstanceID) {
                 this._modelRenderCache.removeInstanceData(this._modelRenderInstanceID);
                 this._modelRenderInstanceID = null;
@@ -389,14 +394,14 @@
             super._prepareRenderPre(context);
 
             // If the model changed and we have already an instance, we must remove this instance from the obsolete model
-            if (this._modelDirty && this._modelRenderInstanceID) {
+            if (this._isFlagSet(SmartPropertyPrim.flagModelDirty) && this._modelRenderInstanceID) {
                 this._modelRenderCache.removeInstanceData(this._modelRenderInstanceID);
                 this._modelRenderInstanceID = null;
             }
 
             // Need to create the model?
             let setupModelRenderCache = false;
-            if (!this._modelRenderCache || this._modelDirty) {
+            if (!this._modelRenderCache || this._isFlagSet(SmartPropertyPrim.flagModelDirty)) {
                 setupModelRenderCache = this._createModelRenderCache();
             }
 
@@ -418,7 +423,12 @@
             // At this stage we have everything correctly initialized, ModelRenderCache is setup, Model Instance data are good too, they have allocated elements in the Instanced DynamicFloatArray.
 
             // The last thing to do is check if the instanced related data must be updated because a InstanceLevel property had changed or the primitive visibility changed.
-            if (this._visibilityChanged || context.forceRefreshPrimitive || newInstance || (this._instanceDirtyFlags !== 0) || (this._globalTransformProcessStep !== this._globalTransformStep)) {
+            if (this._isFlagSet(SmartPropertyPrim.flagVisibilityChanged) || context.forceRefreshPrimitive || newInstance || (this._instanceDirtyFlags !== 0) || (this._globalTransformProcessStep !== this._globalTransformStep)) {
+
+                if (this.isTransparent) {
+                    //this.renderGroup._renderableData._transparentListChanged = true;
+                }
+
                 this._updateInstanceDataParts(gii);
             }
         }
@@ -434,7 +444,7 @@
                 setupModelRenderCache = true;
                 return mrc;
             });
-            this._modelDirty = false;
+            this._clearFlags(SmartPropertyPrim.flagModelDirty);
 
             // if this is still false it means the MRC already exists, so we add a reference to it
             if (!setupModelRenderCache) {
@@ -555,7 +565,7 @@
             // Handle changes related to ZOffset
             if (this.isTransparent) {
                 // Handle visibility change, which is also triggered when the primitive just got created
-                if (this._visibilityChanged) {
+                if (this._isFlagSet(SmartPropertyPrim.flagVisibilityChanged)) {
                     if (this.isVisible) {
                         if (!this._transparentPrimitiveInfo) {
                             // Add the primitive to the list of transparent ones in the group that render is
@@ -574,7 +584,7 @@
             // For each Instance Data part, refresh it to update the data in the DynamicFloatArray
             for (let part of this._instanceDataParts) {
                 // Check if we need to allocate data elements (hidden prim which becomes visible again)
-                if (this._visibilityChanged && !part.dataElements) {
+                if (this._isFlagSet(SmartPropertyPrim.flagVisibilityChanged) && !part.dataElements) {
                     part.allocElements();
                 }
 
@@ -599,7 +609,7 @@
                 gii.opaqueDirty = true;
             }
 
-            this._visibilityChanged = false;    // Reset the flag as we've handled the case            
+            this._clearFlags(SmartPropertyPrim.flagVisibilityChanged);    // Reset the flag as we've handled the case            
         }
 
         public _getFirstIndexInDataBuffer(): number {
@@ -759,20 +769,19 @@
         /**
          * Update the instanceDataBase level properties of a part
          * @param part the part to update
-         * @param positionOffset to use in multi part per primitive (e.g. the Text2D has N parts for N letter to display), this give the offset to apply (e.g. the position of the letter from the bottom/left corner of the text). You MUST also set customSize.
-         * @param customSize to use in multi part per primitive, this is the size of the overall primitive to display (the bounding rect's size of the Text, for instance). This is mandatory to compute correct transformation based on the Primitive's origin property.
+         * @param positionOffset to use in multi part per primitive (e.g. the Text2D has N parts for N letter to display), this give the offset to apply (e.g. the position of the letter from the bottom/left corner of the text).
          */
-        protected updateInstanceDataPart(part: InstanceDataBase, positionOffset: Vector2 = null, customSize: Size = null) {
+        protected updateInstanceDataPart(part: InstanceDataBase, positionOffset: Vector2 = null) {
             let t = this._globalTransform.multiply(this.renderGroup.invGlobalTransform);
             let size = (<Size>this.renderGroup.viewportSize);
-            let zBias = this.getActualZOffset();
+            let zBias = this.actualZOffset;
 
             let offX = 0;
             let offY = 0;
             // If there's an offset, apply the global transformation matrix on it to get a global offset
-            if (positionOffset && customSize) {
-                offX = (positionOffset.x-(customSize.width*this.origin.x)) * t.m[0] + (positionOffset.y-(customSize.height*this.origin.y)) * t.m[4];
-                offY = (positionOffset.x-(customSize.width*this.origin.x)) * t.m[1] + (positionOffset.y-(customSize.height*this.origin.y)) * t.m[5];
+            if (positionOffset) {
+                offX = positionOffset.x * t.m[0] + positionOffset.y * t.m[4];
+                offY = positionOffset.x * t.m[1] + positionOffset.y * t.m[5];
             }
 
             // Have to convert the coordinates to clip space which is ranged between [-1;1] on X and Y axis, with 0,0 being the left/bottom corner
@@ -787,7 +796,6 @@
             let ty = new Vector4(t.m[1] * 2 / h, t.m[5] * 2 / h, 0/*t.m[9]*/, ((t.m[13] + offY) * 2 / h) - 1);
             part.transformX = tx;
             part.transformY = ty;
-            part.origin = this.origin;
 
             // Stores zBias and it's inverse value because that's needed to compute the clip space W coordinate (which is 1/Z, so 1/zBias)
             part.zBias = new Vector2(zBias, invZBias);

+ 32 - 5
src/Canvas2d/babylon.shape2d.ts

@@ -44,12 +44,39 @@
             this._borderThickness = value;
         }
 
-        setupShape2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, origin: Vector2, isVisible: boolean, fill: IBrush2D, border: IBrush2D, borderThickness: number, marginTop: number, marginLeft: number, marginRight: number, marginBottom: number, vAlignment: number, hAlignment: number) {
+        constructor(settings?: {
+            fill           ?: IBrush2D | string,
+            border         ?: IBrush2D | string,
+            borderThickness?: number,
+        }) {
 
-            this.setupRenderablePrim2D(owner, parent, id, position, origin, isVisible, marginTop, marginLeft, marginRight, marginBottom, hAlignment || Prim2DBase.HAlignLeft, vAlignment || Prim2DBase.VAlignTop);
-            this.border = border;
-            this.fill = fill;
-            this.borderThickness = borderThickness;
+            super(settings);
+
+            if (!settings) {
+                settings = {};
+            }
+
+            let borderBrush: IBrush2D = null;
+            if (settings.border) {
+                if (typeof (settings.border) === "string") {
+                    borderBrush = Canvas2D.GetBrushFromString(<string>settings.border);
+                } else {
+                    borderBrush = <IBrush2D>settings.border;
+                }
+            }
+
+            let fillBrush: IBrush2D = null;
+            if (settings.fill) {
+                if (typeof (settings.fill) === "string") {
+                    fillBrush = Canvas2D.GetBrushFromString(<string>settings.fill);
+                } else {
+                    fillBrush = <IBrush2D>settings.fill;
+                }
+            }
+
+            this.border = borderBrush;
+            this.fill = fillBrush;
+            this.borderThickness = settings.borderThickness;
         }
 
         protected getUsedShaderCategories(dataPart: InstanceDataBase): string[] {

+ 155 - 51
src/Canvas2d/babylon.smartPropertyPrim.ts

@@ -135,12 +135,10 @@
     @className("SmartPropertyPrim")
     export class SmartPropertyPrim implements IPropertyChanged {
 
-        protected setupSmartPropertyPrim() {
+        constructor() {
+            this._flags = 0;
             this._modelKey = null;
-            this._modelDirty = false;
-            this._levelBoundingInfoDirty = false;
             this._instanceDirtyFlags = 0;
-            this._isDisposed = false;
             this._levelBoundingInfo = new BoundingInfo2D();
             this.animations = new Array<Animation>();
         }
@@ -156,7 +154,7 @@
          * @returns true if the object is dispose, false otherwise.
          */
         public get isDisposed(): boolean {
-            return this._isDisposed;
+            return this._isFlagSet(SmartPropertyPrim.flagIsDisposed);
         }
 
         /**
@@ -171,7 +169,7 @@
             // Don't set to null, it may upset somebody...
             this.animations.splice(0);
 
-            this._isDisposed = true;
+            this._setFlags(SmartPropertyPrim.flagIsDisposed);
             return true;
         }
 
@@ -196,7 +194,7 @@
         public get modelKey(): string {
 
             // No need to compute it?
-            if (!this._modelDirty && this._modelKey) {
+            if (!this._isFlagSet(SmartPropertyPrim.flagModelDirty) && this._modelKey) {
                 return this._modelKey;
             }
 
@@ -220,7 +218,7 @@
                 }
             });
 
-            this._modelDirty = false;
+            this._clearFlags(SmartPropertyPrim.flagModelDirty);
             this._modelKey = modelKey;
 
             return modelKey;
@@ -231,7 +229,7 @@
          * @returns true is dirty, false otherwise
          */
         public get isDirty(): boolean {
-            return (this._instanceDirtyFlags !== 0) || this._modelDirty;
+            return (this._instanceDirtyFlags !== 0) || this._areSomeFlagsSet(SmartPropertyPrim.flagModelDirty | SmartPropertyPrim.flagPositioningDirty | SmartPropertyPrim.flagLayoutDirty);
         }
 
         /**
@@ -315,27 +313,35 @@
             this._handlePropChanged(undefined, newValue, propertyName, propInfo, propInfo.typeLevelCompare);
         }
 
-        private _handlePropChanged<T>(curValue: T, newValue: T, propName: string, propInfo: Prim2DPropInfo, typeLevelCompare: boolean) {
-            // If the property change also dirty the boundingInfo, update the boundingInfo dirty flags
-            if (propInfo.dirtyBoundingInfo) {
-                this._levelBoundingInfoDirty = true;
-
-                // Escalate the dirty flag in the instance hierarchy, stop when a renderable group is found or at the end
-                if (this instanceof Prim2DBase) {
-                    let curprim = (<any>this).parent;
-                    while (curprim) {
-                        curprim._boundingInfoDirty = true;
-
-                        if (curprim instanceof Group2D) {
-                            if (curprim.isRenderableGroup) {
-                                break;
-                            }
-                        }
+        protected _boundingBoxDirty() {
+            this._setFlags(SmartPropertyPrim.flagLevelBoundingInfoDirty);
+
+            // Escalate the dirty flag in the instance hierarchy, stop when a renderable group is found or at the end
+            if (this instanceof Prim2DBase) {
+                let curprim: Prim2DBase = (<any>this);
+                while (curprim) {
+                    curprim._boundingSize = null;
+                    curprim._setFlags(SmartPropertyPrim.flagBoundingInfoDirty);
+                    if (curprim.isSizeAuto) {
+                        curprim.onPrimitivePropertyDirty(Prim2DBase.sizeProperty.flagId);
+                    }
 
-                        curprim = curprim.parent;
+                    if (curprim instanceof Group2D) {
+                        if (curprim.isRenderableGroup) {
+                            break;
+                        }
                     }
+
+                    curprim = curprim.parent;
                 }
             }
+        }
+
+        private _handlePropChanged<T>(curValue: T, newValue: T, propName: string, propInfo: Prim2DPropInfo, typeLevelCompare: boolean) {
+            // If the property change also dirty the boundingInfo, update the boundingInfo dirty flags
+            if (propInfo.dirtyBoundingInfo) {
+                this._boundingBoxDirty();
+            }
 
             // Trigger property changed
             let info = SmartPropertyPrim.propChangedInfo;
@@ -350,29 +356,36 @@
                 this.handleGroupChanged(propInfo);
             }
 
-            // Check if we need to dirty only if the type change and make the test
-            var skipDirty = false;
+            // Check for parent layout dirty
+            if (this instanceof Prim2DBase) {
+                let p = (<any>this)._parent;
+                if (p != null && p.layoutEngine && (p.layoutEngine.layoutDirtyOnPropertyChangedMask & propInfo.flagId) !== 0) {
+                    p._setLayoutDirty();
+                }
+            }
+
+            // For type level compare, if there's a change of type it's a change of model, otherwise we issue an instance change
+            var instanceDirty = false;
             if (typeLevelCompare && curValue != null && newValue != null) {
                 var cvProto = (<any>curValue).__proto__;
                 var nvProto = (<any>newValue).__proto__;
 
-                skipDirty = (cvProto === nvProto);
+                instanceDirty = (cvProto === nvProto);
             }
 
             // Set the dirty flags
-            if (!skipDirty) {
-                if (propInfo.kind === Prim2DPropInfo.PROPKIND_MODEL) {
-                    if (!this.isDirty) {
-                        this.onPrimBecomesDirty();
-                    }
-                    this._modelDirty = true;
-                } else if ((propInfo.kind === Prim2DPropInfo.PROPKIND_INSTANCE) || (propInfo.kind === Prim2DPropInfo.PROPKIND_DYNAMIC)) {
-                    if (!this.isDirty) {
-                        this.onPrimBecomesDirty();
-                    }
-                    this._instanceDirtyFlags |= propMask;
-                }
+            if (!instanceDirty && (propInfo.kind === Prim2DPropInfo.PROPKIND_MODEL)) {
+                this.onPrimitivePropertyDirty(SmartPropertyPrim.flagModelDirty);
+            } else if (instanceDirty || (propInfo.kind === Prim2DPropInfo.PROPKIND_INSTANCE) || (propInfo.kind === Prim2DPropInfo.PROPKIND_DYNAMIC)) {
+                this.onPrimitivePropertyDirty(propMask);
+            }
+        }
+
+        protected onPrimitivePropertyDirty(propFlagId: number) {
+            if (!this.isDirty) {
+                this.onPrimBecomesDirty();
             }
+            this._instanceDirtyFlags |= propFlagId;
         }
 
         protected handleGroupChanged(prop: Prim2DPropInfo) {
@@ -404,12 +417,11 @@
 
         /**
          * Retrieve the boundingInfo for this Primitive, computed based on the primitive itself and NOT its children
-         * @returns {} 
          */
         public get levelBoundingInfo(): BoundingInfo2D {
-            if (this._levelBoundingInfoDirty) {
+            if (this._isFlagSet(SmartPropertyPrim.flagLevelBoundingInfoDirty)) {
                 this.updateLevelBoundingInfo();
-                this._levelBoundingInfoDirty = false;
+                this._clearFlags(SmartPropertyPrim.flagLevelBoundingInfoDirty);
             }
             return this._levelBoundingInfo;
         }
@@ -462,13 +474,105 @@
             }
         }
 
-        private _modelKey; string;
-        private _propInfo: StringDictionary<Prim2DPropInfo>;
-        private _isDisposed: boolean;
-        protected _levelBoundingInfoDirty: boolean;
-        protected _levelBoundingInfo: BoundingInfo2D;
-        protected _boundingInfo: BoundingInfo2D;
-        protected _modelDirty: boolean;
+        /**
+         * Add an externally attached data from its key.
+         * This method call will fail and return false, if such key already exists.
+         * If you don't care and just want to get the data no matter what, use the more convenient getOrAddExternalDataWithFactory() method.
+         * @param key the unique key that identifies the data
+         * @param data the data object to associate to the key for this Engine instance
+         * @return true if no such key were already present and the data was added successfully, false otherwise
+         */
+        public addExternalData<T>(key: string, data: T): boolean {
+            if (!this._externalData) {
+                this._externalData = new StringDictionary<Object>();
+            }
+            return this._externalData.add(key, data);
+        }
+
+        /**
+         * Get an externally attached data from its key
+         * @param key the unique key that identifies the data
+         * @return the associated data, if present (can be null), or undefined if not present
+         */
+        public getExternalData<T>(key: string): T {
+            if (!this._externalData) {
+                return null;
+            }
+            return <T>this._externalData.get(key);
+        }
+
+        /**
+         * Get an externally attached data from its key, create it using a factory if it's not already present
+         * @param key the unique key that identifies the data
+         * @param factory the factory that will be called to create the instance if and only if it doesn't exists
+         * @return the associated data, can be null if the factory returned null.
+         */
+        public getOrAddExternalDataWithFactory<T>(key: string, factory: (k: string) => T): T {
+            if (!this._externalData) {
+                this._externalData = new StringDictionary<Object>();
+            }
+            return <T>this._externalData.getOrAddWithFactory(key, factory);
+        }
+
+        /**
+         * Remove an externally attached data from the Engine instance
+         * @param key the unique key that identifies the data
+         * @return true if the data was successfully removed, false if it doesn't exist
+         */
+        public removeExternalData(key): boolean {
+            if (!this._externalData) {
+                return false;
+            }
+            return this._externalData.remove(key);
+        }
+
+        public _isFlagSet(flag: number): boolean {
+            return (this._flags & flag) !== 0;
+        }
+
+        public _areAllFlagsSet(flags: number): boolean {
+            return (this._flags & flags) === flags;
+        }
+
+        public _areSomeFlagsSet(flags: number): boolean {
+            return (this._flags & flags) !== 0;
+        }
+
+        public _clearFlags(flags: number) {
+            this._flags &= ~flags;
+        }
+
+        public _setFlags(flags: number): number {
+            let cur = this._flags;
+            this._flags |= flags;
+            return cur;
+        }
+
+        public _changeFlags(flags: number, state: boolean) {
+            if (state) {
+                this._flags |= flags;
+            } else {
+                this._flags &= ~flags;
+            }
+        }
+
+        public static flagIsDisposed             = 0x0000001;    // set if the object is already disposed
+        public static flagLevelBoundingInfoDirty = 0x0000002;    // set if the primitive's level bounding box (not including children) is dirty
+        public static flagModelDirty             = 0x0000004;    // set if the model must be changed
+        public static flagLayoutDirty            = 0x0000008;    // set if the layout must be computed
+        public static flagLevelVisible           = 0x0000010;    // set if the primitive is set as visible for its level only
+        public static flagBoundingInfoDirty      = 0x0000020;    // set if the primitive's overall bounding box (including children) is dirty
+        public static flagIsPickable             = 0x0000040;    // set if the primitive can be picked during interaction
+        public static flagIsVisible              = 0x0000080;    // set if the primitive is concretely visible (use the levelVisible of parents)
+        public static flagVisibilityChanged      = 0x0000100;    // set if there was a transition between visible/hidden status
+        public static flagPositioningDirty       = 0x0000200;    // set if the primitive positioning must be computed
+
+        private   _flags             : number;
+        private   _externalData      : StringDictionary<Object>;
+        private   _modelKey          : string;
+        private   _propInfo          : StringDictionary<Prim2DPropInfo>;
+        protected _levelBoundingInfo : BoundingInfo2D;
+        protected _boundingInfo      : BoundingInfo2D;
         protected _instanceDirtyFlags: number;
     }
 

+ 60 - 61
src/Canvas2d/babylon.sprite2d.ts

@@ -121,7 +121,7 @@
         static SPRITE2D_MAINPARTID = 1;
 
         public static textureProperty: Prim2DPropInfo;
-        public static spriteSizeProperty: Prim2DPropInfo;
+        public static actualSizeProperty: Prim2DPropInfo;
         public static spriteLocationProperty: Prim2DPropInfo;
         public static spriteFrameProperty: Prim2DPropInfo;
         public static invertYProperty: Prim2DPropInfo;
@@ -136,17 +136,16 @@
             this._texture = value;
         }
 
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, pi => Sprite2D.actualSizeProperty = pi, false, true)
         public get actualSize(): Size {
-            return this.spriteSize;
-        }
-
-        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, pi => Sprite2D.spriteSizeProperty = pi, false, true)
-        public get spriteSize(): Size {
-            return this._size;
+            if (this._actualSize) {
+                return this._actualSize;
+            }
+            return this.size;
         }
 
-        public set spriteSize(value: Size) {
-            this._size = value;
+        public set actualSize(value: Size) {
+            this._actualSize = value;
         }
 
         @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 3, pi => Sprite2D.spriteLocationProperty = pi)
@@ -185,7 +184,7 @@
         }
 
         protected updateLevelBoundingInfo() {
-            BoundingInfo2D.CreateFromSizeToRef(this.spriteSize, this._levelBoundingInfo, this.origin);
+            BoundingInfo2D.CreateFromSizeToRef(this.size, this._levelBoundingInfo);
         }
 
         public getAnimatables(): IAnimatable[] {
@@ -202,24 +201,6 @@
             return true;
         }
 
-        protected setupSprite2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, origin: Vector2, texture: Texture, spriteSize: Size, spriteLocation: Vector2, invertY: boolean, alignToPixel: boolean, isVisible: boolean, marginTop: number, marginLeft: number, marginRight: number, marginBottom: number, vAlignment: number, hAlignment: number) {
-            this.setupRenderablePrim2D(owner, parent, id, position, origin, isVisible, marginTop, marginLeft, marginRight, marginBottom, hAlignment, vAlignment);
-            this.texture = texture;
-            this.texture.wrapU = Texture.CLAMP_ADDRESSMODE;
-            this.texture.wrapV = Texture.CLAMP_ADDRESSMODE;
-            this.spriteSize = spriteSize || null;
-            this.spriteLocation = spriteLocation || new Vector2(0,0);
-            this.spriteFrame = 0;
-            this.invertY = invertY;
-            this.alignToPixel = alignToPixel;
-            this._isTransparent = true;
-
-            if (!this.spriteSize) {
-                var s = texture.getSize();
-                this.spriteSize = new Size(s.width, s.height);
-            }
-        }
-
         /**
          * Create an 2D Sprite primitive
          * @param parent the parent primitive, must be a valid primitive (or the Canvas)
@@ -237,43 +218,62 @@
          *  - hAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
          *  - vAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
          */
-        public static Create(parent: Prim2DBase, texture: Texture, options: { id?: string, position?: Vector2, x?: number, y?: number, origin?: Vector2, spriteSize?: Size, spriteLocation?: Vector2, invertY?: boolean, alignToPixel?: boolean, isVisible?: boolean, marginTop?: number, marginLeft?: number, marginRight?: number, marginBottom?: number, vAlignment?: number, hAlignment?: number}): Sprite2D {
-            Prim2DBase.CheckParent(parent);
-
-            let sprite = new Sprite2D();
-            if (!options) {
-                sprite.setupSprite2D(parent.owner, parent, null, Vector2.Zero(), null, texture, null, null, false, true, true, null, null, null, null, null, null);
-            } else {
-                let pos = options.position || new Vector2(options.x || 0, options.y || 0);
-                sprite.setupSprite2D
-                (
-                    parent.owner,
-                    parent,
-                    options.id || null,
-                    pos,
-                    options.origin || null,
-                    texture, options.spriteSize || null,
-                    options.spriteLocation || null,
-                    (options.invertY == null) ? false : options.invertY,
-                    (options.alignToPixel == null) ? true : options.alignToPixel,
-                    (options.isVisible == null) ? true : options.isVisible,
-                    options.marginTop || null,
-                    options.marginLeft || null,
-                    options.marginRight || null,
-                    options.marginBottom || null,
-                    options.vAlignment || null,
-                    options.hAlignment || null
-                );
+        constructor(texture: Texture, settings?: {
+
+            parent           ?: Prim2DBase, 
+            children         ?: Array<Prim2DBase>,
+            id               ?: string,
+            position         ?: Vector2,
+            x                ?: number,
+            y                ?: number,
+            origin           ?: Vector2,
+            spriteSize       ?: Size,
+            spriteLocation   ?: Vector2,
+            invertY          ?: boolean,
+            alignToPixel     ?: boolean,
+            isVisible        ?: boolean,
+            marginTop        ?: number | string,
+            marginLeft       ?: number | string,
+            marginRight      ?: number | string,
+            marginBottom     ?: number | string,
+            margin           ?: number | string,
+            marginHAlignment ?: number,
+            marginVAlignment ?: number,
+            marginAlignment  ?: string,
+            paddingTop       ?: number | string,
+            paddingLeft      ?: number | string,
+            paddingRight     ?: number | string,
+            paddingBottom    ?: number | string,
+            padding          ?: string,
+            paddingHAlignment?: number,
+            paddingVAlignment?: number,
+        }) {
+
+            if (!settings) {
+                settings = {};
             }
 
-            return sprite;
+            super(settings);
+
+            this.texture = texture;
+            this.texture.wrapU = Texture.CLAMP_ADDRESSMODE;
+            this.texture.wrapV = Texture.CLAMP_ADDRESSMODE;
+            this.size = settings.spriteSize || null;
+            this.spriteLocation = settings.spriteLocation || new Vector2(0, 0);
+            this.spriteFrame = 0;
+            this.invertY = (settings.invertY == null) ? false : settings.invertY;
+            this.alignToPixel = (settings.alignToPixel == null) ? true : settings.alignToPixel;
+            this._isTransparent = true;
+
+            if (!this.size) {
+                var s = texture.getSize();
+                this.size = new Size(s.width, s.height);
+            }
         }
 
         static _createCachedCanvasSprite(owner: Canvas2D, texture: MapTexture, size: Size, pos: Vector2): Sprite2D {
 
-            let sprite = new Sprite2D();
-            sprite.setupSprite2D(owner, null, "__cachedCanvasSprite__", new Vector2(0, 0), null, texture, size, pos, false, true, true, null, null, null, null, null, null);
-
+            let sprite = new Sprite2D(texture, { parent: owner, id:"__cachedCanvasSprite__", position: Vector2.Zero(), origin: Vector2.Zero(), spriteSize: size, spriteLocation:pos, alignToPixel: true});
             return sprite;
         }
 
@@ -331,7 +331,7 @@
                 let d = <Sprite2DInstanceData>this._instanceDataParts[0];
                 let ts = this.texture.getBaseSize();
                 let sl = this.spriteLocation;
-                let ss = this.spriteSize;
+                let ss = this.actualSize;
                 d.topLeftUV = new Vector2(sl.x / ts.width, sl.y / ts.height);
                 let suv = new Vector2(ss.width / ts.width, ss.height / ts.height);
                 d.sizeUV = suv;
@@ -347,7 +347,6 @@
         }
 
         private _texture: Texture;
-        private _size: Size;
         private _location: Vector2;
         private _spriteFrame: number;
         private _invertY: boolean;

+ 71 - 60
src/Canvas2d/babylon.text2d.ts

@@ -120,7 +120,7 @@
         public static fontProperty: Prim2DPropInfo;
         public static defaultFontColorProperty: Prim2DPropInfo;
         public static textProperty: Prim2DPropInfo;
-        public static areaSizeProperty: Prim2DPropInfo;
+        public static sizeProperty: Prim2DPropInfo;
 
         @modelLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 1, pi => Text2D.fontProperty = pi, false, true)
         public get fontName(): string {
@@ -150,31 +150,42 @@
 
         public set text(value: string) {
             this._text = value;
-            this._actualSize = null;    // A change of text will reset the Actual Area Size which will be recomputed next time it's used
+            this._textSize = null;    // A change of text will reset the TextSize which will be recomputed next time it's used
+            this._size = null;
             this._updateCharCount();
+
+            // Trigger a textSize to for a sizeChange if necessary, which is needed for layout to recompute
+            let s = this.textSize;
         }
 
-        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 4, pi => Text2D.areaSizeProperty = pi)
-        public get areaSize(): Size {
-            return this._areaSize;
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 4, pi => Text2D.sizeProperty = pi)
+        public get size(): Size {
+            if (this._size != null) {
+                return this._size;
+            }
+            return this.textSize;
         }
 
-        public set areaSize(value: Size) {
-            this._areaSize = value;
+        public set size(value: Size) {
+            this._size = value;
         }
 
         public get actualSize(): Size {
-            if (this.areaSize) {
-                return this.areaSize;
-            }
-
             if (this._actualSize) {
                 return this._actualSize;
             }
+            return this.size;
+        }
 
-            this._actualSize = this.fontTexture.measureText(this._text, this._tabulationSize);
-
-            return this._actualSize;
+        public get textSize(): Size {
+            if (!this._textSize && this.owner) {
+                let newSize = this.fontTexture.measureText(this._text, this._tabulationSize);
+                if (newSize !== this._textSize) {
+                    this.onPrimitivePropertyDirty(Prim2DBase.sizeProperty.flagId);
+                }
+                this._textSize = newSize;
+            }
+            return this._textSize;
         }
 
         protected get fontTexture(): FontTexture {
@@ -200,18 +211,7 @@
         }
 
         protected updateLevelBoundingInfo() {
-            BoundingInfo2D.CreateFromSizeToRef(this.actualSize, this._levelBoundingInfo, this.origin);
-        }
-
-        protected setupText2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, origin: Vector2, fontName: string, text: string, areaSize: Size, defaultFontColor: Color4, tabulationSize: number, isVisible: boolean, marginTop: number, marginLeft: number, marginRight: number, marginBottom: number, vAlignment: number, hAlignment: number) {
-            this.setupRenderablePrim2D(owner, parent, id, position, origin, isVisible, marginTop, marginLeft, marginRight, marginBottom, hAlignment, vAlignment);
-
-            this.fontName = fontName;
-            this.defaultFontColor = defaultFontColor;
-            this.text = text;
-            this.areaSize = areaSize;
-            this._tabulationSize = tabulationSize;
-            this.isAlphaTest = true;
+            BoundingInfo2D.CreateFromSizeToRef(this.actualSize, this._levelBoundingInfo);
         }
 
         /**
@@ -231,35 +231,50 @@
          *  - hAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
          *  - vAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
          */
-        public static Create(parent: Prim2DBase, text: string, options?: { id?: string, position?: Vector2, x?: number, y?: number, origin?: Vector2, fontName?: string, defaultFontColor?: Color4, areaSize?: Size, tabulationSize?: number, isVisible?: boolean, marginTop?: number, marginLeft?: number, marginRight?: number, marginBottom?: number, hAlignment?: number, vAlignment?: number}): Text2D {
-            Prim2DBase.CheckParent(parent);
-
-            let text2d = new Text2D();
-            if (!options) {
-                text2d.setupText2D(parent.owner, parent, null, Vector2.Zero(), null, "12pt Arial", text, null, new Color4(1,1,1,1), 4, true, null, null, null, null, null, null);
-            } else {
-                let pos = options.position || new Vector2(options.x || 0, options.y || 0);
-                text2d.setupText2D
-                (
-                    parent.owner,
-                    parent,
-                    options.id || null,
-                    pos,
-                    options.origin || null,
-                    options.fontName || "12pt Arial",
-                    text,
-                    options.areaSize,
-                    options.defaultFontColor || new Color4(1, 1, 1, 1),
-                    (options.tabulationSize==null) ? 4 : options.tabulationSize,
-                    (options.isVisible==null) ? true : options.isVisible,
-                    options.marginTop || null,
-                    options.marginLeft || null,
-                    options.marginRight || null,
-                    options.marginBottom || null,
-                    options.vAlignment || null,
-                    options.hAlignment || null);
+        constructor(text: string, settings?: {
+
+            parent           ?: Prim2DBase, 
+            children         ?: Array<Prim2DBase>,
+            id               ?: string,
+            position         ?: Vector2,
+            x                ?: number,
+            y                ?: number,
+            origin           ?: Vector2,
+            fontName         ?: string,
+            defaultFontColor ?: Color4,
+            size             ?: Size,
+            tabulationSize   ?: number,
+            isVisible        ?: boolean,
+            marginTop        ?: number | string,
+            marginLeft       ?: number | string,
+            marginRight      ?: number | string,
+            marginBottom     ?: number | string,
+            margin           ?: number | string,
+            marginHAlignment ?: number,
+            marginVAlignment ?: number,
+            marginAlignment  ?: string,
+            paddingTop       ?: number | string,
+            paddingLeft      ?: number | string,
+            paddingRight     ?: number | string,
+            paddingBottom    ?: number | string,
+            padding          ?: string,
+            paddingHAlignment?: number,
+            paddingVAlignment?: number,
+        }) {
+
+            if (!settings) {
+                settings = {};
             }
-            return text2d;
+
+            super(settings);
+
+            this.fontName         = (settings.fontName==null)         ? "12pt Arial"        : settings.fontName;
+            this.defaultFontColor = (settings.defaultFontColor==null) ? new Color4(1,1,1,1) : settings.defaultFontColor;
+            this._tabulationSize  = (settings.tabulationSize == null) ? 4 : settings.tabulationSize;
+            this._textSize        = null;
+            this.text             = text;
+            this.size             = (settings.size==null) ? null : settings.size;
+            this.isAlphaTest      = true;
         }
 
         protected levelIntersect(intersectInfo: IntersectInfo2D): boolean {
@@ -337,7 +352,6 @@
                 let d = <Text2DInstanceData>part;
                 let texture = this.fontTexture;
                 let ts = texture.getSize();
-                let textSize = texture.measureText(this.text, this._tabulationSize);
                 let offset = Vector2.Zero();
                 let charxpos = 0;
                 d.dataElementCount = this._charCount;
@@ -364,7 +378,7 @@
                         continue;
                     }
 
-                    this.updateInstanceDataPart(d, offset, textSize);
+                    this.updateInstanceDataPart(d, offset);
 
                     let ci = texture.getChar(char);
                     offset.x += ci.charWidth;
@@ -398,10 +412,7 @@
         private _fontName: string;
         private _defaultFontColor: Color4;
         private _text: string;
-        private _areaSize: Size;
-        private _actualSize: Size;
-        private _vAlign: number;
-        private _hAlign: number;
+        private _textSize: Size;
     }
 
 

+ 1 - 1
src/Canvas2d/babylon.worldSpaceCanvas2d.ts

@@ -2,7 +2,7 @@
     /**
      * This is the class that is used to display a World Space Canvas into a scene
      */
-    export class WorldSpaceCanvas2D extends Mesh {
+    export class WorldSpaceCanvas2DNode extends Mesh {
         constructor(name: string, scene: Scene, canvas: Canvas2D) {
             super(name, scene);
 

+ 5 - 0
src/Math/babylon.math.ts

@@ -1708,6 +1708,11 @@
             return hash;
         }
 
+        public copyFrom(src: Size) {
+            this.width  = src.width;
+            this.height = src.height;
+        }
+
         public clone(): Size {
             return new Size(this.width, this.height);
         }

+ 1 - 2
src/Shaders/ellipse2d.vertex.fx

@@ -9,7 +9,6 @@ attribute float index;
 att vec2 zBias;
 att vec4 transformX;
 att vec4 transformY;
-att vec2 origin;
 
 #ifdef Border
 att float borderThickness;
@@ -100,7 +99,7 @@ void main(void) {
 #endif
 
 	vec4 pos;
-	pos.xy = (pos2.xy - origin) * properties.xy;
+	pos.xy = pos2.xy * properties.xy;
 	pos.z = 1.0;
 	pos.w = 1.0;
 	gl_Position = vec4(dot(pos, transformX), dot(pos, transformY), zBias.x, 1);

+ 3 - 4
src/Shaders/lines2d.vertex.fx

@@ -9,9 +9,6 @@ attribute vec2 position;
 att vec2 zBias;
 att vec4 transformX;
 att vec4 transformY;
-att vec2 origin;
-att vec2 boundingMin;
-att vec2 boundingMax;
 
 #ifdef FillSolid
 att vec4 fillSolidColor;
@@ -22,6 +19,8 @@ att vec4 borderSolidColor;
 #endif
 
 #ifdef FillGradient
+att vec2 boundingMin;
+att vec2 boundingMax;
 att vec4 fillGradientColor1;
 att vec4 fillGradientColor2;
 att vec4 fillGradientTY;
@@ -60,7 +59,7 @@ void main(void) {
 #endif
 
 	vec4 pos;
-	pos.xy = position.xy - (origin.xy * (boundingMax - boundingMin));
+	pos.xy = position.xy;
 	pos.z = 1.0;
 	pos.w = 1.0;
 	gl_Position = vec4(dot(pos, transformX), dot(pos, transformY), zBias.x, 1);

+ 1 - 2
src/Shaders/rect2d.vertex.fx

@@ -9,7 +9,6 @@ attribute float index;
 att vec2 zBias;
 att vec4 transformX;
 att vec4 transformY;
-att vec2 origin;
 
 #ifdef Border
 att float borderThickness;
@@ -198,7 +197,7 @@ void main(void) {
 #endif
 
 	vec4 pos;
-	pos.xy = (pos2.xy - origin) * properties.xy;
+	pos.xy = pos2.xy * properties.xy;
 	pos.z = 1.0;
 	pos.w = 1.0;
 	gl_Position = vec4(dot(pos, transformX), dot(pos, transformY), zBias.x, 1);

+ 2 - 3
src/Shaders/sprite2d.vertex.fx

@@ -10,7 +10,6 @@ attribute float index;
 
 att vec2 topLeftUV;
 att vec2 sizeUV;
-att vec2 origin;
 att vec2 textureSize;
 
 // x: frame, y: invertY, z: alignToPixel
@@ -68,9 +67,9 @@ void main(void) {
 	vec4 pos;
 	if (alignToPixel == 1.0)
 	{
-		pos.xy = floor((pos2.xy * sizeUV * textureSize) - origin);
+		pos.xy = floor(pos2.xy * sizeUV * textureSize);
 	} else {
-		pos.xy = (pos2.xy * sizeUV * textureSize) - origin;
+		pos.xy = pos2.xy * sizeUV * textureSize;
 	}
 
 	pos.z = 1.0;

+ 1 - 2
src/Shaders/text2d.vertex.fx

@@ -14,7 +14,6 @@ att vec4 transformY;
 
 att vec2 topLeftUV;
 att vec2 sizeUV;
-att vec2 origin;
 att vec2 textureSize;
 att vec4 color;
 
@@ -52,7 +51,7 @@ void main(void) {
 
 	vColor = color;
 	vec4 pos;
-	pos.xy = (pos2.xy * sizeUV * textureSize) - origin;
+	pos.xy = floor(pos2.xy * sizeUV * textureSize);	// Align on target pixel to avoid bad interpolation
 	pos.z = 1.0;
 	pos.w = 1.0;
 	gl_Position = vec4(dot(pos, transformX), dot(pos, transformY), zBias.x, 1);

+ 1 - 1
src/babylon.engine.js

@@ -2135,4 +2135,4 @@ var BABYLON;
         return Engine;
     })();
     BABYLON.Engine = Engine;
-})(BABYLON || (BABYLON = {}));
+})(BABYLON || (BABYLON = {}));