@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
454 lines (364 loc) • 10.7 kB
JavaScript
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;