import { BuildableComponent } from "../package/component/buildable_component.js";
import { Reporter } from "../reporter/reporter.js";
import { Configuration } from "./configuration.js";
import { checkPropTypes } from '../lib.js';
// consideration:
// might be helpful to create phantom connectors instances that can be either connected or not
// simply to track state
// but these will not have an id, so the connection can not refer to them directly
// then what is the use?
const emptyConnectorTemplate = {
connectors: []
}
/**
* Base class for instantiated connectable elements, such as BlockInstance (which
* instantiates Block) and Connection (which can be seen as an instance of ConnectionType).
* @class
*/
class Connectable extends BuildableComponent {
/**
*
* @param {Reporter} reporter
* @param {Object} settings
*/
// * @param {BuildableComponent} settings.connectorTemplate
// * @param {Array<Connector>} settings.connectors
constructor(reporter, settings) {
super(
reporter,
settings
);
if (!this.constructor.connectorTemplateSetting) {
throw new Error(`${this.constructor.name} is missing static property connectorTemplateSetting`);
}
// ideally, a connectable has a connector template on construction
// alas, for a connection, the connectionType is the connection template
// and when it is constructed, the connector type is still unknown
// therefore, when a connectable doesn't specify a connector template
// on initialisation, the emptyConnectorTemplate is used as placeholder
// until the ._tree field is set
checkPropTypes(
settings,
{},
{
[this.constructor.connectorTemplateSetting]: [
BuildableComponent,
]
}
);
if (settings[this.constructor.connectorTemplateSetting]) {
this.connectorTemplate = settings[this.constructor.connectorTemplateSetting];
}
else {
this.connectorTemplate = emptyConnectorTemplate;
}
}
clone(newSettings = {}) {
const cl = super.clone(newSettings);
cl.connectorTemplate = this.connectorTemplate;
return cl;
}
/**
* The Component that specifies which connectors this Connectable has
* @type {Component}
*/
connectorTemplate;
/**
* All connections of which this connectable is a part
* @type {Array<Connection>}
*/
connections;
/**
* All equal connections (no parent/child relationship) of which this connectable is a part
* @type {Array<Connection>}
*/
equalConnections;
/**
* All unequal connections (with parent/child relationship) of which this connection is a part
* @type {Array<Connection>}
*/
unequalConnections;
/**
* All other connectables this one is connected to
* @type {Array<Connectable>}
*/
connectedTo;
/**
* A Connectable that acts as a sibling to this one
* @type {(Array<Connectable>)}
*/
siblings;
/**
* A Connectable that acts as a parent to this one
* @type {(Connection)}
*/
parentConnection;
/**
* A Connectable that acts as a parent to this one
* @type {(Connectable)}
*/
parent;
/**
* Connectables that acts as children to this one
* @type {Array<Connection>}
*/
childConnections;
/**
* Connectables that acts as children to this one
* @type {Array<Connectable>}
*/
children;
/**
*
*/
childMaterialAssignments;
get siblingChains() {
const siblingsInAChain = [];
const chains = {};
for ( let sibling of this.siblings || [] ) {
// console.log(`${this.label} find siblings of sibling ${sibling.label}`);
const chain = [ sibling, ...Connectable.findSiblings(sibling,[this, ...siblingsInAChain])]; // safeguard against circular designs
chains[ sibling.id ] = chain;
siblingsInAChain.push(...chain);
}
return chains;
};
findMaterialAssignments() {
if ( this._tree && this._tree.materialAssignments ) {
return this._tree.materialAssignments.filter(ma => ma.blockInstance === this);
}
else {
return [];
}
}
static findSiblings = (connectable, excludeList = []) => {
// console.log( 'conn', connectable, 'excl', excludeList);
return ( connectable.siblings || [] )
.filter(sibling => !excludeList.includes(sibling))
.map(sibling => [ sibling, ...Connectable.findSiblings( sibling, [connectable, ...excludeList])])
.flat();
}
_updateConnections() {
// console.log( 'update connections', this._tree.connections.length )
this.materialAssignments = this.findMaterialAssignments();
this.connections = (this._tree.connections || []).filter(cnn =>
(cnn.from.connectable === this && this.connectorTemplate.connectors.includes(cnn.from.connector))
||
(cnn.to.connectable === this && this.connectorTemplate.connectors.includes(cnn.to.connector))
);
this.equalConnections = this.connections.filter(cnn => cnn.type.parent === undefined);
this.unequalConnections = this.connections.filter(cnn => cnn.type.parent !== undefined);
//console.log( this.unequalConnections )
this.connectedTo = this.connections.map(cnn => cnn.from.connectable === this ? cnn.to.connectable : cnn.from.connectable);
this.siblings = this.equalConnections.map(cnn => cnn.from.connectable === this ? cnn.to.connectable : cnn.from.connectable);
// this.siblingChains = {};
// It is known that the unequal connections can not have the same connector type twice
// and it is known that one of the two sides must be the parent. So, if the from side
// connectable equals this and the from side connector is the parent type, this is
// the parent, otherwise this is the child
const myLabel = this.block ? this.block.label + ` Instance` : this.label;
//console.log(`${myLabel} has ${this.equalConnections.length} equal connections`, this.equalConnections);
//console.log(`${myLabel} has ${this.unequalConnections.length} unequal connections`, this.unequalConnections);
let parentConnection = null;
// find all connections to parents
const connectionToMyParents = this.unequalConnections.filter( cnn => {
//Get the "to" or "from" side
const mySide = cnn.from.connectable === this ? 'from' : 'to';
// Check the parentConnection (cnn) based on if the connectorType of mySide matches the connectorType in the parent prop of the connection.
const iAmParent = cnn[mySide].connector.type === cnn.type.parent;
//console.log( `${myLabel} unequal connection type ${cnn.type.name} my side ${mySide}, iamparent = ${iAmParent}`);
// if I am parent, I am NOT a child, so this is not a connection to my parent
return !iAmParent;
});
// check whether only one parent connection exists
let connectionToMyParent = null;
switch ( connectionToMyParents.length ) {
case 0:
// connectable has no parent
//console.log(`${myLabel} has NO parent (connection)`);
break;
case 1:
// connectable has a parent
connectionToMyParent = connectionToMyParents[0];
//console.log(`${myLabel} has 1 parent (connection):`, connectionToMyParent);
break;
default:
// connectable has more than 1 parent, error
console.info(`Multiple parent connections:`, connectionToMyParents);
throw new Error(`${this.label} has more than one parent!`);
}
if ( connectionToMyParent ) {
//Map the parentConnection as prop on the blockInstance.
this.parentConnection = connectionToMyParent;
const parentSide = connectionToMyParent.from.connectable === this ? 'to' : 'from';
//Map the connectable that is the parent
this.parent = connectionToMyParent[parentSide].connectable;
//console.log(`${myLabel} parent is`, this.parent );
}
const childConnections = this.unequalConnections
.filter(cnn => {
const isChildConnection = cnn !== connectionToMyParent;
//console.log(`${myLabel} connection ${cnn.type.name} is child connection = ${isChildConnection}`, cnn);
return isChildConnection;
});
//console.log(`${myLabel} has ${childConnections.length} child connections`, childConnections);
//---
if(childConnections) {
this.childConnections = childConnections;
this.children = childConnections
.map(ucnn => ucnn.connectables.find(ctb => ctb !== this));
this.childMaterialAssignments = this.children.map(child => child.findMaterialAssignments()).flat();
}
}
_onTreeSet() {
// console.error( 'tree set', this._tree.label)
console.assert(this._tree instanceof Configuration, `${this.constructor.name} can not be added to a ${this._tree.constructor.name}`);
const component = this;
if (!this._tree.findComponentById(this.connectorTemplate.id)) {
throw new Error(`ConnectorTemplate ${this.connectorTemplate.label} not in tree`);
}
this._tree.on(
'change',
({ cls, type }) => {
// console.log( 'change', type, cls)
if (cls.find( cl => cl.name === 'Connection')) {
component._updateConnections();
} else if(cls.find( cl => cl.name === 'MaterialAssignment' )) {
this.materialAssignments = this.findMaterialAssignments();
}
},
this.id
);
this._updateConnections();
}
_onTreeUnset() {
this._tree.removeListeners('change', this.id);
this.connections = undefined;
this.connectedTo = undefined;
this.materialAssignments = undefined;
this.childMaterialAssignments = undefined;
}
}
export { Connectable };