0.3.0: Loading and Saving, new buffer element, fixed bug on grid draw

This commit is contained in:
MatCat 2021-02-24 18:56:49 -08:00
parent 86fb2873fb
commit a50c655670
8 changed files with 1801 additions and 1350 deletions

View File

@ -12,6 +12,12 @@ To be decided, but at this moment this code is open source and free to use for n
## Changelog
### 0.3.0
* Saving / Loading of designs
* New Element: Buffer, allows for construction of circuits that would otherwise be recursive
* Added pulse count to the delay block printout
* Fixed grid draw bug
### 0.2.12
* Fixed floating active link when deleting an element while linking
* Fixed the delay element so that it properly buffers all input pulses

View File

@ -42,12 +42,16 @@
<input type="button" id="btn_AddXOR" value="^ XOR"/><br />
<input type="button" id="btn_AddXNOR" value="!^ XNOR"/><br />
<input type="button" id="btn_AddNOT" value="! NOT"/><br />
<input type="button" id="btn_AddBUFFER" value="|> BUFFER"/><br />
<input type="button" id="btn_AddSWITCH" value="|- SWITCH"/><br />
<input type="button" id="btn_AddBTN" value="[o] BUTTON"/><br />
<input type="button" id="btn_AddCLK" value="🕑 Clock"/><br />
<input type="button" id="btn_AddPULSE" value="|\__ Pulse"/><br />
<input type="button" id="btn_AddDELAY" value="__|\ Delay"/><br />
<input type="button" id="btn_Delete" value="🗑 Delete"/>
<input type="button" id="btn_Delete" value="🗑 Delete"/><br /><br />
<input type="button" id="btn_Save" value="Save"/><br />
<input type="button" id="btn_Load" value="Load"/>
<input type="file" id="file_Load" style="display: none;" />
</div>
</div>
<canvas id="GridPlane" width="400" height="300" style="position: absolute; top: 50px; left 202px;"></canvas>
@ -61,6 +65,10 @@
</div>
</div>
<div id="darkout-overlay"></div>
<script src="js/globalfunctions.js"></script>
<script src="js/baseclasses.js"></script>
<script src="js/scheduler.js"></script>
<script src="js/elements.js"></script>
<script src="js/logicengine.js"></script>
<script src="js/main.js"></script>

225
js/baseclasses.js Normal file
View File

@ -0,0 +1,225 @@
class CanvasTools {
constructor() {
}
textSize(ctx,text,fontStyle) {
ctx.save();
ctx.font = fontStyle;
let tHeight = Math.round(ctx.measureText(text).actualBoundingBoxAscent + ctx.measureText(text).actualBoundingBoxDescent);
let tWidth = Math.round(ctx.measureText(text).width);
ctx.restore();
return {
width: tWidth,
height: tHeight
};
}
drawBorderBox(ctx,x,y,drawWidth,drawHeight,borderWidth=1,borderColor="#000",fillColor="#f7e979",shadowColor = "transparent") {
ctx.save();
ctx.beginPath();
ctx.fillStyle = borderColor;
if (shadowColor != "transparent") {
ctx.shadowBlur = "6";
ctx.shadowColor = shadowColor;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.stroke();
}
ctx.fillRect(x,y,drawWidth,drawHeight);
ctx.fillStyle = fillColor;
ctx.fillRect(x+borderWidth,y+borderWidth,drawWidth-(borderWidth*2),drawHeight-(borderWidth*2));
ctx.restore();
}
drawTextCentered(ctx,x,y,x2,y2,text,fontStyle="24px Console",fontColor = "#555") {
let old_fillStyle = ctx.fillStyle;
let old_font = ctx.font;
ctx.font = fontStyle;
ctx.fillStyle = fontColor;
let tHeight = ctx.measureText(text).actualBoundingBoxAscent + ctx.measureText(text).actualBoundingBoxDescent;
let tX = x+((x2/2)-(ctx.measureText(text).width/2));
let tY = y+tHeight+((y2/2)-(tHeight/2));
ctx.fillText(text,tX,tY);
ctx.fillStyle = old_fillStyle;
ctx.font = old_font;
}
drawText(ctx,x,y,text,fontStyle="24px Console",fontColor = "#555") {
let old_fillStyle = ctx.fillStyle;
let old_font = ctx.font;
ctx.font = fontStyle;
ctx.fillStyle = fontColor;
ctx.fillText(text,x,y);
ctx.fillStyle = old_fillStyle;
ctx.font = old_font;
}
}
class elementContainer {
constructor() {
this.Elements = new Array();
this.Selected = false;
}
toJSON(key) {
let elements = new Array();
for (let a = 0; a < this.Elements.length; a++) {
elements.push(this.Elements[a].toJSON());
}
return elements;
}
AddElement(element) {
let designatorNumber = 1;
let designatorTest = element.Name + designatorNumber;
let unused = false;
while (!unused) {
let foundMatch = false;
for (let a=0;a < this.Elements.length;a++) {
if (this.Elements[a].Designator == designatorTest) foundMatch = true;
}
if (foundMatch) {
designatorNumber++;
designatorTest = element.Name + designatorNumber;
} else {
unused = true;
element.Designator = designatorTest;
this.Elements.push(element);
}
}
}
DeleteElement(element) {
// Can pass object or Designator
for (let a = 0; a < this.Elements.length; a++) {
if ((this.Elements[a] == element) || (this.Elements[a].Designator == element)) {
this.Elements[a].Delete();
this.Elements.splice(a,1);
return true;
}
}
return false;
}
HasElement(element) {
// Can pass object or Designator
for (let a = 0; a < this.Elements.length; a++) {
if ((this.Elements[a] == element) || (this.Elements[a].Designator == element)) {
return this.Elements[a];
}
}
return false;
}
DrawAll(ctx,settings) {
for (let a = 0; a < this.Elements.length; a++) {
if (this.Elements[a] == this.Selected) this.Elements[a].drawBorderBox(ctx, this.Elements[a].X - 2, this.Elements[a].Y - 2, this.Elements[a].Width + 4, this.Elements[a].Height + 4, 1, "rgba(100,200,255,0.25)", "rgba(100,200,255,0.25)");
this.Elements[a].drawElement(this.Elements[a].X, this.Elements[a].Y, ctx);
let old_font = ctx.font;
let old_fillStyle = ctx.fillStyle;
ctx.font = "10px Console";
let x = this.Elements[a].X;
let y = this.Elements[a].Y + (this.Elements[a].Height - 12);
let x2 = this.Elements[a].Width;
let y2 = 10;
//this.Elements[a].drawTextCentered(ctx, x, y, x2, y2, this.Elements[a].Designator, ctx.font, "#000");
ctx.font = old_font;
ctx.fillStyle = old_fillStyle;
}
if (!this.Selected) {
let PropertiesBox = document.getElementById("PropertiesBox");
if (PropertiesBox.style.display != "none") PropertiesBox.style.display = "none";
}
for (let a = 0; a < this.Elements.length; a++) {
// Not ideal to loop twice but we need the connections drawn all at once to prevent layer issues
this.Elements[a].drawConnections(ctx, settings);
}
}
Select(element) {
this.Selected = element;
let PropertiesBox = document.getElementById("PropertiesBox");
let PropertiesBoxTitle = document.getElementById("PropertiesBoxTitle");
let PropertiesBoxContent = document.getElementById("PropertiesBoxContent");
PropertiesBoxTitle.innerText = this.Selected.Designator + " Properties";
let contentString = "<table id='propertiesTable'>";
for (let a = 0; a < this.Selected.Properties.length;a++) {
contentString += "<tr><td>" + this.Selected.Properties[a].Name + "</td><td>";
switch (this.Selected.Properties[a].Type) {
case "int":
contentString += "<input type='number' id='prop_" + this.Selected.Properties[a].Name + "' min='" + this.Selected.Properties[a].Minimium + "' max='" + this.Selected.Properties[a].Maximium + "' value='" + this.Selected.Properties[a].CurrentValue + "' onchange='logicEngine.PropertyChange(" + String.fromCharCode(34) + this.Selected.Properties[a].Name + String.fromCharCode(34) +");'>";
break;
}
contentString += "</td></tr>";
}
PropertiesBoxContent.innerHTML = contentString;
PropertiesBox.style.display = "block";
}
checkMouseBounds(mousePos) {
// We go backwards so that the newest (highest drawn) element is clicked before one lower.
for (let a = (this.Elements.length - 1); a >= 0; a--) {
if (this.Elements[a].mouseInside(mousePos)) return this.Elements[a];
}
return false;
}
checkOverlayBounds(x,y,width,height) {
for (let a = 0; a < this.Elements.length; a++) {
if ((x >= this.Elements[a].X) && (x <= (this.Elements[a].X + this.Elements[a].Width)) && (y >= this.Elements[a].Y) && (y <= (this.Elements[a].Y + this.Elements[a].Height))) return this.Elements[a];
if (((x + width) >= this.Elements[a].X) && ((x + width) <= (this.Elements[a].X + this.Elements[a].Width)) && (y >= this.Elements[a].Y) && (y <= (this.Elements[a].Y + this.Elements[a].Height))) return this.Elements[a];
if ((x >= this.Elements[a].X) && (x <= (this.Elements[a].X + this.Elements[a].Width)) && ((y + height) >= this.Elements[a].Y) && ((y + height) <= (this.Elements[a].Y + this.Elements[a].Height))) return this.Elements[a];
if (((x + width) >= this.Elements[a].X) && ((x + width) <= (this.Elements[a].X + this.Elements[a].Width)) && ((y + height) >= this.Elements[a].Y) && ((y + height) <= (this.Elements[a].Y + this.Elements[a].Height))) return this.Elements[a];
}
return false;
}
}
class ElementCatalog_Category {
constructor(name,icon) {
this.Name = name;
this.Icon = icon;
this.Elements = new Array();
}
addElement(element) {
if (element instanceof ElementCatalog_Element) {
this.Elements.push(element);
}
}
}
class ElementCatalog_Element {
constructor(name,description,icon,classref,args) {
this.Name = name;
this.Description = description;
this.Icon = icon;
this.Class = classref;
this.Args = args;
}
}
class ElementCatalog {
constructor(categories = new Array()) {
this.Categories = categories;
for (let a = 0; a < this.Categories.length; a++) {
if (!this.Categories[a] instanceof ElementCatalog_Category) {
this.Categories.splice(a,1);
a--;
}
}
}
addCategory(category) {
if (category instanceof ElementCatalog_Category) {
this.Categories.push(category);
}
}
}

1291
js/elements.js Normal file

File diff suppressed because it is too large Load Diff

163
js/globalfunctions.js Normal file
View File

@ -0,0 +1,163 @@
function addElement(RestoreData = null,refClass,props) {
let newElement = new refClass(RestoreData,logicEngine,...props);
if (!RestoreData) {
let x = Math.round(logicEngine.Canvas.width / 2);
let y = Math.round(logicEngine.Canvas.height / 2);
let width = newElement.Width;
let height = newElement.Height;
let noclearspot = true;
while (noclearspot) {
if (!logicEngine.ActiveContainer.checkOverlayBounds(x, y, width, height)) {
noclearspot = false;
} else {
x += Math.floor(Math.random() * (width - (width - (width * 2)))) + (width - (width * 2));
y += Math.floor(Math.random() * (height - (height - (height * 2)))) + (height - (height * 2));
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x + width > logicEngine.Canvas.width) x = logicEngine.Canvas.width - width;
if (y + height > logicEngine.Canvas.height) y = logicEngine.Canvas.height - height;
}
}
newElement.X = x;
newElement.Y = y;
}
logicEngine.ActiveContainer.AddElement(newElement);
logicEngine.ActiveContainer.Select(newElement);
return newElement;
}
function setCookie(cname, cvalue, exdays) {
let d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
document.cookie = cname + '="' + cvalue + '";' + expires;
localStorage.setItem(cname,cvalue);
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return localStorage.getItem(cname);
}
function CheckForWelcomeCookie() {
if (getCookie("hidewelcomescreen")) {
let WelcomeScreen = document.getElementById("WelcomeWindow");
let DarkOverlay = document.getElementById("darkout-overlay");
WelcomeScreen.style.display = "none";
DarkOverlay.style.display = "none";
}
}
function getMousePos(canvas, evt) {
let rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
function averageArray(arr) {
return arr.reduce((a, b) => (a + b)) / arr.length;
}
function length2D(x1,y1,x2,y2) {
let xDist = x1 - x2;
let yDist = y1 - y2;
return Math.sqrt(xDist*xDist + yDist * yDist);
}
function getElementInfo(element) {
for (let a = 0; a < ElementReferenceTable.length; a++) {
if (ElementReferenceTable[a].Name == element) return ElementReferenceTable[a];
}
return false;
}
function isVersionNewer(version1,version2,orEqual = true) {
let v1 = version1.split(".");
let v2 = version1.split(".");
if (v1.length != 3) return false;
if (v2.length != 3) return false;
for (let a = 0; a < 3; a++) {
v1[a] = parseInt(v1[a]);
v2[a] = parseInt(v2[a]);
}
if (v1[0] > v2[0]) return true;
if (v1[0] == v2[0] && v1[1] > v2[1]) return true;
if (v1[0] == v2[0] && v1[1] == v2[1] && v1[2] > v2[2]) return true;
if ((v1[0] == v2[0] && v1[1] == v2[1]) && (orEqual && v1[2] == v2[2])) return true;
return false;
}
function createSaveState(container = false) {
let saveState = {
Name: "LogicDesign",
Version: Version,
Timestamp: Date.now(),
Elements: new Array()
};
if (container.Elements.length > 0) saveState.Elements = container.Elements;
return saveState;
}
function download(filename, savestate) {
let text = JSON.stringify(savestate);
let element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function loadsave(savedata) {
if (!savedata) return false;
if (!savedata.Version) return false; // TODO: Let the person know invalid save file
if (!isVersionNewer(savedata.Version,"0.3.0")) return false; // TODO: Let the person know the version is too old
let newContainer = new elementContainer();
let elementConnections = new Array();
for (let a = 0; a < savedata.Elements.length; a++) {
let classRef = getElementInfo(savedata.Elements[a].Name).Class;
let newElement = new classRef(savedata.Elements[a],logicEngine,savedata.Elements[a].Args);
newContainer.AddElement(newElement);
newElement.Designator = savedata.Elements[a].Designator;
if (savedata.Elements[a].Outputs) {
if (savedata.Elements[a].Outputs.length > 0) {
for (let b=0; b < savedata.Elements[a].Outputs.length; b++) {
elementConnections.push({
FromElement: newElement,
Input: savedata.Elements[a].Outputs[b].Input,
ToElement: savedata.Elements[a].Outputs[b].Element
});
}
}
}
}
// Now we need to make all of the connections
for (let a = 0; a < elementConnections.length; a++) {
let toElement = newContainer.HasElement(elementConnections[a].ToElement);
if (toElement) {
let newConnection = new ElementConnection(newContainer,toElement,elementConnections[a].Input);
elementConnections[a].FromElement.OutputConnections.push(newConnection);
}
}
logicEngine.ActiveContainer = newContainer;
return true;
}

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
MatCat BrowserLogic Simulator
*/
let Version = "0.2.12";
let Version = "0.3.0";
let spanVersion = document.getElementById("version");
spanVersion.innerText = Version;
// get the canvas and get the engine object going
@ -13,7 +13,6 @@ let logicEngine = new LogicEngine(lCanvasElement);
// by the HTML5 spec!
setInterval(logicEngine.Scheduler.Tick.bind(logicEngine.Scheduler), 4);
// Sadly this doesn't work well inside of the class so we will do it here real fast
window.addEventListener('resize', function(evt) {
logicEngine.Resize(evt);
}, false);
@ -57,73 +56,95 @@ btn_Delete.addEventListener('click', function(evt) {
let btn_AddAND = document.getElementById("btn_AddAND");
btn_AddAND.addEventListener('click', function(evt) {
addElement(LogicAND, [2]);
addElement(null,LogicAND, [2]);
}, false);
let btn_AddNAND = document.getElementById("btn_AddNAND");
btn_AddNAND.addEventListener('click', function(evt) {
addElement(LogicNAND, [2]);
addElement(null,LogicNAND, [2]);
}, false);
let btn_AddOR = document.getElementById("btn_AddOR");
btn_AddOR.addEventListener('click', function(evt) {
addElement(LogicOR, [2]);
addElement(null,LogicOR, [2]);
}, false);
let btn_AddNOR = document.getElementById("btn_AddNOR");
btn_AddNOR.addEventListener('click', function(evt) {
addElement(LogicNOR, [2]);
addElement(null,LogicNOR, [2]);
}, false);
let btn_AddXOR = document.getElementById("btn_AddXOR");
btn_AddXOR.addEventListener('click', function(evt) {
addElement(LogicXOR,[]);
addElement(null,LogicXOR,[]);
}, false);
let btn_AddXNOR = document.getElementById("btn_AddXNOR");
btn_AddXNOR.addEventListener('click', function(evt) {
addElement(LogicXNOR,[]);
addElement(null,LogicXNOR,[]);
}, false);
let btn_AddNOT = document.getElementById("btn_AddNOT");
btn_AddNOT.addEventListener('click', function(evt) {
addElement(LogicNOT,[]);
addElement(null,LogicNOT,[]);
}, false);
let btn_AddBUFFER = document.getElementById("btn_AddBUFFER");
btn_AddBUFFER.addEventListener('click', function(evt) {
let logicBuffer = addElement(null,LogicBuffer,[]);
}, false);
let btn_AddSWITCH = document.getElementById("btn_AddSWITCH");
btn_AddSWITCH.addEventListener('click', function(evt) {
addElement(InputSwitch,[]);
addElement(null,InputSwitch,[]);
}, false);
let btn_AddBTN = document.getElementById("btn_AddBTN");
btn_AddBTN.addEventListener('click', function(evt) {
addElement(InputButton,[]);
addElement(null,InputButton,[]);
}, false);
let btn_AddCLK = document.getElementById("btn_AddCLK");
btn_AddCLK.addEventListener('click', function(evt) {
let clk = addElement(ClockElement,[]);
logicEngine.Scheduler.addTask(clk.Task);
let clk = addElement(null,ClockElement,[]);
}, false);
let btn_AddPulse = document.getElementById("btn_AddPULSE");
btn_AddPulse.addEventListener('click', function(evt) {
let pulse = addElement(PulseElement,[]);
logicEngine.Scheduler.addTask(pulse.Task);
let pulse = addElement(null,PulseElement,[]);
}, false);
let btn_AddDelay = document.getElementById("btn_AddDELAY");
btn_AddDelay.addEventListener('click', function(evt) {
let delay = addElement(DelayElement,[]);
logicEngine.Scheduler.addTask(delay.Task);
let delay = addElement(null,DelayElement,[]);
}, false);
let btn_Save = document.getElementById("btn_Save");
btn_Save.addEventListener('click', function(evt) {
download("mydeign.LogicParts",createSaveState(logicEngine.ActiveContainer));
});
let file_Load = document.getElementById("file_Load");
let btn_Load = document.getElementById("btn_Load");
btn_Load.addEventListener('click', function(evt) {
file_Load.click();
});
file_Load.addEventListener('change', function(evt) {
let fread = new FileReader();
fread.onload = (function (theFile) {
return function (e) {
try {
let restoredata = JSON.parse(e.target.result);
if (!loadsave(restoredata)) {
alert("Bad file!");
}
} catch (ex) {
alert("Bad file!");
}
}
})(evt.target.files[0]);
fread.readAsText(evt.target.files[0]);
}, false);
function CheckForWelcomeCookie() {
if (getCookie("hidewelcomescreen")) {
let WelcomeScreen = document.getElementById("WelcomeWindow");
let DarkOverlay = document.getElementById("darkout-overlay");
WelcomeScreen.style.display = "none";
DarkOverlay.style.display = "none";
}
}
CheckForWelcomeCookie();

60
js/scheduler.js Normal file
View File

@ -0,0 +1,60 @@
class Task {
constructor(taskname,taskdescription,tasktype,tasktime,callback,deleteonrun = false) {
// tasktype: 0: interval, 1: fixed time
this.Name = taskname;
this.Description = taskdescription;
this.Type = tasktype;
this.Enabled = true;
this.Time = tasktime;
this.LastCall = Date.now();
this.CallCount = 0;
this.DeleteOnRun = false;
if (deleteonrun) this.DeleteOnRun = true;
this.Callback = callback;
if (!(tasktype >= 0 && tasktype <= 1)) this.Type = 0;
}
CheckTime() {
let time = this.Time;
if (this.Type == 0) time = this.LastCall + this.Time;
if (this.Enabled && (Date.now() >= time)) {
this.LastCall = Date.now();
this.CallCount++;
if (this.Type == 1 || this.DeleteOnRun == true) this.Enabled = false;
this.Callback();
return true;
}
return false;
}
}
class ScheduleEngine {
constructor() {
this.Tasks = new Array();
}
addTask(task) {
this.Tasks.push(task);
}
deleteTask(task) {
for (let a = 0; a < this.Tasks.length; a++) {
if (this.Tasks[a] == task) {
this.Tasks.splice(a,1);
return true;
}
}
return false;
}
Tick() {
for (let a = 0; a < this.Tasks.length; a++) {
this.Tasks[a].CheckTime();
if (!this.Tasks[a].Enabled && this.Tasks[a].DeleteOnRun) {
this.Tasks.splice(a,1);
a--;
}
}
}
}