Source: framework/entity.js

Object.assign(pc, function () {
    /**
     * @constructor
     * @name pc.Entity
     * @classdesc The Entity is the core primitive of a PlayCanvas game. Generally speaking an object in your game will consist of an {@link pc.Entity},
     * and a set of {@link pc.Component}s which are managed by their respective {@link pc.ComponentSystem}s. One of those components maybe a
     * {@link pc.ScriptComponent} which allows you to write custom code to attach to your Entity.
     * <p>
     * The Entity uniquely identifies the object and also provides a transform for position and orientation
     * which it inherits from {@link pc.GraphNode} so can be added into the scene graph.
     * The Component and ComponentSystem provide the logic to give an Entity a specific type of behavior. e.g. the ability to
     * render a model or play a sound. Components are specific to an instance of an Entity and are attached (e.g. `this.entity.model`)
     * ComponentSystems allow access to all Entities and Components and are attached to the {@link pc.Application}.
     * @param {String} [name] The non-unique name of the entity, default is "Untitled".
     * @param {pc.Application} [app] The application the entity belongs to, default is the current application.
     * @example
     * var app = ... // Get the pc.Application
     *
     * var entity = new pc.Entity();
     *
     * // Add a Component to the Entity
     * entity.addComponent("camera", {
     *   fov: 45,
     *   nearClip: 1,
     *   farClip: 10000
     * });
     *
     * // Add the Entity into the scene graph
     * app.root.addChild(entity);
     *
     * // Move the entity
     * entity.translate(10, 0, 0);
     *
     * // Or translate it by setting it's position directly
     * var p = entity.getPosition();
     * entity.setPosition(p.x + 10, p.y, p.z);
     *
     * // Change the entity's rotation in local space
     * var e = entity.getLocalEulerAngles();
     * entity.setLocalEulerAngles(e.x, e.y + 90, e.z);
     *
     * // Or use rotateLocal
     * entity.rotateLocal(0, 90, 0);
     *
     * @extends pc.GraphNode
     */
    var Entity = function (name, app){
        pc.GraphNode.call(this, name);

        if (name instanceof pc.Application) app = name;
        this._batchHandle = null; // The handle for a RequestBatch, set this if you want to Component's to load their resources using a pre-existing RequestBatch.
        this.c = {}; // Component storage

        this._app = app; // store app
        if (!app) {
            this._app = pc.Application.getApplication(); // get the current application
            if (!this._app) {
                throw new Error("Couldn't find current application");
            }
        }

        this._guid = null;
        this._request = null;

        // used by component systems to speed up destruction
        this._destroying = false;

        pc.events.attach(this);
    };
    Entity.prototype = Object.create(pc.GraphNode.prototype);
    Entity.prototype.constructor = Entity;

    /**
     * @function
     * @name pc.Entity#addComponent
     * @description Create a new component and add it to the entity.
     * Use this to add functionality to the entity like rendering a model, playing sounds and so on.
     * @param {String} type The name of the component to add. Valid strings are:
     * <ul>
     *   <li>"animation" - see {@link pc.AnimationComponent}</li>
     *   <li>"audiolistener" - see {@link pc.AudioListenerComponent}</li>
     *   <li>"camera" - see {@link pc.CameraComponent}</li>
     *   <li>"collision" - see {@link pc.CollisionComponent}</li>
     *   <li>"element" - see {@link pc.ElementComponent}</li>
     *   <li>"light" - see {@link pc.LightComponent}</li>
     *   <li>"layoutchild" - see {@link pc.LayoutChildComponent}</li>
     *   <li>"layoutgroup" - see {@link pc.LayoutGroupComponent}</li>
     *   <li>"model" - see {@link pc.ModelComponent}</li>
     *   <li>"particlesystem" - see {@link pc.ParticleSystemComponent}</li>
     *   <li>"rigidbody" - see {@link pc.RigidBodyComponent}</li>
     *   <li>"screen" - see {@link pc.ScreenComponent}</li>
     *   <li>"script" - see {@link pc.ScriptComponent}</li>
     *   <li>"sound" - see {@link pc.SoundComponent}</li>
     *   <li>"zone" - see {@link pc.ZoneComponent}</li>
     * </ul>
     * @param {Object} data The initialization data for the specific component type. Refer to each
     * specific component's API reference page for details on valid values for this parameter.
     * @returns {pc.Component} The new Component that was attached to the entity or null if there
     * was an error.
     * @example
     * var entity = new pc.Entity();
     * entity.addComponent("light"); // Add a light component with default properties
     * entity.addComponent("camera", { // Add a camera component with some specified properties
     *   fov: 45,
     *   clearColor: new pc.Color(1,0,0),
     * });
     */
    Entity.prototype.addComponent = function (type, data) {
        var system = this._app.systems[type];
        if (!system) {
            // #ifdef DEBUG
            console.error("addComponent: System " + type + " doesn't exist");
            // #endif
            return null;
        }
        if (this.c[type]) {
            // #ifdef DEBUG
            console.warn("addComponent: Entity already has " + type + " component");
            // #endif
            return null;
        }
        return system.addComponent(this, data);
    };

    /**
     * @function
     * @name pc.Entity#removeComponent
     * @description Remove a component from the Entity.
     * @param {String} type The name of the Component type
     * @example
     * var entity = new pc.Entity();
     * entity.addComponent("light"); // add new light component
     * //...
     * entity.removeComponent("light"); // remove light component
     */
    Entity.prototype.removeComponent = function (type) {
        var system = this._app.systems[type];
        if (!system) {
            // #ifdef DEBUG
            console.error("removeComponent: System " + type + " doesn't exist");
            // #endif
            return;
        }
        if (!this.c[type]) {
            // #ifdef DEBUG
            console.warn("removeComponent: Entity doesn't have " + type + " component");
            // #endif
            return;
        }
        system.removeComponent(this);
    };

    /**
     * @function
     * @name pc.Entity#findComponent
     * @description Search the entity and all of its descendants for the first component of specified type.
     * @param {String} type The name of the component type to retrieve.
     * @returns {pc.Component} A component of specified type, if the entity or any of its descendants has
     * one. Returns undefined otherwise.
     * @example
     * // Get the first found light component in the hierarchy tree that starts with this entity
     * var light = entity.findComponent("light");
     */
    Entity.prototype.findComponent = function (type) {
        var entity = this.findOne(function (node) {
            return node.c && node.c[type];
        });
        return entity && entity.c[type];
    };

    /**
     * @function
     * @name pc.Entity#findComponents
     * @description Search the entity and all of its descendants for all components of specified type.
     * @param {String} type The name of the component type to retrieve.
     * @returns {pc.Component} All components of specified type in the entity or any of its descendants.
     * Returns empty array if none found.
     * @example
     * // Get all light components in the hierarchy tree that starts with this entity
     * var lights = entity.findComponents("light");
     */
    Entity.prototype.findComponents = function (type) {
        var entities = this.find(function (node) {
            return node.c && node.c[type];
        });
        return entities.map(function (entity) {
            return entity.c[type];
        });
    };

    /**
     * @private
     * @function
     * @name pc.Entity#getGuid
     * @description Get the GUID value for this Entity
     * @returns {String} The GUID of the Entity
     */
    Entity.prototype.getGuid = function () {
        // if the guid hasn't been set yet then set it now
        // before returning it
        if (! this._guid) {
            this.setGuid(pc.guid.create());
        }

        return this._guid;
    };

    /**
     * @private
     * @function
     * @name pc.Entity#setGuid
     * @description Set the GUID value for this Entity.
     *
     * N.B. It is unlikely that you should need to change the GUID value of an Entity at run-time. Doing so will corrupt the graph this Entity is in.
     * @param {String} guid The GUID to assign to the Entity
     */
    Entity.prototype.setGuid = function (guid) {
        // remove current guid from entityIndex
        var index = this._app._entityIndex;
        if (this._guid) {
            delete index[this._guid];
        }

        // add new guid to entityIndex
        this._guid = guid;
        index[this._guid] = this;
    };

    Entity.prototype._notifyHierarchyStateChanged = function (node, enabled) {
        var enableFirst = false;
        if (node === this && this._app._enableList.length === 0)
            enableFirst = true;

        node._beingEnabled = true;

        node._onHierarchyStateChanged(enabled);

        if (node._onHierarchyStatePostChanged)
            this._app._enableList.push(node);

        var i, len;
        var c = node._children;
        for (i = 0, len = c.length; i < len; i++) {
            if (c[i]._enabled)
                this._notifyHierarchyStateChanged(c[i], enabled);
        }

        node._beingEnabled = false;

        if (enableFirst) {
            for (i = 0, len = this._app._enableList.length; i < len; i++)
                this._app._enableList[i]._onHierarchyStatePostChanged();

            this._app._enableList.length = 0;
        }
    };

    Entity.prototype._onHierarchyStateChanged = function (enabled) {
        pc.GraphNode.prototype._onHierarchyStateChanged.call(this, enabled);

        // enable / disable all the components
        var component;
        var components = this.c;
        for (var type in components) {
            if (components.hasOwnProperty(type)) {
                component = components[type];
                if (component.enabled) {
                    if (enabled) {
                        component.onEnable();
                    } else {
                        component.onDisable();
                    }
                }
            }
        }
    };

    Entity.prototype._onHierarchyStatePostChanged = function () {
        // post enable all the components
        var components = this.c;
        for (var type in components) {
            if (components.hasOwnProperty(type))
                components[type].onPostStateChange();
        }
    };

    /**
     * @private
     * @function
     * @name pc.Entity#setRequest
     * @description Used during resource loading to ensure that child resources of Entities are tracked
     * @param {ResourceRequest} request The request being used to load this entity
     */
    Entity.prototype.setRequest = function (request) {
        this._request = request;
    };

    /**
     * @private
     * @function
     * @name pc.Entity#getRequest
     * @description Get the Request that is being used to load this Entity
     * @returns {ResourceRequest} The Request
     */
    Entity.prototype.getRequest = function () {
        return this._request;
    };

    /**
     * @function
     * @name pc.Entity#findByGuid
     * @description Find a descendant of this Entity with the GUID
     * @param {String} guid The GUID to search for.
     * @returns {pc.Entity} The Entity with the GUID or null
     */
    Entity.prototype.findByGuid = function (guid) {
        if (this._guid === guid) return this;

        var e = this._app._entityIndex[guid];
        if (e && (e === this || e.isDescendantOf(this))) {
            return e;
        }

        return null;
    };

    /**
     * @function
     * @name pc.Entity#destroy
     * @description Remove all components from the Entity and detach it from the Entity hierarchy. Then recursively destroy all ancestor Entities
     * @example
     * var firstChild = this.entity.children[0];
     * firstChild.destroy(); // delete child, all components and remove from hierarchy
     */
    Entity.prototype.destroy = function () {
        var name;

        this._destroying = true;

        // Disable all enabled components first
        for (name in this.c) {
            this.c[name].enabled = false;
        }

        // Remove all components
        for (name in this.c) {
            this.c[name].system.removeComponent(this);
        }

        // Detach from parent
        if (this._parent)
            this._parent.removeChild(this);

        var children = this._children;
        var child = children.shift();
        while (child) {
            if (child instanceof pc.Entity) {
                child.destroy();
            }

            // make sure child._parent is null because
            // we have removed it from the children array before calling
            // destroy on it
            child._parent = null;

            child = children.shift();
        }

        // fire destroy event
        this.fire('destroy', this);

        // clear all events
        this.off();

        // remove from entity index
        if (this._guid) {
            delete this._app._entityIndex[this._guid];
        }

        this._destroying = false;
    };

    /**
     * @function
     * @name pc.Entity#clone
     * @description Create a deep copy of the Entity. Duplicate the full Entity hierarchy, with all Components and all descendants.
     * Note, this Entity is not in the hierarchy and must be added manually.
     * @returns {pc.Entity} A new Entity which is a deep copy of the original.
     * @example
     *   var e = this.entity.clone(); // Clone Entity
     *   this.entity.parent.addChild(e); // Add it as a sibling to the original
     */
    Entity.prototype.clone = function () {
        var duplicatedIdsMap = {};
        var clone = this._cloneRecursively(duplicatedIdsMap);
        duplicatedIdsMap[this.getGuid()] = clone;

        resolveDuplicatedEntityReferenceProperties(this, this, clone, duplicatedIdsMap);

        return clone;
    };

    Entity.prototype._cloneRecursively = function (duplicatedIdsMap) {
        var clone = new pc.Entity(this._app);
        pc.GraphNode.prototype._cloneInternal.call(this, clone);

        for (var type in this.c) {
            var component = this.c[type];
            component.system.cloneComponent(this, clone);
        }

        var i;
        for (i = 0; i < this._children.length; i++) {
            var oldChild = this._children[i];
            if (oldChild instanceof pc.Entity) {
                var newChild = oldChild._cloneRecursively(duplicatedIdsMap);
                clone.addChild(newChild);
                duplicatedIdsMap[oldChild.getGuid()] = newChild;
            }
        }

        return clone;
    };

    // When an entity that has properties that contain references to other
    // entities within its subtree is duplicated, the expectation of the
    // user is likely that those properties will be updated to point to
    // the corresponding entities within the newly-created duplicate subtree.
    //
    // To handle this, we need to search for properties that refer to entities
    // within the old duplicated structure, find their newly-cloned partners
    // within the new structure, and update the references accordingly. This
    // function implements that requirement.
    function resolveDuplicatedEntityReferenceProperties(oldSubtreeRoot, oldEntity, newEntity, duplicatedIdsMap) {
        var i, len;

        if (oldEntity instanceof pc.Entity) {
            var components = oldEntity.c;

            // Handle component properties
            for (var componentName in components) {
                var component = components[componentName];
                var entityProperties = component.system.getPropertiesOfType('entity');

                for (i = 0, len = entityProperties.length; i < len; i++) {
                    var propertyDescriptor = entityProperties[i];
                    var propertyName = propertyDescriptor.name;
                    var oldEntityReferenceId = component[propertyName];
                    var entityIsWithinOldSubtree = !!oldSubtreeRoot.findByGuid(oldEntityReferenceId);

                    if (entityIsWithinOldSubtree) {
                        var newEntityReferenceId = duplicatedIdsMap[oldEntityReferenceId].getGuid();

                        if (newEntityReferenceId) {
                            newEntity.c[componentName][propertyName] = newEntityReferenceId;
                        } else {
                            console.warn('Could not find corresponding entity id when resolving duplicated entity references');
                        }
                    }
                }
            }

            // Handle entity script attributes
            if (components.script && ! newEntity._app.useLegacyScriptAttributeCloning) {
                newEntity.script.resolveDuplicatedEntityReferenceProperties(components.script, duplicatedIdsMap);
            }

            // Recurse into children. Note that we continue to pass in the same `oldSubtreeRoot`,
            // in order to correctly handle cases where a child has an entity reference
            // field that points to a parent or other ancestor that is still within the
            // duplicated subtree.
            var _old = oldEntity.children.filter(function (e) {
                return (e instanceof pc.Entity);
            });
            var _new = newEntity.children.filter(function (e) {
                return (e instanceof pc.Entity);
            });

            for (i = 0, len = _old.length; i < len; i++) {
                resolveDuplicatedEntityReferenceProperties(oldSubtreeRoot, _old[i], _new[i], duplicatedIdsMap);
            }
        }
    }

    return {
        Entity: Entity
    };
}());


/**
 * @event
 * @name pc.Entity#destroy
 * @description Fired after the entity is destroyed.
 * @param {pc.Entity} entity The entity that was destroyed.
 * @example
 * entity.on("destroy", function (e) {
 *     console.log('entity ' + e.name + ' has been destroyed');
 * });
 */