Skip to content

Commit d927770

Browse files
authored
Added Uploads for Online Services (#568)
* Added an upload dialog that allows users to upload their models to online services
1 parent 8232c26 commit d927770

2 files changed

Lines changed: 206 additions & 0 deletions

File tree

cq_editor/main_window.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from .widgets.debugger import Debugger, LocalsView
2323
from .widgets.cq_object_inspector import CQObjectInspector
2424
from .widgets.log import LogViewer
25+
from .widgets.upload_dialog import UploadDialog
2526

2627
from . import __version__
2728
from .utils import (
@@ -365,6 +366,15 @@ def prepare_menubar(self):
365366
)
366367
)
367368

369+
# Add the Upload dialog action
370+
menu_tools.addAction(
371+
QAction(
372+
"Upload Model...",
373+
self,
374+
triggered=self._upload_model,
375+
)
376+
)
377+
368378
def prepare_menubar_component(self, menus, comp_menu_dict):
369379

370380
for name, action in comp_menu_dict.items():
@@ -572,6 +582,26 @@ def update_statusbar(self, status_text):
572582
# Update the statusbar text
573583
self.status_label.setText(status_text)
574584

585+
def _upload_model(self):
586+
"""
587+
Allows the userr to easily upload models to an online service for manufacturing,
588+
analysis, simulation, display, etc.
589+
"""
590+
591+
obj_tree = self.components["object_tree"]
592+
selected = [
593+
item
594+
for item in obj_tree.tree.selectedItems()
595+
if item.parent() is obj_tree.CQ
596+
]
597+
dlg = UploadDialog(
598+
self,
599+
self.components["object_tree"],
600+
self.components["editor"],
601+
selected_shapes=[item.shape for item in selected],
602+
)
603+
dlg.exec_()
604+
575605

576606
if __name__ == "__main__":
577607

cq_editor/widgets/upload_dialog.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import json
2+
import os
3+
import tempfile
4+
5+
import requests
6+
from PyQt5.QtGui import QDesktopServices
7+
from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject
8+
from PyQt5.QtWidgets import (
9+
QComboBox,
10+
QDialog,
11+
QHBoxLayout,
12+
QLabel,
13+
QLineEdit,
14+
QPushButton,
15+
QTextBrowser,
16+
QVBoxLayout,
17+
)
18+
from ..cq_utils import export
19+
20+
PRESETS = [
21+
{
22+
"name": "PCBWay",
23+
"url": "https://www.pcbway.com/common/CadQueryUpFile",
24+
"file_type": "step",
25+
"result_key": "redirect",
26+
},
27+
]
28+
29+
30+
class UploadSignals(QObject):
31+
finished = pyqtSignal(str) # Emits formatted JSON or error message
32+
error = pyqtSignal(str)
33+
34+
35+
class _UploadRunnable(QRunnable):
36+
def __init__(self, url, file_path, file_type):
37+
super().__init__()
38+
self.url = url
39+
self.file_path = file_path
40+
self.file_type = file_type
41+
self.signals = UploadSignals()
42+
43+
def run(self):
44+
try:
45+
with open(self.file_path, "rb") as f:
46+
resp = requests.post(
47+
self.url,
48+
files={"file": (f"model.{self.file_type}", f)},
49+
)
50+
resp.raise_for_status()
51+
self.signals.finished.emit(json.dumps(resp.json(), indent=2))
52+
except Exception as e:
53+
self.signals.error.emit(str(e))
54+
55+
56+
class UploadDialog(QDialog):
57+
def __init__(self, parent, object_tree, editor, selected_shapes=None):
58+
super().__init__(parent, windowTitle="Upload Model")
59+
self._object_tree = object_tree
60+
self._editor = editor
61+
self._active_preset = None
62+
self._selected_shapes = selected_shapes or []
63+
64+
# URL row
65+
url_layout = QHBoxLayout()
66+
url_layout.addWidget(QLabel("URL:"))
67+
self._url_input = QLineEdit()
68+
url_layout.addWidget(self._url_input)
69+
70+
# Preset buttons
71+
preset_layout = QHBoxLayout()
72+
preset_layout.addWidget(QLabel("Presets:"))
73+
for preset in PRESETS:
74+
btn = QPushButton(preset["name"])
75+
btn.clicked.connect(lambda checked, p=preset: self._apply_preset(p))
76+
preset_layout.addWidget(btn)
77+
preset_layout.addStretch()
78+
79+
# File type selector
80+
type_layout = QHBoxLayout()
81+
type_layout.addWidget(QLabel("File type:"))
82+
self._type_combo = QComboBox()
83+
self._type_combo.addItems(["STEP", "STL", "CadQuery"])
84+
type_layout.addWidget(self._type_combo)
85+
type_layout.addStretch()
86+
87+
# Upload button
88+
self._upload_btn = QPushButton("Upload")
89+
self._upload_btn.clicked.connect(self._do_upload)
90+
91+
# Response area
92+
self._response = QTextBrowser(placeholderText="Response will appear here...")
93+
self._response.setOpenLinks(False)
94+
self._response.anchorClicked.connect(lambda url: QDesktopServices.openUrl(url))
95+
96+
layout = QVBoxLayout()
97+
layout.addLayout(url_layout)
98+
layout.addLayout(preset_layout)
99+
layout.addLayout(type_layout)
100+
layout.addWidget(self._upload_btn)
101+
layout.addWidget(self._response)
102+
self.setLayout(layout)
103+
self.resize(500, 400)
104+
105+
def _apply_preset(self, preset):
106+
self._url_input.setText(preset["url"])
107+
self._type_combo.setCurrentText(preset["file_type"].upper())
108+
self._active_preset = preset
109+
110+
def _do_upload(self):
111+
url = self._url_input.text().strip()
112+
if not url:
113+
self._response.setPlainText("Error: no URL entered.")
114+
return
115+
116+
file_type = self._type_combo.currentText()
117+
self._upload_btn.setEnabled(False)
118+
self._response.setPlainText("Uploading...")
119+
120+
if file_type == "CadQuery":
121+
tmp = tempfile.NamedTemporaryFile(suffix=".py", delete=False, mode="w")
122+
tmp.write(self._editor.toPlainText())
123+
tmp.close()
124+
self._run_upload(url, tmp.name, "py")
125+
return
126+
else:
127+
# Prefer selected item, fall back to add top-level CQ objects
128+
shapes = (
129+
self._selected_shapes
130+
if self._selected_shapes
131+
else [
132+
self._object_tree.CQ.child(i).shape
133+
for i in range(self._object_tree.CQ.childCount())
134+
]
135+
)
136+
137+
# If the user did not select an object, let them know
138+
if not shapes:
139+
self._response.setPlainText("ERROR: Please select a model to upload.")
140+
self._upload_btn.setEnabled(True)
141+
return
142+
143+
# Assume that only one object is selected and upload it
144+
cur_type = file_type.lower()
145+
tmp = tempfile.NamedTemporaryFile(suffix=f".{cur_type}", delete=False)
146+
tmp.close()
147+
export(shapes, cur_type, tmp.name)
148+
self._run_upload(url, tmp.name, cur_type)
149+
150+
def _run_upload(self, url, file_path, file_type):
151+
runnable = _UploadRunnable(url, file_path, file_type)
152+
runnable.signals.finished.connect(lambda msg: self._on_done(file_path, msg))
153+
runnable.signals.error.connect(
154+
lambda msg: self._on_done(file_path, f"Error: {msg}")
155+
)
156+
QThreadPool.globalInstance().start(runnable)
157+
158+
def _on_done(self, file_path, message):
159+
os.unlink(file_path)
160+
try:
161+
data = json.loads(message)
162+
result_key = (
163+
self._active_preset.get("result_key") if self._active_preset else None
164+
)
165+
if result_key and result_key in data:
166+
link = data[result_key]
167+
name = self._active_preset.get("name", "unknown service")
168+
self._response.setHtml(
169+
f"<p>Click the link below to view your model on {name}'s website.</p>"
170+
f'<a href="{link}">{link}</a>'
171+
)
172+
else:
173+
self._response.setPlainText(message)
174+
except json.JSONDecodeError:
175+
self._response.setPlainText(message)
176+
self._upload_btn.setEnabled(True)

0 commit comments

Comments
 (0)