class LogicEngineSettings { constructor(restoresettings = false) { this.ActiveConnectionColor = "#aabbaa"; this.ActiveConnectionHoverColor = "#ccffcc"; this.InactiveConnectionColor = "#bbaaaa"; this.InactiveConnectionHoverColor = "#ffcccc"; this.LinkWidth = "2"; this.LinkDash = []; this.LinkingConnectionColor = "#aabbbb"; this.LinkingWidth = "3"; this.LinkingDash = [2,2]; this.ShadowColor = "#222"; this.InputCircleSize = 10; this.OutputCircleSize = 10; this.ShowGrid = true; this.SnapGrid = true; this.TopConnections = true; this.HideConnections = false; this.GridSize = 20; this.ShowFPS = true; this.Keybindings = { FileNew: {Key: "n", // lowercase Alt: false, Shift: false, Ctrl: true, Meta: false, Name: "New Design", Description: "Start a new design", Category: "File"}, FileOpen: {Key: "o", // lowercase Alt: false, Shift: false, Ctrl: true, Meta: false, Name: "Open Design", Description: "Open a design", Category: "File"}, FileSave: {Key: "s", // lowercase Alt: false, Shift: false, Ctrl: true, Meta: false, Name: "Save Design", Description: "Save current design", Category: "File"}, EditUndo: {Key: "z", // lowercase Alt: false, Shift: false, Ctrl: true, Meta: false, Name: "Undo", Description: "Steps back through the last things you have done reversing them", Category: "Edit"}, EditRedo: {Key: "y", // lowercase Alt: false, Shift: false, Ctrl: true, Meta: false, Name: "Redo", Description: "Reverse any changes done via Redo", Category: "Edit"}, DeleteElements: {Key: "delete", Alt: false, Shift: false, Ctrl: false, Meta: false, Name: "Delete Element(s)", Description: "Delete currently selected elements", Category: "Edit"}, DisconnectElements: {Key: "d", Alt: false, Shift: true, Ctrl: false, Meta: false, Name: "Disconnect Element(s)", Description: "Disconnect currently selected elements", Category: "Edit"}, SelectAll: {Key: "a", // lowercase Alt: false, Shift: false, Ctrl: true, Meta: false, Name: "Select All", Description: "Select all elments in the design", Category: "Edit"}, ResetCanvas: {Key: "home", // lowercase Alt: false, Shift: false, Ctrl: false, Meta: false, Name: "Reset View", Description: "Resets the pan back to the default location", Category: "View"}, ShowConnections: {Key: "c", // lowercase Alt: false, Shift: true, Ctrl: false, Meta: false, Name: "Toggle Connections", Description: "Show / Hide connections", Category: "View"}, ConnectionLayer: {Key: "c", // lowercase Alt: false, Shift: true, Ctrl: true, Meta: false, Name: "Connection Layer", Description: "Toggle connections above / below elements", Category: "View"}, ShowGrid: {Key: "g", // lowercase Alt: false, Shift: true, Ctrl: false, Meta: false, Name: "Toggle Grid", Description: "Show / Hide the grid", Category: "View"}, SnapGrid: {Key: "g", // lowercase Alt: true, Shift: true, Ctrl: false, Meta: false, Name: "Snap to Grid", Description: "Snap to grid or not", Category: "View"}, GridPlus: {Key: "+", // lowercase Alt: false, Shift: true, Ctrl: false, Meta: false, Name: "Enlarge Grid", Description: "Make the grid larger", Category: "View"}, GridMinus: {Key: "-", // lowercase Alt: false, Shift: true, Ctrl: false, Meta: false, Name: "Shrink Grid", Description: "Make the grid smaller", Category: "View"}, ShowFPS: {Key: "f3", // lowercase Alt: false, Shift: false, Ctrl: false, Meta: false, Name: "Toggle FPS", Description: "Show / Hide FPS counter", Category: "View"}, CreateIC: {Key: "c", // lowercase Alt: false, Shift: false, Ctrl: true, Meta: false, Name: "Create IC", Description: "Turns the current design into an IC", Category: "Tools"}, Help: {Key: "f1", // lowercase Alt: false, Shift: false, Ctrl: false, Meta: false, Name: "Help Window", Description: "Opens help window", Category: "Other"} /* keybind_name: {Key: "", // lowercase Alt: false, Shift: false, Ctrl: false, Meta: false, Name: "", Description: "", Category: ""} */ }; if (restoresettings) { let othis = this; Object.keys(restoresettings).forEach(function(key) { othis[key] = restoresettings[key]; }); } } } class LogicEngine { Resize(evt) { let leftmenu = document.getElementById("left-menu"); let topbar = document.getElementById("top-bar"); let lmrect = leftmenu.getBoundingClientRect(); let tbrect = topbar.getBoundingClientRect(); leftmenu.style.height = (window.innerHeight - (tbrect.height + 2)) + "px"; this.Canvas.width = window.innerWidth - lmrect.width; this.Canvas.height = window.innerHeight - tbrect.height; this.Mouse = false; let gridPlane = document.getElementById("GridPlane"); gridPlane.style.top = tbrect.height + "px"; gridPlane.style.left = lmrect.width + "px"; this.Canvas.style.top = tbrect.height + "px"; this.Canvas.style.left = lmrect.width + "px"; gridPlane.width = this.Canvas.width; gridPlane.height = this.Canvas.height; this.Ctx.setTransform(1,0,0,1,0,0); this.Ctx.translate(this.Panning.OffsetX,this.Panning.OffsetY); if (this.Settings.ShowGrid) { let Ctx = gridPlane.getContext("2d"); Ctx.save(); let gridWidth = this.Settings.GridSize; for (let x = gridWidth; x < (this.Canvas.width); x += gridWidth) { Ctx.beginPath(); Ctx.moveTo(x, 0); Ctx.lineTo(x, this.Canvas.height); Ctx.strokeStyle = "#777"; Ctx.lineWidth = "1"; Ctx.stroke(); } for (let y = gridWidth; y < (this.Canvas.height); y += gridWidth) { Ctx.beginPath(); Ctx.moveTo(0, y); Ctx.lineTo(this.Canvas.width, y); Ctx.lineWidth = "1"; Ctx.strokeStyle = "#777"; Ctx.stroke(); } Ctx.restore(); } } PropertyChange(property) { if (this.ActiveContainer.Selected.length > 1) return false; if (!this.ActiveContainer.Selected[0].getProperty(property)) return false; let propElement = document.getElementById("prop_" + property); this.ActiveContainer.Selected[0].getProperty(property).Call(propElement.value); } Mouse_Down(evt) { if (evt.which === 1) { let mousePos = getMousePos(this.Canvas, evt); this.MouseDownTime = performance.now(); let element = this.ActiveContainer.checkMouseBounds(mousePos); if (element) { this.MouseDown = true; if (this.ActiveContainer.isSelected(element)) { } else { this.ActiveContainer.Select(element); } this.MovingElement = new Array(this.ActiveContainer.Selected.length); for (let a = 0; a < this.ActiveContainer.Selected.length; a++) { this.MovingElement[a] = { StartX: this.ActiveContainer.Selected[a].X, StartY: this.ActiveContainer.Selected[a].Y }; this.MovingElementMouseStartX = mousePos.x; this.MovingElementMouseStartY = mousePos.y; } element.MouseDown(mousePos); } else { this.MouseDown = true; this.ActiveLink = false; if (this.ControlPressed) { this.Panning.StartOffsetX = this.Panning.OffsetX; this.Panning.StartOffsetY = this.Panning.OffsetY; this.Panning.StartX = mousePos.x; this.Panning.StartY = mousePos.y; } else { let cmPos = this.getCanvasMousePos(mousePos); this.MultiSelectStart.InProgress = true; this.MultiSelectStart.x = cmPos.x; this.MultiSelectStart.y = cmPos.y; this.ActiveContainer.Selected = false; let PropertiesBox = document.getElementById("PropertiesBox"); PropertiesBox.style.display = "none"; } } if (this.ActiveContainer.Selected?.length > 0) { disableSelectedRCMs(false); } else { disableSelectedRCMs(true); } } } Mouse_Up(evt) { let mousePos = getMousePos(this.Canvas, evt); if (this.MovingElement) { let element = this.ActiveContainer.checkMouseBounds(mousePos); if (element) element.MouseUp(mousePos); } if (this.MovingElement && (this.MovingElement.X == this.MovingElementStartX) && (this.MovingElement.Y == this.MovingElementStartY)) { if ((performance.now() - this.MouseDownTime) < 3000) { // Presume this was a click let element = this.ActiveContainer.checkMouseBounds(mousePos); if (element) element.MouseClick(mousePos); } //console.log("Mouse Up"); } if (!this.MovingElement && evt.which === 1) { this.ActiveContainer.Selected = false; let PropertiesBox = document.getElementById("PropertiesBox"); PropertiesBox.style.display = "none"; } this.MovingElement = false; this.MouseDown = false; if (this.MultiSelectStart.InProgress) { this.MultiSelectStart.InProgress = false; let cmStartPos = {x: this.MultiSelectStart.x, y: this.MultiSelectStart.y}; let cmEndPos = this.getCanvasMousePos(mousePos); this.ActiveContainer.SelectWithin((cmStartPos.x>cmEndPos.x) ? cmEndPos.x : cmStartPos.x, (cmStartPos.y>cmEndPos.y) ? cmEndPos.y : cmStartPos.y, (cmStartPos.x>cmEndPos.x) ? cmStartPos.x : cmEndPos.x, (cmStartPos.y>cmEndPos.y) ? cmStartPos.y : cmEndPos.y); } if (this.ActiveContainer.Selected?.length > 0) { disableSelectedRCMs(false); } else { disableSelectedRCMs(true); } } Mouse_Move(evt) { //this.Canvas.focus(); let mousePos = getMousePos(this.Canvas, evt); this.Mouse = mousePos; if(this.MouseDown) { //console.log('Mouse at position: ' + mousePos.x + ',' + mousePos.y); if (this.MovingElement) { if ((performance.now() - this.MouseDownTime) > 100) { let xOffset = mousePos.x - this.MovingElementMouseStartX; let yOffset = mousePos.y - this.MovingElementMouseStartY; for (let a = 0; a < this.ActiveContainer.Selected.length; a++) { let diffxOffset = this.MovingElementMouseStartX - this.MovingElement[a].StartX; let diffyOffset = this.MovingElementMouseStartY - this.MovingElement[a].StartY; let actualPosX = (this.MovingElementMouseStartX + xOffset) - diffxOffset; let actualPosY = (this.MovingElementMouseStartY + yOffset) - diffyOffset; if (!this.ControlPressed && this.Settings.SnapGrid) actualPosX = Math.round(actualPosX / this.Settings.GridSize) * this.Settings.GridSize; if (!this.ControlPressed && this.Settings.SnapGrid) actualPosY = Math.round(actualPosY / this.Settings.GridSize) * this.Settings.GridSize; this.ActiveContainer.Selected[a].X = actualPosX; this.ActiveContainer.Selected[a].Y = actualPosY; this.ActiveContainer.Selected[a].redraw = true; } } } else { if (this.ControlPressed) { let distX = mousePos.x - this.Panning.StartX; let distY = mousePos.y - this.Panning.StartY; this.Panning.StartX += distX; this.Panning.StartY += distY; this.Panning.OffsetX += distX; this.Panning.OffsetY += distY; this.Ctx.translate(distX, distY); } } } else { this.ActiveContainer.checkMouseBounds(mousePos); } } Key_Up(evt) { if (!evt.ctrlKey) { this.ControlPressed = false; } } Key_Press(evt) { if (evt.ctrlKey) { this.ControlPressed = true; } if (document.activeElement.tagName.toLowerCase() == "input") return; // Dont interupt a textbox if (evt.key == "Escape") { if (this.MovingElement && this.MouseDown) { this.MovingElement.X = this.MovingElementStartX; this.MovingElement.Y = this.MovingElementStartY; this.MovingElement = false; } } if (MatchesPress(this.Settings.Keybindings.FileNew,evt)) { evt.preventDefault(); tfm_New.click(evt); } if (MatchesPress(this.Settings.Keybindings.FileOpen,evt)) { evt.preventDefault(); alert("It is not possible to do a dialog box file open with javascript keyboard shortcuts, however this feature will work soon once server storage is implemented! Until then you will have to click on Open."); } if (MatchesPress(this.Settings.Keybindings.FileSave,evt)) { evt.preventDefault(); tfm_Save.click(evt); } if (MatchesPress(this.Settings.Keybindings.ResetCanvas,evt)) { evt.preventDefault(); tfm_Pan2Center.click(evt); } if (MatchesPress(this.Settings.Keybindings.ShowConnections,evt)) { evt.preventDefault(); tfm_ShowConns.click(evt); } if (MatchesPress(this.Settings.Keybindings.ConnectionLayer,evt)) { evt.preventDefault(); tfm_ConnLayer.click(evt); } if (MatchesPress(this.Settings.Keybindings.SelectAll,evt)) { evt.preventDefault(); tfm_SelectAll.click(evt); } if (MatchesPress(this.Settings.Keybindings.ShowGrid,evt)) { evt.preventDefault(); tfm_ShowGrid.click(evt); } if (MatchesPress(this.Settings.Keybindings.SnapGrid,evt)) { evt.preventDefault(); tfm_SnapGrid.click(evt); } if (MatchesPress(this.Settings.Keybindings.GridPlus,evt)) { evt.preventDefault(); in_GridSize.value = logicEngine.Settings.GridSize + Math.ceil(logicEngine.Settings.GridSize * 0.05); in_GridSize.dispatchEvent(new Event('change')); } if (MatchesPress(this.Settings.Keybindings.GridMinus,evt)) { evt.preventDefault(); in_GridSize.value = ((logicEngine.Settings.GridSize - Math.ceil(logicEngine.Settings.GridSize * 0.05)) < 2) ? 2 : logicEngine.Settings.GridSize - Math.ceil(logicEngine.Settings.GridSize * 0.05) ; in_GridSize.dispatchEvent(new Event('change')); } if (MatchesPress(this.Settings.Keybindings.ShowFPS,evt)) { evt.preventDefault(); tfm_ShowFPS.click(evt); } if (MatchesPress(this.Settings.Keybindings.CreateIC,evt)) { evt.preventDefault(); tfm_CreateIC.click(evt); } if (MatchesPress(this.Settings.Keybindings.Help,evt)) { evt.preventDefault(); ShowHelp(); } if (MatchesPress(this.Settings.Keybindings.DeleteElements,evt)) { if (this.ActiveContainer.Selected?.length > 0) { this.ActiveContainer.DeleteElement(this.ActiveContainer.Selected); this.ActiveContainer.Selected = false; let PropertiesBox = document.getElementById("PropertiesBox"); PropertiesBox.style.display = "none"; } } if (MatchesPress(this.Settings.Keybindings.DisconnectElements,evt)) { if (this.ActiveContainer.Selected?.length > 0) { for (let a = 0; a < logicEngine.ActiveContainer.Selected.length; a++) { logicEngine.ActiveContainer.Selected[a].Disconnect(); } logicEngine.ActiveContainer.Disconnect(logicEngine.ActiveContainer.Selected); } } } constructor(canvas) { this.Canvas = canvas; this.Ctx = canvas.getContext("2d"); let restoresettings = false; if (localStorage.getItem("LogicEngineSettings")) { restoresettings = JSON.parse(localStorage.getItem("LogicEngineSettings")); console.log("Restoring Settings"); } this.Settings = new LogicEngineSettings(restoresettings); this.FPSCounter = 0; this.FPS = 0; this.PotentialFPS = 0; this.PotentialFPSAVGs = new Array(20); this.PotentialFPSAVGLoc = 0; this.LastFPSCheck = performance.now(); this.MouseDown = false; this.MouseDownTime = 0; this.MovingElementContainer = false; this.MovingElement = false; this.MovingElementMouseStartX = 0; this.MovingElementMouseStartY = 0; this.ActiveContainer = new elementContainer(); this.ActiveLink = false; this.Scheduler = new ScheduleEngine(); this.RecursionCount = 0; this.RecursionError = false; this.Canvas.setAttribute('tabindex','0'); this.ControlPressed = false; this.Panning = {OffsetX: 0, OffsetY: 0,StartOffsetX: 0, StartOffsetY: 0, StartX: 0, StartY: 0}; this.MultiSelectStart = { x: 0, y: 0, InProgress: false }; } Link(input = 0) { if (this.ActiveLink) { if ((this.ActiveContainer.Selected?.length == 1) && (this.ActiveContainer.Selected?.[0] != this.ActiveLink)) { this.ActiveLink.addConnection(this.ActiveContainer,this.ActiveContainer.Selected[0],input,this.ActiveLink.OutputLink.Output); this.ActiveLink = false; } else { this.ActiveLink = false; } } else { if (this.ActiveContainer.Selected.length == 1) { if (!this.ActiveContainer.Selected[0].NoOutput) this.ActiveLink = this.ActiveContainer.Selected[0]; } } } getCanvasMousePos(mousePos) { return {x: mousePos.x - this.Panning.OffsetX, y: mousePos.y - this.Panning.OffsetY}; } RedrawStatics() { for (let a = 0; a < this.ActiveContainer.Elements.length; a++) { this.ActiveContainer.Elements[a].drawElement(0,0,this.ActiveContainer.Elements[a].StaticCtx); } } DrawLoop() { if (this.RecursionError) { this.RecursionError = false; alert("Recursion Error! Whatever you last did is causing an oscillating loop, please check your connections and try again!"); } let startLoop = performance.now(); this.Ctx.clearRect(0- this.Panning.OffsetX,0- this.Panning.OffsetY,this.Canvas.width,this.Canvas.height); let ct = new CanvasTools(); if (this.MultiSelectStart.InProgress) { let cmPos = this.getCanvasMousePos(this.Mouse); ct.drawBorderBox(this.Ctx,this.MultiSelectStart.x,this.MultiSelectStart.y,cmPos.x - this.MultiSelectStart.x,cmPos.y - this.MultiSelectStart.y,0,"rgba(100,200,255,0.25)","rgba(100,200,255,0.25)"); } this.ActiveContainer.DrawAll(this.Ctx,this.Settings); let tfm_CreateIC = document.getElementById("tfm_CreateIC"); let rcm_CreateIC = document.getElementById("rcm_CreateIC"); tfm_CreateIC.classList.add("disabled"); rcm_CreateIC.classList.add("disabled"); if (this.ActiveContainer.ICOutputs > 0) tfm_CreateIC.classList.remove("disabled"); if (this.ActiveContainer.ICOutputs > 0) rcm_CreateIC.classList.remove("disabled"); if (this.ActiveLink) { let startX = this.ActiveLink.LinkOutLocation().x; let startY = this.ActiveLink.LinkOutLocation().y; let endX = this.Mouse.x - this.Panning.OffsetX; let endY = this.Mouse.y - this.Panning.OffsetY; let startMidX = startX + ((endX - startX)/2); let startMidY = startY; let midX = startMidX; let midY = startY + ((endY - startY)/2); let endMidX = startMidX; let endMidY = endY; this.Ctx.save(); this.Ctx.strokeStyle = this.Settings.LinkingConnectionColor; this.Ctx.lineWidth = this.Settings.LinkingWidth; this.Ctx.setLineDash(this.Settings.LinkingDash); this.Ctx.beginPath(); this.Ctx.moveTo(startX, startY); this.Ctx.quadraticCurveTo(startMidX,startMidY,midX,midY); this.Ctx.quadraticCurveTo(endMidX,endMidY,endX,endY); this.Ctx.stroke(); this.Ctx.restore(); } let FPSOffset = 5 - this.Panning.OffsetX; if (this.Settings.ShowFPS) { ct.drawText(this.Ctx, FPSOffset, 15 - this.Panning.OffsetY, "FPS: " + this.FPS, "12px console", "#00ff00"); ct.drawText(this.Ctx, FPSOffset, 29 - this.Panning.OffsetY, "Potential FPS: " + Math.floor(this.PotentialFPS), "12px console", "#00ff00"); } let timeCheck = performance.now(); this.FPSCounter++; if (!(Math.round(timeCheck - this.LastFPSCheck) % 50)) { let frameTimeUS = (performance.now() - startLoop) * 1000; let potentialFPS = 1000000 / frameTimeUS; this.PotentialFPSAVGs[this.PotentialFPSAVGLoc] = Math.round(potentialFPS); this.PotentialFPSAVGLoc++; if (this.PotentialFPSAVGLoc == this.PotentialFPSAVGs.length) this.PotentialFPSAVGLoc = 0; this.PotentialFPS = averageArray(this.PotentialFPSAVGs); } if ((timeCheck - this.LastFPSCheck) >= 1000) { this.FPS = this.FPSCounter; this.FPSCounter = 0; this.LastFPSCheck = performance.now(); //console.log("Frame Time: " + frameTimeUS + "uS" + ", Potential FPS: " + potentialFPS); //console.log("FPS: " + FPS); } requestAnimationFrame(this.DrawLoop.bind(this)); } StartEngine() { this.Resize(""); this.DrawLoop(); // Get the animation loop going } }