Browse Source

Canvas2D bug fixes and Interaction in progress

BoundingInfo2D: now supports an optional origin argument in all the methods that needs it. Intersection support added.

Prim2DBase:
 - added isPickable property
 - added actualSize property
 - added localTransform to speed up transformations update
 - boundingInfo is computed faster and is now using the origin of its primitive for an accurate result.
 - added levelIntersect() method which must be implemented by all primitive to compute intersection!
 - added a public api: intersect() to perform intersection test

Sprite2D wasn't supporting origin correctly, now it does. Support actualSize and levelIntersect.

Rectangle: support actualSize and a approximated levelIntersect implementation (doesn't consider roundRadius right now)

Text2D: supports actualSize, NO levelIntersection yet!

Group2D: now support origin accurately, in nested hierarchy of prim2Dbase

Canvas2D: makes the background not pickable.
nockawa 9 years ago
parent
commit
869c4761ab

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

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

+ 2 - 1
src/Canvas2d/babylon.canvas2d.ts

@@ -141,6 +141,7 @@
 
             if (cachingstrategy !== Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
                 this._background = Rectangle2D.Create(this, "###CANVAS BACKGROUND###", 0, 0, size.width, size.height);
+                this._background.isPickable = false;
                 this._background.origin = Vector2.Zero();
                 this._background.levelVisible = false;
             }
@@ -434,7 +435,7 @@
                 // Create a Sprite that will be used to render this cache, the "__cachedSpriteOfGroup__" starting id is a hack to bypass exception throwing in case of the Canvas doesn't normally allows direct primitives
                 else {
                     let sprite = Sprite2D.Create(parent, `__cachedSpriteOfGroup__${group.id}`, group.position.x, group.position.y, map, node.contentSize, node.pos, false);
-                    sprite.origin = Vector2.Zero();
+                    sprite.origin = group.origin.clone();
                     res.sprite = sprite;
                 }
             }

+ 3 - 1
src/Canvas2d/babylon.group2d.ts

@@ -43,7 +43,7 @@
         static _createCachedCanvasGroup(owner: Canvas2D): Group2D {
             var g = new Group2D();
             g.setupGroup2D(owner, null, "__cachedCanvasGroup__", Vector2.Zero());
-
+            g.origin = Vector2.Zero();
             return g;
             
         }
@@ -412,6 +412,8 @@
                 this._cacheRenderSprite.rotation = this.rotation;
             } else if (prop.id === Prim2DBase.scaleProperty.id) {
                 this._cacheRenderSprite.scale = this.scale;
+            } else if (prop.id === Prim2DBase.originProperty.id) {
+                this._cacheRenderSprite.origin = this.origin.clone();
             } else if (prop.id === Group2D.actualSizeProperty.id) {
                 this._cacheRenderSprite.spriteSize = this.actualSize.clone();
                 //console.log(`[${this._globalTransformProcessStep}] Sync Sprite ${this.id}, width: ${this.actualSize.width}, height: ${this.actualSize.height}`);

+ 147 - 10
src/Canvas2d/babylon.prim2dBase.ts

@@ -4,6 +4,35 @@
         forceRefreshPrimitive: boolean;
     }
 
+    export class IntersectInfo2D {
+        constructor() {
+            this.findFirstOnly = false;
+            this.intersectHidden = false;
+        }
+
+        // Input settings, to setup before calling an intersection related method
+        public pickPosition: Vector2;
+        public findFirstOnly: boolean;
+        public intersectHidden: boolean;
+
+        // Intermediate data, don't use!
+        public _globalPickPosition: Vector2;
+        public _localPickPosition: Vector2;
+
+        // Output settings, up to date in return of a call to an intersection related method
+        public intersectedPrimitives: Array<{ prim: Prim2DBase, intersectionLocation: Vector2 }>;
+        public get isIntersected(): boolean {
+            return this.intersectedPrimitives && this.intersectedPrimitives.length > 0;
+        }
+
+        // Internals, don't use
+        public _exit(firstLevel: boolean) {
+            if (firstLevel) {
+                this._globalPickPosition = null;
+            }
+        }
+    }
+
     @className("Prim2DBase")
     export class Prim2DBase extends SmartPropertyPrim {
         static PRIM2DBASE_PROPCOUNT: number = 10;
@@ -14,6 +43,7 @@
             }
 
             this.setupSmartPropertyPrim();
+            this._isPickable = true;
             this._boundingInfoDirty = true;
             this._boundingInfo = new BoundingInfo2D();
             this._owner = owner;
@@ -109,6 +139,14 @@
         }
 
         /**
+         * this method must be implemented by the primitive type to return its size
+         * @returns The size of the primitive
+         */
+        public get actualSize(): Size {
+            return undefined;
+        }
+
+        /**
          * The origin defines the normalized coordinate of the center of the primitive, from the top/left corner.
          * The origin is used only to compute transformation of the primitive, it has no meaning in the primitive local frame of reference
          * For instance:
@@ -148,6 +186,14 @@
             this._zOrder = value;
         }
 
+        public get isPickable(): boolean {
+            return this._isPickable;
+        }
+
+        public set isPickable(value: boolean) {
+            this._isPickable = value;
+        }
+
         public get hierarchyDepth(): number {
             return this._hierarchyDepth;
         }
@@ -164,6 +210,11 @@
             return this._invGlobalTransform;
         }
 
+        public get localTransform(): Matrix {
+            this._updateLocalTransform();
+            return this._localTransform;
+        }
+
         public get boundingInfo(): BoundingInfo2D {
             if (this._boundingInfoDirty) {
                 this._boundingInfo = this.levelBoundingInfo.clone();
@@ -171,8 +222,7 @@
 
                 var tps = new BoundingInfo2D();
                 for (let curChild of this._children) {
-                    let t = curChild.globalTransform.multiply(this.invGlobalTransform);
-                    curChild.boundingInfo.transformToRef(t, curChild.origin, tps);
+                    curChild.boundingInfo.transformToRef(this.localTransform, tps);
                     bi.unionToRef(tps, bi);
                 }
 
@@ -181,6 +231,72 @@
             return this._boundingInfo;
         }
 
+        protected levelIntersect(intersectInfo: IntersectInfo2D): boolean {
+
+            return false;
+        }
+
+        public intersect(intersectInfo: IntersectInfo2D): boolean {
+            if (!intersectInfo) {
+                return false;
+            }
+
+            // If this is null it means this method is call for the first level, initialize stuffs
+            let firstLevel = !intersectInfo._globalPickPosition;
+            if (firstLevel) {
+                // Compute the pickPosition in global space and use it to find the local position for each level down, always relative from the world to get the maximum accuracy (and speed). The other way would have been to compute in local every level down relative to its parent's local, which wouldn't be as accurate (even if javascript number is 80bits accurate).
+                intersectInfo._globalPickPosition = Vector2.Zero();
+                Vector2.TransformToRef(intersectInfo.pickPosition, this.globalTransform, intersectInfo._globalPickPosition);
+                intersectInfo._localPickPosition = intersectInfo.pickPosition.clone();
+                intersectInfo.intersectedPrimitives = new Array<{ prim: Prim2DBase, intersectionLocation: Vector2 }>();
+            }
+
+            if (!intersectInfo.intersectHidden && !this.isVisible) {
+                return false;
+            }
+
+            // Fast rejection test with boundingInfo
+            if (!this.boundingInfo.doesIntersect(intersectInfo._localPickPosition)) {
+                // Important to call this before each return to allow a good recursion next time this intersectInfo is reused
+                intersectInfo._exit(firstLevel);
+                return false;
+            }
+
+            // We hit the boundingInfo that bounds this primitive and its children, now we have to test on the primitive of this level
+            let levelIntersectRes = this.levelIntersect(intersectInfo);
+            if (levelIntersectRes) {
+                intersectInfo.intersectedPrimitives.push({ prim: this, intersectionLocation: intersectInfo._localPickPosition});
+
+                // If we must stop at the first intersection, we're done, quit!
+                if (intersectInfo.findFirstOnly) {
+                    intersectInfo._exit(firstLevel);
+                    return true;
+                }
+            }
+
+            // Recurse to children if needed
+            if (!levelIntersectRes || !intersectInfo.findFirstOnly) {
+                for (let curChild of this._children) {
+                    // Don't test primitive not pick able or if it's hidden and we don't test hidden ones
+                    if (!curChild.isPickable || (!intersectInfo.intersectHidden && !curChild.isVisible)) {
+                        continue;
+                    }
+
+                    // Must compute the localPickLocation for the children level
+                    Vector2.TransformToRef(intersectInfo._globalPickPosition, curChild.invGlobalTransform, intersectInfo._localPickPosition);
+
+                    // If we got an intersection with the child and we only need to find the first one, quit!
+                    if (curChild.intersect(intersectInfo) && intersectInfo.findFirstOnly) {
+                        intersectInfo._exit(firstLevel);
+                        return true;
+                    }
+                }
+            }
+
+            intersectInfo._exit(firstLevel);
+            return intersectInfo.isIntersected;
+        }
+
         public moveChild(child: Prim2DBase, previous: Prim2DBase): boolean {
             if (child.parent !== this) {
                 return false;
@@ -299,6 +415,28 @@
             }
         }
 
+        private _updateLocalTransform(): boolean {
+            let tflags = Prim2DBase.positionProperty.flagId | Prim2DBase.rotationProperty.flagId | Prim2DBase.scaleProperty.flagId;
+            if (this.checkPropertiesDirty(tflags)) {
+                var rot = Quaternion.RotationAxis(new Vector3(0, 0, 1), this._rotation);
+                var local = Matrix.Compose(new Vector3(this._scale, this._scale, this._scale), rot, new Vector3(this._position.x, this._position.y, 0));
+
+                this._localTransform = local;
+                this.clearPropertiesDirty(tflags);
+
+                // this is important to access actualSize AFTER fetching a first version of the local transform and reset the dirty flag, because accessing actualSize on a Group2D which actualSize is built from its content will trigger a call to this very method on this very object. We won't mind about the origin offset not being computed, as long as we return a local transform based on the position/rotation/scale
+                var actualSize = this.actualSize;
+                if (!actualSize) {
+                    throw new Error(`The primitive type: ${Tools.getClassName(this)} must implement the actualSize get property!`);
+                }
+
+                local.m[12] -= (actualSize.width * this.origin.x) * local.m[0] + (actualSize.height * this.origin.y) * local.m[4];
+                local.m[13] -= (actualSize.width * this.origin.x) * local.m[1] + (actualSize.height * this.origin.y) * local.m[5];
+                return true;
+            }
+            return false;
+        }
+
         protected updateGlobalTransVis(recurse: boolean) {
             if (this.isDisposed) {
                 return;
@@ -317,19 +455,16 @@
                 // Detect a change of visibility
                 this._visibilityChanged = (curVisibleState !== undefined) && curVisibleState !== this.isVisible;
 
-                // Detect if either the parent or this node changed
-                let tflags = Prim2DBase.positionProperty.flagId | Prim2DBase.rotationProperty.flagId | Prim2DBase.scaleProperty.flagId;
-                if (this.isVisible && (this._parent && this._parent._globalTransformStep !== this._parentTransformStep) || this.checkPropertiesDirty(tflags)) {
-                    var rot = Quaternion.RotationAxis(new Vector3(0, 0, 1), this._rotation);
-                    var local = Matrix.Compose(new Vector3(this._scale, this._scale, this._scale), rot, new Vector3(this._position.x, this._position.y, 0));
+                // Get/compute the localTransform
+                let localDirty = this._updateLocalTransform();
 
-                    this._globalTransform = this._parent ? local.multiply(this._parent._globalTransform) : local;
+                // Check if we have to update the globalTransform
+                if (!this._globalTransform || localDirty || (this._parent && this._parent._globalTransformStep !== this._parentTransformStep)) {
+                    this._globalTransform = this._parent ? this._localTransform.multiply(this._parent._globalTransform) : this._localTransform;
                     this._invGlobalTransform = Matrix.Invert(this._globalTransform);
 
                     this._globalTransformStep = this.owner._globalTransformProcessStep + 1;
                     this._parentTransformStep = this._parent ? this._parent._globalTransformStep : 0;
-
-                    this.clearPropertiesDirty(tflags);
                 }
                 this._globalTransformProcessStep = this.owner._globalTransformProcessStep;
             }
@@ -353,6 +488,7 @@
         private _levelVisible: boolean;
         public _boundingInfoDirty: boolean;
         protected _visibilityChanged;
+        private _isPickable;
         private _isVisible: boolean;
         private _id: string;
         private _position: Vector2;
@@ -370,6 +506,7 @@
 
         // Stores the previous 
         protected _globalTransformProcessStep: number;
+        protected _localTransform: Matrix;
         protected _globalTransform: Matrix;
         protected _invGlobalTransform: Matrix;
     }

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

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

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

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

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

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

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

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