src/flowDraw.js
import GlobalVariables from './js/globalvariables'
import Molecule from './js/molecules/molecule.js'
import GitHubMolecule from './js/molecules/githubmolecule.js'
import {cmenu, showGitHubSearch} from './js/NewMenu.js'
GlobalVariables.canvas = document.querySelector('canvas')
GlobalVariables.c = GlobalVariables.canvas.getContext('2d')
GlobalVariables.runMode = window.location.href.includes('run') //Check if we are using the run mode based on url
GlobalVariables.canvas.width = window.innerWidth
GlobalVariables.canvas.height = window.innerHeight/2.5
// Event Listeners
/**
* The canvas on which the atoms are placed.
* @type {object}
*/
let flowCanvas = document.getElementById('flow-canvas')
var longTouchTimer
var lastMoveTouch
/**
* The last time a touch was detected...used for timing a long touch.
*/
var lastTouchTime = new Date().getTime()
flowCanvas.addEventListener('touchstart', event => {
//Keep track of this for the touch up
lastMoveTouch = event.touches[0]
GlobalVariables.touchInterface = true
//Check for a double touch
var timesinceLastTouch = new Date().getTime() - lastTouchTime
if((timesinceLastTouch < 600) && (timesinceLastTouch > 0)){
onDoubleClick(event.touches[0])
}
else{
onMouseDown(event.touches[0])
}
lastTouchTime = new Date().getTime()
//This should be a fake right click
longTouchTimer = setTimeout(function() {
const downEvt = new MouseEvent('mousedown', {
clientX: event.touches[0].clientX,
clientY: event.touches[0].clientY,
which: 3,
button: 2,
detail: 1
})
document.getElementById('flow-canvas').dispatchEvent(downEvt)
}, 1500)
})
flowCanvas.addEventListener('mousedown', event => {
onMouseDown(event)
})
flowCanvas.addEventListener('touchmove', event => {
lastMoveTouch = event.touches[0]
clearTimeout(longTouchTimer)
onMouseMove(lastMoveTouch)
})
flowCanvas.addEventListener('mousemove', event => {
onMouseMove(event)
})
flowCanvas.addEventListener('dblclick', event => {
onDoubleClick(event)
})
document.addEventListener('mouseup',(e)=>{
if(e.srcElement.tagName.toLowerCase() !== ("textarea")
&& e.srcElement.tagName.toLowerCase() !== ("input")
&& e.srcElement.tagName.toLowerCase() !== ("select")
&&(!e.srcElement.isContentEditable)){
//puts focus back into mainbody after clicking button
document.activeElement.blur()
document.getElementById("mainBody").focus()
}
})
flowCanvas.addEventListener('touchend', () => {
clearTimeout(longTouchTimer)
onMouseUp(lastMoveTouch)
})
flowCanvas.addEventListener('mouseup', event => {
onMouseUp(event)
})
/**
* Called by mouse down
*/
function onMouseDown(event){
var isRightMB
if ("which" in event){ // Gecko (Firefox), WebKit (Safari/Chrome) & Opera
isRightMB = event.which == 3
}
else if ("button" in event){ // IE, Opera
isRightMB = event.button == 2
}
if(isRightMB){
return
}
var clickHandledByMolecule = false
GlobalVariables.currentMolecule.nodesOnTheScreen.forEach(molecule => {
if (molecule.clickDown(event.clientX,event.clientY,clickHandledByMolecule) == true){
clickHandledByMolecule = true
}
})
//Draw the selection box
if (!clickHandledByMolecule){
GlobalVariables.currentMolecule.placeAtom({
parentMolecule: GlobalVariables.currentMolecule,
x: GlobalVariables.pixelsToWidth(event.clientX),
y: GlobalVariables.pixelsToHeight(event.clientY),
parent: GlobalVariables.currentMolecule,
name: 'Box',
atomType: 'Box'
}, null, GlobalVariables.availableTypes)
}
if(!clickHandledByMolecule){
GlobalVariables.currentMolecule.backgroundClick()
}
else{
GlobalVariables.currentMolecule.selected = false
}
//hide the menu if it is visible
if (!document.querySelector('#circle-menu1').contains(event.target)) {
cmenu.hide()
}
//hide search menu if it is visible
if (!document.querySelector('#canvas_menu').contains(event.target)) {
const menu = document.querySelector('#canvas_menu')
menu.classList.add('off')
menu.style.top = '-200%'
menu.style.left = '-200%'
}
//hide the menu if it is visible
if (!document.querySelector('#straight_menu').contains(event.target)) {
closeTopMenu()
let options = document.querySelectorAll('.option')
Array.prototype.forEach.call(options, a => {
a.classList.remove("openMenu")
})
}
}
/**
* Called by mouse up
*/
function onMouseUp(event){
//every time the mouse button goes up
GlobalVariables.currentMolecule.nodesOnTheScreen.forEach(molecule => {
molecule.clickUp(event.clientX,event.clientY)
})
GlobalVariables.currentMolecule.clickUp(event.clientX,event.clientY)
}
/**
* Called by mouse moves
*/
function onMouseMove(event){
GlobalVariables.currentMolecule.nodesOnTheScreen.forEach(molecule => {
molecule.clickMove(event.clientX,event.clientY)
})
}
/**
* Called by double clicks
*/
function onDoubleClick(event){
GlobalVariables.currentMolecule.nodesOnTheScreen.forEach(molecule => {
molecule.doubleClick(event.clientX,event.clientY)
})
}
/**
* Array containing selected atoms to copy or delete
* @type {array}
*/
window.addEventListener('keydown', e => {
//Prevents default behavior of the browser on canvas to allow for copy/paste/delete
if(e.srcElement.tagName.toLowerCase() !== ("textarea")
&& e.srcElement.tagName.toLowerCase() !== ("input")
&&(!e.srcElement.isContentEditable)
&& ['c','v','Backspace'].includes(e.key)){
e.preventDefault()
}
if (document.activeElement.id == "mainBody"){
if (e.key == "Backspace" || e.key == "Delete") {
GlobalVariables.atomsSelected = []
//Adds items to the array that we will use to delete
GlobalVariables.currentMolecule.copy()
GlobalVariables.atomsSelected.forEach(item => {
GlobalVariables.currentMolecule.nodesOnTheScreen.forEach(nodeOnTheScreen => {
if(nodeOnTheScreen.uniqueID == item.uniqueID){
nodeOnTheScreen.deleteNode()
}
})
})
}
/**
* Object containing letters and values used for keyboard shortcuts
* @type {object?}
*/
var shortCuts = {
a: "Assembly",
b: "ShrinkWrap",//>
c: "Copy",
d: "Difference",
e: "Extrude",
g: "GitHub", // Not working yet
i: "Input",
j: "Translate",
k: "Rectangle",
l: "Circle",
m: "Molecule",
s: "Save",
v: "Paste",
x: "Equation",
y: "Code", //is there a more natural code letter? can't seem to prevent command t new tab behavior
z: "Undo" //saving this letter
}
//Copy /paste listeners
if (e.key == "Control" || e.key == "Meta") {
GlobalVariables.ctrlDown = true
}
if (GlobalVariables.ctrlDown && shortCuts.hasOwnProperty([e.key])) {
e.preventDefault()
//Copy & Paste
if (e.key == "c") {
GlobalVariables.atomsSelected = []
GlobalVariables.currentMolecule.copy()
}
if (e.key == "v") {
GlobalVariables.atomsSelected.forEach(item => {
let newAtomID = GlobalVariables.generateUniqueID()
item.uniqueID = newAtomID
GlobalVariables.currentMolecule.placeAtom(item, true)
})
}
//Save project
if (e.key == "s") {
GlobalVariables.gitHub.saveProject()
}
//Opens menu to search for github molecule
if (e.key == "g") {
showGitHubSearch()
}
else {
GlobalVariables.currentMolecule.placeAtom({
parentMolecule: GlobalVariables.currentMolecule,
x: 0.5,
y: 0.5,
parent: GlobalVariables.currentMolecule,
atomType: `${shortCuts[e.key]}`,
uniqueID: GlobalVariables.generateUniqueID()
}, true)
}
}
}
//every time a key is pressed
GlobalVariables.currentMolecule.nodesOnTheScreen.forEach(molecule => {
molecule.keyPress(e.key)
})
})
window.addEventListener('keyup', e => {
if (e.key == "Control" || e.key == "Meta") {
GlobalVariables.ctrlDown = false
}
})
/* Button to open top menu */
document.getElementById('straight_menu').addEventListener('mousedown', () => {
openTopMenu()
})
/**
* Checks if menu is open and changes class to trigger hiding of individual buttons
*/
function openTopMenu(){
document.querySelector('#toggle_wrap').style.display = "inline"
let options = document.querySelectorAll('.option')
var step = -150
Array.prototype.forEach.call(options, a => {
if (a.classList.contains("openMenu")){
closeTopMenu()
a.classList.remove("openMenu")
}
else{
a.classList.add("openMenu")
a.style.transition = `transform 0.5s`
a.style.transform = `translateX(${step}%)`
step-=100
document.getElementById('goup_top').style.visibility = "hidden"
}
})
}
/**
* Closes main menu on background click or on button click if open
*/
function closeTopMenu(){
document.querySelector('#toggle_wrap').style.display = "inline"
let options = document.querySelectorAll('.option')
var step = 0
document.getElementById('goup_top').style.visibility = "visible"
Array.prototype.forEach.call(options, a => {
a.style.transition = `transform 0.5s`
a.style.transform = `translateX(${step}%)`
})
}
/**
* Top Button menu event listeners if not in run mode
*/
if (!GlobalVariables.runMode){
let githubButton = document.getElementById('github_top')
githubButton.addEventListener('mousedown', () => {
GlobalVariables.gitHub.openGitHubPage()
})
let otherProjectsButton = document.getElementById('projectmenu_top')
otherProjectsButton.addEventListener('mousedown', () => {
GlobalVariables.gitHub.showProjectsToLoad()
})
let shareButton = document.getElementById('share_top')
shareButton.addEventListener('mousedown', () => {
GlobalVariables.gitHub.shareOpenedProject()
})
let bomButton = document.getElementById('bom_top')
bomButton.addEventListener('mousedown', () => {
GlobalVariables.gitHub.openBillOfMaterialsPage()
})
let readButton = document.getElementById('read_top')
readButton.addEventListener('mousedown', () => {
GlobalVariables.gitHub.openREADMEPage()
})
let saveButton = document.getElementById('save_top')
saveButton.addEventListener('mousedown', () => {
GlobalVariables.gitHub.saveProject()
})
let parentButton = document.getElementById('goup_top')
parentButton.addEventListener('mousedown', () => {
if(!GlobalVariables.currentMolecule.topLevel){
GlobalVariables.currentMolecule.goToParentMolecule()
}
})
let deleteButton = document.getElementById('delete_top')
deleteButton.addEventListener('mousedown', () => {
GlobalVariables.gitHub.deleteProject()
})
let pullButton = document.getElementById('pull_top')
pullButton.addEventListener('mousedown', () => {
GlobalVariables.gitHub.makePullRequest()
})
}
//Add viewer bar which lets you turn on and off things like wireframe view
/**
* Contains the check boxes to hide and show the display attributes
*/
let viewerBar = document.querySelector('#viewer_bar')
/**
* The up arrow for going up one level
*/
let arrowUpMenu = document.querySelector('#arrow-up-menu')
/**
* Creates the checkbox hidden menu when viewer is active. These really shouldn't be regenerated every time. They should just be hidden.
*/
function checkBoxes(){
//Update the values from all the check boxes
function checkBoxChange(){
GlobalVariables.displayGrid = document.getElementById('gridCheck').checked
GlobalVariables.displayAxis = document.getElementById('axesCheck').checked
GlobalVariables.displayEdges = document.getElementById('edgesCheck').checked
GlobalVariables.displayTriangles = document.getElementById('facesCheck').checked
GlobalVariables.writeToDisplay(GlobalVariables.displayedPath)
}
let viewerBar = document.querySelector('#viewer_bar')
viewerBar.classList.add('slidedown')
//Grid display html element
var gridDiv = document.createElement('div')
viewerBar.appendChild(gridDiv)
gridDiv.setAttribute('id', 'gridDiv')
var gridCheck = document.createElement('input')
gridDiv.appendChild(gridCheck)
gridCheck.setAttribute('type', 'checkbox')
gridCheck.setAttribute('id', 'gridCheck')
gridDiv.setAttribute('style', 'float:right;')
if (GlobalVariables.displayGrid){
gridCheck.setAttribute('checked', 'true')
}
var gridCheckLabel = document.createElement('label')
gridDiv.appendChild(gridCheckLabel)
gridCheckLabel.setAttribute('for', 'gridCheck')
gridCheckLabel.setAttribute('style', 'margin-right:1em;')
gridCheckLabel.textContent= "Grid"
gridCheckLabel.setAttribute('style', 'user-select: none;')
gridCheck.addEventListener('change', checkBoxChange)
//Axes Html
var axesDiv = document.createElement('div')
viewerBar.appendChild(axesDiv)
var axesCheck = document.createElement('input')
axesDiv.appendChild(axesCheck)
axesCheck.setAttribute('type', 'checkbox')
axesCheck.setAttribute('id', 'axesCheck')
if (GlobalVariables.displayAxis){
axesCheck.setAttribute('checked', 'true')
}
var axesCheckLabel = document.createElement('label')
axesDiv.appendChild(axesCheckLabel)
axesCheckLabel.setAttribute('for', 'axesCheck')
axesCheckLabel.setAttribute('style', 'margin-right:1em;')
axesDiv.setAttribute('style', 'float:right;')
axesCheckLabel.textContent= "Axes"
axesCheckLabel.setAttribute('style', 'user-select: none;')
axesCheck.addEventListener('change', checkBoxChange)
//Display faces
var facesDiv = document.createElement('div')
viewerBar.appendChild(facesDiv)
var facesCheck = document.createElement('input')
facesDiv.appendChild(facesCheck)
facesCheck.setAttribute('type', 'checkbox')
facesCheck.setAttribute('id', 'facesCheck')
if(GlobalVariables.displayTriangles){
facesCheck.setAttribute('checked', 'true')
}
var facesCheckLabel = document.createElement('label')
facesDiv.appendChild(facesCheckLabel)
facesCheckLabel.setAttribute('for', 'facesCheck')
facesCheckLabel.setAttribute('style', 'margin-right:1em;')
facesDiv.setAttribute('style', 'float:right;')
facesCheckLabel.textContent= "Faces"
facesCheckLabel.setAttribute('style', 'user-select: none;')
facesCheck.addEventListener('change', checkBoxChange)
//Display edges
var edgesDiv = document.createElement('div')
viewerBar.appendChild(edgesDiv)
var edgesCheck = document.createElement('input')
edgesDiv.appendChild(edgesCheck)
edgesCheck.setAttribute('type', 'checkbox')
edgesCheck.setAttribute('id', 'edgesCheck')
if(GlobalVariables.displayEdges){
edgesCheck.setAttribute('checked', 'true')
}
var edgesCheckLabel = document.createElement('label')
edgesDiv.appendChild(edgesCheckLabel)
edgesCheckLabel.setAttribute('for', 'edgesCheck')
edgesCheckLabel.setAttribute('style', 'margin-right:1em;')
edgesDiv.setAttribute('style', 'float:right;')
edgesCheckLabel.textContent= "Edges"
edgesCheckLabel.setAttribute('style', 'user-select: none;')
edgesCheck.addEventListener('change', checkBoxChange)
//Display wireframe
var resetDiv = document.createElement('div')
viewerBar.appendChild(resetDiv)
var resetButton = document.createElement('button')
resetButton.innerHTML = "Reset View"
resetDiv.appendChild(resetButton)
resetButton.setAttribute('type', 'checkbox')
resetButton.setAttribute('id', 'resetButton')
var resetButtonLabel = document.createElement('label')
resetDiv.appendChild(resetButtonLabel)
resetButtonLabel.setAttribute('for', 'resetButton')
resetButtonLabel.setAttribute('style', 'margin-right:1em;')
resetDiv.setAttribute('style', 'float:right;')
resetButtonLabel.textContent= " "
resetButtonLabel.setAttribute('style', 'user-select: none;')
resetButton.addEventListener('click', ()=>{GlobalVariables.writeToDisplay(GlobalVariables.displayedPath, true)})
}
document.getElementById('viewerContext').addEventListener('mouseenter', () => {
if(viewerBar.innerHTML.trim().length == 0){
checkBoxes()
}
})
/**
* A flag to indicate if the startTimer event has already fired
* @type {boolean}
*/
var evtFired = false
var g_timer
/**
* Starts the timer to retract the menu
*/
function startTimer(){
g_timer = setTimeout(function() {
if (!evtFired) {
viewerBar.classList.remove("slideup")
viewerBar.classList.add('slidedown')
}
}, 2000)
}
arrowUpMenu.addEventListener('mouseenter', () =>{
clearTimeout(g_timer)
viewerBar.classList.remove("slidedown")
viewerBar.classList.add('slideup')
})
viewerBar.addEventListener('mouseleave', () =>{
evtFired = false
viewerBar.classList.remove("slideup")
viewerBar.classList.add('slidedown')
})
viewerBar.addEventListener('mouseenter', () =>{
evtFired = true
viewerBar.classList.remove("slidedown")
viewerBar.classList.add('slideup')
})
arrowUpMenu.addEventListener('mouseleave', () =>{
startTimer()
})
// Implementation
/**
* Runs once when the program begins to initialize variables.
*/
function init() {
if(!GlobalVariables.runMode){ //If we are in CAD mode load an empty project as a placeholder
GlobalVariables.currentMolecule = new Molecule({
x: 0,
y: 0,
topLevel: true,
name: 'Maslow Create',
atomType: 'Molecule',
uniqueID: GlobalVariables.generateUniqueID()
})
}
else{
var ID = window.location.href.split('?')[1]
//Have the current molecule load it
if(typeof ID != undefined){
GlobalVariables.currentMolecule = new GitHubMolecule({
projectID: ID,
topLevel: true
})
GlobalVariables.topLevelMolecule = GlobalVariables.currentMolecule
//This is used because window.ask takes some time to load so we need to wait for it. This sets up a callback which will be called in jsxcad.js once window.ask exists
window.askSetupCallback = () => {
GlobalVariables.topLevelMolecule.loadProjectByID(ID).then( ()=> {
GlobalVariables.topLevelMolecule.backgroundClick()
})
}
}
}
window.addEventListener('resize', () => { onWindowResize() }, false)
onWindowResize()
animate()
}
/**
* Handles the window's resize behavior when the browser size changes.
*/
function onWindowResize() {
GlobalVariables.canvas.width = window.innerWidth
//reset screen parameters
if(!GlobalVariables.runMode){
document.querySelector('.flex-parent').setAttribute('style','height:'+ (window.innerHeight - GlobalVariables.canvas.height)+'px')
}else{
document.querySelector('.flex-parent').setAttribute('style','height:'+innerHeight+'px')
}
document.querySelector('.jscad-container').setAttribute('style','width:'+innerWidth/1.7+'px')
}
/**
* Animation loop. Runs with every frame to draw the program on the display.
*/
function animate() {
requestAnimationFrame(animate)
GlobalVariables.c.clearRect(0, 0, GlobalVariables.canvas.width, GlobalVariables.canvas.height)
GlobalVariables.currentMolecule.nodesOnTheScreen.forEach(atom => {
atom.update()
})
}
init()