UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

325 lines (303 loc) 10.1 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { EventDispatcher, MathUtils } from 'three'; /* eslint no-unused-vars: 0 */ /* eslint class-methods-use-this: 0 */ /** * Abstract base class for all entities in Giro3D. * The Entity is the core component of Giro3D and represent an updatable * object that is added to an {@link core.Instance | Instance}. * * The class inherits three.js' [`EventDispatcher`](https://threejs.org/docs/index.html?q=even#api/en/core/EventDispatcher). * * ### The `userData` property * * The `userData` property can be used to attach custom data to the entity, in a type safe manner. * It is recommended to use this property instead of attaching arbitrary properties to the object: * * ```ts * type MyCustomUserData = { * creationDate: Date; * owner: string; * }; * const entity: Entity<MyCustomUserData> = ...; * * entity.userData.creationDate = Date.now(); * entity.userData.owner = 'John Doe'; * ``` * * ### Lifetime * * The lifetime of an entity follows this pattern: when the entity is added to an instance, its * {@link preprocess} method is called. When the promise * returned by this method resolves, the entity can be used in the main loop, where the update * methods (see below) will be used to update the entity over time. Finally, when the entity is * removed from the instance, its {@link dispose} method * is called to cleanup memory. * * ### The update methods * * This class exposes three methods to update the object: * - {@link preUpdate} * to determine which _parts_ of the object should actually be updated. * - {@link update} called for each part returned * by `preUpdate()` * - {@link postUpdate} to finalize * the update step. * * ### A note on "parts" * * The notion of "part to be updated" is entity-specific. For example, if the entity is a tiled map, * the parts may be map tiles. If the entity is a point cloud, it may be point clusters, and so on. * On the other hand, if the entity is not made of distinct objects, the "part to update" may be the * entity itself, or a dummy object. * * ```js * const instance = new Instance(...); * const entity = new Entity('exampleEntity'); * instance.add(entity); * ``` * @typeParam TEventMap - The event map of the entity. * @typeParam TUserData - The type of the `userData` property. */ class Entity extends EventDispatcher { /** * The unique identifier of this entity. */ _ready = false; /** * Determine if this entity is ready to use. */ get ready() { return this._ready; } get instance() { if (!this._instance) { throw new Error('This entity has not been added to an instance or its initialization is not finished.\n' + 'To check if the entity is ready, use the .ready property'); } return this._instance; } /** * The name of this entity. */ /** * An object that can be used to store custom data about the {@link Entity}. */ /** * Read-only flag to check if a given object is of type Entity. */ isEntity = true; /** * The name of the type of this object. */ /** * Creates an entity with the specified unique identifier. */ constructor() { super(); this.id = MathUtils.generateUUID(); this.type = 'Entity'; this._frozen = false; // @ts-expect-error {} cannot be assigned to TUserData, but it's better than null/undefined. this.userData = {}; } /** * Gets or sets the frozen status of this entity. A frozen entity is still visible * but will not be updated automatically. * * Useful for debugging purposes. */ get frozen() { return this._frozen; } set frozen(v) { if (this._frozen !== v) { this._frozen = v; this.dispatchEvent({ type: 'frozen-property-changed', frozen: v }); } } /** * Gets whether this entity is currently loading data. */ get loading() { // Implement this in derived classes. return false; } /** * Gets the current loading progress (between 0 and 1). * Note: This property is only meaningful if {@link loading} is `true`. */ get progress() { // Implement this in derived classes. return 1; } /** * Asynchronously preprocess the entity. This method may be overriden to perform * any operation that must be done before the entity can be used in the scene, such * as fetching metadata about a dataset, etc. * * @param opts - The preprocess options. * * @returns A promise that resolves when the entity is ready to be used. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars preprocess() { return Promise.resolve(); } /** * @internal */ async initialize(opts) { this._instance = opts.instance; await this.preprocess(opts); this._ready = true; this.dispatchEvent({ type: 'initialized' }); } /** * This method is called before `update` to check if the MainLoop * should try to update this entity or not. For better performances, * it should return `false` if the entity has no impact on the * rendering (e.g. the element is not visible). * * The inherited child _can_ completely ignore this value if it makes sense. * * @returns `true` if should check for update */ shouldCheckForUpdate() { return this._ready; } /** * This method is called at the beginning of the `update` step to determine * if we should do a full render of the object. This should be the case if, for * instance, the source is the camera. * * You can override this depending on your needs. The inherited child should * not ignore this value, it should do a boolean OR, e.g.: * `return super.shouldFullUpdate(updateSource) || this.contains(updateSource);` * * @param updateSource - Source of change * @returns `true` if requires a full update of this object */ shouldFullUpdate(updateSource) { return updateSource === this || updateSource.isCamera; } /** * This method is called at the beginning of the `update` step to determine * if we should re-render `updateSource`. * Not used when `shouldFullUpdate` returns `true`. * * You can override this depending on your needs. The inherited child should * not ignore this value, it should do a boolean OR, e.g.: * `return super.shouldUpdate(updateSource) || this.contains(updateSource);` * * @param updateSource - Source of change * @returns `true` if requires an update of `updateSource` */ // eslint-disable-next-line @typescript-eslint/no-unused-vars shouldUpdate() { return false; } /** * Filters what objects need to be updated, based on `updatedSources`. * The returned objects are then passed to {@link preUpdate} and {@link postUpdate}. * * Inherited classes should override {@link shouldFullUpdate} and {@link shouldUpdate} * if they need to change this behavior. * * @param updateSources - Sources that triggered an update * @returns Set of objects to update */ filterChangeSources(updateSources) { let fullUpdate = false; const filtered = new Set(); updateSources.forEach(src => { fullUpdate = fullUpdate || this.shouldFullUpdate(src); if (this.shouldUpdate(src)) { filtered.add(src); } }); return fullUpdate ? new Set([this]) : filtered; } /** * This method is called just before `update()` to filter and select * which _elements_ should be actually updated. For example, in the * case of complex entities made of a hierarchy of elements, the entire * hierarchy may not need to be updated. * * Use this method to optimize the update step by reducing the number * of elements to process. * * Note: if this functions returns nothing, `update()` will not be called. * * @param context - the update context. * @param changeSources - the objects that triggered an update step. * This is useful to filter out unnecessary updates if no sources are * relevant to this entity. For example, if one of the sources is a * camera that moved during the previous frame, any entity that depends * on the camera's field of view should be updated. * @returns the _elements_ to update during `update()`. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars preUpdate() { return null; } /** * Performs an update on an _element_ of the entity. * * Note: this method will be called for each element returned by `preUpdate()`. * * @param context - the update context. * This is the same object that the entity whose `update()` is being called. * @param element - the element to update. * This is one of the elements returned by {@link preUpdate}. * @returns New elements to update */ // eslint-disable-next-line @typescript-eslint/no-unused-vars update() { return undefined; } /** * Method called after {@link update}. * * @param context - the update context. * @param changeSources - the objects that triggered an update step. * This is useful to filter out unnecessary updates if no sources are * relevant to this entity. For example, if one of the sources is a * camera that moved during the previous frame, any entity that depends * on the camera's field of view should be updated. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars postUpdate() { /** do nothing */ } /** * Disposes this entity and all resources associated with it. * * The default implementation of this method does nothing. * You should implement it in your custom entities to handle any special logic of disposal. * * For example: disposing materials, geometries, stopping HTTP requests, etc. * */ dispose() { /** do nothing */ } /** * Notifies the parent instance that a change occured in the scene. */ notifyChange(source) { this._instance?.notifyChange(source); } } export function isEntity(o) { return o?.isEntity; } export default Entity;