plotdeck 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plotdeck/__init__.py +4 -0
- plotdeck/__main__.py +6 -0
- plotdeck/app.py +55 -0
- plotdeck/core/__init__.py +4 -0
- plotdeck/core/controller.py +561 -0
- plotdeck/core/gcode_parser.py +404 -0
- plotdeck/core/grbl_protocol.py +278 -0
- plotdeck/core/status.py +148 -0
- plotdeck/core/streamer.py +328 -0
- plotdeck/core/transport.py +375 -0
- plotdeck/ui/__init__.py +1 -0
- plotdeck/ui/connection_panel.py +139 -0
- plotdeck/ui/console.py +171 -0
- plotdeck/ui/dialogs.py +317 -0
- plotdeck/ui/editor.py +544 -0
- plotdeck/ui/job_panel.py +130 -0
- plotdeck/ui/jog_panel.py +222 -0
- plotdeck/ui/main_window.py +1116 -0
- plotdeck/ui/manual.py +156 -0
- plotdeck/ui/status_panel.py +244 -0
- plotdeck/ui/theme.py +329 -0
- plotdeck/ui/visualizer.py +374 -0
- plotdeck/utils/__init__.py +1 -0
- plotdeck/utils/logging_setup.py +42 -0
- plotdeck/utils/settings.py +86 -0
- plotdeck-1.0.0.dist-info/METADATA +227 -0
- plotdeck-1.0.0.dist-info/RECORD +30 -0
- plotdeck-1.0.0.dist-info/WHEEL +5 -0
- plotdeck-1.0.0.dist-info/entry_points.txt +5 -0
- plotdeck-1.0.0.dist-info/top_level.txt +1 -0
plotdeck/ui/dialogs.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Modal dialogs: pre-flight summary, tool change, Z probe and settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt, Signal
|
|
8
|
+
from PySide6.QtWidgets import (
|
|
9
|
+
QCheckBox,
|
|
10
|
+
QDialog,
|
|
11
|
+
QDialogButtonBox,
|
|
12
|
+
QDoubleSpinBox,
|
|
13
|
+
QFormLayout,
|
|
14
|
+
QGridLayout,
|
|
15
|
+
QHBoxLayout,
|
|
16
|
+
QHeaderView,
|
|
17
|
+
QLabel,
|
|
18
|
+
QPushButton,
|
|
19
|
+
QTableWidget,
|
|
20
|
+
QTableWidgetItem,
|
|
21
|
+
QVBoxLayout,
|
|
22
|
+
QWidget,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from ..core.controller import SettingsEntry
|
|
26
|
+
from ..core.gcode_parser import Toolpath
|
|
27
|
+
from .job_panel import format_duration
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PreflightDialog(QDialog):
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
file_name: str,
|
|
34
|
+
line_count: int,
|
|
35
|
+
toolpath: Toolpath | None,
|
|
36
|
+
warnings: list[str],
|
|
37
|
+
parent=None, # noqa: ANN001
|
|
38
|
+
) -> None:
|
|
39
|
+
super().__init__(parent)
|
|
40
|
+
self.setWindowTitle("Pre-flight check")
|
|
41
|
+
self.setMinimumWidth(480)
|
|
42
|
+
self.choice = "cancel"
|
|
43
|
+
|
|
44
|
+
title = QLabel(file_name or "Untitled")
|
|
45
|
+
font = title.font()
|
|
46
|
+
font.setPointSize(15)
|
|
47
|
+
font.setBold(True)
|
|
48
|
+
title.setFont(font)
|
|
49
|
+
|
|
50
|
+
grid = QGridLayout()
|
|
51
|
+
grid.setColumnStretch(1, 1)
|
|
52
|
+
|
|
53
|
+
rows = [("Lines", f"{line_count:,}")]
|
|
54
|
+
if toolpath is not None:
|
|
55
|
+
rows.append(("Estimated time", format_duration(toolpath.estimated_seconds)))
|
|
56
|
+
rows.append(("Moves", f"{len(toolpath.segments):,}"))
|
|
57
|
+
if toolpath.bounds:
|
|
58
|
+
low, high = toolpath.bounds
|
|
59
|
+
rows.append(
|
|
60
|
+
(
|
|
61
|
+
"Bounding box",
|
|
62
|
+
f"{high[0] - low[0]:.2f} × {high[1] - low[1]:.2f} × "
|
|
63
|
+
f"{high[2] - low[2]:.2f} mm",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
rows.append(
|
|
67
|
+
(
|
|
68
|
+
"Max cut depth",
|
|
69
|
+
f"{toolpath.min_z:.2f} mm" if toolpath.min_z is not None else "--",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
rows.append(
|
|
73
|
+
(
|
|
74
|
+
"Tools",
|
|
75
|
+
", ".join(f"T{tool}" for tool in toolpath.tools) if toolpath.tools else "none",
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
for row, (label, value) in enumerate(rows):
|
|
79
|
+
key = QLabel(label)
|
|
80
|
+
key.setObjectName("Muted")
|
|
81
|
+
grid.addWidget(key, row, 0)
|
|
82
|
+
grid.addWidget(QLabel(value), row, 1)
|
|
83
|
+
|
|
84
|
+
warning_box = QWidget()
|
|
85
|
+
warning_layout = QVBoxLayout(warning_box)
|
|
86
|
+
warning_layout.setContentsMargins(0, 6, 0, 0)
|
|
87
|
+
if warnings:
|
|
88
|
+
heading = QLabel("Warnings")
|
|
89
|
+
heading.setObjectName("SectionHeader")
|
|
90
|
+
warning_layout.addWidget(heading)
|
|
91
|
+
for warning in warnings:
|
|
92
|
+
warning_label = QLabel(f"• {warning}")
|
|
93
|
+
warning_label.setWordWrap(True)
|
|
94
|
+
warning_label.setStyleSheet("color: #E0A030;")
|
|
95
|
+
warning_layout.addWidget(warning_label)
|
|
96
|
+
else:
|
|
97
|
+
ready = QLabel("No problems detected.")
|
|
98
|
+
ready.setObjectName("Muted")
|
|
99
|
+
warning_layout.addWidget(ready)
|
|
100
|
+
|
|
101
|
+
self._dry_run = QCheckBox("Dry run (GRBL check mode, no motion)")
|
|
102
|
+
self._dry_run.setToolTip("Runs the job with GRBL check mode enabled ($C).")
|
|
103
|
+
|
|
104
|
+
start = QPushButton("Start")
|
|
105
|
+
start.setObjectName("Primary")
|
|
106
|
+
cancel = QPushButton("Cancel")
|
|
107
|
+
start.setDefault(True)
|
|
108
|
+
start.clicked.connect(self._start)
|
|
109
|
+
cancel.clicked.connect(self.reject)
|
|
110
|
+
|
|
111
|
+
buttons = QHBoxLayout()
|
|
112
|
+
buttons.addWidget(self._dry_run)
|
|
113
|
+
buttons.addStretch(1)
|
|
114
|
+
buttons.addWidget(cancel)
|
|
115
|
+
buttons.addWidget(start)
|
|
116
|
+
|
|
117
|
+
layout = QVBoxLayout(self)
|
|
118
|
+
layout.setContentsMargins(18, 16, 18, 14)
|
|
119
|
+
layout.setSpacing(10)
|
|
120
|
+
layout.addWidget(title)
|
|
121
|
+
layout.addLayout(grid)
|
|
122
|
+
layout.addWidget(warning_box)
|
|
123
|
+
layout.addStretch(1)
|
|
124
|
+
layout.addLayout(buttons)
|
|
125
|
+
|
|
126
|
+
def _start(self) -> None:
|
|
127
|
+
self.choice = "dry_run" if self._dry_run.isChecked() else "start"
|
|
128
|
+
self.accept()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ToolChangeDialog(QDialog):
|
|
132
|
+
def __init__(self, tool_number: int | None, parent=None) -> None: # noqa: ANN001
|
|
133
|
+
super().__init__(parent)
|
|
134
|
+
self.setWindowTitle("Tool change")
|
|
135
|
+
self.setMinimumWidth(420)
|
|
136
|
+
|
|
137
|
+
title = QLabel(
|
|
138
|
+
f"Insert tool {tool_number}" if tool_number is not None else "Insert the next tool"
|
|
139
|
+
)
|
|
140
|
+
font = title.font()
|
|
141
|
+
font.setPointSize(14)
|
|
142
|
+
font.setBold(True)
|
|
143
|
+
title.setFont(font)
|
|
144
|
+
|
|
145
|
+
body = QLabel(
|
|
146
|
+
"The job has stopped and the machine is on hold. Change the tool, "
|
|
147
|
+
"re-probe Z if needed, then resume.\n\n"
|
|
148
|
+
"M6 is handled by PlotDeck and is never sent to GRBL."
|
|
149
|
+
)
|
|
150
|
+
body.setWordWrap(True)
|
|
151
|
+
note = QLabel(
|
|
152
|
+
"Make sure the spindle is off and the machine has stopped before reaching in."
|
|
153
|
+
)
|
|
154
|
+
note.setWordWrap(True)
|
|
155
|
+
note.setStyleSheet("color: #E0A030;")
|
|
156
|
+
|
|
157
|
+
buttons = QDialogButtonBox()
|
|
158
|
+
resume = buttons.addButton("Resume job", QDialogButtonBox.ButtonRole.AcceptRole)
|
|
159
|
+
buttons.addButton("Abort job", QDialogButtonBox.ButtonRole.RejectRole)
|
|
160
|
+
resume.setObjectName("Primary")
|
|
161
|
+
buttons.accepted.connect(self.accept)
|
|
162
|
+
buttons.rejected.connect(self.reject)
|
|
163
|
+
|
|
164
|
+
layout = QVBoxLayout(self)
|
|
165
|
+
layout.setContentsMargins(18, 16, 18, 14)
|
|
166
|
+
layout.setSpacing(10)
|
|
167
|
+
layout.addWidget(title)
|
|
168
|
+
layout.addWidget(body)
|
|
169
|
+
layout.addWidget(note)
|
|
170
|
+
layout.addWidget(buttons)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ProbeDialog(QDialog):
|
|
174
|
+
def __init__(self, parent=None) -> None: # noqa: ANN001
|
|
175
|
+
super().__init__(parent)
|
|
176
|
+
self.setWindowTitle("Probe Z")
|
|
177
|
+
self.setMinimumWidth(360)
|
|
178
|
+
|
|
179
|
+
intro = QLabel(
|
|
180
|
+
"Attach the touch-plate clip, place the plate under the bit and press Probe. "
|
|
181
|
+
"PlotDeck probes down slowly, sets work zero to the plate thickness, "
|
|
182
|
+
"then retracts."
|
|
183
|
+
)
|
|
184
|
+
intro.setWordWrap(True)
|
|
185
|
+
|
|
186
|
+
self.thickness = QDoubleSpinBox()
|
|
187
|
+
self.thickness.setRange(0.0, 50.0)
|
|
188
|
+
self.thickness.setDecimals(2)
|
|
189
|
+
self.thickness.setValue(10.0)
|
|
190
|
+
self.thickness.setSuffix(" mm")
|
|
191
|
+
self.feed = QDoubleSpinBox()
|
|
192
|
+
self.feed.setRange(1, 500)
|
|
193
|
+
self.feed.setValue(50)
|
|
194
|
+
self.feed.setSuffix(" mm/min")
|
|
195
|
+
self.travel = QDoubleSpinBox()
|
|
196
|
+
self.travel.setRange(1, 200)
|
|
197
|
+
self.travel.setValue(30)
|
|
198
|
+
self.travel.setSuffix(" mm")
|
|
199
|
+
self.retract = QDoubleSpinBox()
|
|
200
|
+
self.retract.setRange(0, 50)
|
|
201
|
+
self.retract.setValue(5)
|
|
202
|
+
self.retract.setSuffix(" mm")
|
|
203
|
+
|
|
204
|
+
form = QFormLayout()
|
|
205
|
+
form.addRow("Plate thickness", self.thickness)
|
|
206
|
+
form.addRow("Probe feed", self.feed)
|
|
207
|
+
form.addRow("Max travel", self.travel)
|
|
208
|
+
form.addRow("Retract height", self.retract)
|
|
209
|
+
|
|
210
|
+
buttons = QDialogButtonBox(
|
|
211
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
212
|
+
)
|
|
213
|
+
buttons.button(QDialogButtonBox.StandardButton.Ok).setText("Probe")
|
|
214
|
+
buttons.accepted.connect(self.accept)
|
|
215
|
+
buttons.rejected.connect(self.reject)
|
|
216
|
+
|
|
217
|
+
layout = QVBoxLayout(self)
|
|
218
|
+
layout.setContentsMargins(18, 16, 18, 14)
|
|
219
|
+
layout.setSpacing(10)
|
|
220
|
+
layout.addWidget(intro)
|
|
221
|
+
layout.addLayout(form)
|
|
222
|
+
layout.addWidget(buttons)
|
|
223
|
+
|
|
224
|
+
def values(self) -> tuple[float, float, float, float]:
|
|
225
|
+
return (
|
|
226
|
+
self.thickness.value(),
|
|
227
|
+
self.feed.value(),
|
|
228
|
+
self.travel.value(),
|
|
229
|
+
self.retract.value(),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class SettingsDialog(QDialog):
|
|
234
|
+
"""Editable ``$$`` settings table."""
|
|
235
|
+
|
|
236
|
+
refreshRequested = Signal()
|
|
237
|
+
writeRequested = Signal(int, str)
|
|
238
|
+
|
|
239
|
+
def __init__(self, token: dict[str, str] | None = None, parent=None) -> None: # noqa: ANN001
|
|
240
|
+
super().__init__(parent)
|
|
241
|
+
self._token = token or {}
|
|
242
|
+
self.setWindowTitle("GRBL settings")
|
|
243
|
+
self.setMinimumSize(640, 520)
|
|
244
|
+
|
|
245
|
+
intro = QLabel(
|
|
246
|
+
"Values are read from the controller with $$. Select a row, edit the value "
|
|
247
|
+
"and press Write. Changes take effect immediately."
|
|
248
|
+
)
|
|
249
|
+
intro.setWordWrap(True)
|
|
250
|
+
intro.setObjectName("Muted")
|
|
251
|
+
|
|
252
|
+
self._table = QTableWidget(0, 3)
|
|
253
|
+
self._table.setHorizontalHeaderLabels(["#", "Value", "Description"])
|
|
254
|
+
self._table.verticalHeader().setVisible(False)
|
|
255
|
+
header = self._table.horizontalHeader()
|
|
256
|
+
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
|
257
|
+
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
|
258
|
+
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
|
259
|
+
self._table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
260
|
+
self._table.setEditTriggers(
|
|
261
|
+
QTableWidget.EditTrigger.DoubleClicked | QTableWidget.EditTrigger.EditKeyPressed
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
self._status = QLabel("Press Refresh to read settings from the machine.")
|
|
265
|
+
self._status.setObjectName("Muted")
|
|
266
|
+
|
|
267
|
+
refresh = QPushButton("Refresh")
|
|
268
|
+
write = QPushButton("Write selected")
|
|
269
|
+
close = QPushButton("Close")
|
|
270
|
+
refresh.clicked.connect(self.refreshRequested)
|
|
271
|
+
write.clicked.connect(self._write)
|
|
272
|
+
close.clicked.connect(self.accept)
|
|
273
|
+
|
|
274
|
+
buttons = QHBoxLayout()
|
|
275
|
+
buttons.addWidget(self._status, 1)
|
|
276
|
+
buttons.addWidget(refresh)
|
|
277
|
+
buttons.addWidget(write)
|
|
278
|
+
buttons.addWidget(close)
|
|
279
|
+
|
|
280
|
+
layout = QVBoxLayout(self)
|
|
281
|
+
layout.setContentsMargins(16, 14, 16, 12)
|
|
282
|
+
layout.setSpacing(8)
|
|
283
|
+
layout.addWidget(intro)
|
|
284
|
+
layout.addWidget(self._table, 1)
|
|
285
|
+
layout.addLayout(buttons)
|
|
286
|
+
|
|
287
|
+
def set_settings(self, entries: list[SettingsEntry]) -> None:
|
|
288
|
+
self._table.setRowCount(0)
|
|
289
|
+
for entry in entries:
|
|
290
|
+
row = self._table.rowCount()
|
|
291
|
+
self._table.insertRow(row)
|
|
292
|
+
number = QTableWidgetItem(f"${entry.number}")
|
|
293
|
+
number.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
|
294
|
+
value = QTableWidgetItem(entry.value)
|
|
295
|
+
number.setData(Qt.ItemDataRole.UserRole, entry.number)
|
|
296
|
+
self._table.setItem(row, 0, number)
|
|
297
|
+
self._table.setItem(row, 1, value)
|
|
298
|
+
description = QTableWidgetItem(entry.description)
|
|
299
|
+
description.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
|
300
|
+
self._table.setItem(row, 2, description)
|
|
301
|
+
self._status.setText(f"{len(entries)} settings loaded.")
|
|
302
|
+
|
|
303
|
+
def _write(self) -> None:
|
|
304
|
+
row = self._table.currentRow()
|
|
305
|
+
if row < 0:
|
|
306
|
+
self._status.setText("Select a setting first.")
|
|
307
|
+
return
|
|
308
|
+
number_item = self._table.item(row, 0)
|
|
309
|
+
value_item = self._table.item(row, 1)
|
|
310
|
+
if number_item is None or value_item is None:
|
|
311
|
+
return
|
|
312
|
+
value = value_item.text().strip()
|
|
313
|
+
if not re.fullmatch(r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)", value):
|
|
314
|
+
self._status.setText("Enter a numeric value.")
|
|
315
|
+
return
|
|
316
|
+
self.writeRequested.emit(int(number_item.data(Qt.ItemDataRole.UserRole)), value)
|
|
317
|
+
self._status.setText(f"Wrote ${number_item.data(Qt.ItemDataRole.UserRole)}={value}")
|