diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a1b63682..a0eb3feb 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "dbaeumer.vscode-eslint", + "firefox-devtools.vscode-firefox-debug", "streetsidesoftware.code-spell-checker" ] } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 9cfcde95..f94732cc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,20 +4,27 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Chrome Vite Debug", + "url": "http://localhost:5173/", + "webRoot": "${workspaceRoot}/src", + //"sourceMaps": true, + }, { "type": "msedge", - "name": "Edge Vite Debug", "request": "launch", + "name": "Edge Vite Debug", "url": "http://localhost:5173/", "webRoot": "${workspaceFolder}/src" }, { - "type": "chrome", + "type": "firefox", "request": "launch", - "name": "Chrome Vite Debug", + "name": "Firefox Vite Debug", "url": "http://localhost:5173/", - "webRoot": "${workspaceRoot}/src", - //"sourceMaps": true, - } + "webRoot": "${workspaceFolder}/src" + }, ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 185efcdc..412a7698 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "file-saver": "^2.0.5", "fork-awesome": "^1.2.0", "theme-change": "^2.5.0" }, "devDependencies": { + "@types/file-saver": "^2.0.7", "@types/node": "20.12.8", "@types/wicg-file-system-access": "^2023.10.5", "gts": "^5.3.0", @@ -868,6 +870,12 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2122,6 +2130,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", diff --git a/package.json b/package.json index 493781b0..f8fc6251 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "homepage": "https://github.com/RAIRLab/PeirceMyHeart#readme", "devDependencies": { + "@types/file-saver": "^2.0.7", "@types/node": "20.12.8", "@types/wicg-file-system-access": "^2023.10.5", "gts": "^5.3.0", @@ -43,6 +44,7 @@ "vitest": "^1.6.0" }, "dependencies": { + "file-saver": "^2.0.5", "fork-awesome": "^1.2.0", "theme-change": "^2.5.0" } diff --git a/src/AEG-IO.ts b/src/AEG-IO.ts index bca608e6..e79e9322 100644 --- a/src/AEG-IO.ts +++ b/src/AEG-IO.ts @@ -4,6 +4,10 @@ * @author Anusha Tiwari */ +import {TreeContext} from "./TreeContext"; +import {redrawProof, redrawTree} from "./SharedToolUtils/DrawUtils"; +import {appendStep} from "./ProofHistory/ProofHistory"; + import {AEGTree} from "./AEG/AEGTree"; import {AtomNode} from "./AEG/AtomNode"; import {CutNode} from "./AEG/CutNode"; @@ -11,6 +15,9 @@ import {Ellipse} from "./AEG/Ellipse"; import {Point} from "./AEG/Point"; import {ProofModeMove, ProofModeNode} from "./ProofHistory/ProofModeNode"; +//Cross-browser file saving library. +import FileSaver from "file-saver"; + /** * Describes The Sheet of Assertion in JSON files. */ @@ -146,7 +153,7 @@ function toCut(cutData: cutObj): CutNode { } /** - * Parses the incoming AtomObject and returns and equivalent AtomNode. + * Parses the incoming AtomObject and returns an equivalent AtomNode. * * @param atomData Incoming AtomObject. * @returns AtomNode equivalent of atomData. @@ -158,3 +165,118 @@ function toAtom(atomData: atomObj): AtomNode { return new AtomNode(identifier, origin, atomData.internalWidth, atomData.internalHeight); } + +/** + * Creates and returns the json string of the given AEG Tree object. + * Uses tab characters as delimiters. + * + * @param treeData An AEG Tree object. + * @returns json string of treeData. + */ +export function aegJsonString(treeData: AEGTree | ProofModeNode[]): string { + return JSON.stringify(treeData, null, "\t"); +} + +/** + * Calls appropriate methods to save the current AEGTree as a file. + */ +export async function saveMode(): Promise { + let name: string; + let data: AEGTree | ProofModeNode[]; + + if (TreeContext.modeState === "Draw") { + name = TreeContext.tree.toString(); + data = TreeContext.tree; + } else { + if (TreeContext.proof.length === 1) { + name = "One-Step Proof"; + } else { + name = + TreeContext.proof[0].tree.toString() + + " PROVES " + + TreeContext.getLastProofStep().tree.toString(); + } + data = TreeContext.proof; + } + + //Errors caused by file handler or HTML download element should not be displayed. + try { + //Dialog based download + if ("showSaveFilePicker" in window) { + const saveHandle = await window.showSaveFilePicker({ + excludeAcceptAllOption: true, + suggestedName: name, + startIn: "downloads", + types: [{accept: {"text/json": [".json"]}}], + }); + saveFile(saveHandle, data); + } else { + //Fallback to immediate download if showSaveFilePicker is not supported. + const blob = new Blob([aegJsonString(data)], {type: "text/json"}); + FileSaver(blob, name + ".json"); + } + } catch (error) { + //Catch error but do nothing. Discussed in Issue #247. + } +} + +function readFile(file: File) { + const reader = new FileReader(); + reader.addEventListener("load", () => { + const aegData = reader.result; + if (typeof aegData === "string") { + const loadData = loadFile(TreeContext.modeState, aegData); + if (TreeContext.modeState === "Draw") { + //Loads data. + TreeContext.tree = loadData as AEGTree; + //Redraws tree which is now the parsed loadData. + redrawTree(TreeContext.tree); + } else if (TreeContext.modeState === "Proof") { + //Clears current proof. + TreeContext.clearProof(); + //Loads data for the new proof. + TreeContext.proof = loadData as ProofModeNode[]; + //Removes default start step. + document.getElementById("Row: 1")?.remove(); + //Adds button for each step of the loaded proof to the history bar. + for (let i = 0; i < TreeContext.proof.length; i++) { + appendStep(TreeContext.proof[i], i + 1); + } + TreeContext.currentProofStep = TreeContext.proof[TreeContext.proof.length - 1]; + redrawProof(); + } + } else { + console.log("Loading failed because reading the file was unsuccessful."); + } + }); + reader.readAsText(file); +} + +/** + * Calls the appropriate methods to load files and convert them to equivalent AEGTrees. + */ +export async function loadMode(): Promise { + try { + if ("showOpenFilePicker" in window) { + const [fileHandle] = await window.showOpenFilePicker({ + excludeAcceptAllOption: true, + multiple: false, + startIn: "downloads", + types: [{accept: {"text/json": [".json"]}}], + }); + const file = await fileHandle.getFile(); + readFile(file); + } else { + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = ".json"; + fileInput.addEventListener("change", () => { + const file = fileInput.files?.item(0); + readFile(file!); + }); + fileInput.click(); + } + } catch (error) { + //Do nothing. + } +} diff --git a/src/SharedToolUtils/DrawUtils.ts b/src/SharedToolUtils/DrawUtils.ts index 1fbecdcb..dc100a2a 100644 --- a/src/SharedToolUtils/DrawUtils.ts +++ b/src/SharedToolUtils/DrawUtils.ts @@ -5,7 +5,7 @@ * @author Anusha Tiwari */ -import {aegStringify} from "../index"; +import {aegJsonString} from "../AEG-IO"; import {AEGTree} from "../AEG/AEGTree"; import {AtomNode} from "../AEG/AtomNode"; import {CutNode} from "../AEG/CutNode"; @@ -174,7 +174,7 @@ export function redrawTree(tree: AEGTree, color?: string): void { cutDisplay.innerHTML = tree.toString(); cleanCanvas(); redrawCut(tree.sheet, color); - window.treeString = aegStringify(tree); + window.treeString = aegJsonString(tree); } /** diff --git a/src/index.ts b/src/index.ts index 106e6169..6e92d3a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,10 +8,8 @@ */ import {AEGTree} from "./AEG/AEGTree"; -import {appendStep} from "./ProofHistory/ProofHistory"; -import {loadFile, saveFile} from "./AEG-IO"; +import {loadMode, saveMode, aegJsonString} from "./AEG-IO"; import {ProofModeNode} from "./ProofHistory/ProofModeNode"; -import {redrawProof, redrawTree} from "./SharedToolUtils/DrawUtils"; import {toggleHandler} from "./ToggleModes"; import {Tool, TreeContext} from "./TreeContext"; @@ -75,11 +73,11 @@ let hasMouseIn = true; //Global window exports. //TODO: move these under the global import window.tree = TreeContext.tree; -window.treeString = aegStringify(window.tree); +window.treeString = aegJsonString(window.tree); window.atomTool = Tool.atomTool; window.cutTool = Tool.cutTool; window.dragTool = Tool.dragTool; -window.aegStringify = aegStringify; +window.aegJsonString = aegJsonString; window.saveMode = saveMode; window.loadMode = loadMode; window.drawMoveSingleTool = Tool.drawMoveSingleTool; @@ -115,7 +113,7 @@ declare global { dragTool: Tool; saveMode: () => void; loadMode: () => void; - aegStringify: (treeData: AEGTree | ProofModeNode[]) => string; + aegJsonString: (treeData: AEGTree | ProofModeNode[]) => string; drawMoveSingleTool: Tool; drawMoveMultiTool: Tool; copySingleTool: Tool; @@ -195,121 +193,6 @@ function setTool(state: Tool): void { break; } } -/** - * Creates and returns the stringification of the incoming data. Uses tab characters as delimiters. - * - * @param treeData Incoming data. - * @returns Stringification of treeData. - */ -export function aegStringify(treeData: AEGTree | ProofModeNode[]): string { - return JSON.stringify(treeData, null, "\t"); -} - -/** - * Calls appropriate methods to save the current AEGTree as a file. - */ -async function saveMode(): Promise { - let name: string; - let data: AEGTree | ProofModeNode[]; - - if (TreeContext.modeState === "Draw") { - name = "AEG Tree"; - data = TreeContext.tree; - } else { - if (TreeContext.proof.length === 1) { - name = "One-Step Proof"; - } else { - name = - TreeContext.proof[0].tree.toString() + - " PROVES " + - TreeContext.getLastProofStep().tree.toString(); - } - data = TreeContext.proof; - } - - //Errors caused by file handler or HTML download element should not be displayed. - try { - //Slow Download... - if ("showSaveFilePicker" in window) { - const saveHandle = await window.showSaveFilePicker({ - excludeAcceptAllOption: true, - suggestedName: name, - startIn: "downloads", - types: [ - { - description: "JSON Files", - accept: { - "text/json": [".json"], - }, - }, - ], - }); - saveFile(saveHandle, data); - } else { - //Quick Download... - const f = document.createElement("a"); - f.href = aegStringify(data); - f.download = name + ".json"; - f.click(); - } - } catch (error) { - //Catch error but do nothing. Discussed in Issue #247. - } -} - -/** - * Calls the appropriate methods to load files and convert them to equivalent AEGTrees. - */ -async function loadMode(): Promise { - try { - const [fileHandle] = await window.showOpenFilePicker({ - excludeAcceptAllOption: true, - multiple: false, - startIn: "downloads", - types: [ - { - description: "JSON Files", - accept: { - "text/json": [".json"], - }, - }, - ], - }); - - const file = await fileHandle.getFile(); - const reader = new FileReader(); - reader.addEventListener("load", () => { - const aegData = reader.result; - if (typeof aegData === "string") { - const loadData = loadFile(TreeContext.modeState, aegData); - if (TreeContext.modeState === "Draw") { - //Loads data. - TreeContext.tree = loadData as AEGTree; - //Redraws tree which is now the parsed loadData. - redrawTree(TreeContext.tree); - } else if (TreeContext.modeState === "Proof") { - //Clears current proof. - TreeContext.clearProof(); - //Loads data for the new proof. - TreeContext.proof = loadData as ProofModeNode[]; - //Removes default start step. - document.getElementById("Row: 1")?.remove(); - //Adds button for each step of the loaded proof to the history bar. - for (let i = 0; i < TreeContext.proof.length; i++) { - appendStep(TreeContext.proof[i], i + 1); - } - TreeContext.currentProofStep = TreeContext.proof[TreeContext.proof.length - 1]; - redrawProof(); - } - } else { - console.log("Loading failed because reading the file was unsuccessful."); - } - }); - reader.readAsText(file); - } catch (error) { - //Do nothing. - } -} /** * Handles CTRL+Z undo operations according to whether the program is in Draw or Proof Mode.