|
11 | 11 | from PyQt5.QtCore import ( |
12 | 12 | Qt, |
13 | 13 | QObject, |
| 14 | + QThread, |
14 | 15 | pyqtSlot, |
15 | 16 | pyqtSignal, |
16 | 17 | QEventLoop, |
@@ -111,6 +112,50 @@ def update_frame(self, frame): |
111 | 112 | self.setModel(model) |
112 | 113 |
|
113 | 114 |
|
| 115 | +class RenderWorker(QObject): |
| 116 | + """Runs a CadQuery script on a background thread. |
| 117 | +
|
| 118 | + All signals are emitted from the worker thread; Qt's automatic queued- |
| 119 | + connection mechanism ensures they are safely delivered to main-thread slots. |
| 120 | + """ |
| 121 | + |
| 122 | + finished = pyqtSignal() |
| 123 | + sigRendered = pyqtSignal(dict) |
| 124 | + sigTraceback = pyqtSignal(object, str) |
| 125 | + sigLocals = pyqtSignal(dict) |
| 126 | + |
| 127 | + def __init__(self, debugger, cq_script, cq_script_path): |
| 128 | + super().__init__() |
| 129 | + self._debugger = debugger |
| 130 | + self._cq_script = cq_script |
| 131 | + self._cq_script_path = cq_script_path |
| 132 | + |
| 133 | + @pyqtSlot() |
| 134 | + def run(self): |
| 135 | + d = self._debugger |
| 136 | + cq_code, module = d.compile_code(self._cq_script, self._cq_script_path) |
| 137 | + |
| 138 | + if cq_code is not None: |
| 139 | + cq_objects, injected_names = d._inject_locals(module) |
| 140 | + try: |
| 141 | + d._exec(cq_code, module.__dict__, module.__dict__, |
| 142 | + self._cq_script_path) |
| 143 | + d._cleanup_locals(module, injected_names) |
| 144 | + |
| 145 | + if len(cq_objects) == 0: |
| 146 | + cq_objects = find_cq_objects(module.__dict__) |
| 147 | + |
| 148 | + self.sigRendered.emit(cq_objects) |
| 149 | + self.sigTraceback.emit(None, self._cq_script) |
| 150 | + self.sigLocals.emit(module.__dict__) |
| 151 | + except Exception: |
| 152 | + exc_info = sys.exc_info() |
| 153 | + sys.last_traceback = exc_info[-1] |
| 154 | + self.sigTraceback.emit(exc_info, self._cq_script) |
| 155 | + |
| 156 | + self.finished.emit() |
| 157 | + |
| 158 | + |
114 | 159 | class Debugger(QObject, ComponentMixin): |
115 | 160 |
|
116 | 161 | name = "Debugger" |
@@ -184,6 +229,8 @@ def __init__(self, parent): |
184 | 229 |
|
185 | 230 | self._frames = [] |
186 | 231 | self._stop_debugging = False |
| 232 | + self._render_thread = None |
| 233 | + self._render_worker = None |
187 | 234 |
|
188 | 235 | def get_current_script(self): |
189 | 236 |
|
@@ -214,10 +261,10 @@ def compile_code(self, cq_script, cq_script_path=None): |
214 | 261 | self.sigTraceback.emit(sys.exc_info(), cq_script) |
215 | 262 | return None, None |
216 | 263 |
|
217 | | - def _exec(self, code, locals_dict, globals_dict): |
| 264 | + def _exec(self, code, locals_dict, globals_dict, script_path=None): |
218 | 265 |
|
219 | 266 | with ExitStack() as stack: |
220 | | - p = (self.get_current_script_path() or Path("")).absolute().dirname() |
| 267 | + p = (script_path or Path("")).absolute().dirname() |
221 | 268 |
|
222 | 269 | if self.preferences["Add script dir to path"] and p.exists(): |
223 | 270 | sys.path.insert(0, p) |
@@ -292,35 +339,42 @@ def _cleanup_locals(self, module, injected_names): |
292 | 339 | @pyqtSlot(bool) |
293 | 340 | def render(self): |
294 | 341 |
|
| 342 | + if self._render_thread is not None and self._render_thread.isRunning(): |
| 343 | + return # ignore re-entrant render requests |
| 344 | + |
295 | 345 | seed(59798267586177) |
296 | 346 | if self.preferences["Reload CQ"]: |
297 | 347 | reload_cq() |
298 | 348 |
|
| 349 | + # Capture editor state on the main thread before handing off. |
299 | 350 | cq_script = self.get_current_script() |
300 | 351 | cq_script_path = self.get_current_script_path() |
301 | | - cq_code, module = self.compile_code(cq_script, cq_script_path) |
302 | 352 |
|
303 | | - if cq_code is None: |
304 | | - return |
| 353 | + for action in self._actions["Run"]: |
| 354 | + action.setEnabled(False) |
305 | 355 |
|
306 | | - cq_objects, injected_names = self._inject_locals(module) |
| 356 | + self._render_worker = RenderWorker(self, cq_script, cq_script_path) |
| 357 | + self._render_thread = QThread() |
| 358 | + self._render_worker.moveToThread(self._render_thread) |
307 | 359 |
|
308 | | - try: |
309 | | - self._exec(cq_code, module.__dict__, module.__dict__) |
| 360 | + self._render_thread.started.connect(self._render_worker.run) |
| 361 | + self._render_worker.sigRendered.connect(self.sigRendered) |
| 362 | + self._render_worker.sigTraceback.connect(self.sigTraceback) |
| 363 | + self._render_worker.sigLocals.connect(self.sigLocals) |
| 364 | + self._render_worker.finished.connect(self._render_thread.quit) |
| 365 | + self._render_worker.finished.connect(self._render_worker.deleteLater) |
| 366 | + self._render_thread.finished.connect(self._render_thread.deleteLater) |
| 367 | + self._render_thread.finished.connect(self._on_render_finished) |
310 | 368 |
|
311 | | - # remove the special methods |
312 | | - self._cleanup_locals(module, injected_names) |
| 369 | + self._render_thread.start() |
313 | 370 |
|
314 | | - # collect all CQ objects if no explicit show_object was called |
315 | | - if len(cq_objects) == 0: |
316 | | - cq_objects = find_cq_objects(module.__dict__) |
317 | | - self.sigRendered.emit(cq_objects) |
318 | | - self.sigTraceback.emit(None, cq_script) |
319 | | - self.sigLocals.emit(module.__dict__) |
320 | | - except Exception: |
321 | | - exc_info = sys.exc_info() |
322 | | - sys.last_traceback = exc_info[-1] |
323 | | - self.sigTraceback.emit(exc_info, cq_script) |
| 371 | + def _on_render_finished(self): |
| 372 | + |
| 373 | + self._render_thread = None |
| 374 | + self._render_worker = None |
| 375 | + |
| 376 | + for action in self._actions["Run"]: |
| 377 | + action.setEnabled(True) |
324 | 378 |
|
325 | 379 | @property |
326 | 380 | def breakpoints(self): |
|
0 commit comments