Source: display/DisplayGroup.js

//------------------------------------------------------------------------------
// Constructor scope
//------------------------------------------------------------------------------

/**
 * Creates a new instance of the DisplayGroup class.
 *
 * @constructor
 *
 * @param {rune.display.DisplayObjectContainer} container Display object container object that will contain members.
 *
 * @class
 * @classdesc
 * 
 * The DisplayGroup class represents a collection (usually a subset) of display 
 * objects from one and the same display object container. The group offers 
 * easy access to the collection as you do not have to go through the entire 
 * display object container to find them. The contents of the group are 
 * automatically synchronized with the contents of the display object 
 * container, ie if an object is added to the group, it is also added as a 
 * child object to the container. A group also offers fast and easy collision 
 * handling of display objects that are part of the group, but the group is not 
 * limited to handling internal collisions; it is also possible to hit test 
 * against objects that are not group members.
 */
rune.display.DisplayGroup = function(container) {
    
    //--------------------------------------------------------------------------
    // Public properties
    //--------------------------------------------------------------------------
    
    /**
     * Whether the group is active (true) or not (false). Inactive groups are 
     * not automatically updated by the group manager.
     *
     * @type {boolean}
     * @default true
     */
    this.active = true;
    
    //--------------------------------------------------------------------------
    // Protected properties
    //--------------------------------------------------------------------------
    
    /**
     * Internal cache for the member's calculated area. This is calculated for 
     * each update when the object is active. If the object is inactive, the 
     * area is calculated on request, ie when the getArea() method is called.
     *
     * @type {rune.geom.Rectangle}
     * @protected
     * @ignore
     */
    this.m_area = new rune.geom.Rectangle();
    
    /**
     * The display object container that contains all group members.
     *
     * @type {rune.display.DisplayObjectContainer}
     * @protected
     * @ignore
     */
    this.m_container = container;
    
    /**
     * Register of group members. If an object is a member, it must be present 
     * as a child to the display object container.
     *
     * @type {Array.<rune.display.DisplayObject>}
     * @protected
     * @ignore
     */
    this.m_members = [];
    
    /**
     * Internal quad tree.
     *
     * @type {rune.display.Quadtree}
     * @protected
     * @ignore
     */
    this.m_quadtree = null;
};

//------------------------------------------------------------------------------
// Public prototype getter and setter methods
//------------------------------------------------------------------------------

/**
 * Reference to the application's entry point class, ie. the main class of the 
 * application. Useful for accessing the application's subsystem.
 *
 * @member {rune.system.Main} application
 * @memberof rune.display.DisplayGroup
 * @instance
 * @readonly
 */
Object.defineProperty(rune.display.DisplayGroup.prototype, "application", {
    /**
     * @this rune.display.DisplayGroup
     * @ignore
     */
    get : function() {
        return rune.system.Application['instance'];
    }
});

/**
 * Reference to the current scene's camera system.
 *
 * @member {rune.camera.Cameras} cameras
 * @memberof rune.display.DisplayGroup
 * @instance
 * @readonly
 */
Object.defineProperty(rune.display.DisplayGroup.prototype, "cameras", {
    /**
     * @this rune.display.DisplayGroup
     * @ignore
     */
    get : function() {
        return this['application']['scenes']['selected']['cameras'];
    }
});

/**
 * Reference to the display object container to which the group is linked. When 
 * new members are added to the group, they are also added to the display 
 * object container's display list.
 *
 * @member {rune.display.DisplayObject} container
 * @memberof rune.display.DisplayGroup
 * @instance
 * @readonly
 */
Object.defineProperty(rune.display.DisplayGroup.prototype, "container", {
    /**
     * @this rune.display.DisplayGroup
     * @ignore
     */
    get : function() {
        return this.m_container;
    }
});

/**
 * Reference to the application's subsystem for connected gamepad devices.
 *
 * @member {rune.input.Gamepads} gamepads
 * @memberof rune.display.DisplayGroup
 * @instance
 * @readonly
 */
Object.defineProperty(rune.display.DisplayGroup.prototype, "gamepads", {
    /**
     * @this rune.display.DisplayGroup
     * @ignore
     */
    get : function() {
        return rune.system.Application['instance']['inputs']['gamepads'];
    }
});

/**
 * Reference to the subsystem representing the keyboard input.
 *
 * @member {rune.input.Keyboard} keyboard
 * @memberof rune.display.DisplayGroup
 * @instance
 * @readonly
 */
Object.defineProperty(rune.display.DisplayGroup.prototype, "keyboard", {
    /**
     * @this rune.display.DisplayGroup
     * @ignore
     */
    get : function() {
        return rune.system.Application['instance']['inputs']['keyboard'];
    }
});

/**
 * The number of members included in the group. Note that this number is not 
 * necessarily the same as the number of children in the display object 
 * container to which the group is linked.
 *
 * @member {number} numMembers
 * @memberof rune.display.DisplayGroup
 * @instance
 * @readonly
 */
Object.defineProperty(rune.display.DisplayGroup.prototype, "numMembers", {
    /**
     * @this rune.display.DisplayGroup
     * @ignore
     */
    get : function() {
        return this.m_members.length;
    }
});

/**
 * Whether the group should use a quad tree to improve hit testing performance. 
 * Note that the quad tree can not guarantee noticeably better performance, but 
 * it should be evaluated when you experience performance problems with the 
 * quad tree disabled.
 *
 * @member {boolean} useQuadtree
 * @memberof rune.display.DisplayGroup
 * @instance
 */
Object.defineProperty(rune.display.DisplayGroup.prototype, "useQuadtree", {
    /**
     * @this rune.display.DisplayGroup
     * @ignore
     */
    get : function() {
        return (this.m_quadtree != null);
    },
    
    /**
     * @this rune.display.DisplayGroup
     * @ignore
     */
    set : function(value) {
        if (this.m_quadtree == null && value == true) {
            this.m_constructQuadtree();
        } else {
            this.m_disposeQuadtree();
        }
    },
});

//------------------------------------------------------------------------------
// Public prototype methods (API)
//------------------------------------------------------------------------------

/**
 * Adds a member to the display group. Note that attempts to add existing 
 * members are denied. The method always returns a boolean that indicates 
 * whether the extension was successful (true) or not (false). When an item is 
 * added to a group, it is automatically removed from any other groups, and it 
 * is also removed from other display lists. This means that an object can only 
 * be a member of one group at a time and that it is always available in the 
 * group's container object view list as long as it is a member of that group.
 *
 * @param {rune.display.DisplayObject} member Prospect to add.
 *
 * @returns {boolean}
 */
rune.display.DisplayGroup.prototype.addMember = function(member) {
    var index = this.m_members.indexOf(member);
    if (index === -1) {
        if (this.m_container.hasChild(member) == false) {
            this.m_container.addChild(member);
        }
        
        this.m_members.push(member);
        member.setGroup(this);
        
        return true;
    }
    
    return false;
};

/**
 * Executes a callback method for each member of the group. Reference to the 
 * current member and its member index are sent to the callback method as 
 * arguments.
 *
 * @param {Function} callback Call function.
 * @param {Object} scope Scope of execution.
 *
 * @returns {undefined}
 */
rune.display.DisplayGroup.prototype.forEachMember = function(callback, scope) {
    for (var i = 0; i < this.m_members.length; i++) {
        callback.call(scope, this.m_members[i], i);
    }
};

/**
 * Calculates the geometric area that the members of the group allocate. 
 * The method returns a rectangle object that encloses all members of the group.
 *
 * @param {rune.geom.Rectangle} [rect] Rectangle to overwrite, if nothing is specified, a new rectangle object is returned.
 *
 * @returns {rune.geom.Rectangle}
 * @private
 */
rune.display.DisplayGroup.prototype.getArea = function(rect) {
    if (this.active == true) {
        if (rect != null) {
            return rune.geom.Rectangle.copy(this.m_area, rect);
        } else {
            return this.m_area;
        }
    } else {
        return this.m_calculateArea(rect);
    }
};

/**
 * Retrieves a member object at a specific index. Note that the member index is 
 * not necessarily the same as the object's child index in the display object 
 * container.
 *
 * @param {number} index The index position of the member object.
 *
 * @throws {RangeError} Throws if the index does not exist in the member list.
 *
 * @returns {rune.display.DisplayObject} The member object at the specified index position.
 */
rune.display.DisplayGroup.prototype.getMemberAt = function(index) {
    if (index > -1 && index < this.m_members.length) {
        return this.m_members[index];
    } else throw new RangeError();
};

/**
 * Retrieves the list of all members. This method is primarily intended for 
 * internal use. Consider it read-only, as changes in the structure can cause 
 * problems for the group.
 *
 * @returns {Array.<rune.display.DisplayObject>}
 */
rune.display.DisplayGroup.prototype.getMembers = function() {
    return this.m_members;
};

/**
 * If the useQuadtree property is set to true, only member objects that are in 
 * close proximity to the argument object are returned; otherwise a list of all 
 * members is returned.
 *
 * @param {rune.display.DisplayObject} obj Main object.
 *
 * @return {Array.<rune.display.DisplayObject>}
 */
rune.display.DisplayGroup.prototype.getMembersCloseTo = function(obj) {
    if (this.m_quadtree != null) {
        var prospects = this.m_quadtree.retrieve(obj);
        var i = prospects.indexOf(obj);
        if (i > -1) {
            prospects.splice(i, 1);
        }
        
        return prospects;
    }
    
    return this.m_members;
};

/**
 * Returns whether an object is a member of the group (true) or not (false.)
 *
 * @param {rune.display.DisplayObject} prospect Main object.
 *
 * @returns {boolean}
 */
rune.display.DisplayGroup.prototype.hasMember = function(prospect) {
    return (this.m_members.indexOf(prospect) > -1) ? true : false;
};

/**
 * Performs hit detection between an object and all members of the group. For 
 * each hit, a callback method is also executed, which attaches the two objects 
 * that collide as arguments to the method.
 *
 * @param {rune.display.DisplayObject|rune.display.DisplayGroup|rune.geom.Point|Array} obj Object to test against.
 * @param {Function} [callback] Method that is called when a collision is detected.
 * @param {Object} [scope] Scope of execution of the callback method.
 *
 * @returns {boolean} Returns true if any hit was found, otherwise false.
 */
rune.display.DisplayGroup.prototype.hitTest = function(obj, callback, scope) {
    if      (obj instanceof rune.display.DisplayGroup)  return this.hitTestGroup(obj,  callback, scope);
    else if (obj instanceof rune.display.DisplayObject) return this.hitTestObject(obj, callback, scope);
    else if (obj instanceof rune.geom.Point)            return this.hitTestPoint(obj,  callback, scope);
    else if (obj instanceof Array)                      return this.hitTestContentOf(obj,  callback, scope);
    else                                                return false;
};

/**
 * Performs hit detection between an object and all members of the group. If a 
 * hit is detected, the objects are separated according to their physical 
 * properties. For each hit, a callback method is also executed, which attaches 
 * the two objects that collide as arguments to the method.
 *
 * @param {rune.display.DisplayObject|rune.display.DisplayGroup|Array} obj Object to test against.
 * @param {Function} [callback] Method that is called when a collision is detected.
 * @param {Object} [scope] Scope of execution of the callback method.
 *
 * @returns {boolean} Returns true if any hit was found, otherwise false.
 */
rune.display.DisplayGroup.prototype.hitTestAndSeparate = function(obj, callback, scope) {
    if      (obj instanceof rune.display.DisplayGroup)  return this.hitTestAndSeparateGroup(obj,  callback, scope);
    else if (obj instanceof rune.display.DisplayObject) return this.hitTestAndSeparateObject(obj, callback, scope);
    else if (obj instanceof Array)                      return this.hitTestAndSeparateContentOf(obj, callback, scope);
    else                                                return false;
};

/**
 * Evaluates and handles collisions between group member hitboxes and the 
 * hitboxes of objects in an array.
 *
 * @param {Array} array The array to test against.
 * @param {Function} [callback] Method that is called when a collision is detected.
 * @param {Object} [scope] Scope of execution of the callback method.
 *
 * @returns {boolean}
 */
rune.display.DisplayGroup.prototype.hitTestAndSeparateContentOf = function(array, callback, scope) {
    var collide = false;
    this.forEachMember(function(member) {
        for (var i = 0; i < array.length; i++) {
            if (member.hitTestAndSeparate(array[i], callback, scope)) {
                collide = true;
            }
        }
    }, this);
    
    return collide;
};

/**
 * Performs hit detection between members of this and another group. If a hit 
 * is detected, the objects are separated based on their physical properties. 
 * For each hit, a callback method is also executed, which attaches the two 
 * objects that collide as arguments to the method.
 *
 * @param {rune.display.DisplayGroup} group Group to test against.
 * @param {Function} [callback] Method that is called when a collision is detected.
 * @param {Object} [scope] Scope of execution of the callback method.
 *
 * @returns {boolean} True if there was a collision, otherwise false.
 */
rune.display.DisplayGroup.prototype.hitTestAndSeparateGroup = function(group, callback, scope) {
    var a = this.getMembers();
    var b = null;
    var c = false;
    
    for (var x = 0; x < a.length; x++) {
        b = group.getMembersCloseTo(a[x]);
        for (var y = 0; y < b.length; y++) {
            if (a[x] != b[y]) {
                if (a[x].hitTestAndSeparateObject(b[y], callback, scope)) {
                    c = true;
                }
            }
        }
    }
    
    return c;
};

/**
 * Performs hit detection between members of this group and another display 
 * object. If a hit is detected, the objects are separated based on their 
 * physical properties. For each hit, a callback method is also executed, which 
 * attaches the two objects that collide as arguments to the method.
 *
 * @param {rune.display.DisplayObject} obj Object to test against.
 * @param {Function} [callback] Method that is called when a collision is detected.
 * @param {Object} [scope] Scope of execution of the callback method.
 *
 * @returns {boolean}
 */
rune.display.DisplayGroup.prototype.hitTestAndSeparateObject = function(obj, callback, scope) {
    var members = this.getMembersCloseTo(obj);
    var collide = false;
    
    for (var i = 0; i < members.length; i++) {
        if (members[i].hitTestAndSeparateObject(obj, callback, scope)) {
            collide = true;
        }
    }
    
    return collide;
};

/**
 * Evaluates collision between the hitboxes of group members and the hitboxes 
 * of objects in an array.
 *
 * @param {Array} array The array to test against.
 * @param {Function} [callback] Method that is called when a collision is detected.
 * @param {Object} [scope] Scope of execution of the callback method.
 *
 * @returns {boolean}
 */
rune.display.DisplayGroup.prototype.hitTestContentOf = function(array, callback, scope) {
    var collide = false;
    this.forEachMember(function(member) {
        for (var i = 0; i < array.length; i++) {
            if (member.hitTest(array[i], callback, scope)) {
                collide = true;
            }
        }
    }, this);
    
    return collide;
};

/**
 * Hit detect all members of the group against the members of (the same or) 
 * another group.
 *
 * @param {rune.display.DisplayGroup} group The group to test against.
 * @param {Function} [callback] Method that is called when a collision is detected.
 * @param {Object} [scope] Scope of execution of the callback method.
 *
 * @returns {boolean}
 */
rune.display.DisplayGroup.prototype.hitTestGroup = function(group, callback, scope) {
    var a = this.getMembers();
    var b = null;
    var c = false;
    
    for (var x = 0; x < a.length; x++) {
        b = group.getMembersCloseTo(a[x]);
        for (var y = 0; y < b.length; y++) {
            if (a[x] != b[y]) {
                if (a[x].hitTestObject(b[y], callback, scope)) {
                    c = true;
                }
            }
        }
    }
    
    return c;
};

/**
 * Hit detect all members of the group against another display object.
 *
 * @param {rune.display.DisplayObject} obj The display object to test against.
 * @param {Function} [callback] Method that is called when a collision is detected.
 * @param {Object} [scope] Scope of execution of the callback method.
 *
 * @returns {boolean}
 */
rune.display.DisplayGroup.prototype.hitTestObject = function(obj, callback, scope) {
    var members = this.getMembersCloseTo(obj);
    var collide = false;
    
    for (var i = 0; i < members.length; i++) {
        if (members[i].hitTestObject(obj, callback, scope)) {
            collide = true;
        }
    }
    
    return collide;
};

/**
 * Hit Detect all members of the group against a point. The method returns true 
 * if the point overlaps one of the group members, otherwise false is returned.
 *
 * @param {rune.geom.Point} point Point to test against.
 * @param {Function} [callback] Method that is called when a collision is detected.
 * @param {Object} [scope] Scope of execution of the callback method.
 *
 * @returns {boolean}
 */
rune.display.DisplayGroup.prototype.hitTestPoint = function(point, callback, scope) {
    var collide = false;
    var area = this.getArea();
    
    if (area && area.containsPoint(point)) {
        var members = this.getMembers();
        for (var i = 0; i < members.length; i++) {
            if (members[i].hitTestPoint(point, callback, scope)) {
                collide = true;
            }
        }
    }
    
    return collide;
};

/**
 * Removes a member from the group. Note that the member will also be removed 
 * from the display object's display list. It is not possible to be a member 
 * of the group without being available in the container object's display list.
 *
 * @param {rune.display.DisplayObject} member Member to be removed.
 * @param {boolean} dispose Whether the member should be destroyed when it is removed.
 *
 * @returns {rune.display.DisplayObject}
 */
rune.display.DisplayGroup.prototype.removeMember = function(member, dispose) {
    var index = this.m_members.indexOf(member);
    if (index > -1) {
        
        this.m_members.splice(index, 1);
        
        if (member['parent'] == this.m_container) {
            this.m_container.removeChild(member, false);
        }
        
        if (dispose == true) {
            member.dispose();
            member = null;
        }
    }
    
    return (dispose) ? null : member;
};

/**
 * Removes all members from the group.
 *
 * @param {boolean} dispose Whether the members should be destroyed when they are removed.
 *
 * @returns {undefined}
 */
rune.display.DisplayGroup.prototype.removeMembers = function(dispose) {
    while (this.m_members.length > 0) {
        this.removeMember(this.getMemberAt(0), dispose);
    }
};

//------------------------------------------------------------------------------
// Public prototype methods (ENGINE)
//------------------------------------------------------------------------------

/**
 * This method is automatically called once by the group manager when the group 
 * is created, or added. The method is public but should be considered 
 * internal. External calls should only be made if 1. The group is disabled, or 
 * 2. If the group is not added to a manager.
 *
 * @returns {undefined}
 */
rune.display.DisplayGroup.prototype.init = function() {
    //@note: Override from sub class.
};

/**
 * This method is called just before the group's update method and performs 
 * processes that must be completed before the group's main update process 
 * begins. Avoid overriding and modifying this method.
 *
 * @param {number} step Current time step.
 *
 * @returns {undefined}
 */
rune.display.DisplayGroup.prototype.preUpdate = function(step) {
    this.m_updateArea(step);
    this.m_updateQuadtree(step);
};

/**
 * This method represents the group's main update process. The method can be 
 * overridden in order to add custom logic.
 *
 * @param {number} step Current time step.
 *
 * @returns {undefined}
 */
rune.display.DisplayGroup.prototype.update = function(step) {
    //@note: Override from sub class.
};

/**
 * This method is called automatically after the group's main update method is 
 * completed. The method can be overridden and used to complete closing 
 * processes just before the next update.
 *
 * @param {number} step Current time step.
 *
 * @returns {undefined}
 */
rune.display.DisplayGroup.prototype.postUpdate = function(step) {
    //@note: Override from sub class.
};

/**
 * This method is called automatically just before the group is destroyed. 
 * Override the method for creating your own deallocation code, ie code that 
 * deletes objects created by the group.
 *
 * @returns {undefined}
 */
rune.display.DisplayGroup.prototype.dispose = function() {
    this.removeMembers(true);
    this.m_disposeQuadtree();
};

//------------------------------------------------------------------------------
// Internal prototype methods
//------------------------------------------------------------------------------

/**
 * Adds a new member to the group without adding it to the group container 
 * display list. Note that this method is internal and should therefore only 
 * be used by classes and objects within the display package.
 *
 * @param {rune.display.DisplayObject} member Display objects to add.
 *
 * @returns {boolean}
 * @package
 * @ignore
 */
rune.display.DisplayGroup.prototype.include = function(member) {
    var index = this.m_members.indexOf(member);
    if (index === -1) {
        this.m_members.push(member);
        member.setGroup(this);
        
        return true;
    }
    
    return false;
};

/**
 * Deletes a member from the group without removing it from the display list of 
 * the group's container object. Note that this method is internal and should 
 * therefore only be used by classes and objects within the display package.
 *
 * @param {rune.display.DisplayObject} member Display objects to remove.
 *
 * @returns {boolean}
 * @package
 * @ignore
 */
rune.display.DisplayGroup.prototype.exclude = function(member) {
    var index = this.m_members.indexOf(member);
    if (index > -1) {
        this.m_members.splice(index, 1);
        member.setGroup(null);
        
        return true;
    }
    
    return false;
};

//------------------------------------------------------------------------------
// Private prototype methods
//------------------------------------------------------------------------------

/**
 * Creates the group's internal quad tree.
 *
 * @throws {Error} If the object already exists.
 *
 * @returns {undefined}
 * @private
 */
rune.display.DisplayGroup.prototype.m_constructQuadtree = function() {
    this.m_disposeQuadtree();
    if (this.m_quadtree == null) {
        this.m_quadtree = new rune.display.Quadtree();
    } else throw new Error();
};

/**
 * Updates the group's internal quad tree.
 *
 * @param {number} step Current time step.
 *
 * @returns {undefined}
 * @private
 */
rune.display.DisplayGroup.prototype.m_updateQuadtree = function(step) {
    if (this.active == true && this.m_quadtree != null) {
        var rect = this.getArea();
        
        this.m_quadtree.clear();
        
        this.m_quadtree.x = rect.x;
        this.m_quadtree.y = rect.y;
        this.m_quadtree.width  = rect.width;
        this.m_quadtree.height = rect.height;
                
        var m = this.m_members;
        var i = m.length;
        
        while (i--) {
            this.m_quadtree.insert(m[i]);
        }  
    } 
};

/**
 * Updates the member area.
 *
 * @param {number} step Current time step.
 *
 * @returns {undefined}
 * @private
 */
rune.display.DisplayGroup.prototype.m_updateArea = function(step) {
    if (this.active == true) {
        this.m_area = this.m_calculateArea(this.m_area);  
    }
};

/**
 * Destroy the internal quad tree.
 *
 * @returns {undefined}
 * @private
 */
rune.display.DisplayGroup.prototype.m_disposeQuadtree = function() {
    if (this.m_quadtree instanceof rune.display.Quadtree) {
        this.m_quadtree.clear();
        this.m_quadtree = null;
    }
};

/**
 * Calculates the member area, ie the rectangular area within all group members can fit.
 *
 * @param {rune.geom.Rectangle} [rect] Rectangle to overwrite, if nothing is specified, a new rectangle object is returned.
 *
 * @returns {rune.geom.Rectangle}
 * @private
 */
rune.display.DisplayGroup.prototype.m_calculateArea = function(rect) {
    //@todo Does not work correctly within negative coordinates
    rect = rect || new rune.geom.Rectangle();
    
    var min = new rune.geom.Point(Number.MAX_VALUE, Number.MAX_VALUE);
    var max = new rune.geom.Point(Number.MIN_VALUE, Number.MIN_VALUE);
    
    var i = this.m_members.length;
    if (i > 0) {
        while (i--) {
            min['x'] = Math.min(min['x'], this.m_members[i]['left']);
            min['y'] = Math.min(min['y'], this.m_members[i]['top']);
            max['x'] = Math.max(max['x'], this.m_members[i]['right']);
            max['y'] = Math.max(max['y'], this.m_members[i]['bottom']);
        }
    } else {
        min['x'] = 0;
        min['y'] = 0;
        max['x'] = 0;
        max['y'] = 0;
    }
    
    rect['x'] = min['x'];
    rect['y'] = min['y'];
    rect['width']  = Math.abs(max['x'] - min['x']);
    rect['height'] = Math.abs(max['y'] - min['y']);
    
    return rect;
};