CircuitVerse/CircuitVerse

View on GitHub
simulator/src/subcircuit.js

Summary

Maintainability
F
1 wk
Test Coverage
/* eslint-disable import/no-cycle */
import Scope, { scopeList, switchCircuit } from "./circuit";
import CircuitElement from "./circuitElement";
import simulationArea from "./simulationArea";
import { scheduleBackup, checkIfBackup } from "./data/backupCircuit";
import {
    scheduleUpdate,
    updateSimulationSet,
    updateCanvasSet,
    updateSubcircuitSet,
    forceResetNodesSet
} from "./engine";
import { loadScope } from "./data/load";
import { showError } from "./utils";

import Node, { findNode } from "./node";
import { fillText, correctWidth, rect2} from "./canvasApi";
import { colors } from "./themer/themer";
import { layoutModeGet } from "./layoutMode"
import { verilogModeGet } from "./Verilog2CV"
import { sanitizeLabel } from './verilogHelpers';
/**
 * Function to load a subcicuit
 * @category subcircuit
 */
export function loadSubCircuit(savedData, scope) {
    new SubCircuit(savedData.x, savedData.y, scope, savedData.id, savedData);
}

/**
 * Prompt to create subcircuit, shows list of circuits which dont depend on the current circuit
 * @param {Scope=} scope
 * @category subcircuit
 */
export function createSubCircuitPrompt(scope = globalScope) {
    if(verilogModeGet() || layoutModeGet()) {
        showError("Subcircuit cannot be inserted in this mode");
        return;
    }
    $("#insertSubcircuitDialog").empty();
    let flag = true;
    for (id in scopeList) {
        if (!scopeList[id].checkDependency(scope.id) && scopeList[id].isVisible()) {
            flag = false; 
            $("#insertSubcircuitDialog").append(
                `<label class="option custom-radio inline"><input type="radio" name="subCircuitId" value="${id}" />${scopeList[id].name}<span></span></label>`
            );
        }
    }  
    if(flag) {    
        $("#insertSubcircuitDialog").append(
            [$("#insertSubcircuitcontent").dialog(),
             $("#insertSubcircuitcontent").dialog("close")]
        );
    } 
    $("#insertSubcircuitDialog").dialog({
        resizable:false,
        maxHeight: 800,
        width: 450,
        maxWidth: 800,
        minWidth: 250,
        buttons: !flag
            ? [
                  {
                      text: "Insert SubCircuit",
                      click() {
                          if (!$("input[name=subCircuitId]:checked").val())
                              return;
                          simulationArea.lastSelected = new SubCircuit(
                              undefined,
                              undefined,
                              globalScope,
                              $("input[name=subCircuitId]:checked").val()
                          );
                          $(this).dialog("close");
                      },
                  },
              ]
            : [],
    });
}

/**
 * @class
 * @extends CircuitElement
 * @param {number} x - x coord of subcircuit
 * @param {number} y - y coord of subcircuit
 * @param {Scope=} scope - the circuit in which subcircuit has been added
 * @param {string} id - the id of the subcircuit scope
 * @param {JSON} savedData - the saved data
 * @category subcircuit
 */
export default class SubCircuit extends CircuitElement {
    constructor(
        x,
        y,
        scope = globalScope,
        id = undefined,
        savedData = undefined
    ) {
        super(x, y, scope, "RIGHT", 1); // super call
        this.objectType = "SubCircuit";
        this.scope.SubCircuit.push(this);
        this.id = id || prompt("Enter Id: ");
        this.directionFixed = true;
        this.fixedBitWidth = true;
        this.savedData = savedData;
        this.inputNodes = [];
        this.outputNodes = [];
        this.localScope = new Scope();
        this.preventCircuitSwitch = false; // prevents from switching circuit if double clicking element
        this.rectangleObject = false;

        var subcircuitScope = scopeList[this.id]; // Scope of the subcircuit
        // Error handing
        if (subcircuitScope == undefined) {
            // if no such scope for subcircuit exists
            showError(
                `SubCircuit : ${
                    (savedData && savedData.title) || this.id
                } Not found`
            );
        } else if (!checkIfBackup(subcircuitScope)) {
            // if there is no input/output nodes there will be no backup
            showError(
                `SubCircuit : ${
                    (savedData && savedData.title) || subcircuitScope.name
                } is an empty circuit`
            );
        } else if (subcircuitScope.checkDependency(scope.id)) {
            // check for cyclic dependency
            showError("Cyclic Circuit Error");
        }
        // Error handling, cleanup
        if (
            subcircuitScope == undefined ||
            subcircuitScope.checkDependency(scope.id)
        ) {
            if (savedData) {
                for (var i = 0; i < savedData.inputNodes.length; i++) {
                    scope.allNodes[savedData.inputNodes[i]].deleted = true;
                }
                for (var i = 0; i < savedData.outputNodes.length; i++) {
                    scope.allNodes[savedData.outputNodes[i]].deleted = true;
                }
            }
            return;
        }

        if (this.savedData != undefined) {
            updateSubcircuitSet(true);
            scheduleUpdate();
            this.version = this.savedData.version || "1.0";

            this.id = this.savedData.id;
            this.label = this.savedData.label || "";
            this.labelDirection = this.savedData.labelDirection || "RIGHT";
            for (var i = 0; i < this.savedData.inputNodes.length; i++) {
                this.inputNodes.push(
                    this.scope.allNodes[this.savedData.inputNodes[i]]
                );
                this.inputNodes[i].parent = this;
                this.inputNodes[i].layout_id =
                    subcircuitScope.Input[i]?.layoutProperties.id;
            }
            for (var i = 0; i < this.savedData.outputNodes.length; i++) {
                this.outputNodes.push(
                    this.scope.allNodes[this.savedData.outputNodes[i]]
                );
                this.outputNodes[i].parent = this;
                this.outputNodes[i].layout_id =
                    subcircuitScope.Output[i]?.layoutProperties.id;
            }
            if (this.version == "1.0") {
                // For backward compatibility
                this.version = "2.0";
                this.x -= subcircuitScope.layout.width / 2;
                this.y -= subcircuitScope.layout.height / 2;
                for (var i = 0; i < this.inputNodes.length; i++) {
                    this.inputNodes[i].x =
                        subcircuitScope.Input[i].layoutProperties.x;
                    this.inputNodes[i].y =
                        subcircuitScope.Input[i].layoutProperties.y;
                    this.inputNodes[i].leftx = this.inputNodes[i].x;
                    this.inputNodes[i].lefty = this.inputNodes[i].y;
                }
                for (var i = 0; i < this.outputNodes.length; i++) {
                    this.outputNodes[i].x =
                        subcircuitScope.Output[i].layoutProperties.x;
                    this.outputNodes[i].y =
                        subcircuitScope.Output[i].layoutProperties.y;
                    this.outputNodes[i].leftx = this.outputNodes[i].x;
                    this.outputNodes[i].lefty = this.outputNodes[i].y;
                }
            }

            if (this.version == "2.0") {
                this.leftDimensionX = 0;
                this.upDimensionY = 0;
                this.rightDimensionX = subcircuitScope.layout.width;
                this.downDimensionY = subcircuitScope.layout.height;
            }

            this.nodeList.extend(this.inputNodes);
            this.nodeList.extend(this.outputNodes);
        } else {
            this.version = "2.0";
        }

        this.data = JSON.parse(scheduleBackup(subcircuitScope));
        this.buildCircuit(); // load the localScope for the subcircuit
        this.makeConnections(); // which will be wireless
    }

    /**
     * actually make all connection but are invisible so
     * it seems like the simulation is happening in other
     * Scope but it actually is not.
     */
    makeConnections() {
        for (let i = 0; i < this.inputNodes.length; i++) {
            this.localScope.Input[i]?.output1.connectWireLess(
                this.inputNodes[i]
            );
            this.localScope.Input[i].output1.subcircuitOverride = true;
        }

        for (let i = 0; i < this.outputNodes.length; i++) {
            this.localScope.Output[i]?.inp1.connectWireLess(this.outputNodes[i]);
            this.outputNodes[i].subcircuitOverride = true;
        }
    }

    /**
     * Function to remove wireless connections
     */
    removeConnections() {
        for (let i = 0; i < this.inputNodes.length; i++) {
            this.localScope.Input[i]?.output1.disconnectWireLess(
                this.inputNodes[i]
            );
        }

        for (let i = 0; i < this.outputNodes.length; i++) {
            this.localScope.Output[i]?.inp1.disconnectWireLess(
                this.outputNodes[i]
            );
        }
    }

    /**
     * loads the subcircuit and draws all the nodes
     */
    buildCircuit() {
        var subcircuitScope = scopeList[this.id];
        loadScope(this.localScope, this.data);
        this.localScope.name = this.data.name;
        this.lastUpdated = this.localScope.timeStamp;
        updateSimulationSet(true);
        updateCanvasSet(true);

        if (this.savedData == undefined) {
            this.leftDimensionX = 0;
            this.upDimensionY = 0;
            this.rightDimensionX = subcircuitScope.layout.width;
            this.downDimensionY = subcircuitScope.layout.height;
            for (var i = 0; i < subcircuitScope.Output.length; i++) {
                var a = new Node(
                    subcircuitScope.Output[i].layoutProperties.x,
                    subcircuitScope.Output[i].layoutProperties.y,
                    1,
                    this,
                    subcircuitScope.Output[i].bitWidth
                );
                a.layout_id = subcircuitScope.Output[i].layoutProperties.id;
                this.outputNodes.push(a);
            }
            for (var i = 0; i < subcircuitScope.Input.length; i++) {
                var a = new Node(
                    subcircuitScope.Input[i].layoutProperties.x,
                    subcircuitScope.Input[i].layoutProperties.y,
                    0,
                    this,
                    subcircuitScope.Input[i].bitWidth
                );
                a.layout_id = subcircuitScope.Input[i].layoutProperties.id;
                this.inputNodes.push(a);
            }
        }
    }

    // Needs to be deprecated, removed
    reBuild() {
        // new SubCircuit(x = this.x, y = this.y, scope = this.scope, this.id);
        // this.scope.backups = []; // Because all previous states are invalid now
        // this.delete();
        // showMessage('Subcircuit: ' + subcircuitScope.name + ' has been reloaded.');
    }

    /**
     * If the circuit referenced by localscope is changed, then the localscope
     * needs to be updated. This function does that.
     */
    reBuildCircuit() {
        this.data = JSON.parse(scheduleBackup(scopeList[this.id]));
        this.localScope = new Scope(data.name);
        loadScope(this.localScope, this.data);
        this.lastUpdated = this.localScope.timeStamp;
        this.scope.timeStamp = this.localScope.timeStamp;
    }

    reset() {
        this.removeConnections();

        var subcircuitScope = scopeList[this.id];

        for (var i = 0; i < subcircuitScope.SubCircuit.length; i++) {
            subcircuitScope.SubCircuit[i].reset();
        }

        // No Inputs or Outputs
        let emptyCircuit = subcircuitScope.Input.length == 0 && subcircuitScope.Output.length == 0;
        // No LayoutElements
        for(let element of circuitElementList){
            if(subcircuitScope[element].length > 0 && subcircuitScope[element][0].canShowInSubcircuit){
                emptyCircuit = false;
                break;
            }
        }

        if (emptyCircuit) {
            showError(
                `SubCircuit : ${subcircuitScope.name} is an empty circuit`
            );
        }

        subcircuitScope.layout.height = subcircuitScope.layout.height;
        subcircuitScope.layout.width = subcircuitScope.layout.width;
        this.leftDimensionX = 0;
        this.upDimensionY = 0;
        this.rightDimensionX = subcircuitScope.layout.width;
        this.downDimensionY = subcircuitScope.layout.height;

        var temp_map_inp = {};
        for (var i = 0; i < subcircuitScope.Input.length; i++) {
            temp_map_inp[subcircuitScope.Input[i].layoutProperties.id] = [
                subcircuitScope.Input[i],
                undefined,
            ];
        }
        for (var i = 0; i < this.inputNodes.length; i++) {
            if (temp_map_inp.hasOwnProperty(this.inputNodes[i].layout_id)) {
                temp_map_inp[this.inputNodes[i].layout_id][1] = this.inputNodes[
                    i
                ];
            } else {
                this.scope.backups = [];
                this.inputNodes[i].delete();
                this.nodeList.clean(this.inputNodes[i]);
            }
        }

        for (id in temp_map_inp) {
            if (temp_map_inp[id][1]) {
                if (
                    temp_map_inp[id][0].layoutProperties.x ==
                        temp_map_inp[id][1].x &&
                    temp_map_inp[id][0].layoutProperties.y ==
                        temp_map_inp[id][1].y
                ) {
                    temp_map_inp[id][1].bitWidth = temp_map_inp[id][0].bitWidth;
                } else {
                    this.scope.backups = [];
                    temp_map_inp[id][1].delete();
                    this.nodeList.clean(temp_map_inp[id][1]);
                    temp_map_inp[id][1] = new Node(
                        temp_map_inp[id][0].layoutProperties.x,
                        temp_map_inp[id][0].layoutProperties.y,
                        0,
                        this,
                        temp_map_inp[id][0].bitWidth
                    );
                    temp_map_inp[id][1].layout_id = id;
                }
            }
        }

        this.inputNodes = [];
        for (var i = 0; i < subcircuitScope.Input.length; i++) {
            var input =
                temp_map_inp[subcircuitScope.Input[i].layoutProperties.id][0];
            if (temp_map_inp[input.layoutProperties.id][1]) {
                this.inputNodes.push(
                    temp_map_inp[input.layoutProperties.id][1]
                );
            } else {
                var a = new Node(
                    input.layoutProperties.x,
                    input.layoutProperties.y,
                    0,
                    this,
                    input.bitWidth
                );
                a.layout_id = input.layoutProperties.id;
                this.inputNodes.push(a);
            }
        }

        var temp_map_out = {};
        for (var i = 0; i < subcircuitScope.Output.length; i++) {
            temp_map_out[subcircuitScope.Output[i].layoutProperties.id] = [
                subcircuitScope.Output[i],
                undefined,
            ];
        }
        for (var i = 0; i < this.outputNodes.length; i++) {
            if (temp_map_out.hasOwnProperty(this.outputNodes[i].layout_id)) {
                temp_map_out[
                    this.outputNodes[i].layout_id
                ][1] = this.outputNodes[i];
            } else {
                this.outputNodes[i].delete();
                this.nodeList.clean(this.outputNodes[i]);
            }
        }

        for (id in temp_map_out) {
            if (temp_map_out[id][1]) {
                if (
                    temp_map_out[id][0].layoutProperties.x ==
                        temp_map_out[id][1].x &&
                    temp_map_out[id][0].layoutProperties.y ==
                        temp_map_out[id][1].y
                ) {
                    temp_map_out[id][1].bitWidth = temp_map_out[id][0].bitWidth;
                } else {
                    temp_map_out[id][1].delete();
                    this.nodeList.clean(temp_map_out[id][1]);
                    temp_map_out[id][1] = new Node(
                        temp_map_out[id][0].layoutProperties.x,
                        temp_map_out[id][0].layoutProperties.y,
                        1,
                        this,
                        temp_map_out[id][0].bitWidth
                    );
                    temp_map_out[id][1].layout_id = id;
                }
            }
        }

        this.outputNodes = [];
        for (var i = 0; i < subcircuitScope.Output.length; i++) {
            var output =
                temp_map_out[subcircuitScope.Output[i].layoutProperties.id][0];
            if (temp_map_out[output.layoutProperties.id][1]) {
                this.outputNodes.push(
                    temp_map_out[output.layoutProperties.id][1]
                );
            } else {
                var a = new Node(
                    output.layoutProperties.x,
                    output.layoutProperties.y,
                    1,
                    this,
                    output.bitWidth
                );
                a.layout_id = output.layoutProperties.id;
                this.outputNodes.push(a);
            }
        }
        // console.log(subcircuitScope.name, subcircuitScope.timeStamp, this.lastUpdated)
        if (subcircuitScope.timeStamp > this.lastUpdated) {
            this.reBuildCircuit();
        }

        this.localScope.reset();
        updateSimulationSet(true);
        forceResetNodesSet(true);

        this.makeConnections();
    }

    /**
     * Procedure after a element is clicked inside a subcircuit
    **/
    click() {
        var elementClicked = this.getElementHover();
        if(elementClicked) {
            this.lastClickedElement = elementClicked;
            elementClicked.wasClicked = true;
        }
    }

    getElementHover() {

        var rX = this.layoutProperties.rightDimensionX;
        var lX = this.layoutProperties.leftDimensionX;
        var uY = this.layoutProperties.upDimensionY;
        var dY = this.layoutProperties.downDimensionY;

        for(let el of circuitElementList){
            if(this.localScope[el].length === 0) continue;
            if(!this.localScope[el][0].canShowInSubcircuit) continue;
            for(let i = 0; i < this.localScope[el].length; i++){
                var obj = this.localScope[el][i];
                if (obj.subcircuitMetadata.showInSubcircuit && obj.isSubcircuitHover(this.x, this.y)) {
                    return obj;
                }
            }
        }
    }

    /**
      * Sets the elements' wasClicked property in the subcircuit to false
    **/
    releaseClick(){
        if(this.lastClickedElement !== undefined) {
            this.lastClickedElement.wasClicked = false;
            this.lastClickedElement = undefined
        }
    }

    /**
     * adds all local scope inputs to the global scope simulation queue
     */
    addInputs() {
        for (let i = 0; i < subCircuitInputList.length; i++) {
            for (
                let j = 0;
                j < this.localScope[subCircuitInputList[i]].length;
                j++
            ) {
                simulationArea.simulationQueue.add(
                    this.localScope[subCircuitInputList[i]][j],
                    0
                );
            }
        }
        for (let j = 0; j < this.localScope.SubCircuit.length; j++) {
            this.localScope.SubCircuit[j].addInputs();
        }
    }

    /**
     * Procedure if any element is double clicked inside a subcircuit
    **/
    dblclick() {
        if(this.elementHover) return;
        switchCircuit(this.id);
    }

    /**
     * Returns a javascript object of subcircuit data.
     * Does not include data of subcircuit elements apart from Input and Output (that is a part of element.subcircuitMetadata)
    **/
    saveObject() {
        var data = {
            x: this.x,
            y: this.y,
            id: this.id,
            label: this.label,
            labelDirection: this.labelDirection,
            inputNodes: this.inputNodes.map(findNode),
            outputNodes: this.outputNodes.map(findNode),
            version: this.version,
        };
        return data;
    }

    /**
     * By design, subcircuit element's input and output nodes are wirelessly
     * connected to the localscope (clone of the scope of the subcircuit's
     * circuit). So it is almost like the actual circuit is copied in the
     * location of the subcircuit element. Therefore no resolve needed.
     */
    isResolvable() {
        return false;
    }

    /**
     * If element not resolvable (always in subcircuits), removePropagation
     * is called on it.
     */
    removePropagation() {
        // Leave this to the scope of the subcircuit. Do nothing.
    }

    verilogName(){
        return sanitizeLabel(scopeList[this.id].name);
    }
    /**
     * determines where to show label
     */
    determine_label(x, y) {
        if (x == 0) return ["left", 5, 5];
        if (x == scopeList[this.id].layout.width) return ["right", -5, 5];
        if (y == 0) return ["center", 0, 13];
        return ["center", 0, -6];
    }

    checkHover() {
        super.checkHover();
        if(this.elementHover) {
            this.elementHover.hover = false;
            this.elementHover = undefined;
            simulationArea.hover = undefined;
        }
        var elementHover = this.getElementHover();
        if(elementHover) {
            elementHover.hover = true;
            this.elementHover = elementHover;
            this.hover = false;
            simulationArea.hover = elementHover;
        }
    }

    /**
     * Draws the subcircuit (and contained elements) on the screen when the subcircuit is included
       in another circuit
    **/
    customDraw() {
        var subcircuitScope = scopeList[this.id];

        var ctx = simulationArea.context;

        ctx.lineWidth = globalScope.scale * 3;
        ctx.strokeStyle = colors["stroke"]; // ("rgba(0,0,0,1)");
        ctx.fillStyle = colors["fill"];
        var xx = this.x;
        var yy = this.y;

        ctx.strokeStyle = colors['stroke'];
        ctx.fillStyle = colors['fill'];
        ctx.lineWidth = correctWidth(3);
        ctx.beginPath();
        rect2(ctx, -this.leftDimensionX, -this.upDimensionY, this.leftDimensionX + this.rightDimensionX, this.upDimensionY + this.downDimensionY, this.x, this.y, [this.direction, 'RIGHT'][+this.directionFixed]);
        if(!this.elementHover) {
            if ((this.hover && !simulationArea.shiftDown) || simulationArea.lastSelected === this || simulationArea.multipleObjectSelections.contains(this))
                ctx.fillStyle = colors["hover_select"];
        }
        ctx.fill();
        ctx.stroke();

        ctx.beginPath();

        ctx.textAlign = "center";
        ctx.fillStyle = "black";
        if (this.version == "1.0") {
            fillText(
                ctx,
                subcircuitScope.name,
                xx,
                yy - subcircuitScope.layout.height / 2 + 13,
                11
            );
        } else if (this.version == "2.0") {
            if (subcircuitScope.layout.titleEnabled) {
                fillText(
                    ctx,
                    subcircuitScope.name,
                    subcircuitScope.layout.title_x + xx,
                    yy + subcircuitScope.layout.title_y,
                    11
                );
            }
        } else {
            console.log("Unknown Version: ", this.version);
        }

        for (var i = 0; i < subcircuitScope.Input.length; i++) {
            if (!subcircuitScope.Input[i].label) continue;
            var info = this.determine_label(
                this.inputNodes[i].x,
                this.inputNodes[i].y
            );
            ctx.textAlign = info[0];
            fillText(
                ctx,
                subcircuitScope.Input[i].label,
                this.inputNodes[i].x + info[1] + xx,
                yy + this.inputNodes[i].y + info[2],
                12
            );
        }

        for (var i = 0; i < subcircuitScope.Output.length; i++) {
            if (!subcircuitScope.Output[i].label) continue;
            var info = this.determine_label(
                this.outputNodes[i].x,
                this.outputNodes[i].y
            );
            ctx.textAlign = info[0];
            fillText(
                ctx,
                subcircuitScope.Output[i].label,
                this.outputNodes[i].x + info[1] + xx,
                yy + this.outputNodes[i].y + info[2],
                12
            );
        }
        ctx.fill();
        for (let i = 0; i < this.outputNodes.length; i++) {
            this.outputNodes[i].draw();
        }
        for (let i = 0; i < this.inputNodes.length; i++) {
            this.inputNodes[i].draw();
        }

         // draw subcircuitElements
        for(let el of circuitElementList){
            if(this.localScope[el].length === 0) continue;
            if(!this.localScope[el][0].canShowInSubcircuit) continue;
            for(let i = 0; i < this.localScope[el].length; i++){
                if (this.localScope[el][i].subcircuitMetadata.showInSubcircuit) {
                    this.localScope[el][i].drawLayoutMode(this.x, this.y);
                }
            }
        }
    }
}
SubCircuit.prototype.centerElement = true; // To center subcircuit when new
SubCircuit.prototype.propagationDelayFixed = true;