simulator/src/node.js
/* eslint-disable import/no-cycle */
import { drawCircle, drawLine, arc } from './canvasApi';
import simulationArea from './simulationArea';
import { distance, showError } from './utils';
import {
renderCanvas, scheduleUpdate, wireToBeCheckedSet,
updateSimulationSet, updateCanvasSet, forceResetNodesSet,
canvasMessageData,
} from './engine';
import Wire from './wire';
// import { colors } from './themer/themer';
import { colors } from './themer/themer';
import ContentionMeta from './contention'
/**
* Constructs all the connections of Node node
* @param {Node} node - node to be constructed
* @param {JSON} data - the saved data which is used to load
* @category node
*/
export function constructNodeConnections(node, data) {
for (var i = 0; i < data.connections.length; i++) { if (!node.connections.contains(node.scope.allNodes[data.connections[i]])) node.connect(node.scope.allNodes[data.connections[i]]); }
}
/**
* Fn to replace node by node @ index in global Node List - used when loading
* @param {Node} node - node to be replaced
* @param {number} index - index of node to be replaced
* @category node
*/
export function replace(node, index) {
if (index == -1) {
return node;
}
var { scope } = node;
var { parent } = node;
parent.nodeList.clean(node);
node.delete();
node = scope.allNodes[index];
node.parent = parent;
parent.nodeList.push(node);
node.updateRotation();
node.scope.timeStamp = new Date().getTime();
return node;
}
function rotate(x1, y1, dir) {
if (dir == 'LEFT') { return [-x1, y1]; } if (dir == 'DOWN') { return [y1, x1]; } if (dir == 'UP') { return [y1, -x1]; } return [x1, y1];
}
export function extractBits(num, start, end) {
return (num << (32 - end)) >>> (32 - (end - start + 1));
}
export function bin2dec(binString) {
return parseInt(binString, 2);
}
export function dec2bin(dec, bitWidth = undefined) {
// only for positive nos
var bin = (dec).toString(2);
if (bitWidth == undefined) return bin;
return '0'.repeat(bitWidth - bin.length) + bin;
}
/**
* find Index of a node
* @param {Node} x - Node to be dound
* @category node
*/
export function findNode(x) {
return x.scope.allNodes.indexOf(x);
}
/**
* function makes a node according to data providede
* @param {JSON} data - the data used to load a Project
* @param {Scope} scope - scope to which node has to be loaded
* @category node
*/
export function loadNode(data, scope) {
var n = new Node(data.x, data.y, data.type, scope.root, data.bitWidth, data.label);
}
/**
* get Node in index x in scope and set parent
* @param {Node} x - the desired node
* @param {Scope} scope - the scope
* @param {CircuitElement} parent - The parent of node
* @category node
*/
function extractNode(x, scope, parent) {
var n = scope.allNodes[x];
n.parent = parent;
return n;
}
// output node=1
// input node=0
// intermediate node =2
window.NODE_INPUT = 0;
window.NODE_OUTPUT = 1;
window.NODE_INTERMEDIATE = 2;
/**
* used to give id to a node.
* @type {number}
* @category node
*/
var uniqueIdCounter = 10;
/**
* This class is responsible for all the Nodes.Nodes are connected using Wires
* Nodes are of 3 types;
* NODE_INPUT = 0;
* NODE_OUTPUT = 1;
* NODE_INTERMEDIATE = 2;
* Input and output nodes belong to some CircuitElement(it's parent)
* @param {number} x - x coord of Node
* @param {number} y - y coord of Node
* @param {number} type - type of node
* @param {CircuitElement} parent - parent element
* @param {?number} bitWidth - the bits of node in input and output nodes
* @param {string=} label - label for a node
* @category node
*/
export default class Node {
constructor(x, y, type, parent, bitWidth = undefined, label = '') {
// Should never raise, but just in case
if (isNaN(x) || isNaN(y)) {
this.delete();
showError('Fatal error occurred');
return;
}
forceResetNodesSet(true);
this.objectType = 'Node';
this.subcircuitOverride = false;
this.id = `node${uniqueIdCounter}`;
uniqueIdCounter++;
this.parent = parent;
if (type != NODE_INTERMEDIATE && this.parent.nodeList !== undefined) { this.parent.nodeList.push(this); }
if (bitWidth == undefined) {
this.bitWidth = parent.bitWidth;
} else {
this.bitWidth = bitWidth;
}
this.label = label;
this.prevx = undefined;
this.prevy = undefined;
this.leftx = x;
this.lefty = y;
this.x = x;
this.y = y;
this.type = type;
this.connections = new Array();
this.value = undefined;
this.radius = 5;
this.clicked = false;
this.hover = false;
this.wasClicked = false;
this.scope = this.parent.scope;
this.scope.timeStamp = new Date().getTime();
/**
* @type {string}
* value of this.prev is
* 'a' : whenever a node is not being dragged this.prev is 'a'
* 'x' : when node is being dragged horizontally
* 'y' : when node is being dragged vertically
*/
this.prev = 'a';
this.count = 0;
this.highlighted = false;
// This fn is called during rotations and setup
this.refresh();
if (this.type == NODE_INTERMEDIATE) { this.parent.scope.nodes.push(this); }
this.parent.scope.allNodes.push(this);
this.queueProperties = {
inQueue: false,
time: undefined,
index: undefined,
};
}
/**
* @param {string} - new label
* Function to set label
*/
setLabel(label) {
this.label = label; // || "";
}
/**
* function to convert a node to intermediate node
*/
converToIntermediate() {
this.type = NODE_INTERMEDIATE;
this.x = this.absX();
this.y = this.absY();
this.parent = this.scope.root;
this.scope.nodes.push(this);
}
/**
* Helper fuction to move a node. Sets up some variable which help in changing node.
*/
startDragging() {
this.oldx = this.x;
this.oldy = this.y;
}
/**
* Helper fuction to move a node.
*/
drag() {
this.x = this.oldx + simulationArea.mouseX - simulationArea.mouseDownX;
this.y = this.oldy + simulationArea.mouseY - simulationArea.mouseDownY;
}
/**
* Function for saving a node
*/
saveObject() {
if (this.type == NODE_INTERMEDIATE) {
this.leftx = this.x;
this.lefty = this.y;
}
var data = {
x: this.leftx,
y: this.lefty,
type: this.type,
bitWidth: this.bitWidth,
label: this.label,
connections: [],
};
for (var i = 0; i < this.connections.length; i++) {
data.connections.push(findNode(this.connections[i]));
}
return data;
}
/**
* helper function to help rotating parent
*/
updateRotation() {
var x; var
y;
[x, y] = rotate(this.leftx, this.lefty, this.parent.direction);
this.x = x;
this.y = y;
}
/**
* Refreshes a node after roation of parent
*/
refresh() {
this.updateRotation();
for (var i = 0; i < this.connections.length; i++) {
this.connections[i].connections.clean(this);
}
this.scope.timeStamp = new Date().getTime();
this.connections = [];
}
/**
* gives absolute x position of the node
*/
absX() {
return this.x + this.parent.x;
}
/**
* gives absolute y position of the node
*/
absY() {
return this.y + this.parent.y;
}
/**
* update the scope of a node
*/
updateScope(scope) {
this.scope = scope;
if (this.type == NODE_INTERMEDIATE) this.parent = scope.root;
}
/**
* return true if node is connected or not connected but false if undefined.
*/
isResolvable() {
return this.value != undefined;
}
/**
* function used to reset the nodes
*/
reset() {
this.value = undefined;
this.highlighted = false;
}
/**
* function to connect two nodes.
*/
connect(n) {
if (n == this) return;
if (n.connections.contains(this)) return;
var w = new Wire(this, n, this.parent.scope);
this.connections.push(n);
n.connections.push(this);
this.scope.timeStamp = new Date().getTime();
updateCanvasSet(true);
updateSimulationSet(true);
scheduleUpdate();
}
/**
* connects but doesnt draw the wire between nodes
*/
connectWireLess(n) {
if (n == this) return;
if (n.connections.contains(this)) return;
this.connections.push(n);
n.connections.push(this);
this.scope.timeStamp = new Date().getTime();
// updateCanvasSet(true);
updateSimulationSet(true);
scheduleUpdate();
}
/**
* disconnecting two nodes connected wirelessly
*/
disconnectWireLess(n) {
this.connections.clean(n);
n.connections.clean(this);
this.scope.timeStamp = new Date().getTime();
}
/**
* function to resolve a node
*/
resolve() {
if (this.type == NODE_OUTPUT) {
// Since output node forces its value on its neighbours, remove its contentions.
// An existing contention will now trickle to the other output node that was causing
// the contention.
simulationArea.contentionPending.removeAllContentionsForNode(this);
}
// Remove Propogation of values (TriState)
if (this.value == undefined) {
for (var i = 0; i < this.connections.length; i++) {
if (this.connections[i].value !== undefined) {
this.connections[i].value = undefined;
simulationArea.simulationQueue.add(this.connections[i]);
}
}
if (this.type == NODE_INPUT) {
if (this.parent.objectType == 'Splitter') {
this.parent.removePropagation();
} else
if (this.parent.isResolvable()) { simulationArea.simulationQueue.add(this.parent); } else { this.parent.removePropagation(); }
}
if (this.type == NODE_OUTPUT && !this.subcircuitOverride) {
if (this.parent.isResolvable() && !this.parent.queueProperties.inQueue) {
if (this.parent.objectType == 'TriState' || this.parent.objectType == 'ControlledInverter') {
if (this.parent.state.value) { simulationArea.simulationQueue.add(this.parent); }
} else {
simulationArea.simulationQueue.add(this.parent);
}
}
}
return;
}
// For input nodes, resolve its parents if they are resolvable at this point.
if (this.type == NODE_INPUT) {
if (this.parent.isResolvable()) { simulationArea.simulationQueue.add(this.parent); }
}
for (var i = 0; i < this.connections.length; i++) {
const node = this.connections[i];
switch (node.type) {
// TODO: For an output node, a downstream value (value given by elements other than the parent)
// should be overwritten in contention check and should not cause contention.
case NODE_OUTPUT:
if (node.value != this.value || node.bitWidth != this.bitWidth) {
// Check contentions
if (node.value != undefined && node.parent.objectType != 'SubCircuit'
&& !(node.subcircuitOverride && node.scope != this.scope)) {
// Tristate has always been a pain in the ass.
if ((node.parent.objectType == 'TriState' || node.parent.objectType == 'ControlledInverter') && node.value != undefined) {
if (node.parent.state.value) {
simulationArea.contentionPending.add(node, this);
break;
}
}
else {
simulationArea.contentionPending.add(node, this);
break;
}
}
} else {
// Output node was given an agreeing value, so remove any contention
// entry between these two nodes if it exists.
simulationArea.contentionPending.remove(node, this);
}
// Fallthrough. NODE_OUTPUT propagates like a contention checked NODE_INPUT
case NODE_INPUT:
// Check bitwidths
if (this.bitWidth != node.bitWidth) {
this.highlighted = true;
node.highlighted = true;
showError(`BitWidth Error: ${this.bitWidth} and ${node.bitWidth}`);
break;
}
// Fallthrough. NODE_INPUT propagates like a bitwidth checked NODE_INTERMEDIATE
case NODE_INTERMEDIATE:
if (node.value != this.value || node.bitWidth != this.bitWidth) {
// Propagate
node.bitWidth = this.bitWidth;
node.value = this.value;
simulationArea.simulationQueue.add(node);
}
default:
break;
}
}
}
/**
* this function checks if hover over the node
*/
checkHover() {
if (!simulationArea.mouseDown) {
if (simulationArea.hover == this) {
this.hover = this.isHover();
if (!this.hover) {
simulationArea.hover = undefined;
this.showHover = false;
}
} else if (!simulationArea.hover) {
this.hover = this.isHover();
if (this.hover) {
simulationArea.hover = this;
} else {
this.showHover = false;
}
} else {
this.hover = false;
this.showHover = false;
}
}
}
/**
* this function draw a node
*/
draw() {
const ctx = simulationArea.context;
//
const color = colors["color_wire_draw"];
if (this.clicked) {
if (this.prev == 'x') {
drawLine(ctx, this.absX(), this.absY(), simulationArea.mouseX, this.absY(), color, 3);
drawLine(ctx, simulationArea.mouseX, this.absY(), simulationArea.mouseX, simulationArea.mouseY, color, 3);
} else if (this.prev == 'y') {
drawLine(ctx, this.absX(), this.absY(), this.absX(), simulationArea.mouseY, color, 3);
drawLine(ctx, this.absX(), simulationArea.mouseY, simulationArea.mouseX, simulationArea.mouseY, color, 3);
} else if (Math.abs(this.x + this.parent.x - simulationArea.mouseX) > Math.abs(this.y + this.parent.y - simulationArea.mouseY)) {
drawLine(ctx, this.absX(), this.absY(), simulationArea.mouseX, this.absY(), color, 3);
} else {
drawLine(ctx, this.absX(), this.absY(), this.absX(), simulationArea.mouseY, color, 3);
}
}
var colorNode = colors['stroke'];
const colorNodeConnect = colors['color_wire_con']
const colorNodePow = colors['color_wire_pow']
const colorNodeLose = colors['color_wire_lose']
const colorNodeSelected = colors['node'];
if (this.bitWidth == 1) colorNode = [colorNodeConnect, colorNodePow][this.value];
if (this.value == undefined) colorNode = colorNodeLose;
if (this.type == NODE_INTERMEDIATE) this.checkHover();
if (this.type == NODE_INTERMEDIATE) { drawCircle(ctx, this.absX(), this.absY(), 3, colorNode); } else { drawCircle(ctx, this.absX(), this.absY(), 3, colorNodeSelected); }
if (this.highlighted || simulationArea.lastSelected == this || (this.isHover() && !simulationArea.selected && !simulationArea.shiftDown) || simulationArea.multipleObjectSelections.contains(this)) {
ctx.strokeStyle = colorNodeSelected;
ctx.beginPath();
ctx.lineWidth = 3;
arc(ctx, this.x, this.y, 8, 0, Math.PI * 2, this.parent.x, this.parent.y, 'RIGHT');
ctx.closePath();
ctx.stroke();
}
if (this.hover || (simulationArea.lastSelected == this)) {
if (this.showHover || simulationArea.lastSelected == this) {
canvasMessageData.x = this.absX();
canvasMessageData.y = this.absY() - 15;
if (this.type == NODE_INTERMEDIATE) {
var v = 'X';
if (this.value !== undefined) { v = this.value.toString(16); }
if (this.label.length) {
canvasMessageData.string = `${this.label} : ${v}`;
} else {
canvasMessageData.string = v;
}
} else if (this.label.length) {
canvasMessageData.string = this.label;
}
} else {
setTimeout(() => {
if (simulationArea.hover) simulationArea.hover.showHover = true;
updateCanvasSet(true);
renderCanvas(globalScope);
}, 400);
}
}
}
/**
* checks if a node has been deleted
*/
checkDeleted() {
if (this.deleted) this.delete();
if (this.connections.length == 0 && this.type == NODE_INTERMEDIATE) this.delete();
}
/**
* used to update nodes if there is a event like click or hover on the node.
* many booleans are used to check if certain properties are to be updated.
*/
update() {
if (embed) return;
if (this == simulationArea.hover) simulationArea.hover = undefined;
this.hover = this.isHover();
if (!simulationArea.mouseDown) {
if (this.absX() != this.prevx || this.absY() != this.prevy) { // Connect to any node
this.prevx = this.absX();
this.prevy = this.absY();
this.nodeConnect();
}
}
if (this.hover) {
simulationArea.hover = this;
}
if (simulationArea.mouseDown && ((this.hover && !simulationArea.selected) || simulationArea.lastSelected == this)) {
simulationArea.selected = true;
simulationArea.lastSelected = this;
this.clicked = true;
} else {
this.clicked = false;
}
if (!this.wasClicked && this.clicked) {
this.wasClicked = true;
this.prev = 'a';
if (this.type == NODE_INTERMEDIATE) {
if (!simulationArea.shiftDown && simulationArea.multipleObjectSelections.contains(this)) {
for (var i = 0; i < simulationArea.multipleObjectSelections.length; i++) {
simulationArea.multipleObjectSelections[i].startDragging();
}
}
if (simulationArea.shiftDown) {
simulationArea.lastSelected = undefined;
if (simulationArea.multipleObjectSelections.contains(this)) {
simulationArea.multipleObjectSelections.clean(this);
} else {
simulationArea.multipleObjectSelections.push(this);
}
} else {
simulationArea.lastSelected = this;
}
}
} else if (this.wasClicked && this.clicked) {
if (!simulationArea.shiftDown && simulationArea.multipleObjectSelections.contains(this)) {
for (var i = 0; i < simulationArea.multipleObjectSelections.length; i++) {
simulationArea.multipleObjectSelections[i].drag();
}
}
if (this.type == NODE_INTERMEDIATE) {
if (this.connections.length == 1 && this.connections[0].absX() == simulationArea.mouseX && this.absX() == simulationArea.mouseX) {
this.y = simulationArea.mouseY - this.parent.y;
this.prev = 'a';
return;
} if (this.connections.length == 1 && this.connections[0].absY() == simulationArea.mouseY && this.absY() == simulationArea.mouseY) {
this.x = simulationArea.mouseX - this.parent.x;
this.prev = 'a';
return;
}
if (this.connections.length == 1 && this.connections[0].absX() == this.absX() && this.connections[0].absY() == this.absY()) {
this.connections[0].clicked = true;
this.connections[0].wasClicked = true;
simulationArea.lastSelected = this.connections[0];
this.delete();
return;
}
}
if (this.prev == 'a' && distance(simulationArea.mouseX, simulationArea.mouseY, this.absX(), this.absY()) >= 10) {
if (Math.abs(this.x + this.parent.x - simulationArea.mouseX) > Math.abs(this.y + this.parent.y - simulationArea.mouseY)) {
this.prev = 'x';
} else {
this.prev = 'y';
}
} else if (this.prev == 'x' && this.absY() == simulationArea.mouseY) {
this.prev = 'a';
} else if (this.prev == 'y' && this.absX() == simulationArea.mouseX) {
this.prev = 'a';
}
} else if (this.wasClicked && !this.clicked) {
this.wasClicked = false;
if (simulationArea.mouseX == this.absX() && simulationArea.mouseY == this.absY()) {
return; // no new node situation
}
var x1; var y1; var x2; var y2; var
flag = 0;
var n1; var
n2;
// (x,y) present node, (x1,y1) node 1 , (x2,y2) node 2
// n1 - node 1, n2 - node 2
// node 1 may or may not be there
// flag = 0 - node 2 only
// flag = 1 - node 1 and node 2
x2 = simulationArea.mouseX;
y2 = simulationArea.mouseY;
const x = this.absX();
const y = this.absY();
if (x != x2 && y != y2) {
// Rare Exception Cases
if (this.prev == 'a' && distance(simulationArea.mouseX, simulationArea.mouseY, this.absX(), this.absY()) >= 10) {
if (Math.abs(this.x + this.parent.x - simulationArea.mouseX) > Math.abs(this.y + this.parent.y - simulationArea.mouseY)) {
this.prev = 'x';
} else {
this.prev = 'y';
}
}
flag = 1;
if (this.prev == 'x') {
x1 = x2;
y1 = y;
} else if (this.prev == 'y') {
y1 = y2;
x1 = x;
}
}
if (flag == 1) {
for (var i = 0; i < this.parent.scope.allNodes.length; i++) {
if (x1 == this.parent.scope.allNodes[i].absX() && y1 == this.parent.scope.allNodes[i].absY()) {
n1 = this.parent.scope.allNodes[i];
break;
}
}
if (n1 == undefined) {
n1 = new Node(x1, y1, 2, this.scope.root);
for (var i = 0; i < this.parent.scope.wires.length; i++) {
if (this.parent.scope.wires[i].checkConvergence(n1)) {
this.parent.scope.wires[i].converge(n1);
break;
}
}
}
this.connect(n1);
}
for (var i = 0; i < this.parent.scope.allNodes.length; i++) {
if (x2 == this.parent.scope.allNodes[i].absX() && y2 == this.parent.scope.allNodes[i].absY()) {
n2 = this.parent.scope.allNodes[i];
break;
}
}
if (n2 == undefined) {
n2 = new Node(x2, y2, 2, this.scope.root);
for (var i = 0; i < this.parent.scope.wires.length; i++) {
if (this.parent.scope.wires[i].checkConvergence(n2)) {
this.parent.scope.wires[i].converge(n2);
break;
}
}
}
if (flag == 0) this.connect(n2);
else n1.connect(n2);
if (simulationArea.lastSelected == this) simulationArea.lastSelected = n2;
}
if (this.type == NODE_INTERMEDIATE && simulationArea.mouseDown == false) {
if (this.connections.length == 2) {
if ((this.connections[0].absX() == this.connections[1].absX()) || (this.connections[0].absY() == this.connections[1].absY())) {
this.connections[0].connect(this.connections[1]);
this.delete();
}
} else if (this.connections.length == 0) this.delete();
}
}
/**
* function delete a node
*/
delete() {
updateSimulationSet(true);
this.deleted = true;
this.parent.scope.allNodes.clean(this);
this.parent.scope.nodes.clean(this);
this.parent.scope.root.nodeList.clean(this); // Hope this works! - Can cause bugs
if (simulationArea.lastSelected == this) simulationArea.lastSelected = undefined;
for (var i = 0; i < this.connections.length; i++) {
this.connections[i].connections.clean(this);
this.connections[i].checkDeleted();
}
this.scope.timeStamp = new Date().getTime();
wireToBeCheckedSet(1);
forceResetNodesSet(true);
scheduleUpdate();
}
isClicked() {
return this.absX() == simulationArea.mouseX && this.absY() == simulationArea.mouseY;
}
isHover() {
return this.absX() == simulationArea.mouseX && this.absY() == simulationArea.mouseY;
}
/**
* if input nodde: it resolves the parent
* else: it adds all the nodes onto the stack
* and they are processed to generate verilog
*/
nodeConnect() {
var x = this.absX();
var y = this.absY();
var n;
for (var i = 0; i < this.parent.scope.allNodes.length; i++) {
if (this != this.parent.scope.allNodes[i] && x == this.parent.scope.allNodes[i].absX() && y == this.parent.scope.allNodes[i].absY()) {
n = this.parent.scope.allNodes[i];
if (this.type == NODE_INTERMEDIATE) {
for (var j = 0; j < this.connections.length; j++) {
n.connect(this.connections[j]);
}
this.delete();
} else {
this.connect(n);
}
break;
}
}
if (n == undefined) {
for (var i = 0; i < this.parent.scope.wires.length; i++) {
if (this.parent.scope.wires[i].checkConvergence(this)) {
var n = this;
if (this.type != NODE_INTERMEDIATE) {
n = new Node(this.absX(), this.absY(), 2, this.scope.root);
this.connect(n);
}
this.parent.scope.wires[i].converge(n);
break;
}
}
}
}
processVerilog() {
if (this.type == NODE_INPUT) {
if (this.parent.isVerilogResolvable()) { this.scope.stack.push(this.parent); }
}
for (var i = 0; i < this.connections.length; i++) {
if (this.connections[i].verilogLabel != this.verilogLabel) {
this.connections[i].verilogLabel = this.verilogLabel;
this.scope.stack.push(this.connections[i]);
}
}
}
}
/**
* delay in simulation of the node.
* @category node
*/
Node.prototype.propagationDelay = 0;
/**
* backward comaptibilty?
* @category node
*/
Node.prototype.cleanDelete = Node.prototype.delete;
Node.prototype.processVerilog = function() {
if (this.type == NODE_INPUT) {
this.scope.stack.push(this.parent);
}
for (var i = 0; i < this.connections.length; i++) {
if (this.connections[i].verilogLabel != this.verilogLabel) {
this.connections[i].verilogLabel = this.verilogLabel;
this.scope.stack.push(this.connections[i]);
}
}
}