/home/lindsay/xeolabs/xeogl-next/xeogl/src/component.js
API Docs for:

File: /home/lindsay/xeolabs/xeogl-next/xeogl/src/component.js

  1. /**
  2. The **Component** class is the base class for all xeogl components.
  3.  
  4. ## Usage
  5.  
  6. * [Component IDs](#component-ids)
  7. * [Metadata](#metadata)
  8. * [Logging](#logging)
  9. * [Destruction](#destruction)
  10. * [Creating custom Components](#creating-custom-components)
  11.  
  12. ### Component IDs
  13.  
  14. Every Component has an ID that's unique within the parent {{#crossLink "Scene"}}{{/crossLink}}. xeogl generates
  15. the IDs automatically by default, however you can also specify them yourself. In the example below, we're creating a
  16. scene comprised of {{#crossLink "Scene"}}{{/crossLink}}, {{#crossLink "Material"}}{{/crossLink}}, {{#crossLink "Geometry"}}{{/crossLink}} and
  17. {{#crossLink "Mesh"}}{{/crossLink}} components, while letting xeogl generate its own ID for
  18. the {{#crossLink "Geometry"}}{{/crossLink}}:
  19.  
  20. ````javascript
  21. // The Scene is a Component too
  22. var scene = new xeogl.Scene({
  23. id: "myScene"
  24. });
  25.  
  26. var material = new xeogl.PhongMaterial(scene, {
  27. id: "myMaterial"
  28. });
  29.  
  30. var geometry = new xeogl.Geometry(scene, {
  31. id: "myGeometry"
  32. });
  33.  
  34. // Let xeogl automatically generate the ID for our Mesh
  35. var mesh = new xeogl.Mesh(scene, {
  36. material: material,
  37. geometry: geometry
  38. });
  39. ````
  40.  
  41. We can then find those components like this:
  42.  
  43. ````javascript
  44. // Find the Scene
  45. var theScene = xeogl.scenes["myScene"];
  46.  
  47. // Find the Material
  48. var theMaterial = theScene.components["myMaterial"];
  49.  
  50. // Find all PhongMaterials in the Scene
  51. var phongMaterials = theScene.types["xeogl.PhongMaterial"];
  52.  
  53. // Find our Material within the PhongMaterials
  54. var theMaterial = phongMaterials["myMaterial"];
  55. ````
  56.  
  57. ### Component inheritance
  58.  
  59.  
  60. TODO
  61.  
  62. All xeogl components are (at least indirect) subclasses of the Component base type.
  63.  
  64. For most components, you can get the name of its class via its {{#crossLink "Component/type:property"}}{{/crossLink}} property:
  65.  
  66. ````javascript
  67. var type = theMaterial.type; // "xeogl.PhongMaterial"
  68. ````
  69.  
  70. You can also test if a component implements or extends a given component class, like so:
  71.  
  72. ````javascript
  73. // Evaluates true:
  74. var isComponent = theMaterial.isType("xeogl.Component");
  75.  
  76. // Evaluates true:
  77. var isMaterial = theMaterial.isType("xeogl.Material");
  78.  
  79. // Evaluates true:
  80. var isPhongMaterial = theMaterial.isType("xeogl.PhongMaterial");
  81.  
  82. // Evaluates false:
  83. var isMetallicMaterial = theMaterial.isType("xeogl.MetallicMaterial");
  84. ````
  85. Note that the chain is ordered downwards in the hierarchy, ie. from super-class down towards sub-class.
  86.  
  87. ### Metadata
  88.  
  89. You can set optional **metadata** on your Components, which can be anything you like. These are intended
  90. to help manage your components within your application code or content pipeline. You could use metadata to attach
  91. authoring or version information, like this:
  92.  
  93. ````javascript
  94. // Scene with authoring metadata
  95. var scene = new xeogl.Scene({
  96. id: "myScene",
  97. meta: {
  98. title: "My bodacious 3D scene",
  99. author: "@xeographics",
  100. date: "February 30 2018"
  101. }
  102. });
  103.  
  104. // Material with descriptive metadata
  105. var material = new xeogl.PhongMaterial(scene, {
  106. id: "myMaterial",
  107. diffuse: [1, 0, 0],
  108. meta: {
  109. description: "Bright red color with no textures",
  110. version: "0.1",
  111. foo: "bar"
  112. }
  113. });
  114. ````
  115.  
  116. ### Logging
  117.  
  118. Components have methods to log ID-prefixed messages to the JavaScript console:
  119.  
  120. ````javascript
  121. material.log("Everything is fine, situation normal.");
  122. material.warn("Wait, whats that red light?");
  123. material.error("Aw, snap!");
  124. ````
  125.  
  126. The logged messages will look like this in the console:
  127.  
  128. ````text
  129. [LOG] myMaterial: Everything is fine, situation normal.
  130. [WARN] myMaterial: Wait, whats that red light..
  131. [ERROR] myMaterial: Aw, snap!
  132. ````
  133.  
  134. ### Destruction
  135.  
  136. Get notification of destruction directly on the Components:
  137.  
  138. ````javascript
  139. material.on("destroyed", function() {
  140. this.log("Component was destroyed: " + this.id);
  141. });
  142. ````
  143.  
  144. Or get notification of destruction of any Component within its {{#crossLink "Scene"}}{{/crossLink}}, indiscriminately:
  145.  
  146. ````javascript
  147. scene.on("componentDestroyed", function(component) {
  148. this.log("Component was destroyed: " + component.id);
  149. });
  150. ````
  151.  
  152. Then destroy a component like this:
  153.  
  154. ````javascript
  155. material.destroy();
  156. ````
  157.  
  158. ### Creating custom Components
  159.  
  160. Subclassing a Component to create a new ````xeogl.ColoredTorus```` type:
  161.  
  162. ````javascript
  163. class ColoredTorus extends xeogl.Component{
  164.  
  165. get type() {
  166. return "ColoredTorus";
  167. }
  168.  
  169. constructor(scene=null, cfg) { // Constructor
  170.  
  171. super(scene. cfg);
  172.  
  173. this._torus = new xeogl.Mesh({
  174. geometry: new xeogl.TorusGeometry({radius: 2, tube: .6}),
  175. material: new xeogl.MetallicMaterial({
  176. baseColor: [1.0, 0.5, 0.5],
  177. roughness: 0.4,
  178. metallic: 0.1
  179. })
  180. });
  181.  
  182. this.color = cfg.color;
  183. },
  184.  
  185. set color(color) {
  186. this._torus.material.baseColor = color;
  187. }
  188.  
  189. get color() {
  190. return this._torus.material.baseColor;
  191. }
  192.  
  193. destroy() {
  194. super.destroy();
  195. this._torus.geometry.destroy();
  196. this._torus.material.destroy();
  197. this._torus.destroy();
  198. }
  199. };
  200. ````
  201.  
  202. #### Examples
  203.  
  204. * [Custom component definition](../../examples/#extending_component_basic)
  205. * [Custom component that fires events](../../examples/#extending_component_changeEvents)
  206. * [Custom component that manages child components](../../examples/#extending_component_childCleanup)
  207. * [Custom component that schedules asynch tasks](../../examples/#extending_component_update)
  208.  
  209. @class Component
  210. @module xeogl
  211. @constructor
  212. @param [owner] {Component} Owner component. When destroyed, the owner will destroy this component as well. Creates this component within the default {{#crossLink "Scene"}}{{/crossLink}} when omitted.
  213. @param [cfg] {*} DepthBuf configuration
  214. @param [cfg.id] {String} Optional ID, unique among all components in the parent {{#crossLink "Scene"}}Scene{{/crossLink}}, generated automatically when omitted.
  215. @param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this Component.
  216. */
  217.  
  218. import {core} from "./core.js";
  219. import {utils} from './utils.js';
  220. import {tasks} from './tasks.js';
  221. import {Map} from "./utils/map.js";
  222. import {componentClasses} from "./componentClasses.js";
  223.  
  224. const type = "xeogl.Component";
  225.  
  226. class Component {
  227.  
  228. /**
  229. JavaScript class name for this Component.
  230.  
  231. For example: "xeogl.AmbientLight", "xeogl.MetallicMaterial" etc.
  232.  
  233. @property type
  234. @type String
  235. @final
  236. */
  237. get type() {
  238. return type;
  239. }
  240.  
  241. constructor() {
  242.  
  243. var cfg = {};
  244.  
  245. var arg1 = arguments[0];
  246. var arg2 = arguments[1];
  247.  
  248. var owner = null;
  249.  
  250. /**
  251. The parent {{#crossLink "Scene"}}{{/crossLink}} that contains this Component.
  252.  
  253. @property scene
  254. @type {Scene}
  255. @final
  256. */
  257. this.scene = null;
  258.  
  259. if (this.type === "xeogl.Scene") {
  260. this.scene = this;
  261. if (arg1) {
  262. cfg = arg1;
  263. }
  264. } else {
  265. if (arg1) {
  266. if (arg1.type === "xeogl.Scene") {
  267. this.scene = arg1;
  268. owner = this.scene;
  269. if (arg2) {
  270. cfg = arg2;
  271. }
  272.  
  273. } else if (arg1 instanceof Component) {
  274. this.scene = arg1.scene;
  275. owner = arg1;
  276. if (arg2) {
  277. cfg = arg2;
  278. }
  279.  
  280. } else {
  281. // Create this component within the default xeogl Scene
  282. this.scene = core.getDefaultScene();
  283. owner = this.scene;
  284. cfg = arg1;
  285. }
  286. } else {
  287. // Create this component within the default xeogl Scene
  288. this.scene = core.getDefaultScene();
  289. owner = this.scene;
  290. }
  291. this._renderer = this.scene._renderer;
  292. }
  293.  
  294. this._dontClear = !!cfg.dontClear; // Prevent Scene#clear from destroying this component
  295.  
  296. this._model = null;
  297. this._renderer = this.scene._renderer;
  298.  
  299. /**
  300. Arbitrary, user-defined metadata on this component.
  301.  
  302. @property metadata
  303. @type Object
  304. */
  305. this.meta = cfg.meta || {};
  306.  
  307. /**
  308. Unique ID for this Component within its parent {{#crossLink "Scene"}}Scene{{/crossLink}}.
  309.  
  310. @property id
  311. @type String
  312. @final
  313. */
  314. this.id = cfg.id; // Auto-generated by xeogl.Scene by default
  315.  
  316. /**
  317. True as soon as this Component has been destroyed
  318.  
  319. @property destroyed
  320. @type Boolean
  321. */
  322. this.destroyed = false;
  323.  
  324. this._attached = {}; // Attached components with names.
  325. this._attachments = null; // Attached components keyed to IDs - lazy-instantiated
  326. this._subIdMap = null; // Subscription subId pool
  327. this._subIdEvents = null; // Subscription subIds mapped to event names
  328. this._eventSubs = null; // Event names mapped to subscribers
  329. this._events = null; // Maps names to events
  330. this._eventCallDepth = 0; // Helps us catch stack overflows from recursive events
  331. this._adoptees = null; // // Components created with #create - lazy-instantiated
  332.  
  333. if (this !== this.scene) { // Don't add scene to itself
  334. this.scene._addComponent(this); // Assigns this component an automatic ID if not yet assigned
  335. }
  336.  
  337. this._updateScheduled = false; // True when #_update will be called on next tick
  338.  
  339. this.init(cfg);
  340.  
  341. if (owner) {
  342. owner._adopt(this);
  343. }
  344. }
  345.  
  346. init() { // No-op
  347.  
  348. }
  349.  
  350. _addedToModel(model) { // Called by xeogl.Model.add()
  351. this._model = model;
  352. }
  353.  
  354. _removedFromModel(model) { // Called by xeogl.Model.remove()
  355. this._model = null;
  356. }
  357.  
  358. /**
  359. The {{#crossLink "Model"}}{{/crossLink}} which contains this Component, if any.
  360.  
  361. Will be null if this Component is not in a Model.
  362.  
  363. @property model
  364. @final
  365. @type Model
  366. */
  367. get model() {
  368. return this._model;
  369. }
  370.  
  371. /**
  372. Tests if this component is of the given type, or is a subclass of the given type.
  373.  
  374. The type may be given as either a string or a component constructor.
  375.  
  376. This method works by walking up the inheritance type chain, which this component provides in
  377. property {{#crossLink "Component/superTypes:property"}}{{/crossLink}}, returning true as soon as one of the type strings in
  378. the chain matches the given type, of false if none match.
  379.  
  380. #### Examples:
  381.  
  382. ````javascript
  383. var myRotate = new xeogl.Rotate({ ... });
  384.  
  385. myRotate.isType(xeogl.Component); // Returns true for all xeogl components
  386. myRotate.isType("xeogl.Component"); // Returns true for all xeogl components
  387. myRotate.isType(xeogl.Rotate); // Returns true
  388. myRotate.isType(xeogl.Transform); // Returns true
  389. myRotate.isType("xeogl.Transform"); // Returns true
  390. myRotate.isType(xeogl.Mesh); // Returns false, because xeogl.Rotate does not (even indirectly) extend xeogl.Mesh
  391. ````
  392.  
  393. @method isType
  394. @param {String|Function} type Component type to compare with, eg "xeogl.PhongMaterial", or a xeogl component constructor.
  395. @returns {Boolean} True if this component is of given type or is subclass of the given type.
  396. */
  397. isType(type) {
  398. if (!utils.isString(type)) {
  399. type = type.type;
  400. if (!type) {
  401. return false;
  402. }
  403. }
  404. return core.isComponentType(this.type, type);
  405. }
  406.  
  407. /**
  408. * Fires an event on this component.
  409. *
  410. * Notifies existing subscribers to the event, optionally retains the event to give to
  411. * any subsequent notifications on the event as they are made.
  412. *
  413. * @method fire
  414. * @param {String} event The event type name
  415. * @param {Object} value The event parameters
  416. * @param {Boolean} [forget=false] When true, does not retain for subsequent subscribers
  417. */
  418. fire(event, value, forget) {
  419. if (!this._events) {
  420. this._events = {};
  421. }
  422. if (!this._eventSubs) {
  423. this._eventSubs = {};
  424. }
  425. if (forget !== true) {
  426. this._events[event] = value || true; // Save notification
  427. }
  428. const subs = this._eventSubs[event];
  429. let sub;
  430. if (subs) { // Notify subscriptions
  431. for (const subId in subs) {
  432. if (subs.hasOwnProperty(subId)) {
  433. sub = subs[subId];
  434. this._eventCallDepth++;
  435. if (this._eventCallDepth < 300) {
  436. sub.callback.call(sub.scope, value);
  437. } else {
  438. this.error("fire: potential stack overflow from recursive event '" + event + "' - dropping this event");
  439. }
  440. this._eventCallDepth--;
  441. }
  442. }
  443. }
  444. }
  445.  
  446. /**
  447. * Subscribes to an event on this component.
  448. *
  449. * The callback is be called with this component as scope.
  450. *
  451. * @method on
  452. * @param {String} event The event
  453. * @param {Function} callback Called fired on the event
  454. * @param {Object} [scope=this] Scope for the callback
  455. * @return {String} Handle to the subscription, which may be used to unsubscribe with {@link #off}.
  456. */
  457. on(event, callback, scope) {
  458. if (!this._events) {
  459. this._events = {};
  460. }
  461. if (!this._subIdMap) {
  462. this._subIdMap = new Map(); // Subscription subId pool
  463. }
  464. if (!this._subIdEvents) {
  465. this._subIdEvents = {};
  466. }
  467. if (!this._eventSubs) {
  468. this._eventSubs = {};
  469. }
  470. let subs = this._eventSubs[event];
  471. if (!subs) {
  472. subs = {};
  473. this._eventSubs[event] = subs;
  474. }
  475. const subId = this._subIdMap.addItem(); // Create unique subId
  476. subs[subId] = {
  477. callback: callback,
  478. scope: scope || this
  479. };
  480. this._subIdEvents[subId] = event;
  481. const value = this._events[event];
  482. if (value !== undefined) { // A publication exists, notify callback immediately
  483. callback.call(scope || this, value);
  484. }
  485. return subId;
  486. }
  487.  
  488. /**
  489. * Cancels an event subscription that was previously made with {{#crossLink "Component/on:method"}}Component#on(){{/crossLink}} or
  490. * {{#crossLink "Component/once:method"}}Component#once(){{/crossLink}}.
  491. *
  492. * @method off
  493. * @param {String} subId Publication subId
  494. */
  495. off(subId) {
  496. if (subId === undefined || subId === null) {
  497. return;
  498. }
  499. if (!this._subIdEvents) {
  500. return;
  501. }
  502. const event = this._subIdEvents[subId];
  503. if (event) {
  504. delete this._subIdEvents[subId];
  505. const subs = this._eventSubs[event];
  506. if (subs) {
  507. delete subs[subId];
  508. }
  509. this._subIdMap.removeItem(subId); // Release subId
  510. }
  511. }
  512.  
  513. /**
  514. * Subscribes to the next occurrence of the given event, then un-subscribes as soon as the event is subIdd.
  515. *
  516. * This is equivalent to calling {{#crossLink "Component/on:method"}}Component#on(){{/crossLink}}, and then calling
  517. * {{#crossLink "Component/off:method"}}Component#off(){{/crossLink}} inside the callback function.
  518. *
  519. * @method once
  520. * @param {String} event Data event to listen to
  521. * @param {Function(data)} callback Called when fresh data is available at the event
  522. * @param {Object} [scope=this] Scope for the callback
  523. */
  524. once(event, callback, scope) {
  525. const self = this;
  526. const subId = this.on(event,
  527. function (value) {
  528. self.off(subId);
  529. callback(value);
  530. },
  531. scope);
  532. }
  533.  
  534. /**
  535. * Returns true if there are any subscribers to the given event on this component.
  536. *
  537. * @method hasSubs
  538. * @param {String} event The event
  539. * @return {Boolean} True if there are any subscribers to the given event on this component.
  540. */
  541. hasSubs(event) {
  542. return (this._eventSubs && !!this._eventSubs[event]);
  543. }
  544.  
  545. /**
  546. * Logs a console debugging message for this component.
  547. *
  548. * The console message will have this format: *````[LOG] [<component type> <component id>: <message>````*
  549. *
  550. * Also fires the message as a {{#crossLink "Scene/log:event"}}{{/crossLink}} event on the
  551. * parent {{#crossLink "Scene"}}Scene{{/crossLink}}.
  552. *
  553. * @method log
  554. * @param {String} message The message to log
  555. */
  556. log(message) {
  557. message = "[LOG]" + this._message(message);
  558. window.console.log(message);
  559. this.scene.fire("log", message);
  560. }
  561.  
  562. _message(message) {
  563. return " [" + this.type + " " + utils.inQuotes(this.id) + "]: " + message;
  564. }
  565.  
  566. /**
  567. * Logs a warning for this component to the JavaScript console.
  568. *
  569. * The console message will have this format: *````[WARN] [<component type> =<component id>: <message>````*
  570. *
  571. * Also fires the message as a {{#crossLink "Scene/warn:event"}}{{/crossLink}} event on the
  572. * parent {{#crossLink "Scene"}}Scene{{/crossLink}}.
  573. *
  574. * @method warn
  575. * @param {String} message The message to log
  576. */
  577. warn(message) {
  578. message = "[WARN]" + this._message(message);
  579. window.console.warn(message);
  580. this.scene.fire("warn", message);
  581. }
  582.  
  583. /**
  584. * Logs an error for this component to the JavaScript console.
  585. *
  586. * The console message will have this format: *````[ERROR] [<component type> =<component id>: <message>````*
  587. *
  588. * Also fires the message as an {{#crossLink "Scene/error:event"}}{{/crossLink}} event on the
  589. * parent {{#crossLink "Scene"}}Scene{{/crossLink}}.
  590. *
  591. * @method error
  592. * @param {String} message The message to log
  593. */
  594. error(message) {
  595. message = "[ERROR]" + this._message(message);
  596. window.console.error(message);
  597. this.scene.fire("error", message);
  598. }
  599.  
  600. /**
  601. * Adds a child component to this.
  602. * When component not given, attaches the scene's default instance for the given name (if any).
  603. * Publishes the new child component on this component, keyed to the given name.
  604. *
  605. * @param {*} params
  606. * @param {String} params.name component name
  607. * @param {Component} [params.component] The component
  608. * @param {String} [params.type] Optional expected type of base type of the child; when supplied, will
  609. * cause an exception if the given child is not the same type or a subtype of this.
  610. * @param {Boolean} [params.sceneDefault=false]
  611. * @param {Boolean} [params.sceneSingleton=false]
  612. * @param {Function} [params.onAttached] Optional callback called when component attached
  613. * @param {Function} [params.onAttached.callback] Callback function
  614. * @param {Function} [params.onAttached.scope] Optional scope for callback
  615. * @param {Function} [params.onDetached] Optional callback called when component is detached
  616. * @param {Function} [params.onDetached.callback] Callback function
  617. * @param {Function} [params.onDetached.scope] Optional scope for callback
  618. * @param {{String:Function}} [params.on] Callbacks to subscribe to properties on component
  619. * @param {Boolean} [params.recompiles=true] When true, fires "dirty" events on this component
  620. * @private
  621. */
  622. _attach(params) {
  623.  
  624. const name = params.name;
  625.  
  626. if (!name) {
  627. this.error("Component 'name' expected");
  628. return;
  629. }
  630.  
  631. let component = params.component;
  632. const sceneDefault = params.sceneDefault;
  633. const sceneSingleton = params.sceneSingleton;
  634. const type = params.type;
  635. const on = params.on;
  636. const recompiles = params.recompiles !== false;
  637.  
  638. // True when child given as config object, where parent manages its instantiation and destruction
  639. let managingLifecycle = false;
  640.  
  641. if (component) {
  642.  
  643. if (utils.isNumeric(component) || utils.isString(component)) {
  644.  
  645. // Component ID given
  646. // Both numeric and string IDs are supported
  647.  
  648. const id = component;
  649.  
  650. component = this.scene.components[id];
  651.  
  652. if (!component) {
  653.  
  654. // Quote string IDs in errors
  655.  
  656. this.error("Component not found: " + utils.inQuotes(id));
  657. return;
  658. }
  659.  
  660. } else if (utils.isObject(component)) {
  661.  
  662. // Component config given
  663.  
  664. const componentCfg = component;
  665. const componentType = componentCfg.type || type || "xeogl.Component";
  666. const componentClass = componentClasses[componentType];
  667.  
  668. if (!componentClass) {
  669. this.error("Component type not found: " + componentType);
  670. return;
  671. }
  672.  
  673. if (type) {
  674. if (!core.isComponentType(componentType, type)) {
  675. this.error("Expected a " + type + " type or subtype, not a " + componentType);
  676. return;
  677. }
  678. }
  679.  
  680. component = new componentClass(this.scene, componentCfg);
  681.  
  682. managingLifecycle = true;
  683. }
  684. }
  685.  
  686. if (!component) {
  687.  
  688. if (sceneSingleton === true) {
  689.  
  690. // Using the first instance of the component type we find
  691.  
  692. const instances = this.scene.types[type];
  693. for (const id2 in instances) {
  694. if (instances.hasOwnProperty) {
  695. component = instances[id2];
  696. break;
  697. }
  698. }
  699.  
  700. if (!component) {
  701. this.error("Scene has no default component for '" + name + "'");
  702. return null;
  703. }
  704.  
  705. } else if (sceneDefault === true) {
  706.  
  707. // Using a default scene component
  708.  
  709. component = this.scene[name];
  710.  
  711. if (!component) {
  712. this.error("Scene has no default component for '" + name + "'");
  713. return null;
  714. }
  715. }
  716. }
  717.  
  718. if (component) {
  719.  
  720. if (component.scene.id !== this.scene.id) {
  721. this.error("Not in same scene: " + component.type + " " + utils.inQuotes(component.id));
  722. return;
  723. }
  724.  
  725. if (type) {
  726.  
  727. if (!component.isType(type)) {
  728. this.error("Expected a " + type + " type or subtype: " + component.type + " " + utils.inQuotes(component.id));
  729. return;
  730. }
  731. }
  732. }
  733.  
  734. if (!this._attachments) {
  735. this._attachments = {};
  736. }
  737.  
  738. const oldComponent = this._attached[name];
  739. let subs;
  740. let i;
  741. let len;
  742.  
  743. if (oldComponent) {
  744.  
  745. if (component && oldComponent.id === component.id) {
  746.  
  747. // Reject attempt to reattach same component
  748. return;
  749. }
  750.  
  751. const oldAttachment = this._attachments[oldComponent.id];
  752.  
  753. // Unsubscribe from events on old component
  754.  
  755. subs = oldAttachment.subs;
  756.  
  757. for (i = 0, len = subs.length; i < len; i++) {
  758. oldComponent.off(subs[i]);
  759. }
  760.  
  761. delete this._attached[name];
  762. delete this._attachments[oldComponent.id];
  763.  
  764. const onDetached = oldAttachment.params.onDetached;
  765. if (onDetached) {
  766. if (utils.isFunction(onDetached)) {
  767. onDetached(oldComponent);
  768. } else {
  769. onDetached.scope ? onDetached.callback.call(onDetached.scope, oldComponent) : onDetached.callback(oldComponent);
  770. }
  771. }
  772.  
  773. if (oldAttachment.managingLifecycle) {
  774.  
  775. // Note that we just unsubscribed from all events fired by the child
  776. // component, so destroying it won't fire events back at us now.
  777.  
  778. oldComponent.destroy();
  779. }
  780. }
  781.  
  782. if (component) {
  783.  
  784. // Set and publish the new component on this component
  785.  
  786. const attachment = {
  787. params: params,
  788. component: component,
  789. subs: [],
  790. managingLifecycle: managingLifecycle
  791. };
  792.  
  793. attachment.subs.push(
  794. component.on("destroyed",
  795. function () {
  796. attachment.params.component = null;
  797. this._attach(attachment.params);
  798. },
  799. this));
  800.  
  801. if (recompiles) {
  802. attachment.subs.push(
  803. component.on("dirty",
  804. function () {
  805. this.fire("dirty", this);
  806. },
  807. this));
  808. }
  809.  
  810. this._attached[name] = component;
  811. this._attachments[component.id] = attachment;
  812.  
  813. // Bind destruct listener to new component to remove it
  814. // from this component when destroyed
  815.  
  816. const onAttached = params.onAttached;
  817. if (onAttached) {
  818. if (utils.isFunction(onAttached)) {
  819. onAttached(component);
  820. } else {
  821. onAttached.scope ? onAttached.callback.call(onAttached.scope, component) : onAttached.callback(component);
  822. }
  823. }
  824.  
  825. if (on) {
  826.  
  827. let event;
  828. let subIdr;
  829. let callback;
  830. let scope;
  831.  
  832. for (event in on) {
  833. if (on.hasOwnProperty(event)) {
  834.  
  835. subIdr = on[event];
  836.  
  837. if (utils.isFunction(subIdr)) {
  838. callback = subIdr;
  839. scope = null;
  840. } else {
  841. callback = subIdr.callback;
  842. scope = subIdr.scope;
  843. }
  844.  
  845. if (!callback) {
  846. continue;
  847. }
  848.  
  849. attachment.subs.push(component.on(event, callback, scope));
  850. }
  851. }
  852. }
  853. }
  854.  
  855. if (recompiles) {
  856. this.fire("dirty", this); // FIXME: May trigger spurous mesh recompilations unless able to limit with param?
  857. }
  858.  
  859. this.fire(name, component); // Component can be null
  860.  
  861. return component;
  862. }
  863.  
  864. _checkComponent(expectedType, component) {
  865. if (utils.isObject(component)) {
  866. if (component.type) {
  867. if (!core.isComponentType(component.type, expectedType)) {
  868. this.error("Expected a " + expectedType + " type or subtype: " + component.type + " " + utils.inQuotes(component.id));
  869. return;
  870. }
  871. } else {
  872. component.type = expectedType;
  873. }
  874. component = new componentClasses[component.type](this.scene, component);
  875. } else {
  876. if (utils.isID(component)) { // Expensive test
  877. const id = component;
  878. component = this.scene.components[id];
  879. if (!component) {
  880. this.error("Component not found: " + utils.inQuotes(component.id));
  881. return;
  882. }
  883. }
  884. }
  885. if (component.scene.id !== this.scene.id) {
  886. this.error("Not in same scene: " + component.type + " " + utils.inQuotes(component.id));
  887. return;
  888. }
  889. if (!component.isType(expectedType)) {
  890. this.error("Expected a " + expectedType + " type or subtype: " + component.type + " " + utils.inQuotes(component.id));
  891. return;
  892. }
  893. return component;
  894. }
  895.  
  896. /**
  897. * Convenience method for creating a Component within this Component's {{#crossLink "Scene"}}{{/crossLink}}.
  898. *
  899. * The method is given a component configuration, like so:
  900. *
  901. * ````javascript
  902. * var material = myComponent.create({
  903. * type: "xeogl.PhongMaterial",
  904. * diffuse: [1,0,0],
  905. * specular: [1,1,0]
  906. * }, "myMaterial");
  907. * ````
  908. *
  909. * @method create
  910. * @param {*} [cfg] Configuration for the component instance.
  911. * @returns {*}
  912. */
  913. create(cfg) {
  914.  
  915. let type;
  916. let claz;
  917.  
  918. if (utils.isObject(cfg)) {
  919. type = cfg.type || "xeogl.Component";
  920. claz = componentClasses[type];
  921.  
  922. } else if (utils.isString(cfg)) {
  923. type = cfg;
  924. claz = componentClasses[type];
  925.  
  926. } else {
  927. claz = cfg;
  928. type = cfg.prototype.type;
  929. // TODO: catch unknown component class
  930. }
  931.  
  932. if (!claz) {
  933. this.error("Component type not found: " + type);
  934. return;
  935. }
  936.  
  937. if (!core.isComponentType(type, "xeogl.Component")) {
  938. this.error("Expected a xeogl.Component type or subtype");
  939. return;
  940. }
  941.  
  942. if (cfg && cfg.id && this.components[cfg.id]) {
  943. this.error("Component " + utils.inQuotes(cfg.id) + " already exists in Scene - ignoring ID, will randomly-generate instead");
  944. cfg.id = undefined;
  945. //return null;
  946. }
  947.  
  948. const component = new claz(this, cfg);
  949. if (component) {
  950. this._adopt(component);
  951. }
  952.  
  953. return component;
  954. }
  955.  
  956. _adopt(component) {
  957. if (!this._adoptees) {
  958. this._adoptees = {};
  959. }
  960. if (!this._adoptees[component.id]) {
  961. this._adoptees[component.id] = component;
  962. }
  963. component.on("destroyed", function () {
  964. delete this._adoptees[component.id];
  965. }, this);
  966. }
  967.  
  968. /**
  969. * Protected method, called by sub-classes to queue a call to _update().
  970. * @protected
  971. * @param {Number} [priority=1]
  972. */
  973. _needUpdate(priority) {
  974. if (!this._updateScheduled) {
  975. this._updateScheduled = true;
  976. if (priority === 0) {
  977. this._doUpdate();
  978. } else {
  979. tasks.scheduleTask(this._doUpdate, this);
  980. }
  981. }
  982. }
  983.  
  984. /**
  985. * @private
  986. */
  987. _doUpdate() {
  988. if (this._updateScheduled) {
  989. this._updateScheduled = false;
  990. if (this._update) {
  991. this._update();
  992. }
  993. }
  994. }
  995.  
  996. /**
  997. * Protected virtual template method, optionally implemented
  998. * by sub-classes to perform a scheduled task.
  999. *
  1000. * @protected
  1001. */
  1002. _update() {
  1003. }
  1004.  
  1005. /**
  1006. * Destroys this component.
  1007. *
  1008. * Fires a {{#crossLink "Component/destroyed:event"}}{{/crossLink}} event on this Component.
  1009. *
  1010. * Automatically disassociates this component from other components, causing them to fall back on any
  1011. * defaults that this component overrode on them.
  1012. *
  1013. * TODO: describe effect with respect to #create
  1014. *
  1015. * @method destroy
  1016. */
  1017. destroy() {
  1018.  
  1019. if (this.destroyed) {
  1020. return;
  1021. }
  1022.  
  1023. // Unsubscribe from child components and destroy then
  1024.  
  1025. let id;
  1026. let attachment;
  1027. let component;
  1028. let subs;
  1029. let i;
  1030. let len;
  1031.  
  1032. if (this._attachments) {
  1033. for (id in this._attachments) {
  1034. if (this._attachments.hasOwnProperty(id)) {
  1035. attachment = this._attachments[id];
  1036. component = attachment.component;
  1037. subs = attachment.subs;
  1038. for (i = 0, len = subs.length; i < len; i++) {
  1039. component.off(subs[i]);
  1040. }
  1041. if (attachment.managingLifecycle) {
  1042. component.destroy();
  1043. }
  1044. }
  1045. }
  1046. }
  1047.  
  1048. // Release components created with #create
  1049.  
  1050. if (this._adoptees) {
  1051. for (id in this._adoptees) {
  1052. if (this._adoptees.hasOwnProperty(id)) {
  1053. component = this._adoptees[id];
  1054. component.destroy();
  1055. delete this._adoptees[id];
  1056. }
  1057. }
  1058. }
  1059.  
  1060. this.scene._removeComponent(this);
  1061.  
  1062. // Memory leak avoidance
  1063. this._attached = {};
  1064. this._attachments = null;
  1065. this._subIdMap = null;
  1066. this._subIdEvents = null;
  1067. this._eventSubs = null;
  1068. this._events = null;
  1069. this._eventCallDepth = 0;
  1070. this._adoptees = null;
  1071. this._updateScheduled = false;
  1072.  
  1073. /**
  1074. * Fired when this Component is destroyed.
  1075. * @event destroyed
  1076. */
  1077. this.fire("destroyed", this.destroyed = true);
  1078. }
  1079. }
  1080.  
  1081. componentClasses[type] = Component;
  1082.  
  1083. export {Component};
  1084.