Home Reference Source

src/js/molecules/molecule.js

import Atom from '../prototypes/atom.js'
import Connector from '../prototypes/connector.js'
import GlobalVariables from '../globalvariables.js'
//import saveAs from '../lib/FileSaver.js'
import { extractBomTags } from '../BOM.js'

/**
 * This class creates the Molecule atom.
 */
export default class Molecule extends Atom{
    
    /**
     * The constructor function.
     * @param {object} values An array of values passed in which will be assigned to the class as this.x
     */ 
    constructor(values){
        
        super(values)
        
        /** 
         * A list of all of the atoms within this Molecule which should be drawn on the screen as objects.
         * @type {array}
         */
        this.nodesOnTheScreen = []
        /** 
         * An array of the molecules inputs. Is this not inherited from atom?
         * @type {array}
         */
        this.inputs = []
        /** 
         * This atom's type
         * @type {string}
         */
        this.name = 'Molecule'
        /** 
         * A description of this atom
         * @type {string}
         */
        this.description = "Molecules provide an organizational structure to contain atoms. Double click on a molecule to enter it. Use the up arrow in the upper right hand corner of the screen to go up one level."
        /** 
         * This atom's type
         * @type {string}
         */
        this.atomType = 'Molecule'
        /** 
         * The color for the middle dot in the molecule
         * @type {string}
         */
        this.centerColor = '#949294'
        /** 
         * A flag to indicate if this molecule is the top level molecule.
         * @type {boolean}
         */
        this.topLevel = false
        /** 
         * A flag to indicate if this molecule should simplify it's output.
         * @type {boolean}
         */
        this.simplify = false
        /** 
         * The threshold for simplification. This is the maximum fraction of vertices which will be removed.
         * @type {float}
         */
        this.threshold = 0.80
        /** 
         * A flag to indicate if this molecule is currently processing.
         * @type {boolean}
         */
        this.processing = false //Should be pulled from atom. Docs made me put this here
        /** 
         * A list of things which should be displayed on the the top level sideBar when in toplevel mode.
         * @type {array}
         */
        this.runModeSidebarAdditions = []
        
        /** 
         * The total number of atoms contained in this molecule
         * @type {integer}
         */
        this.totalAtomCount = 1
        /** 
         * The total number of atoms contained in this molecule which are waiting to process
         * @type {integer}
         */
        this.toProcess = 0
        /**
         * A flag to indicate if this molecule was waiting propagation. If it is it will take place
         *the next time we go up one level.
         * @type {number}
         */
        this.awaitingPropagationFlag = false
        /**
         * A list of available units.
         * @type {object}
         */
        this.units = {"MM": 1, "Inches": 25.4}
        /**
         * The index of the currently selected unit.
         * @type {array}
         */
        this.unitsIndex = 0
        
        this.setValues(values)
        
        //Add the molecule's output
        this.placeAtom({
            parentMolecule: this, 
            x: GlobalVariables.pixelsToWidth(GlobalVariables.canvas.width - 20),
            y: GlobalVariables.pixelsToHeight(GlobalVariables.canvas.height/2),
            parent: this,
            name: 'Output',
            atomType: 'Output',
            uniqueID: GlobalVariables.generateUniqueID()
        }, false)
    }
    
    /**
     * Gives this molecule inputs with the same names as all of it's parent's inputs
     */ 
    copyInputsFromParent(){
        if(this.parent){
            this.parent.nodesOnTheScreen.forEach(node => {
                if(node.atomType == "Input"){
                    this.placeAtom({
                        parentMolecule: this,
                        y: node.y,
                        parent: this,
                        name: node.name,
                        atomType: 'Input',
                        uniqueID: GlobalVariables.generateUniqueID()
                    }, null, GlobalVariables.availableTypes, true)
                }
            })
        } 
    }
    
    /**
     * Add the center dot to the molecule
     */ 
    draw(){
        const percentLoaded = 1-this.toProcess/this.totalAtomCount
        if(this.toProcess > 1){
            this.processing = true
        }
        else{
            this.processing = false
        }
        
        super.draw() //Super call to draw the rest
        
        //draw the circle in the middle
        GlobalVariables.c.beginPath()
        GlobalVariables.c.fillStyle = this.centerColor
        GlobalVariables.c.moveTo(GlobalVariables.widthToPixels(this.x), GlobalVariables.heightToPixels(this.y))
        GlobalVariables.c.arc(GlobalVariables.widthToPixels(this.x), GlobalVariables.heightToPixels(this.y), GlobalVariables.widthToPixels(this.radius)/2, 0, percentLoaded*Math.PI * 2, false)
        GlobalVariables.c.closePath()
        GlobalVariables.c.fill()
    }
    
    /**
     * Set the atom's response to a mouse click up. If the atom is moving this makes it stop moving.
     * @param {number} x - The X coordinate of the click
     * @param {number} y - The Y coordinate of the click
     */ 
    clickUp(x,y){
        super.clickUp(x,y)
        GlobalVariables.currentMolecule.nodesOnTheScreen.forEach(atom =>{
            atom.isMoving = false
        })
    }

    /**
     * Handle double clicks by replacing the molecule currently on the screen with this one, esentially diving into it.
     * @param {number} x - The x coordinate of the click
     * @param {number} y - The y coordinate of the click
     */ 
    doubleClick(x,y){
        //returns true if something was done with the click
        x = GlobalVariables.pixelsToWidth(x)
        y = GlobalVariables.pixelsToHeight(y)
        
        var clickProcessed = false
        
        var distFromClick = GlobalVariables.distBetweenPoints(x, this.x, y, this.y)
        
        if (distFromClick < this.radius*2){
            GlobalVariables.currentMolecule = this //set this to be the currently displayed molecule
            GlobalVariables.currentMolecule.backgroundClick()
            /**
            * Deselects Atom
            * @type {boolean}
            */
            this.selected = false
            clickProcessed = true
        }
        
        return clickProcessed 
    }
    
    /**
     * Handle a background click (a click which doesn't land on one of the contained molecules) by deselected everything and displaying a 3D rendering of this molecules output.
     */ 
    backgroundClick(){
        /**
         * Flag that the atom is now selected.
         */
        if(this.selected == false){
            this.selected = true
            this.updateSidebar()
            this.sendToRender()   //This is might need to be removed because it was happening too often during loading
        }
    }

    /**
     * Pushes serialized atoms into array if selected
     */
    copy(){
        this.nodesOnTheScreen.forEach(atom => {
            if(atom.selected){
                GlobalVariables.atomsSelected.push(atom.serialize({x: .03, y: .03}))
            }
        })
    }
    
    /**
     * Unselect this molecule
     */ 
    deselect(){
        this.selected = false

    }
    
    /**
     * Grab values from the inputs and push them out to the input atoms.
     */ 
    updateValue(targetName){
        
        //Molecules are fully transparent so we don't wait for all of the inputs to begin processing the things inside
        
        //Tell the correct input to update
        this.nodesOnTheScreen.forEach(atom => { //Scan all the input atoms
            if(atom.atomType == 'Input' && atom.name == targetName){  //When there is a match
                atom.updateValue() //Tell that input to update it's value
            }
        })
    }
    
    /**
     * Reads the path of this molecule's output atom
     */ 
    readOutputAtomPath(){
        var returnPath = ""
        this.nodesOnTheScreen.forEach(atom => {
            //If we have found this molecule's output atom use it to update the path here
            if(atom.atomType == "Output"){
                returnPath = atom.path
            }
        })
        return returnPath
    }
    
    /**
     * Sets the atom to wait on coming information. Basically a pass through, but used for molecules
     */ 
    waitOnComingInformation(inputName){
        this.nodesOnTheScreen.forEach( atom => {
            if(atom.name == inputName){
                atom.waitOnComingInformation()
            }
        })
    }
    
    /**
     * Called when this molecules value changes
     */ 
    propagate(){
        //Set the output nodes with type 'geometry' to be the generated code
        if(this.simplify){
            try{
                this.processing = true
                const values = {op: "simplify", readPath: this.readOutputAtomPath(), writePath: this.path, threshold: this.threshold}
                const {answer} = window.ask(values)
                answer.then( () => {
                    this.processing = false
                    this.pushPropagation()
                })
            }catch(err){this.setAlert(err)}
        }
        else{
            try{
                this.processing = true
                const values = {op: "copy", readPath: this.readOutputAtomPath(), writePath: this.path}
                const {answer} = window.ask(values)
                answer.then( () => {
                    this.processing = false
                    this.pushPropagation()
                })
            }catch(err){this.setAlert(err)}
        }
    }
    
    /**
     * Called when this molecules value changes
     */ 
    pushPropagation(){
        //Only propagate up if 
        if(this != GlobalVariables.currentMolecule){
            if(typeof this.readOutputAtomPath() == "number"){
                this.output.setValue(this.readOutputAtomPath())
            }
            else{
                this.output.setValue(this.path)
            }
            this.output.ready = true
        }
        else{
            this.awaitingPropagationFlag = true
        }
        
        //If this molecule is selected, send the updated value to the renderer
        if(this.selected){
            this.sendToRender()
        }
    }
    
    /**
     * Walks through each of the atoms in this molecule and begins Propagation from them if they have no inputs to wait for
     */ 
    beginPropagation(force = false){
        
        //Tell every atom inside this molecule to begin Propagation
        this.nodesOnTheScreen.forEach(node => {
            node.beginPropagation(force)
        })
    }
    
    /**
     * Walks through each of the atoms in this molecule and takes a census of how many there are and how many are currently waiting to be processed.
     */
    census(){
        this.totalAtomCount = 0
        this.toProcess = 0
        
        this.nodesOnTheScreen.forEach(atom => {
            const newInformation = atom.census()
            this.totalAtomCount = this.totalAtomCount + newInformation[0]
            this.toProcess      = this.toProcess + newInformation[1]
        })
        
        if(this.topLevel && this.selected){
            this.updateSidebar()
        }
        
        return [this.totalAtomCount, this.toProcess]
    }
    
    /**
     * Called when the simplify check box is checked or unchecked.
     */
    setSimplifyFlag(anEvent){
        this.simplify = anEvent.target.checked
        this.propagate()
        this.updateSidebar()
    }
    
    changeUnits(newUnitsIndex){
        this.unitsIndex = newUnitsIndex
        this.updateSidebar()
    }

    /**
     * Updates the side bar to display options like 'go to parent' and 'load a different project'. What is displayed depends on if this atom is the top level, and if we are using run mode.
     */ 
    updateSidebar(){
        //Update the side bar to make it possible to change the molecule name
        
        var valueList = super.initializeSideBar()
        
        if(!this.topLevel){
            this.createEditableValueListItem(valueList,this,'name','Name', false)
        }
        else if(this.topLevel){
            //If we are the top level molecule

            this.createSegmentSlider(valueList)

            const dropdown = document.createElement('div')
            valueList.appendChild(dropdown)
            this.createDropDown(dropdown, this, Object.keys(this.units), this.unitsIndex, "Units", (index)=>{this.changeUnits(index)})
        }
        
        //Display the percent loaded while loading
        const percentLoaded = 100*(1-this.toProcess/this.totalAtomCount)
        if(this.toProcess > 0 && this.topLevel){
            this.createNonEditableValueListItem(valueList,{percentLoaded:percentLoaded.toFixed(0) + "%"},"percentLoaded",'Loading')
        }
        
        //removes 3d view menu on background click
        let viewerBar = document.querySelector('#viewer_bar')
        if(viewerBar && viewerBar.firstChild){
            while (viewerBar.firstChild) {
                viewerBar.removeChild(viewerBar.firstChild)
                viewerBar.setAttribute('style', 'background-color:none;')
            }
        }
        
        
        
        //Add options to set all of the inputs
        this.inputs.forEach(child => {
            if(child.type == 'input' && child.valueType != 'geometry'){
                this.createEditableValueListItem(valueList,child,'value', child.name, true)
            }
        })
        
        //Add the check box to simplify
        this.createCheckbox(valueList,"Simplify output",this.simplify,(anEvent)=>{this.setSimplifyFlag(anEvent)})
        
        if(this.simplify){
            this.createEditableValueListItem(valueList,this,'threshold', 'Threshold', true)
        }

        //Only bother to generate the bom if we are not currently processing data
        if(this.toProcess == 0){
            this.displaySimpleBOM(valueList)
        }
        
        this.displaySidebarReadme(valueList)
        
        return valueList
        
    }

    /**
     * Creates segment length slider and passes value to Global Variables
     */ 
    createSegmentSlider(valueList){

        const unitsScalor = this.units[Object.keys(this.units)[this.unitsIndex]]

        //Creates value slider
        var rangeElement = document.createElement('input')
        //Div which contains the entire element
        var div = document.createElement('div')
        div.setAttribute('class', 'slider-container')
        valueList.appendChild(div)
        var rangeLabel = document.createElement('label')
        rangeLabel.textContent = "Display quality/Length of Segments"
        div.appendChild(rangeLabel)
        rangeLabel.appendChild(rangeElement)
        rangeElement.setAttribute('type', 'range')
        rangeElement.setAttribute('min', '' + .001/unitsScalor)
        rangeElement.setAttribute('max', '' + 1/unitsScalor)
        rangeElement.setAttribute('step', '' + .05/unitsScalor)
        rangeElement.setAttribute('class', 'slider')
        rangeElement.setAttribute('value', GlobalVariables.circleSegmentSize)
            
        var rangeValueLabel = document.createElement('ul')
        rangeValueLabel.innerHTML= '<li>Export</li><li>Draft</li> '
        rangeValueLabel.setAttribute('class', 'range-labels')
        rangeLabel.appendChild(rangeValueLabel)

        var rangeValue = document.createElement('p')
        rangeValue.textContent = parseFloat(rangeElement.value).toFixed(5).toString()
        rangeLabel.appendChild(rangeValue)


        //on slider change send value to global variables
        rangeElement.oninput = function() {
            rangeValue.textContent = this.value
            GlobalVariables.circleSegmentSize = this.value
            
        }
        
        rangeElement.addEventListener('touchend', () => {
            GlobalVariables.topLevelMolecule.refreshCircles()
        })
        
        rangeElement.addEventListener('mouseup', () => {
            GlobalVariables.topLevelMolecule.refreshCircles()
        })
    }
    
    /**
     * Used to trigger all of the circle atoms within a molecule and all of the molecules within it to update their value. Used when the number of segments changes.
     */ 
    refreshCircles(){
        this.nodesOnTheScreen.forEach(atom => {
            if(atom.atomType == "Circle"){
                atom.updateValue()
            }
            else if(atom.atomType == "Molecule" || atom.atomType == "GitHubMolecule"){
                atom.refreshCircles()
            }
        })
    }
    
    /**
     * Creates a simple BOM list which cannot be edited. The generated element is added to the passed list.
     * @param {object} list - The HTML object to append the created element to.
     */ 
    displaySimpleBOM(list){
        try{
            
            const placementFunction = (bomList) => {
                
                if(bomList.length > 0){
                
                    list.appendChild(document.createElement('br'))
                    list.appendChild(document.createElement('br'))
                    
                    var div = document.createElement('h3')
                    div.setAttribute('style','text-align:center;')
                    list.appendChild(div)
                    var valueText = document.createTextNode('Bill Of Materials')
                    div.appendChild(valueText)
                    
                    var x = document.createElement('HR')
                    list.appendChild(x)
                    
                    bomList.forEach(bomEntry => {
                        this.createNonEditableValueListItem(list,bomEntry,'numberNeeded', bomEntry.BOMitemName, false)
                    })
                }
            }
            
            extractBomTags(this.path, placementFunction)
            
        }catch(err){
            this.setAlert("Unable to read BOM")
        }
    }
    
    /**
     * Creates markdown version of the readme content for this atom in the sidebar
     * @param {object} list - The HTML object to append the created element to.
     */ 
    displaySidebarReadme(list){
        
        
        var readmeContent = ""
        
        this.requestReadme().forEach(item => {
            
            readmeContent = readmeContent + item + "\n\n\n"
        })
        
        if(readmeContent.length > 0){    //If there is anything to say
        
            list.appendChild(document.createElement('br'))
            list.appendChild(document.createElement('br'))

            var div = document.createElement('h3')
            div.setAttribute('style','float:right;')
           
            list.appendChild(div)
            var valueText = document.createTextNode(`- ReadMe`)
            div.appendChild(valueText)
            
            var x = document.createElement('HR')
            x.setAttribute('style','width:100%;')
            list.appendChild(x)
            
            this.createMarkdownListItem(list,readmeContent)
        }
    }
    
    /**
     * Replace the currently displayed molecule with the parent of this molecule...moves the user up one level.
     */
    goToParentMolecule(){
        //Go to the parent molecule if there is one
        if(!this.topLevel){
            this.nodesOnTheScreen.forEach(atom => {
                atom.selected = false
            })
            
            GlobalVariables.currentMolecule = this.parent //set parent this to be the currently displayed molecule
            GlobalVariables.currentMolecule.backgroundClick()
            
            //Push any changes up to the next level if there are any changes waiting in the output
            if(this.awaitingPropagationFlag == true){
                this.propagate()
                this.awaitingPropagationFlag = false
            }
        }
    }
    
    /**
     * Check to see if any of this molecules children have contributions to make to the README file. Children closer to the top left will be applied first. TODO: No contribution should be made if it's just a title.
     */
    requestReadme(){
        var generatedReadme = super.requestReadme()
        
        var sortableAtomsList = this.nodesOnTheScreen
        sortableAtomsList.sort(function(a, b){return GlobalVariables.distBetweenPoints(a.x, 0, a.y, 0)-GlobalVariables.distBetweenPoints(b.x, 0, b.y, 0)})
        
        sortableAtomsList.forEach(atom => {
            generatedReadme = generatedReadme.concat(atom.requestReadme())
        })
        
        //Check to see if any of the children added anything if not, remove the bit we added
        if(generatedReadme[generatedReadme.length - 1] == '## ' + this.name){
            generatedReadme.pop()
        }
        
        return generatedReadme
    }
    
    /**
     * Generates and returns a object representation of this molecule and all of its children.
     */
    serialize(offset = {x: 0, y: 0}){
        
        var allAtoms = [] //An array of all the atoms contained in this molecule
        var allConnectors = [] //An array of all the connectors contained in this molecule
        
        this.nodesOnTheScreen.forEach(atom => {
            //Store a representation of the atom
            allAtoms.push(atom.serialize())
            //Store a representation of the atom's connectors
            if(atom.output){
                atom.output.connectors.forEach(connector => {
                    allConnectors.push(connector.serialize())
                })
            }
        })
        
        var thisAsObject = super.serialize(offset)    //Do the atom serialization to create an object, then add all the bits of this one to it
        thisAsObject.topLevel = this.topLevel
        thisAsObject.allAtoms = allAtoms
        thisAsObject.allConnectors = allConnectors
        thisAsObject.fileTypeVersion = 1
        thisAsObject.simplify= this.simplify
        thisAsObject.unitsIndex = this.unitsIndex
        
        return thisAsObject
    }
    
    /**
     * Load the children of this from a JSON representation
     * @param {object} json - A json representation of the molecule
     * @param {object} values - An array of values to apply to this molecule before de-serializing it's contents. Used by githubmolecules to set top level correctly
     */
    deserialize(json, values = {}, forceBeginPropagation = false){
        //Find the target molecule in the list
        let promiseArray = []
        
        this.setValues(json) //Grab the values of everything from the passed object
        this.setValues(values) //Over write those values with the passed ones where needed
        
        if(json.allAtoms){
            json.allAtoms.forEach(atom => { //Place the atoms
                const promise = this.placeAtom(atom, false)
                promiseArray.push(promise)
                this.setValues([]) //Call set values again with an empty list to trigger loading of IO values from memory
            })
        }
        
        return Promise.all(promiseArray).then( ()=> { //Once all the atoms are placed we can finish
            
            this.setValues([])//Call set values again with an empty list to trigger loading of IO values from memory
            
            //Place the connectors
            if(json.allConnectors){
                json.allConnectors.forEach(connector => {
                    this.placeConnector(connector)
                })
            }
            
            if(this.topLevel){
                
                GlobalVariables.totalAtomCount = GlobalVariables.numberOfAtomsToLoad
                
                this.census()
                this.loadTree()  //Walks back up the tree from this molecule loading input values from any connected atoms
                
                const splits = this.path.split('/')
                const values = {op: "getPathsList", prefacePath: splits[0]+'/'+splits[1]}
                const {answer} = window.ask(values)
                answer.then( answer => {
                
                    GlobalVariables.availablePaths = answer
                    this.beginPropagation(forceBeginPropagation)
                
                })
                this.backgroundClick()
            }
        })
    }
    
    /**
     * Delete this molecule and everything in it.
     */ 
    deleteNode(backgroundClickAfter = true, deletePath = true, silent = false){
        
        //make a copy of the nodes on the screen array since we will be modifying it
        const copyOfNodesOnTheScreen = [...this.nodesOnTheScreen]
        
        copyOfNodesOnTheScreen.forEach(atom => {
            atom.deleteNode(backgroundClickAfter, deletePath, silent)
        })
        
        super.deleteNode(backgroundClickAfter, deletePath, silent)
    }
    
    /**
     * Triggers the loadTree process from this molecules output
     */ 
    loadTree(){
        //We want to walk the tree from this's output and anything which has nothing coming out of it. Basically all the graph end points.
        
        this.nodesOnTheScreen.forEach(atom => {
            //If we have found this molecule's output atom use it to update the path here
            if(atom.atomType == "Output"){
                atom.loadTree()
            }
            //If we have found an atom with nothing connected to it
            if(atom.output){
                if(atom.output.connectors.length == 0){
                    atom.loadTree()
                }
            }
        })
        
        this.output.value = this.path
        return this.path
    }
    
    /**
     * Places a new atom inside the molecule
     * @param {object} newAtomObj - An object defining the new atom to be placed
     * @param {array} moleculeList - Only passed if we are placing an instance of Molecule.
     * @param {object} typesList - A dictionary of all of the available types with references to their constructors
     * @param {boolean} unlock - A flag to indicate if this atom should spawn in the unlocked state.
     */
    async placeAtom(newAtomObj, unlock){
        
        GlobalVariables.numberOfAtomsToLoad = GlobalVariables.numberOfAtomsToLoad + 1 //Indicate that one more atom needs to be loaded
        
        try{
            var promise = null
            
            for(var key in GlobalVariables.availableTypes) {
                if (GlobalVariables.availableTypes[key].atomType == newAtomObj.atomType){
                    newAtomObj.parent = this
                    var atom = new GlobalVariables.availableTypes[key].creator(newAtomObj)
                    
                    //reassign the name of the Inputs to preserve linking
                    if(atom.atomType == 'Input' && typeof newAtomObj.name !== 'undefined'){
                        atom.name = newAtomObj.name
                        atom.draw() //The poling happens in draw :roll_eyes:
                    }

                    //If this is a molecule, de-serialize it
                    if(atom.atomType == 'Molecule'){
                        promise = atom.deserialize(newAtomObj)
                    }
                    
                    //If this is a github molecule load it from the web
                    if(atom.atomType == 'GitHubMolecule'){
                        promise = atom.loadProjectByID(atom.projectID)
                        if(unlock){
                            promise.then( ()=> {
                                atom.beginPropagation()
                            })
                        }
                    }
                    
                    //If this is an output, check to make sure there are no existing outputs, and if there are delete the existing one because there can only be one
                    if(atom.atomType == 'Output'){
                        //Check for existing outputs
                        this.nodesOnTheScreen.forEach(atom => {
                            if(atom.atomType == 'Output'){
                                atom.deleteOutputAtom(false) //Remove them
                            }
                        })
                    }
                    
                    //Add the atom to the list to display
                    this.nodesOnTheScreen.push(atom)
                    
                    if(unlock){
                        
                        //Make this molecule spawn with all of it's parent's inputs
                        if(atom.atomType == 'Molecule'){ //Not GitHubMolecule
                            atom.copyInputsFromParent()
                            
                            //Make begin propagation from an atom when it is placed. This is used when copy and pasting molecules.
                            if(promise != null){
                                promise.then( ()=> {
                                    atom.beginPropagation()
                                })
                            }
                            else{
                                atom.beginPropagation()
                            }
                            
                        }
                        
                        //Fake a click on the newly placed atom
                        const downEvt = new MouseEvent('mousedown', {
                            clientX: atom.x,
                            clientY: atom.y
                        })
                        const upEvt = new MouseEvent('mouseup', {
                            clientX: atom.x,
                            clientY: atom.y
                        })
                        
                        atom.updateValue()
                        
                        document.getElementById('flow-canvas').dispatchEvent(downEvt)
                        document.getElementById('flow-canvas').dispatchEvent(upEvt)
                    }
                }
            }
            return promise
        }catch(err){
            console.warn("Unable to place: " + newAtomObj)
            console.warn(err)
            return Promise.resolve()
        }

    }
    
    /**
     * Places a new connector within the molecule
     * @param {object} connectorObj - An object representation of the connector specifying its inputs and outputs.
     */
    placeConnector(connectorObj){
        
        var outputAttachmentPoint = false
        var inputAttachmentPoint = false
        
        this.nodesOnTheScreen.forEach(atom => {             //Check each atom on the screen
            if (atom.uniqueID == connectorObj.ap1ID){           //When we have found the output atom
                outputAttachmentPoint = atom.output
            }
            if (atom.uniqueID == connectorObj.ap2ID){           //When we have found the input atom
                atom.inputs.forEach(input => {                  //Check each of its inputs
                    if(input.name == connectorObj.ap2Name){
                        inputAttachmentPoint = input                //Until we find the one with the right name
                    }
                })
            }
        })
        
        if(outputAttachmentPoint && inputAttachmentPoint){             //If we have found the output and input
            new Connector({
                atomType: 'Connector',
                attachmentPoint1: outputAttachmentPoint,
                attachmentPoint2: inputAttachmentPoint,
            })
        }
        else{
            console.warn("Unable to place connector")
        }
    }
    
    /**
     * Sends the output of this molecule to be displayed in the 3D view.
     */
    sendToRender(){
        super.sendToRender()
        if(this.value != null){
            if(this.topLevel){
                this.basicThreadValueProcessing(this.value, "bounding box")
            }
        }
    }
}