Переглянути джерело

Canvas2D: Primitive Collision Manager (WIP) and WireFrame2D Prim

 - BoundingInfo2D now has a worldAABB, with dirty/update mechanism
 - Canvas2D now support a Primitive Collision Manager (PCM)
 - A ModelRenderCache now supports update of its model data
 - Prim2DBase has a default Collision Shape which is the bounding rect of the prim.
 - SmartPropertyPrim has a uid property that returns a unique id.
 - New type of Primitive: WireFrame2D to render a multiple group of Wire Lines List.
 - math2D.ts file added in the Tools, contains a Math2D class with static helpers
 - fix a bug in ObservableDictionary
nockawa 8 роки тому
батько
коміт
3e73d6ea44

+ 3 - 0
Tools/Gulp/config.json

@@ -464,11 +464,13 @@
     "libraries": [  
     {
       "files": [
+        "../../canvas2D/src/Tools/babylon.math2D.ts",
         "../../canvas2D/src/Tools/babylon.IPropertyChanged.ts",
         "../../canvas2D/src/Tools/babylon.observableArray.ts",
         "../../canvas2D/src/Tools/babylon.observableStringDictionary.ts",
         "../../canvas2D/src/Engine/babylon.fontTexture.ts",
         "../../canvas2D/src/Engine/babylon.bounding2d.ts",
+        "../../canvas2D/src/Engine/babylon.primitiveCollisionManager.ts",
         "../../canvas2D/src/Engine/babylon.canvas2dLayoutEngine.ts",
         "../../canvas2D/src/Engine/babylon.brushes2d.ts",
         "../../canvas2D/src/Engine/babylon.smartPropertyPrim.ts",
@@ -477,6 +479,7 @@
         "../../canvas2D/src/Engine/babylon.renderablePrim2d.ts",
         "../../canvas2D/src/Engine/babylon.shape2d.ts",
         "../../canvas2D/src/Engine/babylon.group2d.ts",
+        "../../canvas2D/src/Engine/babylon.wireFrame2d.ts",
         "../../canvas2D/src/Engine/babylon.rectangle2d.ts",
         "../../canvas2D/src/Engine/babylon.ellipse2d.ts",
         "../../canvas2D/src/Engine/babylon.sprite2d.ts",

+ 80 - 0
canvas2D/src/Engine/babylon.bounding2d.ts

@@ -26,6 +26,9 @@
             this.radius = 0;
             this.center = Vector2.Zero();
             this.extent = Vector2.Zero();
+            this._worldAABBDirty = false;
+            this._worldAABBDirtyObservable = null;
+            this._worldAABB = Vector4.Zero();
         }
 
         /**
@@ -74,6 +77,7 @@
             b.extent.x = b.center.x;
             b.extent.y = b.center.y;
             b.radius = b.extent.length();
+            b._worldAABBDirty = true;
         }
 
         /**
@@ -87,6 +91,7 @@
             b.extent.x = r;
             b.extent.y = r;
             b.radius = r;
+            b._worldAABBDirty = true;
         }
 
         /**
@@ -119,6 +124,7 @@
             b.center = new Vector2(xmin + w / 2, ymin + h / 2);
             b.extent = new Vector2(xmax - b.center.x, ymax - b.center.y);
             b.radius = b.extent.length();
+            b._worldAABBDirty = true;
         }
 
         /**
@@ -137,12 +143,14 @@
             this.center.copyFromFloats(0, 0);
             this.radius = 0;
             this.extent.copyFromFloats(0, 0);
+            this._worldAABBDirty = true;
         }
 
         public copyFrom(src: BoundingInfo2D) {
             this.center.copyFrom(src.center);
             this.radius = src.radius;
             this.extent.copyFrom(src.extent);
+            this._worldAABBDirty = true;
         }
 
         /**
@@ -185,6 +193,13 @@
             return r;
         }
 
+        public worldAABBIntersectionTest(other: BoundingInfo2D): boolean {
+            let a = this.worldAABB;
+            let b = other.worldAABB;
+
+            return b.z >= a.x && b.x <= a.z && b.w >= a.y && b.y <= a.w;
+        }
+
         private static _transform: Array<Vector2> = new Array<Vector2>(Vector2.Zero(), Vector2.Zero(), Vector2.Zero(), Vector2.Zero());
 
         /**
@@ -212,6 +227,67 @@
             BoundingInfo2D.CreateFromPointsToRef(p, result);
         }
 
+        private _updateWorldAABB(worldMatrix: Matrix) {
+            // Construct a bounding box based on the extent values
+            let p = BoundingInfo2D._transform;
+            p[0].x = this.center.x + this.extent.x;
+            p[0].y = this.center.y + this.extent.y;
+            p[1].x = this.center.x + this.extent.x;
+            p[1].y = this.center.y - this.extent.y;
+            p[2].x = this.center.x - this.extent.x;
+            p[2].y = this.center.y - this.extent.y;
+            p[3].x = this.center.x - this.extent.x;
+            p[3].y = this.center.y + this.extent.y;
+
+            // Transform the four points of the bounding box with the matrix
+            for (let i = 0; i < 4; i++) {
+                Vector2.TransformToRef(p[i], worldMatrix, p[i]);
+            }
+
+            this._worldAABB.x = Math.min(Math.min(p[0].x, p[1].x), Math.min(p[2].x, p[3].x));
+            this._worldAABB.y = Math.min(Math.min(p[0].y, p[1].y), Math.min(p[2].y, p[3].y));
+            this._worldAABB.z = Math.max(Math.max(p[0].x, p[1].x), Math.max(p[2].x, p[3].x));
+            this._worldAABB.w = Math.max(Math.max(p[0].y, p[1].y), Math.max(p[2].y, p[3].y));
+        }
+
+        public worldMatrixAccess: () => Matrix;
+
+        public get worldAABBDirtyObservable(): Observable<BoundingInfo2D> {
+            if (!this._worldAABBDirtyObservable) {
+                this._worldAABBDirtyObservable = new Observable<BoundingInfo2D>();
+            }
+            return this._worldAABBDirtyObservable;
+        }
+
+        public get isWorldAABBDirty() {
+            return this._worldAABBDirty;
+        }
+
+        public dirtyWorldAABB() {
+            if (this._worldAABBDirty) {
+                return;
+            }
+
+            this._worldAABBDirty = true;
+            if (this._worldAABBDirtyObservable && this._worldAABBDirtyObservable.hasObservers()) {
+                this._worldAABBDirtyObservable.notifyObservers(this);
+            }
+        }
+
+        /**
+         * Retrieve the world AABB, the Vector4's data is x=xmin, y=ymin, z=xmax, w=ymax
+         */
+        public get worldAABB(): Vector4 {
+            if (this._worldAABBDirty) {
+                if (!this.worldMatrixAccess) {
+                    throw new Error("you must set the worldMatrixAccess function first");
+                }
+                this._updateWorldAABB(this.worldMatrixAccess());
+                this._worldAABBDirty = false;
+            }
+            return this._worldAABB;
+        }
+
         /**
          * Compute the union of this BoundingInfo2D with another one and store the result in a third valid BoundingInfo2D object
          * This is a GC friendly version, try to use it as much as possible, specially if your transformation is inside a loop, allocate the result object once for good outside of the loop and use it every time.
@@ -241,5 +317,9 @@
             }
             return false;
         }
+
+        private _worldAABBDirtyObservable: Observable<BoundingInfo2D>;
+        private _worldAABBDirty: boolean;
+        private _worldAABB: Vector4;
     }
 }

+ 60 - 55
canvas2D/src/Engine/babylon.canvas2d.ts

@@ -66,21 +66,24 @@
         private static _INSTANCES : Array<Canvas2D> = [];
 
         constructor(scene: Scene, settings?: {
-            id?: string,
-            children?: Array<Prim2DBase>,
-            size?: Size,
-            renderingPhase?: { camera: Camera, renderingGroupID: number },
-            designSize?: Size,
-            designUseHorizAxis?: boolean,
-            isScreenSpace?: boolean,
-            cachingStrategy?: number,
-            enableInteraction?: boolean,
-            origin?: Vector2,
-            isVisible?: boolean,
-            backgroundRoundRadius?: number,
-            backgroundFill?: IBrush2D | string,
-            backgroundBorder?: IBrush2D | string,
-            backgroundBorderThickNess?: number,
+            id                            ?: string,
+            children                      ?: Array<Prim2DBase>,
+            size                          ?: Size,
+            renderingPhase                ?: { camera: Camera, renderingGroupID: number },
+            designSize                    ?: Size,
+            designUseHorizAxis            ?: boolean,
+            isScreenSpace                 ?: boolean,
+            cachingStrategy               ?: number,
+            enableInteraction             ?: boolean,
+            enableCollisionManager        ?: boolean,
+            customCollisionManager        ?: (owner: Canvas2D, enableBorders: boolean) => PirimitiveCollisionManagerBase,
+            collisionManagerUseBorders    ?: boolean,
+            origin                        ?: Vector2,
+            isVisible                     ?: boolean,
+            backgroundRoundRadius         ?: number,
+            backgroundFill                ?: IBrush2D | string,
+            backgroundBorder              ?: IBrush2D | string,
+            backgroundBorderThickNess     ?: number,
         }) {
             super(settings);
 
@@ -97,7 +100,6 @@
             this._updateGlobalTransformCounter = new PerfCounter();
             this._boundingInfoRecomputeCounter = new PerfCounter();
 
-            this._uid = null;
             this._cachedCanvasGroup = null;
 
             this._renderingGroupObserver = null;
@@ -173,7 +175,6 @@
             }
             this._maxAdaptiveWorldSpaceCanvasSize = null;
             this._groupCacheMaps = new StringDictionary<MapTexture[]>();
-
             this._patchHierarchy(this);
 
             let enableInteraction = (settings.enableInteraction == null) ? true : settings.enableInteraction;
@@ -214,8 +215,15 @@
             this._supprtInstancedArray = this._engine.getCaps().instancedArrays !== null;
                         //this._supprtInstancedArray = false; // TODO REMOVE!!!
 
+            // Setup the canvas for interaction (or not)
             this._setupInteraction(enableInteraction);
 
+            // Initialize the Primitive Collision Manager
+            if (settings.enableCollisionManager) {
+                let enableBorders = settings.collisionManagerUseBorders;
+                this._primitiveCollisionManager = (settings.customCollisionManager==null) ? new BasicPrimitiviceCollisionManager(this, enableBorders) : settings.customCollisionManager(this, enableBorders);
+            }
+
             // Register this instance
             Canvas2D._INSTANCES.push(this);
         }
@@ -895,16 +903,6 @@
         }
 
         /**
-         * return a unique identifier for the Canvas2D
-         */
-        public get uid(): string {
-            if (!this._uid) {
-                this._uid = Tools.RandomId();
-            }
-            return this._uid;
-        }
-
-        /**
          * And observable called during the Canvas rendering process.
          * This observable is called twice per render, each time with a different mask:
          *  - 1: before render is executed
@@ -1212,7 +1210,6 @@
             this._updateGlobalTransformCounter.addCount(count, false);
         }
 
-        private _uid: string;
         private _renderObservable: Observable<Canvas2D>;
         private __engineData: Canvas2DEngineBoundData;
         private _interactionEnabled: boolean;
@@ -1249,6 +1246,7 @@
         protected _maxAdaptiveWorldSpaceCanvasSize: number;
         private _designSize: Size;
         private _designUseHorizAxis: boolean;
+        public  _primitiveCollisionManager: PirimitiveCollisionManagerBase;
 
         public _renderingSize: Size;
 
@@ -1375,8 +1373,8 @@
 
             // If the canvas fit the rendering size and it changed, update
             if (renderingSizeChanged && this._fitRenderingDevice) {
-                this._actualSize = this._renderingSize.clone();
-                this._size = this._renderingSize.clone();
+                this.actualSize = this._renderingSize.clone();
+                this.size = this._renderingSize.clone();
                 if (this._background) {
                     this._background.size = this.size;
                 }
@@ -1434,6 +1432,10 @@
 
             this._updateCanvasState(false);
 
+            if (this._primitiveCollisionManager) {
+                this._primitiveCollisionManager.update();
+            }
+
             if (this._primPointerInfo.canvasPointerPos) {
                 this._updateIntersectionList(this._primPointerInfo.canvasPointerPos, false, false);
                 this._updateOverStatus(false);
@@ -1877,31 +1879,34 @@
          */
         constructor(scene: Scene, settings?: {
 
-            children?: Array<Prim2DBase>,
-            id?: string,
-            x?: number,
-            y?: number,
-            position?: Vector2,
-            origin?: Vector2,
-            width?: number,
-            height?: number,
-            size?: Size,
-            renderingPhase?: {camera: Camera, renderingGroupID: number },
-            designSize?: Size,
-            designUseHorizAxis?: boolean,
-            cachingStrategy?: number,
-            cacheBehavior?: number,
-            enableInteraction?: boolean,
-            isVisible?: boolean,
-            backgroundRoundRadius?: number,
-            backgroundFill?: IBrush2D | string,
-            backgroundBorder?: IBrush2D | string,
-            backgroundBorderThickNess?: number,
-            paddingTop?: number | string,
-            paddingLeft?: number | string,
-            paddingRight?: number | string,
-            paddingBottom?: number | string,
-            padding?: string,
+            children                   ?: Array<Prim2DBase>,
+            id                         ?: string,
+            x                          ?: number,
+            y                          ?: number,
+            position                   ?: Vector2,
+            origin                     ?: Vector2,
+            width                      ?: number,
+            height                     ?: number,
+            size                       ?: Size,
+            renderingPhase             ?: {camera: Camera, renderingGroupID: number },
+            designSize                 ?: Size,
+            designUseHorizAxis         ?: boolean,
+            cachingStrategy            ?: number,
+            cacheBehavior              ?: number,
+            enableInteraction          ?: boolean,
+            enableCollisionManager     ?: boolean,
+            customCollisionManager     ?: (owner: Canvas2D, enableBorders: boolean) => PirimitiveCollisionManagerBase,
+            collisionManagerUseBorders ?: boolean,
+            isVisible                  ?: boolean,
+            backgroundRoundRadius      ?: number,
+            backgroundFill             ?: IBrush2D | string,
+            backgroundBorder           ?: IBrush2D | string,
+            backgroundBorderThickNess  ?: number,
+            paddingTop                 ?: number | string,
+            paddingLeft                ?: number | string,
+            paddingRight               ?: number | string,
+            paddingBottom              ?: number | string,
+            padding                    ?: string,
 
         }) {
             Prim2DBase._isCanvasInit = true;

+ 33 - 0
canvas2D/src/Engine/babylon.ellipse2d.ts

@@ -229,6 +229,8 @@
          * - isPickable: if true the Primitive can be used with interaction mode and will issue Pointer Event. If false it will be ignored for interaction/intersection test. Default value is true.
          * - isContainer: if true the Primitive acts as a container for interaction, if the primitive is not pickable or doesn't intersection, no further test will be perform on its children. If set to false, children will always be considered for intersection/interaction. Default value is true.
          * - childrenFlatZOrder: if true all the children (direct and indirect) will share the same Z-Order. Use this when there's a lot of children which don't overlap. The drawing order IS NOT GUARANTED!
+         * - levelCollision: this primitive is an actor of the Collision Manager and only this level will be used for collision (i.e. not the children). Use deepCollision if you want collision detection on the primitives and its children.
+         * - deepCollision: this primitive is an actor of the Collision Manager, this level AND ALSO its children will be used for collision (note: you don't need to set the children as level/deepCollision).
          * - marginTop: top margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginLeft: left margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginRight: right margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
@@ -270,6 +272,8 @@
             isPickable            ?: boolean,
             isContainer           ?: boolean,
             childrenFlatZOrder    ?: boolean,
+            levelCollision        ?: boolean,
+            deepCollision         ?: boolean,
             marginTop             ?: number | string,
             marginLeft            ?: number | string,
             marginRight           ?: number | string,
@@ -304,6 +308,35 @@
             this.subdivisions = sub;
         }
 
+        protected updateTriArray() {
+            let subDiv = this._subdivisions;
+            if (this._primTriArray == null) {
+                this._primTriArray = new Tri2DArray(subDiv);
+            } else {
+                this._primTriArray.clear(subDiv);
+            }
+
+            let size = this.actualSize;
+            let center = new Vector2(0.5 * size.width, 0.5 * size.height);
+            let v1 = Vector2.Zero();
+            let v2 = Vector2.Zero();
+
+            for (let i = 0; i < subDiv; i++) {
+	            let angle1 = Math.PI * 2 * (i-1) / subDiv;
+	            let angle2 = Math.PI * 2 * i / subDiv;
+
+                v1.x = ((Math.cos(angle1) / 2.0) + 0.5) * size.width;
+                v1.y = ((Math.sin(angle1) / 2.0) + 0.5) * size.height;
+
+                v2.x = ((Math.cos(angle2) / 2.0) + 0.5) * size.width;
+                v2.y = ((Math.sin(angle2) / 2.0) + 0.5) * size.height;
+
+                this._primTriArray.storeTriangle(i, center, v1, v2);
+
+            }
+
+        }
+
         protected createModelRenderCache(modelKey: string): ModelRenderCache {
             let renderCache = new Ellipse2DRenderCache(this.owner.engine, modelKey);
             return renderCache;

+ 4 - 0
canvas2D/src/Engine/babylon.group2d.ts

@@ -53,6 +53,8 @@
          * - isPickable: if true the Primitive can be used with interaction mode and will issue Pointer Event. If false it will be ignored for interaction/intersection test. Default value is true.
          * - isContainer: if true the Primitive acts as a container for interaction, if the primitive is not pickable or doesn't intersection, no further test will be perform on its children. If set to false, children will always be considered for intersection/interaction. Default value is true.
          * - childrenFlatZOrder: if true all the children (direct and indirect) will share the same Z-Order. Use this when there's a lot of children which don't overlap. The drawing order IS NOT GUARANTED!
+         * - levelCollision: this primitive is an actor of the Collision Manager and only this level will be used for collision (i.e. not the children). Use deepCollision if you want collision detection on the primitives and its children.
+         * - deepCollision: this primitive is an actor of the Collision Manager, this level AND ALSO its children will be used for collision (note: you don't need to set the children as level/deepCollision).
          * - marginTop: top margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginLeft: left margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginRight: right margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
@@ -92,6 +94,8 @@
             isPickable              ?: boolean,
             isContainer             ?: boolean,
             childrenFlatZOrder      ?: boolean,
+            levelCollision          ?: boolean,
+            deepCollision           ?: boolean,
             marginTop               ?: number | string,
             marginLeft              ?: number | string,
             marginRight             ?: number | string,

+ 4 - 0
canvas2D/src/Engine/babylon.lines2d.ts

@@ -401,6 +401,8 @@
          * - isPickable: if true the Primitive can be used with interaction mode and will issue Pointer Event. If false it will be ignored for interaction/intersection test. Default value is true.
          * - isContainer: if true the Primitive acts as a container for interaction, if the primitive is not pickable or doesn't intersection, no further test will be perform on its children. If set to false, children will always be considered for intersection/interaction. Default value is true.
          * - childrenFlatZOrder: if true all the children (direct and indirect) will share the same Z-Order. Use this when there's a lot of children which don't overlap. The drawing order IS NOT GUARANTED!
+         * - levelCollision: this primitive is an actor of the Collision Manager and only this level will be used for collision (i.e. not the children). Use deepCollision if you want collision detection on the primitives and its children.
+         * - deepCollision: this primitive is an actor of the Collision Manager, this level AND ALSO its children will be used for collision (note: you don't need to set the children as level/deepCollision).
          * - marginTop: top margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginLeft: left margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginRight: right margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
@@ -442,6 +444,8 @@
             isPickable            ?: boolean,
             isContainer           ?: boolean,
             childrenFlatZOrder    ?: boolean,
+            levelCollision        ?: boolean,
+            deepCollision         ?: boolean,
             marginTop             ?: number | string,
             marginLeft            ?: number | string,
             marginRight           ?: number | string,

+ 4 - 0
canvas2D/src/Engine/babylon.modelRenderCache.ts

@@ -231,6 +231,10 @@
             return this._modelKey;
         }
 
+        public updateModelRenderCache(prim: Prim2DBase): boolean {
+            return false;
+        }
+
         /**
          * Render the model instances
          * @param instanceInfo

+ 63 - 5
canvas2D/src/Engine/babylon.prim2dBase.ts

@@ -1383,6 +1383,8 @@
             isPickable              ?: boolean,
             isContainer             ?: boolean,
             childrenFlatZOrder      ?: boolean,
+            levelCollision          ?: boolean,
+            deepCollision           ?: boolean,
             marginTop               ?: number | string,
             marginLeft              ?: number | string,
             marginRight             ?: number | string,
@@ -1443,7 +1445,6 @@
             this._lastAutoSizeArea = Size.Zero();
             this._contentArea = new Size(null, null);
             this._pointerEventObservable = new Observable<PrimitivePointerInfo>();
-            this._boundingInfo = new BoundingInfo2D();
             this._owner = owner;
             this._parent = null;
             this._margin = null;
@@ -1466,6 +1467,11 @@
             this._actualScale = Vector2.Zero();
             this._displayDebugAreas = false;
             this._debugAreaGroup = null;
+            this._primTriArray = null;
+            this._primTriArrayDirty = true;
+
+            this._levelBoundingInfo.worldMatrixAccess = () => this.globalTransform;
+            this._boundingInfo.worldMatrixAccess = () => this.globalTransform;
 
             let isPickable = true;
             let isContainer = true;
@@ -1615,6 +1621,13 @@
             // Dirty layout and positioning
             this._parentLayoutDirty();
             this._positioningDirty();
+
+            // Add in the PCM
+            if (settings.levelCollision || settings.deepCollision) {
+                this.owner._primitiveCollisionManager.addActor(this, settings.deepCollision===true);
+                this._setFlags(SmartPropertyPrim.flagCollisionActor);
+            }
+
         }
 
         public get actionManager(): ActionManager {
@@ -2509,7 +2522,9 @@
          * Get the global transformation matrix of the primitive
          */
         public get globalTransform(): Matrix {
-            this._updateLocalTransform();
+            if (this._globalTransformProcessStep !== this.owner._globalTransformProcessStep) {
+                this.updateCachedStates(false);
+            }
             return this._globalTransform;
         }
 
@@ -2944,12 +2959,12 @@
             return this.owner._releasePointerCapture(pointerId, this);
         }
 
+        private static _bypassGroup2DExclusion = false;
+
         /**
          * Make an intersection test with the primitive, all inputs/outputs are stored in the IntersectInfo2D class, see its documentation for more information.
          * @param intersectInfo contains the settings of the intersection to perform, to setup before calling this method as well as the result, available after a call to this method.
          */
-        private static _bypassGroup2DExclusion = false;
-
         public intersect(intersectInfo: IntersectInfo2D): boolean {
             if (!intersectInfo) {
                 return false;
@@ -3049,6 +3064,38 @@
             return intersectInfo.isIntersected;
         }
 
+        public intersectOtherPrim(other: Prim2DBase): boolean {
+            let setA = this.triList;
+            let setB = other.triList;
+
+            return Tri2DArray.doesIntersect(setA, setB, other.globalTransform.multiply(this.globalTransform.clone().invert()));
+        }
+
+        public get triList(): Tri2DArray {
+            if (this._primTriArrayDirty) {
+                this.updateTriArray();
+                this._primTriArrayDirty = false;
+            }
+            return this._primTriArray;
+        }
+
+        // This is the worst implementation, if the top level primitive doesn't override this method we will just store a quad that defines the bounding rect of the prim
+        protected updateTriArray() {
+            if (this._primTriArray == null) {
+                this._primTriArray = new Tri2DArray(2);
+            } else {
+                this._primTriArray.clear(2);
+            }
+
+            let size = this.actualSize;
+            let lb = new Vector2(0, 0);
+            let rt = new Vector2(size.width, size.height);
+            let lt = new Vector2(0, size.height);
+            let rb = new Vector2(size.width, 0);
+            this._primTriArray.storeTriangle(0, lb, lt, rt);
+            this._primTriArray.storeTriangle(1, lb, rt, rb);
+        }
+
         /**
          * Move a child object into a new position regarding its siblings to change its rendering order.
          * You can also use the shortcut methods to move top/bottom: moveChildToTop, moveChildToBottom, moveToTop, moveToBottom.
@@ -3131,6 +3178,10 @@
                 return false;
             }
 
+            if (this._isFlagSet(SmartPropertyPrim.flagCollisionActor)) {
+                this.owner._primitiveCollisionManager.removeActor(this);
+            }
+
             if (this._pointerEventObservable) {
                 this._pointerEventObservable.clear();
                 this._pointerEventObservable = null;
@@ -3183,7 +3234,7 @@
         }
 
         public _needPrepare(): boolean {
-            return this._areSomeFlagsSet(SmartPropertyPrim.flagVisibilityChanged | SmartPropertyPrim.flagModelDirty | SmartPropertyPrim.flagNeedRefresh) || (this._instanceDirtyFlags !== 0) || (this._globalTransformProcessStep !== this._globalTransformStep);
+            return this._areSomeFlagsSet(SmartPropertyPrim.flagVisibilityChanged | SmartPropertyPrim.flagModelDirty | SmartPropertyPrim.flagModelUpdate | SmartPropertyPrim.flagNeedRefresh) || (this._instanceDirtyFlags !== 0) || (this._globalTransformProcessStep !== this._globalTransformStep);
         }
 
         public _prepareRender(context: PrepareRender2DContext) {
@@ -3431,6 +3482,9 @@
 
                     this._invGlobalTransform = Matrix.Invert(this._globalTransform);
 
+                    this._levelBoundingInfo.dirtyWorldAABB();
+                    this._boundingInfo.dirtyWorldAABB();
+
                     this._globalTransformStep = this.owner._globalTransformProcessStep + 1;
                     this._parentTransformStep = this._parent ? this._parent._globalTransformStep : 0;
                     this._clearFlags(SmartPropertyPrim.flagGlobalTransformDirty);
@@ -3819,6 +3873,10 @@
         protected _localTransform: Matrix;
         protected _globalTransform: Matrix;
         protected _invGlobalTransform: Matrix;
+
+        // Intersection related data
+        protected _primTriArrayDirty: boolean;
+        protected _primTriArray: Tri2DArray;
     }
 
 }

+ 617 - 0
canvas2D/src/Engine/babylon.primitiveCollisionManager.ts

@@ -0,0 +1,617 @@
+module BABYLON {
+    export abstract class PirimitiveCollisionManagerBase {
+        constructor(owner: Canvas2D) {
+            this._owner = owner;
+        }
+
+        abstract addActor(actor: Prim2DBase, deep: boolean);
+        abstract removeActor(actor: Prim2DBase);
+
+        abstract update();
+
+        abstract get leftBorderIntersectedActors(): ObservableStringDictionary<Prim2DBase>;
+        abstract get bottomBorderIntersectedActors(): ObservableStringDictionary<Prim2DBase>;
+        abstract get rightBorderIntersectedActors(): ObservableStringDictionary<Prim2DBase>;
+        abstract get topBorderIntersectedActors(): ObservableStringDictionary<Prim2DBase>;
+        abstract get intersectedActors(): ObservableStringDictionary<{ a: Prim2DBase, b: Prim2DBase }>;
+
+        protected _owner: Canvas2D;
+
+    }
+
+    class ActorInfo {
+        constructor(owner: BasicPrimitiviceCollisionManager, actor: Prim2DBase, deep: boolean) {
+            this.owner = owner;
+            this.prim = actor;
+            this.flags = 0;
+            this.presentInClusters = new StringDictionary<ClusterInfo>();
+            this.intersectWith = new StringDictionary<ActorInfo>();
+            this.setFlags((deep ? ActorInfo.flagDeep : 0) | ActorInfo.flagDirty);
+
+            let bi = (deep ? actor.boundingInfo : actor.levelBoundingInfo);
+
+            // Dirty Actor if its WorldAABB changed
+            bi.worldAABBDirtyObservable.add((e, d) => {
+                this.owner.actorDirty(this);
+            });
+
+            // Dirty Actor if it's getting enabled/disabled
+            actor.propertyChanged.add((e, d) => {
+                if (d.mask === -1) {
+                    return;
+                }
+                this.setFlagsValue(ActorInfo.flagEnabled, e.newValue === true);
+                this.owner.actorDirty(this);
+            }, Prim2DBase.isVisibleProperty.flagId);
+        }
+
+        setFlags(flags: number) {
+            this.flags |= flags;
+        }
+
+        clearFlags(flags: number) {
+            this.flags &= ~flags;
+        }
+
+        isAllFlagsSet(flags: number) {
+            return (this.flags & flags) === flags;
+        }
+
+        isSomeFlagsSet(flags: number) {
+            return (this.flags & flags) !== 0;
+        }
+
+        setFlagsValue(flags: number, value: boolean) {
+            if (value) {
+                this.flags |= flags;
+            } else {
+                this.flags &= ~flags;
+            }
+        }
+
+        get worldAABB(): Vector4 {
+            return (this.isSomeFlagsSet(ActorInfo.flagDeep) ? this.prim.boundingInfo : this.prim.levelBoundingInfo).worldAABB;
+        }
+
+        get isEnabled(): boolean {
+            return this.isSomeFlagsSet(ActorInfo.flagEnabled);
+        }
+
+        get isDeep(): boolean {
+            return this.isSomeFlagsSet(ActorInfo.flagDeep);
+        }
+
+        get isDirty(): boolean {
+            return this.isSomeFlagsSet(ActorInfo.flagDirty);
+        }
+
+        get isRemoved(): boolean {
+            return this.isSomeFlagsSet(ActorInfo.flagRemoved);
+        }
+
+        prim: Prim2DBase;
+        flags: number;
+        owner: BasicPrimitiviceCollisionManager;
+        presentInClusters: StringDictionary<ClusterInfo>;
+        intersectWith: StringDictionary<ActorInfo>;
+
+        public static flagDeep       = 0x0001;      // set if the actor boundingInfo must be used instead of the levelBoundingInfo
+        public static flagEnabled    = 0x0002;      // set if the actor is enabled and should be considered for intersection tests
+        public static flagDirty      = 0x0004;      // set if the actor's AABB is dirty
+        public static flagRemoved    = 0x0008;      // set if the actor was removed from the PCM
+    }
+
+    class ClusterInfo {
+        constructor() {
+            this.actors = new StringDictionary<ActorInfo>();
+        }
+
+        clear() {
+            this.actors.clear();
+        }
+
+        actors: StringDictionary<ActorInfo>;
+    }
+
+    export class BasicPrimitiviceCollisionManager extends PirimitiveCollisionManagerBase {
+
+        constructor(owner: Canvas2D, enableBorders: boolean) {
+            super(owner);
+            this._actors = new StringDictionary<ActorInfo>();
+            this._dirtyActors = new StringDictionary<ActorInfo>();
+            this._clusters = null;
+            this._maxActorByCluster = 0;
+            this._AABBRenderPrim = null;
+            this._canvasSize = Size.Zero();
+            this._ClusterRenderPrim = null;
+            this._debugTextBackground = null;
+            this._clusterDirty = true;
+            this._clusterSize = new Size(2, 2);
+            this._clusterStep = Vector2.Zero();
+            this._lastClusterResizeCounter = 0;
+            this._freeClusters = new Array<ClusterInfo>();
+            this._enableBorder = enableBorders;
+            this._debugUpdateOpCount = new PerfCounter();
+            this._debugUpdateTime = new PerfCounter();
+            this._intersectedActors = new ObservableStringDictionary<{ a: Prim2DBase; b: Prim2DBase }>(false);
+            this._borderIntersecteddActors = new Array<ObservableStringDictionary<Prim2DBase>>(4);
+            for (let j = 0; j < 4; j++) {
+                this._borderIntersecteddActors[j] = new ObservableStringDictionary<Prim2DBase>(false);
+            }
+            let flagId = Canvas2D.actualSizeProperty.flagId;
+
+            if (!BasicPrimitiviceCollisionManager.WAABBCorners) {
+                BasicPrimitiviceCollisionManager.WAABBCorners = new Array<Vector2>(4);
+                for (let i = 0; i < 4; i++) {
+                    BasicPrimitiviceCollisionManager.WAABBCorners[i] = Vector2.Zero();
+                }
+                BasicPrimitiviceCollisionManager.WAABBCornersCluster = new Array<Vector2>(4);
+                for (let i = 0; i < 4; i++) {
+                    BasicPrimitiviceCollisionManager.WAABBCornersCluster[i] = Vector2.Zero();
+                }
+            }
+
+            owner.propertyChanged.add((e: PropertyChangedInfo, d) => {
+                if (d.mask === -1) {
+                    return;
+                }
+                this._clusterDirty = true;
+                console.log("canvas size changed");
+            }, flagId);
+
+            this.debugRenderAABB = true;
+            this.debugRenderClusters = true;
+            this.debugStats = true;
+        }
+
+        addActor(actor: Prim2DBase, deep: boolean) {
+            this._actors.getOrAddWithFactory(actor.uid, () => {
+                let ai = new ActorInfo(this, actor, deep);
+                this.actorDirty(ai);
+                return ai;
+            });
+        }
+
+        removeActor(actor: Prim2DBase) {
+            let ai = this._actors.getAndRemove(actor.uid);
+            ai.setFlags(ActorInfo.flagRemoved);
+            this.actorDirty(ai);
+        }
+
+        actorDirty(actor: ActorInfo) {
+            actor.setFlags(ActorInfo.flagDirty);
+            this._dirtyActors.add(actor.prim.uid, actor);
+        }
+
+        update() {
+            this._canvasSize.copyFrom(this._owner.actualSize);
+
+            // Should we update the WireFrame2D Primitive that displays the WorldAABB ?
+            if (this.debugRenderAABB) {
+                if (this._dirtyActors.count > 0) {
+                    this._updateAABBDisplay();
+                }
+            }
+
+            let cw = this._clusterSize.width;
+            let ch = this._clusterSize.height;
+
+            // Check for Cluster resize
+            if (((this._clusterSize.width < 16 && this._clusterSize.height < 16 && this._maxActorByCluster >= 10) ||
+                 (this._clusterSize.width > 2 && this._clusterSize.height > 2 && this._maxActorByCluster <= 7)) &&
+                this._lastClusterResizeCounter > 100) {
+
+                if (this._maxActorByCluster >= 10) {
+                    ++cw;
+                    ++ch;
+                } else {
+                    --cw;
+                    --ch;
+                }
+                console.log(`Change cluster size to ${cw}:${ch}, max actor ${this._maxActorByCluster}`);
+                this._clusterDirty = true;
+            }
+
+            // Should we update the WireFrame2D Primitive that displays the clusters
+            if (this.debugRenderClusters && this._clusterDirty) {
+                this._updateClusterDisplay(cw, ch);
+            }
+
+            let updateStats = this.debugStats && (this._dirtyActors.count > 0 || this._clusterDirty);
+
+            this._debugUpdateTime.beginMonitoring();
+
+            // If the Cluster Size changed: rebuild it and add all actors. Otherwise add only new (dirty) actors
+            if (this._clusterDirty) {
+                this._initializeCluster(cw, ch);
+                this._rebuildAllActors();
+            } else {
+                this._rebuildDirtyActors();
+                ++this._lastClusterResizeCounter;
+            }
+
+            // Proceed to the collision detection between primitives
+            this._collisionDetection();
+
+            this._debugUpdateTime.endMonitoring();
+
+            if (updateStats) {
+                this._updateDebugStats();
+            }
+
+            // Reset the dirty actor list: everything is processed
+            this._dirtyActors.clear();
+        }
+
+        public debugRenderAABB;
+        public debugRenderClusters;
+        public debugStats;
+
+        get intersectedActors(): ObservableStringDictionary<{ a: Prim2DBase; b: Prim2DBase }> {
+            return this._intersectedActors;
+        }
+
+        get leftBorderIntersectedActors(): ObservableStringDictionary<Prim2DBase> {
+            return this._borderIntersecteddActors[0];
+        }
+
+        get bottomBorderIntersectedActors(): ObservableStringDictionary<Prim2DBase> {
+            return this._borderIntersecteddActors[1];
+        }
+
+        get rightBorderIntersectedActors(): ObservableStringDictionary<Prim2DBase> {
+            return this._borderIntersecteddActors[2];
+        }
+
+        get topBorderIntersectedActors(): ObservableStringDictionary<Prim2DBase> {
+            return this._borderIntersecteddActors[3];
+        }
+
+        private _initializeCluster(countW: number, countH: number) {
+            // Check for free
+            if (this._clusters) {
+                for (let w = 0; w < this._clusterSize.height; w++) {
+                    for (let h = 0; h < this._clusterSize.width; h++) {
+                        this._freeClusterInfo(this._clusters[w][h]);
+                    }
+                }
+            }
+
+            // Allocate
+            this._clusterSize.copyFromFloats(countW, countH);
+            this._clusters = [];
+            for (let w = 0; w < this._clusterSize.height; w++) {
+                this._clusters[w] = [];
+                for (let h = 0; h < this._clusterSize.width; h++) {
+                    let ci = this._allocClusterInfo();
+                    this._clusters[w][h] = ci;
+                }
+            }
+
+            this._clusterStep.copyFromFloats(this._owner.actualWidth / countW, this._owner.actualHeight / countH);
+            this._maxActorByCluster = 0;
+            this._lastClusterResizeCounter = 0;
+
+            this._clusterDirty = false;
+        }
+
+        private _rebuildAllActors() {
+            this._actors.forEach((k, ai) => {
+                this._processActor(ai);
+            });
+        }
+
+        private _rebuildDirtyActors() {
+            this._dirtyActors.forEach((k, ai) => {
+                this._processActor(ai);
+            });
+        }
+
+        static WAABBCorners: Array<Vector2> = null;
+        static WAABBCornersCluster: Array<Vector2> = null;
+
+        private _processActor(actor: ActorInfo) {
+            // Check if the actor is being disabled or removed
+            if (!actor.isEnabled || actor.isRemoved) {
+                actor.presentInClusters.forEach((k, ci) => {
+                    ci.actors.remove(actor.prim.uid);
+                });
+                actor.presentInClusters.clear();
+                return;
+            }
+
+            let wab = actor.worldAABB;
+
+            // Build the worldAABB corners
+            let wac = BasicPrimitiviceCollisionManager.WAABBCorners;
+            wac[0].copyFromFloats(wab.x, wab.y); // Bottom/Left
+            wac[1].copyFromFloats(wab.z, wab.y); // Bottom/Right
+            wac[2].copyFromFloats(wab.z, wab.w); // Top/Right
+            wac[3].copyFromFloats(wab.x, wab.w); // Top/Left
+
+            let cs = this._clusterStep;
+            let wacc = BasicPrimitiviceCollisionManager.WAABBCornersCluster;
+            for (let i = 0; i < 4; i++) {
+                let p = wac[i];
+                let cx = (p.x - (p.x % cs.x)) / cs.x;
+                let cy = (p.y - (p.y % cs.y)) / cs.y;
+                wacc[i].copyFromFloats(Math.floor(cx), Math.floor(cy));
+            }
+
+            let opCount = 0;
+            let totalClusters = 0;
+            let newCI = new Array<ClusterInfo>();
+            let sx = Math.max(0, wacc[0].x);                              // Start Cluster X
+            let sy = Math.max(0, wacc[0].y);                              // Start Cluster Y
+            let ex = Math.min(this._clusterSize.width - 1,  wacc[2].x);   // End Cluster X
+            let ey = Math.min(this._clusterSize.height - 1, wacc[2].y);   // End Cluster Y
+
+            if (this._enableBorder) {
+                if (wac[0].x < 0) {
+                    this._borderIntersecteddActors[0].add(actor.prim.uid, actor.prim);
+                } else {
+                    this._borderIntersecteddActors[0].remove(actor.prim.uid);
+                }
+                if (wac[0].y < 0) {
+                    this._borderIntersecteddActors[1].add(actor.prim.uid, actor.prim);
+                } else {
+                    this._borderIntersecteddActors[1].remove(actor.prim.uid);
+                }
+                if (wac[2].x >= this._canvasSize.width) {
+                    this._borderIntersecteddActors[2].add(actor.prim.uid, actor.prim);
+                } else {
+                    this._borderIntersecteddActors[2].remove(actor.prim.uid);
+                }
+                if (wac[2].y >= this._canvasSize.height) {
+                    this._borderIntersecteddActors[3].add(actor.prim.uid, actor.prim);
+                } else {
+                    this._borderIntersecteddActors[3].remove(actor.prim.uid);
+                }
+            }
+
+            for (var y = sy; y <= ey; y++) {
+                for (let x = sx; x <= ex; x++) {
+                    let k = `${x}:${y}`;
+                    let cx = x, cy = y;
+                    let ci = actor.presentInClusters.getOrAddWithFactory(k,
+                        (k) => {
+                            let nci = this._getCluster(cx, cy);
+                            nci.actors.add(actor.prim.uid, actor);
+                            this._maxActorByCluster = Math.max(this._maxActorByCluster, nci.actors.count);
+                            ++opCount;
+                            ++totalClusters;
+                            return nci;
+                        });
+                    newCI.push(ci);
+                }
+            }
+
+            // Check if there were no change
+            if (opCount === 0 && actor.presentInClusters.count === totalClusters) {
+                return;
+            }
+
+            // Build the array of the cluster where the actor is no longer in
+            let clusterToRemove = new Array<string>();
+            actor.presentInClusters.forEach((k, ci) => {
+                if (newCI.indexOf(ci) === -1) {
+                    clusterToRemove.push(k);
+                    // remove the primitive from the Cluster Info object
+                    ci.actors.remove(actor.prim.uid);
+                }
+            });
+
+            // Remove these clusters from the actor's dictionary
+            for (let key of clusterToRemove) {
+                actor.presentInClusters.remove(key);
+            }
+        }
+
+        private static CandidatesActors = new StringDictionary<ActorInfo>();
+        private static PreviousIntersections = new StringDictionary<ActorInfo>();
+
+        // The algorithm is simple, we have previously partitioned the Actors in the Clusters: each actor has a list of the Cluster(s) it's inside.
+        // Then for a given Actor that is dirty we evaluate the intersection with all the other actors present in the same Cluster(s)
+        // So it's basically O(n²), BUT only inside a Cluster and only for dirty Actors.
+        private _collisionDetection() {
+            let hash = BasicPrimitiviceCollisionManager.CandidatesActors;
+            let prev = BasicPrimitiviceCollisionManager.PreviousIntersections;
+            let opCount = 0;
+
+            this._dirtyActors.forEach((k1, ai1) => {
+                ++opCount;
+
+                // Build the list of candidates
+                hash.clear();
+                ai1.presentInClusters.forEach((k, ci) => {
+                    ++opCount;
+                    ci.actors.forEach((k, v) => hash.add(k, v));
+                });
+
+                let wab1 = ai1.worldAABB;
+
+                // Save the previous intersections
+                prev.clear();
+                prev.copyFrom(ai1.intersectWith);
+
+                ai1.intersectWith.clear();
+
+                // For each candidate
+                hash.forEach((k2, ai2) => {
+                    ++opCount;
+
+                    // Check if we're testing against itself
+                    if (k1 === k2) {
+                        return;
+                    }
+
+                    let wab2 = ai2.worldAABB;
+                     
+                    if (wab2.z >= wab1.x && wab2.x <= wab1.z && wab2.w >= wab1.y && wab2.y <= wab1.w) {
+
+                        if (ai1.prim.intersectOtherPrim(ai2.prim)) {
+                            ++opCount;
+                            ai1.intersectWith.add(k2, ai2);
+
+                            if (k1 < k2) {
+                                this._intersectedActors.add(`${k1};${k2}`, { a: ai1.prim, b: ai2.prim });
+                            } else {
+                                this._intersectedActors.add(`${k2};${k1}`, { a: ai2.prim, b: ai1.prim });
+                            }
+                        }
+                    }
+                });
+
+                // Check and remove the associations that no longer exist in the main intersection list
+                prev.forEach((k, ai) => {
+                    if (!ai1.intersectWith.contains(k)) {
+                        ++opCount;
+                        this._intersectedActors.remove(`${k<k1 ? k : k1};${k<k1 ? k1 : k}`);
+                    }
+                });
+
+            });
+
+            this._debugUpdateOpCount.fetchNewFrame();
+            this._debugUpdateOpCount.addCount(opCount, true);
+        }
+
+        private _getCluster(x: number, y: number): ClusterInfo {
+            return this._clusters[x][y];
+        }
+
+        private _updateDebugStats() {
+
+            let format = (v: number) => (Math.round(v*100)/100).toString();
+            let txt =   `Primitive Collision Stats\n` + 
+                        ` - PCM Execution Time: ${format(this._debugUpdateTime.lastSecAverage)}ms\n` +
+                        ` - Operation Count: ${format(this._debugUpdateOpCount.current)}, (avg:${format(this._debugUpdateOpCount.lastSecAverage)}, t:${format(this._debugUpdateOpCount.total)})\n` +
+                        ` - Max Actor per Cluster: ${this._maxActorByCluster}\n` +
+                        ` - Intersections count: ${this.intersectedActors.count}`;
+
+            if (!this._debugTextBackground) {
+
+                this._debugTextBackground = new Rectangle2D({
+                    id: "###DEBUG PMC STATS###", parent: this._owner, marginAlignment: "h: left, v: top", fill: "#C0404080", padding: "top: 10, left: 10, right: 10, bottom: 10", roundRadius: 10, children: [
+                        new Text2D(txt, { id: "###DEBUG PMC TEXT###", fontName: "12pt Lucida Console" })
+                    ]
+                });
+                    
+            } else {
+                let text2d = this._debugTextBackground.children[0] as Text2D;
+                text2d.text = txt;
+            }
+        }
+
+        private _updateAABBDisplay() {
+            let g = new WireFrameGroup2D("main", new Color4(0.5, 0.8, 1.0, 1.0));
+
+            let v = Vector2.Zero();
+
+            this._actors.forEach((k, ai) => {
+                if (ai.isEnabled) {
+                    let ab = ai.worldAABB;
+
+                    v.x = ab.x;
+                    v.y = ab.y;
+                    g.startLineStrip(v);
+
+                    v.x = ab.z;
+                    g.pushVertex(v);
+
+                    v.y = ab.w;
+                    g.pushVertex(v);
+
+                    v.x = ab.x;
+                    g.pushVertex(v);
+
+                    v.y = ab.y;
+                    g.endLineStrip(v);
+                }
+            });
+
+            if (!this._AABBRenderPrim) {
+                this._AABBRenderPrim = new WireFrame2D([g], { parent: this._owner, alignToPixel: true, id: "###DEBUG PCM AABB###" });
+            } else {
+                this._AABBRenderPrim.wireFrameGroups.set("main", g);
+                this._AABBRenderPrim.wireFrameGroupsDirty();
+            }
+        }
+
+        private _updateClusterDisplay(cw: number, ch: number) {
+            let g = new WireFrameGroup2D("main", new Color4(0.8, 0.1, 0.5, 1.0));
+
+            let v1 = Vector2.Zero();
+            let v2 = Vector2.Zero();
+
+            // Vertical lines
+            let step = (this._owner.actualWidth-1) / cw;
+            v1.y = 0;
+            v2.y = this._owner.actualHeight;
+            for (let x = 0; x <= cw; x++) {
+                g.pushVertex(v1);
+                g.pushVertex(v2);
+
+                v1.x += step;
+                v2.x += step;
+            }
+
+            // Horizontal lines
+            step = (this._owner.actualHeight-1) / ch;
+            v1.x = v1.y = v2.y = 0;
+            v2.x = this._owner.actualWidth;
+            for (let y = 0; y <= ch; y++) {
+                g.pushVertex(v1);
+                g.pushVertex(v2);
+
+                v1.y += step;
+                v2.y += step;
+            }
+
+            if (!this._ClusterRenderPrim) {
+                this._ClusterRenderPrim = new WireFrame2D([g], { parent: this._owner, alignToPixel: true, id: "###DEBUG PCM Clusters###" });
+            } else {
+                this._ClusterRenderPrim.wireFrameGroups.set("main", g);
+                this._ClusterRenderPrim.wireFrameGroupsDirty();
+            }
+        }
+
+        // Basically: we don't want to spend our time playing with the GC each time the Cluster Array is rebuilt, so we keep a list of available
+        //  ClusterInfo object and we have two method to allocate/free them. This way we always deal with the same objects.
+        // The free array never shrink, always grows...For the better...and the worst!
+        private _allocClusterInfo(): ClusterInfo {
+            if (this._freeClusters.length === 0) {
+                for (let i = 0; i < 8; i++) {
+                    this._freeClusters.push(new ClusterInfo());
+                }
+            }
+
+            return this._freeClusters.pop();
+        }
+
+        private _freeClusterInfo(ci: ClusterInfo) {
+            ci.clear();
+            this._freeClusters.push(ci);
+        }
+
+        private _canvasSize: Size;
+        private _clusterDirty: boolean;
+        private _clusterSize: Size;
+        private _clusterStep: Vector2;
+        private _clusters: ClusterInfo[][];
+        private _maxActorByCluster: number;
+        private _lastClusterResizeCounter: number;
+        private _actors: StringDictionary<ActorInfo>;
+        private _dirtyActors: StringDictionary<ActorInfo>;
+
+        private _freeClusters: Array<ClusterInfo>;
+        private _enableBorder: boolean;
+        private _intersectedActors: ObservableStringDictionary<{ a: Prim2DBase; b: Prim2DBase }>;
+        private _borderIntersecteddActors: ObservableStringDictionary<Prim2DBase>[];
+        private _debugUpdateOpCount: PerfCounter;
+        private _debugUpdateTime: PerfCounter;
+        private _AABBRenderPrim: WireFrame2D;
+        private _ClusterRenderPrim: WireFrame2D;
+        private _debugTextBackground: Rectangle2D;
+    }
+}

+ 77 - 0
canvas2D/src/Engine/babylon.rectangle2d.ts

@@ -314,6 +314,8 @@
          * - isPickable: if true the Primitive can be used with interaction mode and will issue Pointer Event. If false it will be ignored for interaction/intersection test. Default value is true.
          * - isContainer: if true the Primitive acts as a container for interaction, if the primitive is not pickable or doesn't intersection, no further test will be perform on its children. If set to false, children will always be considered for intersection/interaction. Default value is true.
          * - childrenFlatZOrder: if true all the children (direct and indirect) will share the same Z-Order. Use this when there's a lot of children which don't overlap. The drawing order IS NOT GUARANTED!
+         * - levelCollision: this primitive is an actor of the Collision Manager and only this level will be used for collision (i.e. not the children). Use deepCollision if you want collision detection on the primitives and its children.
+         * - deepCollision: this primitive is an actor of the Collision Manager, this level AND ALSO its children will be used for collision (note: you don't need to set the children as level/deepCollision).
          * - marginTop: top margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginLeft: left margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginRight: right margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
@@ -354,6 +356,8 @@
             isPickable            ?: boolean,
             isContainer           ?: boolean,
             childrenFlatZOrder    ?: boolean,
+            levelCollision        ?: boolean,
+            deepCollision         ?: boolean,
             marginTop             ?: number | string,
             marginLeft            ?: number | string,
             marginRight           ?: number | string,
@@ -400,6 +404,79 @@
             return renderCache;
         }
 
+        protected updateTriArray() {
+            // Not Rounded = sharp edge rect, the default implementation is the right one!
+            if (this.notRounded) {
+                super.updateTriArray();
+                return;
+            }
+
+            // Rounded Corner? It's more complicated! :)
+
+            let subDiv = Rectangle2D.roundSubdivisions * 4;
+            if (this._primTriArray == null) {
+                this._primTriArray = new Tri2DArray(subDiv);
+            } else {
+                this._primTriArray.clear(subDiv);
+            }
+
+            let size = this.actualSize;
+			let w = size.width;
+			let h = size.height;
+            let r = this.roundRadius;
+            let rsub0 = subDiv * 0.25;
+            let rsub1 = subDiv * 0.50;
+            let rsub2 = subDiv * 0.75;
+            let center = new Vector2(0.5 * size.width, 0.5 * size.height);
+            let twopi = Math.PI * 2;
+			let nru = r / w;
+			let nrv = r / h;
+
+            let computePos = (index: number, p: Vector2) => {
+			    // right/bottom
+			    if (index < rsub0) {
+				    p.x = 1.0 - nru;
+				    p.y = nrv;
+			    }
+			    // left/bottom
+			    else if (index < rsub1) {
+				    p.x = nru;
+				    p.y = nrv;
+			    }
+			    // left/top
+			    else if (index < rsub2) {
+				    p.x = nru;
+				    p.y = 1.0 - nrv;
+			    }
+			    // right/top
+			    else {
+				    p.x = 1.0 - nru;
+				    p.y = 1.0 - nrv;
+			    }
+
+                let angle = twopi - (index * twopi / (subDiv - 0.5));
+			    p.x += Math.cos(angle) * nru;
+                p.y += Math.sin(angle) * nrv;
+                p.x *= w;
+                p.y *= h;
+            }
+
+            console.log("Genetre TriList for " + this.id);
+            let first = Vector2.Zero();
+            let cur = Vector2.Zero();
+            computePos(0, first);
+            let prev = first.clone();
+            for (let index = 1; index < subDiv; index++) {
+                computePos(index, cur);
+                this._primTriArray.storeTriangle(index - 1, center, prev, cur);
+                console.log(`${index-1}, ${center}, ${prev}, ${cur}`);
+                prev.copyFrom(cur);
+            }
+            this._primTriArray.storeTriangle(subDiv-1, center, first, prev);
+                console.log(`${subDiv-1}, ${center}, ${prev}, ${first}`);
+
+        }
+
         protected setupModelRenderCache(modelRenderCache: ModelRenderCache) {
             let renderCache = <Rectangle2DRenderCache>modelRenderCache;
             let engine = this.owner.engine;

+ 13 - 2
canvas2D/src/Engine/babylon.renderablePrim2d.ts

@@ -492,6 +492,11 @@
                 this.setupModelRenderCache(this._modelRenderCache);
             }
 
+            if (this._isFlagSet(SmartPropertyPrim.flagModelUpdate)) {
+                if (this._modelRenderCache.updateModelRenderCache(this)) {
+                    this._clearFlags(SmartPropertyPrim.flagModelUpdate);
+                }
+            }
             // 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.
@@ -955,8 +960,8 @@
             let w = size.width;
             let h = size.height;
             let invZBias = 1 / zBias;
-            let tx = new Vector4(t.m[0] * rgScale.x * 2 / w, t.m[4] * rgScale.x * 2 / w, 0/*t.m[8]*/, ((t.m[12] + offX) * rgScale.x * 2 / w) - 1);
-            let ty = new Vector4(t.m[1] * rgScale.y * 2 / h, t.m[5] * rgScale.y * 2 / h, 0/*t.m[9]*/, ((t.m[13] + offY) * rgScale.y * 2 / h) - 1);
+            let tx = new Vector4(t.m[0] * rgScale.x * 2/* / w*/, t.m[4] * rgScale.x * 2/* / w*/, 0/*t.m[8]*/, ((t.m[12] + offX) * rgScale.x * 2 / w) - 1);
+            let ty = new Vector4(t.m[1] * rgScale.y * 2/* / h*/, t.m[5] * rgScale.y * 2/* / h*/, 0/*t.m[9]*/, ((t.m[13] + offY) * rgScale.y * 2 / h) - 1);
 
             if (!this.applyActualScaleOnTransform()) {
                 t.m[0] = tx.x, t.m[4] = tx.y, t.m[12] = tx.w;
@@ -969,6 +974,12 @@
                 ty = new Vector4(t.m[1], t.m[5], 0, t.m[13]);
             }
 
+            tx.x /= w;
+            tx.y /= w;
+
+            ty.x /= h;
+            ty.y /= h;
+
             part.transformX = tx;
             part.transformY = ty;
             part.opacity = this.actualOpacity;

+ 20 - 2
canvas2D/src/Engine/babylon.smartPropertyPrim.ts

@@ -993,6 +993,7 @@
         constructor() {
             super();
             this._flags = 0;
+            this._uid = null;
             this._modelKey = null;
             this._levelBoundingInfo = new BoundingInfo2D();
             this._boundingInfo = new BoundingInfo2D();
@@ -1022,6 +1023,16 @@
         public animations: Animation[];
 
         /**
+         * return a unique identifier for the Canvas2D
+         */
+        public get uid(): string {
+            if (!this._uid) {
+                this._uid = Tools.RandomId();
+            }
+            return this._uid;
+        }
+
+        /**
          * Returns as a new array populated with the Animatable used by the primitive. Must be overloaded by derived primitives.
          * Look at Sprite2D for more information
          */
@@ -1062,7 +1073,10 @@
                         if (v.typeLevelCompare) {
                             value = Tools.getClassName(propVal);
                         } else {
-                            if (propVal instanceof BaseTexture) {
+                            // String Dictionaries' content are too complex, with use a Random GUID to make the model unique
+                            if (propVal instanceof StringDictionary) {
+                                value = Tools.RandomId();
+                            } else if (propVal instanceof BaseTexture) {
                                 value = propVal.uid;
                             } else {
                                 value = propVal.toString();
@@ -1085,7 +1099,7 @@
          * @returns true is dirty, false otherwise
          */
         public get isDirty(): boolean {
-            return (this._instanceDirtyFlags !== 0) || this._areSomeFlagsSet(SmartPropertyPrim.flagModelDirty | SmartPropertyPrim.flagPositioningDirty | SmartPropertyPrim.flagLayoutDirty);
+            return (this._instanceDirtyFlags !== 0) || this._areSomeFlagsSet(SmartPropertyPrim.flagModelDirty | SmartPropertyPrim.flagModelUpdate | SmartPropertyPrim.flagPositioningDirty | SmartPropertyPrim.flagLayoutDirty);
         }
 
         protected _boundingBoxDirty() {
@@ -1179,6 +1193,7 @@
         public get levelBoundingInfo(): BoundingInfo2D {
             if (this._isFlagSet(SmartPropertyPrim.flagLevelBoundingInfoDirty)) {
                 this.updateLevelBoundingInfo();
+                this._boundingInfo.dirtyWorldAABB();
                 this._clearFlags(SmartPropertyPrim.flagLevelBoundingInfoDirty);
             }
             return this._levelBoundingInfo;
@@ -1279,7 +1294,10 @@
         public static flagDontInheritParentScale  = 0x0080000;    // set if the actualScale must not use its parent's scale to be computed
         public static flagGlobalTransformDirty    = 0x0100000;    // set if the global transform must be recomputed due to a local transform change
         public static flagLayoutBoundingInfoDirty = 0x0200000;    // set if the layout bounding info is dirty
+        public static flagCollisionActor          = 0x0400000;    // set if the primitive is part of the collision engine
+        public static flagModelUpdate             = 0x0800000;    // set if the primitive's model data is to update
 
+        private   _uid                : string;
         private   _flags              : number;
         private   _modelKey           : string;
         protected _levelBoundingInfo  : BoundingInfo2D;

+ 4 - 0
canvas2D/src/Engine/babylon.sprite2d.ts

@@ -291,6 +291,8 @@
          * - isPickable: if true the Primitive can be used with interaction mode and will issue Pointer Event. If false it will be ignored for interaction/intersection test. Default value is true.
          * - isContainer: if true the Primitive acts as a container for interaction, if the primitive is not pickable or doesn't intersection, no further test will be perform on its children. If set to false, children will always be considered for intersection/interaction. Default value is true.
          * - childrenFlatZOrder: if true all the children (direct and indirect) will share the same Z-Order. Use this when there's a lot of children which don't overlap. The drawing order IS NOT GUARANTED!
+         * - levelCollision: this primitive is an actor of the Collision Manager and only this level will be used for collision (i.e. not the children). Use deepCollision if you want collision detection on the primitives and its children.
+         * - deepCollision: this primitive is an actor of the Collision Manager, this level AND ALSO its children will be used for collision (note: you don't need to set the children as level/deepCollision).
          * - marginTop: top margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginLeft: left margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginRight: right margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
@@ -332,6 +334,8 @@
             isPickable            ?: boolean,
             isContainer           ?: boolean,
             childrenFlatZOrder    ?: boolean,
+            levelCollision        ?: boolean,
+            deepCollision         ?: boolean,
             marginTop             ?: number | string,
             marginLeft            ?: number | string,
             marginRight           ?: number | string,

+ 4 - 0
canvas2D/src/Engine/babylon.text2d.ts

@@ -323,6 +323,8 @@
          * - isPickable: if true the Primitive can be used with interaction mode and will issue Pointer Event. If false it will be ignored for interaction/intersection test. Default value is true.
          * - isContainer: if true the Primitive acts as a container for interaction, if the primitive is not pickable or doesn't intersection, no further test will be perform on its children. If set to false, children will always be considered for intersection/interaction. Default value is true.
          * - childrenFlatZOrder: if true all the children (direct and indirect) will share the same Z-Order. Use this when there's a lot of children which don't overlap. The drawing order IS NOT GUARANTED!
+         * - levelCollision: this primitive is an actor of the Collision Manager and only this level will be used for collision (i.e. not the children). Use deepCollision if you want collision detection on the primitives and its children.
+         * - deepCollision: this primitive is an actor of the Collision Manager, this level AND ALSO its children will be used for collision (note: you don't need to set the children as level/deepCollision).
          * - marginTop: top margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginLeft: left margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
          * - marginRight: right margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
@@ -364,6 +366,8 @@
             isPickable              ?: boolean,
             isContainer             ?: boolean,
             childrenFlatZOrder      ?: boolean,
+            levelCollision          ?: boolean,
+            deepCollision           ?: boolean,
             marginTop               ?: number | string,
             marginLeft              ?: number | string,
             marginRight             ?: number | string,

+ 476 - 0
canvas2D/src/Engine/babylon.wireFrame2d.ts

@@ -0,0 +1,476 @@
+module BABYLON {
+    export class WireFrame2DRenderCache extends ModelRenderCache {
+        effectsReady: boolean = false;
+        vb: WebGLBuffer = null;
+        vtxCount = 0;
+        instancingAttributes: InstancingAttributeInfo[] = null;
+        effect: Effect = null;
+        effectInstanced: Effect = null;
+
+        render(instanceInfo: GroupInstanceInfo, context: Render2DContext): boolean {
+            // Do nothing if the shader is still loading/preparing 
+            if (!this.effectsReady) {
+                if ((this.effect && (!this.effect.isReady() || (this.effectInstanced && !this.effectInstanced.isReady())))) {
+                    return false;
+                }
+                this.effectsReady = true;
+            }
+
+            // Compute the offset locations of the attributes in the vertex shader that will be mapped to the instance buffer data
+            let canvas = instanceInfo.owner.owner;
+            var engine = canvas.engine;
+
+            var cur = engine.getAlphaMode();
+            let effect = context.useInstancing ? this.effectInstanced : this.effect;
+
+            engine.enableEffect(effect);
+            engine.bindBuffersDirectly(this.vb, null, [2, 4], 24, effect);
+
+            if (context.renderMode !== Render2DContext.RenderModeOpaque) {
+                engine.setAlphaMode(Engine.ALPHA_COMBINE, true);
+            }
+
+            let pid = context.groupInfoPartData[0];
+            if (context.useInstancing) {
+                if (!this.instancingAttributes) {
+                    this.instancingAttributes = this.loadInstancingAttributes(WireFrame2D.WIREFRAME2D_MAINPARTID, effect);
+                }
+                let glBuffer = context.instancedBuffers ? context.instancedBuffers[0] : pid._partBuffer;
+                let count = context.instancedBuffers ? context.instancesCount : pid._partData.usedElementCount;
+                canvas._addDrawCallCount(1, context.renderMode);
+                engine.updateAndBindInstancesBuffer(glBuffer, null, this.instancingAttributes);
+                engine.drawUnIndexed(false, 0, this.vtxCount, count);
+//                engine.draw(true, 0, 6, count);
+                engine.unbindInstanceAttributes();
+            } else {
+                canvas._addDrawCallCount(context.partDataEndIndex - context.partDataStartIndex, context.renderMode);
+                for (let i = context.partDataStartIndex; i < context.partDataEndIndex; i++) {
+                    this.setupUniforms(effect, 0, pid._partData, i);
+                    engine.drawUnIndexed(false, 0, this.vtxCount);
+  //                  engine.draw(true, 0, 6);
+                }
+            }
+
+            engine.setAlphaMode(cur, true);
+
+            return true;
+        }
+
+        public updateModelRenderCache(prim: Prim2DBase): boolean {
+            let w = prim as WireFrame2D;
+            w._updateVertexBuffer(this);
+            return true;
+        }
+
+        public dispose(): boolean {
+            if (!super.dispose()) {
+                return false;
+            }
+
+            if (this.vb) {
+                this._engine._releaseBuffer(this.vb);
+                this.vb = null;
+            }
+
+            this.effect = null;
+            this.effectInstanced = null;
+
+            return true;
+        }
+    }
+
+    @className("WireFrameVertex2D", "BABYLON")
+    export class WireFrameVertex2D {
+        x: number;
+        y: number;
+        r: number;
+        g: number;
+        b: number;
+        a: number;
+
+        constructor(p: Vector2, c: Color4=null) {
+            this.fromVector2(p);
+            if (c != null) {
+                this.fromColor4(c);
+            } else {
+                this.r = this.g = this.b = this.a = 1;
+            }
+        }
+
+        fromVector2(p: Vector2) {
+            this.x = p.x;
+            this.y = p.y;
+        }
+
+        fromColor3(c: Color3) {
+            this.r = c.r;
+            this.g = c.g;
+            this.b = c.b;
+            this.a = 1;
+        }
+
+        fromColor4(c: Color4) {
+            this.r = c.r;
+            this.g = c.g;
+            this.b = c.b;
+            this.a = c.a;
+        }
+    }
+
+    @className("WireFrameGroup2D", "BABYLON")
+    /**
+     * A WireFrameGroup2D has a unique id (among the WireFrame2D primitive) and a collection of WireFrameVertex2D which form a Line list.
+     * A Line is defined by two vertices, the storage of vertices doesn't follow the Line Strip convention, so to create consecutive lines the intermediate vertex must be doubled. The best way to build a Line Strip is to use the startLineStrip, pushVertex and endLineStrip methods.
+     * You can manually add vertices using the pushVertex method, but mind that the vertices array must be a multiple of 2 as each line are defined with TWO SEPARATED vertices. I hope this is clear enough.
+     */
+    export class WireFrameGroup2D {
+        /**
+         * Construct a WireFrameGroup2D object
+         * @param id a unique ID among the Groups added to a given WireFrame2D primitive, if you don't specify an id, a random one will be generated. The id is immutable.
+         * @param defaultColor specify the default color that will be used when a vertex is pushed, white will be used if not specified.
+         */
+        constructor(id: string=null, defaultColor: Color4=null) {
+            this._id = (id == null) ? Tools.RandomId() : id;
+            this._uid = Tools.RandomId();
+            this._defaultColor = (defaultColor == null) ? new Color4(1,1,1,1) : defaultColor;
+            this._buildingStrip = false;
+            this._vertices = new Array<WireFrameVertex2D>();
+        }
+
+        public get uid() {
+            return this._uid;
+        }
+
+        /**
+         * Retrieve the ID of the group
+         */
+        public get id(): string {
+            return this._id;
+        }
+
+        /**
+         * Push a vertex in the array of vertices.
+         * If you're previously called startLineStrip, the vertex will be pushed twice in order to describe the end of a line and the start of a new one.
+         * @param p Position of the vertex
+         * @param c Color of the vertex, if null the default color of the group will be used
+         */
+        public pushVertex(p: Vector2, c: Color4=null) {
+            let v = new WireFrameVertex2D(p, (c == null) ? this._defaultColor : c);
+            this._vertices.push(v);
+            if (this._buildingStrip) {
+                let v2 = new WireFrameVertex2D(p, (c == null) ? this._defaultColor : c);
+                this._vertices.push(v2);
+            }
+        }
+
+        /**
+         * Start to store a Line Strip. The given vertex will be pushed in the array. The you have to call pushVertex to add subsequent vertices describing the strip and don't forget to call endLineStrip to close the strip!!!
+         * @param p Position of the vertex
+         * @param c Color of the vertex, if null the default color of the group will be used
+         */
+        public startLineStrip(p: Vector2, c: Color4=null) {
+            this.pushVertex(p, (c == null) ? this._defaultColor : c);
+            this._buildingStrip = true;
+        }
+
+        /**
+         * Close the Strip by storing a last vertex
+         * @param p Position of the vertex
+         * @param c Color of the vertex, if null the default color of the group will be used
+         */
+        public endLineStrip(p: Vector2, c: Color4=null) {
+            this._buildingStrip = false;
+            this.pushVertex(p, (c == null) ? this._defaultColor : c);
+        }
+
+        /**
+         * Access to the array of Vertices, you can manipulate its content but BEWARE of what you're doing!
+         */
+        public get vertices(): Array<WireFrameVertex2D> {
+            return this._vertices;
+        }
+
+        private _uid: string;
+        private _id: string;
+        private _defaultColor: Color4;
+        private _vertices: Array<WireFrameVertex2D>;
+        private _buildingStrip: boolean;
+    }
+
+    @className("WireFrame2D", "BABYLON")
+    /**
+     * Primitive that displays a WireFrame
+     */
+    export class WireFrame2D extends RenderablePrim2D {
+        static WIREFRAME2D_MAINPARTID = 1;
+
+        public static wireFrameGroupsProperty: Prim2DPropInfo;
+
+        @modelLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 1, pi => WireFrame2D.wireFrameGroupsProperty = pi)
+        /**
+         * Get/set the texture that contains the sprite to display
+         */
+        public get wireFrameGroups(): StringDictionary<WireFrameGroup2D> {
+            return this._wireFrameGroups;
+        }
+
+        /**
+         * If you change the content of the wireFrameGroups you MUST call this method for the changes to be reflected during rendering
+         */
+        public wireFrameGroupsDirty() {
+            this._setFlags(SmartPropertyPrim.flagModelUpdate);
+            this.onPrimBecomesDirty();
+        }
+
+        public get size(): Size {
+            if (this._size == null) {
+                this._computeMinMaxTrans();
+            }
+            return this._size;
+        }
+
+        public set size(value: Size) {
+            this.internalSetSize(value);
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, pi => WireFrame2D.actualSizeProperty = pi, false, true)
+        /**
+         * Get/set the actual size of the sprite to display
+         */
+        public get actualSize(): Size {
+            if (this._actualSize) {
+                return this._actualSize;
+            }
+            return this.size;
+        }
+
+        public set actualSize(value: Size) {
+            this._actualSize = value;
+        }
+
+        protected updateLevelBoundingInfo() {
+            let v = this._computeMinMaxTrans();
+            BoundingInfo2D.CreateFromMinMaxToRef(v.x, v.z, v.y, v.w, this._levelBoundingInfo);
+        }
+
+        protected levelIntersect(intersectInfo: IntersectInfo2D): boolean {
+            // TODO !
+            return true;
+        }
+
+        /**
+         * Create an WireFrame 2D primitive
+         * @param wireFrameGroups an array of WireFrameGroup. 
+         * @param settings a combination of settings, possible ones are
+         * - parent: the parent primitive/canvas, must be specified if the primitive is not constructed as a child of another one (i.e. as part of the children array setting)
+         * - children: an array of direct children
+         * - id a text identifier, for information purpose
+         * - position: the X & Y positions relative to its parent. Alternatively the x and y properties can be set. Default is [0;0]
+         * - rotation: the initial rotation (in radian) of the primitive. default is 0
+         * - scale: the initial scale of the primitive. default is 1. You can alternatively use scaleX &| scaleY to apply non uniform scale
+         * - size: the size of the sprite displayed in the canvas, if not specified the spriteSize will be used
+         * - dontInheritParentScale: if set the parent's scale won't be taken into consideration to compute the actualScale property
+         * - opacity: set the overall opacity of the primitive, 1 to be opaque (default), less than 1 to be transparent.
+         * - zOrder: override the zOrder with the specified value
+         * - origin: define the normalized origin point location, default [0.5;0.5]
+         * - alignToPixel: the rendered lines will be aligned to the rendering device' pixels
+         * - isVisible: true if the sprite must be visible, false for hidden. Default is true.
+         * - isPickable: if true the Primitive can be used with interaction mode and will issue Pointer Event. If false it will be ignored for interaction/intersection test. Default value is true.
+         * - isContainer: if true the Primitive acts as a container for interaction, if the primitive is not pickable or doesn't intersection, no further test will be perform on its children. If set to false, children will always be considered for intersection/interaction. Default value is true.
+         * - childrenFlatZOrder: if true all the children (direct and indirect) will share the same Z-Order. Use this when there's a lot of children which don't overlap. The drawing order IS NOT GUARANTED!
+         * - levelCollision: this primitive is an actor of the Collision Manager and only this level will be used for collision (i.e. not the children). Use deepCollision if you want collision detection on the primitives and its children.
+         * - deepCollision: this primitive is an actor of the Collision Manager, this level AND ALSO its children will be used for collision (note: you don't need to set the children as level/deepCollision).
+         * - marginTop: top margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
+         * - marginLeft: left margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
+         * - marginRight: right margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
+         * - marginBottom: bottom margin, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
+         * - margin: top, left, right and bottom margin formatted as a single string (see PrimitiveThickness.fromString)
+         * - marginHAlignment: one value of the PrimitiveAlignment type's static properties
+         * - marginVAlignment: one value of the PrimitiveAlignment type's static properties
+         * - marginAlignment: a string defining the alignment, see PrimitiveAlignment.fromString
+         * - paddingTop: top padding, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
+         * - paddingLeft: left padding, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
+         * - paddingRight: right padding, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
+         * - paddingBottom: bottom padding, can be a number (will be pixels) or a string (see PrimitiveThickness.fromString)
+         * - padding: top, left, right and bottom padding formatted as a single string (see PrimitiveThickness.fromString)
+         */
+        constructor(wireFrameGroups: Array<WireFrameGroup2D>, settings?: {
+
+            parent                ?: Prim2DBase,
+            children              ?: Array<Prim2DBase>,
+            id                    ?: string,
+            position              ?: Vector2,
+            x                     ?: number,
+            y                     ?: number,
+            rotation              ?: number,
+            size                  ?: Size,
+            scale                 ?: number,
+            scaleX                ?: number,
+            scaleY                ?: number,
+            dontInheritParentScale?: boolean,
+            opacity               ?: number,
+            zOrder                ?: number, 
+            origin                ?: Vector2,
+            alignToPixel          ?: boolean,
+            isVisible             ?: boolean,
+            isPickable            ?: boolean,
+            isContainer           ?: boolean,
+            childrenFlatZOrder    ?: boolean,
+            levelCollision        ?: boolean,
+            deepCollision         ?: 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,
+        }) {
+
+            if (!settings) {
+                settings = {};
+            }
+
+            super(settings);
+
+            this._wireFrameGroups = new StringDictionary<WireFrameGroup2D>();
+            for (let wfg of wireFrameGroups) {
+                this._wireFrameGroups.add(wfg.id, wfg);
+            }
+
+            this._vtxTransparent = false;
+            if (settings.size != null) {
+                this.size = settings.size;
+            }
+
+            this.alignToPixel = (settings.alignToPixel == null) ? true : settings.alignToPixel;
+        }
+
+        /**
+         * Get/set if the sprite rendering should be aligned to the target rendering device pixel or not
+         */
+        public get alignToPixel(): boolean {
+            return this._alignToPixel;
+        }
+
+        public set alignToPixel(value: boolean) {
+            this._alignToPixel = value;
+        }
+
+        protected createModelRenderCache(modelKey: string): ModelRenderCache {
+            let renderCache = new WireFrame2DRenderCache(this.owner.engine, modelKey);
+            return renderCache;
+        }
+
+        protected setupModelRenderCache(modelRenderCache: ModelRenderCache) {
+            let renderCache = <WireFrame2DRenderCache>modelRenderCache;
+            let engine = this.owner.engine;
+
+            // Create the VertexBuffer
+            this._updateVertexBuffer(renderCache);
+
+            // Get the instanced version of the effect, if the engine does not support it, null is return and we'll only draw on by one
+            let ei = this.getDataPartEffectInfo(WireFrame2D.WIREFRAME2D_MAINPARTID, ["pos", "col"], [], true);
+            if (ei) {
+                renderCache.effectInstanced = engine.createEffect("wireframe2D", ei.attributes, ei.uniforms, [], ei.defines, null);
+            }
+
+            ei = this.getDataPartEffectInfo(WireFrame2D.WIREFRAME2D_MAINPARTID, ["pos", "col"], [], false);
+            renderCache.effect = engine.createEffect("wireframe2D", ei.attributes, ei.uniforms, [], ei.defines, null);
+
+            return renderCache;
+        }
+
+        public _updateVertexBuffer(mrc: WireFrame2DRenderCache) {
+            let engine = this.owner.engine;
+
+            if (mrc.vb != null) {
+                engine._releaseBuffer(mrc.vb);
+            }
+
+            let vtxCount = 0;
+            this._wireFrameGroups.forEach((k, v) => vtxCount += v.vertices.length);
+
+            let vb = new Float32Array(vtxCount * 6);
+            let i = 0;
+            this._wireFrameGroups.forEach((k, v) => {
+                for (let vtx of v.vertices) {
+                    vb[i++] = vtx.x;
+                    vb[i++] = vtx.y;
+                    vb[i++] = vtx.r;
+                    vb[i++] = vtx.g;
+                    vb[i++] = vtx.b;
+                    vb[i++] = vtx.a;
+                }
+            });
+
+            mrc.vb = engine.createVertexBuffer(vb);
+            mrc.vtxCount = vtxCount;
+        }
+
+        protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataPart(part)) {
+                return false;
+            }
+
+            if (part.id === WireFrame2D.WIREFRAME2D_MAINPARTID) {
+                let d = <WireFrame2DInstanceData>this._instanceDataParts[0];
+                d.properties = new Vector3(this.alignToPixel ? 1 : 0, 2/this.renderGroup.actualWidth, 2/this.renderGroup.actualHeight);
+            }
+            return true;
+        }
+
+        private _computeMinMaxTrans(): Vector4 {
+            let xmin = Number.MAX_VALUE;
+            let xmax = Number.MIN_VALUE;
+            let ymin = Number.MAX_VALUE;
+            let ymax = Number.MIN_VALUE;
+            let transparent = false;
+
+            this._wireFrameGroups.forEach((k, v) => {
+                for (let vtx of v.vertices) {
+                    xmin = Math.min(xmin, vtx.x);
+                    xmax = Math.max(xmax, vtx.x);
+                    ymin = Math.min(ymin, vtx.y);
+                    ymax = Math.max(ymax, vtx.y);
+
+                    if (vtx.a < 1) {
+                        transparent = true;
+                    }
+                }
+            });
+
+            this._vtxTransparent = transparent;
+            this._size = new Size(xmax - xmin, ymax - ymin);
+            return new Vector4(xmin, ymin, xmax, ymax);
+        }
+
+        protected createInstanceDataParts(): InstanceDataBase[] {
+            return [new WireFrame2DInstanceData(WireFrame2D.WIREFRAME2D_MAINPARTID)];
+        }
+
+        private _vtxTransparent: boolean;
+        private _wireFrameGroups: StringDictionary<WireFrameGroup2D>;
+        private _alignToPixel: boolean;
+    }
+
+    export class WireFrame2DInstanceData extends InstanceDataBase {
+        constructor(partId: number) {
+            super(partId, 1);
+        }
+
+        // the properties is for now the alignedToPixel value
+        @instanceData()
+        get properties(): Vector3 {
+            return null;
+        }
+        set properties(value: Vector3) {
+        }
+    }
+}

+ 261 - 0
canvas2D/src/Tools/babylon.math2D.ts

@@ -0,0 +1,261 @@
+module BABYLON {
+
+    export class Tri2DInfo {
+        /**
+         * Construct an instance of Tri2DInfo, you can either pass null to a, b and c and the instance will be allocated "clear", or give actual triangle info and the center/radius will be computed
+         */
+        constructor(a: Vector2, b: Vector2, c: Vector2) {
+            if (a === null && b === null && c === null) {
+                this.a = Vector2.Zero();
+                this.b = Vector2.Zero();
+                this.c = Vector2.Zero();
+                this.center = Vector2.Zero();
+                this.radius = 0;
+                return;
+            }
+
+            this.a = a.clone();
+            this.b = b.clone();
+            this.c = c.clone();
+
+            this._updateCenterRadius();
+        }
+
+        a: Vector2;
+        b: Vector2;
+        c: Vector2;
+        center: Vector2;
+        radius: number;
+
+        public static Zero() {
+            return new Tri2DInfo(null, null, null);
+        }
+
+        public set(a: Vector2, b: Vector2, c: Vector2) {
+            this.a.copyFrom(a);
+            this.b.copyFrom(b);
+            this.c.copyFrom(c);
+
+            this._updateCenterRadius();
+        }
+
+        public transformInPlace(transform: Matrix) {
+            Vector2.TransformToRef(this.a, transform, this.a);
+            Vector2.TransformToRef(this.b, transform, this.b);
+            Vector2.TransformToRef(this.c, transform, this.c);
+
+            this._updateCenterRadius();
+        }
+
+        private _updateCenterRadius() {
+            this.center.x = (this.a.x + this.b.x + this.c.x) / 3;
+            this.center.y = (this.a.y + this.b.y + this.c.y) / 3;
+
+            let la = Vector2.DistanceSquared(this.a, this.center);
+            let lb = Vector2.DistanceSquared(this.b, this.center);
+            let lc = Vector2.DistanceSquared(this.c, this.center);
+
+            let rs = Math.max(Math.max(la, lb), lc);
+            this.radius = Math.sqrt(rs);
+        }
+    }
+
+    export class Tri2DArray {
+        constructor(count: number) {
+            this._count = count;
+            this._array = new Float32Array(9 * count);
+        }
+
+        public clear(count: number) {
+            if (this._count === count) {
+                return;
+            }
+
+            this._count = count;
+            this._array = new Float32Array(9 * count);
+        }
+
+        public storeTriangle(index: number, a: Vector2, b: Vector2, c: Vector2) {
+            let center = new Vector2((a.x + b.x + c.x) / 3, (a.y + b.y + c.y) / 3);
+
+            let la = Vector2.DistanceSquared(a, center);
+            let lb = Vector2.DistanceSquared(b, center);
+            let lc = Vector2.DistanceSquared(c, center);
+
+            let rs = Math.max(Math.max(la, lb), lc);
+            let radius = Math.sqrt(rs);
+
+            let offset = index * 9;
+            this._array[offset + 0] = a.x;
+            this._array[offset + 1] = a.y;
+            this._array[offset + 2] = b.x;
+            this._array[offset + 3] = b.y;
+            this._array[offset + 4] = c.x;
+            this._array[offset + 5] = c.y;
+            this._array[offset + 6] = center.x;
+            this._array[offset + 7] = center.y;
+            this._array[offset + 8] = radius;
+        }
+
+        /**
+         * Store a triangle in a Tri2DInfo object
+         * @param index the index of the triangle to store
+         * @param tri2dInfo the instance that will contain the data, it must be already allocated with its inner object also allocated
+         */
+        public storeToTri2DInfo(index: number, tri2dInfo: Tri2DInfo) {
+            if (index >= this._count) {
+                throw new Error(`Can't fetch the triangle at index ${index}, max index is ${this._count - 1}`);
+            }
+
+            let offset = index * 9;
+            tri2dInfo.a.x      = this._array[offset + 0];
+            tri2dInfo.a.y      = this._array[offset + 1];
+            tri2dInfo.b.x      = this._array[offset + 2];
+            tri2dInfo.b.y      = this._array[offset + 3];
+            tri2dInfo.c.x      = this._array[offset + 4];
+            tri2dInfo.c.y      = this._array[offset + 5];
+            tri2dInfo.center.x = this._array[offset + 6];
+            tri2dInfo.center.y = this._array[offset + 7];
+            tri2dInfo.radius   = this._array[offset + 8];
+        }
+
+        public transformAndStoreToTri2DInfo(index: number, tri2dInfo: Tri2DInfo, transform: Matrix) {
+            if (index >= this._count) {
+                throw new Error(`Can't fetch the triangle at index ${index}, max index is ${this._count - 1}`);
+            }
+
+            let offset = index * 9;
+            tri2dInfo.a.x = this._array[offset + 0];
+            tri2dInfo.a.y = this._array[offset + 1];
+            tri2dInfo.b.x = this._array[offset + 2];
+            tri2dInfo.b.y = this._array[offset + 3];
+            tri2dInfo.c.x = this._array[offset + 4];
+            tri2dInfo.c.y = this._array[offset + 5];
+
+            tri2dInfo.transformInPlace(transform);
+        }
+
+        public get count(): number {
+            return this._count;
+        }
+
+        public static doesIntersect(setA: Tri2DArray, setB: Tri2DArray, bToATransform: Matrix): boolean {
+            Tri2DArray._checkInitStatics();
+
+            let a = Tri2DArray.tempT[0];
+            let b = Tri2DArray.tempT[1];
+            let v0 = Tri2DArray.tempV[0];
+
+            for (let curB = 0; curB < setB.count; curB++) {
+                setB.transformAndStoreToTri2DInfo(curB, b, bToATransform);
+
+                for (let curA = 0; curA < setA.count; curA++) {
+                    setA.storeToTri2DInfo(curA, a);
+
+
+                    // Fast rejection first
+                    v0.x = a.center.x - b.center.x;
+                    v0.y = a.center.y - b.center.y;
+                    if (v0.lengthSquared() > ((a.radius * a.radius) + (b.radius * b.radius))) {
+                        continue;
+                    }
+
+                    // Actual intersection test
+                    if (Math2D.TriangleTriangleDosIntersect(a.a, a.b, a.c, b.a, b.b, b.c)) {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        private static _checkInitStatics() {
+            if (Tri2DArray.tempT !== null) {
+                return;
+            }
+
+            Tri2DArray.tempT = new Array<Tri2DInfo>(2);
+            Tri2DArray.tempT[0] = new Tri2DInfo(null, null, null);
+            Tri2DArray.tempT[1] = new Tri2DInfo(null, null, null);
+
+            Tri2DArray.tempV = new Array<Vector2>(6);
+            for (let i = 0; i < 6; i++) {
+                Tri2DArray.tempV[i] = Vector2.Zero();
+            }
+        }
+
+        private _count: number;
+        private _array: Float32Array;
+
+        private static tempV: Vector2[] = null;
+        private static tempT: Tri2DInfo[] = null;
+    }
+
+    class Math2D {
+
+        static Dot(a: Vector2, b: Vector2): number {
+            return a.x * b.x + a.y * b.y;
+        }
+
+        // From http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
+        // Note: this one might also be considered with the above one proves to not be good enough: http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
+        static LineLineDoesIntersect(segA1: Vector2, segA2: Vector2, segB1: Vector2, segB2: Vector2): boolean {
+            let s1_x = segA2.x - segA1.x; let s1_y = segA2.y - segA1.y;
+            let s2_x = segB2.x - segB1.x; let s2_y = segB2.y - segB1.y;
+            let s = (-s1_y * (segA1.x - segB1.x) + s1_x * (segA1.y - segB1.y)) / (-s2_x * s1_y + s1_x * s2_y);
+            let t = ( s2_x * (segA1.y - segB1.y) - s2_y * (segA1.x - segB1.x)) / (-s2_x * s1_y + s1_x * s2_y);
+
+            if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
+                return true;
+            }
+
+            return false;
+        }
+
+        // From http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
+        static LineLineIntersection(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2): { res: boolean, xr: number, yr: number } {
+            let s1_x = p1.x - p0.x; let s1_y = p1.y - p0.y;
+            let s2_x = p3.x - p2.x; let s2_y = p3.y - p2.y;
+            let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / (-s2_x * s1_y + s1_x * s2_y);
+            let t = (s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / (-s2_x * s1_y + s1_x * s2_y);
+
+            if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
+                return { res: true, xr: p0.x + (t * s1_x), yr: p0.y + (t * s1_y) };
+            }
+
+            return { res: false, xr: 0, yr: 0 };
+        }
+
+        private static v0 = Vector2.Zero();
+        private static v1 = Vector2.Zero();
+        private static v2 = Vector2.Zero();
+
+        // Tell me that it's slow and I'll answer: yes it is!
+        // If you fancy to implement the SAT (Separating Axis Theorem) version: BE MY VERY WELCOMED GUEST!
+        static TriangleTriangleDosIntersect(tri1A: Vector2, tri1B: Vector2, tri1C: Vector2, tri2A: Vector2, tri2B: Vector2, tri2C: Vector2): boolean {
+            if (Math2D.LineLineDoesIntersect(tri1A, tri1B, tri2A, tri2B))     return true;
+            if (Math2D.LineLineDoesIntersect(tri1A, tri1B, tri2A, tri2C))     return true;
+            if (Math2D.LineLineDoesIntersect(tri1A, tri1B, tri2B, tri2C))     return true;
+
+            if (Math2D.LineLineDoesIntersect(tri1A, tri1C, tri2A, tri2B))     return true;
+            if (Math2D.LineLineDoesIntersect(tri1A, tri1C, tri2A, tri2C))     return true;
+            if (Math2D.LineLineDoesIntersect(tri1A, tri1C, tri2B, tri2C))     return true;
+
+            if (Math2D.LineLineDoesIntersect(tri1B, tri1C, tri2A, tri2B))     return true;
+            if (Math2D.LineLineDoesIntersect(tri1B, tri1C, tri2A, tri2C))     return true;
+            if (Math2D.LineLineDoesIntersect(tri1B, tri1C, tri2B, tri2C))     return true;
+
+            if (Vector2.PointInTriangle(tri2A, tri1A, tri1B, tri1C) &&
+                Vector2.PointInTriangle(tri2B, tri1A, tri1B, tri1C) &&
+                Vector2.PointInTriangle(tri2C, tri1A, tri1B, tri1C))           return true;
+
+            if (Vector2.PointInTriangle(tri1A, tri2A, tri2B, tri2C) &&
+                Vector2.PointInTriangle(tri1B, tri2A, tri2B, tri2C) &&
+                Vector2.PointInTriangle(tri1C, tri2A, tri2B, tri2C))           return true;
+
+            return false;
+        }
+    }
+
+}

+ 1 - 1
canvas2D/src/Tools/babylon.observableStringDictionary.ts

@@ -129,7 +129,7 @@
          * @return true if the operation completed successfully, false if we couldn't insert the key/value because there was already this key in the dictionary
          */
         public add(key: string, value: T): boolean {
-            return this._add(key, value, true, true);
+            return this._add(key, value, true, this._watchObjectsPropertyChange);
         }
 
         public getAndRemove(key: string): T {

+ 5 - 0
canvas2D/src/shaders/wireframe2d.fragment.fx

@@ -0,0 +1,5 @@
+varying vec4 vColor;
+
+void main(void) {
+	gl_FragColor = vColor;
+}

+ 40 - 0
canvas2D/src/shaders/wireframe2d.vertex.fx

@@ -0,0 +1,40 @@
+// based on if Instanced Array are supported or not, declare the field either as attribute or uniform
+#ifdef Instanced
+#define att attribute
+#else
+#define att uniform
+#endif
+
+// Attributes
+attribute vec2 pos;
+attribute vec4 col;
+
+// x, y, z are
+//  x : alignedToPixel: 1.0 === yes, otherwise no
+//  y : 1/renderGroup.Width
+//  z : 1/renderGroup.height
+att vec3 properties;
+
+att vec2 zBias;
+att vec4 transformX;
+att vec4 transformY;
+att float opacity;
+
+// Uniforms
+
+// Output
+varying vec4 vColor;
+
+void main(void) {
+
+	vec4 p = vec4(pos.xy, 1.0, 1.0);
+	vColor = vec4(col.xyz, col.w*opacity);
+
+	vec4 pp = vec4(dot(p, transformX), dot(p, transformY), zBias.x, 1);
+	
+	if (properties.x == 1.0) {
+		pp.xy = pp.xy - mod(pp.xy, properties.yz);
+	}
+
+	gl_Position = pp;
+}	

+ 2 - 0
tests/Canvas2d/Jasmine/chutzpah.json

@@ -93,6 +93,7 @@
         { "Path": "../../../src/Materials/babylon.multiMaterial.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.fontTexture.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.bounding2d.js" },
+        { "Path": "../../../canvas2d/src/Engine/babylon.primitiveCollisionManager.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.canvas2dLayoutEngine.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.brushes2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.smartPropertyPrim.js" },
@@ -101,6 +102,7 @@
         { "Path": "../../../canvas2d/src/Engine/babylon.renderablePrim2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.shape2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.group2d.js" },
+        { "Path": "../../../canvas2d/src/Engine/babylon.wireFrame2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.rectangle2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.sprite2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.text2d.js" },

+ 2 - 0
tests/Tools/Jasmine/chutzpah.json

@@ -93,6 +93,7 @@
         { "Path": "../../../src/Materials/babylon.multiMaterial.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.fontTexture.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.bounding2d.js" },
+        { "Path": "../../../canvas2d/src/Engine/babylon.primitiveCollisionManager.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.canvas2dLayoutEngine.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.brushes2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.smartPropertyPrim.js" },
@@ -101,6 +102,7 @@
         { "Path": "../../../canvas2d/src/Engine/babylon.renderablePrim2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.shape2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.group2d.js" },
+        { "Path": "../../../canvas2d/src/Engine/babylon.wireFrame2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.rectangle2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.sprite2d.js" },
         { "Path": "../../../canvas2d/src/Engine/babylon.text2d.js" },