h48

A prototype for an optimal Rubik's cube solver, work in progress.
git clone https://git.tronto.net/h48
Download | Log | Files | Refs | README | LICENSE

commit f91e5c4c83a071ab720417d490fd44a8f4678bc1
parent c182f3f16e56e9533060ffa06b25391e239a28ee
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date:   Thu, 17 Apr 2025 16:27:38 +0200

Improvements to QT UI

Diffstat:
Mqt/CMakeLists.txt | 2+-
Aqt/Main.qml | 332+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dqt/NissyMain.qml | 261-------------------------------------------------------------------------------
Mqt/adapter.cpp | 33+++++++++++++++++++++++++++------
Mqt/adapter.h | 7+++++--
Mqt/main.cpp | 2+-
6 files changed, 366 insertions(+), 271 deletions(-)

diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt @@ -19,7 +19,7 @@ qt_add_executable(appnissyqt qt_add_qml_module(appnissyqt URI nissyqt VERSION 1.0 - QML_FILES NissyMain.qml + QML_FILES Main.qml ) target_link_libraries(appnissyqt diff --git a/qt/Main.qml b/qt/Main.qml @@ -0,0 +1,332 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Window { + id: mainWindow + + width: 800 + height: 600 + visible: true + title: "Nissy 3.0 - Preview" + + SplitView { + id: splitView + anchors.fill: parent + orientation: Qt.Vertical + + handle: Rectangle { + implicitHeight: 3 + color: SplitHandle.hovered ? "black" : "#AAAAAA" + } + + component MyScrollBar: ScrollBar { + orientation: Qt.Vertical + size: parent.height + policy: ScrollBar.AlwaysOn + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + contentItem: Rectangle { + implicitWidth: 4 + radius: implicitWidth/2 + color: "black" + } + background: Rectangle { + implicitWidth: 4 + radius: implicitWidth/2 + color: "#AAAAAA" + } + } + + ColumnLayout { + id: mainArea + + property alias scramble: scrambleRow.scramble + property alias solver: solverCfg.solver + property alias minmoves: solverCfg.minmoves + property alias maxmoves: solverCfg.maxmoves + property alias maxsolutions: solverCfg.maxsolutions + property alias optimal: solverCfg.optimal + property alias sols: sols.text + property alias solsHeader: solsHeader.text + + property bool solutionsLoading: false + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: logView.top + anchors.margins: 6 + spacing: 10 + + SplitView.minimumHeight: 180 + SplitView.preferredHeight: 500 + + component Separator: Rectangle { + height: 1 + Layout.fillWidth: true + color: "black" + } + + component OptionalValue: RowLayout { + property alias currentValue: valueRect.value + property alias from: spinBox.from + property alias to: spinBox.to + property alias defaultValue: spinBox.value + property alias defaultEnabled: sw.checked + property alias label: sw.text + property int defaultSavedValue: 1 + property int savedValue: defaultSavedValue + + Switch { + id: sw + + checked: true + + onToggled: () => { + if (checked) { + currentValue = savedValue + } else { + savedValue = currentValue + currentValue = spinBox.to + } + } + } + + Rectangle { + id: valueRect + + property alias enabled: sw.checked + property alias value: spinBox.value + + width: 65 + height: 20 + + SpinBox { + id: spinBox + + width: parent.width + editable: true + enabled: parent.enabled + } + } + } + + ColumnLayout { + id: scrambleRow + + property alias scramble: scrambleRowLayout.scramble + + RowLayout { + id: scrambleRowLayout + + property alias scramble: scrambleEditor.text + + spacing: 6 + + TextField { + id: scrambleEditor + + placeholderText: "Enter scramble here" + Layout.fillWidth: true + padding: 4 + + readonly property bool empty: text.trim().length == 0 + readonly property bool valid: NissyAdapter.isValidScramble(text) + + onAccepted: if (!empty && valid) submitScramble() + } + + Button { + id: solveButton + + enabled: !scrambleEditor.empty && scrambleEditor.valid && + !mainArea.solutionsLoading + text: "Solve!" + + onPressed: submitScramble() + } + } + + Label { + id: invalidScrambleWarning + text: scrambleEditor.empty || scrambleEditor.valid ? + "" : "Invalid Scramble" + } + } + + Separator {} + + ColumnLayout { + id: solverCfg + + property alias minmoves: minMaxRow.min + property alias maxmoves: minMaxRow.max + property alias maxsolutions: maxSols.currentValue + property alias optimal: optimal.currentValue + property alias solver: solverRow.solver + + RowLayout { + id: solverRow + + property alias solver: comboBox.currentValue + + Label { text: "Solver" } + ComboBox { + id: comboBox + + currentIndex: 3 + textRole: "text" + valueRole: "name" + implicitContentWidthPolicy: ComboBox.WidestTextWhenCompleted + + model: ListModel { + ListElement { text: "h48 h=0, k=4 (59 Mb)"; name: "h48h0k4" } + ListElement { text: "h48 h=1, k=2 (115 Mb)"; name: "h48h1k2" } + ListElement { text: "h48 h=2, k=2 (171 Mb)"; name: "h48h2k2" } + ListElement { text: "h48 h=3, k=2 (283 Mb)"; name: "h48h3k2" } + ListElement { text: "h48 h=7, k=2 (3.6 Gb)"; name: "h48h7k2" } + } + } + } + + RowLayout { + id: minMaxRow + + property alias min: slider.min + property alias max: slider.max + + Rectangle { + width: 100 + height: 20 + Label { text: "Min moves: " + slider.min } + } + RangeSlider { + id: slider + from: 0 + to: 20 + first.value: from + second.value: to + stepSize: 1 + snapMode: RangeSlider.SnapAlways + + readonly property int min: Math.round(first.value) + readonly property int max: Math.round(second.value) + } + Rectangle { + width: 100 + height: 20 + Label { text: "Max moves: " + slider.max } + } + } + + OptionalValue { + id: optimal + + label: "Above optimal by at most" + from: 0 + to: 20 + defaultValue: 20 + defaultEnabled: false + defaultSavedValue: 0 + } + + OptionalValue { + id: maxSols + + label: "Limit number of solutions to" + from: 1 + to: 999 + defaultValue: 1 + defaultEnabled: true + defaultSavedValue: 1 + } + } + + Separator {} + + StackLayout { + Layout.maximumHeight: 30 + currentIndex: mainArea.solutionsLoading ? 0 : 1 + + BusyIndicator { running: mainArea.solutionsLoading } + Label { id: solsHeader } + } + + ScrollView { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.bottomMargin: 10 + ScrollBar.vertical: MyScrollBar {} + + TextEdit { + id: sols + readOnly: true + font.family: "Monospace" + } + } + } + + ScrollView { + id: logView + + property alias text: logText.text + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 6 + + SplitView.preferredHeight: 300 + + background: Rectangle { + color: "#404040" + radius: 4 + } + + ScrollBar.vertical: MyScrollBar { + id: scrollBar + position: 1.0 - size + } + Label { + id: logText + + font.family: "Monospace" + color: "white" + } + } + } + + function submitScramble() { + mainArea.solutionsLoading = true; + mainArea.solsHeader = "" + mainArea.sols = "" + logView.text = "" + NissyAdapter.requestSolve( + mainArea.scramble, + mainArea.solver, + mainArea.minmoves, + mainArea.maxmoves, + mainArea.maxsolutions, + mainArea.optimal + ) + } + + Connections { + target: NissyAdapter + function onSolutionsReady(header, sols) { + mainArea.solutionsLoading = false + mainArea.solsHeader = header + mainArea.sols = sols + } + function onSolverError(msg) { + mainArea.solutionsLoading = false + mainArea.solsHeader = msg + mainArea.sols = "" + } + function onAppendLog(msg) { + logView.text += msg + } + } +} diff --git a/qt/NissyMain.qml b/qt/NissyMain.qml @@ -1,261 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -Window { - id: mainWindow - - width: 800 - height: 600 - visible: true - title: "Nissy 3.0 - Preview" - - SplitView { - id: splitView - anchors.fill: parent - orientation: Qt.Vertical - - handle: Rectangle { - implicitHeight: 3 - color: SplitHandle.hovered ? "black" : "#AAAAAA" - } - - component MyScrollBar: ScrollBar { - orientation: Qt.Vertical - size: parent.height - policy: ScrollBar.AlwaysOn - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - contentItem: Rectangle { - implicitWidth: 4 - radius: implicitWidth/2 - color: "black" - } - background: Rectangle { - implicitWidth: 4 - radius: implicitWidth/2 - color: "#AAAAAA" - } - } - - ColumnLayout { - id: mainArea - - property alias scramble: scrambleRow.scramble - property alias minmoves: solverCfg.minmoves - property alias maxmoves: solverCfg.maxmoves - property alias maxsolutions: solverCfg.maxsolutions - property alias sols: sols.text - property alias solsHeader: solsHeader.text - - property bool solutionsLoading: false - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: logView.top - anchors.margins: 6 - spacing: 10 - - SplitView.minimumHeight: 180 - SplitView.preferredHeight: 300 - - component Separator: Rectangle { - height: 1 - Layout.fillWidth: true - color: "black" - } - - ColumnLayout { - id: scrambleRow - - property alias scramble: scrambleRowLayout.scramble - - RowLayout { - id: scrambleRowLayout - - property alias scramble: scrambleEditor.text - - spacing: 6 - - TextField { - id: scrambleEditor - - placeholderText: "Enter scramble here" - Layout.fillWidth: true - padding: 4 - - readonly property bool empty: text.trim().length == 0 - readonly property bool valid: NissyAdapter.isValidScramble(text) - - onAccepted: if (!empty && valid) submitScramble() - } - - Button { - id: solveButton - - enabled: !scrambleEditor.empty && scrambleEditor.valid && - !mainArea.solutionsLoading - text: "Solve!" - - onPressed: submitScramble() - } - } - - Label { - id: invalidScrambleWarning - text: scrambleEditor.empty || scrambleEditor.valid ? - "" : "Invalid Scramble" - } - } - - Separator {} - - ColumnLayout { - id: solverCfg - property alias minmoves: minMaxRow.min - property alias maxmoves: minMaxRow.max - property alias maxsolutions: maxOptimal.maxsolutions - - RowLayout { - id: minMaxRow - - property alias min: slider.min - property alias max: slider.max - - Rectangle { - width: 100 - height: 20 - Label { text: "Min moves: " + slider.min } - } - RangeSlider { - id: slider - from: 0 - to: 20 - first.value: from - second.value: to - stepSize: 1 - snapMode: RangeSlider.SnapAlways - - readonly property int min: Math.round(first.value) - readonly property int max: Math.round(second.value) - } - Rectangle { - width: 100 - height: 20 - Label { text: "Max moves: " + slider.max } - } - } - - RowLayout { - id: maxOptimal - - property alias maxsolutions: maxSolsRect.maxsolutions - - Rectangle { - width: 220 - height: 20 - Label { text: "Maximum number of solutions:" } - } - Rectangle { - id: maxSolsRect - - property int maxsolutions: parseInt(textField.text) - - width: 35 - height: 20 - - TextField { - id: textField - - width: parent.width - text: "1" - validator: IntValidator{ bottom: 1; top: 999; } - - onAccepted: submitScramble() - } - } - } - } - - Separator {} - - Label { id: solsHeader } - - ScrollView { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.bottomMargin: 10 - ScrollBar.vertical: MyScrollBar {} - - TextEdit { - id: sols - readOnly: true - font.family: "Monospace" - } - } - } - - ScrollView { - id: logView - - property alias text: logText.text - - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 6 - - SplitView.preferredHeight: 300 - - background: Rectangle { - color: "#404040" - radius: 4 - } - - ScrollBar.vertical: MyScrollBar { - id: scrollBar - position: 1.0 - size - } - Label { - id: logText - - font.family: "Monospace" - color: "white" - } - } - } - - function submitScramble() { - mainArea.solutionsLoading = true; - mainArea.solsHeader = "Loading solutions..." - mainArea.sols = "" - logView.text = "" - NissyAdapter.requestSolve( - mainArea.scramble, - "h48h3k2", - mainArea.minmoves, - mainArea.maxmoves, - mainArea.maxsolutions, - -1 - ) - } - - Connections { - target: NissyAdapter - function onSolutionsReady(header, sols) { - mainArea.solutionsLoading = false - mainArea.solsHeader = header - mainArea.sols = sols - } - function onSolverError(msg) { - mainArea.solutionsLoading = false - mainArea.solusHeader = msg - mainArea.sols = "" - } - function onAppendLog(msg) { - logView.text += msg - } - } -} diff --git a/qt/adapter.cpp b/qt/adapter.cpp @@ -9,20 +9,25 @@ void logWrapper(const char *str, void *data) { - auto f = *reinterpret_cast<std::function<void(const char*)>*>(data); - f(str); + auto f = *reinterpret_cast<std::function<void(std::string)>*>(data); + f(std::string{str}); } NissyAdapter::NissyAdapter() { + // TODO: this list must be kept in sync with UI code, it is a bit ugly std::vector<std::string> solverNames { - "h48h3k2" + "h48h0k4", + "h48h1k2", + "h48h2k2", + "h48h3k2", + "h48h7k2", }; for (auto s : solverNames) initSolver(s); - writeLog = [&](const char *str) { + writeLog = [&](std::string str) { emit appendLog(QString::fromStdString(str)); }; @@ -45,6 +50,8 @@ bool NissyAdapter::loadSolverData(nissy::solver& solver) { std::filesystem::path filePath("./tables/" + solver.id); if (!std::filesystem::exists(filePath)) { + logLine("Data file for solver " + solver.name + " not found, " + "generating it..."); auto err = solver.generate_data(); if (!err.ok()) { emit solverError(QString("Error generating data!")); @@ -55,16 +62,23 @@ bool NissyAdapter::loadSolverData(nissy::solver& solver) { ofs.write(reinterpret_cast<char *>(solver.data.data()), solver.size); ofs.close(); + logLine("Data generated succesfully"); } else { + logLine("Reading data for solver " + solver.name + + " from file"); std::ifstream ifs(filePath, std::ios::binary); solver.read_data(ifs); ifs.close(); + logLine("Data loaded"); } + logLine("Checking data integrity " + "(this is done only once per solver per session)..."); if (!solver.check_data().ok()) { emit solverError(QString("Error reading data!")); return false; } + logLine("Data checked"); return true; } @@ -102,7 +116,7 @@ Q_INVOKABLE void NissyAdapter::requestSolve( } SolveOptions opts{c, ss, (unsigned)minmoves, (unsigned)maxmoves, - (unsigned)maxsolutions, optimal}; + (unsigned)maxsolutions, (unsigned)optimal}; auto _ = QtConcurrent::run(&NissyAdapter::startSolve, this, opts); return; } @@ -132,9 +146,16 @@ void NissyAdapter::startSolve(SolveOptions opts) std::stringstream ss; for (auto s : sols) { auto n = nissy::count_moves(s).value; - ss << s << "(" << n << ")" << std::endl; // TODO: remove last newline + ss << s << " (" << n << ")" << std::endl; // TODO: remove last newline } emit solutionsReady(QString::fromStdString(hs.str()), QString::fromStdString(ss.str())); } } + +void NissyAdapter::logLine(std::string str) +{ + std::stringstream ss; + ss << str << std::endl; + writeLog(ss.str()); +} diff --git a/qt/adapter.h b/qt/adapter.h @@ -14,7 +14,7 @@ struct SolveOptions { unsigned minmoves; unsigned maxmoves; unsigned maxsolutions; - int optimal; + unsigned optimal; }; class NissyAdapter : public QObject { @@ -23,6 +23,8 @@ class NissyAdapter : public QObject { QML_ELEMENT public: + static constexpr int maxSolutionsHardLimit = 9999; + NissyAdapter(); Q_INVOKABLE bool isValidScramble(QString); @@ -42,11 +44,12 @@ signals: private: std::vector<nissy::solver> solvers; - std::function<void(const char*)> writeLog; + std::function<void(std::string)> writeLog; void initSolver(const std::string&); void startSolve(SolveOptions); bool loadSolverData(nissy::solver&); + void logLine(std::string); }; #endif diff --git a/qt/main.cpp b/qt/main.cpp @@ -10,7 +10,7 @@ int main(int argc, char *argv[]) QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection); - engine.loadFromModule("nissyqt", "NissyMain"); + engine.loadFromModule("nissyqt", "Main"); return app.exec(); }