diff options
| author | Sebastiano Tronto <sebastiano@tronto.net> | 2025-05-28 12:17:55 +0200 |
|---|---|---|
| committer | Sebastiano Tronto <sebastiano@tronto.net> | 2025-05-28 12:17:55 +0200 |
| commit | fcf5a819ef52ab3069f687e6943599fa3ccf96b3 (patch) | |
| tree | d969a0bb60508f7af0130051c580884cdf3c6d63 | |
| parent | cf2896324e60e2dc3890c658378499383100e79c (diff) | |
| download | nissy-core-fcf5a819ef52ab3069f687e6943599fa3ccf96b3.tar.gz nissy-core-fcf5a819ef52ab3069f687e6943599fa3ccf96b3.zip | |
Web solver is usable
| -rw-r--r-- | web/http/index.html | 21 | ||||
| -rw-r--r-- | web/http/nissyapp.mjs | 134 | ||||
| -rw-r--r-- | web/worker.mjs | 89 |
3 files changed, 222 insertions, 22 deletions
diff --git a/web/http/index.html b/web/http/index.html index c80f7ec..0846ea8 100644 --- a/web/http/index.html +++ b/web/http/index.html | |||
| @@ -9,12 +9,27 @@ | |||
| 9 | <body> | 9 | <body> |
| 10 | <input id="scrambleText" placeholder="Type the scramble here..."> | 10 | <input id="scrambleText" placeholder="Type the scramble here..."> |
| 11 | <select id="solverSelector"> | 11 | <select id="solverSelector"> |
| 12 | <option value="h48h0k4" selected="selected">h48 h=0 k=4 (59Mb)</option> | 12 | <option value="h48h0k4">h48 h=0 k=4 (59Mb)</option> |
| 13 | <option value="h48h3k2">h48 h=3 k=2 (283Mb)</option> | 13 | <option value="h48h3k2" selected="selected">h48 h=3 k=2 (283Mb)</option> |
| 14 | <option value="h48h7k2">h48 h=7 k=2 (3.6Gb)</option> | 14 | <!--option value="h48h7k2">h48 h=7 k=2 (3.6Gb)</option--> |
| 15 | </select> | 15 | </select> |
| 16 | <button id="solveButton">Solve</button> | 16 | <button id="solveButton">Solve</button> |
| 17 | <div id="confirmDownload" style="display:none"> | ||
| 18 | <p id="confirmDownloadText"> | ||
| 19 | For this solver to work, a large data table is required. | ||
| 20 | You can either download it or generate it locally. | ||
| 21 | </p> | ||
| 22 | <p id="confirmDownloadExtraText"> | ||
| 23 | In either case, this data will be stored locally for future use. You | ||
| 24 | can delete it at any time by clearing your browser's data. | ||
| 25 | </p> | ||
| 26 | <button id="confirmDownloadCancel">Cancel</button> | ||
| 27 | <button id="confirmDownloadConfirm">Download</button> | ||
| 28 | <button id="confirmDownloadGenerate">Generate locally</button> | ||
| 29 | </div> | ||
| 17 | <p id="resultsLabel"></p> | 30 | <p id="resultsLabel"></p> |
| 18 | <p id="results"></p> | 31 | <p id="results"></p> |
| 32 | <button id="toggleShowLog">Show nissy log messages</button> | ||
| 33 | <p id="logPane" style="display:none"></p> | ||
| 19 | </body> | 34 | </body> |
| 20 | </html> | 35 | </html> |
diff --git a/web/http/nissyapp.mjs b/web/http/nissyapp.mjs index 23e74d0..b8a525c 100644 --- a/web/http/nissyapp.mjs +++ b/web/http/nissyapp.mjs | |||
| @@ -7,30 +7,149 @@ var scrField = document.getElementById("scrambleText"); | |||
| 7 | var resultsLabel = document.getElementById("resultsLabel"); | 7 | var resultsLabel = document.getElementById("resultsLabel"); |
| 8 | var resultsText = document.getElementById("results"); | 8 | var resultsText = document.getElementById("results"); |
| 9 | var solverSelector = document.getElementById("solverSelector"); | 9 | var solverSelector = document.getElementById("solverSelector"); |
| 10 | var toggleLog = document.getElementById("toggleShowLog"); | ||
| 11 | var logPane = document.getElementById("logPane"); | ||
| 10 | 12 | ||
| 11 | var lastCallbackId = 0; | 13 | var lastCallbackId = 0; |
| 12 | var callbacks = new Map(); // Values: { f: function, arg: object } | 14 | var callbacks = new Map(); // Values: { f: function, arg: object } |
| 13 | var worker = new Worker("./worker.mjs", { type: "module" }); | 15 | var worker = new Worker("./worker.mjs", { type: "module" }); |
| 14 | worker.onmessage = (event) => { | 16 | worker.onmessage = (event) => { |
| 15 | if (event.data.command == "log") { | 17 | if (event.data.command == "log") { |
| 16 | console.log(event.data.object); // TODO | 18 | logPane.innerText += event.data.object; |
| 19 | console.log(event.data.object); | ||
| 17 | } else if (!callbacks.has(event.data.id)) { | 20 | } else if (!callbacks.has(event.data.id)) { |
| 18 | console.log("[nissy app] Unknown callback " + event.data.id + | 21 | console.log("[nissy app] Unknown callback " + event.data.id + |
| 19 | " for command " + event.data.command); | 22 | " for command " + event.data.command); |
| 20 | } else { | 23 | } else { |
| 21 | var callback = callbacks.get(event.data.id); | 24 | var callback = callbacks.get(event.data.id); |
| 25 | callbacks.delete(event.data.id); | ||
| 22 | callback.f(callback.arg, event.data.arg); | 26 | callback.f(callback.arg, event.data.arg); |
| 23 | } | 27 | } |
| 24 | }; | 28 | }; |
| 25 | 29 | ||
| 26 | function updateResults(label, results, buttonState) { | 30 | function updateResults(label, results, enable) { |
| 27 | resultsLabel.innerText = label; | 31 | resultsLabel.innerText = label; |
| 28 | resultsText.innerText = results; | 32 | resultsText.innerText = results; |
| 29 | solveButton.disable = !buttonState; | 33 | solveButton.disable = !enable; |
| 34 | solverSelector.disable = !enable; | ||
| 30 | } | 35 | } |
| 31 | 36 | ||
| 37 | var logVisible = false; | ||
| 38 | toggleLog.addEventListener("click", () => { | ||
| 39 | logVisible = !logVisible; | ||
| 40 | if (logVisible) { | ||
| 41 | logPane.style.display = "block"; | ||
| 42 | toggleLog.innerText = "Hide nissy log messages"; | ||
| 43 | } else { | ||
| 44 | logPane.style.display = "none"; | ||
| 45 | toggleLog.innerText = "Show nissy log messages"; | ||
| 46 | } | ||
| 47 | }); | ||
| 48 | |||
| 32 | solveButton.addEventListener("click", () => { | 49 | solveButton.addEventListener("click", () => { |
| 33 | updateResults("Loading solutions...", "", false); | 50 | const solver = solverSelector.options[solverSelector.selectedIndex].value; |
| 51 | const scramble = scrField.value; | ||
| 52 | loadDataThenSolve(solver, scramble); | ||
| 53 | |||
| 54 | const solveCallbackId = ++lastCallbackId; | ||
| 55 | callbacks.set(solveCallbackId, { | ||
| 56 | f: (callbackArg, loadResult) => { | ||
| 57 | if (loadResult.success) { | ||
| 58 | startSolve(callbackArg.solver, callbackArg.scramble); | ||
| 59 | } else { | ||
| 60 | updateResults("", "", true); | ||
| 61 | } | ||
| 62 | }, | ||
| 63 | arg: { solver: solver, scramble: scramble } | ||
| 64 | }); | ||
| 65 | |||
| 66 | const downloadCallbackId = ++lastCallbackId; | ||
| 67 | }); | ||
| 68 | |||
| 69 | function loadDataThenSolve(solver, scramble) { | ||
| 70 | updateResults("Loading solver " + solver + "...", "", false); | ||
| 71 | |||
| 72 | const callbackId = ++lastCallbackId; | ||
| 73 | callbacks.set(callbackId, { | ||
| 74 | f: (callbackArg, loadResult) => { | ||
| 75 | if (loadResult.success) | ||
| 76 | startSolve(callbackArg.solver, callbackArg.scramble); | ||
| 77 | else | ||
| 78 | askDownloadThenSolve(callbackArg.solver, callbackArg.scramble); | ||
| 79 | }, | ||
| 80 | arg: { solver: solver, scramble: scramble } | ||
| 81 | }); | ||
| 82 | |||
| 83 | worker.postMessage({ | ||
| 84 | command: "load solver data", | ||
| 85 | id: callbackId, | ||
| 86 | arg: solver | ||
| 87 | }); | ||
| 88 | } | ||
| 89 | |||
| 90 | function askDownloadThenSolve(solver, scramble) { | ||
| 91 | var confirmDiv = document.getElementById("confirmDownload"); | ||
| 92 | var cancel = document.getElementById("confirmDownloadCancel"); | ||
| 93 | var download = document.getElementById("confirmDownloadConfirm"); | ||
| 94 | var generate = document.getElementById("confirmDownloadGenerate"); | ||
| 95 | const cleanup = () => { | ||
| 96 | confirmDiv.style.display = "none"; | ||
| 97 | |||
| 98 | // Remove event listeners by cloning the buttons | ||
| 99 | cancel.outerHTML = cancel.outerHTML; | ||
| 100 | download.outerHTML = download.outerHTML; | ||
| 101 | generate.outerHTML = generate.outerHTML; | ||
| 102 | }; | ||
| 103 | |||
| 104 | updateResults("", "", false); | ||
| 105 | confirmDiv.style.display = "block"; | ||
| 106 | |||
| 107 | cancel.addEventListener("click", cleanup); | ||
| 108 | |||
| 109 | download.addEventListener("click", () => { | ||
| 110 | cleanup(); | ||
| 111 | downloadDataThenSolve(solver, scramble); | ||
| 112 | }); | ||
| 113 | |||
| 114 | generate.addEventListener("click", () => { | ||
| 115 | cleanup(); | ||
| 116 | generateDataThenSolve(solver, scramble); | ||
| 117 | }); | ||
| 118 | } | ||
| 119 | |||
| 120 | const downloadDataThenSolve = (solver, scramble) => | ||
| 121 | downloadOrGenerateThenSolve(solver, scramble, true); | ||
| 122 | |||
| 123 | const generateDataThenSolve = (solver, scramble) => | ||
| 124 | downloadOrGenerateThenSolve(solver, scramble, false); | ||
| 125 | |||
| 126 | function downloadOrGenerateThenSolve(solver, scramble, download) { | ||
| 127 | const msg = download ? "Downloading data for " : "Generating data for "; | ||
| 128 | const command = download ? "download solver data" : "generate solver data"; | ||
| 129 | |||
| 130 | updateResults(msg + solver + "...", "", false); | ||
| 131 | |||
| 132 | const callbackId = ++lastCallbackId; | ||
| 133 | callbacks.set(callbackId, { | ||
| 134 | f: (callbackArg, loadResult) => { | ||
| 135 | if (loadResult.success) | ||
| 136 | startSolve(callbackArg.solver, callbackArg.scramble); | ||
| 137 | else | ||
| 138 | updateResults("Failed", loadResult.message, true); | ||
| 139 | }, | ||
| 140 | arg: { solver: solver, scramble: scramble } | ||
| 141 | }); | ||
| 142 | |||
| 143 | worker.postMessage({ | ||
| 144 | command: command, | ||
| 145 | id: callbackId, | ||
| 146 | arg: solver | ||
| 147 | }); | ||
| 148 | } | ||
| 149 | |||
| 150 | function startSolve(solver, scramble) { | ||
| 151 | updateResults("Solving...", "", false); | ||
| 152 | |||
| 34 | const callbackId = ++lastCallbackId; | 153 | const callbackId = ++lastCallbackId; |
| 35 | callbacks.set(callbackId, { | 154 | callbacks.set(callbackId, { |
| 36 | f: (callbackArg, solveResult) => { | 155 | f: (callbackArg, solveResult) => { |
| @@ -46,12 +165,13 @@ solveButton.addEventListener("click", () => { | |||
| 46 | }, | 165 | }, |
| 47 | arg: "" // Currently unused | 166 | arg: "" // Currently unused |
| 48 | }); | 167 | }); |
| 168 | |||
| 49 | worker.postMessage({ | 169 | worker.postMessage({ |
| 50 | command: "solve", | 170 | command: "solve", |
| 51 | id: callbackId, | 171 | id: callbackId, |
| 52 | arg: { | 172 | arg: { |
| 53 | solver: solverSelector.options[solverSelector.selectedIndex].value, | 173 | solver: solver, |
| 54 | scramble: scrField.value, | 174 | scramble: scramble, |
| 55 | minmoves: 0, | 175 | minmoves: 0, |
| 56 | maxmoves: 20, | 176 | maxmoves: 20, |
| 57 | maxsolutions: 1, | 177 | maxsolutions: 1, |
| @@ -59,4 +179,4 @@ solveButton.addEventListener("click", () => { | |||
| 59 | threads: window.navigator.hardwareConcurrency, | 179 | threads: window.navigator.hardwareConcurrency, |
| 60 | } | 180 | } |
| 61 | }); | 181 | }); |
| 62 | }); | 182 | } |
diff --git a/web/worker.mjs b/web/worker.mjs index dca980e..ceceaec 100644 --- a/web/worker.mjs +++ b/web/worker.mjs | |||
| @@ -8,8 +8,10 @@ function log(message) { | |||
| 8 | nissy.setLogger(nissy._addCallbackFunction(log)); | 8 | nissy.setLogger(nissy._addCallbackFunction(log)); |
| 9 | 9 | ||
| 10 | const commands = [ | 10 | const commands = [ |
| 11 | { name: "solve", exec: solve }, | 11 | { name: "load solver data", exec: loadSolverDataFromStorage }, |
| 12 | { name: "download solver data", exec: downloadSolverData }, | 12 | { name: "download solver data", exec: downloadSolverData }, |
| 13 | { name: "generate solver data", exec: generateSolverData }, | ||
| 14 | { name: "solve", exec: solve }, | ||
| 13 | ]; | 15 | ]; |
| 14 | 16 | ||
| 15 | // Message structure: | 17 | // Message structure: |
| @@ -25,6 +27,7 @@ onmessage = (event) => { | |||
| 25 | for (var i = 0; i < commands.length; i++) { | 27 | for (var i = 0; i < commands.length; i++) { |
| 26 | if (commands[i].name == event.data.command) { | 28 | if (commands[i].name == event.data.command) { |
| 27 | commands[i].exec(event.data.id, event.data.arg); | 29 | commands[i].exec(event.data.id, event.data.arg); |
| 30 | console.log("[worker] Received '" + commands[i].name + "'"); | ||
| 28 | return; | 31 | return; |
| 29 | } | 32 | } |
| 30 | } | 33 | } |
| @@ -32,10 +35,41 @@ onmessage = (event) => { | |||
| 32 | log("[nissy worker] unknown command " + event.data.command); | 35 | log("[nissy worker] unknown command " + event.data.command); |
| 33 | }; | 36 | }; |
| 34 | 37 | ||
| 38 | // Load solver data from storage. | ||
| 39 | // Argument: string (the name of the solver) | ||
| 40 | async function loadSolverDataFromStorage(id, solver) { | ||
| 41 | const async_return = (success, message = "") => postMessage({ | ||
| 42 | command: "load solver data", | ||
| 43 | id: id, | ||
| 44 | arg: { | ||
| 45 | success: success, | ||
| 46 | message: message | ||
| 47 | } | ||
| 48 | }); | ||
| 49 | |||
| 50 | if (!(await nissy.isSolverAvailable(solver))) { | ||
| 51 | async_return(false, "Error: solver " + arg.solver + | ||
| 52 | " is not available in this version of nissy."); | ||
| 53 | return; | ||
| 54 | } | ||
| 55 | |||
| 56 | if (await nissy.isSolverLoaded(solver)) { | ||
| 57 | async_return(true); | ||
| 58 | return; | ||
| 59 | } | ||
| 60 | |||
| 61 | if (await nissy.initSolverFromStorage(solver)) { | ||
| 62 | async_return(true); | ||
| 63 | } else { | ||
| 64 | async_return(false, "Could not read data for " + solver + | ||
| 65 | " from local storage"); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 35 | // Download solver data. | 69 | // Download solver data. |
| 36 | // Argument: string | 70 | // Argument: string (the name of the solver) |
| 37 | async function downloadSolverData(id, solver) { | 71 | async function downloadSolverData(id, solver) { |
| 38 | const downloadSolverDataReturn = (success, message = "") => postMessage({ | 72 | const async_return = (success, message = "") => postMessage({ |
| 39 | command: "download solver data", | 73 | command: "download solver data", |
| 40 | id: id, | 74 | id: id, |
| 41 | arg: { | 75 | arg: { |
| @@ -44,10 +78,41 @@ async function downloadSolverData(id, solver) { | |||
| 44 | } | 78 | } |
| 45 | }); | 79 | }); |
| 46 | 80 | ||
| 47 | if (await nissy.initSolverDownload(arg.solver, "/tables")) { | 81 | if (!(await nissy.isSolverAvailable(solver))) { |
| 48 | downloadSolverDataReturn(true); | 82 | async_return(false, "Error: solver " + arg.solver + |
| 83 | " is not available in this version of nissy."); | ||
| 84 | return; | ||
| 85 | } | ||
| 86 | |||
| 87 | if (await nissy.initSolverDownload(solver, "/tables")) { | ||
| 88 | async_return(true); | ||
| 89 | } else { | ||
| 90 | async_return(false, "Error retrieving solver data"); | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | // Generate solver data locally. | ||
| 95 | // Argument: string (the name of the solver) | ||
| 96 | async function generateSolverData(id, solver) { | ||
| 97 | const async_return = (success, message = "") => postMessage({ | ||
| 98 | command: "generate solver data", | ||
| 99 | id: id, | ||
| 100 | arg: { | ||
| 101 | success: success, | ||
| 102 | message: message | ||
| 103 | } | ||
| 104 | }); | ||
| 105 | |||
| 106 | if (!(await nissy.isSolverAvailable(solver))) { | ||
| 107 | async_return(false, "Error: solver " + arg.solver + | ||
| 108 | " is not available in this version of nissy."); | ||
| 109 | return; | ||
| 110 | } | ||
| 111 | |||
| 112 | if (await nissy.initSolverGenerate(solver)) { | ||
| 113 | async_return(true); | ||
| 49 | } else { | 114 | } else { |
| 50 | downloadSolverDataReturn(false, "Error retrieving the solver data"); | 115 | async_return(false, "Error generating solver data"); |
| 51 | } | 116 | } |
| 52 | } | 117 | } |
| 53 | 118 | ||
| @@ -62,7 +127,7 @@ async function downloadSolverData(id, solver) { | |||
| 62 | // threads: number | 127 | // threads: number |
| 63 | // } | 128 | // } |
| 64 | async function solve(id, arg) { | 129 | async function solve(id, arg) { |
| 65 | const solveReturn = (success, solutions = [], message = "") => postMessage({ | 130 | const async_return = (success, solutions = [], message = "") => postMessage({ |
| 66 | command: "solve", | 131 | command: "solve", |
| 67 | id: id, | 132 | id: id, |
| 68 | arg: { | 133 | arg: { |
| @@ -73,14 +138,14 @@ async function solve(id, arg) { | |||
| 73 | }); | 138 | }); |
| 74 | 139 | ||
| 75 | if (!(await nissy.isSolverAvailable(arg.solver))) { | 140 | if (!(await nissy.isSolverAvailable(arg.solver))) { |
| 76 | solveReturn(false, [], "Error: solver " + arg.solver + | 141 | async_return(false, [], "Error: solver " + arg.solver + |
| 77 | " is not available in this version of nissy."); | 142 | " is not available in this version of nissy."); |
| 78 | return; | 143 | return; |
| 79 | } | 144 | } |
| 80 | 145 | ||
| 81 | if (!(await nissy.isSolverLoaded(arg.solver)) && | 146 | if (!(await nissy.isSolverLoaded(arg.solver)) && |
| 82 | !(await nissy.initSolverFromStorage(arg.solver))) { | 147 | !(await nissy.initSolverFromStorage(arg.solver))) { |
| 83 | solveReturn(false, [], "Error: solver " + arg.solver + " has not been " + | 148 | async_return(false, [], "Error: solver " + arg.solver + " has not been " + |
| 84 | "loaded. Its data must be donwloaded or generated before using it."); | 149 | "loaded. Its data must be donwloaded or generated before using it."); |
| 85 | return; | 150 | return; |
| 86 | } | 151 | } |
| @@ -106,9 +171,9 @@ async function solve(id, arg) { | |||
| 106 | //TODO: add solution lenght? | 171 | //TODO: add solution lenght? |
| 107 | var solutions = result.solutions == "" ? [] : result.solutions.split("\n"); | 172 | var solutions = result.solutions == "" ? [] : result.solutions.split("\n"); |
| 108 | solutions.pop(); // Solver always returns string ending in newline | 173 | solutions.pop(); // Solver always returns string ending in newline |
| 109 | solveReturn(true, solutions); | 174 | async_return(true, solutions); |
| 110 | } else { | 175 | } else { |
| 111 | solveReturn(false, [], | 176 | async_return(false, [], |
| 112 | "Error while solving (error " + result.err.value + ")"); | 177 | "Error while solving (error " + result.err.value + ")"); |
| 113 | } | 178 | } |
| 114 | }; | 179 | }; |
