commit 9589072687726dd51776b0e952625ae1996f4721
parent dc5e8188a7e29909cbdf661e9b2a80700fe0491d
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date: Wed, 16 Apr 2025 09:56:55 +0200
Added rudimentary QT UI
Diffstat:
8 files changed, 521 insertions(+), 3 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -9,11 +9,12 @@ utils/.DS_Store
perf.data
perf.data.old
run
+nissyqt
+qt/build
debugrun
shell/lasttest.out
shell/lasttest.err
tables/*
-tables-old*/*
test/*/runtest
test/.DS_Store
test/run
diff --git a/Makefile b/Makefile
@@ -18,7 +18,7 @@ debugnissy.o:
${CC} ${MACROS} ${DBGFLAGS} -c -o debugnissy.o src/nissy.c
clean:
- rm -rf *.out *.o *.s *.so run debugrun
+ rm -rf *.out *.o *.s *.so run debugrun nissyqt qt/build
test: debugnissy.o
CC="${CC} ${MACROS} ${DBGFLAGS}" OBJ=debugnissy.o ./test/test.sh
@@ -46,4 +46,10 @@ python: nissy.o
${CC} ${CFLAGS} -shared ${PYTHON3_INCLUDES} -o nissy_python_module.so \
nissy.o python/nissy_module.c
-.PHONY: all clean test tool debugtool shell debugshell shelltest python
+qt: nissy.o
+ mkdir -p qt/build
+ cd qt && cmake . -B build
+ cd qt/build && make
+ cp qt/build/appnissyqt ./nissyqt
+
+.PHONY: all clean test tool debugtool shell debugshell shelltest python qt
diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt
@@ -0,0 +1,27 @@
+cmake_minimum_required(VERSION 3.16)
+
+project(nissyqt VERSION 0.1 LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_STANDARD 20)
+
+find_package(Qt6 REQUIRED COMPONENTS Quick Concurrent)
+
+qt_standard_project_setup(REQUIRES 6.5)
+
+qt_add_executable(appnissyqt
+ main.cpp
+ adapter.cpp adapter.h
+ ../cpp/nissy.h ../cpp/nissy.cpp
+ ../nissy.o
+)
+
+qt_add_qml_module(appnissyqt
+ URI nissyqt
+ VERSION 1.0
+ QML_FILES NissyMain.qml
+)
+
+target_link_libraries(appnissyqt
+ PRIVATE Qt6::Quick
+)
diff --git a/qt/Makefile b/qt/Makefile
@@ -0,0 +1,15 @@
+all: nissyqt
+
+nissyqt:
+ mkdir -p build
+ cmake . -B build
+ cd build && make
+ cp build/appnissyqt ./run
+
+run:
+ QT_LOGGING_RULES="*.debug=true; qt.*.debug=false" ./run
+
+clean:
+ rm -rf run build
+
+.PHONY: all nissyqt run clean
diff --git a/qt/NissyMain.qml b/qt/NissyMain.qml
@@ -0,0 +1,261 @@
+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
@@ -0,0 +1,140 @@
+#include "adapter.h"
+
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <vector>
+#include <QDebug>
+#include <QtConcurrent/QtConcurrent>
+
+void logWrapper(const char *str, void *data)
+{
+ auto f = *reinterpret_cast<std::function<void(const char*)>*>(data);
+ f(str);
+}
+
+NissyAdapter::NissyAdapter()
+{
+ std::vector<std::string> solverNames {
+ "h48h3k2"
+ };
+
+ for (auto s : solverNames)
+ initSolver(s);
+
+ writeLog = [&](const char *str) {
+ emit appendLog(QString::fromStdString(str));
+ };
+
+ nissy::set_logger(&logWrapper, &writeLog);
+}
+
+void NissyAdapter::initSolver(const std::string& s) {
+ auto se = nissy::solver::get(s);
+ if (std::holds_alternative<nissy::error>(se)) {
+ qDebug("Error loading solver!");
+ return;
+ }
+ auto ss = std::get<nissy::solver>(se);
+ solvers.push_back(ss);
+}
+
+bool NissyAdapter::loadSolverData(nissy::solver& solver) {
+ if (solver.data_checked)
+ return true;
+
+ std::filesystem::path filePath("./tables/" + solver.id);
+ if (!std::filesystem::exists(filePath)) {
+ auto err = solver.generate_data();
+ if (!err.ok()) {
+ emit solverError(QString("Error generating data!"));
+ return false;
+ }
+ std::filesystem::create_directory("./tables/");
+ std::ofstream ofs(filePath, std::ios::binary);
+ ofs.write(reinterpret_cast<char *>(solver.data.data()),
+ solver.size);
+ ofs.close();
+ } else {
+ std::ifstream ifs(filePath, std::ios::binary);
+ solver.read_data(ifs);
+ ifs.close();
+ }
+
+ if (!solver.check_data().ok()) {
+ emit solverError(QString("Error reading data!"));
+ return false;
+ }
+
+ return true;
+}
+
+Q_INVOKABLE bool NissyAdapter::isValidScramble(QString qscr)
+{
+ nissy::cube c;
+ return c.move(qscr.toStdString()).ok();
+}
+
+Q_INVOKABLE void NissyAdapter::requestSolve(
+ QString scramble,
+ QString solver,
+ int minmoves,
+ int maxmoves,
+ int maxsolutions,
+ int optimal
+)
+{
+ nissy::cube c;
+ if (!c.move(scramble.toStdString()).ok()) {
+ emit solverError(QString("Unexpected error: invalid scramble"));
+ return;
+ }
+
+ nissy::solver *ss = nullptr;
+ for (auto& s : solvers)
+ if (s.name == solver)
+ ss = &s;
+ if (ss == nullptr) {
+ std::string msg = "Error: solver '" + solver.toStdString()
+ + "' not available";
+ emit solverError(QString::fromStdString(msg));
+ return;
+ }
+
+ SolveOptions opts{c, ss, (unsigned)minmoves, (unsigned)maxmoves,
+ (unsigned)maxsolutions, optimal};
+ auto _ = QtConcurrent::run(&NissyAdapter::startSolve, this, opts);
+ return;
+}
+
+void NissyAdapter::startSolve(SolveOptions opts)
+{
+ loadSolverData(*opts.solver);
+
+ auto result = opts.solver->solve(opts.cube, nissy::nissflag::NORMAL,
+ opts.minmoves, opts.maxmoves, opts.maxsolutions, opts.optimal, 8);
+
+ if (!result.err.ok()) {
+ std::string msg = "Error computing solutions: " +
+ std::to_string(result.err.value);
+ emit solverError(QString::fromStdString(msg));
+ return;
+ }
+
+ auto& sols = result.solutions;
+ if (sols.size() == 0) {
+ emit solutionsReady("No solution found", "");
+ } else {
+ std::stringstream hs;
+ hs << "Found " << sols.size() << " solution"
+ << (sols.size() > 1 ? "s:" : ":");
+
+ std::stringstream ss;
+ for (auto s : sols) {
+ auto n = nissy::count_moves(s).value;
+ ss << s << "(" << n << ")" << std::endl; // TODO: remove last newline
+ }
+ emit solutionsReady(QString::fromStdString(hs.str()),
+ QString::fromStdString(ss.str()));
+ }
+}
diff --git a/qt/adapter.h b/qt/adapter.h
@@ -0,0 +1,52 @@
+#ifndef ADAPTER_H
+#define ADAPTER_H
+
+#include "../cpp/nissy.h"
+
+#include <map>
+#include <string>
+#include <QObject>
+#include <QtQmlIntegration>
+
+struct SolveOptions {
+ nissy::cube cube;
+ nissy::solver *solver;
+ unsigned minmoves;
+ unsigned maxmoves;
+ unsigned maxsolutions;
+ int optimal;
+};
+
+class NissyAdapter : public QObject {
+ Q_OBJECT
+ QML_SINGLETON
+ QML_ELEMENT
+
+public:
+ NissyAdapter();
+
+ Q_INVOKABLE bool isValidScramble(QString);
+ Q_INVOKABLE void requestSolve(
+ QString scramble,
+ QString solver,
+ int minmoves,
+ int maxmoves,
+ int maxsolutions,
+ int optimal
+ );
+
+signals:
+ void solutionsReady(QString, QString);
+ void solverError(QString);
+ void appendLog(QString);
+
+private:
+ std::vector<nissy::solver> solvers;
+ std::function<void(const char*)> writeLog;
+
+ void initSolver(const std::string&);
+ void startSolve(SolveOptions);
+ bool loadSolverData(nissy::solver&);
+};
+
+#endif
diff --git a/qt/main.cpp b/qt/main.cpp
@@ -0,0 +1,16 @@
+#include "adapter.h"
+
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+
+int main(int argc, char *argv[])
+{
+ QGuiApplication app(argc, argv);
+ QQmlApplicationEngine engine;
+ QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
+ &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
+
+ engine.loadFromModule("nissyqt", "NissyMain");
+
+ return app.exec();
+}