src/js/molecules/code.js
import Atom from '../prototypes/atom.js'
import CodeMirror from 'codemirror'
import GlobalVariables from '../globalvariables'
/**
* The Code molecule type adds support for executing arbitrary jsxcad code.
*/
export default class Code 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)
/**
* This atom's name
* @type {string}
*/
this.name = "Code"
/**
* This atom's name
* @type {string}
*/
this.atomType = "Code"
/**
* A description of this atom
* @type {string}
*/
this.description = "Defines a JSxCAD code block."
/**
* The code contained within the atom stored as a string.
* @type {string}
*/
this.code = "//You can learn more about all of the available methods at https://jsxcad.js.org/app/UserGuide.html \n//Inputs:[Input1, Input2];\n\n\nreturn Orb(10)"
this.addIO("output", "geometry", this, "geometry", "")
this.setValues(values)
this.parseInputs(false)
}
/**
* Draw the code atom which has a code icon.
*/
draw(){
super.draw() //Super call to draw the rest
GlobalVariables.c.beginPath()
GlobalVariables.c.fillStyle = '#949294'
GlobalVariables.c.font = `${GlobalVariables.widthToPixels(this.radius)}px Work Sans Bold`
GlobalVariables.c.fillText('</>', GlobalVariables.widthToPixels(this.x - (this.radius/1.5)), GlobalVariables.heightToPixels(this.y + (this.radius*1.5)))
}
/**
* Begin propagation from this code atom if it has no inputs or if none of the inputs are connected.
*/
beginPropagation(){
//If there are no inputs
if(this.inputs.length == 0){
this.updateValue()
}
//If none of the inputs are connected
var connectedInput = false
this.inputs.forEach(input => {
if(input.connectors.length > 0){
connectedInput = true
}
})
if(!connectedInput){
this.updateValue()
}
}
/**
* Grab the code as a text string and execute it.
*/
updateValue(){
try{
this.parseInputs()
var argumentsArray = {}
this.inputs.forEach(input => {
argumentsArray[input.name] = input.value
})
const values = { op: "code", code: this.code, paths: argumentsArray, writePath: this.path }
var go = true
this.inputs.forEach(input => {
if(!input.ready){
go = false
}
})
if(go){ //Then we update the value
this.waitOnComingInformation() //This sends a chain command through the tree to lock all the inputs which are down stream of this one. It also cancels anything processing if this atom was doing a calculation already.
/**
* Indicates that this atom is computing
* @type {boolean}
*/
this.processing = true
this.decreaseToProcessCountByOne()
this.clearAlert()
const {answer, terminate} = window.ask(values)
answer.then(result => {
if (result.success){
if(result.type == "path"){
this.displayAndPropagate()
}
else{
if(this.output){
this.output.setValue(result.value)
this.output.ready = true
}
}
}else{
this.setAlert("Unable to compute")
}
this.processing = false
})
/**
* This can be called to interrupt the computation
* @type {function}
*/
this.cancelProcessing = terminate
}
}catch(err){this.setAlert(err)}
}
/**
* This function reads the string of inputs the user specifies and adds them to the atom.
*/
parseInputs(ready = true){
//Parse this.code for the line "\nmain(input1, input2....) and add those as inputs if needed
var variables = /Inputs:\[\s*([^)]+?)\s*\]/.exec(this.code)
if(variables){
if (variables[1]) {
variables = variables[1].split(/\s*,\s*/)
}
//Add any inputs which are needed
for (var variable in variables){
if(!this.inputs.some(input => input.Name === variables[variable])){
this.addIO('input', variables[variable], this, 'geometry', null, ready)
}
}
//Remove any inputs which are not needed
for (var input in this.inputs){
if( !variables.includes(this.inputs[input].name) ){
this.removeIO('input', this.inputs[input].name, this)
}
}
}
}
/**
* Edit the atom's code when it is double clicked
* @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
let xInPixels = GlobalVariables.widthToPixels(this.x)
let yInPixels = GlobalVariables.heightToPixels(this.y)
var clickProcessed = false
var distFromClick = GlobalVariables.distBetweenPoints(x, xInPixels, y, yInPixels)
if (distFromClick < this.radius){
this.editCode()
clickProcessed = true
}
return clickProcessed
}
/**
* Called to trigger editing the code atom
*/
editCode(){
//Remove everything in the popup now
const popup = document.getElementById('projects-popup')
while (popup.firstChild) {
popup.removeChild(popup.firstChild)
}
popup.classList.remove('off')
//Add a title
var codeMirror = CodeMirror(popup, {
value: this.code,
mode: "javascript",
lineNumbers: true,
gutter: true,
lineWrapping: true
})
var form = document.createElement("form")
popup.appendChild(form)
var button = document.createElement("button")
button.setAttribute("type", "button")
button.appendChild(document.createTextNode("Save Code"))
button.addEventListener("click", () => {
this.code = codeMirror.getDoc().getValue('\n')
this.updateValue()
popup.classList.add('off')
})
form.appendChild(button)
}
/**
* Add a button to open the code editor to the side bar
*/
updateSidebar(){
var valueList = super.updateSidebar()
this.createButton(valueList,this,"Edit Code",() => {
this.editCode()
})
}
/**
* Save the input code to be loaded next time
*/
serialize(values){
//Save the readme text to the serial stream
var valuesObj = super.serialize(values)
valuesObj.code = this.code
return valuesObj
}
}