Home Reference Source

src/js/prototypes/attachmentpoint.js

import Connector from './connector'
import GlobalVariables from '../globalvariables'

/**
 * This class creates a new attachmentPoint which are the input and output blobs on Atoms
 */
export default class AttachmentPoint {
    /**
     * The constructor function.
     * @param {object} values An array of values passed in which will be assigned to the class as this.x
     */ 
    constructor(values){
        
        /** 
         * This atom's default radius (non hover)
         * @type {number}
         */
        this.defaultRadius = 1/60
        /** 
         * A flag to indicate if this attachment point is currently expanded.
         * @type {boolean}
         */
        this.expandedRadius = false
        /** 
         * This atom's current radius as displayed.
         * @type {number}
         */
        this.radius = 1/60
        /** 
         * When the mouse is hovering where should the AP move in X
         * @type {number}
         */
        this.hoverOffsetX = 0
        /** 
         * When the mouse is hovering where should the AP move in Y
         * @type {number}
         */
        this.hoverOffsetY = 0
        /** 
         * The attachment points X position
         * @type {number}
         */
        this.x = 0
        /** 
         * The attachment point's Y position
         * @type {number}
         */
        this.y = 0
        /** 
         * A unique identifying number for this attachment point
         * @type {number}
         */
        this.uniqueID = 0
        /** 
         * The default offset position in X referenced to the center of the parent atom.
         * @type {number}
         */
        this.defaultOffsetX = 0
        /** 
         * The default offset position in Y referenced to the center of the parent atom.
         * @type {number}
         */
        this.defaultOffsetY = 0
        /** 
         * The current offset position in X referenced to the center of the parent atom.
         * @type {number}
         */
        this.offsetX = 0
        /** 
         * The current offset position in Y referenced to the center of the parent atom.
         * @type {number}
         */
        this.offsetY = 0
        /** 
         * A flag to determine if the hover text is shown next to the attachment point.
         * @type {boolean}
         */
        this.showHoverText = true
        /** 
         * The attachment point type.
         * @type {string}
         */
        this.atomType = 'AttachmentPoint'
        
        /** 
         * The attachment point value type. Options are number, geometry, array.
         * @type {string}
         */
        this.valueType = 'number'
        /** 
         * The attachment point type. Options are input, output.
         * @type {string}
         */
        this.type = 'output'
        
        /** 
         * This is a flag to indicate if the attachment point is of the primary type.
         * Primary type inputs are of the form geometry.translate(input2, input3, input4) for example
         * This value is useful for importing molecules into other formats. I don't know if this is used any more. Maybe it can be deleted.
         * @type {boolean}
         */
        this.primary = false
        
        /** 
         * The attachment point current value.
         * @type {number}
         */
        this.value = 10
        
        /**
         * The default value to be used by the ap when nothing is attached
         * @type {string}
         */
        this.defaultValue = 10
        
        /** 
         * A flag to indicate if the attachment point is currently ready. Used to order initilization when program is loaded.
         * @type {string}
         */
        this.ready = true
        /** 
         * A list of all of the connectors attached to this attachment point
         * @type {object}
         */
        this.connectors = []
        
        this.offsetX = this.defaultOffsetX
        this.offsetY = this.defaultOffsetY
        
        for(var key in values) {
            /**
             * Assign values in values as this.x
             */
            this[key] = values[key]
        }
        this.clickMove(0,0) //trigger a refresh to get all the current values
    }
    
    /**
     * Draws the attachment point on the screen. Called with each frame.
     */ 
    draw() {
       
        let xInPixels = GlobalVariables.widthToPixels(this.x)
        let yInPixels = GlobalVariables.heightToPixels(this.y)
        let radiusInPixels = GlobalVariables.widthToPixels(this.radius)
        let parentRadiusInPixels = GlobalVariables.widthToPixels(this.parentMolecule.radius)
        let parentXInPixels = GlobalVariables.widthToPixels(this.parentMolecule.x)
        let parentYInPixels = GlobalVariables.heightToPixels(this.parentMolecule.y)

        this.defaultRadius = radiusInPixels
        radiusInPixels = parentRadiusInPixels/2.7

        if (this.expandedRadius){
            radiusInPixels = parentRadiusInPixels/2.4
        }
        if(this.parentMolecule.inputs.length < 2 && this.type == 'input'){
            //This prevents single attachment points from expanding out
            /**
             * The x coordinate of the attachment point.
             */
            xInPixels = parentXInPixels - parentRadiusInPixels
            this.x = this.parentMolecule.x - this.parentMolecule.radius //This makes sure it says it is where it actually is
            /**
             * The y coordinate of the attachment point.
             */
            yInPixels = parentYInPixels
        }    
        else if(this.parentMolecule.inputs.length < 2 && this.type == 'output'){
            xInPixels = parentXInPixels + parentRadiusInPixels
            yInPixels = parentYInPixels
        }                 

        var txt = this.name
        var textWidth = GlobalVariables.c.measureText(txt).width
        GlobalVariables.c.font = '10px Work Sans'

        var bubbleColor = '#C300FF'
        var scaleRadiusDown = radiusInPixels*.7
        var halfRadius = radiusInPixels *.5

        
        if (this.showHoverText){
            if(this.type == 'input'){
 
                GlobalVariables.c.globalCompositeOperation='destination-over'
                GlobalVariables.c.beginPath()

                if (this.name === 'geometry'){
                    GlobalVariables.c.fillStyle = this.parentMolecule.selectedColor   
                }
                else{
                    GlobalVariables.c.fillStyle = bubbleColor
                }
            
                //Draws bubble shape
                GlobalVariables.c.rect(xInPixels - textWidth - radiusInPixels - halfRadius, yInPixels - radiusInPixels, textWidth + radiusInPixels + halfRadius , radiusInPixels*2)   
                GlobalVariables.c.arc(xInPixels - textWidth - radiusInPixels - halfRadius, yInPixels, radiusInPixels, 0, Math.PI * 2, false)

                //Bubble text
                GlobalVariables.c.fill()
                GlobalVariables.c.globalCompositeOperation='source-over'
                GlobalVariables.c.beginPath()
                GlobalVariables.c.fillStyle = this.parentMolecule.defaultColor
                GlobalVariables.c.textAlign = 'end'
                GlobalVariables.c.fillText(this.name, xInPixels - (radiusInPixels + 3), yInPixels+2)
                GlobalVariables.c.fill()
                GlobalVariables.c.closePath()
            }
            else{

                GlobalVariables.c.beginPath()
                    
                if (this.name === 'geometry'){
                    GlobalVariables.c.fillStyle = this.parentMolecule.selectedColor   
                }
                else{
                    GlobalVariables.c.fillStyle = bubbleColor
                }

                GlobalVariables.c.rect(xInPixels, yInPixels - scaleRadiusDown, textWidth + radiusInPixels + halfRadius, scaleRadiusDown*2)
                GlobalVariables.c.arc(xInPixels + textWidth + radiusInPixels + halfRadius, yInPixels, scaleRadiusDown, 0, Math.PI * 2, false)
                GlobalVariables.c.fill()
                GlobalVariables.c.closePath()
                GlobalVariables.c.beginPath()
                GlobalVariables.c.fillStyle = this.parentMolecule.defaultColor
                GlobalVariables.c.textAlign = 'start' 
                GlobalVariables.c.fillText(this.name, (xInPixels + halfRadius) + (radiusInPixels + 3), yInPixels+2)
                GlobalVariables.c.fill()
                GlobalVariables.c.closePath()
            }

        }
 
        GlobalVariables.c.beginPath()
        if(this.ready){
            GlobalVariables.c.fillStyle = this.parentMolecule.color
        }else{
            GlobalVariables.c.fillStyle = '#6ba4ff'
        }
        GlobalVariables.c.strokeStyle = this.parentMolecule.strokeColor
        GlobalVariables.c.lineWidth = 1

        GlobalVariables.c.arc(xInPixels, yInPixels, radiusInPixels, 0, Math.PI * 2, false)
        if(this.showHoverText == true){
            GlobalVariables.c.fill()
            GlobalVariables.c.stroke()
        }
        GlobalVariables.c.closePath()  

        if (!this.expandedRadius){ 
            if (this.type == 'output'){     
                this.offsetX = this.parentMolecule.radius  
            }
        }
    }
    
    /**
     * Handles mouse click down. If the click is inside the AP it's connectors are selected if it is an input.
     * @param {number} x - The x coordinate of the click
     * @param {number} y - The y coordinate of the click
     * @param {boolean} clickProcessed - Has the click already been handled
     */ 
    clickDown(x,y, clickProcessed){

        let xInPixels = GlobalVariables.widthToPixels(this.x)
        let yInPixels = GlobalVariables.heightToPixels(this.y)

        if(GlobalVariables.distBetweenPoints (xInPixels, x, yInPixels, y) < this.defaultRadius && !clickProcessed){
            //console.log(this.value)
            if(this.type == 'output'){                  //begin to extend a connector from this if it is an output
                new Connector({
                    parentMolecule: this.parentMolecule, 
                    attachmentPoint1: this,
                    atomType: 'Connector',
                    isMoving: true
                })
            }
            if(this.type == 'input'){ //connectors can only be selected by clicking on an input
                this.connectors.forEach(connector => {     //select any connectors attached to this node
                    connector.selected = true
                })
            }
            
            return true //indicate that the click was handled by this object
        }
        else{
            if(this.type == 'input'){ //connectors can only be selected by clicking on an input
                this.connectors.forEach(connector => {      //unselect any connectors attached to this node
                    connector.selected = false
                })
            }
            return false //indicate that the click was not handled by this object
        }
    }

    /**
     * Handles mouse click up. If the click is inside the AP and a connector is currently extending, then a connection is made
     * @param {number} x - The x coordinate of the click
     * @param {number} y - The y coordinate of the click
     */ 
    clickUp(x,y){
        this.connectors.forEach(connector => {
            connector.clickUp(x, y)
        })
    }
    
    /**
     * Handles mouse click and move to expand the AP. Could this be done with a call to expand out?
     * @param {number} x - The x coordinate of the click
     * @param {number} y - The y coordinate of the click
     */ 
    clickMove(x,y){
        let xInPixels = GlobalVariables.widthToPixels(this.x)
        let yInPixels = GlobalVariables.heightToPixels(this.y)
        let radiusInPixels = GlobalVariables.widthToPixels(this.radius)
        
        let parentXInPixels = GlobalVariables.widthToPixels(this.parentMolecule.x)
        let parentYInPixels = GlobalVariables.heightToPixels(this.parentMolecule.y)
        let parentRadiusInPixels = GlobalVariables.widthToPixels(this.parentMolecule.radius)
       
        //expand if touched by mouse 
        var distFromClick =  Math.abs(GlobalVariables.distBetweenPoints(parentXInPixels, x, parentYInPixels, y))
        //If we are close to the attachment point move it to it's hover location to make it accessible
        if (distFromClick < parentRadiusInPixels*2.7 && this.type == 'input'){       
            this.expandOut(distFromClick)
            this.showHoverText = true     
        }         
        else if( distFromClick < parentRadiusInPixels *1.5 && this.type == 'output'){       
            this.showHoverText = true
        }
        else{
            this.reset()
            this.expandedRadius = false
        }
        //Expand it if you are close enough to make connection
        if (GlobalVariables.distBetweenPoints(xInPixels, x, yInPixels, y) < radiusInPixels ){
            this.expandedRadius = true
        }  
        else{
            this.expandedRadius = false
        }
        
        this.connectors.forEach(connector => {
            connector.clickMove(x, y)       
        })
    }
    
    /**
     * I'm not sure what this does. Can it be deleted?
     */ 
    reset(){

        if (this.type == 'input'){
            this.offsetX = -1* this.parentMolecule.radius
            this.offsetY = this.defaultOffsetY
        }
        this.showHoverText = false
    }
    
    /**
     * Handles mouse click down. If the click is inside the AP it's connectors are selected if it is an input.
     * @param {number} cursorDistance - The distance the cursor is from the attachment point.
     */ 
    expandOut(cursorDistance){

        let radiusInPixels = GlobalVariables.widthToPixels(this.radius)

        const inputList = this.parentMolecule.inputs.filter(input => input.type == 'input')
        const attachmentPointNumber = inputList.indexOf(this) 
        const anglePerIO = (Math.PI) / (inputList.length + 1)
        // angle correction so that it centers menu adjusting to however many attachment points there are 
        const angleCorrection = -Math.PI/2 - anglePerIO
        this.hoverOffsetY = 12 * this.parentMolecule.radius * (Math.sin((attachmentPointNumber * anglePerIO) - angleCorrection))
        this.hoverOffsetX = 4 * this.parentMolecule.radius * (Math.cos((attachmentPointNumber * anglePerIO) - angleCorrection))
        cursorDistance = Math.max( cursorDistance, radiusInPixels*2) //maxes cursor distance so we can hover over each attachment without expansion movement
        //this.offset uses radius in pixels before translating to pixels because that's also the value that limits cursor distance
        this.offsetX = GlobalVariables.widthToPixels(radiusInPixels * 1.2 * this.hoverOffsetX * this.parentMolecule.radius * GlobalVariables.pixelsToWidth((radiusInPixels*3)/cursorDistance))  
        this.offsetY = GlobalVariables.heightToPixels( radiusInPixels* 2.1 * this.hoverOffsetY * this.parentMolecule.radius* GlobalVariables.pixelsToHeight((radiusInPixels*3)/cursorDistance))
    

    }
    
    /**
     * Just passes a key press to the attached connectors. No impact on the connector.
     * @param {string} key - The key which was pressed
     */ 
    keyPress(key){
        this.connectors.forEach(connector => {
            connector.keyPress(key)       
        })
    }
    
    /**
     * Delete any connectors attached to this ap
     */ 
    deleteSelf(silent = false){
        //remove any connectors which were attached to this attachment point
        var connectorsList = [...this.connectors] //Make a copy of the list so that we can delete elements without having issues with forEach as we remove things from the list
        connectorsList.forEach( connector => {
            connector.deleteSelf(silent)
        })
    }
    
    /**
     * Delete a target connector which is passed in. The default option is to delete all of the connectors.
     */ 
    deleteConnector(connector = "all"){
        try{
            const connectorIndex = this.connectors.indexOf(connector)
            if(connectorIndex != -1){
                this.connectors.splice(connectorIndex,1) //Remove the target connector
            }
            else{
                this.connectors = [] //Remove all of the connectors
            }
        }
        catch(err){
            console.warn("Error deleting connector: ")
            console.warn(err)
        }
    }
    
    /**
     * Can be called to see if the target coordinates are within this ap. Returns true/false.
     * @param {number} x - The x coordinate of the target
     * @param {number} y - The y coordinate of the target
     */ 
    wasConnectionMade(x,y){
        
        let xInPixels = GlobalVariables.widthToPixels(this.x)
        let yInPixels = GlobalVariables.heightToPixels(this.y)
        let radiusInPixels = GlobalVariables.widthToPixels(this.radius)

        //this function returns itself if the coordinates passed in are within itself
        if (GlobalVariables.distBetweenPoints(xInPixels, x, yInPixels, y) < radiusInPixels && this.type == 'input'){  //If we have released the mouse here and this is an input...
        
            if(this.connectors.length > 0){ //Don't accept a second connection to an input
                return false
            }
            else{
                return true
            }
        }
        else{
            return false
        }
    }
    
    /**
     * Attaches a new connector to this ap
     * @param {object} connector - The connector to attach
     */ 
    attach(connector){
        this.connectors.push(connector)
    }
    
    /**
     * Starts propagation from this attachmentPoint if it is not waiting for anything up stream.
     */ 
    beginPropagation(){
        
        //If nothing is connected it is a starting point
        if(this.connectors.length == 0){
            this.setValue(this.value)
        }
    }
    
    /**
     * Passes a lock command to the parent molecule, or to the attached connector depending on input/output.
     */ 
    waitOnComingInformation(){
        if(this.type == 'output'){
            this.connectors.forEach(connector => {
                connector.waitOnComingInformation()
            })
        }
        else{  //If this is an input
            this.ready = false
            this.parentMolecule.waitOnComingInformation(this.name)
        }
    }
    
    /**
     * Restores the ap to it's default value.
     */ 
    setDefault(){
        this.setValue(this.defaultValue)
    }
    
    /**
     * Updates the default value for the ap.
     */ 
    updateDefault(newDefault){
        var oldDefault = this.defaultValue
        this.defaultValue = newDefault
        
        if(this.connectors.length == 0 && this.value == oldDefault){    //Update the value to be the default if there is nothing attached
            this.value = this.defaultValue
        }
    }
    
    /**
     * Reads and returns the current value of the ap.
     */ 
    getValue(){
        return this.value
    }
    
    /**
     * Sets the current value of the ap. Force forces an update even if the value hasn't changed.
     */ 
    setValue(newValue){
        this.value = newValue
        this.ready = true
        //propagate the change to linked elements if this is an output
        if (this.type == 'output'){
            this.connectors.forEach(connector => {     //select any connectors attached to this node
                connector.propogate()
            })
        }
        //if this is an input attachment point
        else{
            this.parentMolecule.updateValue(this.name)
        }
    }
    
    /**
     * Sets all the input and output values to match their associated atoms.
     */ 
    loadTree(){
        this.connectors.forEach(connector => {
            this.value = connector.loadTree()
        })
        return this.value
    }
    
    /**
     * Computes the curent position and then draws the ap on the screen.
     */ 
    update() { 
        this.x = this.parentMolecule.x + this.offsetX
        this.y = this.parentMolecule.y + this.offsetY
        this.draw()
       
        this.connectors.forEach(connector => {  //update any connectors attached to this node
            connector.update()       
        })
    }
}