aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastiano Tronto <sebastiano@tronto.net>2025-04-16 09:56:55 +0200
committerSebastiano Tronto <sebastiano@tronto.net>2025-04-16 09:56:55 +0200
commit9589072687726dd51776b0e952625ae1996f4721 (patch)
tree2dbc63de0b3e5805b5dca2919d05b4356c1194a4
parentdc5e8188a7e29909cbdf661e9b2a80700fe0491d (diff)
downloadnissy-core-9589072687726dd51776b0e952625ae1996f4721.tar.gz
nissy-core-9589072687726dd51776b0e952625ae1996f4721.zip
Added rudimentary QT UI
-rw-r--r--.gitignore3
-rw-r--r--Makefile10
-rw-r--r--qt/CMakeLists.txt27
-rw-r--r--qt/Makefile15
-rw-r--r--qt/NissyMain.qml261
-rw-r--r--qt/adapter.cpp140
-rw-r--r--qt/adapter.h52
-rw-r--r--qt/main.cpp16
8 files changed, 521 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index eeea1b6..961f54d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,11 +9,12 @@ utils/.DS_Store
9perf.data 9perf.data
10perf.data.old 10perf.data.old
11run 11run
12nissyqt
13qt/build
12debugrun 14debugrun
13shell/lasttest.out 15shell/lasttest.out
14shell/lasttest.err 16shell/lasttest.err
15tables/* 17tables/*
16tables-old*/*
17test/*/runtest 18test/*/runtest
18test/.DS_Store 19test/.DS_Store
19test/run 20test/run
diff --git a/Makefile b/Makefile
index c9a2c09..b72ceee 100644
--- a/Makefile
+++ b/Makefile
@@ -18,7 +18,7 @@ debugnissy.o:
18 ${CC} ${MACROS} ${DBGFLAGS} -c -o debugnissy.o src/nissy.c 18 ${CC} ${MACROS} ${DBGFLAGS} -c -o debugnissy.o src/nissy.c
19 19
20clean: 20clean:
21 rm -rf *.out *.o *.s *.so run debugrun 21 rm -rf *.out *.o *.s *.so run debugrun nissyqt qt/build
22 22
23test: debugnissy.o 23test: debugnissy.o
24 CC="${CC} ${MACROS} ${DBGFLAGS}" OBJ=debugnissy.o ./test/test.sh 24 CC="${CC} ${MACROS} ${DBGFLAGS}" OBJ=debugnissy.o ./test/test.sh
@@ -46,4 +46,10 @@ python: nissy.o
46 ${CC} ${CFLAGS} -shared ${PYTHON3_INCLUDES} -o nissy_python_module.so \ 46 ${CC} ${CFLAGS} -shared ${PYTHON3_INCLUDES} -o nissy_python_module.so \
47 nissy.o python/nissy_module.c 47 nissy.o python/nissy_module.c
48 48
49.PHONY: all clean test tool debugtool shell debugshell shelltest python 49qt: nissy.o
50 mkdir -p qt/build
51 cd qt && cmake . -B build
52 cd qt/build && make
53 cp qt/build/appnissyqt ./nissyqt
54
55.PHONY: all clean test tool debugtool shell debugshell shelltest python qt
diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt
new file mode 100644
index 0000000..8ba768c
--- /dev/null
+++ b/qt/CMakeLists.txt
@@ -0,0 +1,27 @@
1cmake_minimum_required(VERSION 3.16)
2
3project(nissyqt VERSION 0.1 LANGUAGES CXX)
4
5set(CMAKE_CXX_STANDARD_REQUIRED ON)
6set(CMAKE_CXX_STANDARD 20)
7
8find_package(Qt6 REQUIRED COMPONENTS Quick Concurrent)
9
10qt_standard_project_setup(REQUIRES 6.5)
11
12qt_add_executable(appnissyqt
13 main.cpp
14 adapter.cpp adapter.h
15 ../cpp/nissy.h ../cpp/nissy.cpp
16 ../nissy.o
17)
18
19qt_add_qml_module(appnissyqt
20 URI nissyqt
21 VERSION 1.0
22 QML_FILES NissyMain.qml
23)
24
25target_link_libraries(appnissyqt
26 PRIVATE Qt6::Quick
27)
diff --git a/qt/Makefile b/qt/Makefile
new file mode 100644
index 0000000..5b13026
--- /dev/null
+++ b/qt/Makefile
@@ -0,0 +1,15 @@
1all: nissyqt
2
3nissyqt:
4 mkdir -p build
5 cmake . -B build
6 cd build && make
7 cp build/appnissyqt ./run
8
9run:
10 QT_LOGGING_RULES="*.debug=true; qt.*.debug=false" ./run
11
12clean:
13 rm -rf run build
14
15.PHONY: all nissyqt run clean
diff --git a/qt/NissyMain.qml b/qt/NissyMain.qml
new file mode 100644
index 0000000..24aff43
--- /dev/null
+++ b/qt/NissyMain.qml
@@ -0,0 +1,261 @@
1import QtQuick
2import QtQuick.Controls
3import QtQuick.Layouts
4
5Window {
6 id: mainWindow
7
8 width: 800
9 height: 600
10 visible: true
11 title: "Nissy 3.0 - Preview"
12
13 SplitView {
14 id: splitView
15 anchors.fill: parent
16 orientation: Qt.Vertical
17
18 handle: Rectangle {
19 implicitHeight: 3
20 color: SplitHandle.hovered ? "black" : "#AAAAAA"
21 }
22
23 component MyScrollBar: ScrollBar {
24 orientation: Qt.Vertical
25 size: parent.height
26 policy: ScrollBar.AlwaysOn
27 anchors.right: parent.right
28 anchors.top: parent.top
29 anchors.bottom: parent.bottom
30 contentItem: Rectangle {
31 implicitWidth: 4
32 radius: implicitWidth/2
33 color: "black"
34 }
35 background: Rectangle {
36 implicitWidth: 4
37 radius: implicitWidth/2
38 color: "#AAAAAA"
39 }
40 }
41
42 ColumnLayout {
43 id: mainArea
44
45 property alias scramble: scrambleRow.scramble
46 property alias minmoves: solverCfg.minmoves
47 property alias maxmoves: solverCfg.maxmoves
48 property alias maxsolutions: solverCfg.maxsolutions
49 property alias sols: sols.text
50 property alias solsHeader: solsHeader.text
51
52 property bool solutionsLoading: false
53
54 anchors.top: parent.top
55 anchors.left: parent.left
56 anchors.right: parent.right
57 anchors.bottom: logView.top
58 anchors.margins: 6
59 spacing: 10
60
61 SplitView.minimumHeight: 180
62 SplitView.preferredHeight: 300
63
64 component Separator: Rectangle {
65 height: 1
66 Layout.fillWidth: true
67 color: "black"
68 }
69
70 ColumnLayout {
71 id: scrambleRow
72
73 property alias scramble: scrambleRowLayout.scramble
74
75 RowLayout {
76 id: scrambleRowLayout
77
78 property alias scramble: scrambleEditor.text
79
80 spacing: 6
81
82 TextField {
83 id: scrambleEditor
84
85 placeholderText: "Enter scramble here"
86 Layout.fillWidth: true
87 padding: 4
88
89 readonly property bool empty: text.trim().length == 0
90 readonly property bool valid: NissyAdapter.isValidScramble(text)
91
92 onAccepted: if (!empty && valid) submitScramble()
93 }
94
95 Button {
96 id: solveButton
97
98 enabled: !scrambleEditor.empty && scrambleEditor.valid &&
99 !mainArea.solutionsLoading
100 text: "Solve!"
101
102 onPressed: submitScramble()
103 }
104 }
105
106 Label {
107 id: invalidScrambleWarning
108 text: scrambleEditor.empty || scrambleEditor.valid ?
109 "" : "Invalid Scramble"
110 }
111 }
112
113 Separator {}
114
115 ColumnLayout {
116 id: solverCfg
117 property alias minmoves: minMaxRow.min
118 property alias maxmoves: minMaxRow.max
119 property alias maxsolutions: maxOptimal.maxsolutions
120
121 RowLayout {
122 id: minMaxRow
123
124 property alias min: slider.min
125 property alias max: slider.max
126
127 Rectangle {
128 width: 100
129 height: 20
130 Label { text: "Min moves: " + slider.min }
131 }
132 RangeSlider {
133 id: slider
134 from: 0
135 to: 20
136 first.value: from
137 second.value: to
138 stepSize: 1
139 snapMode: RangeSlider.SnapAlways
140
141 readonly property int min: Math.round(first.value)
142 readonly property int max: Math.round(second.value)
143 }
144 Rectangle {
145 width: 100
146 height: 20
147 Label { text: "Max moves: " + slider.max }
148 }
149 }
150
151 RowLayout {
152 id: maxOptimal
153
154 property alias maxsolutions: maxSolsRect.maxsolutions
155
156 Rectangle {
157 width: 220
158 height: 20
159 Label { text: "Maximum number of solutions:" }
160 }
161 Rectangle {
162 id: maxSolsRect
163
164 property int maxsolutions: parseInt(textField.text)
165
166 width: 35
167 height: 20
168
169 TextField {
170 id: textField
171
172 width: parent.width
173 text: "1"
174 validator: IntValidator{ bottom: 1; top: 999; }
175
176 onAccepted: submitScramble()
177 }
178 }
179 }
180 }
181
182 Separator {}
183
184 Label { id: solsHeader }
185
186 ScrollView {
187 Layout.fillHeight: true
188 Layout.fillWidth: true
189 Layout.bottomMargin: 10
190 ScrollBar.vertical: MyScrollBar {}
191
192 TextEdit {
193 id: sols
194 readOnly: true
195 font.family: "Monospace"
196 }
197 }
198 }
199
200 ScrollView {
201 id: logView
202
203 property alias text: logText.text
204
205 anchors.left: parent.left
206 anchors.right: parent.right
207 anchors.bottom: parent.bottom
208 anchors.margins: 6
209
210 SplitView.preferredHeight: 300
211
212 background: Rectangle {
213 color: "#404040"
214 radius: 4
215 }
216
217 ScrollBar.vertical: MyScrollBar {
218 id: scrollBar
219 position: 1.0 - size
220 }
221 Label {
222 id: logText
223
224 font.family: "Monospace"
225 color: "white"
226 }
227 }
228 }
229
230 function submitScramble() {
231 mainArea.solutionsLoading = true;
232 mainArea.solsHeader = "Loading solutions..."
233 mainArea.sols = ""
234 logView.text = ""
235 NissyAdapter.requestSolve(
236 mainArea.scramble,
237 "h48h3k2",
238 mainArea.minmoves,
239 mainArea.maxmoves,
240 mainArea.maxsolutions,
241 -1
242 )
243 }
244
245 Connections {
246 target: NissyAdapter
247 function onSolutionsReady(header, sols) {
248 mainArea.solutionsLoading = false
249 mainArea.solsHeader = header
250 mainArea.sols = sols
251 }
252 function onSolverError(msg) {
253 mainArea.solutionsLoading = false
254 mainArea.solusHeader = msg
255 mainArea.sols = ""
256 }
257 function onAppendLog(msg) {
258 logView.text += msg
259 }
260 }
261}
diff --git a/qt/adapter.cpp b/qt/adapter.cpp
new file mode 100644
index 0000000..3711fa4
--- /dev/null
+++ b/qt/adapter.cpp
@@ -0,0 +1,140 @@
1#include "adapter.h"
2
3#include <fstream>
4#include <sstream>
5#include <string>
6#include <vector>
7#include <QDebug>
8#include <QtConcurrent/QtConcurrent>
9
10void logWrapper(const char *str, void *data)
11{
12 auto f = *reinterpret_cast<std::function<void(const char*)>*>(data);
13 f(str);
14}
15
16NissyAdapter::NissyAdapter()
17{
18 std::vector<std::string> solverNames {
19 "h48h3k2"
20 };
21
22 for (auto s : solverNames)
23 initSolver(s);
24
25 writeLog = [&](const char *str) {
26 emit appendLog(QString::fromStdString(str));
27 };
28
29 nissy::set_logger(&logWrapper, &writeLog);
30}
31
32void NissyAdapter::initSolver(const std::string& s) {
33 auto se = nissy::solver::get(s);
34 if (std::holds_alternative<nissy::error>(se)) {
35 qDebug("Error loading solver!");
36 return;
37 }
38 auto ss = std::get<nissy::solver>(se);
39 solvers.push_back(ss);
40}
41
42bool NissyAdapter::loadSolverData(nissy::solver& solver) {
43 if (solver.data_checked)
44 return true;
45
46 std::filesystem::path filePath("./tables/" + solver.id);
47 if (!std::filesystem::exists(filePath)) {
48 auto err = solver.generate_data();
49 if (!err.ok()) {
50 emit solverError(QString("Error generating data!"));
51 return false;
52 }
53 std::filesystem::create_directory("./tables/");
54 std::ofstream ofs(filePath, std::ios::binary);
55 ofs.write(reinterpret_cast<char *>(solver.data.data()),
56 solver.size);
57 ofs.close();
58 } else {
59 std::ifstream ifs(filePath, std::ios::binary);
60 solver.read_data(ifs);
61 ifs.close();
62 }
63
64 if (!solver.check_data().ok()) {
65 emit solverError(QString("Error reading data!"));
66 return false;
67 }
68
69 return true;
70}
71
72Q_INVOKABLE bool NissyAdapter::isValidScramble(QString qscr)
73{
74 nissy::cube c;
75 return c.move(qscr.toStdString()).ok();
76}
77
78Q_INVOKABLE void NissyAdapter::requestSolve(
79 QString scramble,
80 QString solver,
81 int minmoves,
82 int maxmoves,
83 int maxsolutions,
84 int optimal
85)
86{
87 nissy::cube c;
88 if (!c.move(scramble.toStdString()).ok()) {
89 emit solverError(QString("Unexpected error: invalid scramble"));
90 return;
91 }
92
93 nissy::solver *ss = nullptr;
94 for (auto& s : solvers)
95 if (s.name == solver)
96 ss = &s;
97 if (ss == nullptr) {
98 std::string msg = "Error: solver '" + solver.toStdString()
99 + "' not available";
100 emit solverError(QString::fromStdString(msg));
101 return;
102 }
103
104 SolveOptions opts{c, ss, (unsigned)minmoves, (unsigned)maxmoves,
105 (unsigned)maxsolutions, optimal};
106 auto _ = QtConcurrent::run(&NissyAdapter::startSolve, this, opts);
107 return;
108}
109
110void NissyAdapter::startSolve(SolveOptions opts)
111{
112 loadSolverData(*opts.solver);
113
114 auto result = opts.solver->solve(opts.cube, nissy::nissflag::NORMAL,
115 opts.minmoves, opts.maxmoves, opts.maxsolutions, opts.optimal, 8);
116
117 if (!result.err.ok()) {
118 std::string msg = "Error computing solutions: " +
119 std::to_string(result.err.value);
120 emit solverError(QString::fromStdString(msg));
121 return;
122 }
123
124 auto& sols = result.solutions;
125 if (sols.size() == 0) {
126 emit solutionsReady("No solution found", "");
127 } else {
128 std::stringstream hs;
129 hs << "Found " << sols.size() << " solution"
130 << (sols.size() > 1 ? "s:" : ":");
131
132 std::stringstream ss;
133 for (auto s : sols) {
134 auto n = nissy::count_moves(s).value;
135 ss << s << "(" << n << ")" << std::endl; // TODO: remove last newline
136 }
137 emit solutionsReady(QString::fromStdString(hs.str()),
138 QString::fromStdString(ss.str()));
139 }
140}
diff --git a/qt/adapter.h b/qt/adapter.h
new file mode 100644
index 0000000..856cdad
--- /dev/null
+++ b/qt/adapter.h
@@ -0,0 +1,52 @@
1#ifndef ADAPTER_H
2#define ADAPTER_H
3
4#include "../cpp/nissy.h"
5
6#include <map>
7#include <string>
8#include <QObject>
9#include <QtQmlIntegration>
10
11struct SolveOptions {
12 nissy::cube cube;
13 nissy::solver *solver;
14 unsigned minmoves;
15 unsigned maxmoves;
16 unsigned maxsolutions;
17 int optimal;
18};
19
20class NissyAdapter : public QObject {
21 Q_OBJECT
22 QML_SINGLETON
23 QML_ELEMENT
24
25public:
26 NissyAdapter();
27
28 Q_INVOKABLE bool isValidScramble(QString);
29 Q_INVOKABLE void requestSolve(
30 QString scramble,
31 QString solver,
32 int minmoves,
33 int maxmoves,
34 int maxsolutions,
35 int optimal
36 );
37
38signals:
39 void solutionsReady(QString, QString);
40 void solverError(QString);
41 void appendLog(QString);
42
43private:
44 std::vector<nissy::solver> solvers;
45 std::function<void(const char*)> writeLog;
46
47 void initSolver(const std::string&);
48 void startSolve(SolveOptions);
49 bool loadSolverData(nissy::solver&);
50};
51
52#endif
diff --git a/qt/main.cpp b/qt/main.cpp
new file mode 100644
index 0000000..658d448
--- /dev/null
+++ b/qt/main.cpp
@@ -0,0 +1,16 @@
1#include "adapter.h"
2
3#include <QGuiApplication>
4#include <QQmlApplicationEngine>
5
6int main(int argc, char *argv[])
7{
8 QGuiApplication app(argc, argv);
9 QQmlApplicationEngine engine;
10 QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
11 &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
12
13 engine.loadFromModule("nissyqt", "NissyMain");
14
15 return app.exec();
16}

Generated with cgit - Back to sebastiano.tronto.net