UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

412 lines (331 loc) • 10 kB
import { assert } from "../../../assert.js"; import { isArrayEqual } from "../../../collection/array/isArrayEqual.js"; import List from "../../../collection/list/List.js"; import Signal from "../../../events/signal/Signal.js"; import { objectDeepEquals } from "../../object/objectDeepEquals.js"; import { NodeInstancePortReference } from "./NodeInstancePortReference.js"; import { PortDirection } from "./PortDirection.js"; /** * * @type {number} */ let id_counter = 0; /** * Represents a node instance in a graph, its main purpose is to provide connection medium for the graph. * A useful analogy is that of a "class" and an "object", there can be multiple instances (objects) of the same class. In this analogy `NodeInstance` is the object, and `NodeDescription` is a class. * There can be multiple `NodeInstance`s referencing the same `NodeDescription`. */ export class NodeInstance { /** * Unique identifier * @readonly * @type {number} */ id = id_counter++; /** * * @type {NodeDescription} */ description = null; /** * * @type {NodeInstancePortReference[]} */ endpoints = []; /** * Internal instance data * @type {Object} */ parameters = {}; /** * @transient * @type {Array} */ outputsValues = []; /** * @readonly * @type {List<Connection>} */ connections = new List(); /** * Extra ports that are present just on this instance, these exist in addition to ports from the description * IDs of these ports must not collide with IDs of ports on the description * TODO implement functionality * @protected * @type {Port[]} */ dynamicPorts = []; /** * @readonly */ on = { /** * @readonly * @type {Signal<string, *, *>} */ parameterChanged: new Signal(), /** * @readonly * @type {Signal<string, *>} */ parameterAdded: new Signal(), /** * @readonly * @type {Signal<string, *>} */ parameterRemoved: new Signal(), /** * fires: (newDescription:NodeDescription, oldDescription:NodeDescription|null, this) * @readonly * @type {Signal<NodeDescription, NodeDescription, NodeInstance>} */ descriptionChanged: new Signal() }; /** * * @param {number} port_id * @param {PortDirection} direction * @param {Connection[]} result * @returns {number} number of connections matched */ getConnectionsByPort(port_id, direction, result) { let count = 0; const connections = this.connections; const l = connections.length; for (let i = 0; i < l; i++) { const connection = connections.get(i); if ( (direction === PortDirection.In && connection.target.port.id === port_id) || (direction === PortDirection.Out && connection.source.port.id === port_id) ) { result[count] = connection; count++; } } return count; } /** * Output port references * @returns {NodeInstancePortReference[]} */ get outEndpoints() { return this.endpoints.filter(ref => ref.port.direction === PortDirection.Out); } /** * Input port references * @returns {NodeInstancePortReference[]} */ get inEndpoints() { return this.endpoints.filter(ref => ref.port.direction === PortDirection.In); } /** * Outgoing connections from this node * @return {Connection[]} */ get outConnections() { return this.connections.filter(c => c.source.instance === this); } /** * * Incoming connections to this node * @return {Connection[]} */ get inConnections() { return this.connections.filter(c => c.target.instance === this); } /** * * @param {number} id * @param {*} value */ setOutputValue(id, value) { this.outputsValues[id] = value; } /** * * @param {number} id * @returns {*} */ getOutputValue(id) { return this.outputsValues[id]; } /** * @template T * @param {string} id * @returns {T|undefined} */ getParameterValue(id) { assert.isString(id, 'id'); return this.parameters[id]; } /** * @template T * @param {string} id * @param {T} value */ setParameterValue(id, value) { assert.isString(id, 'id'); assert.defined(value, 'value'); const parameters = this.parameters; const old_value = parameters[id]; if (old_value === value) { return; } parameters[id] = value; if (old_value === undefined) { this.on.parameterAdded.send2(id, value); } // perform .equals check if available if ( value !== undefined && old_value !== undefined && typeof value === "object" && typeof value.equals === "function" && value.equals(old_value) ) { // parameter value has not changed return; } this.on.parameterChanged.send3(id, value, old_value); } /** * * @param {string} id * @returns {boolean} */ hasParameter(id) { return this.parameters.hasOwnProperty(id) } /** * * @param {string} id * @returns {boolean} */ deleteParameter(id) { assert.isString(id, 'id'); if (!this.hasParameter(id)) { return false; } const parameters = this.parameters; const existing = parameters[id]; delete parameters[id]; this.on.parameterRemoved.send2(id, existing); return true; } /** * Will overwrite only those properties that are present in the hash * @param {Object} partial_hash */ setParameters(partial_hash) { assert.defined(partial_hash, 'parameters') for (const key in partial_hash) { this.setParameterValue(key, partial_hash[key]); } } clearParameters() { for (const key in this.parameters) { this.deleteParameter(key); } } /** * * @param {NodeDescription} description */ setDescription(description) { assert.defined(description, 'description'); assert.isObject(description, 'description'); assert.equal(description.isNodeDescription, true, 'description.isNodeDescription !== true'); if (!this.connections.isEmpty()) { throw new Error("Node is has connections, can only change description for unconnected nodes"); } const old_description = this.description; this.description = description; description.configureNode(this); //generate endpoints const ports = description.getPorts(); const port_count = ports.length; this.endpoints = new Array(port_count); for (let i = 0; i < port_count; i++) { const port = ports[i]; this.endpoints[i] = NodeInstancePortReference.from(this, port); } //clear parameters this.clearParameters(); // TODO address parameters in NodeDescription as well //populate parameter defaults description.parameters.forEach(pd => { this.parameters[pd.id] = pd.defaultValue; }); this.on.descriptionChanged.send3(description, old_description, this); } /** * * @param {number} port_id Port ID * @returns {NodeInstancePortReference|undefined} */ getEndpoint(port_id) { assert.isNonNegativeInteger(port_id, 'port'); const endpoints = this.endpoints; const endpoint_count = endpoints.length; for (let i = 0; i < endpoint_count; i++) { const endpoint = endpoints[i]; if (endpoint.port.id === port_id) { return endpoint; } } //not found return undefined; } /** * * @param {string} name * @returns {undefined|NodeInstancePortReference} */ getFirstEndpointByName(name) { assert.isString(name, 'name'); const endpoints = this.endpoints; const endpoint_count = endpoints.length; for (let i = 0; i < endpoint_count; i++) { const ref = endpoints[i]; if (ref.port.name === name) { return ref; } } // not found return undefined; } /** * * @return {number} */ hash() { return this.id; } /** * * @param {NodeInstance} other * @returns {boolean} */ equals(other) { return this.id === other.id && this.description === other.description && isArrayEqual(this.endpoints, other.endpoints) && objectDeepEquals(this.parameters, other.parameters) ; } toString() { let result = `NodeInstance{ id = ${this.id}`; // add description const d = this.description; if (d !== undefined && d !== null) { result += `, description = ${d.id}(${d.name})` } result += ' }'; return result; } } /** * @readonly * @type {boolean} */ NodeInstance.prototype.isNodeInstance = true;