// editor elements (gates, wires...)
import * as editorElements from './editorElements';
// svg elements
import { Pattern, Rectangle, PolyLinePoint, PolyLinePoints, PolyLine } from './svgObjects';
// network logic and simulation
import Logic from './Logic';
import Simulation from './Simulation';
import { SimulationDummy } from './Simulation';
// ui stuff
import ContextMenu from './ui/ContextMenu';
import FloatingMenu from './ui/FloatingMenu';
import Tutorial from './ui/Tutorial';
import Messages from './ui/Messages';
import ViewBox from './ui/ViewBox';
// mouse scroll event listerer for ui, manhattan distance for importData
import { addMouseScrollEventListener, manhattanDistance } from './other/helperFunctions';
// used in importData
// note: imported from a node module
import { PriorityQueue } from 'libstl';
const ctrlKey = 17,
cmdKey = 91;
/** @module App */
/**
* Main class of the application. It represents an instance of the whole editor and holds
* references to all its elements.
*/
export default class App {
/**
* Initialize the Svg class
* @param {string} canvas query selector of the SVG element, that will contain all SVG content of the application
* @param {number} gridSize initial size of the grid in SVG pixels
*/
constructor(canvas, gridSize) {
/**
* jQuery element for the SVG document
*/
this.$svg = $(canvas);
/**
* space between grid lines in SVG pixels
* @type {number}
*/
this.gridSize = gridSize;
/**
* Array of all boxes (instances of objects derived from editorElements.Box) used on canvas
* @type {Array}
*/
this.boxes = []; // stores all boxes
/**
* Array of all wires (instances of editorElements.Wire) used on canvas
* @type {Array}
*/
this.wires = []; // stores all wires
/**
* Interface for showing messages to the user
* @type {Messages}
*/
this.messages = new Messages();
this.simulationEnabled = true;
this.simulation = new SimulationDummy(); // dummy, will be overwritten on startNewSimulation
/**
* distance from the left top corner to the first element in the imported network
* and distance from the left top corner to the imported black box _in grid pixels_
* @type {number}
*/
this.leftTopPadding = 4;
// create the defs element, used for patterns
this.$defs = $('<defs>');
this.$svg.prepend(this.$defs);
// BACKGROUND PATTERN
let pattern = new Pattern('grid', this.gridSize, this.gridSize);
let patternPoints = new PolyLinePoints()
.append(new PolyLinePoint(0, 0))
.append(new PolyLinePoint(this.gridSize, 0))
.append(new PolyLinePoint(this.gridSize, this.gridSize));
pattern.addChild(new PolyLine(patternPoints, 2, '#c2c3e4'));
this.addPattern(pattern.get());
this.background = new Rectangle(0, 0, this.width, this.height, 'url(#grid)', 'none');
this.appendJQueryObject(this.background.get());
this.refresh();
// set the viewbox for future zooming and moving of the canvas
this.$svg.attr('preserveAspectRatio', 'xMinYMin slice');
this.viewbox = new ViewBox(0, 0, this.width, this.height);
this.applyViewbox();
// CONSTRUCT CONTEXT MENU
this.contextMenu = new ContextMenu(this);
// CONSTRUCT FLOATING MENU
this.floatingMenu = new FloatingMenu(this);
let target;
// ALL EVENT CALLBACKS
this.$svg
.on('mousedown', event => {
target = this.getRealTarget(event.target);
if (target !== undefined) {
// propagate mousedown to the real target
target.onMouseDown(event);
} else {
// mousedown happened directly on the svg
this.onMouseDown(event);
}
this.hideContextMenu();
event.preventDefault();
})
.on('mousemove', event => {
if (target !== undefined) {
target.onMouseMove(event);
} else {
// mousemove happened directly on the svg
this.onMouseMove(event);
}
event.preventDefault();
})
.on('mouseup', event => {
if (target !== undefined) {
target.onMouseUp(event);
} else {
// mouseup happened directly on the svg
this.onMouseUp(event);
}
target = undefined;
event.preventDefault();
})
.on('contextmenu', event => {
this.displayContextMenu(
event.pageX,
event.pageY,
this.getRealJQueryTarget(event.target)
);
event.preventDefault();
});
$(document)
.on('keydown', event => {
this.onKeyDown(event);
})
.on('keyup', event => {
this.onKeyUp(event);
});
// update the viewbox on window resize
$(window).on('resize', () => {
this.viewbox.newDimensions(this.width, this.height);
this.applyViewbox();
});
addMouseScrollEventListener(canvas, event => {
// zoom only if the ctrl key is not pressed
if (!event.ctrlKey) {
this.zoom += event.delta * 0.1;
event.preventDefault();
}
});
$(window).on('keydown', event => {
const actions = {
'+': 0.1,
'-': -0.1
};
if (actions[event.key]) {
this.zoom += actions[event.key];
}
});
/**
* property containing an instance of [Tutorial](./module-Tutorial.html), if there is any
* @type {Tutorial}
*/
this.tutorial;
// check if the user visits for the first time, if so, start the tutorial
try {
if (!localStorage.userHasVisited) {
this.startTutorial();
}
} catch (e) {
console.warn(e);
}
}
/**
* Get the width of the main SVG element
* @return {number} width of the SVG element in pixels
*/
get width() {
return this.$svg.width();
}
/**
* Get the height of the main SVG element
* @return {number} height of the SVG element in pixels
*/
get height() {
return this.$svg.height();
}
/**
* Process all keydown events that are connected to the app
* @param {jquery.KeyboardEvent} event KeyboardEvent generated by a listener
*/
onKeyDown(event) {
if (event.keyCode === ctrlKey || event.keyCode === cmdKey) {
this.$svg.addClass('grabbable');
}
}
/**
* Process all keyup events that are connected to the app
* @param {jquery.KeyboardEvent} event KeyboardEvent generated by a listener
*/
onKeyUp(event) {
if (event.keyCode === ctrlKey || event.keyCode === cmdKey) {
this.$svg.removeClass('grabbable');
}
}
/**
* Process all mousedown events that are happening directly on the canvas
* @param {jquery.MouseEvent} event MouseEvent generated by a listener
*/
onMouseDown(event) {
// any click on canvas cancels the wire creation
this.cancelWireCreation();
// middle mouse or left mouse + ctrl moves the canvas
if (event.which === 2 || (event.which === 1 && event.ctrlKey)) {
this.$svg.addClass('grabbed');
this.moveCanvas = {
left: event.pageX,
top: event.pageY
};
}
}
/**
* Process all mousemove events that are happening directly on the canvas
* @param {jquery.MouseEvent} event MouseEvent generated by a listener
*/
onMouseMove(event) {
if (this.moveCanvas) {
let left = event.pageX - this.moveCanvas.left;
let top = event.pageY - this.moveCanvas.top;
this.viewbox.move(left, top);
this.applyViewbox();
this.moveCanvas = {
left: event.pageX,
top: event.pageY
};
}
}
/**
* Process all mouseup events that are happening directly on the canvas
*/
onMouseUp() {
if (this.moveCanvas) {
this.$svg.removeClass('grabbed');
this.moveCanvas = undefined;
// if tutorial exists, call tutorial callback
if (this.tutorial) {
this.tutorial.onCanvasMoved();
}
}
}
/**
* Set the viewBox attribute of the SVG element and size and position attributes
* of the rectangle with the background grid to match the values in this.viewbox
*/
applyViewbox() {
// adjust background
this.background.addAttr({
x: this.viewbox.left,
y: this.viewbox.top,
width: this.viewbox.width,
height: this.viewbox.height
});
// set the viewBox attribute
this.$svg.attr('viewBox', this.viewbox.str);
}
/**
* Get the current zoom multiplier of the canvas
* @return {number}
*/
get zoom() {
return this.viewbox.zoom;
}
/**
* Set the zoom multiplier of the canvas.
* I sets the viewbox zoom and then applies the new value by calling this.applyViewbox()
* @param {number} value set the zoom to this value
*/
set zoom(value) {
this.viewbox.zoom = value;
this.applyViewbox();
// if tutorial exists, call tutorial callback
if (this.tutorial) {
this.tutorial.onCanvasZoomed();
}
}
/**
* start the tutorial
*/
startTutorial() {
// instantiate the tutorial
this.tutorial = new Tutorial(this, () => {
// set userHasVisited to true when user closes (or finishes) the tutorial
localStorage.userHasVisited = true;
// unset the this.tutorial property
this.tutorial = undefined;
});
// start the tutorial
this.tutorial.start();
}
/**
* Generate an object containing export data for the canvas and all elements.
* Data from this function should cover all important information needed to import the
* network in a different session.
* @return {object} object containing infomration about the network
*/
get exportData() {
this.exportWireIdMap = new Map();
this.exportWireId = 0;
let data = {
boxes: []
};
for (const box of this.boxes) {
data.boxes.push(box.exportData);
}
return data;
}
/**
* Recreate a logic network from the data provided
* @param {object} data object containing information about the imported network
* @param {number} [x] horizontal position of the left top corner of the network in grid pixels
* @param {number} [y] vertical position of the left top corner of the network in grid pixels
*/
importData(data, x, y) {
return new Promise(resolve => {
let warnings = [];
// if the x or y is undefined, set it to leftTopPadding instead
// (cannot use x || leftTopPadding because of 0)
x = x !== undefined ? x : this.leftTopPadding;
y = y !== undefined ? y : this.leftTopPadding;
this.simulationEnabled = false;
// list of wires to be added
let newWires = new Map();
// find the leftmost and topmost coordinate of any box, save them to leftTopCorner
let leftTopCorner = {
x: 0,
y: 0
};
for (const boxData of data.boxes) {
if (boxData.transform && boxData.transform.items) {
for (const transformInfo of boxData.transform.items) {
if (transformInfo.name === 'translate') {
if (leftTopCorner) {
leftTopCorner = {
x: Math.min(leftTopCorner.x, transformInfo.args[0]),
y: Math.min(leftTopCorner.y, transformInfo.args[1])
};
} else {
leftTopCorner = {
x: transformInfo.args[0],
y: transformInfo.args[1]
};
}
}
}
}
}
for (let boxData of data.boxes) {
// mapping of dataBox.name of the objects that have category "other"
const otherMap = {
input: () => this.newInput(0, 0, boxData.isOn, false),
output: () => this.newOutput(0, 0, false)
};
// mapping of dataBox.category
const boxMap = {
gate: () => this.newGate(boxData.name, 0, 0, false),
blackbox: () =>
this.newBlackbox(
boxData.inputs,
boxData.outputs,
boxData.table,
boxData.name,
0,
0,
false
),
other: () => {
if (!boxData.name) throw `This network contains a box without a name.`;
if (!otherMap[boxData.name])
throw `This network contains unknown box names. (${boxData.name})`;
return otherMap[boxData.name]();
}
};
const createBox = () => {
if (!boxData.category) throw `This network a box without a category.`;
if (!boxMap[boxData.category])
throw `This network contains unknown box categories. (${boxData.category})`;
return boxMap[boxData.category]();
};
let box;
try {
box = createBox();
} catch (e) {
warnings.push(e);
}
if (box) {
// proccess box transforms (translation and rotation)
let transform = new editorElements.Transform();
let rotationCount = 0;
const transformItemMap = {
translate: args => {
transform.setTranslate(
args[0] -
leftTopCorner.x + // make it the relative distance from the leftmost element
x, // apply the position
args[1] -
leftTopCorner.y + // make it the relative distance from the topmost element
y // apply the position
);
},
rotate: args => {
rotationCount = (args[0] % 360) / 90;
}
};
if (boxData.transform && boxData.transform.items) {
for (const transformItem of boxData.transform.items) {
const { name, args } = transformItem;
if (!name) {
warnings.push(
`This network contains unnamed transform properties.`
);
break;
}
if (!transformItemMap[name]) {
warnings.push(
`This network contains unknown transform properties. (${
transformItem.name
})`
);
break;
}
transformItemMap[name](args);
}
}
transform.toSVGPixels(this);
box.setTransform(transform);
for (let i = 0; i < rotationCount; ++i) {
box.rotate(true);
}
// add all wires to the list of wires to be added
if (boxData.connections) {
for (const connection of boxData.connections) {
// get the artificial wire id
let wireId = connection.wireId;
// pass the values got from json into a variable that will be added into the map
let value = {
index: connection.index,
boxId: box.id
};
// add the value to the map
if (newWires.has(wireId)) {
// if there already is a wire with this id in the map,
// add the value to the end of the array of values
let mapValue = newWires.get(wireId);
mapValue.push(value);
newWires.set(wireId, mapValue);
} else {
// if there is no wire with this id in the map
// add the wire and set the value to be the first element in the array
newWires.set(wireId, [value]);
}
}
}
}
}
// refresh the SVG document (needed for wiring)
this.refresh();
// with all boxes added, we can now connect them with wires
// priority queue for the new wires, priority being (1 / manhattanDistance) between the conenctors, higher is better
let wireQueue = new PriorityQueue();
// get all ids for lal the
for (const wireInfo of newWires.values()) {
let connectorIds = [];
// create an array [connector1Id, connector2Id]
for (const { boxId, index } of wireInfo) {
connectorIds.push(this.getBoxById(boxId).connectors[index].id);
}
// create and array [{x, y}, {x, y}] containing positions for connectors 1 and 2
const connectorsPositions = connectorIds.map(connectorId =>
this.getConnectorPosition(this.getConnectorById(connectorId), true)
);
if (connectorsPositions.length === 2) {
let wire = this.newWire(...connectorIds, false, false);
// get the manhattan distance between these two connectors
const distance = manhattanDistance(...connectorsPositions);
// add connectorids to the priority queue
wireQueue.enqueue(wire, 1 / distance);
} else {
warnings.push(
`Found a wire that does not have two endings. (It had ${
connectorsPositions.length
} instead.)`
);
}
}
if (window.Worker) {
let wirePoints = [];
let wireReferences = [];
// convert the queue to an array (this is needed by the web worker)
while (!wireQueue.isEmpty()) {
const wire = wireQueue.dequeue();
let wireStart = this.getConnectorPosition(wire.connection.from.connector, true);
let wireEnd = this.getConnectorPosition(wire.connection.to.connector, true);
wirePoints.push([
{
x: wireStart.x / this.gridSize,
y: wireStart.y / this.gridSize
},
{
x: wireEnd.x / this.gridSize,
y: wireEnd.y / this.gridSize
}
]);
wireReferences.push(wire);
}
// [routeWorkerFileName] replaced in the build process (defined in gulpfile) depending on devel / prod build
let myWorker = new Worker('js/[routeWorkerFileName]');
let loadingMessage = this.messages.newLoadingMessage(
'looking for the best wiring…'
);
myWorker.onmessage = event => {
const { paths } = event.data;
// iterate wireReferences and paths synchronously
wireReferences.forEach((wire, key) => {
wire.setWirePath(wire.pathToPolyLine(paths[key]));
wire.updateWireState();
});
loadingMessage.hide();
};
const message = {
wires: wirePoints,
nonRoutableNodes: this.getNonRoutableNodes(),
inconvenientNodes: this.getInconvenientNodes()
};
myWorker.postMessage(message);
} else {
// web worker is not supported: use an interval to make the import a bit slower
// by dividing it into chunks, so the browser window is not entirely frozen when the wiring is happening
const wiresToBeRoutedAtOnce = 10;
const delayBetweenIterations = 200;
// add wires in the order from short to long
let wirePlacingInterval = window.setInterval(() => {
if (!wireQueue.isEmpty()) {
for (let i = 0; i < wiresToBeRoutedAtOnce; ++i) {
if (wireQueue.isEmpty()) {
break;
}
const wire = wireQueue.dequeue();
wire.routeWire(true, false);
wire.updateWireState();
}
} else {
console.log('finished');
clearInterval(wirePlacingInterval);
}
}, delayBetweenIterations);
}
// refresh the SVG document
this.refresh();
this.simulationEnabled = true;
resolve(warnings);
});
}
/**
* When user clicks on a connector, remember it until they click on some other connector.
* Than call newWire with the last two connectors ids as arguments.
* Visualize the process by displaying a grey wire between the first conenctor and the mouse pointer.
* @param {string} connectorId id of the connector that the user clicked on
*/
wireCreationHelper(connectorId, mousePosition) {
if (!this.wireCreation) {
this.wireCreation = {
fromId: connectorId
};
this.displayCreatedWire(mousePosition);
} else {
if (this.wireCreation.fromId !== connectorId) {
this.hideCreatedWire();
this.newWire(this.wireCreation.fromId, connectorId);
this.wireCreation = undefined;
}
}
}
/**
* helper for wireCreationHelper that displays a grey wire between the first connector and the specified mousePosition
* @param {Object} mousePosition object with x and y coordinates in SVG pixels
*/
displayCreatedWire(mousePosition) {
this.wireCreation.tempWire = new editorElements.HelperWire(
this,
this.wireCreation.fromId,
mousePosition
);
$(window).on('mousemove.wireCreation', event => {
event = this.viewbox.transformEvent(event);
mousePosition = {
x: event.pageX,
y: event.pageY
};
this.wireCreation.tempWire.updateMousePosition(mousePosition);
});
this.appendElement(this.wireCreation.tempWire);
this.moveToBackById(this.wireCreation.tempWire.id);
}
/**
* helper for wireCreationHelper that hides the temporary wire when wire creation is done
*/
hideCreatedWire() {
$(window).off('mousemove.wireCreation');
this.wireCreation.tempWire.get().remove();
this.wireCreation.tempWire = undefined;
}
/**
* helper for wireCreationHelper that cancels the wire creation process
*/
cancelWireCreation() {
if (this.wireCreation) {
this.hideCreatedWire();
this.wireCreation = undefined;
}
}
/**
* Run a logic simulation from the startingConnector.
* This refreshes the states of all elements in the network whose inputs are
* directly (or by transition) connected to startingConnector's output
* @param {OutputConnector} startingConnector run simulation from this output connector
* @param {Logic.state} state new state of the startingConnector
*/
startNewSimulation(startingConnector, state) {
if (this.simulationEnabled) {
this.simulation = new Simulation(this);
this.simulation.notifyChange(startingConnector.id, state);
this.simulation.run();
}
}
/**
* Create a new gate on the specified position
* @param {string} name type of the gate (and, or ...)
* @param {number} x horizontal position of the gate in SVG pixels
* @param {number} y vertical position of the gate in SVG pixels
* @param {boolean} [refresh=true] if true, this.refresh() will be called after adding the gate
* @return {editorElements.Gate} instance of Gate that has been newly added
*/
newGate(name, x, y, refresh = true) {
return this.newBox(x, y, new editorElements.Gate(this, name, x, y), refresh);
}
/**
* Create an input box on the specified position
* @param {number} x horizontal position of the gate in SVG pixels
* @param {number} y vertical position of the gate in SVG pixels
* @param {boolean} [isOn=false] state of the input box (default is false (off))
* @param {boolean} [refresh=true] if true, this.refresh() will be called after adding the input box
* @return {editorElements.InputBox} instance of the InputBox that has been newly added
*/
newInput(x, y, isOn = false, refresh = true) {
return this.newBox(x, y, new editorElements.InputBox(this, isOn), refresh);
}
/**
* Create an output box on the specified position
* @param {number} x horizontal position of the gate in SVG pixels
* @param {number} y vertical position of the gate in SVG pixels
* @param {boolean} [refresh=true] if true, this.refresh() will be called after adding the output box
* @return {editorElements.InputBox} instance of the OutputBox that has been newly added
*/
newOutput(x, y, refresh = true) {
return this.newBox(x, y, new editorElements.OutputBox(this), refresh);
}
/**
* Add a new Box to the canvas
* @param {number} x horizontal position of the box in SVG pixels
* @param {number} y vertical position of the box in SVG pixels
* @param {editorElements.Box} object instance of an object derived from the editorElements.Box class
* @param {Boolean} [refresh=true] if true, this.refresh() will be called after adding the box
* @return {editorElements.Box} return the instance of the newly added object
*/
newBox(x, y, object, refresh = true) {
let index = this.boxes.length;
this.boxes[index] = object;
// translate the gate if x and y has been specified
if (x && y) {
let tr = new editorElements.Transform();
tr.setTranslate(x, y);
this.boxes[index].svgObj.addAttr({ transform: tr.get() });
}
this.appendElement(this.boxes[index], refresh);
// if tutorial exists, call tutorial callback
if (this.tutorial) {
this.tutorial.onElementAdded(this.boxes[index].name);
}
return this.boxes[index];
}
/**
* Remove a box from canvas based on the provided ID
* @param {string} boxId id of the box that should be removed
*/
removeBox(boxId) {
let $gate = $('#' + boxId);
// find the gate in svg's list of gates
let gateIndex = -1;
for (let i = 0; i < this.boxes.length; i++) {
if (this.boxes[i].svgObj.id === boxId) {
gateIndex = i;
break;
}
}
if (gateIndex > -1) {
// remove all wires connected to this gate
for (let i = 0; i < this.boxes[gateIndex].connectors.length; i++) {
this.removeWiresByConnectorId(this.boxes[gateIndex].connectors[i].id);
}
// remove the gate
this.boxes.splice(gateIndex, 1);
$gate.remove();
// if tutorial exists, call tutorial callback
if (this.tutorial) {
this.tutorial.onElementRemoved();
}
} else {
console.error('Trying to remove an nonexisting box. Box id:', boxId);
}
}
/**
* Remove all boxes from the canvas
*/
cleanCanvas() {
// cannot simply iterate through the array because removeBox works with it
// create an array of ids
const ids = this.boxes.map(box => box.id);
// remove all boxes by their ids
for (const id of ids) {
this.removeBox(id);
}
}
/**
* Create a new wire connecting the provided connectors
* @param {string} fromId id of the connector that the wire is attached to
* @param {string} toId id of the connector that the wire is attached to
* @param {Boolean} [refresh=true] if refresh is set to true, the SVG document will be reloaded after adding the wire
* @return {editorElements.Wire} instance of editorElements.Wire that has been added to the canvas
*/
newWire(fromId, toId, refresh = true, route = true) {
// wire must connect two distinct connectors
if (fromId === toId) return undefined;
let connectors = [this.getConnectorById(fromId), this.getConnectorById(toId)];
// input connectors can be connected to one wire max
connectors.forEach(conn => {
if (conn.isInputConnector) this.removeWiresByConnectorId(conn.id);
});
let index = this.wires.length;
try {
this.wires[index] = new editorElements.Wire(this, fromId, toId, refresh, route);
} catch (e) {
this.messages.newErrorMessage(e);
return undefined;
}
connectors.forEach(conn => {
conn.addWireId(this.wires[index].svgObj.id);
});
this.appendElement(this.wires[index], refresh);
this.moveToBackById(this.wires[index].svgObj.id);
if (refresh) this.wires[index].updateWireState();
return this.wires[index];
}
/**
* get the coordinates of the specified connector
* @param {Connector} connector instance of {@link Connector}
* @param {Boolean} [snapToGrid=true] if true, the connector position will be snapped to the grid
* @return {Object} point - object containing numeric attributes `x` and `y`
*/
getConnectorPosition(connector, snapToGrid = true) {
// connector.svgObj.id has to be called, else the getCoordinates does not work on the first call in Firefox 55
const dummy = connector.svgObj.id; // eslint-disable-line no-unused-vars
let $connector = connector.svgObj.$el;
let position = $connector.position();
position.left = this.viewbox.transformX(position.left);
position.top = this.viewbox.transformY(position.top);
let width = $connector.attr('width');
let height = $connector.attr('height');
let x = position.left + width / 2;
let y = position.top + height / 2;
if (snapToGrid) {
x = this.snapToGrid(x);
y = this.snapToGrid(y);
}
return { x: x, y: y };
}
/**
* creates a new blackbox
* @param {number} x horizontal position of the blackbox in SVG pixels
* @param {number} y vertical position of the gate in SVG pixels
* @param {number} inputs number of input pins of this blackbox
* @param {number} outputs number of output pins of this blackbox
* @param {Array} table Array of arrays, each inner array contains list of [Logic.state](./module-Logic.html#.state)s,
* that describe the combination of input pin and output pin states in the order from the top to bottom for both input and output connectors.
* If we had an AND array as a blackbox, one of the states could be `[Logic.state.on, Logic.state.off, Logic.state.off]`
* which means that if the first input connector is in the `on` state and the second connector is in the `off` state,
* the state of the output connector will be `off`.
* The array can be described as `[state for input conn 1, state for input conn 2, ..., state for output conn 1, state for output conn 2 ...]`.
* @param {string} name a name that will be displayed on the blackbox
* @param {boolean} [refresh=true] if true, this.refresh() will be called after adding the gate
*
* @return {editorElements.Blackbox} instance of {@link Blackbox} that has been added to the canvas
*/
newBlackbox(inputs, outputs, table, name, x, y, refresh = true) {
const index = this.boxes.length;
this.boxes[index] = new editorElements.Blackbox(
this,
inputs,
outputs,
(...inputStates) => {
for (const line of table) {
const lineInputStates = line.slice(0, inputs);
// if every input state matches the corresponding input state in this line of the truth table
if (inputStates.every((value, index) => value === lineInputStates[index])) {
// return the rest of the line as output
return line.slice(inputs);
}
}
// if nothing matches, set all outputs to undefined
return Array.from(new Array(outputs), () => Logic.state.unknown);
},
name
);
if (x && y) {
let tr = new editorElements.Transform();
tr.setTranslate(x, y);
this.boxes[index].svgObj.addAttr({ transform: tr.get() });
}
this.appendElement(this.boxes[index], refresh);
return this.boxes[index];
}
/**
* Find the correct instance of editorElements.Wire in the app's wires by the provided id
* @param {string} wireId id of the wire
* @return {editorElements.Wire} instance of the wire
*/
getWireById(wireId) {
for (const wire of this.wires) {
if (wire.svgObj.id === wireId) {
return wire;
}
}
return false;
}
/**
* Find all wires that are connected to the specified connector
* @param {string} connectorId id of the connector
* @return {Set} set of ID's of the wires connected to this connector
*/
getWiresByConnectorId(connectorId) {
let connector = this.getConnectorById(connectorId);
return connector.wireIds;
}
/**
* Remove wire that has the provided ID
* @param {string} wireId ID of the wire that should be removed
*/
removeWireById(wireId) {
for (let i = 0; i < this.wires.length; ++i) {
if (this.wires[i].svgObj.id === wireId) {
let { connectors } = this.wires[i];
for (let connector of connectors) {
connector.removeWireIdAndUpdate(wireId);
}
// start simulation from the input connector to
// refresh the network after this wire
let inputConnector = this.wires[i].connection.to.connector;
this.startNewSimulation(inputConnector, inputConnector.state);
this.wires[i].svgObj.$el.remove();
this.wires.splice(i, 1);
break;
}
}
}
/**
* Remove all wires that are connected to the connector provided by its ID
* @param {string} connectorId ID of the connector
*/
removeWiresByConnectorId(connectorId) {
let connector = this.getConnectorById(connectorId);
connector.wireIds.forEach(wireId => {
let wire = this.getWireById(wireId);
let { from, to } = wire.connection;
// get the other connector that is the wire connected to
let otherConnector = connectorId === from.id ? to.connector : from.connector;
// delete the wire record from the other connector
otherConnector.wireIds.delete(wireId);
// remove the wire representation using jQuery
$('#' + wireId).remove();
// if otherConnector is an input connector, set its state to unknown
if (otherConnector.isInputConnector) {
otherConnector.setState(Logic.state.unknown);
this.startNewSimulation(otherConnector, Logic.state.unknown);
}
});
// clear the list of wire Ids
connector.wireIds.clear();
// if connector is an input connector, set its state to unknown
if (connector.isInputConnector) {
connector.setState(Logic.state.unknown);
this.startNewSimulation(connector, Logic.state.unknown);
}
}
/**
* Find the correct instance of editorElements.Box in the app's boxes by the provided id
* @param {string} boxId id of the box
* @return {editorElements.Box} instance of the box
*/
getBoxById(boxId) {
for (let i = 0; i < this.boxes.length; i++) {
if (this.boxes[i].svgObj.id === boxId) {
return this.boxes[i];
}
}
return undefined;
}
/**
* Find the correct instance of editorElements.Box in the app's boxes by ID of a connector that belongs to this box
* @param {string} boxId id of the connector
* @return {editorElements.Box} instance of the box
*/
getBoxByConnectorId(connectorId) {
for (let i = 0; i < this.boxes.length; i++) {
if (this.boxes[i].getConnectorById(connectorId) !== undefined) {
return this.boxes[i];
}
}
return false;
}
/**
* Get instance of a connector based on it's ID (and also on an instance of editorElements.Wire if provided)
*
* The wire variable is used as heuristic: When we know the wire, we have to check only
* two gates instead of all of them
* @param {string} connectorId id of the connector
* @param {editorElements.Wire} [wire] instance of the Wire that is connected to this connector
* @return {editorElements.Connector} instance of the connector
*/
getConnectorById(connectorId, wire = undefined) {
if (wire !== undefined) {
// we know the wire -- we can check only gates at the ends of this wire
const { from, to } = wire.connection;
if (from.id === connectorId) return from.connector;
if (to.id === connectorId) return to.connector;
} else {
// we do not know the wire -- we have to check all gates
for (const box of this.boxes) {
const connector = box.getConnectorById(connectorId);
if (connector) {
return connector;
}
}
}
return undefined;
}
/**
* Get the logical jQuery target based on the factual jQuery target.
*
* If the object, that user interacted with, is not a connector and is in a group,
* return the group jQuery object instead of the original jQuery object.
* @param {target} target jQuery target of the object user interacted with
* @return {target} jQuery target of the object user wanted to interact with
*/
getRealJQueryTarget(target) {
let $target = $(target);
if (!$target.hasClass('connector') && $target.parents('g').length > 0) {
$target = $target.parent();
while ($target.prop('tagName') !== 'G' && $target.prop('tagName') !== 'g') {
$target = $target.parent();
}
}
return $target;
}
// returns the editorElement that user interacted with, the "target" argument is a jQuery element
/**
* Get instance of some object from editorElement based on the jQuery target
* @param {target} target jQuery target that user interacted with
* @return {editorElements.NetworkElement} instance of an object derived from editorElements.NetworkElement that the user interacted with
*/
getRealTarget(target) {
if (target === undefined) {
return undefined;
}
// eventy se museji zpracovat tady, protoze v SVG se eventy nepropaguji
let $target = $(target);
if ($target.hasClass('connector')) {
// this is a connector, don't traverse groups
return this.getConnectorById($target.attr('id'));
} else if ($target.parents('g').length > 0) {
// this element is in a group and it is not a connector
// traversing up the DOM tree until we find the closest group
let $parentGroup = $target.parent();
while ($parentGroup.prop('tagName') !== 'G' && $parentGroup.prop('tagName') !== 'g') {
$parentGroup = $parentGroup.parent();
}
// try to match the jQuery element to the logical element using DOM classes
if ($parentGroup.hasClass('box')) {
// return the corresponding box
return this.getBoxById($parentGroup.attr('id'));
} else if ($parentGroup.hasClass('wire')) {
// return the corresponding wire
return this.getWireById($parentGroup.attr('id'));
} else {
// found a group that contains the target, but this group does not match any known element types
return undefined;
}
} else {
// element does not match any known element types
return undefined;
}
}
/**
* Add an element to the canvas
* @param {editorElements.NetworkElement} element Element that will be added on the canvas
* @param {Boolean} [refresh=true] if true, the SVG document will be reloaded after adding this element
*/
appendElement(element, refresh = true) {
this.appendJQueryObject(element.get(), refresh);
}
/**
* Append a jQuery element to the SVG document (helper for this.appendElement)
* @param {object} object jQuery element that will be added to the SVG document
* @param {Boolean} [refresh=true] if true, the SVG document will be reloaded after adding this element
*/
appendJQueryObject(object, refresh = true) {
this.$svg.append(object);
if (refresh) this.refresh();
}
/**
* Add a new pattern to the definitions element in the SVG document
* @param {svgObj.Pattern} pattern pattern that will be added to the <devs> element in the SVG document
*/
addPattern(pattern) {
this.$defs.append(pattern);
this.refresh();
}
/**
* Reload the SVG document (needed to display a newly appended jQuery object)
*/
refresh() {
this.$svg.html(this.$svg.html());
console.log('SVG document has been reloaded.');
}
/**
* Display the context menu on the specified position
* @param {number} x horizontal position in CSS pixels
* @param {number} y vertical position in CSS pixels
* @param {jQuery.element} $target the item user clicked on (used to display "remove this element"-type items in the menu)
*/
displayContextMenu(x, y, $target) {
this.contextMenu.display(x, y, $target);
// if tutorial exists, call tutorial callback
if (this.tutorial) {
this.tutorial.onContextMenuOpened();
}
}
/**
* hide the context menu
*/
hideContextMenu() {
this.contextMenu.hide();
}
/**
* snap a value to a grid
* @param {number} value value in SVG pixels
* @return {number} the value rounded to the closest number divisible by the grid size
*/
snapToGrid(value) {
return Math.round(value / this.gridSize) * this.gridSize;
}
/**
* convert grid pixels to SVG pixels
* @param {number} value distance in grid pixels
* @return {number} distance in SVG pixels
*/
gridToSVG(value) {
return value * this.gridSize;
}
/**
* convert SVG pixels to grid pixels
* @param {number} value distance in SVG pixels
* @return {number} distance in grud pixels
*/
SVGToGrid(value) {
return value / this.gridSize;
}
/**
* static function for snapping a value to a grid
* @param {number} value value in SVG pixels
* @param {number} gridSize size of the grid in SVG pixels
* @return {number} the value rounded to the closest number divisible by the grid size
*/
static snapToGrid(value, gridSize) {
return Math.round(value / gridSize) * gridSize;
}
/**
* move an element to the front in the canvas
* @param {string} objId id of the element
*/
moveToFrontById(objId) {
this.$svg.append($('#' + objId));
}
/**
* move an element to the back in the canvas
* @param {string} objId id of the element
*/
moveToBackById(objId) {
$('#' + this.background.id).after($('#' + objId));
}
/**
* get set of nodes, that cannot be used for wiring at any circumstances
* @return {Set} set of nodes (objects containing x and y coordinates) that are not suitable for wiring
*/
getNonRoutableNodes() {
let blockedNodes = new Set();
// for each box
for (const box of this.boxes) {
const translate = box.getGridPixelTransform().getTranslate();
// for each item in blockedNodes (set of blocked nodes with coordinates relative
// to the left upper corner of rect; unit used is "one gridSize") convert the coordinates
// to absolute (multiple with gridSize and add position of rect) and add the result to the set
for (const node of box.blockedNodes) {
blockedNodes.add({
x: translate.x + node.x,
y: translate.y + node.y
});
}
}
// FOR DEBUG ONLY: display the non routable nodes
/*
if(this.nodeDisplay) {
for (const rectangleId of this.nodeDisplay) {
$(`#${rectangleId}`).remove();
}
}
this.nodeDisplay = [];
let first = true;
for (const node of blockedNodes) {
const x = this.gridToSVG(node.x);
const y = this.gridToSVG(node.y);
const w = 4;
const p = w / 2;
const nodeRectangle = new Rectangle(x - p, y - p, w, w, first ? "blue" : "red", "none")
this.nodeDisplay.push(nodeRectangle.id);
this.appendElement(nodeRectangle, false);
first = false;
}
this.refresh();
// */
// END FOR DEBUG ONLY
// return the set
return blockedNodes;
}
/**
* get set of nodes, that are inconvenient for wiring, but can be used, just are not preferred
* @return {Set} set of nodes (objects containing x and y coordinates) that are not preferred for wiring
*/
getInconvenientNodes(ignoreWireId) {
let inconvenientNodes = new Set();
// for each wire
for (const wire of this.wires) {
if (ignoreWireId === undefined || ignoreWireId !== wire.id) {
if (wire.inconvenientNodes) {
for (const node of wire.inconvenientNodes) {
inconvenientNodes.add(node);
}
}
}
}
// FOR DEBUG ONLY: display the inconvenient nodes
/*
if(this.inconvenientNodeDisplay) {
for (const rectangleId of this.inconvenientNodeDisplay) {
$(`#${rectangleId}`).remove();
}
}
this.inconvenientNodeDisplay = [];
for (const node of inconvenientNodes) {
const x = this.gridToSVG(node.x);
const y = this.gridToSVG(node.y);
const w = 4;
const p = w / 2;
const nodeRectangle = new Rectangle(x - p, y - p, w, w, "orange", "none")
this.inconvenientNodeDisplay.push(nodeRectangle.id);
this.appendElement(nodeRectangle, false);
}
this.refresh();
// */
// END FOR DEBUG ONLY
// return the set
return inconvenientNodes;
}
}