UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

454 lines (364 loc) • 10.7 kB
import { assert } from "../../../assert.js"; import { BitSet } from "../../../binary/BitSet.js"; import { isArrayEqual } from "../../../collection/array/isArrayEqual.js"; import Signal from "../../../events/signal/Signal.js"; import { invokeObjectToJSON } from "../../object/invokeObjectToJSON.js"; import { NodeParameterDataType } from "./parameter/NodeParameterDataType.js"; import { NodeParameterDescription } from "./parameter/NodeParameterDescription.js"; import { Port } from "./Port.js"; import { PortDirection } from "./PortDirection.js"; /** * @private * @param {{id:number}[]} things * @returns {number} */ function pickNewSetId(things) { const used = new BitSet(); const n = things.length; for (let i = 0; i < n; i++) { const thing = things[i]; if (thing !== undefined) { used.set(thing.id, true); } } return used.nextClearBit(0); } /** * * @type {number} */ let node_id_counter = 0; export class NodeDescription { /** * Useful human-readable label * @type {string} */ name = ""; /** * A unique identifier in the scope of a single registry * If two nodes belong to the same registry - they must have different IDs * By default an auto-incrementing ID is assigned, as this ID is assigned at the time of object construction - it may be subject to race conditions * In case nodes are loaded or constructed in async manner - consider using your own ID assignment scheme * @type {number} */ id = node_id_counter++; /** * @protected * @type {Port[]} */ ports = []; /** * * @type {NodeParameterDescription[]} */ parameters = []; /** * @readonly */ on = { /** * @readonly * @type {Signal<Port>} */ portAdded: new Signal(), /** * @readonly * @type {Signal<Port>} */ portRemoved: new Signal() }; /** * Perform additional processing on the node instance * This gives us an opportunity to add/change ports based on current state of a specific node instance * @param {NodeInstance} instance */ configureNode(instance) { // override in subclasses as necessary } /** * Output ports * @returns {Port[]} */ get outPorts() { return this.getPortsByDirection(PortDirection.Out); } /** * Input ports * @returns {Port[]} */ get inPorts() { return this.getPortsByDirection(PortDirection.In); } /** * * @param {string} name * @param {NodeParameterDataType} type * @param {number|boolean|string} [defaultValue] * @returns {number} */ createParameter(name, type, defaultValue) { assert.isString(name, 'name'); assert.enum(type, NodeParameterDataType, 'type'); let _default = defaultValue; //if default value is not given, pick one if (_default === undefined) { switch (type) { //intended fallthrough case NodeParameterDataType.Number: _default = 0; break; case NodeParameterDataType.Boolean: _default = false; break; case NodeParameterDataType.String: _default = ""; break; default: throw new Error(`Unknown data type '${type}'`); } } const id = pickNewSetId(this.parameters); const pd = new NodeParameterDescription(); pd.name = name; pd.type = type; pd.defaultValue = _default; pd.id = id; assert.ok(pd.validate(console.error), 'parameter is not valid'); this.parameters.push(pd); return id; } /** * * @param {number} id * @returns {NodeParameterDescription|null} */ getParameter(id) { assert.isNonNegativeInteger(id, 'id'); const paramDescriptors = this.parameters; for (let i = 0; i < paramDescriptors.length; i++) { const paramDescriptor = paramDescriptors[i]; if (paramDescriptor.id === id) { return paramDescriptor; } } //not found return null; } /** * * @param {DataType} type * @param {String} name * @param {PortDirection} direction * @returns {number} ID of the newly created port */ createPort(type, name, direction) { assert.defined(type, 'type'); assert.notNull(type, 'type'); assert.equal(type.isDataType, true, 'type.isDataType !== true'); assert.defined(direction, 'direction'); assert.enum(direction, PortDirection, 'direction'); assert.isString(name, 'name'); const port = new Port(); const id = pickNewSetId(this.ports); port.id = id; port.dataType = type; port.name = name; port.direction = direction; this.ports.push(port); this.on.portAdded.send1(port); return id; } /** * * @param {number} id port ID * @returns {boolean} true if port was deleted, false if port wasn't found */ deletePort(id) { assert.defined(id, 'id'); assert.isNumber(id, 'id'); assert.isNonNegativeInteger(id, 'id'); const ports = this.ports; const port_count = ports.length; for (let i = 0; i < port_count; i++) { const port = ports[i]; if (port.id === id) { ports.splice(i, 1); this.on.portRemoved.send1(port); return true; } } // port not found return false; } /** * * @param {number} id * @returns {Port|null} */ getPortById(id) { assert.isNonNegativeInteger(id, 'id'); const ports = this.ports; const port_count = ports.length; for (let i = 0; i < port_count; i++) { const port = ports[i]; if (port.id === id) { return port; } } //not found return null; } /** * * @param {string} name * @return {Port[]} */ getPortsByName(name) { assert.isString(name, 'name'); /** * * @type {Port[]} */ const result = []; const ports = this.ports; const port_count = ports.length; for (let i = 0; i < port_count; i++) { const port = ports[i]; if (port.name === name) { result.push(port); } } return result; } /** * Useful for checking if a node has inputs/outputs * Does not allocate and terminates early, so performance characteristics are very good * @param {PortDirection} direction * @return {boolean} */ hasPortsByDirection(direction) { assert.enum(direction, PortDirection, 'direction'); const ports = this.ports; const port_count = ports.length; for (let i = 0; i < port_count; i++) { const port = ports[i]; if (port.direction === direction) { return true; } } return false; } /** * * @param {PortDirection} direction * @return {Port[]} */ getPortsByDirection(direction) { assert.enum(direction, PortDirection, 'direction'); /** * * @type {Port[]} */ const result = []; const ports = this.ports; const port_count = ports.length; for (let i = 0; i < port_count; i++) { const port = ports[i]; if (port.direction === direction) { result.push(port) } } return result; } /** * * @param {string} name * @param {PortDirection} direction * @return {Port|undefined} */ findPortByNameAndDirection(name, direction) { assert.isString(name, 'name'); assert.enum(direction, PortDirection, 'direction'); const ports = this.ports; const port_count = ports.length; for (let i = 0; i < port_count; i++) { const port = ports[i]; if (port.name === name && port.direction === direction) { return port; } } // port not found return undefined; } /** * * @returns {Port[]} */ getPorts() { return this.ports } /** * * @return {number} */ hash() { return this.id; } /** * * @param {NodeDescription} other * @returns {boolean} */ equals(other) { if (this === other) { // fast shortcut return true; } return this.id === other.id && this.name === other.name && isArrayEqual(this.ports, other.ports) ; } toString() { return `NodeDescription[id=${this.id}, name='${this.name}']`; } toJSON() { return { id: this.id, name: this.name, ports: this.ports.map(invokeObjectToJSON), } } /** * * @param j * @param {NodeRegistry} registry * @returns {NodeDescription} */ static fromJSON(j, registry) { const r = new NodeDescription(); r.fromJSON(j, registry); return r; } /** * * @param {[]} ports * @param {number} id * @param {string} name * @param {NodeRegistry} registry */ fromJSON({ ports = [], id, name = "" }, registry) { assert.isNonNegativeInteger(id, 'id'); assert.isString(name, 'name'); assert.isArray(ports, 'ports'); this.id = id; this.name = name; this.ports = ports.map(p => Port.fromJSON(p, registry)); } } NodeDescription.typeName = "NodeDescription"; /** * @readonly * @type {boolean} */ NodeDescription.prototype.isNodeDescription = true;