import { Group, Rectangle, SvgImage } from '../svgObjects';
import NetworkElement from './NetworkElement';
import InputConnector from './InputConnector';
import OutputConnector from './OutputConnector';
import Transform from './Transform';
/** @module editorElements.Box */
/**
* Parent class for gates and input and output boxes. Defines all the factors
* that the boxes have in common (svgObj structure, draggability and rotatability...)
* @extends NetworkElement
*/
export default class Box extends NetworkElement {
/**
* @param {App} appInstance instance of [App](./module-App.html)
* @param {string} name name of the element (input, output, and, or, xor...)
* @param {string} category type of the element (io, gate)
* @param {number} gridWidth width of the element in grid pixels
* @param {number} gridHeight height of the element in grid pixels
*/
constructor(appInstance, name, category, gridWidth, gridHeight) {
super(appInstance);
/**
* specifies the box type within the category (input/output in io, and/or/... in gate)
* @type {string}
*/
this.name = name;
/**
* specifies the box category (io for input or output, gate for logic gates)
* @type {string}
*/
this.category = category;
/**
* size of the grid in SVG pixels
* @type {number}
*/
this.gridSize = this.appInstance.gridSize;
/**
* array of connectors of this box
* @type {Array}
*/
this.connectors = [];
/**
* svgObj containing all SVG data used to display this box
* @type {svgObj}
*/
this.svgObj = new Group();
/**
* width of this element in SVG pixels
* @type {number}
*/
this.width = gridWidth * this.gridSize;
/**
* height of this element in SVG pixels
* @type {number}
*/
this.height = gridHeight * this.gridSize;
/**
* width of this element in grid pixels
* @type {number}
*/
this.gridWidth = gridWidth;
/**
* height of this element in grid pixels
* @type {number}
*/
this.gridHeight = gridHeight;
// transparent background rectangle
let rectangle = new Rectangle(0, 0, this.width, this.height, 'none', 'none');
rectangle.$el.addClass('rect');
this.svgObj.addChild(rectangle);
// image of the element
this.image = new SvgImage(0, 0, this.width, this.height, this.url);
this.svgObj.addChild(this.image);
// add type="gate", used in special callbacks in contextmenu
this.svgObj.addAttr({ type: category });
this.svgObj.$el.addClass('box');
this.svgObj.$el.addClass(category);
}
/**
* url of the image depicting this object
* @type {string}
*/
get url() {
const category = this.category || '',
name = this.name || '',
suffix = this.imgSuffix || '';
return `img/svg/${category}/${name}${suffix}.svg`;
}
/**
* get all input connectors of this box
* @return {Array} array of input connectors
*/
get inputConnectors() {
return this.connectors.filter(conn => conn.isInputConnector);
}
/**
* get all output connectors of this box
* @return {Array} array of output connectors
*/
get outputConnectors() {
return this.connectors.filter(conn => conn.isOutputConnector);
}
/**
* get data of this box as a JSON-ready object
* @return {Object} javascript object containing essential data for this box
*/
get exportData() {
let connections = [];
// go through all connectors
let counter = 0;
for (const conn of this.connectors) {
// go through each its wire id
for (const item of conn.wireIds) {
let thisWireId;
if (!this.appInstance.exportWireIdMap.has(item)) {
// if the wire id is not in the map, add it and assign new arbitrary id
this.appInstance.exportWireIdMap.set(item, this.appInstance.exportWireId);
thisWireId = this.appInstance.exportWireId;
this.appInstance.exportWireId++;
} else {
// else get id from the map
thisWireId = this.appInstance.exportWireIdMap.get(item);
}
// add this connection to the list
connections[connections.length] = {
index: counter,
type: conn.type,
wireId: thisWireId
};
}
counter++;
}
return {
name: this.name,
category: this.category,
transform: this.getTransform(true),
connections: connections
};
}
/**
* get set of nodes that are not suitable for wire routing
* @param {Number} [marginTop=0] top margin of the element (distance from the element that should be also blocked)
* @param {Number} [marginRight=0] right margin of the element
* @param {Number} [marginBottom=0] bottom margin of the element
* @param {Number} [marginLeft=0] left margin of the element
* @param {Number} specialNodes additional nodes that should be added to the set
* @return {Set} set of not suitable nodes
*/
generateBlockNodes(
marginTop = 0,
marginRight = 0,
marginBottom = 0,
marginLeft = 0,
...specialNodes
) {
this.blockedNodes = new Set();
for (let x = marginLeft; x <= this.gridWidth - marginRight; x++) {
for (let y = marginTop; y <= this.gridHeight - marginBottom; y++) {
this.blockedNodes.add({
x: x,
y: y
});
}
}
for (let node of specialNodes) {
this.blockedNodes.add(node);
}
}
/**
* empty function, redefined in inherited elements
* refreshState takes input connector values and sets output values accordingly
*/
refreshState() {
console.warn('Calling the virtual function refreshState has no effect.');
}
/**
* change image to another one that ends with a specified suffix
*
* *usage:* `changeImage("abc")` changes image url to `image-abc.svg`,
* `changeImage()` changes image url to the default one (`image.svg`)
* @param {string} [suffix] new suffix for the image
*/
changeImage(suffix) {
if (suffix === undefined || suffix === '') {
this.imgSuffix = '';
} else {
this.imgSuffix = '-' + suffix;
}
this.image.changeUrl(this.url);
}
/**
* get a jQuery element representing this box
* @return {jQuery.element}
*/
get() {
return this.svgObj.get();
}
/**
* rotate the set of blocked nodes by 90 degrees to the right or to the left, depending on the parameter
*
* used to rotate the nodes when the object itself is rotated
* @param {boolean} right rotate clockwise if true, counterclockwise if false
*/
rotateBlockedNodes(center, right) {
if (this.rotationParity === undefined) {
this.rotationParity = false;
}
this.rotationParity = !this.rotationParity;
let newBlockedNodes = new Set();
// rotate the node
console.log('center:', center);
for (const node of this.blockedNodes) {
let newNode;
const parityFactor = this.rotationParity ? 1 : -1;
if (right) {
newNode = {
x: -node.y + this.gridHeight + (center.x - center.y) * parityFactor,
y: node.x + (center.y - center.x) * parityFactor
};
} else {
newNode = {
x: node.y + (center.x - center.y) * parityFactor
};
if (this.rotationParity) {
newNode.y =
-node.x +
this.gridWidth +
(this.gridHeight - center.y - (this.gridWidth - center.x));
} else {
newNode.y = -node.x + this.gridHeight + (center.y - center.x);
}
}
newBlockedNodes.add(newNode);
}
this.blockedNodes = newBlockedNodes;
}
/**
* rotate the set of blocked nodes to the right
*
* used to rotate the nodes when the object itself is rotated
*/
rotateBlockedNodesRight(center) {
this.rotateBlockedNodes(center, true);
}
/**
* rotate the set of blocked nodes to the right
*
* used to rotate the nodes when the object itself is rotated
*/
rotateBlockedNodesLeft(center) {
this.rotateBlockedNodes(center, false);
}
rotate(clockWise) {
// get the transform value for this box and convert it to grid pixels
// (so we don't have to convert between SVG and grid pixels manually)
let transform = this.getTransform();
transform.toGridPixels(this.appInstance);
// calculate the center of the box
const realCenter = {
x: Math.round(this.gridWidth / 2),
y: Math.round(this.gridHeight / 2)
};
// swap the coordinates when the rotation parity is 1
const center = this.rotationParity
? {
x: realCenter.y,
y: realCenter.x
}
: realCenter;
// apply the rotation to the transform object
if (clockWise) {
transform.rotateRight(center.x, center.y);
} else {
transform.rotateLeft(center.x, center.y);
}
// rotate the blocked nodes as well
if (clockWise) {
this.rotateBlockedNodesRight(center);
} else {
this.rotateBlockedNodesLeft(center);
}
// convert the modified transform back to SVG pixels
// and apply it to the svgObj
transform.toSVGPixels(this.appInstance);
this.svgObj.addAttr({ transform: transform.get() });
// update the wires
this.updateWires();
// if tutorial exists, call the tutorial callback
if (this.appInstance.tutorial) {
this.appInstance.tutorial.onBoxRotated();
}
}
/**
* add a connector to the element on the specified position
* @param {number} left horizontal distance from the left edge of the element
* @param {number} top vertical distance from the top edge of the element
* @param {Boolean} isInputConnector whether or not should this connector an input connector (`true` for input connector, `false` for output connector)
*/
addConnector(left, top, isInputConnector) {
let index = this.connectors.length;
if (isInputConnector) {
this.connectors[index] = new InputConnector(this.appInstance, left, top);
} else {
this.connectors[index] = new OutputConnector(this.appInstance, left, top);
}
this.svgObj.addChild(this.connectors[index].get());
}
/**
* add an input connector to the element on the specified position
* @param {number} left horizontal distance from the left edge of the element
* @param {number} top vertical distance from the top edge of the element
*/
addInputConnector(left, top) {
return this.addConnector(left, top, true);
}
/**
* add an output connector to the element on the specified position
* @param {number} left horizontal distance from the left edge of the element
* @param {number} top vertical distance from the top edge of the element
*/
addOutputConnector(left, top) {
return this.addConnector(left, top, false);
}
/**
* get the connector object based on its id
* @param {string} connectorId ID of the {@link Connector}
* @return {Connector} instance of the {@link Connector} or `undefined` if not found
*/
getConnectorById(connectorId) {
for (let i = 0; i < this.connectors.length; i++) {
if (this.connectors[i].id === connectorId) {
return this.connectors[i];
}
}
// if connector not found, return undefined
return undefined;
}
/**
* get the instance of {@link Transform} representing the state of the transform attribute of this element
* @param {Boolean} [gridPixels=false] if `true`, function will return the result in grid pixels instead of SVG pixels
* @return {Transform} {@link Transform} of the element
*/
getTransform(gridPixels = false) {
let transform;
if (!this.svgObj.$el.attr('transform')) {
// the element does not have a "transform" property --> create it
transform = new Transform();
transform.setTranslate(0, 0);
this.svgObj.addAttr({ transform: transform.get() });
} else {
// the element does have a "transform" property --> change it
transform = new Transform(this.svgObj.$el.attr('transform'));
}
// convert values to grid pixels
if (gridPixels) {
transform.toGridPixels(this.appInstance);
}
return transform;
}
/**
* get the instance of {@link Transform} representing the state of the transform attribute of this element _with lenght units in grid pixels_
* @return {Transform} {@link Transform} of the element
*/
getGridPixelTransform() {
return this.getTransform(true);
}
/**
* set the transform attribute of this element
* @param {Transform} transform {@link Transform} of the element (with lengths specified in SVG pixels)
*/
setTransform(transform) {
this.svgObj.addAttr({ transform: transform.get() });
}
/**
* function that is called on every mouse down on this element
*
* moves the element to the front and calls onMouseDownLeft if applicable
* @param {jQuery.MouseEvent} event
*/
onMouseDown(event) {
this.mouseLeft = false;
if (event.which === 1) {
this.mouseLeft = true;
this.onMouseDownLeft(event);
// move the DOM element to front
this.appInstance.moveToFrontById(this.svgObj.id);
}
}
/**
* function that is called on every left mouse down on this element
*
* prepares element for the "click" and "drag and drop" actions
* @param {jQuery.MouseEvent} event
*/
onMouseDownLeft(event) {
this.mouseMoved = false;
let transform = this.getTransform();
// save the current item position into a variable
let currentPosition = transform.getTranslate();
let { pageX, pageY } = this.appInstance.viewbox.transformEvent(event);
// calculate mouse offset from the object origin
this.offset = {
x: pageX - currentPosition.x,
y: pageY - currentPosition.y
};
}
/**
* function that is called on every left mouse move with this element
* applies the correct transform values to provide the "drag and drop" functionality
* @param {jQuery.MouseEvent} event
*/
onMouseMove(event) {
if (this.mouseLeft) {
this.svgObj.$el.addClass('grabbed');
this.mouseMoved = true;
let { pageX, pageY } = this.appInstance.viewbox.transformEvent(event);
const left = pageX - this.offset.x;
const top = pageY - this.offset.y;
let transform = this.getTransform();
transform.setTranslate(left, top);
this.setTransform(transform);
this.updateWires(true);
}
}
/**
* function that is called on every mouse up on this element
* provides the "click" functionality and calls the onDrop handler for the "drag and drop" functionality
* @param {jQuery.MouseEvent} event
*/
onMouseUp(event) {
if (event.which === 1) {
if (this.mouseMoved) {
this.onDrop(event);
} else {
this.onClick();
}
} else if (event.which === 2) {
this.onClickMiddle(event);
}
this.svgObj.$el.removeClass('grabbed');
}
/**
* called by onMouseUp when the mouse has been moved between onMouseDown and onMouseUp
*
* applies grid snapping of the element on the end of the "drag and drop" action
* @param {jQuery.MouseEvent} event
*/
onDrop(event) {
let { pageX, pageY } = this.appInstance.viewbox.transformEvent(event);
let left = pageX - this.offset.x;
let top = pageY - this.offset.y;
left = this.appInstance.snapToGrid(left);
top = this.appInstance.snapToGrid(top);
let transform = this.getTransform();
transform.setTranslate(left, top);
this.setTransform(transform);
this.updateWires();
// if tutorial exists, call tutorial callback
if (this.appInstance.tutorial) {
this.appInstance.tutorial.onBoxMoved();
}
}
/**
* empty function, will be redefined in InputBox
*/
onClick() {}
/**
* custom callback function for middle click that rotates the box by 90 degrees to the right
*/
onClickMiddle(event) {
if (event.ctrlKey) {
this.rotate(false);
} else {
this.rotate(true);
}
}
/**
* Updates all wires connected to this box. Iterates over all wires that are connected to this box
* and calls routeWire (or temporaryWire if the `temporary` parameter is set to true) to update the wire routing
* @param {Boolean} [temporary=false] [description]
*/
updateWires(temporary = false) {
this.connectors.forEach(conn => {
conn.wireIds.forEach(wireId => {
let wire = this.appInstance.getWireById(wireId);
if (temporary) {
wire.temporaryWire();
} else {
wire.routeWire();
}
});
});
}
}