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