@@ -193,6 +211,7 @@
+
diff --git a/js/elements/BaseElementClasses.js b/js/elements/BaseElementClasses.js
index 0c8496c..e11bceb 100644
--- a/js/elements/BaseElementClasses.js
+++ b/js/elements/BaseElementClasses.js
@@ -413,47 +413,53 @@ class Element extends CanvasTools {
let firstY = (centerY - (totalHeight/2)) + 12;
for (let a = 0; a < this.OutputConnections.length;a++) {
- let mouseDist = length2D(this.X+(this.Width - 10), firstY + (this.OutputConnections[a].Output*24),this.MousePosition.x,this.MousePosition.y);
- if (!this.OutputConnections[a].Container.HasElement(this.OutputConnections[a].Element)) {
- // This is a ghosted connection, lets get rid of it
- this.OutputConnections.splice(a,1);
- a--;
- } else {
- let endCenterY = this.OutputConnections[a].Element.Y + Math.round(this.OutputConnections[a].Element.Height / 2);
- let endTotalHeight = this.OutputConnections[a].Element.totalInputs() * ((this.OutputConnections[a].Element.inputCircleRadius*2)+4);
- let endFirstY = (endCenterY - (endTotalHeight/2)) + 12;
+ if (this.isVisible() || this.OutputConnections[a].Element.isVisible()) {
+ let mouseDist = length2D(this.X + (this.Width - 10), firstY + (this.OutputConnections[a].Output * 24), this.MousePosition.x, this.MousePosition.y);
+ if (!this.OutputConnections[a].Container.HasElement(this.OutputConnections[a].Element)) {
+ // This is a ghosted connection, lets get rid of it
+ this.OutputConnections.splice(a, 1);
+ a--;
+ } else {
+ let endCenterY = this.OutputConnections[a].Element.Y + Math.round(this.OutputConnections[a].Element.Height / 2);
+ let endTotalHeight = this.OutputConnections[a].Element.totalInputs() * ((this.OutputConnections[a].Element.inputCircleRadius * 2) + 4);
+ let endFirstY = (endCenterY - (endTotalHeight / 2)) + 12;
- let startX = this.X + this.Width;
- let startY = firstY+ (this.OutputConnections[a].Output*24);
- let endX = this.OutputConnections[a].Element.X;
- //let endY = this.OutputConnections[a].Element.Y+(this.OutputConnections[a].Element.inputCircleRadius + 2)+(((this.OutputConnections[a].Input*(4+(this.OutputConnections[a].Element.inputCircleRadius*2))))-2)+(this.OutputConnections[a].Element.inputCircleRadius/2);
- let endY = endFirstY + (this.OutputConnections[a].Input*24);
- let startMidX = startX + ((endX - startX)/2);
- let startMidY = startY;
- let midX = startMidX;
- let midY = startY + ((endY - startY)/2);
- let endMidX = startMidX;
- let endMidY = endY;
+ let startX = this.X + (this.Width - this.outputCircleRadius);
+ let startY = firstY + (this.OutputConnections[a].Output * 24);
+ let endX = this.OutputConnections[a].Element.X + this.OutputConnections[a].Element.inputCircleRadius;
+ //let endY = this.OutputConnections[a].Element.Y+(this.OutputConnections[a].Element.inputCircleRadius + 2)+(((this.OutputConnections[a].Input*(4+(this.OutputConnections[a].Element.inputCircleRadius*2))))-2)+(this.OutputConnections[a].Element.inputCircleRadius/2);
+ let endY = endFirstY + (this.OutputConnections[a].Input * 24);
+ let startMidX = startX + ((endX - startX) / 2);
+ let startMidY = startY;
+ let midX = startMidX;
+ let midY = startY + ((endY - startY) / 2);
+ let endMidX = startMidX;
+ let endMidY = endY;
+
+ ctx.save();
+ ctx.beginPath();
+ ctx.lineWidth = settings.LinkWidth;
+ if ((mouseDist <= (this.outputCircleRadius)) && !this.LogicEngine.ActiveLink) ctx.lineWidth = (settings.LinkWidth * 2);
+ ctx.setLineDash(settings.LinkDash);
+ ctx.moveTo(startX, startY);
+ if (this instanceof LogicWireNode) {
+ ctx.lineTo(endX, endY);
+ } else {
+ ctx.quadraticCurveTo(startMidX, startMidY, midX, midY);
+ ctx.quadraticCurveTo(endMidX, endMidY, endX, endY);
+ }
+
+ ctx.strokeStyle = settings.ActiveConnectionColor;
+ if ((mouseDist <= (this.outputCircleRadius)) && !this.LogicEngine.ActiveLink) ctx.strokeStyle = settings.ActiveConnectionHoverColor;
+ if (!this.getOutput(this.OutputConnections[a].Output)) {
+ ctx.strokeStyle = settings.InactiveConnectionColor;
+ if ((mouseDist <= (this.outputCircleRadius)) && !this.LogicEngine.ActiveLink) ctx.strokeStyle = settings.InactiveConnectionHoverColor;
+ }
+
+ ctx.stroke();
+ ctx.restore();
- ctx.save();
- ctx.beginPath();
- ctx.lineWidth = settings.LinkWidth;
- if ((mouseDist <= (this.outputCircleRadius)) && !this.LogicEngine.ActiveLink) ctx.lineWidth = (settings.LinkWidth * 2);
- ctx.setLineDash(settings.LinkDash);
- ctx.moveTo(startX, startY);
- //ctx.lineTo(endX, endY);
- ctx.quadraticCurveTo(startMidX,startMidY,midX,midY);
- ctx.quadraticCurveTo(endMidX,endMidY,endX,endY);
- ctx.strokeStyle = settings.ActiveConnectionColor;
- if ((mouseDist <= (this.outputCircleRadius)) && !this.LogicEngine.ActiveLink) ctx.strokeStyle = settings.ActiveConnectionHoverColor;
- if (!this.getOutput(this.OutputConnections[a].Output)) {
- ctx.strokeStyle = settings.InactiveConnectionColor;
- if ((mouseDist <= (this.outputCircleRadius)) && !this.LogicEngine.ActiveLink) ctx.strokeStyle = settings.InactiveConnectionHoverColor;
}
-
- ctx.stroke();
- ctx.restore();
-
}
}
}
diff --git a/js/elements/BasicElements.js b/js/elements/BasicElements.js
index 52de653..adb76ff 100644
--- a/js/elements/BasicElements.js
+++ b/js/elements/BasicElements.js
@@ -545,3 +545,145 @@ class LogicBuffer extends Element {
let ElementCatalog_BUFFER = new ElementCatalog_Element("Buffer","The buffer is a special gate to allow the prevention of recursion loops.","|>",LogicBuffer,[]);
ElementReferenceTable.push(ElementCatalog_BUFFER);
ElementCategory_LOGIC.addElement(ElementCatalog_BUFFER);
+
+class RAMElement extends Element {
+ constructor(_Container, RestoreData = null, logicengine) {
+ super(_Container, RestoreData = null, logicengine,2);
+ this.Name = "RAM";
+ this.removeProperty("Inputs");
+ this.AddressWidth = 16;
+ this.DataWidth = 8;
+ this._Data = new Array((2 ** this.AddressWidth));
+
+ for (let a = 0; a < this._Data.length; a++) {
+ this._Data[a] = (2 ** this.DataWidth)-1;
+ }
+
+ this.Inputs = new Array(this.AddressWidth + this.DataWidth + 3);
+ this.Outputs = new Array(this.DataWidth);
+
+ this.Height = this.Inputs.length * (this.inputCircleRadius + 14)+10;
+ this.Width = (12*(this.DataWidth/4)) + 220;
+
+ this.InputLabels = new Array(this.Inputs.length);
+ for (let a = 0; a < this.DataWidth; a++) {
+ this.InputLabels[a] = "I" + a;
+ this.OutputLabels[a] = "O" + a;
+ }
+ for (let a = 0; a < this.AddressWidth; a++) {
+ this.InputLabels[this.DataWidth+a] = "A" + a;
+ }
+ this.InputLabels[this.DataWidth+this.AddressWidth] = "WriteEnable";
+ this.InputLabels[this.DataWidth+this.AddressWidth+1] = "OutputEnable";
+ this.InputLabels[this.DataWidth+this.AddressWidth+2] = "CLK";
+
+ this.drawElement(0,0,this.StaticCtx);
+ }
+
+ setInput(Input, Value) {
+ if (Value) {
+ Value = true;
+ } else {
+ Value = false;
+ }
+
+ let oldInput = this.Inputs[Input];
+ if (Input < this.totalInputs()) {
+ this.Inputs[Input] = Value;
+ } else {
+ return;
+ }
+
+ let isHigh = this._Container.isHigh(this,Input);
+ if (isHigh !== false) this.Inputs[Input] = true;
+ if (isHigh === false) this.Inputs[Input] = false;
+ if (oldInput != this.Inputs[Input]) {
+
+ if (this.Inputs[this.Inputs.length-2]) {
+ // Output Enable
+ this.setOutput();
+ } else {
+ this.setOutput(true);
+ }
+
+ if (this.Inputs[this.Inputs.length-3] && this.Inputs[this.Inputs.length-1] && (Input == this.Inputs.length-1)) {
+ this._Data[this.currentAddress()] = this.currentInputValue();
+ if (this.Inputs[this.Inputs.length-2]) {
+ // Output Enable
+ this.setOutput();
+ } else {
+ this.setOutput(true);
+ }
+ }
+ }
+ this.setConnections();
+ this.drawElement(0,0,this.StaticCtx);
+
+ }
+ currentAddress() {
+ let addr = 0;
+ for (let a = 0; a < this.AddressWidth; a++) {
+ addr |= ((this.Inputs[this.DataWidth+a]) ? 1 : 0) << a;
+ }
+ return addr;
+ }
+
+ currentInputValue() {
+ let val = 0;
+ for (let a = 0; a < this.DataWidth; a++) {
+ val |= ((this.Inputs[a]) ? 1 : 0) << a;
+ }
+ return val;
+ }
+
+ currentValue() {
+ return this._Data[this.currentAddress()];
+ }
+
+ setOutput(clear = false) {
+ let currentValue = this.currentValue();
+ for (let a = 0; a < this.DataWidth; a++) {
+ this.Outputs[a] = false;
+ if (!clear && (currentValue & (0b1 << a))) this.Outputs[a] = true;
+ }
+ }
+
+ getOutput(output) {
+ if (this.Outputs[output]) return true;
+ return false;
+ }
+
+ drawElement(x, y, ctx) {
+ if (this.LogicEngine.ActiveContainer !== this._Container) return; // No point if we aren't visible
+ this.StaticCanvas.width = this.Width;
+ this.StaticCanvas.height = this.Height;
+
+ if (!this._Data) return;
+
+ this.drawBorderBox(ctx,x+20,y,this.Width-40,this.Height);
+ this.drawTextCentered(ctx,x+20,(y+(this.Height-16)),this.Width-40,14,this.Designator,"12px Console","#000");
+
+ let totalAddr = Math.floor((this.Height - 40)/14);
+ let startAddr = this.currentAddress() - Math.floor(totalAddr/2);
+ if (startAddr < 0) startAddr = 0;
+ let endAddr = startAddr + totalAddr;
+ if (endAddr > this._Data.length) startAddr = this._Data.length - totalAddr;
+ if (startAddr < 0) startAddr = 0;
+
+ for (let a = 0; a < ((this._Data.length > totalAddr) ? totalAddr : this._Data.length) ; a++) {
+ let getAddr = a+startAddr;
+ if (this.currentAddress() == a + startAddr) this.drawBorderBox(ctx,x+60,(y+10+(a*14)),this.Width-100,14,0,undefined,"#ffffaa");
+ this.drawTextCentered(ctx,x+57,(y+10+(a*14)),(this.Width/2)-20,14,parseInt(a+startAddr).toString(16).toUpperCase().padStart(parseInt(this._Data.length-1).toString(16).length,'0') + `: `,"12px Console","#000");
+ this.drawText(ctx,x+(Math.floor(this.Width/2)+10),(y+22+Math.floor(a*14)), this._Data[(a+startAddr)].toString(16).toUpperCase().padStart(parseInt((2 ** this.DataWidth)-1).toString(16).length,'0'),"12px Console","#000");
+ }
+
+ this.drawInputs(ctx,x,y,undefined,undefined,undefined,undefined,true);
+ this.drawOutputs(ctx,x,y,undefined,undefined,undefined,undefined,true);
+ }
+
+}
+
+
+let ElementCatalog_RAM = new ElementCatalog_Element("RAM","RAM, configurable address and data width",":::",RAMElement,[]);
+ElementReferenceTable.push(ElementCatalog_RAM);
+ElementCategory_Other.addElement(ElementCatalog_RAM);
diff --git a/js/globalfunctions.js b/js/globalfunctions.js
index 2a1f8ef..d3de7f9 100644
--- a/js/globalfunctions.js
+++ b/js/globalfunctions.js
@@ -276,20 +276,33 @@ function SaveSettings() {
console.log("Settings Saved");
}
-function createSaveState(container = false) {
- let saveState = {
- Name: "LogicDesign",
- Version: Version,
- Timestamp: Date.now(),
- PanX: logicEngine.Panning.OffsetX,
- PanY: logicEngine.Panning.OffsetY,
- Elements: new Array()
- };
+function createSaveState(container = false,localstorage = false) {
+ if (!container) return false;
+ let saveState = null;
+
+ if (localstorage) {
+ saveState = {
+ V: Version,
+ TS: Date.now(),
+ PX: logicEngine.Panning.OffsetX,
+ PY: logicEngine.Panning.OffsetY,
+ Elements: new Array()
+ };
+ } else {
+ saveState = {
+ Name: "My Design",
+ Version: Version,
+ Timestamp: Date.now(),
+ PanX: logicEngine.Panning.OffsetX,
+ PanY: logicEngine.Panning.OffsetY,
+ Elements: new Array()
+ };
+ }
if (container.Elements.length > 0) saveState.Elements = container.Elements;
let saveCompressed = document.getElementById("saveCompressed");
- if (saveCompressed.checked) {
+ if (saveCompressed.checked || forcecompression) {
saveState.Elements = LZString.compressToUTF16(JSON.stringify(saveState.Elements));
- saveState.Compressed = true;
+ if (!localstorage) saveState.Compressed = true;
} else {
saveState.Compressed = false;
}
diff --git a/js/localstorage.js b/js/localstorage.js
new file mode 100644
index 0000000..7114949
--- /dev/null
+++ b/js/localstorage.js
@@ -0,0 +1,271 @@
+class StorageFolder {
+ constructor(restoreSettings) {
+ this.Name = "Folder";
+ this.CreationDate = Date.now();
+ this.LastModifiedDate = Date.now();
+ this.Folder = undefined;
+ this.Designs = new Array();
+ this.Folders = new Array();
+
+ if (restoreSettings) {
+ if (restoreSettings.Name) this.Name = restoreSettings.Name;
+ if (restoreSettings.CreationDate) this.CreationDate = restoreSettings.CreationDate;
+ if (restoreSettings.LastModifiedDate) this.LastModifiedDate = restoreSettings.LastModifiedDate;
+ if (restoreSettings.Folders) {
+ for (let a = 0; a < restoreSettings.Folders.length;a ++) {
+ let sf = new StorageFolder(restoreSettings.Folders[a]);
+ sf.Folder = this;
+ this.Folders.push(sf);
+ }
+ }
+ if (restoreSettings.Designs) {
+ for (let a = 0; a < restoreSettings.Designs.length;a ++) {
+ let sd = new StorageDesign(restoreSettings.Designs[a]);
+ sd.Folder = this;
+ this.Designs.push(sd);
+ }
+ }
+ }
+ }
+
+ toJSON(key) {
+ let sfjson = {};
+ sfjson.Name = this.Name;
+ sfjson.CreationDate = this.CreationDate;
+ sfjson.LastModifiedDate = this.LastModifiedDate;
+ sfjson.Designs = this.Designs;
+ sfjson.Folders = this.Folders;
+ return sfjson;
+ }
+
+ PathString(ChildrenString = "") {
+ if (this.Folder) {
+ let pathString = `
${this.Name} ${(ChildrenString != "") ? "/" : ""} ${ChildrenString}`;
+ pathString = this.Folder.PathString(pathString);
+ return pathString;
+ } else {
+ if (ChildrenString == "") {
+ return `
/ `;
+ } else {
+ return `
/ ${ChildrenString}`;
+ }
+ }
+ }
+
+ HasFolder(folder = undefined) {
+ if (!folder) return false;
+ for (let a = 0; a < this.Folders.length; a++) {
+ if (folder === this.Folders[a] || folder === this.Folders[a].Name) return this.Folders[a];
+ }
+ return false;
+ }
+
+ AddFolder(folder) {
+ if (!folder) return false;
+ let nameCounter = 1;
+ let ogName = folder.Name;
+ folder.Folder = this;
+
+ while (this.HasFolder(folder.Name)) {
+ // There is a name conflict!
+ folder.Name = ogName + " (" + nameCounter + ")";
+ nameCounter++;
+ }
+
+ this.Folders.push(folder);
+ this.LastModifiedDate = Date.now();
+ return folder;
+ }
+
+ RemoveFolder(folder,overide=false) {
+ if (!folder) return false;
+ for (let a = 0; a < this.Folders; a++) {
+ if (folder === this.Folders[a] || folder == this.Folders[a].Name) {
+ if (this.Folders[a].Designs > 0 && !overide) return -1;
+ this.Folders.splice(a,1);
+ this.LastModifiedDate = Date.now();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ HasDesign(design) {
+ if (!design) return false;
+ for (let a = 0; a < this.Designs; a++) {
+ if (design === this.Designs[a] || design == this.Designs[a].Name) this.Designs[a];
+ }
+ return false;
+ }
+
+ AddDesign(design) {
+ if (!design) return false;
+ let nameCounter = 1;
+ let ogName = design.Name;
+ design.Folder = this;
+
+ while (this.HasDesign(design.Name)) {
+ // There is a name conflict!
+ design.Name = ogName + " (" + nameCounter + ")";
+ nameCounter++;
+ }
+
+ this.Designs.push(design);
+ this.LastModifiedDate = Date.now();
+ return design;
+ }
+
+ RemoveDesign(design) {
+ if (!design) return false;
+ for (let a = 0; a < this.Designs; a++) {
+ if (design === this.Designs[a] || design == this.Designs[a].Name) {
+ this.Designs.splice(a,1);
+ this.LastModifiedDate = Date.now();
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+class StorageDesign {
+ constructor(restoreSettings) {
+ this.Name = "My Design";
+ this.Creator = "Anonymous";
+ this.CreationDate = Date.now();
+ this.LastModifiedDate = Date.now();
+ this.Description = "";
+ this.FileIndex = 0;
+ this.Folder = undefined;
+
+ if (restoreSettings) {
+ if (restoreSettings.Name) this.Name = restoreSettings.Name;
+ if (restoreSettings.Creator) this.Creator = restoreSettings.Creator;
+ if (restoreSettings.CreationDate) this.CreationDate = restoreSettings.CreationDate;
+ if (restoreSettings.LastModifiedDate) this.LastModifiedDate = restoreSettings.LastModifiedDate;
+ if (restoreSettings.Description) this.Description = restoreSettings.Description;
+ if (restoreSettings.FileIndex) this.FileIndex = restoreSettings.FileIndex;
+ }
+ }
+
+ toJSON(key) {
+ let sdjson = {};
+ sdjson.Name = this.Name;
+ sdjson.Creator = this.Creator;
+ sdjson.CreationDate = this.CreationDate;
+ sdjson.LastModifiedDate = this.LastModifiedDate;
+ sdjson.Description = this.Description;
+ sdjson.FileIndex = this.FileIndex;
+ return sdjson;
+ }
+
+ GetFile() {
+ let file = JSON.parse(LZString.decompressFromUTF16(localStorage.getItem("LogicPartsfile" + this.FileIndex)));
+ return file;
+ }
+
+ SaveFile(container) {
+ let saveState = createSaveState(container,true);
+ console.log(saveState);
+ }
+}
+
+class LocalStorage {
+ constructor() {
+ this._FileIndex = 0;
+ this.RootFolder = new StorageFolder({Name: "ROOT"});
+ if (localStorage.getItem('LogicEngineFileTable')) {
+ let LSFT = json.parse(localStorage.getItem('LogicEngineFileTable'));
+ if (LSFT?.Index >= 0) {
+ this._FileIndex = LSFT.Index;
+ if (LSFT.RootFolder) {
+ this.RootFolder = new StorageFolder(LSFT.RootFolder);
+ }
+ }
+ } else {
+ // No file table yet!
+
+ }
+ }
+}
+
+class LocalBrowser {
+ constructor(folder = undefined) {
+ this.DOMElement = document.getElementById("LocalStorageBrowser");
+ this.DOMPath = document.getElementById("lsbPath");
+ this.DOMFolders = document.getElementById("lsbfoldersContainer");
+ this.DOMFiles = document.getElementById("lsbFilesContainer");
+ this.DOMFolders.innerHTML = "";
+ this.DOMFiles.innerHTML = "Empty Folder";
+ this.Folder = (folder instanceof StorageFolder) ? folder : new StorageFolder({Name: "ROOT"});
+ this.DOMPath.innerHTML = this.Folder.PathString();
+
+ this.redrawFolders();
+ this.redrawFiles();
+ //this.DOMFolders.addEventListener("contextmenu",function(evt){});
+ }
+ redrawFolders() {
+ this.DOMFolders.innerHTML="";
+ for (let a = 0; a < this.Folder.Folders.length; a++) {
+ this.DOMFolders.innerHTML += `
${this.Folder.Folders[a].Name}
`;
+ }
+ if (this.Folder.Folder) this.DOMFolders.innerHTML += `
..
`;
+
+ let folders = this.DOMFolders.getElementsByClassName('lsbFolderEntry');
+ for (let a = 0; a < folders.length; a ++) {
+ let lbo = this;
+ folders[a].addEventListener("click",function(evt){
+ lbo.FolderClick(folders[a].innerText);
+ }.bind(lbo));
+ }
+
+ let pathfolders = this.DOMPath.getElementsByTagName('span');
+ for (let a = 0; a < pathfolders.length; a ++) {
+ let lbo = this;
+ pathfolders[a].addEventListener("click",function(evt){
+ lbo.FolderClick(pathfolders[a].innerText);
+ }.bind(lbo));
+ }
+
+ }
+
+ redrawFiles() {
+ this.DOMFiles.innerHTML="";
+ for (let a = 0; a < this.Folder.Designs.length; a++) {
+ this.DOMFiles.innerHTML += `
${this.Folder.Designs[a].Name}
${new Date(this.Folder.Designs[a].LastModifiedDate)}
?.??KB
`;
+ }
+
+ let files = this.DOMFiles.getElementsByClassName('lsbFileEntry');
+ for (let a = 0; a < files.length; a ++) {
+ let lbo = this;
+ files[a].addEventListener("click",function(evt){
+ lbo.FileClick(files[a].getElementsByClassName("FileName")[0].innerText);
+ }.bind(lbo));
+ }
+ }
+
+ FileClick(file) {
+ console.log(`The file ${file} was clicked from within the folder ${this.Folder.Name}`);
+ }
+ FolderClick(folder) {
+ console.log(`Folder ${folder} was clicked.`);
+ let folderobj = this.Folder.HasFolder(folder);
+ if (folder === ".." || folder === "/") {
+ if (this.Folder.Folder instanceof StorageFolder) this.Folder = this.Folder.Folder;
+ } else {
+ if (folderobj) this.Folder = folderobj;
+ }
+
+ this.DOMPath.innerHTML = this.Folder.PathString();
+ this.redrawFolders();
+ this.redrawFiles();
+ }
+ ShowBrowser(x,y,w,h) {
+ let lsb = this.DOMElement;
+ lsb.style.left = x + "px";
+ lsb.style.top = y + "px";
+ lsb.style.width = w + "px";
+ lsb.style.height = h + "px";
+ lsb.style.display = "flex";
+ }
+}
diff --git a/js/logicengine.js b/js/logicengine.js
index 3d7e615..1bbc291 100644
--- a/js/logicengine.js
+++ b/js/logicengine.js
@@ -550,6 +550,7 @@ class LogicEngine {
}
this.Settings = new LogicEngineSettings(restoresettings);
+ this.LocalStorage = new LocalStorage();
this.FPSCounter = 0;
this.FPS = 0;
this.PotentialFPS = 0;
diff --git a/js/main.js b/js/main.js
index c0d02c2..84419a1 100644
--- a/js/main.js
+++ b/js/main.js
@@ -2,7 +2,7 @@
MatCat BrowserLogic Simulator
*/
-let Version = "0.4.16";
+let Version = "0.5.0";
let spanVersion = document.getElementById("version");
spanVersion.innerText = Version;