From 4023b20a82256dc2d3382d872ae9bcd6a9094e02 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Mon, 15 Feb 2021 12:01:15 -0500 Subject: [PATCH 001/134] Added stable release builds to readme --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 35aa39ed..e5058f7c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. * STL * STEP +## Installation (Binary Builds) + +Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download the zip file for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly. + +Development builds are also available, but you must be logged in to GitHub to get access. Click on the newest build with a green checkmark [here](https://github.com/jmwright/CQ-editor/actions?query=workflow%3Abuild), wait for the _Artifacts_ section at the bottom of the page to load, and then click on the appropriate download for your operating system. Extract the archive file and run the shell (Linux/MacOS) or batch (Windows) script in the root CQ-editor directory. The CQ-editor window should launch. + ## Installation (Anaconda) Use conda to install: @@ -47,12 +53,6 @@ On Fedora 29 the packages can be installed as follows: dnf install -y mesa-libGLU mesa-libGL mesa-libGLU-devel ``` -## Installation (Binary Builds) - -Development builds are now available that should work stand-alone without Anaconda. Click on the newest build with a green checkmark [here](https://github.com/jmwright/CQ-editor/actions?query=workflow%3Abuild), wait for the _Artifacts_ section at the bottom of the page to load, and then click on the appropriate download for your operating system. Extract the archive file and run the shell (*nix) or batch (Windows) script in the root CQ-editor directory. The CQ-editor window should launch. - -A stable version of these builds will be provided in the future, but are not available currently. - ## Usage ### Showing Objects From c327c393363c7f0bc7e93e69114e0d34338fdaf5 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Mon, 15 Feb 2021 15:13:37 -0500 Subject: [PATCH 002/134] Added stable release builds to readme (#227) --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 35aa39ed..e5058f7c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. * STL * STEP +## Installation (Binary Builds) + +Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download the zip file for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly. + +Development builds are also available, but you must be logged in to GitHub to get access. Click on the newest build with a green checkmark [here](https://github.com/jmwright/CQ-editor/actions?query=workflow%3Abuild), wait for the _Artifacts_ section at the bottom of the page to load, and then click on the appropriate download for your operating system. Extract the archive file and run the shell (Linux/MacOS) or batch (Windows) script in the root CQ-editor directory. The CQ-editor window should launch. + ## Installation (Anaconda) Use conda to install: @@ -47,12 +53,6 @@ On Fedora 29 the packages can be installed as follows: dnf install -y mesa-libGLU mesa-libGL mesa-libGLU-devel ``` -## Installation (Binary Builds) - -Development builds are now available that should work stand-alone without Anaconda. Click on the newest build with a green checkmark [here](https://github.com/jmwright/CQ-editor/actions?query=workflow%3Abuild), wait for the _Artifacts_ section at the bottom of the page to load, and then click on the appropriate download for your operating system. Extract the archive file and run the shell (*nix) or batch (Windows) script in the root CQ-editor directory. The CQ-editor window should launch. - -A stable version of these builds will be provided in the future, but are not available currently. - ## Usage ### Showing Objects From d7ec077e616d589dcbdcda5f6e9d202baebc89d4 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sun, 21 Feb 2021 09:52:36 -0500 Subject: [PATCH 003/134] Added some common usage patterns to the readme --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/README.md b/README.md index e5058f7c..82a2c8c7 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,58 @@ show_object(result, options={"alpha":0.5, "color": (64, 164, 223)}) ``` Note that `show_object` works for `Shape` and `TopoDS_Shape` objects too. In order to display objects from the embedded Python console use `show`. + +### Rotate, Pan and Zoom + +* _Left Mouse Button_ + _Drag_ = Rotate +* _Middle Mouse Button_ + _Drag_ = Pan +* _Right Mouse Button_ + _Drag_ = Zoom +* _Mouse Wheel_ = Zoom + +### Using an External Code Editor + +1. Open the Preferences dialog by clicking `Edit->Preferences`. +2. Make sure that `Code Editor` is selected in the left pane. +3. Check `autoreload` in the right pane. +4. If CQ-editor is not catching the saves from your external editor, increasing `Autoreload delay` in the right pane may help. This is a fairly common issue when using vim or emacs. + +### Displaying All Wires for Debugging + +**NOTE:** This is for debugging purposes and if not removed, could interfere with the creation of your model. + +Using `consolidateWires()` is a quick way to combine all wires so that they will display together in CQ-editor's viewer. In the following code, it is used to make sure that both rects are displayed. This technique can make it easier to debug in-progress 2D sketches. + +```python +import cadquery as cq +res = cq.Workplane().rect(1,1).rect(3,3).consolidateWires() +show_object(res) +``` + +### Highlighting a Specific Face + +Highlighting a specific face in a different color can be useful when debugging, or when trying to learn CadQuery selectors. The following code creates a separate, highlighted object to show the selected face in red. + +```python +import cadquery as cq + +result = cq.Workplane().box(10, 10, 10) + +highlight = result.faces('>Z') + +show_object(result) +show_object(highlight,'highlight',options=dict(alpha=0.1,color=(1.,0,0))) +``` + +### Naming an Object + +By default, objects have a randomly generated ID in the object inspector. It can be useful to name objects so that it is easier to identify them. The `name` parameter of `show_object()` can be used to do this. + +```python +import cadquery as cq + +result = cq.Workplane().box(10, 10, 10) + +highlight = result.faces('>Z') + +show_object(result, name='box') +``` From 333c0e98983b1cd6b89ceba93cd5a8fea11e14ff Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sun, 21 Feb 2021 17:27:51 -0500 Subject: [PATCH 004/134] Added logging and exporting, and proof-read the additions --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 82a2c8c7..f4561ea2 100644 --- a/README.md +++ b/README.md @@ -65,23 +65,62 @@ show_object(result, options={"alpha":0.5, "color": (64, 164, 223)}) Note that `show_object` works for `Shape` and `TopoDS_Shape` objects too. In order to display objects from the embedded Python console use `show`. -### Rotate, Pan and Zoom +### Rotate, Pan and Zoom the 3D View + +The following mouse controls can be used to alter the view of the 3D object, and should be familiar to CAD users, even if the mouse buttons used may differ. * _Left Mouse Button_ + _Drag_ = Rotate * _Middle Mouse Button_ + _Drag_ = Pan * _Right Mouse Button_ + _Drag_ = Zoom * _Mouse Wheel_ = Zoom +### Debugging Objects + +There are multiple menu options to help in debugging a CadQuery script. They are included in the `Run` menu, with corresponding buttons in the toolbar. Below is a listing of what each menu item does. + +* `Debug` (Ctrl + F5) - Instead of running the script completely through as with the `Render` item, it begins executing the script but stops at the first non-empty line, waiting for the user to continue execution manually. +* `Step` (Ctrl + F10) - Will move execution of the script to the next non-empty line. +* `Step in` (Ctrl + F11) - Currently disabled. +* `Continue` (Ctrl + F12) - Completes execution of the script, starting from the current line that is being debugged. + +It is also possible to do visual debugging of objects. This is possible by using the `debug()` function to display an object instead of `show_object()`. An alternative method for the following code snippet is shown below for highlighting a specific face, but it demonstrates one use of `debug()`. +```python +import cadquery as cq + +result = cq.Workplane().box(10, 10, 10) + +highlight = result.faces('>Z') + +show_object(result, name='box') +debug(highlight) +``` +Objects displayed with `debug()` are colored in red and have their alpha set so they are semi-transparent. This can be useful for checking for interference, clearance, or whether the expected face is being selected, as in the code above. + +### Console Logging + +Python's standard `print()` function will not output to the CQ-editor GUI, and `log()` should be used instead. `log()` will output the provided text to the _Log viewer_ panel, providing another way to debug CadQuery scripts. + ### Using an External Code Editor +Some users prefer to use an external code editor instead of the built-in Spyder-based editor that comes stock with CQ-editor. The steps below should allow CQ-editor to work alongside most text editors. + 1. Open the Preferences dialog by clicking `Edit->Preferences`. 2. Make sure that `Code Editor` is selected in the left pane. -3. Check `autoreload` in the right pane. -4. If CQ-editor is not catching the saves from your external editor, increasing `Autoreload delay` in the right pane may help. This is a fairly common issue when using vim or emacs. +3. Check `Autoreload` in the right pane. +4. If CQ-editor is not catching the saves from your external editor, increasing `Autoreload delay` in the right pane may help. This issue has been reported when using vim or emacs. + +### Exporting an Object + +Any object can be exported to either STEP or STL format. The steps for doing so are listed below. + +1. Highlight the object to be exported in the _Objects_ panel. +2. Click either `Export as STL` or `Export as STEP` from the `Tools` menu, depending on which file format you want to export. Both of these options will be disabled if an object is not selected in the _Objects_ panel. + +Clicking either _Export_ item will present a file dialog that allows the file name ad location of the export file to be set. ### Displaying All Wires for Debugging -**NOTE:** This is for debugging purposes and if not removed, could interfere with the creation of your model. +**NOTE:** This is intended for debugging purposes, and if not removed, could interfere with the execution of your model in some cases. Using `consolidateWires()` is a quick way to combine all wires so that they will display together in CQ-editor's viewer. In the following code, it is used to make sure that both rects are displayed. This technique can make it easier to debug in-progress 2D sketches. @@ -93,7 +132,7 @@ show_object(res) ### Highlighting a Specific Face -Highlighting a specific face in a different color can be useful when debugging, or when trying to learn CadQuery selectors. The following code creates a separate, highlighted object to show the selected face in red. +Highlighting a specific face in a different color can be useful when debugging, or when trying to learn CadQuery selectors. The following code creates a separate, highlighted object to show the selected face in red. This is an alternative to using a `debug()` object, and in most cases `debug()` will provide the same result with less code. However, this method will allow the color and alpha of the highlight object to be customized. ```python import cadquery as cq @@ -108,7 +147,7 @@ show_object(highlight,'highlight',options=dict(alpha=0.1,color=(1.,0,0))) ### Naming an Object -By default, objects have a randomly generated ID in the object inspector. It can be useful to name objects so that it is easier to identify them. The `name` parameter of `show_object()` can be used to do this. +By default, objects have a randomly generated ID in the object inspector. However, it can be useful to name objects so that it is easier to identify them. The `name` parameter of `show_object()` can be used to do this. ```python import cadquery as cq From fee64b272cacdaebc7028a6a02e27686efad8e1d Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Mon, 22 Feb 2021 06:16:52 -0500 Subject: [PATCH 005/134] Added quick mention of how print from within CQ-editor works --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4561ea2..468ca87b 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Objects displayed with `debug()` are colored in red and have their alpha set so ### Console Logging -Python's standard `print()` function will not output to the CQ-editor GUI, and `log()` should be used instead. `log()` will output the provided text to the _Log viewer_ panel, providing another way to debug CadQuery scripts. +Python's standard `print()` function will not output to the CQ-editor GUI, and `log()` should be used instead. `log()` will output the provided text to the _Log viewer_ panel, providing another way to debug CadQuery scripts. If you started CQ-editor from the command line, the `print()` function will output text back to it. ### Using an External Code Editor From 744b0184f8f37731b633af23e0d1dbc8da12debd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Tue, 23 Feb 2021 17:56:58 +0100 Subject: [PATCH 006/134] Fix step in (#236) * Fix step in * Improve coverage * Restore previous tracing function --- cq_editor/widgets/debugger.py | 42 ++++++++++++++++++++++++----------- tests/test_app.py | 6 +++++ 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 668bbe6b..d4231ca2 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -1,7 +1,8 @@ import sys, imp from enum import Enum, auto from imp import reload -from types import SimpleNamespace +from types import SimpleNamespace, FrameType +from typing import List from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction, QLabel, QTableView) @@ -119,6 +120,7 @@ class Debugger(QObject,ComponentMixin): sigCQChanged = pyqtSignal(dict,bool) sigDebugging = pyqtSignal(bool) + _frames : List[FrameType] def __init__(self,parent): @@ -148,13 +150,15 @@ def __init__(self,parent): 'Step in', self, shortcut='ctrl+F11', - triggered=lambda: None), + triggered=lambda: self.debug_cmd(DbgState.STEP_IN)), QAction(icon('arrow-continue'), 'Continue', self, shortcut='ctrl+F12', triggered=lambda: self.debug_cmd(DbgState.CONT)) ]} + + self._frames = [] def get_current_script(self): @@ -187,7 +191,7 @@ def _exec(self, code, locals_dict, globals_dict): if self.preferences['Change working dir to script dir'] and p.exists(): stack.enter_context(p) - exec(code, locals_dict, globals_dict) + exec(code, locals_dict, globals_dict) def _inject_locals(self,module): @@ -244,9 +248,16 @@ def render(self): except Exception: self.sigTraceback.emit(sys.exc_info(), cq_script) + + @property + def breakpoints(self): + return [ el[0] for el in self.get_breakpoints()] @pyqtSlot(bool) def debug(self,value): + + previous_trace = sys.gettrace() + if value: self.sigDebugging.emit(True) self.state = DbgState.STEP @@ -261,11 +272,10 @@ def debug(self,value): cq_objects,injected_names = self._inject_locals(module) - self.breakpoints = [ el[0] for el in self.get_breakpoints()] - #clear possible traceback self.sigTraceback.emit(None, self.script) + try: sys.settrace(self.trace_callback) exec(code,module.__dict__,module.__dict__) @@ -273,7 +283,7 @@ def debug(self,value): self.sigTraceback.emit(sys.exc_info(), self.script) finally: - sys.settrace(None) + sys.settrace(previous_trace) self.sigDebugging.emit(False) self._actions['Run'][1].setChecked(False) @@ -283,12 +293,13 @@ def debug(self,value): self._cleanup_locals(module,injected_names) self.sigLocals.emit(module.__dict__) + + self._frames = [] else: - sys.settrace(None) + sys.settrace(previous_trace) self.inner_event_loop.exit(0) - def debug_cmd(self,state=DbgState.STEP): self.state = state @@ -300,6 +311,8 @@ def trace_callback(self,frame,event,arg): filename = frame.f_code.co_filename if filename==DUMMY_FILE: + if not self._frames: + self._frames.append(frame) self.trace_local(frame,event,arg) return self.trace_callback @@ -309,12 +322,14 @@ def trace_callback(self,frame,event,arg): def trace_local(self,frame,event,arg): lineno = frame.f_lineno - line = self.script.splitlines()[lineno-1] - f_id = id(frame) - if event in (DbgEevent.LINE,DbgEevent.RETURN): - if (self.state in (DbgState.STEP, DbgState.STEP_IN)) \ + if event in (DbgEevent.LINE,): + if (self.state in (DbgState.STEP, DbgState.STEP_IN) and frame is self._frames[-1]) \ or (lineno in self.breakpoints): + + if lineno in self.breakpoints: + self._frames.append(frame) + self.sigLineChanged.emit(lineno) self.sigFrameChanged.emit(frame) self.sigLocalsChanged.emit(frame.f_locals) @@ -324,11 +339,12 @@ def trace_local(self,frame,event,arg): elif event in (DbgEevent.RETURN): self.sigLocalsChanged.emit(frame.f_locals) + self._frames.pop() elif event == DbgEevent.CALL: func_filename = frame.f_code.co_filename - if self.state == DbgState.STEP_IN and func_filename == DUMMY_FILE: self.sigLineChanged.emit(lineno) self.sigFrameChanged.emit(frame) self.state = DbgState.STEP + self._frame.append(frame) diff --git a/tests/test_app.py b/tests/test_app.py index 65ce95a4..6730a49f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -353,6 +353,12 @@ def patch_debugger(debugger,event_loop_mock): viewer = win.components['viewer'] assert(number_visible_items(viewer) == 3) + #check breakpoints + assert(debugger.breakpoints == []) + + #check _frames + assert(debugger._frames == []) + #test step through ev = event_loop([lambda: (assert_func(variables.model().rowCount() == 4), assert_func(number_visible_items(viewer) == 3), From 5d7b672428c9ce3ffe5481ff721fe3ea9db4d6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Tue, 23 Feb 2021 19:47:30 +0100 Subject: [PATCH 007/134] Fix typo --- cq_editor/widgets/debugger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index d4231ca2..10ed0be5 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -347,4 +347,4 @@ def trace_local(self,frame,event,arg): self.sigLineChanged.emit(lineno) self.sigFrameChanged.emit(frame) self.state = DbgState.STEP - self._frame.append(frame) + self._frames.append(frame) From 0bdd2aaf192d783d704a9d1209c7b151561fdb66 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 23 Feb 2021 14:11:55 -0500 Subject: [PATCH 008/134] Updated the Step in description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 468ca87b..4282f3ef 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ There are multiple menu options to help in debugging a CadQuery script. They are * `Debug` (Ctrl + F5) - Instead of running the script completely through as with the `Render` item, it begins executing the script but stops at the first non-empty line, waiting for the user to continue execution manually. * `Step` (Ctrl + F10) - Will move execution of the script to the next non-empty line. -* `Step in` (Ctrl + F11) - Currently disabled. +* `Step in` (Ctrl + F11) - Will follow the flow of execution to the inside of a user-created function defined within the script. * `Continue` (Ctrl + F12) - Completes execution of the script, starting from the current line that is being debugged. It is also possible to do visual debugging of objects. This is possible by using the `debug()` function to display an object instead of `show_object()`. An alternative method for the following code snippet is shown below for highlighting a specific face, but it demonstrates one use of `debug()`. From 641232d6216f4bf5daffcaef2d00bfbc2f9e5f94 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 24 Feb 2021 12:39:01 -0500 Subject: [PATCH 009/134] Readme update - Additional Usage Tips (#234) * Added stable release builds to readme * Added some common usage patterns to the readme * Added logging and exporting, and proof-read the additions * Added quick mention of how print from within CQ-editor works * Updated the Step in description --- README.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/README.md b/README.md index e5058f7c..4282f3ef 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,97 @@ show_object(result, options={"alpha":0.5, "color": (64, 164, 223)}) ``` Note that `show_object` works for `Shape` and `TopoDS_Shape` objects too. In order to display objects from the embedded Python console use `show`. + +### Rotate, Pan and Zoom the 3D View + +The following mouse controls can be used to alter the view of the 3D object, and should be familiar to CAD users, even if the mouse buttons used may differ. + +* _Left Mouse Button_ + _Drag_ = Rotate +* _Middle Mouse Button_ + _Drag_ = Pan +* _Right Mouse Button_ + _Drag_ = Zoom +* _Mouse Wheel_ = Zoom + +### Debugging Objects + +There are multiple menu options to help in debugging a CadQuery script. They are included in the `Run` menu, with corresponding buttons in the toolbar. Below is a listing of what each menu item does. + +* `Debug` (Ctrl + F5) - Instead of running the script completely through as with the `Render` item, it begins executing the script but stops at the first non-empty line, waiting for the user to continue execution manually. +* `Step` (Ctrl + F10) - Will move execution of the script to the next non-empty line. +* `Step in` (Ctrl + F11) - Will follow the flow of execution to the inside of a user-created function defined within the script. +* `Continue` (Ctrl + F12) - Completes execution of the script, starting from the current line that is being debugged. + +It is also possible to do visual debugging of objects. This is possible by using the `debug()` function to display an object instead of `show_object()`. An alternative method for the following code snippet is shown below for highlighting a specific face, but it demonstrates one use of `debug()`. +```python +import cadquery as cq + +result = cq.Workplane().box(10, 10, 10) + +highlight = result.faces('>Z') + +show_object(result, name='box') +debug(highlight) +``` +Objects displayed with `debug()` are colored in red and have their alpha set so they are semi-transparent. This can be useful for checking for interference, clearance, or whether the expected face is being selected, as in the code above. + +### Console Logging + +Python's standard `print()` function will not output to the CQ-editor GUI, and `log()` should be used instead. `log()` will output the provided text to the _Log viewer_ panel, providing another way to debug CadQuery scripts. If you started CQ-editor from the command line, the `print()` function will output text back to it. + +### Using an External Code Editor + +Some users prefer to use an external code editor instead of the built-in Spyder-based editor that comes stock with CQ-editor. The steps below should allow CQ-editor to work alongside most text editors. + +1. Open the Preferences dialog by clicking `Edit->Preferences`. +2. Make sure that `Code Editor` is selected in the left pane. +3. Check `Autoreload` in the right pane. +4. If CQ-editor is not catching the saves from your external editor, increasing `Autoreload delay` in the right pane may help. This issue has been reported when using vim or emacs. + +### Exporting an Object + +Any object can be exported to either STEP or STL format. The steps for doing so are listed below. + +1. Highlight the object to be exported in the _Objects_ panel. +2. Click either `Export as STL` or `Export as STEP` from the `Tools` menu, depending on which file format you want to export. Both of these options will be disabled if an object is not selected in the _Objects_ panel. + +Clicking either _Export_ item will present a file dialog that allows the file name ad location of the export file to be set. + +### Displaying All Wires for Debugging + +**NOTE:** This is intended for debugging purposes, and if not removed, could interfere with the execution of your model in some cases. + +Using `consolidateWires()` is a quick way to combine all wires so that they will display together in CQ-editor's viewer. In the following code, it is used to make sure that both rects are displayed. This technique can make it easier to debug in-progress 2D sketches. + +```python +import cadquery as cq +res = cq.Workplane().rect(1,1).rect(3,3).consolidateWires() +show_object(res) +``` + +### Highlighting a Specific Face + +Highlighting a specific face in a different color can be useful when debugging, or when trying to learn CadQuery selectors. The following code creates a separate, highlighted object to show the selected face in red. This is an alternative to using a `debug()` object, and in most cases `debug()` will provide the same result with less code. However, this method will allow the color and alpha of the highlight object to be customized. + +```python +import cadquery as cq + +result = cq.Workplane().box(10, 10, 10) + +highlight = result.faces('>Z') + +show_object(result) +show_object(highlight,'highlight',options=dict(alpha=0.1,color=(1.,0,0))) +``` + +### Naming an Object + +By default, objects have a randomly generated ID in the object inspector. However, it can be useful to name objects so that it is easier to identify them. The `name` parameter of `show_object()` can be used to do this. + +```python +import cadquery as cq + +result = cq.Workplane().box(10, 10, 10) + +highlight = result.faces('>Z') + +show_object(result, name='box') +``` From 85b1625d08ca0d273ba80a180d305d578f2e7528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Sat, 27 Feb 2021 10:25:03 +0100 Subject: [PATCH 010/134] Store the XCAF doc as shape to fix 7.5 assy crash (#238) * Store the XCAF doc as shape to fix 7.5 assy crash + enable direct display of AIS objects * typo fix --- cq_editor/cq_utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 4596fff1..633e819e 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -7,7 +7,7 @@ from OCP.XCAFPrs import XCAFPrs_AISObject from OCP.TopoDS import TopoDS_Shape -from OCP.AIS import AIS_ColoredShape +from OCP.AIS import AIS_Shape, AIS_ColoredShape from OCP.Quantity import \ Quantity_TOC_RGB as TOC_RGB, Quantity_Color @@ -45,16 +45,20 @@ def to_workplane(obj : cq.Shape): return rv -def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly], +def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly, AIS_Shape], options={}): + shape = None + if isinstance(obj, cq.Assembly): - ais = XCAFPrs_AISObject(toCAF(obj)[0]) - shape = None#cq.Shape(ais.Shape()) + label, shape = toCAF(obj) + ais = XCAFPrs_AISObject(label) + elif isinstance(obj, AIS_Shape): + ais = obj else: shape = to_compound(obj) ais = AIS_ColoredShape(shape.wrapped) - + if 'alpha' in options: ais.SetTransparency(options['alpha']) if 'color' in options: From 2ea50d40b0100984b86dc050eb42f2979881adc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Sat, 27 Feb 2021 10:46:21 +0100 Subject: [PATCH 011/134] Bump OCP version for testing --- cqgui_env.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqgui_env.yml b/cqgui_env.yml index 4854dda7..8791d885 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -9,7 +9,7 @@ dependencies: - pyqtgraph - python=3.8 - spyder=4 - - ocp=7.4 + - ocp=7.5 - path.py - logbook - requests From 32e731769ab044e3ba703a52f7a9fbd803021832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Sat, 27 Feb 2021 18:19:23 +0100 Subject: [PATCH 012/134] Test rendering of AIS objects (#239) --- tests/test_app.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/test_app.py b/tests/test_app.py index 6730a49f..795b808f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1175,3 +1175,42 @@ def test_render_assy(main): console.execute('show(assy)') qtbot.wait(500) assert(obj_tree_comp.CQ.childCount() == 2) + +code_show_ais = \ +'''import cadquery as cq +from cadquery.occ_impl.assembly import toCAF + +import OCP + +result1 = cq.Workplane("XY" ).box(3, 3, 0.5) +assy = cq.Assembly(result1) + +lab, doc = toCAF(assy) +ais = OCP.XCAFPrs.XCAFPrs_AISObject(lab) + +show_object(ais) +''' + +def test_render_ais(main): + + qtbot, win = main + + obj_tree_comp = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + console = win.components['console'] + + # check that object was removed + obj_tree_comp._toolbar_actions[0].triggered.emit() + assert(obj_tree_comp.CQ.childCount() == 0) + + # check that object was rendered usin explicit show_object call + editor.set_text(code_show_ais) + debugger._actions['Run'][0].triggered.emit() + qtbot.wait(500) + assert(obj_tree_comp.CQ.childCount() == 1) + + # test rendering via console + console.execute('show(ais)') + qtbot.wait(500) + assert(obj_tree_comp.CQ.childCount() == 2) From bb61e901e32133e26558dca5984819d1504f757b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Sun, 7 Mar 2021 20:50:57 +0100 Subject: [PATCH 013/134] Do not reload CQ by default (#247) * Do not reload CQ by default * Fix coverage --- cq_editor/widgets/debugger.py | 2 +- tests/test_app.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 10ed0be5..a24374e8 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -105,7 +105,7 @@ class Debugger(QObject,ComponentMixin): name = 'Debugger' preferences = Parameter.create(name='Preferences',children=[ - {'name': 'Reload CQ', 'type': 'bool', 'value': True}, + {'name': 'Reload CQ', 'type': 'bool', 'value': False}, {'name': 'Add script dir to path','type': 'bool', 'value': True}, {'name': 'Change working dir to script dir','type': 'bool', 'value': True}]) diff --git a/tests/test_app.py b/tests/test_app.py index 795b808f..170a6612 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -183,6 +183,9 @@ def test_render(main): debugger = win.components['debugger'] console = win.components['console'] log = win.components['log'] + + # enable CQ reloading + debugger.preferences['Reload CQ'] = True # check that object was rendered assert(obj_tree_comp.CQ.childCount() == 1) From 9393922f4f9357cd43694071a7cf4386dbe12655 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 9 Mar 2021 08:57:10 -0500 Subject: [PATCH 014/134] Fixed a stray code statement in the readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 4282f3ef..c500cae7 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,5 @@ import cadquery as cq result = cq.Workplane().box(10, 10, 10) -highlight = result.faces('>Z') - show_object(result, name='box') ``` From a8dc4ff3e54d7f74cfc25f9fa1509ccbd7d5442e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Mon, 15 Mar 2021 08:51:29 +0100 Subject: [PATCH 015/134] Use cq=master (#251) * Use cq=master * Update version to 0.3dev --- conda/meta.yaml | 2 +- cq_editor/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conda/meta.yaml b/conda/meta.yaml index d3d0cc45..b46cf6b1 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -18,7 +18,7 @@ requirements: run: - python {{ environ.get('PYTHON_VERSION') }} - - cadquery=2.1 + - cadquery=master - ocp - logbook - pyqt=5.* diff --git a/cq_editor/_version.py b/cq_editor/_version.py index d3ec452c..c7d7df65 100644 --- a/cq_editor/_version.py +++ b/cq_editor/_version.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.3.0dev" From 6fd8cdee41731b77f19504146d74d650c8254377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Fri, 2 Apr 2021 14:36:00 +0200 Subject: [PATCH 016/134] Build for py3.9 (#252) * Build for py3.9 * Do not pin conda-build * Use newer conda-build for py3.9 * Change channel prio --- azure-pipelines.yml | 26 +++++++++++++++++++++++++- cqgui_env.yml | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c928a068..8f61f73a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,7 +18,7 @@ jobs: - template: conda-build.yml@templates parameters: name: Linux - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-18.04' py_maj: 3 py_min: 8 conda_bld: '3.20.3' @@ -38,3 +38,27 @@ jobs: py_maj: 3 py_min: 8 conda_bld: '3.20.3' + +- template: conda-build.yml@templates + parameters: + name: Linux + vmImage: 'ubuntu-18.04' + py_maj: 3 + py_min: 9 + conda_bld: '3.21.4' + +- template: conda-build.yml@templates + parameters: + name: macOS + vmImage: 'macOS-10.15' + py_maj: 3 + py_min: 9 + conda_bld: '3.21.4' + +- template: conda-build.yml@templates + parameters: + name: Windows + vmImage: 'vs2017-win2016' + py_maj: 3 + py_min: 9 + conda_bld: '3.21.4' diff --git a/cqgui_env.yml b/cqgui_env.yml index 8791d885..96db700f 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -1,8 +1,8 @@ name: cq-occ-conda-test-py3 channels: - CadQuery - - defaults - conda-forge + - defaults dependencies: - pyqt=5 - pyparsing From 2139d469486f76327963560f127124645d1c1b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Sat, 3 Apr 2021 12:00:58 +0200 Subject: [PATCH 017/134] Set sys.last_traceback on errors (#255) * Set last traceback * correctly handle SyntaxError * Check if last_traceback is set * Adjust prios * Set exception info in debug too * Check both run and debug * Fixed tests --- cq_editor/widgets/debugger.py | 9 +++-- cq_editor/widgets/traceback_viewer.py | 10 +++-- tests/test_app.py | 54 ++++++++++++++++----------- 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index a24374e8..b6810d0f 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -246,8 +246,9 @@ def render(self): cq_script) self.sigLocals.emit(module.__dict__) except Exception: - self.sigTraceback.emit(sys.exc_info(), - cq_script) + exc_info = sys.exc_info() + sys.last_traceback = exc_info[-1] + self.sigTraceback.emit(exc_info, cq_script) @property def breakpoints(self): @@ -280,7 +281,9 @@ def debug(self,value): sys.settrace(self.trace_callback) exec(code,module.__dict__,module.__dict__) except Exception: - self.sigTraceback.emit(sys.exc_info(), + exc_info = sys.exc_info() + sys.last_traceback = exc_info[-1] + self.sigTraceback.emit(exc_info, self.script) finally: sys.settrace(previous_trace) diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index 1c1e2245..6d58f7b1 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -76,9 +76,11 @@ def addTraceback(self,exc_info,code): # handle the special case of a SyntaxError if t is SyntaxError: - root.addChild(QTreeWidgetItem([exc.filename, - str(exc.lineno), - exc.text.strip()])) + root.addChild(QTreeWidgetItem( + [exc.filename, + str(exc.lineno), + exc.text.strip() if exc.text else ''] + )) else: self.current_exception.setText('') @@ -91,4 +93,4 @@ def handleSelection(self,item,*args): if '' in f: self.sigHighlightLine.emit(line) - \ No newline at end of file + diff --git a/tests/test_app.py b/tests/test_app.py index 170a6612..252b2370 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -302,42 +302,40 @@ def test_inspect(main): insp._toolbar_actions[0].toggled.emit(False) assert(number_visible_items(viewer) == 3) +class event_loop(object): + '''Used to mock the QEventLoop for the debugger component + ''' -def test_debug(main,mocker): + def __init__(self,callbacks): - # store the tracing function - trace_function = sys.gettrace() + self.callbacks = callbacks + self.i = 0 - class event_loop(object): - '''Used to mock the QEventLoop for the debugger component - ''' + def exec_(self): + + if self.i Date: Fri, 9 Apr 2021 04:34:27 +0930 Subject: [PATCH 018/134] Set window title to filename (#228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Set window title to filename * Change title * Fix formatting Co-authored-by: Adam Urbańczyk --- cq_editor/main_window.py | 9 ++++++++- tests/test_app.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 9cfdc55c..d66890ec 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -22,7 +22,7 @@ class MainWindow(QMainWindow,MainMixin): - name = 'CQ GUI' + name = 'CQ-Editor' org = 'CadQuery' def __init__(self,parent=None): @@ -258,6 +258,8 @@ def prepare_actions(self): # trigger re-render when file is modified externally or saved self.components['editor'].triggerRerender \ .connect(self.components['debugger'].render) + self.components['editor'].sigFilenameChanged\ + .connect(self.handle_filename_change) def prepare_console(self): @@ -325,6 +327,11 @@ def cq_documentation(self): open_url('https://cadquery.readthedocs.io/en/latest/') + def handle_filename_change(self, fname): + + new_title = fname if fname else "*" + self.setWindowTitle(f"{self.name}: {new_title}") + if __name__ == "__main__": pass diff --git a/tests/test_app.py b/tests/test_app.py index 252b2370..e946c508 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1229,3 +1229,27 @@ def test_render_ais(main): console.execute('show(ais)') qtbot.wait(500) assert(obj_tree_comp.CQ.childCount() == 2) + +def test_window_title(monkeypatch, main): + + fname = 'test_window_title.py' + + with open(fname, 'w') as f: + f.write(code) + + qtbot, win = main + + #monkeypatch QFileDialog methods + def filename(*args, **kwargs): + return fname, None + + monkeypatch.setattr(QFileDialog, 'getOpenFileName', + staticmethod(filename)) + + win.components["editor"].open() + assert(win.windowTitle().endswith(fname)) + + # handle a new file + win.components["editor"].new() + # I don't really care what the title is, as long as it's not a filename + assert(not win.windowTitle().endswith('.py')) \ No newline at end of file From fbf8c8cc677e0bf04dce46486d4f1a53a6c4e9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Tue, 13 Apr 2021 19:00:23 +0200 Subject: [PATCH 019/134] Replace <> (#260) --- cq_editor/widgets/traceback_viewer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index 6d58f7b1..d5e0baa6 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -67,10 +67,11 @@ def addTraceback(self,exc_info,code): root.addChild(QTreeWidgetItem([el.filename, str(el.lineno), line])) - + exc_name = t.__name__ exc_msg = str(exc) - + exc_msg = exc_msg.replace('<', '<').replace('>', '>') #replace <> + self.current_exception.\ setText('{}: {}'.format(exc_name,exc_msg)) From 853c0f4391c4cd185b3ee7178a1303b11eda3ef6 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Sun, 21 Feb 2021 13:16:13 +0000 Subject: [PATCH 020/134] status bar messages to indicate rendering and viewing progress --- cq_editor/main_window.py | 47 +++++++++++++++++++++++---- cq_editor/widgets/debugger.py | 2 ++ cq_editor/widgets/object_tree.py | 20 +++++++----- cq_editor/widgets/traceback_viewer.py | 3 +- cq_editor/widgets/viewer.py | 37 +++++++++------------ 5 files changed, 71 insertions(+), 38 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index d66890ec..259e7574 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,6 +1,9 @@ import sys -from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction) +from typing import Optional +from PyQt5.QtCore import pyqtSlot, Qt +from PyQt5.QtGui import QPalette, QColor +from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction, QApplication) import cadquery as cq @@ -55,6 +58,8 @@ def __init__(self,parent=None): self.restoreWindow() self.restoreComponentState() + self.on_idle() + def closeEvent(self,event): self.saveWindow() @@ -192,7 +197,7 @@ def prepare_toolbar(self): self.toolbar = QToolBar('Main toolbar',self,objectName='Main toolbar') for c in self.components.values(): - add_actions(self.toolbar,c.toolbarActions()) + add_actions(self.toolbar, c.toolbarActions()) self.addToolBar(self.toolbar) @@ -203,18 +208,25 @@ def prepare_statusbar(self): def prepare_actions(self): + self.components['debugger'].sigRenderStarted \ + .connect(self.on_render_start) self.components['debugger'].sigRendered\ .connect(self.components['object_tree'].addObjects) self.components['debugger'].sigTraceback\ .connect(self.components['traceback_viewer'].addTraceback) + self.components['debugger'].sigRendered \ + .connect(lambda _: self.on_idle()) + self.components['debugger'].sigTraceback \ + .connect(lambda _: self.on_idle()) + self.components['debugger'].sigLocals\ .connect(self.components['variables_viewer'].update_frame) self.components['debugger'].sigLocals\ .connect(self.components['console'].push_vars) - self.components['object_tree'].sigObjectsAdded[list]\ - .connect(self.components['viewer'].display_many) - self.components['object_tree'].sigObjectsAdded[list,bool]\ + self.components['object_tree'].sigObjectsAdded[list, list]\ + .connect(lambda objects, names: self.components['viewer'].display_many(objects, None, names)) + self.components['object_tree'].sigObjectsAdded[list, bool, list]\ .connect(self.components['viewer'].display_many) self.components['object_tree'].sigItemChanged.\ connect(self.components['viewer'].update_item) @@ -229,6 +241,8 @@ def prepare_actions(self): self.components['viewer'].sigObjectSelected\ .connect(self.components['object_tree'].handleGraphicalSelection) + self.components['viewer'].sigDisplayProgress \ + .connect(self.on_display_progress) self.components['traceback_viewer'].sigHighlightLine\ .connect(self.components['editor'].go_to_line) @@ -332,6 +346,25 @@ def handle_filename_change(self, fname): new_title = fname if fname else "*" self.setWindowTitle(f"{self.name}: {new_title}") -if __name__ == "__main__": + def on_idle(self): + self.set_status_message('Idle', '#000000') - pass + @pyqtSlot() + def on_render_start(self): + self.set_status_message('Rendering...', '#ff0000') + + @pyqtSlot(int, int, str) + def on_display_progress(self, current: int, total: int, name: Optional[str]): + if current == total: + self.on_idle() + else: + message = f'Displaying Shape {current + 1} / {total}' + if name: + message += f' ({name})' + self.set_status_message(message, '#0000ff') + + def set_status_message(self, message: str, color: str): + self.statusBar().showMessage(message) + self.statusBar().setStyleSheet(f'color: {color}') + # required because rendering is currently done on the main thread + QApplication.processEvents() diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index b6810d0f..1a1d570d 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -110,6 +110,7 @@ class Debugger(QObject,ComponentMixin): {'name': 'Change working dir to script dir','type': 'bool', 'value': True}]) + sigRenderStarted = pyqtSignal() sigRendered = pyqtSignal(dict) sigLocals = pyqtSignal(dict) sigTraceback = pyqtSignal(object,str) @@ -221,6 +222,7 @@ def _cleanup_locals(self,module,injected_names): @pyqtSlot(bool) def render(self): + self.sigRenderStarted.emit() if self.preferences['Reload CQ']: reload_cq() diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index c0ea1dcf..3775b5da 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -91,7 +91,7 @@ class ObjectTree(QWidget,ComponentMixin): {'name': 'Clear all before each run', 'type': 'bool', 'value': True}, {'name': 'STL precision','type': 'float', 'value': .1}]) - sigObjectsAdded = pyqtSignal([list],[list,bool]) + sigObjectsAdded = pyqtSignal([list, list],[list, bool, list]) sigObjectsRemoved = pyqtSignal(list) sigCQObjectSelected = pyqtSignal(object) sigAISObjectsSelected = pyqtSignal(list) @@ -195,6 +195,7 @@ def addLines(self): origin = (0,0,0) ais_list = [] + names = [] for name,color,direction in zip(('X','Y','Z'), ('red','lawngreen','blue'), @@ -208,8 +209,9 @@ def addLines(self): ais=line)) ais_list.append(line) + names.append(name) - self.sigObjectsAdded.emit(ais_list) + self.sigObjectsAdded.emit(ais_list, names) def _current_properties(self): @@ -242,6 +244,7 @@ def addObjects(self,objects,clean=False,root=None): self.removeObjects() ais_list = [] + names = [] #remove empty objects objects_f = {k:v for k,v in objects.items() if not is_obj_empty(v.shape)} @@ -260,20 +263,21 @@ def addObjects(self,objects,clean=False,root=None): if child.properties['Visible']: ais_list.append(ais) - + names.append(name) + root.addChild(child) if request_fit_view: - self.sigObjectsAdded[list,bool].emit(ais_list,True) + self.sigObjectsAdded[list, bool, list].emit(ais_list, True, names) else: - self.sigObjectsAdded[list].emit(ais_list) + self.sigObjectsAdded[list, list].emit(ais_list, names) @pyqtSlot(object,str,object) def addObject(self,obj,name='',options={}): root = self.CQ - ais,shape_display = make_AIS(obj, options) + ais, shape_display = make_AIS(obj, options) root.addChild(ObjectTreeItem(name, shape=obj, @@ -281,7 +285,7 @@ def addObject(self,obj,name='',options={}): ais=ais, sig=self.sigObjectPropertiesChanged)) - self.sigObjectsAdded.emit([ais]) + self.sigObjectsAdded.emit([ais], name) @pyqtSlot(list) @pyqtSlot() @@ -305,7 +309,7 @@ def stashObjects(self,action : bool): self.removeObjects() self.CQ.addChildren(self._stash) ais_list = [el.ais for el in self._stash] - self.sigObjectsAdded.emit(ais_list) + self.sigObjectsAdded.emit(ais_list, [''] * len(ais_list)) @pyqtSlot() def removeSelected(self): diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index d5e0baa6..875429b1 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -35,8 +35,7 @@ def __init__(self,parent): self.tree = TracebackTree(self) self.current_exception = QLabel(self) - self.current_exception.setStyleSheet(\ - "QLabel {color : red; }"); + self.current_exception.setStyleSheet("QLabel {color : red; }"); layout(self, (self.current_exception, diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index a3d4c361..c58416f8 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from typing import List, Optional from PyQt5.QtWidgets import (QWidget, QPushButton, QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QFileDialog, QHBoxLayout, QFrame, QLabel, QApplication, @@ -45,6 +46,7 @@ class OCCViewer(QWidget,ComponentMixin): IMAGE_EXTENSIONS = 'png' sigObjectSelected = pyqtSignal(list) + sigDisplayProgress = pyqtSignal(int, int, str) def __init__(self,parent=None): @@ -140,31 +142,23 @@ def clear(self): context.PurgeDisplay() context.RemoveAll(True) - def _display(self,shape): - - ais = make_AIS(shape) - self.canvas.context.Display(shape,True) - - self.displayed_shapes.append(shape) - self.displayed_ais.append(ais) - - #self.canvas._display.Repaint() - @pyqtSlot(object) - def display(self,ais): - - context = self._get_context() - context.Display(ais,True) - - if self.preferences['Fit automatically']: self.fit() + def display(self, ais): + self.display_many([ais]) @pyqtSlot(list) - @pyqtSlot(list,bool) - def display_many(self,ais_list,fit=None): + @pyqtSlot(list, bool, list) + def display_many(self, ais_list, fit: Optional[bool] = None, names: Optional[List] = None): + if names is None: + names = [None] * len(ais_list) + assert len(ais_list) == len(names) context = self._get_context() - for ais in ais_list: - context.Display(ais,True) + num_objects = len(ais_list) + for i, (ais, name) in enumerate(zip(ais_list, names)): + self.sigDisplayProgress.emit(i, num_objects, name) + context.Display(ais, True) + self.sigDisplayProgress.emit(num_objects, num_objects, None) if self.preferences['Fit automatically'] and fit is None: self.fit() @@ -184,7 +178,8 @@ def update_item(self,item,col): def remove_items(self,ais_items): ctx = self._get_context() - for ais in ais_items: ctx.Erase(ais,True) + for ais in ais_items: + ctx.Erase(ais,True) @pyqtSlot() def redraw(self): From 7c16e99f1bc3501cb629f435eaf4010477349563 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Fri, 28 May 2021 22:48:43 +0100 Subject: [PATCH 021/134] set render button checked during rendering --- cq_editor/main_window.py | 2 ++ cq_editor/widgets/debugger.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 259e7574..9292abc8 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -347,10 +347,12 @@ def handle_filename_change(self, fname): self.setWindowTitle(f"{self.name}: {new_title}") def on_idle(self): + self.components['debugger'].set_rendering_state(False) self.set_status_message('Idle', '#000000') @pyqtSlot() def on_render_start(self): + self.components['debugger'].set_rendering_state(True) self.set_status_message('Rendering...', '#ff0000') @pyqtSlot(int, int, str) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 1a1d570d..4712f909 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -251,7 +251,12 @@ def render(self): exc_info = sys.exc_info() sys.last_traceback = exc_info[-1] self.sigTraceback.emit(exc_info, cq_script) - + + def set_rendering_state(self, rendering): + render_action = self._actions['Run'][0] + render_action.setCheckable(rendering) + render_action.setChecked(rendering) + @property def breakpoints(self): return [ el[0] for el in self.get_breakpoints()] From e16d5eeca1020762d6cc2467b348c3b88d763dc6 Mon Sep 17 00:00:00 2001 From: Matt Broadway Date: Mon, 26 Jul 2021 07:27:57 +0100 Subject: [PATCH 022/134] Multi module support (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * unload modules loaded from the user's script * add imported modules to the file watcher * added unit test * added user preferences for new features * Changed naming * Add test for mult-module reload * Add nlopt * Change imported module name Co-authored-by: Adam Urbańczyk --- cq_editor/widgets/debugger.py | 53 +++++++++++++++++++-------------- cq_editor/widgets/editor.py | 50 ++++++++++++++++++++++++------- cqgui_env.yml | 1 + run.py | 4 ++- tests/test_app.py | 56 ++++++++++++++++++++++++++++++----- 5 files changed, 124 insertions(+), 40 deletions(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index b6810d0f..25fd5b4a 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -1,25 +1,21 @@ -import sys, imp +import sys +from contextlib import ExitStack, contextmanager from enum import Enum, auto -from imp import reload -from types import SimpleNamespace, FrameType +from types import SimpleNamespace, FrameType, ModuleType from typing import List -from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction, - QLabel, QTableView) -from PyQt5.QtCore import Qt, QObject, pyqtSlot, pyqtSignal, QEventLoop, QAbstractTableModel +import cadquery as cq from PyQt5 import QtCore - -from pyqtgraph.parametertree import Parameter, ParameterTree +from PyQt5.QtCore import Qt, QObject, pyqtSlot, pyqtSignal, QEventLoop, QAbstractTableModel +from PyQt5.QtWidgets import (QAction, + QTableView) from logbook import info -from spyder.utils.icon_manager import icon from path import Path -from contextlib import ExitStack - -import cadquery as cq +from pyqtgraph.parametertree import Parameter +from spyder.utils.icon_manager import icon -from ..mixins import ComponentMixin -from ..utils import layout from ..cq_utils import find_cq_objects, reload_cq +from ..mixins import ComponentMixin DUMMY_FILE = '' @@ -107,7 +103,9 @@ class Debugger(QObject,ComponentMixin): preferences = Parameter.create(name='Preferences',children=[ {'name': 'Reload CQ', 'type': 'bool', 'value': False}, {'name': 'Add script dir to path','type': 'bool', 'value': True}, - {'name': 'Change working dir to script dir','type': 'bool', 'value': True}]) + {'name': 'Change working dir to script dir','type': 'bool', 'value': True}, + {'name': 'Reload imported modules', 'type': 'bool', 'value': True}, + ]) sigRendered = pyqtSignal(dict) @@ -168,16 +166,15 @@ def get_breakpoints(self): return self.parent().components['editor'].debugger.get_breakpoints() - def compile_code(self,cq_script): + def compile_code(self, cq_script): try: - module = imp.new_module('temp') - cq_code = compile(cq_script,'','exec') - return cq_code,module + module = ModuleType('temp') + cq_code = compile(cq_script, '', 'exec') + return cq_code, module except Exception: - self.sigTraceback.emit(sys.exc_info(), - cq_script) - return None,None + self.sigTraceback.emit(sys.exc_info(), cq_script) + return None, None def _exec(self, code, locals_dict, globals_dict): @@ -190,6 +187,8 @@ def _exec(self, code, locals_dict, globals_dict): stack.callback(sys.path.remove, p) if self.preferences['Change working dir to script dir'] and p.exists(): stack.enter_context(p) + if self.preferences['Reload imported modules']: + stack.enter_context(module_manager()) exec(code, locals_dict, globals_dict) @@ -351,3 +350,13 @@ def trace_local(self,frame,event,arg): self.sigFrameChanged.emit(frame) self.state = DbgState.STEP self._frames.append(frame) + + +@contextmanager +def module_manager(): + """ unloads any modules loaded while the context manager is active """ + loaded_modules = set(sys.modules.keys()) + yield + new_modules = set(sys.modules.keys()) - loaded_modules + for module_name in new_modules: + del sys.modules[module_name] diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index dd9586a0..d2e2028f 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -1,3 +1,6 @@ +import os +from modulefinder import ModuleFinder + from spyder.plugins.editor.widgets.codeeditor import CodeEditor from PyQt5.QtCore import pyqtSignal, QFileSystemWatcher, QTimer from PyQt5.QtWidgets import QAction, QFileDialog @@ -26,6 +29,7 @@ class Editor(CodeEditor,ComponentMixin): {'name': 'Font size', 'type': 'int', 'value': 12}, {'name': 'Autoreload', 'type': 'bool', 'value': False}, {'name': 'Autoreload delay', 'type': 'int', 'value': 50}, + {'name': 'Autoreload: watch imported modules', 'type': 'bool', 'value': False}, {'name': 'Line wrap', 'type': 'bool', 'value': False}, {'name': 'Color scheme', 'type': 'list', 'values': ['Spyder','Monokai','Zenburn'], 'value': 'Spyder'}]) @@ -116,9 +120,12 @@ def updatePreferences(self,*args): .setChecked(self.preferences['Autoreload']) self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) - + self.toggle_wrap_mode(self.preferences['Line wrap']) + self._clear_watched_paths() + self._watch_paths() + def confirm_discard(self): if self.modified: @@ -156,14 +163,14 @@ def save(self): if self._filename != '': if self.preferences['Autoreload']: - self._file_watcher.removePath(self.filename) + self._file_watcher.blockSignals(True) self._file_watch_timer.stop() - with open(self._filename,'w') as f: + with open(self._filename, 'w') as f: f.write(self.toPlainText()) if self.preferences['Autoreload']: - self._file_watcher.addPath(self.filename) + self._file_watcher.blockSignals(False) self.triggerRerender.emit(True) self.reset_modified() @@ -183,15 +190,15 @@ def save_as(self): def _update_filewatcher(self): if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']): - self._file_watcher.removePath(self._watched_file) + self._clear_watched_paths() self._watched_file = None if self.preferences['Autoreload'] and self.filename and self.filename != self._watched_file: self._watched_file = self._filename - self._file_watcher.addPath(self.filename) + self._watch_paths() @property def filename(self): - return self._filename + return self._filename @filename.setter def filename(self, fname): @@ -199,11 +206,21 @@ def filename(self, fname): self._update_filewatcher() self.sigFilenameChanged.emit(fname) + def _clear_watched_paths(self): + paths = self._file_watcher.files() + if paths: + self._file_watcher.removePaths(paths) + + def _watch_paths(self): + if self._filename: + self._file_watcher.addPath(self._filename) + if self.preferences['Autoreload: watch imported modules']: + self._file_watcher.addPaths(get_imported_module_paths(self._filename)) + # callback triggered by QFileSystemWatcher def _file_changed(self): - # neovim writes a file by removing it first - # this causes QFileSystemWatcher to forget the file - self._file_watcher.addPath(self._filename) + # neovim writes a file by removing it first so must re-add each time + self._watch_paths() self.set_text_from_file(self._filename) self.triggerRerender.emit(True) @@ -236,6 +253,19 @@ def restoreComponentState(self,store): except IOError: self._logger.warning(f'could not open {filename}') + +def get_imported_module_paths(module_path): + finder = ModuleFinder([os.path.dirname(module_path)]) + finder.run_script(module_path) + imported_modules = [] + for module_name, module in finder.modules.items(): + if module_name != '__main__': + path = getattr(module, '__file__', None) + if path is not None and os.path.isfile(path): + imported_modules.append(path) + return imported_modules + + if __name__ == "__main__": from PyQt5.QtWidgets import QApplication diff --git a/cqgui_env.yml b/cqgui_env.yml index 96db700f..10c0758e 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -17,6 +17,7 @@ dependencies: - typing_extensions - scipy - nptyping + - nlopt - pip - pip: - "https://github.com/CadQuery/cadquery/archive/master.zip" diff --git a/run.py b/run.py index 66112f4c..8c0badf6 100644 --- a/run.py +++ b/run.py @@ -11,4 +11,6 @@ from cq_editor.__main__ import main -main() + +if __name__ == '__main__': + main() diff --git a/tests/test_app.py b/tests/test_app.py index e946c508..53690b47 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -14,7 +14,7 @@ from PyQt5.QtWidgets import QFileDialog, QMessageBox from cq_editor.__main__ import MainWindow -from cq_editor.widgets.editor import Editor +from cq_editor.widgets.editor import Editor, get_imported_module_paths from cq_editor.cq_utils import export, get_occ_color code = \ @@ -73,12 +73,19 @@ result2 = cq.Workplane("XY" ).box(3, 3, 0.5).translate((0,15,0)) ''' -def _modify_file(code): - with open('test.py', 'w', 1) as f: - f.write(code) +code_nested_top = """import test_nested_bottom +""" -def modify_file(code): - p = Process(target=_modify_file,args=(code,)) +code_nested_bottom = """a=1 +""" + +def _modify_file(code, path="test.py"): + with open(path, "w", 1) as f: + f.write(code) + + +def modify_file(code, path="test.py"): + p = Process(target=_modify_file, args=(code,path)) p.start() p.join() @@ -645,6 +652,30 @@ def test_editor_autoreload(monkeypatch,editor): # Saving a file with autoreload enabled should trigger a rerender. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): editor.save() + +def test_autoreload_nested(editor): + + qtbot, editor = editor + + TIMEOUT = 500 + + editor.preferences['Autoreload: watch imported modules'] = True + + with open('test_nested_top.py','w') as f: + f.write(code_nested_top) + + with open('test_nested_bottom.py','w') as f: + f.write("") + + assert(editor.get_text_with_eol() == '') + + editor.load_from_file('test_nested_top.py') + assert(len(editor.get_text_with_eol()) > 0) + + # wait for reload. + with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): + # modify file - NB: separate process is needed to avoid Widows quirks + modify_file(code_nested_bottom, 'test_nested_bottom.py') def test_console(main): @@ -1252,4 +1283,15 @@ def filename(*args, **kwargs): # handle a new file win.components["editor"].new() # I don't really care what the title is, as long as it's not a filename - assert(not win.windowTitle().endswith('.py')) \ No newline at end of file + assert(not win.windowTitle().endswith('.py')) + + +def test_module_discovery(tmp_path): + with open(tmp_path.joinpath('main.py'), 'w') as f: + f.write('import b') + + assert get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [] + + tmp_path.joinpath('b.py').touch() + + assert get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [str(tmp_path.joinpath('b.py'))] From 0cc78c55b3250622b766f6a4fc2728596d4ecf7d Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 17 Aug 2021 11:33:03 -0400 Subject: [PATCH 023/134] Cleaned up the pre-built packages section of the readme a little bit --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c500cae7..7bd97418 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,15 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. * STL * STEP -## Installation (Binary Builds) +## Installation - Pre-Built Packages (Recommended) + +### Release Packages Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download the zip file for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly. -Development builds are also available, but you must be logged in to GitHub to get access. Click on the newest build with a green checkmark [here](https://github.com/jmwright/CQ-editor/actions?query=workflow%3Abuild), wait for the _Artifacts_ section at the bottom of the page to load, and then click on the appropriate download for your operating system. Extract the archive file and run the shell (Linux/MacOS) or batch (Windows) script in the root CQ-editor directory. The CQ-editor window should launch. +### Development Packages + +Development builds are also available, but can be unstable and should be used at your own risk. Click on the newest build with a green checkmark [here](https://github.com/jmwright/CQ-editor/actions?query=workflow%3Abuild), wait for the _Artifacts_ section at the bottom of the page to load, and then click on the appropriate download for your operating system. Extract the archive file and run the shell (Linux/MacOS) or cmd (Windows) script in the root CQ-editor directory. The CQ-editor window should launch. ## Installation (Anaconda) From 8e1ea314964ed6dc774c2bcf1e90643748dd8fc4 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Tue, 19 Oct 2021 02:59:36 -0400 Subject: [PATCH 024/134] Fix test autoreload_nested to be independent of other tests (#285) --- tests/test_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index 53690b47..08e23790 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -659,6 +659,7 @@ def test_autoreload_nested(editor): TIMEOUT = 500 + editor.autoreload(True) editor.preferences['Autoreload: watch imported modules'] = True with open('test_nested_top.py','w') as f: @@ -674,7 +675,7 @@ def test_autoreload_nested(editor): # wait for reload. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): - # modify file - NB: separate process is needed to avoid Widows quirks + # modify file - NB: separate process is needed to avoid Windows quirks modify_file(code_nested_bottom, 'test_nested_bottom.py') def test_console(main): From d76b7e78495d450307b1e3e6c76c83c59b5fcbc4 Mon Sep 17 00:00:00 2001 From: Elliot Waite Date: Tue, 19 Oct 2021 13:04:32 -0700 Subject: [PATCH 025/134] Fix typo in README.md (#279) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bd97418..9e3ad17b 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Any object can be exported to either STEP or STL format. The steps for doing so 1. Highlight the object to be exported in the _Objects_ panel. 2. Click either `Export as STL` or `Export as STEP` from the `Tools` menu, depending on which file format you want to export. Both of these options will be disabled if an object is not selected in the _Objects_ panel. -Clicking either _Export_ item will present a file dialog that allows the file name ad location of the export file to be set. +Clicking either _Export_ item will present a file dialog that allows the file name and location of the export file to be set. ### Displaying All Wires for Debugging From dc950180b365ae39840f6787c8f5a061492734ed Mon Sep 17 00:00:00 2001 From: Lorenz Date: Mon, 25 Oct 2021 15:22:11 -0400 Subject: [PATCH 026/134] Fix AppVeyor build failure (#293) * Fix AppVeyor build failure * Do not pin pyvirtualdisplay * Disable redundant Ubuntu 18.04 build * Enable Ubuntu 20.04 Co-authored-by: AU --- appveyor.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a386d9cb..db37ba83 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,9 +3,10 @@ shallow_clone: false image: # - macOS-mojave # - macOS - - Ubuntu +# - Ubuntu + - Ubuntu2004 - Ubuntu1804 - - Visual Studio 2015 + - Visual Studio 2015 environment: matrix: @@ -32,7 +33,7 @@ install: - cmd: activate cqgui - conda list - pip install pytest pluggy pytest-qt - - pip install pytest-mock pytest-cov pytest-repeat codecov pyvirtualdisplay==0.2.1 + - pip install pytest-mock pytest-cov pytest-repeat codecov pyvirtualdisplay build: false From bba0aed5efafb8f2af551b57b7ec0b81925d6472 Mon Sep 17 00:00:00 2001 From: AU Date: Sun, 14 Nov 2021 19:01:29 +0100 Subject: [PATCH 027/134] Sketch support (#297) * Sketch support * Update cqgui_env.yml * Try using mamba * Update the curl call * Update appveyor.yml --- appveyor.yml | 20 ++++++++------------ cq_editor/cq_utils.py | 14 ++++++++++++-- cqgui_env.yml | 10 +--------- tests/test_app.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index db37ba83..abf05ed1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,9 +1,6 @@ shallow_clone: false image: -# - macOS-mojave -# - macOS -# - Ubuntu - Ubuntu2004 - Ubuntu1804 - Visual Studio 2015 @@ -17,21 +14,20 @@ environment: install: - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then sudo apt update; sudo apt -y --force-yes install libglu1-mesa xvfb libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev; fi - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then curl -o miniconda.sh curl -o miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh; fi - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then curl -o miniconda.sh curl -o miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-4.7.10-MacOSX-x86_64.sh; fi + - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Linux-x86_64.sh; fi + - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-MacOSX-x86_64.sh; fi - sh: bash miniconda.sh -b -p $HOME/miniconda - sh: source $HOME/miniconda/bin/activate - - cmd: appveyor DownloadFile https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe - - cmd: Miniconda3-latest-Windows-x86_64.exe /S /InstallationType=JustMe /D=%MINICONDA_DIRNAME% + - cmd: curl -fsSL -o miniconda.exe https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Windows-x86_64.exe + - cmd: miniconda.exe /S /InstallationType=JustMe /D=%MINICONDA_DIRNAME% - cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - cmd: activate - - conda config --set always_yes yes - - conda install -c conda-forge python=3.7 - - conda info - - conda env create --name cqgui -f cqgui_env.yml + - mamba config --set always_yes yes + - mamba info + - mamba env create --name cqgui -f cqgui_env.yml - sh: source activate cqgui - cmd: activate cqgui - - conda list + - mamba list - pip install pytest pluggy pytest-qt - pip install pytest-mock pytest-cov pytest-repeat codecov pyvirtualdisplay diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 633e819e..a16eb714 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -1,7 +1,7 @@ import cadquery as cq from cadquery.occ_impl.assembly import toCAF -from typing import List, Union, Tuple +from typing import List, Union from imp import reload from types import SimpleNamespace @@ -17,7 +17,7 @@ def find_cq_objects(results : dict): return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if isinstance(v,cq.Workplane)} -def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape]]): +def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch]): vals = [] @@ -33,6 +33,11 @@ def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq. vals.append(cq.Shape.cast(obj)) elif isinstance(obj,list) and isinstance(obj[0],TopoDS_Shape): vals.extend(cq.Shape.cast(o) for o in obj) + elif isinstance(obj, cq.Sketch): + if obj._faces: + vals.append(obj._faces) + else: + vals.extend(obj._edges) else: raise ValueError(f'Invalid type {type(obj)}') @@ -110,12 +115,17 @@ def get_occ_color(ais : AIS_ColoredShape) -> QColor: def reload_cq(): # NB: order of reloads is important + reload(cq.types) reload(cq.occ_impl.geom) reload(cq.occ_impl.shapes) + reload(cq.occ_impl.importers.dxf) reload(cq.occ_impl.importers) reload(cq.occ_impl.solver) reload(cq.occ_impl.assembly) + reload(cq.occ_impl.sketch_solver) + reload(cq.hull) reload(cq.selectors) + reload(cq.sketch) reload(cq.occ_impl.exporters.svg) reload(cq.cq) reload(cq.occ_impl.exporters.utils) diff --git a/cqgui_env.yml b/cqgui_env.yml index 10c0758e..eb5e7634 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -9,15 +9,7 @@ dependencies: - pyqtgraph - python=3.8 - spyder=4 - - ocp=7.5 - path.py - logbook - requests - - ezdxf - - typing_extensions - - scipy - - nptyping - - nlopt - - pip - - pip: - - "https://github.com/CadQuery/cadquery/archive/master.zip" + - cadquery=master diff --git a/tests/test_app.py b/tests/test_app.py index 08e23790..168c540f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1262,6 +1262,40 @@ def test_render_ais(main): qtbot.wait(500) assert(obj_tree_comp.CQ.childCount() == 2) +code_show_sketch = \ +'''import cadquery as cq + +s1 = cq.Sketch().rect(1,1) +s2 = cq.Sketch().segment((0,0), (0,3.),"s1") + +show_object(s1) +show_object(s2) +''' + +def test_render_sketch(main): + + qtbot, win = main + + obj_tree_comp = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + console = win.components['console'] + + # check that object was removed + obj_tree_comp._toolbar_actions[0].triggered.emit() + assert(obj_tree_comp.CQ.childCount() == 0) + + # check that object was rendered usin explicit show_object call + editor.set_text(code_show_sketch) + debugger._actions['Run'][0].triggered.emit() + qtbot.wait(500) + assert(obj_tree_comp.CQ.childCount() == 2) + + # test rendering via console + console.execute('show(s1); show(s2)') + qtbot.wait(500) + assert(obj_tree_comp.CQ.childCount() == 4) + def test_window_title(monkeypatch, main): fname = 'test_window_title.py' From fbba9dc95e8d13286844464755d907f54822a2fa Mon Sep 17 00:00:00 2001 From: Lorenz Date: Fri, 26 Nov 2021 13:23:39 -0500 Subject: [PATCH 028/134] Handle exception when watching imported modules (#300) * Handle exception when watching imported modules * Use conda 3.21.6 Co-authored-by: AU --- azure-pipelines.yml | 12 ++++---- cq_editor/widgets/debugger.py | 11 ++++--- tests/test_app.py | 56 +++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8f61f73a..13d41b1a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -21,7 +21,7 @@ jobs: vmImage: 'ubuntu-18.04' py_maj: 3 py_min: 8 - conda_bld: '3.20.3' + conda_bld: 3.21.6 - template: conda-build.yml@templates parameters: @@ -29,7 +29,7 @@ jobs: vmImage: 'macOS-10.15' py_maj: 3 py_min: 8 - conda_bld: '3.20.3' + conda_bld: 3.21.6 - template: conda-build.yml@templates parameters: @@ -37,7 +37,7 @@ jobs: vmImage: 'vs2017-win2016' py_maj: 3 py_min: 8 - conda_bld: '3.20.3' + conda_bld: 3.21.6 - template: conda-build.yml@templates parameters: @@ -45,7 +45,7 @@ jobs: vmImage: 'ubuntu-18.04' py_maj: 3 py_min: 9 - conda_bld: '3.21.4' + conda_bld: 3.21.6 - template: conda-build.yml@templates parameters: @@ -53,7 +53,7 @@ jobs: vmImage: 'macOS-10.15' py_maj: 3 py_min: 9 - conda_bld: '3.21.4' + conda_bld: 3.21.6 - template: conda-build.yml@templates parameters: @@ -61,4 +61,4 @@ jobs: vmImage: 'vs2017-win2016' py_maj: 3 py_min: 9 - conda_bld: '3.21.4' + conda_bld: 3.21.6 diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 25fd5b4a..ffddfa03 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -356,7 +356,10 @@ def trace_local(self,frame,event,arg): def module_manager(): """ unloads any modules loaded while the context manager is active """ loaded_modules = set(sys.modules.keys()) - yield - new_modules = set(sys.modules.keys()) - loaded_modules - for module_name in new_modules: - del sys.modules[module_name] + + try: + yield + finally: + new_modules = set(sys.modules.keys()) - loaded_modules + for module_name in new_modules: + del sys.modules[module_name] diff --git a/tests/test_app.py b/tests/test_app.py index 168c540f..b508b47b 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1330,3 +1330,59 @@ def test_module_discovery(tmp_path): tmp_path.joinpath('b.py').touch() assert get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [str(tmp_path.joinpath('b.py'))] + +code_import_module_makebox = \ +""" +from module_makebox import * +z = 1 +r = makebox(z) +""" + +code_module_makebox = \ +""" +import cadquery as cq +def makebox(z): + zval = z + 1 + return cq.Workplane().box(1, 1, zval) +""" + +def test_reload_import_handle_error(tmp_path, main): + + TIMEOUT = 500 + qtbot, win = main + editor = win.components["editor"] + debugger = win.components["debugger"] + traceback_view = win.components["traceback_viewer"] + + editor.autoreload(True) + editor.preferences["Autoreload: watch imported modules"] = True + + # save the module and top level script files + module_file = Path(tmp_path).joinpath("module_makebox.py") + script = Path(tmp_path).joinpath("main.py") + modify_file(code_module_makebox, module_file) + modify_file(code_import_module_makebox, script) + + # run, verify that no exception was generated + editor.load_from_file(script) + debugger._actions["Run"][0].triggered.emit() + assert(traceback_view.current_exception.text() == "") + + # save the module with an error + with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): + lines = code_module_makebox.splitlines() + lines.remove(" zval = z + 1") # introduce NameError + lines = "\n".join(lines) + modify_file(lines, module_file) + + # verify NameError is generated + debugger._actions["Run"][0].triggered.emit() + assert("NameError" in traceback_view.current_exception.text()) + + # revert the error, verify rerender is triggered + with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): + modify_file(code_module_makebox, module_file) + + # verify that no exception was generated + debugger._actions["Run"][0].triggered.emit() + assert(traceback_view.current_exception.text() == "") From 8b877642c6875dd2266d4473ea4865c79afb3e99 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Fri, 26 Nov 2021 13:24:51 -0500 Subject: [PATCH 029/134] Add shortcuts for perspective buttons (#296) * Add shortcuts for perspective buttons * Show shortcut in tooltip --- cq_editor/widgets/viewer.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index a3d4c361..896ba0a8 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -82,44 +82,54 @@ def create_actions(self,parent): self._actions = \ {'View' : [QAction(qta.icon('fa.arrows-alt'), - 'Fit', + 'Fit (Shift+F1)', parent, + shortcut='shift+F1', triggered=self.fit), QAction(QIcon(':/images/icons/isometric_view.svg'), - 'Iso', + 'Iso (Shift+F2)', parent, + shortcut='shift+F2', triggered=self.iso_view), QAction(QIcon(':/images/icons/top_view.svg'), - 'Top', + 'Top (Shift+F3)', parent, + shortcut='shift+F3', triggered=self.top_view), QAction(QIcon(':/images/icons/bottom_view.svg'), - 'Bottom', + 'Bottom (Shift+F4)', parent, + shortcut='shift+F4', triggered=self.bottom_view), QAction(QIcon(':/images/icons/front_view.svg'), - 'Front', + 'Front (Shift+F5)', parent, + shortcut='shift+F5', triggered=self.front_view), QAction(QIcon(':/images/icons/back_view.svg'), - 'Back', + 'Back (Shift+F6)', parent, + shortcut='shift+F6', triggered=self.back_view), QAction(QIcon(':/images/icons/left_side_view.svg'), - 'Left', + 'Left (Shift+F7)', parent, + shortcut='shift+F7', triggered=self.left_view), QAction(QIcon(':/images/icons/right_side_view.svg'), - 'Right', + 'Right (Shift+F8)', parent, + shortcut='shift+F8', triggered=self.right_view), QAction(qta.icon('fa.square-o'), - 'Wireframe', + 'Wireframe (Shift+F9)', parent, + shortcut='shift+F9', triggered=self.wireframe_view), QAction(qta.icon('fa.square'), - 'Shaded', + 'Shaded (Shift+F10)', parent, + shortcut='shift+F10', triggered=self.shaded_view)], 'Tools' : [QAction(icon('screenshot'), 'Screenshot', From e5739c5a5c3ffcdf7afc293323cd819d04434946 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Fri, 26 Nov 2021 17:05:24 -0500 Subject: [PATCH 030/134] Handle SyntaxError adding imported modules to file watcher on launch (#292) * Handle SyntaxError adding imported modules to file watcher on launch * Fix logging Co-authored-by: AU --- cq_editor/widgets/editor.py | 31 ++++++++++++++++++++----------- tests/test_app.py | 25 +++++++++++++++++++++---- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index d2e2028f..476f44cf 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -215,7 +215,9 @@ def _watch_paths(self): if self._filename: self._file_watcher.addPath(self._filename) if self.preferences['Autoreload: watch imported modules']: - self._file_watcher.addPaths(get_imported_module_paths(self._filename)) + module_paths = self.get_imported_module_paths(self._filename) + if module_paths: + self._file_watcher.addPaths(module_paths) # callback triggered by QFileSystemWatcher def _file_changed(self): @@ -254,16 +256,23 @@ def restoreComponentState(self,store): self._logger.warning(f'could not open {filename}') -def get_imported_module_paths(module_path): - finder = ModuleFinder([os.path.dirname(module_path)]) - finder.run_script(module_path) - imported_modules = [] - for module_name, module in finder.modules.items(): - if module_name != '__main__': - path = getattr(module, '__file__', None) - if path is not None and os.path.isfile(path): - imported_modules.append(path) - return imported_modules + def get_imported_module_paths(self, module_path): + + finder = ModuleFinder([os.path.dirname(module_path)]) + imported_modules = [] + + try: + finder.run_script(module_path) + except SyntaxError as err: + self._logger.warning(f'Syntax error in {module_path}: {err}') + else: + for module_name, module in finder.modules.items(): + if module_name != '__main__': + path = getattr(module, '__file__', None) + if path is not None and os.path.isfile(path): + imported_modules.append(path) + + return imported_modules if __name__ == "__main__": diff --git a/tests/test_app.py b/tests/test_app.py index b508b47b..8dbc4e64 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -14,7 +14,7 @@ from PyQt5.QtWidgets import QFileDialog, QMessageBox from cq_editor.__main__ import MainWindow -from cq_editor.widgets.editor import Editor, get_imported_module_paths +from cq_editor.widgets.editor import Editor from cq_editor.cq_utils import export, get_occ_color code = \ @@ -1320,16 +1320,32 @@ def filename(*args, **kwargs): # I don't really care what the title is, as long as it's not a filename assert(not win.windowTitle().endswith('.py')) +def test_module_discovery(tmp_path, editor): -def test_module_discovery(tmp_path): + qtbot, editor = editor with open(tmp_path.joinpath('main.py'), 'w') as f: f.write('import b') - assert get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [] + assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [] tmp_path.joinpath('b.py').touch() - assert get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [str(tmp_path.joinpath('b.py'))] + assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [str(tmp_path.joinpath('b.py'))] + +def test_launch_syntax_error(tmp_path): + + # verify app launches when input file is bad + win = MainWindow() + + inputfile = Path(tmp_path).joinpath("syntax_error.py") + modify_file("print(", inputfile) + editor = win.components["editor"] + editor.autoreload(True) + editor.preferences["Autoreload: watch imported modules"] = True + editor.load_from_file(inputfile) + + win.show() + assert(win.isVisible()) code_import_module_makebox = \ """ @@ -1386,3 +1402,4 @@ def test_reload_import_handle_error(tmp_path, main): # verify that no exception was generated debugger._actions["Run"][0].triggered.emit() assert(traceback_view.current_exception.text() == "") + \ No newline at end of file From f922b29c65f143b8ef87410176c4dfcf4a1089a9 Mon Sep 17 00:00:00 2001 From: AU Date: Tue, 18 Jan 2022 08:41:36 +0100 Subject: [PATCH 031/134] Do not use defaults (#308) --- cqgui_env.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/cqgui_env.yml b/cqgui_env.yml index eb5e7634..05200e69 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -2,7 +2,6 @@ name: cq-occ-conda-test-py3 channels: - CadQuery - conda-forge - - defaults dependencies: - pyqt=5 - pyparsing From aae7a169cdd0cf1d416b70bde04d93218b2e5996 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Tue, 1 Feb 2022 02:47:16 -0500 Subject: [PATCH 032/134] Handle exceptions when determining imported modules (#312) (#314) * Handle exceptions when determining imported modules (#312) * Skip file watcher update when script does not exist * Try mamba instead of pip * Force yes mamba config --set always_yes yes does not work on windows (it seems) Co-authored-by: AU --- appveyor.yml | 4 ++-- cq_editor/widgets/editor.py | 6 +++++- tests/test_app.py | 24 +++++++++++++++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index abf05ed1..a57590ee 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -28,8 +28,8 @@ install: - sh: source activate cqgui - cmd: activate cqgui - mamba list - - pip install pytest pluggy pytest-qt - - pip install pytest-mock pytest-cov pytest-repeat codecov pyvirtualdisplay + - mamba install -y pytest pluggy pytest-qt + - mamba install -y pytest-mock pytest-cov pytest-repeat codecov pyvirtualdisplay build: false diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 476f44cf..997ca4c3 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -212,7 +212,7 @@ def _clear_watched_paths(self): self._file_watcher.removePaths(paths) def _watch_paths(self): - if self._filename: + if Path(self._filename).exists(): self._file_watcher.addPath(self._filename) if self.preferences['Autoreload: watch imported modules']: module_paths = self.get_imported_module_paths(self._filename) @@ -265,6 +265,10 @@ def get_imported_module_paths(self, module_path): finder.run_script(module_path) except SyntaxError as err: self._logger.warning(f'Syntax error in {module_path}: {err}') + except Exception as err: + self._logger.warning( + f'Cannot determine imported modules in {module_path}: {type(err).__name__} {err}' + ) else: for module_name, module in finder.modules.items(): if module_name != '__main__': diff --git a/tests/test_app.py b/tests/test_app.py index 8dbc4e64..c99750c3 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1402,4 +1402,26 @@ def test_reload_import_handle_error(tmp_path, main): # verify that no exception was generated debugger._actions["Run"][0].triggered.emit() assert(traceback_view.current_exception.text() == "") - \ No newline at end of file + +def test_modulefinder(tmp_path, main): + + TIMEOUT = 500 + qtbot, win = main + editor = win.components["editor"] + debugger = win.components["debugger"] + traceback_view = win.components["traceback_viewer"] + log = win.components['log'] + + editor.autoreload(True) + editor.preferences["Autoreload: watch imported modules"] = True + + script = Path(tmp_path).joinpath("main.py") + Path(tmp_path).joinpath("emptydir").mkdir() + modify_file("#import emptydir", script) + editor.load_from_file(script) + with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): + modify_file("import emptydir", script) + + qtbot.wait(100) + assert("Cannot determine imported modules" in log.toPlainText().splitlines()[-1]) + From b2581f76ae03e5d124db54f8f7d319b313d12838 Mon Sep 17 00:00:00 2001 From: AU Date: Tue, 1 Feb 2022 18:52:14 +0100 Subject: [PATCH 033/134] Support AIS_InteractiveObject (#317) --- cq_editor/cq_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index a16eb714..e32ba6c6 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -7,7 +7,7 @@ from OCP.XCAFPrs import XCAFPrs_AISObject from OCP.TopoDS import TopoDS_Shape -from OCP.AIS import AIS_Shape, AIS_ColoredShape +from OCP.AIS import AIS_InteractiveObject, AIS_Shape, AIS_ColoredShape from OCP.Quantity import \ Quantity_TOC_RGB as TOC_RGB, Quantity_Color @@ -50,7 +50,7 @@ def to_workplane(obj : cq.Shape): return rv -def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly, AIS_Shape], +def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly, AIS_InteractiveObject], options={}): shape = None @@ -58,7 +58,7 @@ def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Sha if isinstance(obj, cq.Assembly): label, shape = toCAF(obj) ais = XCAFPrs_AISObject(label) - elif isinstance(obj, AIS_Shape): + elif isinstance(obj, AIS_InteractiveObject): ais = obj else: shape = to_compound(obj) From 367fee53b86efb5f31a664b3a3f1ef51125ea88f Mon Sep 17 00:00:00 2001 From: AU Date: Thu, 3 Feb 2022 07:27:57 +0100 Subject: [PATCH 034/134] Restore file only if no cmd line arg was given (#318) * Restore file only if no cmd line arg was given * Rework file restoring --- cq_editor/__main__.py | 6 +----- cq_editor/main_window.py | 6 +++++- cq_editor/widgets/editor.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cq_editor/__main__.py b/cq_editor/__main__.py index 3de81d97..0fc8f700 100644 --- a/cq_editor/__main__.py +++ b/cq_editor/__main__.py @@ -13,16 +13,12 @@ def main(): - win = MainWindow() - parser = argparse.ArgumentParser(description=NAME) parser.add_argument('filename',nargs='?',default=None) args = parser.parse_args(app.arguments()[1:]) - print(args) - if args.filename: - win.components['editor'].load_from_file(args.filename) + win = MainWindow(filename=args.filename if args.filename else None) win.show() sys.exit(app.exec_()) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index d66890ec..abe00a01 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -25,7 +25,7 @@ class MainWindow(QMainWindow,MainMixin): name = 'CQ-Editor' org = 'CadQuery' - def __init__(self,parent=None): + def __init__(self,parent=None, filename=None): super(MainWindow,self).__init__(parent) MainMixin.__init__(self) @@ -53,6 +53,10 @@ def __init__(self,parent=None): self.restorePreferences() self.restoreWindow() + + if filename: + self.components['editor'].load_from_file(filename) + self.restoreComponentState() def closeEvent(self,event): diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 997ca4c3..128891b4 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -247,9 +247,9 @@ def saveComponentState(self,store): def restoreComponentState(self,store): - filename = store.value(self.name+'/state',self.filename) + filename = store.value(self.name+'/state') - if filename and filename != '': + if filename and self.filename == '': try: self.load_from_file(filename) except IOError: From 858a7b9c2069e1d9790d5a9fe0dbdc2b6d9ccf20 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Sat, 5 Feb 2022 03:37:27 -0500 Subject: [PATCH 035/134] Use conda config (config not supported through mamba) (#319) * Use conda config (config not supported through mamba) * Get rid of conda config call Co-authored-by: AU --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a57590ee..899f7de6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,7 +22,6 @@ install: - cmd: miniconda.exe /S /InstallationType=JustMe /D=%MINICONDA_DIRNAME% - cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - cmd: activate - - mamba config --set always_yes yes - mamba info - mamba env create --name cqgui -f cqgui_env.yml - sh: source activate cqgui From 0d091ba3d20114b4815f796ec48ab33f4ebbeb41 Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Thu, 10 Feb 2022 22:54:06 +0200 Subject: [PATCH 036/134] Add toggle for orthogonal/perspective modes (#321) * Add ProjectionType and StereoMode to 3D Viewer preferences * Add comment Co-authored-by: AU --- cq_editor/widgets/viewer.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 896ba0a8..f788fb13 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from OCP.Graphic3d import Graphic3d_Camera, Graphic3d_StereoMode from PyQt5.QtWidgets import (QWidget, QPushButton, QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QFileDialog, QHBoxLayout, QFrame, QLabel, QApplication, @@ -39,9 +40,12 @@ class OCCViewer(QWidget,ComponentMixin): {'name': 'Background color (aux)', 'type': 'color', 'value': (30,30,30)}, {'name': 'Default object color', 'type': 'color', 'value': "FF0"}, {'name': 'Deviation', 'type': 'float', 'value': 1e-5, 'dec': True, 'step': 1}, - {'name': 'Angular deviation', 'type': 'float', 'value': 0.1, 'dec': True, 'step': 1}]) - - + {'name': 'Angular deviation', 'type': 'float', 'value': 0.1, 'dec': True, 'step': 1}, + {'name': 'Projection Type', 'type': 'list', 'value': 'Orthographic', + 'values': ['Orthographic', 'Perspective', 'Stereo', 'MonoLeftEye', 'MonoRightEye']}, + {'name': 'Stereo Mode', 'type': 'list', 'value': 'QuadBuffer', + 'values': ['QuadBuffer', 'Anaglyph', 'RowInterlaced', 'ColumnInterlaced', + 'ChessBoard', 'SideBySide', 'OverUnder']}]) IMAGE_EXTENSIONS = 'png' sigObjectSelected = pyqtSignal(list) @@ -78,6 +82,18 @@ def updatePreferences(self,*args): ctx.SetDeviationCoefficient(self.preferences['Deviation']) ctx.SetDeviationAngle(self.preferences['Angular deviation']) + v = self._get_view() + camera = v.Camera() + projection_type = self.preferences['Projection Type'] + camera.SetProjectionType(getattr(Graphic3d_Camera, f'Projection_{projection_type}', + Graphic3d_Camera.Projection_Orthographic)) + + # onle relevant for stereo projection + stereo_mode = self.preferences['Stereo Mode'] + params = v.ChangeRenderingParams() + params.StereoMode = getattr(Graphic3d_StereoMode, f'Graphic3d_StereoMode_{stereo_mode}', + Graphic3d_StereoMode.Graphic3d_StereoMode_QuadBuffer) + def create_actions(self,parent): self._actions = \ From 14146f84fe34dae6513507a004e8b87ce420246b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20=C5=A0milauer?= Date: Fri, 11 Feb 2022 12:15:17 +0100 Subject: [PATCH 037/134] Fix empty dict as default arg See e.g. https://towardsdatascience.com/python-pitfall-mutable-default-arguments-9385e8265422 --- cq_editor/widgets/object_tree.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index c0ea1dcf..2030bd19 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -269,7 +269,9 @@ def addObjects(self,objects,clean=False,root=None): self.sigObjectsAdded[list].emit(ais_list) @pyqtSlot(object,str,object) - def addObject(self,obj,name='',options={}): + def addObject(self,obj,name='',options=None): + + if options is None: options={} root = self.CQ From 4b461fe195d0a4e99b9a6c43b7e1fe0cb4c5e77d Mon Sep 17 00:00:00 2001 From: AU Date: Wed, 16 Feb 2022 18:08:07 +0100 Subject: [PATCH 038/134] Python 3.10 support (#320) * Python 3.10 support * Use py3.10 on appveyor * Use spyder 5 * path.py -> path * Use windows-latest * Update meta.yml too --- azure-pipelines.yml | 79 ++++++++++++++++++--------------------------- conda/meta.yaml | 4 +-- cqgui_env.yml | 7 ++-- 3 files changed, 37 insertions(+), 53 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 13d41b1a..0b3fcdbe 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -14,51 +14,36 @@ resources: name: CadQuery/conda-packages endpoint: CadQuery -jobs: -- template: conda-build.yml@templates - parameters: - name: Linux - vmImage: 'ubuntu-18.04' - py_maj: 3 - py_min: 8 - conda_bld: 3.21.6 - -- template: conda-build.yml@templates - parameters: - name: macOS - vmImage: 'macOS-10.15' - py_maj: 3 - py_min: 8 - conda_bld: 3.21.6 - -- template: conda-build.yml@templates - parameters: - name: Windows - vmImage: 'vs2017-win2016' - py_maj: 3 - py_min: 8 - conda_bld: 3.21.6 +parameters: + - name: minor + type: object + default: + - 8 + - 9 + - 10 -- template: conda-build.yml@templates - parameters: - name: Linux - vmImage: 'ubuntu-18.04' - py_maj: 3 - py_min: 9 - conda_bld: 3.21.6 - -- template: conda-build.yml@templates - parameters: - name: macOS - vmImage: 'macOS-10.15' - py_maj: 3 - py_min: 9 - conda_bld: 3.21.6 - -- template: conda-build.yml@templates - parameters: - name: Windows - vmImage: 'vs2017-win2016' - py_maj: 3 - py_min: 9 - conda_bld: 3.21.6 +jobs: +- ${{ each minor in parameters.minor }}: + - template: conda-build.yml@templates + parameters: + name: Linux + vmImage: 'ubuntu-18.04' + py_maj: 3 + py_min: ${{minor}} + conda_bld: 3.21.6 + + - template: conda-build.yml@templates + parameters: + name: macOS + vmImage: 'macOS-10.15' + py_maj: 3 + py_min: ${{minor}} + conda_bld: 3.21.6 + + - template: conda-build.yml@templates + parameters: + name: Windows + vmImage: 'windows-latest' + py_maj: 3 + py_min: ${{minor}} + conda_bld: 3.21.6 diff --git a/conda/meta.yaml b/conda/meta.yaml index b46cf6b1..4986d98e 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -23,8 +23,8 @@ requirements: - logbook - pyqt=5.* - pyqtgraph - - spyder=4.* - - path.py + - spyder=5.* + - path - logbook - requests diff --git a/cqgui_env.yml b/cqgui_env.yml index 05200e69..79ba4b28 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -4,11 +4,10 @@ channels: - conda-forge dependencies: - pyqt=5 - - pyparsing - pyqtgraph - - python=3.8 - - spyder=4 - - path.py + - python=3.10 + - spyder=5 + - path - logbook - requests - cadquery=master From 56ddbf33f8d1d9b3d5277d242c57afb1c7831d1f Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Tue, 12 Jul 2022 19:02:26 +0300 Subject: [PATCH 039/134] Use original encoding on save. Default to UTF-8 on new files. (#356) --- cq_editor/widgets/editor.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 128891b4..eb70537d 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -1,4 +1,5 @@ import os +import spyder.utils.encoding from modulefinder import ModuleFinder from spyder.plugins.editor.widgets.codeeditor import CodeEditor @@ -158,6 +159,15 @@ def load_from_file(self,fname): self.filename = fname self.reset_modified() + def determine_encoding(self, fname): + if os.path.exists(fname): + # this function returns the encoding spyder used to read the file + _, encoding = spyder.utils.encoding.read(fname) + # spyder returns a -guessed suffix in some cases + return encoding.replace('-guessed', '') + else: + return 'utf-8' + def save(self): if self._filename != '': @@ -166,8 +176,10 @@ def save(self): self._file_watcher.blockSignals(True) self._file_watch_timer.stop() - with open(self._filename, 'w') as f: - f.write(self.toPlainText()) + encoding = self.determine_encoding(self._filename) + encoded = self.toPlainText().encode(encoding) + with open(self._filename, 'wb') as f: + f.write(encoded) if self.preferences['Autoreload']: self._file_watcher.blockSignals(False) @@ -182,8 +194,9 @@ def save_as(self): fname = get_save_filename(self.EXTENSIONS) if fname != '': - with open(fname,'w') as f: - f.write(self.toPlainText()) + encoded = self.toPlainText().encode('utf-8') + with open(fname, 'wb') as f: + f.write(encoded) self.filename = fname self.reset_modified() From 774c3c62b872a4ffb73de3f6a4052ddcad68a7c8 Mon Sep 17 00:00:00 2001 From: AU Date: Wed, 20 Jul 2022 06:31:20 +0200 Subject: [PATCH 040/134] CQ reload fix (#359) * Try to reproduce the error * Try to fix unrelated failure * Fix reloading --- cq_editor/cq_utils.py | 1 + tests/test_app.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index e32ba6c6..cad702f0 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -118,6 +118,7 @@ def reload_cq(): reload(cq.types) reload(cq.occ_impl.geom) reload(cq.occ_impl.shapes) + reload(cq.occ_impl.shapes) reload(cq.occ_impl.importers.dxf) reload(cq.occ_impl.importers) reload(cq.occ_impl.solver) diff --git a/tests/test_app.py b/tests/test_app.py index c99750c3..5222ba10 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -79,6 +79,11 @@ code_nested_bottom = """a=1 """ +code_reload_issue = """wire0 = cq.Workplane().lineTo(5, 5).lineTo(10, 0).close().val() +solid1 = cq.Solid.extrudeLinear(cq.Face.makeFromWires(wire0), cq.Vector(0, 0, 1)) +r1 = cq.Workplane(solid1).translate((10, 0, 0)) +""" + def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) @@ -237,6 +242,20 @@ def test_render(main): qtbot.wait(100) assert(obj_tree_comp.CQ.child(0).text(0) == 'test') assert('test' in log.toPlainText().splitlines()[-1]) + + # cq reloading check + obj_tree_comp._toolbar_actions[0].triggered.emit() + assert(obj_tree_comp.CQ.childCount() == 0) + + editor.set_text(code_reload_issue) + debugger._actions['Run'][0].triggered.emit() + + qtbot.wait(100) + assert(obj_tree_comp.CQ.childCount() == 1) + + debugger._actions['Run'][0].triggered.emit() + qtbot.wait(100) + assert(obj_tree_comp.CQ.childCount() == 1) def test_export(main,mocker): From 65033654a3c173b2bd52326a77cd8b0f00cd841e Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Thu, 4 Aug 2022 16:15:21 -0500 Subject: [PATCH 041/134] reformat default log output to reduce char width --- cq_editor/widgets/log.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cq_editor/widgets/log.py b/cq_editor/widgets/log.py index c7478611..c244a0cb 100644 --- a/cq_editor/widgets/log.py +++ b/cq_editor/widgets/log.py @@ -10,7 +10,10 @@ class QtLogHandler(logging.Handler,logging.StringFormatterHandlerMixin): def __init__(self, log_widget,*args,**kwargs): super(QtLogHandler,self).__init__(*args,**kwargs) - logging.StringFormatterHandlerMixin.__init__(self,None) + + log_format_string = '[{record.time:%H:%M:%S.%f%z}] {record.level_name}: {record.message}' + + logging.StringFormatterHandlerMixin.__init__(self,log_format_string) self.log_widget = log_widget @@ -40,4 +43,4 @@ def __init__(self,*args,**kwargs): def append(self,msg): - self.appendPlainText(msg) \ No newline at end of file + self.appendPlainText(msg) From e66d79237f5f9dba3afd831b49d5804df93e83fc Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Tue, 20 Sep 2022 14:52:02 -0500 Subject: [PATCH 042/134] add log() function to the built-in CQ-editor console (#360) * add log() function to the built-in CQ-editor console * Move new logbook import to top of main_window.py --- cq_editor/main_window.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index abe00a01..de895cf7 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,7 +1,7 @@ import sys from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction) - +from logbook import Logger import cadquery as cq from .widgets.editor import Editor @@ -269,14 +269,15 @@ def prepare_console(self): console = self.components['console'] obj_tree = self.components['object_tree'] - + #application related items console.push_vars({'self' : self}) #CQ related items console.push_vars({'show' : obj_tree.addObject, 'show_object' : obj_tree.addObject, - 'cq' : cq}) + 'cq' : cq, + 'log' : Logger(self.name).info}) def fill_dummy(self): From 7f9739d67af2634400333ea4c20833374b9d06af Mon Sep 17 00:00:00 2001 From: Lorenz Date: Fri, 7 Oct 2022 02:25:04 -0400 Subject: [PATCH 043/134] Prefix hex color strings with '#' (#366) (#367) --- cq_editor/widgets/object_tree.py | 4 ++-- cq_editor/widgets/viewer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index c0ea1dcf..5e831582 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -21,7 +21,7 @@ def __init__(self,*args,**kwargs): class ObjectTreeItem(QTreeWidgetItem): props = [{'name': 'Name', 'type': 'str', 'value': ''}, - {'name': 'Color', 'type': 'color', 'value': "f4a824"}, + {'name': 'Color', 'type': 'color', 'value': "#f4a824"}, {'name': 'Alpha', 'type': 'float', 'value': 0, 'limits': (0,1), 'step': 1e-1}, {'name': 'Visible', 'type': 'bool','value': True}] @@ -32,7 +32,7 @@ def __init__(self, shape_display=None, sig=None, alpha=0., - color='f4a824', + color='#f4a824', **kwargs): super(ObjectTreeItem,self).__init__([name],**kwargs) diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index f788fb13..2cf2d640 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -38,7 +38,7 @@ class OCCViewer(QWidget,ComponentMixin): {'name': 'Use gradient', 'type': 'bool', 'value': False}, {'name': 'Background color', 'type': 'color', 'value': (95,95,95)}, {'name': 'Background color (aux)', 'type': 'color', 'value': (30,30,30)}, - {'name': 'Default object color', 'type': 'color', 'value': "FF0"}, + {'name': 'Default object color', 'type': 'color', 'value': "#FF0"}, {'name': 'Deviation', 'type': 'float', 'value': 1e-5, 'dec': True, 'step': 1}, {'name': 'Angular deviation', 'type': 'float', 'value': 0.1, 'dec': True, 'step': 1}, {'name': 'Projection Type', 'type': 'list', 'value': 'Orthographic', From ec81ad96989b85d87750428f2a5419933bb3012f Mon Sep 17 00:00:00 2001 From: AU Date: Wed, 19 Oct 2022 19:51:03 +0200 Subject: [PATCH 044/134] Better default material and color handling (#371) * Better default material and color handling * Trying to fix the tests NB: default color changed * Another test fix --- cq_editor/cq_utils.py | 24 ++++++++++++------ cq_editor/widgets/object_tree.py | 14 ++++++++--- cq_editor/widgets/viewer.py | 43 +++++++++++++++++++++----------- tests/test_app.py | 20 ++++----------- 4 files changed, 60 insertions(+), 41 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index cad702f0..f3504fd1 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -7,7 +7,7 @@ from OCP.XCAFPrs import XCAFPrs_AISObject from OCP.TopoDS import TopoDS_Shape -from OCP.AIS import AIS_InteractiveObject, AIS_Shape, AIS_ColoredShape +from OCP.AIS import AIS_InteractiveObject, AIS_Shape from OCP.Quantity import \ Quantity_TOC_RGB as TOC_RGB, Quantity_Color @@ -62,7 +62,7 @@ def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Sha ais = obj else: shape = to_compound(obj) - ais = AIS_ColoredShape(shape.wrapped) + ais = AIS_Shape(shape.wrapped) if 'alpha' in options: ais.SetTransparency(options['alpha']) @@ -105,13 +105,23 @@ def to_occ_color(color) -> Quantity_Color: color.blueF(), TOC_RGB) -def get_occ_color(ais : AIS_ColoredShape) -> QColor: - - color = Quantity_Color() - ais.Color(color) - +def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: + + if isinstance(obj, AIS_InteractiveObject): + color = Quantity_Color() + obj.Color(color) + else: + color = obj + return QColor.fromRgbF(color.Red(), color.Green(), color.Blue()) +def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape: + + drawer = ais.Attributes() + drawer.ShadingAspect().SetColor(color) + + return ais + def reload_cq(): # NB: order of reloads is important diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 5e831582..97e3c66e 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -9,7 +9,8 @@ from ..mixins import ComponentMixin from ..icons import icon -from ..cq_utils import make_AIS, export, to_occ_color, is_obj_empty, get_occ_color +from ..cq_utils import make_AIS, export, to_occ_color, is_obj_empty, get_occ_color, set_color +from .viewer import DEFAULT_FACE_COLOR from ..utils import splitter, layout, get_save_filename class TopTreeItem(QTreeWidgetItem): @@ -49,14 +50,19 @@ def __init__(self, self.properties['Name'] = name self.properties['Alpha'] = ais.Transparency() - self.properties['Color'] = get_occ_color(ais) if ais else color + self.properties['Color'] = get_occ_color(ais) if ais and ais.HasColor() else get_occ_color(DEFAULT_FACE_COLOR) self.properties.sigTreeStateChanged.connect(self.propertiesChanged) - def propertiesChanged(self,*args): + def propertiesChanged(self, properties, changed): + + changed_prop = changed[0][0] self.setData(0,0,self.properties['Name']) self.ais.SetTransparency(self.properties['Alpha']) - self.ais.SetColor(to_occ_color(self.properties['Color'])) + + if changed_prop.name() == 'Color': + set_color(self.ais, to_occ_color(self.properties['Color'])) + self.ais.Redisplay() if self.properties['Visible']: diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 2cf2d640..379053a1 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -1,22 +1,16 @@ -# -*- coding: utf-8 -*- +from PyQt5.QtWidgets import QWidget, QDialog, QTreeWidgetItem, QApplication, QAction -from OCP.Graphic3d import Graphic3d_Camera, Graphic3d_StereoMode -from PyQt5.QtWidgets import (QWidget, QPushButton, QDialog, QTreeWidget, - QTreeWidgetItem, QVBoxLayout, QFileDialog, - QHBoxLayout, QFrame, QLabel, QApplication, - QToolBar, QAction) - -from PyQt5.QtCore import QSize, pyqtSlot, pyqtSignal, QMetaObject, Qt +from PyQt5.QtCore import pyqtSlot, pyqtSignal from PyQt5.QtGui import QIcon -from OCP.AIS import AIS_Shaded,AIS_WireFrame, AIS_ColoredShape, \ - AIS_Axis, AIS_Line -from OCP.Aspect import Aspect_GDM_Lines, Aspect_GT_Rectangular, Aspect_GFM_VER -from OCP.Quantity import Quantity_NOC_BLACK as BLACK, \ +from OCP.Graphic3d import Graphic3d_Camera, Graphic3d_StereoMode, Graphic3d_NOM_JADE,\ + Graphic3d_MaterialAspect +from OCP.AIS import AIS_Shaded,AIS_WireFrame, AIS_ColoredShape, AIS_Axis +from OCP.Aspect import Aspect_GDM_Lines, Aspect_GT_Rectangular +from OCP.Quantity import Quantity_NOC_BLACK as BLACK, Quantity_NOC_GOLD as GOLD,\ Quantity_TOC_RGB as TOC_RGB, Quantity_Color -from OCP.Geom import Geom_CylindricalSurface, Geom_Plane, Geom_Circle,\ - Geom_TrimmedCurve, Geom_Axis1Placement, Geom_Axis2Placement, Geom_Line -from OCP.gp import gp_Trsf, gp_Vec, gp_Ax3, gp_Dir, gp_Pnt, gp_Ax1 +from OCP.Geom import Geom_Axis1Placement +from OCP.gp import gp_Ax3, gp_Dir, gp_Pnt, gp_Ax1 from ..utils import layout, get_save_filename from ..mixins import ComponentMixin @@ -29,6 +23,10 @@ import qtawesome as qta +DEFAULT_FACE_COLOR = Quantity_Color(GOLD) +DEFAULT_EDGE_COLOR = Quantity_Color(BLACK) +DEFUALT_EDGE_WIDTH = 2 + class OCCViewer(QWidget,ComponentMixin): name = '3D Viewer' @@ -65,8 +63,23 @@ def __init__(self,parent=None): top_widget=self, margin=0) + self.setup_defualt_drawer() self.updatePreferences() + def setup_defualt_drawer(self): + + # set the default color and material + material = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE) + + shading_aspect = self.canvas.context.DefaultDrawer().ShadingAspect() + shading_aspect.SetMaterial(material) + shading_aspect.SetColor(DEFAULT_FACE_COLOR) + + # face edge lw + line_aspect = self.canvas.context.DefaultDrawer().FaceBoundaryAspect() + line_aspect.SetWidth(DEFUALT_EDGE_WIDTH) + line_aspect.SetColor(DEFAULT_EDGE_COLOR) + def updatePreferences(self,*args): color1 = to_occ_color(self.preferences['Background color']) diff --git a/tests/test_app.py b/tests/test_app.py index 5222ba10..5a041c3d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -114,7 +114,7 @@ def get_rgba(ais): alpha = ais.Transparency() color = get_occ_color(ais) - return color.redF(),color.redF(),color.redF(),alpha + return color.redF(), color.greenF(), color.blueF(), alpha @pytest.fixture def main(qtbot,mocker): @@ -1017,14 +1017,13 @@ def test_render_colors(main_clean): CQ = obj_tree.CQ # object 1 (defualt color) - r,g,b,a = get_rgba(CQ.child(0).ais) - assert( a == 0 ) - assert( r != 1.0 ) - + assert not CQ.child(0).ais.HasColor() + # object 2 r,g,b,a = get_rgba(CQ.child(1).ais) assert( a == 0.5 ) assert( r == 1.0 ) + assert( g == 0.0 ) # object 3 r,g,b,a = get_rgba(CQ.child(2).ais) @@ -1059,20 +1058,11 @@ def test_render_colors_console(main_clean): console = win.components['console'] console.execute_command(code_color) - - def get_rgba(ais): - - alpha = ais.Transparency() - color = get_occ_color(ais) - - return color.redF(),color.redF(),color.redF(),alpha CQ = obj_tree.CQ # object 1 (defualt color) - r,g,b,a = get_rgba(CQ.child(0).ais) - assert( a == 0 ) - assert( r != 1.0 ) + assert not CQ.child(0).ais.HasColor() # object 2 r,g,b,a = get_rgba(CQ.child(1).ais) From 0abdfbc4f1a0ddddc34b518c20785ff2ff192c86 Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Fri, 21 Oct 2022 09:41:30 -0500 Subject: [PATCH 045/134] Update log.py From 97e0829d18bf4db22689a98791b9f84303c56454 Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Tue, 25 Oct 2022 05:15:09 +0000 Subject: [PATCH 046/134] Tiny Spelling fix "DEFUALT" to "DEFAULT" (#372) Fix `DEFUALT_EDGE_WIDTH` and `def setup_default_drawer(self):` and references --- cq_editor/widgets/viewer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 379053a1..c1c7de79 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -25,7 +25,7 @@ DEFAULT_FACE_COLOR = Quantity_Color(GOLD) DEFAULT_EDGE_COLOR = Quantity_Color(BLACK) -DEFUALT_EDGE_WIDTH = 2 +DEFAULT_EDGE_WIDTH = 2 class OCCViewer(QWidget,ComponentMixin): @@ -63,10 +63,10 @@ def __init__(self,parent=None): top_widget=self, margin=0) - self.setup_defualt_drawer() + self.setup_default_drawer() self.updatePreferences() - def setup_defualt_drawer(self): + def setup_default_drawer(self): # set the default color and material material = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE) @@ -77,7 +77,7 @@ def setup_defualt_drawer(self): # face edge lw line_aspect = self.canvas.context.DefaultDrawer().FaceBoundaryAspect() - line_aspect.SetWidth(DEFUALT_EDGE_WIDTH) + line_aspect.SetWidth(DEFAULT_EDGE_WIDTH) line_aspect.SetColor(DEFAULT_EDGE_COLOR) def updatePreferences(self,*args): From a2df6ffb04207aa22a177cab8f1ebad8ba974ab4 Mon Sep 17 00:00:00 2001 From: AU Date: Thu, 27 Oct 2022 21:01:49 +0200 Subject: [PATCH 047/134] Transparency/material fix (#375) * Rough fix * Use correct var name * Setup own shading aspect explicitly * Add test --- cq_editor/cq_utils.py | 54 ++++++++++++++++++++++++++----------- cq_editor/widgets/viewer.py | 14 +++++----- tests/test_app.py | 30 +++++++++++++++++++++ 3 files changed, 76 insertions(+), 22 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index f3504fd1..a42836cc 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -9,10 +9,14 @@ from OCP.TopoDS import TopoDS_Shape from OCP.AIS import AIS_InteractiveObject, AIS_Shape from OCP.Quantity import \ - Quantity_TOC_RGB as TOC_RGB, Quantity_Color - + Quantity_TOC_RGB as TOC_RGB, Quantity_Color, Quantity_NOC_GOLD as GOLD +from OCP.Graphic3d import Graphic3d_NOM_JADE, Graphic3d_MaterialAspect + from PyQt5.QtGui import QColor +DEFAULT_FACE_COLOR = Quantity_Color(GOLD) +DEFAULT_MATERIAL = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE) + def find_cq_objects(results : dict): return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if isinstance(v,cq.Workplane)} @@ -30,7 +34,7 @@ def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq. elif isinstance(obj,list) and isinstance(obj[0],cq.Shape): vals.extend(obj) elif isinstance(obj, TopoDS_Shape): - vals.append(cq.Shape.cast(obj)) + vals.append(cq.Shape.cast(obj)) elif isinstance(obj,list) and isinstance(obj[0],TopoDS_Shape): vals.extend(cq.Shape.cast(o) for o in obj) elif isinstance(obj, cq.Sketch): @@ -63,15 +67,18 @@ def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Sha else: shape = to_compound(obj) ais = AIS_Shape(shape.wrapped) - + + set_material(ais, DEFAULT_MATERIAL) + set_color(ais, DEFAULT_FACE_COLOR) + if 'alpha' in options: - ais.SetTransparency(options['alpha']) + set_transparency(ais, options['alpha']) if 'color' in options: - ais.SetColor(to_occ_color(options['color'])) + set_color(ais, to_occ_color(options['color'])) if 'rgba' in options: r,g,b,a = options['rgba'] - ais.SetColor(to_occ_color((r,g,b))) - ais.SetTransparency(a) + set_color(ais, to_occ_color((r,g,b))) + set_transparency(ais, a) return ais,shape @@ -88,7 +95,7 @@ def export(obj : Union[cq.Workplane, List[cq.Workplane]], type : str, comp.exportBrep(file) def to_occ_color(color) -> Quantity_Color: - + if not isinstance(color, QColor): if isinstance(color, tuple): if isinstance(color[0], int): @@ -118,12 +125,29 @@ def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape: drawer = ais.Attributes() + drawer.SetupOwnShadingAspect() drawer.ShadingAspect().SetColor(color) return ais +def set_material(ais : AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Shape: + + drawer = ais.Attributes() + drawer.SetupOwnShadingAspect() + drawer.ShadingAspect().SetMaterial(material) + + return ais + +def set_transparency(ais : AIS_Shape, alpha: float) -> AIS_Shape: + + drawer = ais.Attributes() + drawer.SetupOwnShadingAspect() + drawer.ShadingAspect().SetTransparency(alpha) + + return ais + def reload_cq(): - + # NB: order of reloads is important reload(cq.types) reload(cq.occ_impl.geom) @@ -147,13 +171,13 @@ def reload_cq(): reload(cq.occ_impl.exporters) reload(cq.assembly) reload(cq) - - + + def is_obj_empty(obj : Union[cq.Workplane,cq.Shape]) -> bool: - + rv = False - + if isinstance(obj, cq.Workplane): rv = True if isinstance(obj.val(), cq.Vector) else False - + return rv diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index c1c7de79..9c5d620b 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -7,15 +7,15 @@ Graphic3d_MaterialAspect from OCP.AIS import AIS_Shaded,AIS_WireFrame, AIS_ColoredShape, AIS_Axis from OCP.Aspect import Aspect_GDM_Lines, Aspect_GT_Rectangular -from OCP.Quantity import Quantity_NOC_BLACK as BLACK, Quantity_NOC_GOLD as GOLD,\ - Quantity_TOC_RGB as TOC_RGB, Quantity_Color +from OCP.Quantity import Quantity_NOC_BLACK as BLACK, Quantity_TOC_RGB as TOC_RGB,\ + Quantity_Color from OCP.Geom import Geom_Axis1Placement from OCP.gp import gp_Ax3, gp_Dir, gp_Pnt, gp_Ax1 from ..utils import layout, get_save_filename from ..mixins import ComponentMixin from ..icons import icon -from ..cq_utils import to_occ_color, make_AIS +from ..cq_utils import to_occ_color, make_AIS, DEFAULT_FACE_COLOR from .occt_widget import OCCTWidget @@ -23,7 +23,7 @@ import qtawesome as qta -DEFAULT_FACE_COLOR = Quantity_Color(GOLD) + DEFAULT_EDGE_COLOR = Quantity_Color(BLACK) DEFAULT_EDGE_WIDTH = 2 @@ -62,7 +62,7 @@ def __init__(self,parent=None): [self.canvas,], top_widget=self, margin=0) - + self.setup_default_drawer() self.updatePreferences() @@ -88,9 +88,9 @@ def updatePreferences(self,*args): if not self.preferences['Use gradient']: color2 = color1 self.canvas.view.SetBgGradientColors(color1,color2,theToUpdate=True) - + self.canvas.update() - + ctx = self.canvas.context ctx.SetDeviationCoefficient(self.preferences['Deviation']) ctx.SetDeviationAngle(self.preferences['Angular deviation']) diff --git a/tests/test_app.py b/tests/test_app.py index 5a041c3d..6254e894 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1092,7 +1092,37 @@ def test_render_colors_console(main_clean): # check if error occured qtbot.wait(100) assert('Unknown color format' in log.toPlainText().splitlines()[-1]) + +code_shading = \ +''' +import cadquery as cq + +res1 = cq.Workplane('XY').box(5, 7, 5) +res2 = cq.Workplane('XY').box(8, 5, 4) +show_object(res1) +show_object(res2,options={"alpha":0}) +''' + +def test_shading_aspect(main_clean): + qtbot, win = main_clean + + obj_tree = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + + editor.set_text(code_shading) + debugger._actions['Run'][0].triggered.emit() + + CQ = obj_tree.CQ + + # get material aspects + ma1 = CQ.child(0).ais.Attributes().ShadingAspect().Material() + ma2 = CQ.child(1).ais.Attributes().ShadingAspect().Material() + + # verify that they are the same + assert ma1.Shininess() == ma2.Shininess() + def test_confirm_new(monkeypatch,editor): qtbot, editor = editor From adf11592c96c2d8490e1e8d332d1a9bb63f5c112 Mon Sep 17 00:00:00 2001 From: AU Date: Thu, 5 Jan 2023 19:35:57 +0100 Subject: [PATCH 048/134] Add MacOS workaround to the README (#387) See https://github.com/CadQuery/CQ-editor/issues/337 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e3ad17b..a97356b4 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. ### Release Packages -Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download the zip file for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly. +Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download the zip file for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. On later MacOS versions you may also need `xattr -r -d com.apple.quarantine path/to/CQ-editor-MacOS`. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly. ### Development Packages From 88d2f7e87904790d0b9cd00218e11eebf2138bd2 Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Tue, 7 Mar 2023 13:18:47 -0600 Subject: [PATCH 049/134] add name param to show_object doc in README.md (#390) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a97356b4..10e3849d 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,10 @@ dnf install -y mesa-libGLU mesa-libGL mesa-libGLU-devel ### Showing Objects -By default, CQ-editor will display a 3D representation of all `Workplane` objects in a script with a default color and alpha (transparency). To have more control over what is shown, and what the color and alpha settings are, the `show_object` method can be used. `show_object` tells CQ-editor to explicity display an object, and accepts the `options` parameter. The `options` parameter is a dictionary of rendering options named `alpha` and `color`. `alpha` is scaled between 0.0 and 1.0, with 0.0 being completely opaque and 1.0 being completely transparent. The color is set using R (red), G (green) and B (blue) values, and each one is scaled from 0 to 255. Either option or both can be omitted. +By default, CQ-editor will display a 3D representation of all `Workplane` objects in a script with a default color and alpha (transparency). To have more control over what is shown, and what the color and alpha settings are, the `show_object` method can be used. `show_object` tells CQ-editor to explicity display an object, and accepts the `options` parameter. The `options` parameter is a dictionary of rendering options named `alpha` and `color`. `alpha` is scaled between 0.0 and 1.0, with 0.0 being completely opaque and 1.0 being completely transparent. The color is set using R (red), G (green) and B (blue) values, and each one is scaled from 0 to 255. Either option or both can be omitted. The `name` parameter can assign a custom name which will appear in the objects pane of CQ-editor. ```python -show_object(result, options={"alpha":0.5, "color": (64, 164, 223)}) +show_object(result, name="somename", options={"alpha":0.5, "color": (64, 164, 223)}) ``` Note that `show_object` works for `Shape` and `TopoDS_Shape` objects too. In order to display objects from the embedded Python console use `show`. From f2ace41c1fa121de4211afa1c0f3d3d7f53fbc94 Mon Sep 17 00:00:00 2001 From: AU Date: Sat, 25 Mar 2023 17:00:20 +0100 Subject: [PATCH 050/134] Build noarch packages (#392) * Build noarch packages * Run only one build --- azure-pipelines.yml | 20 +------------------- conda/meta.yaml | 6 +++--- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0b3fcdbe..a0ed1fa8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,9 +18,7 @@ parameters: - name: minor type: object default: - - 8 - - 9 - - 10 + - 11 jobs: - ${{ each minor in parameters.minor }}: @@ -31,19 +29,3 @@ jobs: py_maj: 3 py_min: ${{minor}} conda_bld: 3.21.6 - - - template: conda-build.yml@templates - parameters: - name: macOS - vmImage: 'macOS-10.15' - py_maj: 3 - py_min: ${{minor}} - conda_bld: 3.21.6 - - - template: conda-build.yml@templates - parameters: - name: Windows - vmImage: 'windows-latest' - py_maj: 3 - py_min: ${{minor}} - conda_bld: 3.21.6 diff --git a/conda/meta.yaml b/conda/meta.yaml index 4986d98e..06317d32 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -6,18 +6,18 @@ source: path: .. build: - string: {{ 'py'+environ.get('PYTHON_VERSION')}} + noarch: python script: python setup.py install --single-version-externally-managed --record=record.txt entry_points: - cq-editor = cq_editor.__main__:main - CQ-editor = cq_editor.__main__:main requirements: build: - - python {{ environ.get('PYTHON_VERSION') }} + - python >=3.8 - setuptools run: - - python {{ environ.get('PYTHON_VERSION') }} + - python >=3.8 - cadquery=master - ocp - logbook From b997c69ecf74ae4d33bbbe1e97251d86779dab9f Mon Sep 17 00:00:00 2001 From: AU Date: Wed, 29 Mar 2023 22:31:35 +0200 Subject: [PATCH 051/134] Mention automatic code reloading --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 10e3849d..af94db23 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. ## Notable features +* Automatic code reloading - you can any editor * OCCT based * Graphical debugger for CadQuery scripts * Step through script and watch how your model changes From 92e2b46b6a99bcea40ee36f52172cb094f7586dc Mon Sep 17 00:00:00 2001 From: AU Date: Sun, 9 Apr 2023 21:22:10 +0200 Subject: [PATCH 052/134] Use ubuntu-latest --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a0ed1fa8..bb1a6b83 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,7 +25,7 @@ jobs: - template: conda-build.yml@templates parameters: name: Linux - vmImage: 'ubuntu-18.04' + vmImage: 'ubuntu-latest' py_maj: 3 py_min: ${{minor}} conda_bld: 3.21.6 From e24b729667f490ef0048c98af135ef3d7c20147e Mon Sep 17 00:00:00 2001 From: AU Date: Mon, 10 Apr 2023 16:54:52 +0200 Subject: [PATCH 053/134] Add a build string --- conda/meta.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/conda/meta.yaml b/conda/meta.yaml index 06317d32..05ed1047 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -6,6 +6,7 @@ source: path: .. build: + string: h{{ PKG_HASH }}_{{ PKG_BUILDNUM }} noarch: python script: python setup.py install --single-version-externally-managed --record=record.txt entry_points: From bf33b6d34999c95958ae89e9967e6d3605334bfc Mon Sep 17 00:00:00 2001 From: AU Date: Sun, 16 Apr 2023 10:28:00 +0200 Subject: [PATCH 054/134] Better build string (#397) * Better build string * Pin qtconsole * Pin qtconsole * Different naming convention --- conda/meta.yaml | 4 ++-- cqgui_env.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/conda/meta.yaml b/conda/meta.yaml index 05ed1047..b6a95f53 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -6,7 +6,7 @@ source: path: .. build: - string: h{{ PKG_HASH }}_{{ PKG_BUILDNUM }} + string: {{ GIT_DESCRIBE_TAG }}_{{ GIT_BUILD_STR }} noarch: python script: python setup.py install --single-version-externally-managed --record=record.txt entry_points: @@ -28,7 +28,7 @@ requirements: - path - logbook - requests - + - qtconsole=5.4.1 test: imports: - cq_editor diff --git a/cqgui_env.yml b/cqgui_env.yml index 79ba4b28..98cae565 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -11,3 +11,4 @@ dependencies: - logbook - requests - cadquery=master + - qtconsole=5.4.1 From 2a9a02a5212ce3b109146f7bcb66340d110f8498 Mon Sep 17 00:00:00 2001 From: AU Date: Fri, 28 Apr 2023 18:42:04 +0200 Subject: [PATCH 055/134] Debugger fix (#399) * Debugger fix * Update tests * Inspect failuers on the agent * Make the name unique This should prevent some false positive stops * Try adding processEvents * Try with qtbot.wait * Revert additions and change __name__ * Update expected values * Turn off rpd --- cq_editor/widgets/debugger.py | 39 +++++---- tests/test_app.py | 146 ++++++++++++++++++++-------------- 2 files changed, 110 insertions(+), 75 deletions(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index ffddfa03..3bb06607 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -3,12 +3,13 @@ from enum import Enum, auto from types import SimpleNamespace, FrameType, ModuleType from typing import List +from bdb import BdbQuit import cadquery as cq from PyQt5 import QtCore from PyQt5.QtCore import Qt, QObject, pyqtSlot, pyqtSignal, QEventLoop, QAbstractTableModel -from PyQt5.QtWidgets import (QAction, - QTableView) +from PyQt5.QtWidgets import QAction, QTableView + from logbook import info from path import Path from pyqtgraph.parametertree import Parameter @@ -17,7 +18,7 @@ from ..cq_utils import find_cq_objects, reload_cq from ..mixins import ComponentMixin -DUMMY_FILE = '' +DUMMY_FILE = '' class DbgState(Enum): @@ -119,6 +120,7 @@ class Debugger(QObject,ComponentMixin): sigDebugging = pyqtSignal(bool) _frames : List[FrameType] + _stop_debugging : bool def __init__(self,parent): @@ -155,8 +157,9 @@ def __init__(self,parent): shortcut='ctrl+F12', triggered=lambda: self.debug_cmd(DbgState.CONT)) ]} - + self._frames = [] + self._stop_debugging = False def get_current_script(self): @@ -169,8 +172,8 @@ def get_breakpoints(self): def compile_code(self, cq_script): try: - module = ModuleType('temp') - cq_code = compile(cq_script, '', 'exec') + module = ModuleType('__cq_main__') + cq_code = compile(cq_script, DUMMY_FILE, 'exec') return cq_code, module except Exception: self.sigTraceback.emit(sys.exc_info(), cq_script) @@ -190,7 +193,7 @@ def _exec(self, code, locals_dict, globals_dict): if self.preferences['Reload imported modules']: stack.enter_context(module_manager()) - exec(code, locals_dict, globals_dict) + exec(code, locals_dict, globals_dict) def _inject_locals(self,module): @@ -248,7 +251,7 @@ def render(self): exc_info = sys.exc_info() sys.last_traceback = exc_info[-1] self.sigTraceback.emit(exc_info, cq_script) - + @property def breakpoints(self): return [ el[0] for el in self.get_breakpoints()] @@ -256,9 +259,12 @@ def breakpoints(self): @pyqtSlot(bool) def debug(self,value): - previous_trace = sys.gettrace() + # used to stop the debugging session early + self._stop_debugging = False if value: + self.previous_trace = previous_trace = sys.gettrace() + self.sigDebugging.emit(True) self.state = DbgState.STEP @@ -279,6 +285,8 @@ def debug(self,value): try: sys.settrace(self.trace_callback) exec(code,module.__dict__,module.__dict__) + except BdbQuit: + pass except Exception: exc_info = sys.exc_info() sys.last_traceback = exc_info[-1] @@ -295,13 +303,13 @@ def debug(self,value): self._cleanup_locals(module,injected_names) self.sigLocals.emit(module.__dict__) - + self._frames = [] + self.inner_event_loop.exit(0) else: - sys.settrace(previous_trace) + self._stop_debugging = True self.inner_event_loop.exit(0) - def debug_cmd(self,state=DbgState.STEP): self.state = state @@ -328,10 +336,10 @@ def trace_local(self,frame,event,arg): if event in (DbgEevent.LINE,): if (self.state in (DbgState.STEP, DbgState.STEP_IN) and frame is self._frames[-1]) \ or (lineno in self.breakpoints): - + if lineno in self.breakpoints: self._frames.append(frame) - + self.sigLineChanged.emit(lineno) self.sigFrameChanged.emit(frame) self.sigLocalsChanged.emit(frame.f_locals) @@ -351,6 +359,9 @@ def trace_local(self,frame,event,arg): self.state = DbgState.STEP self._frames.append(frame) + if self._stop_debugging: + raise BdbQuit #stop debugging if requested + @contextmanager def module_manager(): diff --git a/tests/test_app.py b/tests/test_app.py index 6254e894..eb7463e0 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -110,10 +110,10 @@ def get_bottom_left(widget): return pos def get_rgba(ais): - + alpha = ais.Transparency() color = get_occ_color(ais) - + return color.redF(), color.greenF(), color.blueF(), alpha @pytest.fixture @@ -144,7 +144,7 @@ def main_clean(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - + editor = win.components['editor'] editor.set_text(code) @@ -160,7 +160,7 @@ def main_clean_do_not_close(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - + editor = win.components['editor'] editor.set_text(code) @@ -177,7 +177,7 @@ def main_multi(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - + editor = win.components['editor'] editor.set_text(code_multi) @@ -195,7 +195,7 @@ def test_render(main): debugger = win.components['debugger'] console = win.components['console'] log = win.components['log'] - + # enable CQ reloading debugger.preferences['Reload CQ'] = True @@ -242,17 +242,17 @@ def test_render(main): qtbot.wait(100) assert(obj_tree_comp.CQ.child(0).text(0) == 'test') assert('test' in log.toPlainText().splitlines()[-1]) - + # cq reloading check obj_tree_comp._toolbar_actions[0].triggered.emit() assert(obj_tree_comp.CQ.childCount() == 0) - + editor.set_text(code_reload_issue) debugger._actions['Run'][0].triggered.emit() - + qtbot.wait(100) assert(obj_tree_comp.CQ.childCount() == 1) - + debugger._actions['Run'][0].triggered.emit() qtbot.wait(100) assert(obj_tree_comp.CQ.childCount() == 1) @@ -377,6 +377,13 @@ def assert_func(x): variables = win.components['variables_viewer'] + traceback_view = win.components['traceback_viewer'] + + def check_no_error_occured(): + '''check that no error occured while stepping through the debugger + ''' + assert( '' == traceback_view.current_exception.text()) + viewer = win.components['viewer'] assert(number_visible_items(viewer) == 3) @@ -387,28 +394,37 @@ def assert_func(x): assert(debugger._frames == []) #test step through - ev = event_loop([lambda: (assert_func(variables.model().rowCount() == 4), - assert_func(number_visible_items(viewer) == 3), - step.triggered.emit()), - lambda: (assert_func(variables.model().rowCount() == 4), - assert_func(number_visible_items(viewer) == 3), - step.triggered.emit()), - lambda: (assert_func(variables.model().rowCount() == 5), - assert_func(number_visible_items(viewer) == 3), - step.triggered.emit()), - lambda: (assert_func(variables.model().rowCount() == 5), - assert_func(number_visible_items(viewer) == 4), - cont.triggered.emit())]) + ev = event_loop([ + lambda: ( + assert_func(variables.model().rowCount() == 4), + assert_func(number_visible_items(viewer) == 3), + step.triggered.emit()), + lambda: ( + assert_func(variables.model().rowCount() == 4), + assert_func(number_visible_items(viewer) == 3), + step.triggered.emit()), + lambda: ( + assert_func(variables.model().rowCount() == 5), + assert_func(number_visible_items(viewer) == 3), + step.triggered.emit()), + lambda: ( + assert_func(variables.model().rowCount() == 5), + assert_func(number_visible_items(viewer) == 4), + cont.triggered.emit()) + ]) patch_debugger(debugger,ev) debug.triggered.emit(True) + + check_no_error_occured() + assert(variables.model().rowCount() == 2) assert(number_visible_items(viewer) == 4) #test exit debug ev = event_loop([lambda: (step.triggered.emit(),), - lambda: (assert_func(variables.model().rowCount() == 1), + lambda: (assert_func(variables.model().rowCount() == 4), assert_func(number_visible_items(viewer) == 3), debug.triggered.emit(False),)]) @@ -416,6 +432,8 @@ def assert_func(x): debug.triggered.emit(True) + check_no_error_occured() + assert(variables.model().rowCount() == 1) assert(number_visible_items(viewer) == 3) @@ -431,9 +449,11 @@ def assert_func(x): debug.triggered.emit(True) + check_no_error_occured() + assert(variables.model().rowCount() == 2) assert(number_visible_items(viewer) == 4) - + #test breakpoint without using singals ev = event_loop([lambda: (cont.triggered.emit(),), lambda: (assert_func(variables.model().rowCount() == 5), @@ -446,9 +466,11 @@ def assert_func(x): debugger.debug(True) + check_no_error_occured() + assert(variables.model().rowCount() == 2) assert(number_visible_items(viewer) == 4) - + #test debug() without using singals ev = event_loop([lambda: (cont.triggered.emit(),), lambda: (assert_func(variables.model().rowCount() == 5), @@ -461,9 +483,11 @@ def assert_func(x): editor.debugger.set_breakpoints([(4,None)]) debugger.debug(True) - + + check_no_error_occured() + CQ = obj_tree.CQ - + # object 1 (defualt color) r,g,b,a = get_rgba(CQ.child(0).ais) assert( a == pytest.approx(0.2) ) @@ -516,17 +540,17 @@ def test_traceback(main): assert('NameError' in traceback_view.current_exception.text()) assert(hasattr(sys, 'last_traceback')) - + del sys.last_traceback assert(not hasattr(sys, 'last_traceback')) - - + + #test last_traceback with debug ev = event_loop([lambda: (cont.triggered.emit(),)]) patch_debugger(debugger,ev) - + debugger.debug(True) - + assert('NameError' in traceback_view.current_exception.text()) assert(hasattr(sys, 'last_traceback')) @@ -544,7 +568,7 @@ def editor(qtbot): return qtbot, win def conv_line_ends(text): - + return '\n'.join(text.splitlines()) def test_editor(monkeypatch,editor): @@ -671,9 +695,9 @@ def test_editor_autoreload(monkeypatch,editor): # Saving a file with autoreload enabled should trigger a rerender. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): editor.save() - + def test_autoreload_nested(editor): - + qtbot, editor = editor TIMEOUT = 500 @@ -761,8 +785,8 @@ def approx_view_properties(eye,proj,scale): return pytest.approx(eye+proj+(scale,)) - qtbot, win = main_clean - + qtbot, win = main_clean + editor = win.components['editor'] debugger = win.components['debugger'] viewer = win.components['viewer'] @@ -864,7 +888,7 @@ def test_selection(main_multi,mocker): ctx = viewer._get_context() ctx.InitSelected() shapes = [] - + while ctx.MoreSelected(): shapes.append(ctx.SelectedShape()) ctx.NextSelected() @@ -937,7 +961,7 @@ def test_screenshot(main,mocker): qtbot,win = main mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.png','')) - + viewer = win.components['viewer'] viewer._actions['Tools'][0].triggered.emit() @@ -945,9 +969,9 @@ def test_screenshot(main,mocker): def test_resize(main): - qtbot,win = main + qtbot,win = main editor = win.components['editor'] - + editor.hide() qtbot.wait(50) editor.show() @@ -1013,9 +1037,9 @@ def test_render_colors(main_clean): editor.set_text(code_color) debugger._actions['Run'][0].triggered.emit() - + CQ = obj_tree.CQ - + # object 1 (defualt color) assert not CQ.child(0).ais.HasColor() @@ -1048,7 +1072,7 @@ def test_render_colors(main_clean): # check if error occured qtbot.wait(100) assert('Unknown color format' in log.toPlainText().splitlines()[-1]) - + def test_render_colors_console(main_clean): qtbot, win = main_clean @@ -1058,12 +1082,12 @@ def test_render_colors_console(main_clean): console = win.components['console'] console.execute_command(code_color) - + CQ = obj_tree.CQ - + # object 1 (defualt color) assert not CQ.child(0).ais.HasColor() - + # object 2 r,g,b,a = get_rgba(CQ.child(1).ais) assert( a == 0.5 ) @@ -1088,7 +1112,7 @@ def test_render_colors_console(main_clean): r,g,b,a = get_rgba(CQ.child(5).ais) assert( a == 0.5 ) assert( r == 1.0 ) - + # check if error occured qtbot.wait(100) assert('Unknown color format' in log.toPlainText().splitlines()[-1]) @@ -1104,7 +1128,7 @@ def test_render_colors_console(main_clean): ''' def test_shading_aspect(main_clean): - + qtbot, win = main_clean obj_tree = win.components['object_tree'] @@ -1132,7 +1156,7 @@ def test_confirm_new(monkeypatch,editor): editor.document().setPlainText(code) assert(editor.modified == True) - + #monkeypatch the confirmation dialog and run both scenarios def cancel(*args, **kwargs): return QMessageBox.No @@ -1142,18 +1166,18 @@ def ok(*args, **kwargs): monkeypatch.setattr(QMessageBox, 'question', staticmethod(cancel)) - + editor.new() assert(editor.modified == True) assert(conv_line_ends(editor.get_text_with_eol()) == code) - + monkeypatch.setattr(QMessageBox, 'question', staticmethod(ok)) - + editor.new() assert(editor.modified == False) assert(editor.get_text_with_eol() == '') - + code_show_topods = \ ''' import cadquery as cq @@ -1182,16 +1206,16 @@ def test_render_topods(main): editor.set_text(code_show_topods) debugger._actions['Run'][0].triggered.emit() assert(obj_tree_comp.CQ.childCount() == 1) - + # test rendering of topods object via console console.execute('show(result.val().wrapped)') assert(obj_tree_comp.CQ.childCount() == 2) - + # test rendering of list of topods object via console console.execute('show([result.val().wrapped,result.val().wrapped])') assert(obj_tree_comp.CQ.childCount() == 3) - - + + code_show_shape_list = \ ''' import cadquery as cq @@ -1205,7 +1229,7 @@ def test_render_topods(main): def test_render_shape_list(main): qtbot, win = main - + log = win.components['log'] obj_tree_comp = win.components['object_tree'] @@ -1221,7 +1245,7 @@ def test_render_shape_list(main): editor.set_text(code_show_shape_list) debugger._actions['Run'][0].triggered.emit() assert(obj_tree_comp.CQ.childCount() == 2) - + # test rendering of Shape via console console.execute('show(result1)') console.execute('show([result1,result2])') @@ -1300,7 +1324,7 @@ def test_render_ais(main): console.execute('show(ais)') qtbot.wait(500) assert(obj_tree_comp.CQ.childCount() == 2) - + code_show_sketch = \ '''import cadquery as cq From f9d53f95ab80af7ad1062da301db617016cbe673 Mon Sep 17 00:00:00 2001 From: AU Date: Fri, 2 Jun 2023 23:51:00 +0200 Subject: [PATCH 056/134] Build an installer using constructor (#401) * First version of the constructor file * Installer build * Restructure using stages * Syntax fix * fix names * Add upload stage * Fix param * Try to upload something * Use the default location * path fixes * Remove installer_type * Use latest versions * Fix path * Update add * Update msg * Upload via releases * Change connection * Use prerelease * Use wildcard * Use root * Exclude nightly from triggers * Update README.md * Do not upload from PRs * Install debug step * Change agent to mac * Enable last step for now * Use sh * Add a debug step * Run qtdiag too * Bundle mamba * Remove the debug stage * Cleanup * Add entry points * Include the entry points in the installer * Mention run.sh/bat * Add a verify stage * Typo fix * Copy installers to cwd * use sh * Remove verify * Remove ubuntu 1804 --- README.md | 26 ++++++++++++++------ appveyor.yml | 1 - azure-pipelines.yml | 58 ++++++++++++++++++++++++++++++++++++++------ conda/construct.yaml | 24 ++++++++++++++++++ conda/run.bat | 2 ++ conda/run.sh | 2 ++ 6 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 conda/construct.yaml create mode 100644 conda/run.bat create mode 100644 conda/run.sh diff --git a/README.md b/README.md index af94db23..25a641fc 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. ## Notable features -* Automatic code reloading - you can any editor +* Automatic code reloading - you can use your favourite editor * OCCT based * Graphical debugger for CadQuery scripts * Step through script and watch how your model changes @@ -28,27 +28,37 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. ### Release Packages -Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download the zip file for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. On later MacOS versions you may also need `xattr -r -d com.apple.quarantine path/to/CQ-editor-MacOS`. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly. +Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download installer for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly. ### Development Packages -Development builds are also available, but can be unstable and should be used at your own risk. Click on the newest build with a green checkmark [here](https://github.com/jmwright/CQ-editor/actions?query=workflow%3Abuild), wait for the _Artifacts_ section at the bottom of the page to load, and then click on the appropriate download for your operating system. Extract the archive file and run the shell (Linux/MacOS) or cmd (Windows) script in the root CQ-editor directory. The CQ-editor window should launch. +Development builds are also available, but can be unstable and should be used at your own risk. You can download the newest build [here](https://github.com/CadQuery/CQ-editor/releases/tag/nightly). Install and run the `run.sh` (Linux/MacOS) or `run.bat` (Windows) script in the root CQ-editor directory. The CQ-editor window should launch. -## Installation (Anaconda) +### MacOS workarounds -Use conda to install: +On later MacOS versions you may also need `xattr -r -d com.apple.quarantine path/to/CQ-editor-MacOS`. + +## Installation (conda/mamba) + +Use conda or mamba to install: ``` -conda install -c cadquery -c conda-forge cq-editor=master +mamba install -c cadquery -c conda-forge cq-editor=master ``` and then simply type `cq-editor` to run it. This installs the latest version built directly from the HEAD of this repository. Alternatively clone this git repository and set up the following conda environment: ``` -conda env create -f cqgui_env.yml -n cqgui -conda activate cqgui +mamba env create -f cqgui_env.yml -n cqgui +mamba activate cqgui python run.py ``` +If you are concerned about mamba/conda modifying your shell settings, you can use [micromamba](https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html): +``` +micromamba install -n base -c cadquery cq-editor +micromamba run -n base cq-editor +``` + On some linux distributions (e.g. `Ubuntu 18.04`) it might be necessary to install additonal packages: ``` sudo apt install libglu1-mesa libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev diff --git a/appveyor.yml b/appveyor.yml index 899f7de6..24e019c2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,7 +2,6 @@ shallow_clone: false image: - Ubuntu2004 - - Ubuntu1804 - Visual Studio 2015 environment: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index bb1a6b83..0485c1e6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,6 +3,8 @@ trigger: include: - master - refs/tags/* + exclude: + - refs/tags/nightly pr: - master @@ -20,12 +22,54 @@ parameters: default: - 11 -jobs: -- ${{ each minor in parameters.minor }}: - - template: conda-build.yml@templates +stages: +- stage: build_conda_package + jobs: + - ${{ each minor in parameters.minor }}: + - template: conda-build.yml@templates + parameters: + name: Linux + vmImage: 'ubuntu-latest' + py_maj: 3 + py_min: ${{minor}} + conda_bld: 3.21.6 + +- stage: build_installers + jobs: + - template: constructor-build.yml@templates parameters: - name: Linux + name: linux vmImage: 'ubuntu-latest' - py_maj: 3 - py_min: ${{minor}} - conda_bld: 3.21.6 + - template: constructor-build.yml@templates + parameters: + name: win + vmImage: 'windows-latest' + - template: constructor-build.yml@templates + parameters: + name: macos + vmImage: 'macOS-latest' + +- stage: upload_installers + jobs: + - job: upload_to_github + condition: ne(variables['Build.Reason'], 'PullRequest') + pool: + vmImage: ubuntu-latest + steps: + - download: current + artifact: installer_ubuntu-latest + - download: current + artifact: installer_windows-latest + - download: current + artifact: installer_macOS-latest + - bash: cp $(Pipeline.Workspace)/installer*/*.* . + - task: GitHubRelease@1 + inputs: + gitHubConnection: github.com_oauth + assets: CQ-editor-*.* + action: edit + tag: nightly + target: d8e247d15001bf785ef7498d922b4b5aa017a9c9 + addChangeLog: false + assetUploadMode: replace + isPreRelease: true diff --git a/conda/construct.yaml b/conda/construct.yaml new file mode 100644 index 00000000..3955a7b8 --- /dev/null +++ b/conda/construct.yaml @@ -0,0 +1,24 @@ +name: CQ-editor +company: Cadquery +version: master +icon_image: ../icons/cadquery_logo_dark.ico +license_file: ../LICENSE +register_python: False +initialize_conda: False +keep_pkgs: False + +channels: + - conda-forge + - cadquery + +specs: + - cq-editor=master + - cadquery=master + - nomkl + - mamba + +menu_packages: [] + +extra_files: + - run.sh # [unix] + - run.bat # [win] diff --git a/conda/run.bat b/conda/run.bat new file mode 100644 index 00000000..62d48d05 --- /dev/null +++ b/conda/run.bat @@ -0,0 +1,2 @@ +@echo off +start /B Scripts/CQ-editor.exe diff --git a/conda/run.sh b/conda/run.sh new file mode 100644 index 00000000..cee7b086 --- /dev/null +++ b/conda/run.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec bin/cq-editor From bc05b6bcb714f58b84c8130f763ee2ada819881e Mon Sep 17 00:00:00 2001 From: AU Date: Sun, 4 Jun 2023 13:04:35 +0200 Subject: [PATCH 057/134] Consructor tweaks (#404) * Add verify again * Make run.sh executable * Disable verify --- azure-pipelines.yml | 13 +++++++++++++ conda/run.sh | 0 2 files changed, 13 insertions(+) mode change 100644 => 100755 conda/run.sh diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0485c1e6..77e02d43 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -73,3 +73,16 @@ stages: addChangeLog: false assetUploadMode: replace isPreRelease: true + +# stage left for debugging, disabled by default +- stage: verify + condition: False + jobs: + - job: verify_linux + pool: + vmImage: ubuntu-latest + steps: + - download: current + artifact: installer_ubuntu-latest + - bash: cp $(Pipeline.Workspace)/installer*/*.* . + - bash: sh ./CQ-editor-master-Linux-x86_64.sh -b -p dummy && cd dummy && ./run.sh diff --git a/conda/run.sh b/conda/run.sh old mode 100644 new mode 100755 From 181adb99de9aa8cecba105069502be491e927342 Mon Sep 17 00:00:00 2001 From: snoyer Date: Sat, 26 Aug 2023 21:37:23 +0400 Subject: [PATCH 058/134] set `__file__` variable when running from a file (#408) --- cq_editor/widgets/debugger.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 3bb06607..1e96e89a 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -164,15 +164,23 @@ def __init__(self,parent): def get_current_script(self): return self.parent().components['editor'].get_text_with_eol() + + def get_current_script_path(self): + + filename = self.parent().components["editor"].filename + if filename: + return Path(filename).abspath() def get_breakpoints(self): return self.parent().components['editor'].debugger.get_breakpoints() - def compile_code(self, cq_script): + def compile_code(self, cq_script, cq_script_path=None): try: module = ModuleType('__cq_main__') + if cq_script_path: + module.__dict__["__file__"] = cq_script_path cq_code = compile(cq_script, DUMMY_FILE, 'exec') return cq_code, module except Exception: @@ -182,8 +190,7 @@ def compile_code(self, cq_script): def _exec(self, code, locals_dict, globals_dict): with ExitStack() as stack: - fname = self.parent().components['editor'].filename - p = Path(fname if fname else '').abspath().dirname() + p = (self.get_current_script_path() or Path("")).abspath().dirname() if self.preferences['Add script dir to path'] and p.exists(): sys.path.insert(0,p) @@ -228,7 +235,8 @@ def render(self): reload_cq() cq_script = self.get_current_script() - cq_code,module = self.compile_code(cq_script) + cq_script_path = self.get_current_script_path() + cq_code,module = self.compile_code(cq_script, cq_script_path) if cq_code is None: return @@ -269,7 +277,8 @@ def debug(self,value): self.state = DbgState.STEP self.script = self.get_current_script() - code,module = self.compile_code(self.script) + cq_script_path = self.get_current_script_path() + code,module = self.compile_code(self.script, cq_script_path) if code is None: self.sigDebugging.emit(False) From 4ef178af06d24a53fee87d576f8cada14c0111a3 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Wed, 6 Dec 2023 15:47:28 -0500 Subject: [PATCH 059/134] Fix constructor missing library issue when env is not activated (#417) * Call conda.exe run in win constructor run.bat script --- conda/run.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda/run.bat b/conda/run.bat index 62d48d05..05726338 100644 --- a/conda/run.bat +++ b/conda/run.bat @@ -1,2 +1,2 @@ @echo off -start /B Scripts/CQ-editor.exe +start /B Scripts\conda.exe run -n base python Scripts\cq-editor-script.py \ No newline at end of file From 6b4b0bbdd3005dcaf99d53e7b84ece0243da1b57 Mon Sep 17 00:00:00 2001 From: roel-v Date: Thu, 18 Jan 2024 08:43:52 +0100 Subject: [PATCH 060/134] Win32 strip console sequences in log (#422) * Fix icon in taskbar on Windows * Strip console escape codes from log view. They color the output in the embedded interpreter view but make the log view hard to read. * Revert "Fix icon in taskbar on Windows" This reverts commit 3aa665b8e46539b67c414a46831cf520b2c3ce3d. * Strip escape sequences on all platforms --- cq_editor/widgets/log.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cq_editor/widgets/log.py b/cq_editor/widgets/log.py index c7478611..0186f8f1 100644 --- a/cq_editor/widgets/log.py +++ b/cq_editor/widgets/log.py @@ -1,10 +1,21 @@ import logbook as logging +import sys +import re from PyQt5.QtWidgets import QPlainTextEdit from PyQt5 import QtCore from ..mixins import ComponentMixin +def strip_escape_sequences(input_string): + # Regular expression pattern to match ANSI escape codes + escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + + # Use re.sub to replace escape codes with an empty string + clean_string = re.sub(escape_pattern, '', input_string) + + return clean_string + class QtLogHandler(logging.Handler,logging.StringFormatterHandlerMixin): def __init__(self, log_widget,*args,**kwargs): @@ -17,6 +28,9 @@ def __init__(self, log_widget,*args,**kwargs): def emit(self, record): msg = self.format(record) + + msg = strip_escape_sequences(msg) + QtCore.QMetaObject\ .invokeMethod(self.log_widget, 'appendPlainText', @@ -40,4 +54,4 @@ def __init__(self,*args,**kwargs): def append(self,msg): - self.appendPlainText(msg) \ No newline at end of file + self.appendPlainText(msg) From 7d74cf77c7d8eb02bef91f180ec101627131322e Mon Sep 17 00:00:00 2001 From: AU Date: Sun, 28 Jan 2024 10:26:10 +0100 Subject: [PATCH 061/134] Update text regarding prebuilt packages --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 25a641fc..3a128889 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. ## Installation - Pre-Built Packages (Recommended) -### Release Packages +~~### Release Packages~~ -Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download installer for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly. +~~Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download installer for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly.~~ ### Development Packages From 03df6fa64beda77e374bf4160411e36e0764d332 Mon Sep 17 00:00:00 2001 From: roel-v Date: Thu, 1 Feb 2024 22:19:56 +0100 Subject: [PATCH 062/134] Fix icon in taskbar on Windows (#421) * Fix icon in taskbar on Windows * Final touches --------- Co-authored-by: AU --- cq_editor/main_window.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index de895cf7..d2926977 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -32,6 +32,12 @@ def __init__(self,parent=None, filename=None): self.setWindowIcon(icon('app')) + # Windows workaround - makes the correct task bar icon show up. + if sys.platform == "win32": + import ctypes + myappid = 'cq-editor' # arbitrary string + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) + self.viewer = OCCViewer(self) self.setCentralWidget(self.viewer.canvas) From b4a30e4b58fbb332c69889649b525e358b05ffec Mon Sep 17 00:00:00 2001 From: Stefan Biereigel <6706965+thasti@users.noreply.github.com> Date: Sun, 2 Jun 2024 20:37:35 +0200 Subject: [PATCH 063/134] use importlib instead of imp (#428) --- cq_editor/cq_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index a42836cc..99aae0d8 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -2,7 +2,7 @@ from cadquery.occ_impl.assembly import toCAF from typing import List, Union -from imp import reload +from importlib import reload from types import SimpleNamespace from OCP.XCAFPrs import XCAFPrs_AISObject From acae13377674d54b1e17594f4857c97a09cdbfd3 Mon Sep 17 00:00:00 2001 From: AU Date: Sun, 2 Jun 2024 21:59:26 +0200 Subject: [PATCH 064/134] Show more objects by default (#437) * Show more objects by default * Better instance check * Istance check workaround CQ reloading is not robust, so for now going to use this workaround * Add/fix tests --- cq_editor/cq_utils.py | 20 +++++++++++++++++++- tests/test_app.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 99aae0d8..32115a3f 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -17,9 +17,18 @@ DEFAULT_FACE_COLOR = Quantity_Color(GOLD) DEFAULT_MATERIAL = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE) + +def is_cq_obj(obj): + + from cadquery import Workplane, Shape, Assembly, Sketch + + return isinstance(obj, (Workplane, Shape, Assembly, Sketch)) + + def find_cq_objects(results : dict): - return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if isinstance(v,cq.Workplane)} + return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if is_cq_obj(v)} + def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch]): @@ -47,6 +56,7 @@ def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq. return cq.Compound.makeCompound(vals) + def to_workplane(obj : cq.Shape): rv = cq.Workplane('XY') @@ -54,6 +64,7 @@ def to_workplane(obj : cq.Shape): return rv + def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly, AIS_InteractiveObject], options={}): @@ -82,6 +93,7 @@ def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Sha return ais,shape + def export(obj : Union[cq.Workplane, List[cq.Workplane]], type : str, file, precision=1e-1): @@ -94,6 +106,7 @@ def export(obj : Union[cq.Workplane, List[cq.Workplane]], type : str, elif type == 'brep': comp.exportBrep(file) + def to_occ_color(color) -> Quantity_Color: if not isinstance(color, QColor): @@ -112,6 +125,7 @@ def to_occ_color(color) -> Quantity_Color: color.blueF(), TOC_RGB) + def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: if isinstance(obj, AIS_InteractiveObject): @@ -122,6 +136,7 @@ def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: return QColor.fromRgbF(color.Red(), color.Green(), color.Blue()) + def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape: drawer = ais.Attributes() @@ -130,6 +145,7 @@ def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape: return ais + def set_material(ais : AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Shape: drawer = ais.Attributes() @@ -138,6 +154,7 @@ def set_material(ais : AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Sha return ais + def set_transparency(ais : AIS_Shape, alpha: float) -> AIS_Shape: drawer = ais.Attributes() @@ -146,6 +163,7 @@ def set_transparency(ais : AIS_Shape, alpha: float) -> AIS_Shape: return ais + def reload_cq(): # NB: order of reloads is important diff --git a/tests/test_app.py b/tests/test_app.py index eb7463e0..0621024d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -84,6 +84,13 @@ r1 = cq.Workplane(solid1).translate((10, 0, 0)) """ +code_show_all = """import cadquery as cq +b = cq.Workplane().box(1,1,1) +sh = b.val() +a = cq.Assembly().add(sh) +sk = cq.Sketch().rect(1,1) +""" + def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) @@ -251,11 +258,11 @@ def test_render(main): debugger._actions['Run'][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.childCount() == 1) + assert(obj_tree_comp.CQ.childCount() == 3) debugger._actions['Run'][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.childCount() == 1) + assert(obj_tree_comp.CQ.childCount() == 3) def test_export(main,mocker): @@ -1488,3 +1495,22 @@ def test_modulefinder(tmp_path, main): qtbot.wait(100) assert("Cannot determine imported modules" in log.toPlainText().splitlines()[-1]) +def test_show_all(main): + + qtbot, win = main + + editor = win.components['editor'] + debugger = win.components['debugger'] + object_tree = win.components['object_tree'] + + # remove all objects + object_tree.removeObjects() + assert(object_tree.CQ.childCount() == 0) + + # add code wtih Shape, Workplane, Assy, Sketch + editor.set_text(code_show_all) + + # Run and check if all are shown + debugger._actions['Run'][0].triggered.emit() + + assert(object_tree.CQ.childCount() == 4) From 98711d8a6db20eec4f3607b61e2e73577aae5698 Mon Sep 17 00:00:00 2001 From: Matthias Urlichs Date: Tue, 4 Jun 2024 21:09:38 +0200 Subject: [PATCH 065/134] Show the traceback. (#431) * Show the traceback. Also trace into external modules. Closes #429. * Use itertools for clarity * Cleanup * Remove empty line * Cover new functionality with tests --------- Co-authored-by: AU --- cq_editor/widgets/traceback_viewer.py | 10 ++++++---- tests/test_app.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index d5e0baa6..7d1051a0 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -1,4 +1,5 @@ from traceback import extract_tb, format_exception_only +from itertools import dropwhile from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction, QLabel) @@ -55,15 +56,16 @@ def addTraceback(self,exc_info,code): root = self.tree.root code = code.splitlines() - tb = [t for t in extract_tb(tb) if '' in t.filename] #ignore highest frames (debug, exec) - - for el in tb: + + for el in dropwhile( + lambda el: 'string>' not in el.filename, extract_tb(tb) + ): #workaround of the traceback module if el.line == '': line = code[el.lineno-1].strip() else: line = el.line - + root.addChild(QTreeWidgetItem([el.filename, str(el.lineno), line])) diff --git a/tests/test_app.py b/tests/test_app.py index 0621024d..2295942c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -517,6 +517,10 @@ def check_no_error_occured(): result = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125) f() ''' +code_err3 =\ +'''import cadquery as cq +result = cq.Workplane("XY" ).box(3, 3, 0) +''' def test_traceback(main): @@ -560,10 +564,18 @@ def test_traceback(main): assert('NameError' in traceback_view.current_exception.text()) assert(hasattr(sys, 'last_traceback')) + assert(traceback_view.tree.root.childCount() == 1) # restore the tracing function sys.settrace(trace_function) + # check if errors deeper in CQ are reported too + editor.set_text(code_err3) + run.triggered.emit() + + assert('Standard_DomainError' in traceback_view.current_exception.text()) + assert(traceback_view.tree.root.childCount() == 3) # 1 in user code + 2 in CQ code + @pytest.fixture def editor(qtbot): From d8f9347e5f04b5a2264042daaae2b4779d40de57 Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Fri, 7 Jun 2024 15:42:11 -0500 Subject: [PATCH 066/134] Add a random color function to CQ-editor (#379) * add log() function to the built-in CQ-editor console * Move new logbook import to top of main_window.py * enable rand_color in debugger.py * Register rand_color for use in console in main_window.py * Revert "enable rand_color in debugger.py" This reverts commit 37f6b706cf4da273b6367c66e197f2c812a52647. * Re-enable rand_color in debugger.py and fix diff * Update test_debug assertion assert(variables.model().rowCount() == 1) #L406 * Revert changes * Try waiting * Move _rand_color * Disable for testing * Fix the tests Fix the expected values Add working checks for inner event loop errors * Fix the stop debugging functionality * Add _rand_color again * Update rand_color seed seed(371353) -> seed(59798267586177) The first few colors are more distinguishable with the new seed. (Did a programmatic search of several million seeds to find this one) * Simple smoke test for rand color * Better coverage --------- Co-authored-by: AU --- cq_editor/main_window.py | 1 + cq_editor/widgets/debugger.py | 23 +++++++++++++ tests/test_app.py | 64 +++++++++++++++++++++++------------ 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index d2926977..4bbee280 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -282,6 +282,7 @@ def prepare_console(self): #CQ related items console.push_vars({'show' : obj_tree.addObject, 'show_object' : obj_tree.addObject, + 'rand_color' : self.components['debugger']._rand_color, 'cq' : cq, 'log' : Logger(self.name).info}) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 1e96e89a..6a4e712c 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -14,6 +14,7 @@ from path import Path from pyqtgraph.parametertree import Parameter from spyder.utils.icon_manager import icon +from random import randrange as rrr,seed from ..cq_utils import find_cq_objects, reload_cq from ..mixins import ComponentMixin @@ -202,6 +203,26 @@ def _exec(self, code, locals_dict, globals_dict): exec(code, locals_dict, globals_dict) + @staticmethod + def _rand_color(alpha = 0., cfloat=False): + #helper function to generate a random color dict + #for CQ-editor's show_object function + lower = 10 + upper = 100 #not too high to keep color brightness in check + if cfloat: #for two output types depending on need + return ( + (rrr(lower,upper)/255), + (rrr(lower,upper)/255), + (rrr(lower,upper)/255), + alpha, + ) + return {"alpha": alpha, + "color": ( + rrr(lower,upper), + rrr(lower,upper), + rrr(lower,upper), + )} + def _inject_locals(self,module): cq_objects = {} @@ -219,6 +240,7 @@ def _debug(obj,name=None): module.__dict__['show_object'] = _show_object module.__dict__['debug'] = _debug + module.__dict__['rand_color'] = self._rand_color module.__dict__['log'] = lambda x: info(str(x)) module.__dict__['cq'] = cq @@ -231,6 +253,7 @@ def _cleanup_locals(self,module,injected_names): @pyqtSlot(bool) def render(self): + seed(59798267586177) if self.preferences['Reload CQ']: reload_cq() diff --git a/tests/test_app.py b/tests/test_app.py index 2295942c..11851fb5 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -401,24 +401,18 @@ def check_no_error_occured(): assert(debugger._frames == []) #test step through - ev = event_loop([ - lambda: ( - assert_func(variables.model().rowCount() == 4), - assert_func(number_visible_items(viewer) == 3), - step.triggered.emit()), - lambda: ( - assert_func(variables.model().rowCount() == 4), - assert_func(number_visible_items(viewer) == 3), - step.triggered.emit()), - lambda: ( - assert_func(variables.model().rowCount() == 5), - assert_func(number_visible_items(viewer) == 3), - step.triggered.emit()), - lambda: ( - assert_func(variables.model().rowCount() == 5), - assert_func(number_visible_items(viewer) == 4), - cont.triggered.emit()) - ]) + ev = event_loop([lambda: (assert_func(variables.model().rowCount() == 5), + assert_func(number_visible_items(viewer) == 3), + step.triggered.emit()), + lambda: (assert_func(variables.model().rowCount() == 5), + assert_func(number_visible_items(viewer) == 3), + step.triggered.emit()), + lambda: (assert_func(variables.model().rowCount() == 6), + assert_func(number_visible_items(viewer) == 3), + step.triggered.emit()), + lambda: (assert_func(variables.model().rowCount() == 6), + assert_func(number_visible_items(viewer) == 4), + cont.triggered.emit())]) patch_debugger(debugger,ev) @@ -431,7 +425,7 @@ def check_no_error_occured(): #test exit debug ev = event_loop([lambda: (step.triggered.emit(),), - lambda: (assert_func(variables.model().rowCount() == 4), + lambda: (assert_func(variables.model().rowCount() == 5), assert_func(number_visible_items(viewer) == 3), debug.triggered.emit(False),)]) @@ -446,7 +440,7 @@ def check_no_error_occured(): #test breakpoint ev = event_loop([lambda: (cont.triggered.emit(),), - lambda: (assert_func(variables.model().rowCount() == 5), + lambda: (assert_func(variables.model().rowCount() == 6), assert_func(number_visible_items(viewer) == 4), cont.triggered.emit(),)]) @@ -463,7 +457,7 @@ def check_no_error_occured(): #test breakpoint without using singals ev = event_loop([lambda: (cont.triggered.emit(),), - lambda: (assert_func(variables.model().rowCount() == 5), + lambda: (assert_func(variables.model().rowCount() == 6), assert_func(number_visible_items(viewer) == 4), cont.triggered.emit(),)]) @@ -480,7 +474,7 @@ def check_no_error_occured(): #test debug() without using singals ev = event_loop([lambda: (cont.triggered.emit(),), - lambda: (assert_func(variables.model().rowCount() == 5), + lambda: (assert_func(variables.model().rowCount() == 6), assert_func(number_visible_items(viewer) == 4), cont.triggered.emit(),)]) @@ -1526,3 +1520,29 @@ def test_show_all(main): debugger._actions['Run'][0].triggered.emit() assert(object_tree.CQ.childCount() == 4) + +code_randcolor = \ +"""import cadquery as cq +b = cq.Workplane().box(8, 3, 4) +for i in range(10): + show_object(b.translate((0,5*i,0)), options=rand_color(alpha=0)) + show_object(b.translate((0,5*i,0)), options=rand_color(0, True)) +""" + +def test_randcolor(main): + + qtbot, win = main + + obj_tree_comp = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + console = win.components['console'] + + # check that object was removed + obj_tree_comp._toolbar_actions[0].triggered.emit() + assert(obj_tree_comp.CQ.childCount() == 0) + + # check that object was rendered usin explicit show_object call + editor.set_text(code_randcolor) + debugger._actions['Run'][0].triggered.emit() + assert(obj_tree_comp.CQ.childCount() == 2*10) From 7133ab6bc3b16c3bfd7b362da4cae201a2f4d132 Mon Sep 17 00:00:00 2001 From: Adam Vogt Date: Thu, 13 Jun 2024 15:39:49 -0400 Subject: [PATCH 067/134] fix #338 Current document is not saved (#433) --- cq_editor/widgets/editor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index eb70537d..6e3fb34d 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -237,6 +237,7 @@ def _file_changed(self): # neovim writes a file by removing it first so must re-add each time self._watch_paths() self.set_text_from_file(self._filename) + self.reset_modified() self.triggerRerender.emit(True) # Turn autoreload on/off. From c9f9cbd000496e0045a56763db883211a6f9a5e5 Mon Sep 17 00:00:00 2001 From: AU Date: Sun, 16 Jun 2024 09:02:32 +0200 Subject: [PATCH 068/134] Better show_object (#438) * Better show_object Get object name from the enclosing scope if possible * Add test --- cq_editor/widgets/debugger.py | 13 ++++++++++++- tests/test_app.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 6a4e712c..1e9d8f1e 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -4,6 +4,7 @@ from types import SimpleNamespace, FrameType, ModuleType from typing import List from bdb import BdbQuit +from inspect import currentframe import cadquery as cq from PyQt5 import QtCore @@ -232,7 +233,17 @@ def _show_object(obj,name=None, options={}): if name: cq_objects.update({name : SimpleNamespace(shape=obj,options=options)}) else: - cq_objects.update({str(id(obj)) : SimpleNamespace(shape=obj,options=options)}) + #get locals of the enclosing scope + d = currentframe().f_back.f_locals + + #try to find the name + try: + name = list(d.keys())[list(d.values()).index(obj)] + except ValueError: + #use id if not found + name = str(id(obj)) + + cq_objects.update({name : SimpleNamespace(shape=obj,options=options)}) def _debug(obj,name=None): diff --git a/tests/test_app.py b/tests/test_app.py index 11851fb5..2ec4baa7 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1546,3 +1546,39 @@ def test_randcolor(main): editor.set_text(code_randcolor) debugger._actions['Run'][0].triggered.emit() assert(obj_tree_comp.CQ.childCount() == 2*10) + +code_show_wo_name = \ +""" +import cadquery as cq + +res = cq.Workplane().box(1,1,1) + +show_object(res) +show_object(cq.Workplane().box(1,1,1)) +""" + +def test_show_without_name(main): + + qtbot, win = main + + editor = win.components['editor'] + debugger = win.components['debugger'] + object_tree = win.components['object_tree'] + + # remove all objects + object_tree.removeObjects() + assert(object_tree.CQ.childCount() == 0) + + # add code wtih Shape, Workplane, Assy, Sketch + editor.set_text(code_show_wo_name) + + # Run and check if all are shown + debugger._actions['Run'][0].triggered.emit() + + assert(object_tree.CQ.childCount() == 2) + + # Check the name of the first object + assert(object_tree.CQ.child(0).text(0) == "res") + + # Check that the name of the seconf object is an int + int(object_tree.CQ.child(1).text(0)) From 089bb861e81d8a6201f04f737b2f584de61ba101 Mon Sep 17 00:00:00 2001 From: AU Date: Sun, 11 Aug 2024 18:44:24 +0200 Subject: [PATCH 069/134] Abspath fix (#447) * Abspath fix * Fix editor --- cq_editor/widgets/debugger.py | 4 ++-- cq_editor/widgets/editor.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 1e9d8f1e..70d5795f 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -171,7 +171,7 @@ def get_current_script_path(self): filename = self.parent().components["editor"].filename if filename: - return Path(filename).abspath() + return Path(filename).absolute() def get_breakpoints(self): @@ -192,7 +192,7 @@ def compile_code(self, cq_script, cq_script_path=None): def _exec(self, code, locals_dict, globals_dict): with ExitStack() as stack: - p = (self.get_current_script_path() or Path("")).abspath().dirname() + p = (self.get_current_script_path() or Path("")).absolute().dirname() if self.preferences['Add script dir to path'] and p.exists(): sys.path.insert(0,p) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 6e3fb34d..20bee81f 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -148,7 +148,7 @@ def open(self): if not self.confirm_discard(): return - curr_dir = Path(self.filename).abspath().dirname() + curr_dir = Path(self.filename).absolute().dirname() fname = get_open_filename(self.EXTENSIONS, curr_dir) if fname != '': self.load_from_file(fname) From 848b946fb28564ded0f365c533a10665f2138aad Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 28 Jan 2025 08:28:20 -0500 Subject: [PATCH 070/134] Removed broken occ_impl.exporters.utils import --- cq_editor/cq_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 32115a3f..2378a82c 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -181,7 +181,6 @@ def reload_cq(): reload(cq.sketch) reload(cq.occ_impl.exporters.svg) reload(cq.cq) - reload(cq.occ_impl.exporters.utils) reload(cq.occ_impl.exporters.dxf) reload(cq.occ_impl.exporters.amf) reload(cq.occ_impl.exporters.json) From 46b421863bdc67d1d4c08c167974ac76abd39414 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 28 Jan 2025 08:48:09 -0500 Subject: [PATCH 071/134] Updated links for package name changes --- appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 24e019c2..60ee6084 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,11 +13,11 @@ environment: install: - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then sudo apt update; sudo apt -y --force-yes install libglu1-mesa xvfb libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev; fi - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Linux-x86_64.sh; fi - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-MacOSX-x86_64.sh; fi + - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh; fi + - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Darwin-x86_64.sh; fi - sh: bash miniconda.sh -b -p $HOME/miniconda - sh: source $HOME/miniconda/bin/activate - - cmd: curl -fsSL -o miniconda.exe https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Windows-x86_64.exe + - cmd: curl -fsSL -o miniconda.exe https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Windows-x86_64.exe - cmd: miniconda.exe /S /InstallationType=JustMe /D=%MINICONDA_DIRNAME% - cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - cmd: activate From 7c22e7a36650e487270a890d9a5d79a7333ed561 Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Tue, 28 Jan 2025 10:09:36 -0600 Subject: [PATCH 072/134] log.py -> drop microseconds completely --- cq_editor/widgets/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cq_editor/widgets/log.py b/cq_editor/widgets/log.py index c244a0cb..03a82648 100644 --- a/cq_editor/widgets/log.py +++ b/cq_editor/widgets/log.py @@ -11,7 +11,7 @@ def __init__(self, log_widget,*args,**kwargs): super(QtLogHandler,self).__init__(*args,**kwargs) - log_format_string = '[{record.time:%H:%M:%S.%f%z}] {record.level_name}: {record.message}' + log_format_string = '[{record.time:%H:%M:%S%z}] {record.level_name}: {record.message}' logging.StringFormatterHandlerMixin.__init__(self,log_format_string) From 43caa13dc43dde861b8ea779dc97b15f4bd1e4d8 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Fri, 31 Jan 2025 23:52:08 +0000 Subject: [PATCH 073/134] fix failing tests --- cq_editor/widgets/object_tree.py | 2 +- tests/test_app.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 0884e9e6..de2ffb2b 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -293,7 +293,7 @@ def addObject(self,obj,name='',options=None): ais=ais, sig=self.sigObjectPropertiesChanged)) - self.sigObjectsAdded.emit([ais], name) + self.sigObjectsAdded.emit([ais], [name]) @pyqtSlot(list) @pyqtSlot() diff --git a/tests/test_app.py b/tests/test_app.py index 2ec4baa7..d2007d1a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -747,9 +747,10 @@ def test_console(main): assert(len(a) == 1) # test print_text - pos_orig = console._prompt_pos - console.print_text('a') - assert(console._prompt_pos == pos_orig + len('a')) + text_before = console._control.document().toPlainText() + console.print_text('foo') + text_after = console._control.document().toPlainText() + assert text_after == text_before + 'foo' def test_viewer(main): From 2edd4f56e70aee441c7ad6b4130eff76dcdfdd03 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sat, 1 Feb 2025 10:18:02 -0500 Subject: [PATCH 074/134] Added basic pyproject.toml file --- pyproject.toml | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..157c5b5d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "CQ-editor" +version = "0.3-dev" +dependencies = [ + "cadquery", + "pyqtgraph", + "spyder==5", + "path", + "logbook", + "requests", + "qtconsole==5.4.1" +] +requires-python = ">=3.9,<3.14" +authors = [ + { name="CadQuery Developers" } +] +maintainers = [ + { name="CadQuery Developers" } +] +description = "CadQuery plugin to create a mesh of an assembly with corresponding data" +readme = "README.md" +license = {file = "LICENSE"} +keywords = ["cadquery", "CAD", "engineering", "design"] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python" +] + +[project.optional-dependencies] +test = [ + "pytest", + "pluggy", + "pytest-qt", + "pytest-mock", + "pytest-repeat", + "pyvirtualdisplay" +] +dev = [ + "black", +] + +[project.urls] +Repository = "https://github.com/CadQuery/CQ-editor.git" +"Bug Tracker" = "https://github.com/CadQuery/CQ-editor/issues" From 85a0f3d2d34577d516efb4f2ce720d25e65b4dc9 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sat, 1 Feb 2025 11:04:40 -0500 Subject: [PATCH 075/134] Added a CQ-editor script so that pip installation will match conda --- cq_editor/cqe_run.py | 16 ++++++++++++++++ pyproject.toml | 3 +++ 2 files changed, 19 insertions(+) create mode 100644 cq_editor/cqe_run.py diff --git a/cq_editor/cqe_run.py b/cq_editor/cqe_run.py new file mode 100644 index 00000000..fd9fe09b --- /dev/null +++ b/cq_editor/cqe_run.py @@ -0,0 +1,16 @@ +import os, sys, asyncio +import faulthandler + +faulthandler.enable() + +if 'CASROOT' in os.environ: + del os.environ['CASROOT'] + +if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +from cq_editor.__main__ import main + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 157c5b5d..13fa06ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,9 @@ classifiers = [ "Programming Language :: Python" ] +[project.scripts] +CQ-editor = "cq_editor.cqe_run:main" + [project.optional-dependencies] test = [ "pytest", From 91caa79de28bb155bed0e70e4a0fe81229e59d34 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Sat, 1 Feb 2025 21:48:08 +0000 Subject: [PATCH 076/134] resolve deprecation warning --- tests/test_app.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index d2007d1a..71507cac 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,6 +1,8 @@ from path import Path import os, sys, asyncio +from pytestqt.qtbot import QtBot + if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -124,7 +126,7 @@ def get_rgba(ais): return color.redF(), color.greenF(), color.blueF(), alpha @pytest.fixture -def main(qtbot,mocker): +def main(qtbot: QtBot, mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) @@ -142,15 +144,14 @@ def main(qtbot,mocker): return qtbot, win @pytest.fixture -def main_clean(qtbot,mocker): +def main_clean(qtbot: QtBot, mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) win = MainWindow() - win.show() - qtbot.addWidget(win) - qtbot.waitForWindowShown(win) + with qtbot.waitExposed(win): + win.show() editor = win.components['editor'] editor.set_text(code) @@ -158,15 +159,14 @@ def main_clean(qtbot,mocker): return qtbot, win @pytest.fixture -def main_clean_do_not_close(qtbot,mocker): +def main_clean_do_not_close(qtbot: QtBot, mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.No) win = MainWindow() - win.show() - qtbot.addWidget(win) - qtbot.waitForWindowShown(win) + with qtbot.waitExposed(win): + win.show() editor = win.components['editor'] editor.set_text(code) @@ -174,16 +174,15 @@ def main_clean_do_not_close(qtbot,mocker): return qtbot, win @pytest.fixture -def main_multi(qtbot,mocker): +def main_multi(qtbot: QtBot, mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) win = MainWindow() - win.show() - qtbot.addWidget(win) - qtbot.waitForWindowShown(win) + with qtbot.waitExposed(win): + win.show() editor = win.components['editor'] editor.set_text(code_multi) @@ -571,7 +570,7 @@ def test_traceback(main): assert(traceback_view.tree.root.childCount() == 3) # 1 in user code + 2 in CQ code @pytest.fixture -def editor(qtbot): +def editor(qtbot: QtBot): win = Editor() win.show() From 8454b5e6475fba3974e3cce4e6223daccfd75c18 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Sat, 1 Feb 2025 21:50:13 +0000 Subject: [PATCH 077/134] increase timeout --- tests/test_app.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 71507cac..a5ad8490 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -2,6 +2,7 @@ import os, sys, asyncio from pytestqt.qtbot import QtBot +import pytestqt.exceptions if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -93,6 +94,8 @@ sk = cq.Sketch().rect(1,1) """ +TIMEOUT = 10_000 + def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) @@ -150,7 +153,7 @@ def main_clean(qtbot: QtBot, mocker): win = MainWindow() qtbot.addWidget(win) - with qtbot.waitExposed(win): + with qtbot.waitExposed(win, timeout=TIMEOUT): win.show() editor = win.components['editor'] @@ -165,7 +168,7 @@ def main_clean_do_not_close(qtbot: QtBot, mocker): win = MainWindow() qtbot.addWidget(win) - with qtbot.waitExposed(win): + with qtbot.waitExposed(win, timeout=TIMEOUT): win.show() editor = win.components['editor'] @@ -181,7 +184,7 @@ def main_multi(qtbot: QtBot, mocker): win = MainWindow() qtbot.addWidget(win) - with qtbot.waitExposed(win): + with qtbot.waitExposed(win, timeout=TIMEOUT): win.show() editor = win.components['editor'] @@ -662,8 +665,6 @@ def test_editor_autoreload(monkeypatch,editor): qtbot, editor = editor - TIMEOUT = 500 - # start out with autoreload enabled editor.autoreload(True) @@ -712,7 +713,6 @@ def test_autoreload_nested(editor): qtbot, editor = editor - TIMEOUT = 500 editor.autoreload(True) editor.preferences['Autoreload: watch imported modules'] = True @@ -1440,7 +1440,6 @@ def makebox(z): def test_reload_import_handle_error(tmp_path, main): - TIMEOUT = 500 qtbot, win = main editor = win.components["editor"] debugger = win.components["debugger"] @@ -1481,7 +1480,6 @@ def test_reload_import_handle_error(tmp_path, main): def test_modulefinder(tmp_path, main): - TIMEOUT = 500 qtbot, win = main editor = win.components["editor"] debugger = win.components["debugger"] From cb96c0fb001cb362c3f1ce3a05415c3a376a3171 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sun, 2 Feb 2025 16:48:41 -0500 Subject: [PATCH 078/134] Cleaning up a little bit --- appveyor.yml | 2 +- cq_editor/cqe_run.py | 3 --- cq_editor/main_window.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 60ee6084..6ff5ac89 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,7 +43,7 @@ test_script: - cmd: pytest -v --cov=cq_editor on_success: - - codecov + - codecov -X "cq_editor/cqe_run.py" #on_failure: # - qtdiag diff --git a/cq_editor/cqe_run.py b/cq_editor/cqe_run.py index fd9fe09b..bbd79682 100644 --- a/cq_editor/cqe_run.py +++ b/cq_editor/cqe_run.py @@ -1,7 +1,4 @@ import os, sys, asyncio -import faulthandler - -faulthandler.enable() if 'CASROOT' in os.environ: del os.environ['CASROOT'] diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 4bbee280..dbf8b5f0 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -289,7 +289,7 @@ def prepare_console(self): def fill_dummy(self): self.components['editor']\ - .set_text('import cadquery as cq\nresult = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125)') + .set_text('import cadquery as cq\nresult = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125)\nshow_object(result)') def setup_logging(self): From 906b7bc231b1c5fabe441e2fb5d8c34910f17716 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sun, 2 Feb 2025 18:40:48 -0500 Subject: [PATCH 079/134] Trying a codecov config file --- .codecov.yml | 4 ++++ appveyor.yml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000..c80b02b4 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,4 @@ +codecov: + exclude: + - "run.py" + - "cq_editor/cqe_run.py" diff --git a/appveyor.yml b/appveyor.yml index 6ff5ac89..60ee6084 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,7 +43,7 @@ test_script: - cmd: pytest -v --cov=cq_editor on_success: - - codecov -X "cq_editor/cqe_run.py" + - codecov #on_failure: # - qtdiag From b862cba997229a6bb038f55c2e6d0a5d384bc1ec Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sun, 2 Feb 2025 19:18:37 -0500 Subject: [PATCH 080/134] Removed codecov config file again --- .codecov.yml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index c80b02b4..00000000 --- a/.codecov.yml +++ /dev/null @@ -1,4 +0,0 @@ -codecov: - exclude: - - "run.py" - - "cq_editor/cqe_run.py" From 74c1b7730f3513ba39bb4061959671e9c0df99b6 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sun, 2 Feb 2025 19:21:44 -0500 Subject: [PATCH 081/134] Remove Python 3.13 support for 0.3 release, will fix in 0.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 13fa06ae..4be43159 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "requests", "qtconsole==5.4.1" ] -requires-python = ">=3.9,<3.14" +requires-python = ">=3.9,<3.13" authors = [ { name="CadQuery Developers" } ] From 02a7343c307ee9cd0724afae6a372690cdf358a1 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 4 Feb 2025 17:11:14 -0500 Subject: [PATCH 082/134] Move to release version number --- cq_editor/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cq_editor/_version.py b/cq_editor/_version.py index c7d7df65..493f7415 100644 --- a/cq_editor/_version.py +++ b/cq_editor/_version.py @@ -1 +1 @@ -__version__ = "0.3.0dev" +__version__ = "0.3.0" diff --git a/pyproject.toml b/pyproject.toml index 4be43159..1dee1809 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "CQ-editor" -version = "0.3-dev" +version = "0.3.0" dependencies = [ "cadquery", "pyqtgraph", From 07d2a8188a0e98ec185141d4d5e6c1da0041893b Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 5 Feb 2025 09:09:28 -0500 Subject: [PATCH 083/134] Moved back to development version numbering --- cq_editor/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cq_editor/_version.py b/cq_editor/_version.py index 493f7415..116884f3 100644 --- a/cq_editor/_version.py +++ b/cq_editor/_version.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.4.dev0" diff --git a/pyproject.toml b/pyproject.toml index 1dee1809..aba3a25c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "CQ-editor" -version = "0.3.0" +version = "0.4.dev0" dependencies = [ "cadquery", "pyqtgraph", From 4db7c363bc05154311e545babe876f1f0aa563ec Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 5 Feb 2025 09:38:25 -0500 Subject: [PATCH 084/134] Pinned to latest 5.x version of Spyder to fix segfaults in Python 3.12 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aba3a25c..9c8b9559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.4.dev0" dependencies = [ "cadquery", "pyqtgraph", - "spyder==5", + "spyder>=5.5.6,<6", "path", "logbook", "requests", From 3d8cf2717ffe2fa3bf45ba3e2d4071a6f6436ee7 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 5 Feb 2025 09:45:50 -0500 Subject: [PATCH 085/134] Updating conda dependencies --- conda/meta.yaml | 4 ++-- cqgui_env.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conda/meta.yaml b/conda/meta.yaml index b6a95f53..a1debc5d 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -18,13 +18,13 @@ requirements: - setuptools run: - - python >=3.8 + - python >=3.9 - cadquery=master - ocp - logbook - pyqt=5.* - pyqtgraph - - spyder=5.* + - spyder >=5.5.6,<6 - path - logbook - requests diff --git a/cqgui_env.yml b/cqgui_env.yml index 98cae565..fceca9fb 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -6,7 +6,7 @@ dependencies: - pyqt=5 - pyqtgraph - python=3.10 - - spyder=5 + - spyder >=5.5.6,<6 - path - logbook - requests From 4d58a7c789cfb164b4408f69f0b13efc9b35bd8d Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 5 Feb 2025 09:55:27 -0500 Subject: [PATCH 086/134] Updating qtconsole pin in an attempt to fix conda error --- conda/meta.yaml | 2 +- cqgui_env.yml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conda/meta.yaml b/conda/meta.yaml index a1debc5d..20856f72 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -28,7 +28,7 @@ requirements: - path - logbook - requests - - qtconsole=5.4.1 + - qtconsole >=5.5.1,<5.6.0 test: imports: - cq_editor diff --git a/cqgui_env.yml b/cqgui_env.yml index fceca9fb..dd7351e9 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -11,4 +11,4 @@ dependencies: - logbook - requests - cadquery=master - - qtconsole=5.4.1 + - qtconsole >=5.5.1,<5.6.0 diff --git a/pyproject.toml b/pyproject.toml index 9c8b9559..693a18e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "path", "logbook", "requests", - "qtconsole==5.4.1" + "qtconsole>=5.5.1,<5.6.0" ] requires-python = ">=3.9,<3.13" authors = [ From dd4269df605a6f9687906fb8b6cbe7bf6b59bdb8 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 6 Feb 2025 15:33:10 -0500 Subject: [PATCH 087/134] Revert "Status bar messages to indicate rendering and viewing progress" --- cq_editor/main_window.py | 48 ++++----------------------- cq_editor/widgets/debugger.py | 7 ---- cq_editor/widgets/object_tree.py | 20 +++++------ cq_editor/widgets/traceback_viewer.py | 3 +- cq_editor/widgets/viewer.py | 37 ++++++++++++--------- tests/test_app.py | 42 ++++++++++++----------- 6 files changed, 60 insertions(+), 97 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index a8ee6b70..dbf8b5f0 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,8 +1,6 @@ import sys -from typing import Optional -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtWidgets import (QApplication, QLabel, QMainWindow, QToolBar, QDockWidget, QAction) +from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction) from logbook import Logger import cadquery as cq @@ -67,8 +65,6 @@ def __init__(self,parent=None, filename=None): self.restoreComponentState() - self.on_idle() - def closeEvent(self,event): self.saveWindow() @@ -206,7 +202,7 @@ def prepare_toolbar(self): self.toolbar = QToolBar('Main toolbar',self,objectName='Main toolbar') for c in self.components.values(): - add_actions(self.toolbar, c.toolbarActions()) + add_actions(self.toolbar,c.toolbarActions()) self.addToolBar(self.toolbar) @@ -217,25 +213,18 @@ def prepare_statusbar(self): def prepare_actions(self): - self.components['debugger'].sigRenderStarted \ - .connect(self.on_render_start) self.components['debugger'].sigRendered\ .connect(self.components['object_tree'].addObjects) self.components['debugger'].sigTraceback\ .connect(self.components['traceback_viewer'].addTraceback) - self.components['debugger'].sigRendered \ - .connect(lambda _: self.on_idle()) - self.components['debugger'].sigTraceback \ - .connect(lambda _: self.on_idle()) - self.components['debugger'].sigLocals\ .connect(self.components['variables_viewer'].update_frame) self.components['debugger'].sigLocals\ .connect(self.components['console'].push_vars) - self.components['object_tree'].sigObjectsAdded[list, list]\ - .connect(lambda objects, names: self.components['viewer'].display_many(objects, None, names)) - self.components['object_tree'].sigObjectsAdded[list, bool, list]\ + self.components['object_tree'].sigObjectsAdded[list]\ + .connect(self.components['viewer'].display_many) + self.components['object_tree'].sigObjectsAdded[list,bool]\ .connect(self.components['viewer'].display_many) self.components['object_tree'].sigItemChanged.\ connect(self.components['viewer'].update_item) @@ -250,8 +239,6 @@ def prepare_actions(self): self.components['viewer'].sigObjectSelected\ .connect(self.components['object_tree'].handleGraphicalSelection) - self.components['viewer'].sigDisplayProgress \ - .connect(self.on_display_progress) self.components['traceback_viewer'].sigHighlightLine\ .connect(self.components['editor'].go_to_line) @@ -357,27 +344,6 @@ def handle_filename_change(self, fname): new_title = fname if fname else "*" self.setWindowTitle(f"{self.name}: {new_title}") - def on_idle(self): - self.components['debugger'].set_rendering_state(False) - self.set_status_message('Idle', '#000000') +if __name__ == "__main__": - @pyqtSlot() - def on_render_start(self): - self.components['debugger'].set_rendering_state(True) - self.set_status_message('Rendering...', '#ff0000') - - @pyqtSlot(int, int, str) - def on_display_progress(self, current: int, total: int, name: Optional[str]): - if current == total: - self.on_idle() - else: - message = f'Displaying Shape {current + 1} / {total}' - if name: - message += f' ({name})' - self.set_status_message(message, '#0000ff') - - def set_status_message(self, message: str, color: str): - self.statusBar().showMessage(message) - self.statusBar().setStyleSheet(f'color: {color}') - # required because rendering is currently done on the main thread - QApplication.processEvents() + pass diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index ddef98ad..70d5795f 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -111,7 +111,6 @@ class Debugger(QObject,ComponentMixin): ]) - sigRenderStarted = pyqtSignal() sigRendered = pyqtSignal(dict) sigLocals = pyqtSignal(dict) sigTraceback = pyqtSignal(object,str) @@ -264,7 +263,6 @@ def _cleanup_locals(self,module,injected_names): @pyqtSlot(bool) def render(self): - self.sigRenderStarted.emit() seed(59798267586177) if self.preferences['Reload CQ']: @@ -296,11 +294,6 @@ def render(self): sys.last_traceback = exc_info[-1] self.sigTraceback.emit(exc_info, cq_script) - def set_rendering_state(self, rendering): - render_action = self._actions['Run'][0] - render_action.setCheckable(rendering) - render_action.setChecked(rendering) - @property def breakpoints(self): return [ el[0] for el in self.get_breakpoints()] diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index de2ffb2b..1778bfd5 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -97,7 +97,7 @@ class ObjectTree(QWidget,ComponentMixin): {'name': 'Clear all before each run', 'type': 'bool', 'value': True}, {'name': 'STL precision','type': 'float', 'value': .1}]) - sigObjectsAdded = pyqtSignal([list, list],[list, bool, list]) + sigObjectsAdded = pyqtSignal([list],[list,bool]) sigObjectsRemoved = pyqtSignal(list) sigCQObjectSelected = pyqtSignal(object) sigAISObjectsSelected = pyqtSignal(list) @@ -201,7 +201,6 @@ def addLines(self): origin = (0,0,0) ais_list = [] - names = [] for name,color,direction in zip(('X','Y','Z'), ('red','lawngreen','blue'), @@ -215,9 +214,8 @@ def addLines(self): ais=line)) ais_list.append(line) - names.append(name) - self.sigObjectsAdded.emit(ais_list, names) + self.sigObjectsAdded.emit(ais_list) def _current_properties(self): @@ -250,7 +248,6 @@ def addObjects(self,objects,clean=False,root=None): self.removeObjects() ais_list = [] - names = [] #remove empty objects objects_f = {k:v for k,v in objects.items() if not is_obj_empty(v.shape)} @@ -269,14 +266,13 @@ def addObjects(self,objects,clean=False,root=None): if child.properties['Visible']: ais_list.append(ais) - names.append(name) - + root.addChild(child) if request_fit_view: - self.sigObjectsAdded[list, bool, list].emit(ais_list, True, names) + self.sigObjectsAdded[list,bool].emit(ais_list,True) else: - self.sigObjectsAdded[list, list].emit(ais_list, names) + self.sigObjectsAdded[list].emit(ais_list) @pyqtSlot(object,str,object) def addObject(self,obj,name='',options=None): @@ -285,7 +281,7 @@ def addObject(self,obj,name='',options=None): root = self.CQ - ais, shape_display = make_AIS(obj, options) + ais,shape_display = make_AIS(obj, options) root.addChild(ObjectTreeItem(name, shape=obj, @@ -293,7 +289,7 @@ def addObject(self,obj,name='',options=None): ais=ais, sig=self.sigObjectPropertiesChanged)) - self.sigObjectsAdded.emit([ais], [name]) + self.sigObjectsAdded.emit([ais]) @pyqtSlot(list) @pyqtSlot() @@ -317,7 +313,7 @@ def stashObjects(self,action : bool): self.removeObjects() self.CQ.addChildren(self._stash) ais_list = [el.ais for el in self._stash] - self.sigObjectsAdded.emit(ais_list, [''] * len(ais_list)) + self.sigObjectsAdded.emit(ais_list) @pyqtSlot() def removeSelected(self): diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index aa7cd9a8..7d1051a0 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -36,7 +36,8 @@ def __init__(self,parent): self.tree = TracebackTree(self) self.current_exception = QLabel(self) - self.current_exception.setStyleSheet("QLabel {color : red; }"); + self.current_exception.setStyleSheet(\ + "QLabel {color : red; }"); layout(self, (self.current_exception, diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 498f8438..9c5d620b 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -1,6 +1,5 @@ from PyQt5.QtWidgets import QWidget, QDialog, QTreeWidgetItem, QApplication, QAction -from typing import Optional, List from PyQt5.QtCore import pyqtSlot, pyqtSignal from PyQt5.QtGui import QIcon @@ -48,7 +47,6 @@ class OCCViewer(QWidget,ComponentMixin): IMAGE_EXTENSIONS = 'png' sigObjectSelected = pyqtSignal(list) - sigDisplayProgress = pyqtSignal(int, int, str) def __init__(self,parent=None): @@ -181,23 +179,31 @@ def clear(self): context.PurgeDisplay() context.RemoveAll(True) + def _display(self,shape): + + ais = make_AIS(shape) + self.canvas.context.Display(shape,True) + + self.displayed_shapes.append(shape) + self.displayed_ais.append(ais) + + #self.canvas._display.Repaint() + @pyqtSlot(object) - def display(self, ais): - self.display_many([ais]) + def display(self,ais): + + context = self._get_context() + context.Display(ais,True) + + if self.preferences['Fit automatically']: self.fit() @pyqtSlot(list) - @pyqtSlot(list, bool, list) - def display_many(self, ais_list, fit: Optional[bool] = None, names: Optional[List] = None): - if names is None: - names = [None] * len(ais_list) - assert len(ais_list) == len(names) + @pyqtSlot(list,bool) + def display_many(self,ais_list,fit=None): context = self._get_context() - num_objects = len(ais_list) - for i, (ais, name) in enumerate(zip(ais_list, names)): - self.sigDisplayProgress.emit(i, num_objects, name) - context.Display(ais, True) - self.sigDisplayProgress.emit(num_objects, num_objects, None) + for ais in ais_list: + context.Display(ais,True) if self.preferences['Fit automatically'] and fit is None: self.fit() @@ -217,8 +223,7 @@ def update_item(self,item,col): def remove_items(self,ais_items): ctx = self._get_context() - for ais in ais_items: - ctx.Erase(ais,True) + for ais in ais_items: ctx.Erase(ais,True) @pyqtSlot() def redraw(self): diff --git a/tests/test_app.py b/tests/test_app.py index a5ad8490..2ec4baa7 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,9 +1,6 @@ from path import Path import os, sys, asyncio -from pytestqt.qtbot import QtBot -import pytestqt.exceptions - if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -94,8 +91,6 @@ sk = cq.Sketch().rect(1,1) """ -TIMEOUT = 10_000 - def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) @@ -129,7 +124,7 @@ def get_rgba(ais): return color.redF(), color.greenF(), color.blueF(), alpha @pytest.fixture -def main(qtbot: QtBot, mocker): +def main(qtbot,mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) @@ -147,14 +142,15 @@ def main(qtbot: QtBot, mocker): return qtbot, win @pytest.fixture -def main_clean(qtbot: QtBot, mocker): +def main_clean(qtbot,mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) win = MainWindow() + win.show() + qtbot.addWidget(win) - with qtbot.waitExposed(win, timeout=TIMEOUT): - win.show() + qtbot.waitForWindowShown(win) editor = win.components['editor'] editor.set_text(code) @@ -162,14 +158,15 @@ def main_clean(qtbot: QtBot, mocker): return qtbot, win @pytest.fixture -def main_clean_do_not_close(qtbot: QtBot, mocker): +def main_clean_do_not_close(qtbot,mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.No) win = MainWindow() + win.show() + qtbot.addWidget(win) - with qtbot.waitExposed(win, timeout=TIMEOUT): - win.show() + qtbot.waitForWindowShown(win) editor = win.components['editor'] editor.set_text(code) @@ -177,15 +174,16 @@ def main_clean_do_not_close(qtbot: QtBot, mocker): return qtbot, win @pytest.fixture -def main_multi(qtbot: QtBot, mocker): +def main_multi(qtbot,mocker): mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) win = MainWindow() + win.show() + qtbot.addWidget(win) - with qtbot.waitExposed(win, timeout=TIMEOUT): - win.show() + qtbot.waitForWindowShown(win) editor = win.components['editor'] editor.set_text(code_multi) @@ -573,7 +571,7 @@ def test_traceback(main): assert(traceback_view.tree.root.childCount() == 3) # 1 in user code + 2 in CQ code @pytest.fixture -def editor(qtbot: QtBot): +def editor(qtbot): win = Editor() win.show() @@ -665,6 +663,8 @@ def test_editor_autoreload(monkeypatch,editor): qtbot, editor = editor + TIMEOUT = 500 + # start out with autoreload enabled editor.autoreload(True) @@ -713,6 +713,7 @@ def test_autoreload_nested(editor): qtbot, editor = editor + TIMEOUT = 500 editor.autoreload(True) editor.preferences['Autoreload: watch imported modules'] = True @@ -746,10 +747,9 @@ def test_console(main): assert(len(a) == 1) # test print_text - text_before = console._control.document().toPlainText() - console.print_text('foo') - text_after = console._control.document().toPlainText() - assert text_after == text_before + 'foo' + pos_orig = console._prompt_pos + console.print_text('a') + assert(console._prompt_pos == pos_orig + len('a')) def test_viewer(main): @@ -1440,6 +1440,7 @@ def makebox(z): def test_reload_import_handle_error(tmp_path, main): + TIMEOUT = 500 qtbot, win = main editor = win.components["editor"] debugger = win.components["debugger"] @@ -1480,6 +1481,7 @@ def test_reload_import_handle_error(tmp_path, main): def test_modulefinder(tmp_path, main): + TIMEOUT = 500 qtbot, win = main editor = win.components["editor"] debugger = win.components["debugger"] From a9dbd6117c5dc3f0351153aa65ad37ddbfaa300f Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Fri, 7 Feb 2025 10:48:07 -0500 Subject: [PATCH 088/134] Change to always forcing UTF-8 when saving --- cq_editor/widgets/editor.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 20bee81f..d78e72a2 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -159,15 +159,6 @@ def load_from_file(self,fname): self.filename = fname self.reset_modified() - def determine_encoding(self, fname): - if os.path.exists(fname): - # this function returns the encoding spyder used to read the file - _, encoding = spyder.utils.encoding.read(fname) - # spyder returns a -guessed suffix in some cases - return encoding.replace('-guessed', '') - else: - return 'utf-8' - def save(self): if self._filename != '': @@ -176,10 +167,8 @@ def save(self): self._file_watcher.blockSignals(True) self._file_watch_timer.stop() - encoding = self.determine_encoding(self._filename) - encoded = self.toPlainText().encode(encoding) - with open(self._filename, 'wb') as f: - f.write(encoded) + with open(self._filename, 'w', encoding='utf-8') as f: + f.write(self.toPlainText()) if self.preferences['Autoreload']: self._file_watcher.blockSignals(False) @@ -194,9 +183,8 @@ def save_as(self): fname = get_save_filename(self.EXTENSIONS) if fname != '': - encoded = self.toPlainText().encode('utf-8') - with open(fname, 'wb') as f: - f.write(encoded) + with open(fname, 'w', encoding='utf-8') as f: + f.write(self.toPlainText()) self.filename = fname self.reset_modified() From 3923be410f0e77155c88de14de54fda111068905 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sat, 8 Feb 2025 11:35:21 -0500 Subject: [PATCH 089/134] Trying to fix double-saves and also setting Ctrl+/ as an alternate comment toggle --- cq_editor/main_window.py | 6 ++++++ cq_editor/widgets/editor.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index dbf8b5f0..7529e114 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -169,6 +169,12 @@ def prepare_menubar(self): for t in self.findChildren(QToolBar): menu_view.addAction(t.toggleViewAction()) + menu_edit.addAction( \ + QAction(icon('preferences'), + 'Toggle Comment', + self, + shortcut='ctrl+/', + triggered=self.components['editor'].toggle_comment)) menu_edit.addAction( \ QAction(icon('preferences'), 'Preferences', diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index d78e72a2..783fd70e 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -171,8 +171,8 @@ def save(self): f.write(self.toPlainText()) if self.preferences['Autoreload']: - self._file_watcher.blockSignals(False) self.triggerRerender.emit(True) + # self._file_watcher.blockSignals(False) self.reset_modified() From d0e3646985818b8170e90b8212a0ac92e4cd1e36 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sat, 8 Feb 2025 19:40:37 -0500 Subject: [PATCH 090/134] Updated the toggle comment icon --- cq_editor/icons.py | 3 ++- cq_editor/main_window.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cq_editor/icons.py b/cq_editor/icons.py index 6d568baa..572e4a01 100644 --- a/cq_editor/icons.py +++ b/cq_editor/icons.py @@ -46,7 +46,8 @@ {'options' : \ [{'scale_factor': 0.8}, {'scale_factor': 0.8, - 'offset': (.2,.2)}]}) + 'offset': (.2,.2)}]}), + 'toggle-comment' : (('fa.hashtag',),{}), } def icon(name): diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 7529e114..a726f465 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -170,7 +170,7 @@ def prepare_menubar(self): menu_view.addAction(t.toggleViewAction()) menu_edit.addAction( \ - QAction(icon('preferences'), + QAction(icon('toggle-comment'), 'Toggle Comment', self, shortcut='ctrl+/', From 8469415c850415fbfdd7cd0bf31eb3dd6c1416a3 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sat, 8 Feb 2025 21:30:24 -0500 Subject: [PATCH 091/134] Added stdout redirection so that print statements will appear in the log viewer --- cq_editor/main_window.py | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index a726f465..b414ce9a 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,5 +1,6 @@ import sys +from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction) from logbook import Logger import cadquery as cq @@ -137,6 +138,12 @@ def prepare_panes(self): for d in self.docks.values(): d.show() + # Handle the stdout redirection + self.output_redirector = OutputRedirector() + sys.stdout = self.output_redirector + self.output_redirector.printOccurred.connect(self.components['log'].appendPlainText) + + def prepare_menubar(self): menu = self.menuBar() @@ -350,6 +357,41 @@ def handle_filename_change(self, fname): new_title = fname if fname else "*" self.setWindowTitle(f"{self.name}: {new_title}") + +class OutputRedirector(QObject): + """ + Mimics stdout so that we can redirect print statement output to the log viewer. + """ + + printOccurred = pyqtSignal(str) + + def __init__(self): + super().__init__() + self.buffer = "" + + def write(self, text): + """ + Append text to the buffer and emit the signal if a newline is detected. + """ + self.buffer += text + + # This buffer methods eliminates extra newlines that are injected due to this redirect + if '\n' in self.buffer: + lines = self.buffer.splitlines(True) + for line in lines: + if line.endswith('\n'): + self.printOccurred.emit(line.rstrip('\n')) + self.buffer = "" + + def flush(self): + """ + Emit the signal if there is anything in the buffer. + """ + if self.buffer: + self.printOccurred.emit(self.buffer) + self.buffer = "" + + if __name__ == "__main__": pass From 55be9644e1a3e39e526fe7463fd98ad440c9e77f Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sun, 9 Feb 2025 21:05:24 -0500 Subject: [PATCH 092/134] Make window title show document modifications and make comments trigger modification --- cq_editor/main_window.py | 11 +++++++++++ cq_editor/widgets/editor.py | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index b414ce9a..2d95507e 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -61,6 +61,9 @@ def __init__(self,parent=None, filename=None): self.restorePreferences() self.restoreWindow() + # Let the user know when the file has been modified + self.components['editor'].document().modificationChanged.connect(self.update_window_title) + if filename: self.components['editor'].load_from_file(filename) @@ -357,6 +360,14 @@ def handle_filename_change(self, fname): new_title = fname if fname else "*" self.setWindowTitle(f"{self.name}: {new_title}") + def update_window_title(self, modified): + """ + Allows updating the window title to show that the document has been modified. + """ + title = self.windowTitle().rstrip('*') + if modified: + title += '*' + self.setWindowTitle(title) class OutputRedirector(QObject): """ diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 783fd70e..36af896a 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -189,6 +189,13 @@ def save_as(self): self.reset_modified() + def toggle_comment(self): + """ + Allows us to mark the document as modified when the user toggles a comment. + """ + super(Editor,self).toggle_comment() + self.document().setModified(True) + def _update_filewatcher(self): if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']): self._clear_watched_paths() From 8dea36518433bdfde9b528c1c2a95cd9fa807151 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 11 Feb 2025 14:09:24 -0500 Subject: [PATCH 093/134] Fixed the case where a long exception would expand the window to the right, sometimes off the screen --- cq_editor/widgets/editor.py | 2 +- cq_editor/widgets/traceback_viewer.py | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 36af896a..e2f73f45 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -172,7 +172,7 @@ def save(self): if self.preferences['Autoreload']: self.triggerRerender.emit(True) - # self._file_watcher.blockSignals(False) + self._file_watcher.blockSignals(False) self.reset_modified() diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index 7d1051a0..244d9acc 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -4,6 +4,7 @@ from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction, QLabel) from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal +from PyQt5.QtGui import QFontMetrics from ..mixins import ComponentMixin from ..utils import layout @@ -37,7 +38,7 @@ def __init__(self,parent): self.tree = TracebackTree(self) self.current_exception = QLabel(self) self.current_exception.setStyleSheet(\ - "QLabel {color : red; }"); + "QLabel {color : red; }") layout(self, (self.current_exception, @@ -45,7 +46,16 @@ def __init__(self,parent): self) self.tree.currentItemChanged.connect(self.handleSelection) - + + def truncate_text(self, text, max_length=100): + """ + Used to prevent the label from expanding the window width off the screen. + """ + metrics = QFontMetrics(self.current_exception.font()) + elided_text = metrics.elidedText(text, Qt.ElideRight, self.current_exception.width() - 75) + + return elided_text + @pyqtSlot(object,str) def addTraceback(self,exc_info,code): @@ -74,8 +84,9 @@ def addTraceback(self,exc_info,code): exc_msg = str(exc) exc_msg = exc_msg.replace('<', '<').replace('>', '>') #replace <> - self.current_exception.\ - setText('{}: {}'.format(exc_name,exc_msg)) + truncated_msg = self.truncate_text(exc_msg) + self.current_exception.setText('{}: {}'.format(exc_name,truncated_msg)) + self.current_exception.setToolTip(exc_msg) # handle the special case of a SyntaxError if t is SyntaxError: @@ -86,6 +97,7 @@ def addTraceback(self,exc_info,code): )) else: self.current_exception.setText('') + self.current_exception.setToolTip('') @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem) def handleSelection(self,item,*args): From e3c89b94d36656cc46436d7ce323c1b597d0b893 Mon Sep 17 00:00:00 2001 From: snoyer Date: Wed, 12 Feb 2025 09:01:39 +0400 Subject: [PATCH 094/134] refactor output redirector --- cq_editor/main_window.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 2d95507e..05419bb2 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,5 +1,6 @@ import sys +from PyQt5 import QtGui from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction) from logbook import Logger @@ -143,8 +144,20 @@ def prepare_panes(self): # Handle the stdout redirection self.output_redirector = OutputRedirector() + stdout_buffer = sys.stdout.buffer sys.stdout = self.output_redirector - self.output_redirector.printOccurred.connect(self.components['log'].appendPlainText) + + def append_to_log_viewer(text): + log_viewer = self.components['log'] + log_viewer.moveCursor(QtGui.QTextCursor.End) + log_viewer.insertPlainText(text) + + def write_to_stdout_buffer(text): + stdout_buffer.write(text.encode()) + stdout_buffer.flush() + + self.output_redirector.writeOccurred.connect(append_to_log_viewer) + self.output_redirector.writeOccurred.connect(write_to_stdout_buffer) def prepare_menubar(self): @@ -374,33 +387,16 @@ class OutputRedirector(QObject): Mimics stdout so that we can redirect print statement output to the log viewer. """ - printOccurred = pyqtSignal(str) + writeOccurred = pyqtSignal(str) def __init__(self): super().__init__() - self.buffer = "" def write(self, text): - """ - Append text to the buffer and emit the signal if a newline is detected. - """ - self.buffer += text - - # This buffer methods eliminates extra newlines that are injected due to this redirect - if '\n' in self.buffer: - lines = self.buffer.splitlines(True) - for line in lines: - if line.endswith('\n'): - self.printOccurred.emit(line.rstrip('\n')) - self.buffer = "" + self.writeOccurred.emit(text) def flush(self): - """ - Emit the signal if there is anything in the buffer. - """ - if self.buffer: - self.printOccurred.emit(self.buffer) - self.buffer = "" + pass if __name__ == "__main__": From 17e9ac618b7f052560ff4af7b4b2b5894d190831 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 12 Feb 2025 10:08:58 -0500 Subject: [PATCH 095/134] Fixed PyQtGraph drop downs not populating --- cq_editor/preferences.py | 22 ++++++++++++++++++++++ cq_editor/widgets/editor.py | 2 +- cq_editor/widgets/viewer.py | 4 +--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/cq_editor/preferences.py b/cq_editor/preferences.py index 5dc947ca..f312caae 100644 --- a/cq_editor/preferences.py +++ b/cq_editor/preferences.py @@ -53,6 +53,28 @@ def add(self,name,component): widget)) self.stacked.addWidget(widget) + + # PyQtGraph is not setting items in drop down lists properly, so we do it manually + for child in component.preferences.children(): + # Fill the editor color scheme drop down list + if child.name() == 'Color scheme': + child.setLimits(['Spyder','Monokai','Zenburn']) + # Fill the camera projection type + elif child.name() == 'Projection Type': + child.setLimits(['Orthographic', + 'Perspective', + 'Stereo', + 'MonoLeftEye', + 'MonoRightEye']) + # Fill the stereo mode, or lack thereof + elif child.name() == 'Stereo Mode': + child.setLimits(['QuadBuffer', + 'Anaglyph', + 'RowInterlaced', + 'ColumnInterlaced', + 'ChessBoard', + 'SideBySide', + 'OverUnder']) @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem) def handleSelection(self,item,*args): diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index e2f73f45..71fa3d88 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -26,7 +26,7 @@ class Editor(CodeEditor,ComponentMixin): triggerRerender = pyqtSignal(bool) sigFilenameChanged = pyqtSignal(str) - preferences = Parameter.create(name='Preferences',children=[ + preferences = Parameter.create(name='Preferences', children=[ {'name': 'Font size', 'type': 'int', 'value': 12}, {'name': 'Autoreload', 'type': 'bool', 'value': False}, {'name': 'Autoreload delay', 'type': 'int', 'value': 50}, diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 9c5d620b..2f54eaca 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -31,12 +31,11 @@ class OCCViewer(QWidget,ComponentMixin): name = '3D Viewer' - preferences = Parameter.create(name='Pref',children=[ + preferences = Parameter.create(name='Pref', children=[ {'name': 'Fit automatically', 'type': 'bool', 'value': True}, {'name': 'Use gradient', 'type': 'bool', 'value': False}, {'name': 'Background color', 'type': 'color', 'value': (95,95,95)}, {'name': 'Background color (aux)', 'type': 'color', 'value': (30,30,30)}, - {'name': 'Default object color', 'type': 'color', 'value': "#FF0"}, {'name': 'Deviation', 'type': 'float', 'value': 1e-5, 'dec': True, 'step': 1}, {'name': 'Angular deviation', 'type': 'float', 'value': 0.1, 'dec': True, 'step': 1}, {'name': 'Projection Type', 'type': 'list', 'value': 'Orthographic', @@ -200,7 +199,6 @@ def display(self,ais): @pyqtSlot(list) @pyqtSlot(list,bool) def display_many(self,ais_list,fit=None): - context = self._get_context() for ais in ais_list: context.Display(ais,True) From c42625f05f0a85289793a4ba0de6d7dbeba90aa8 Mon Sep 17 00:00:00 2001 From: snoyer Date: Wed, 12 Feb 2025 21:16:18 +0400 Subject: [PATCH 096/134] simplify output redirection --- cq_editor/main_window.py | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 05419bb2..b8169aad 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -143,21 +143,16 @@ def prepare_panes(self): d.show() # Handle the stdout redirection - self.output_redirector = OutputRedirector() - stdout_buffer = sys.stdout.buffer - sys.stdout = self.output_redirector + original_stdout_write = sys.stdout.write + + def new_stdout_write(text): + original_stdout_write(text) - def append_to_log_viewer(text): log_viewer = self.components['log'] log_viewer.moveCursor(QtGui.QTextCursor.End) log_viewer.insertPlainText(text) - def write_to_stdout_buffer(text): - stdout_buffer.write(text.encode()) - stdout_buffer.flush() - - self.output_redirector.writeOccurred.connect(append_to_log_viewer) - self.output_redirector.writeOccurred.connect(write_to_stdout_buffer) + sys.stdout.write = new_stdout_write def prepare_menubar(self): @@ -382,22 +377,6 @@ def update_window_title(self, modified): title += '*' self.setWindowTitle(title) -class OutputRedirector(QObject): - """ - Mimics stdout so that we can redirect print statement output to the log viewer. - """ - - writeOccurred = pyqtSignal(str) - - def __init__(self): - super().__init__() - - def write(self, text): - self.writeOccurred.emit(text) - - def flush(self): - pass - if __name__ == "__main__": From a024de2a5254c5ead7db89a6832409e17f21c8be Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 12 Feb 2025 17:34:32 -0500 Subject: [PATCH 097/134] Fixed double renders when saving in the Spyder window --- cq_editor/widgets/editor.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 71fa3d88..0e6b2c98 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -160,20 +160,16 @@ def load_from_file(self,fname): self.reset_modified() def save(self): + """ + Saves the current document to the current filename if it exists, otherwise it triggers a + save-as dialog. + """ if self._filename != '': - - if self.preferences['Autoreload']: - self._file_watcher.blockSignals(True) - self._file_watch_timer.stop() - with open(self._filename, 'w', encoding='utf-8') as f: f.write(self.toPlainText()) - if self.preferences['Autoreload']: - self.triggerRerender.emit(True) - self._file_watcher.blockSignals(False) - + # Let the editor and the rest of the app know that the file is no longer dirty self.reset_modified() else: From 0ca845a75067f23bb1f667d4052ae6bbc5fdbb07 Mon Sep 17 00:00:00 2001 From: snoyer Date: Thu, 13 Feb 2025 07:48:14 +0400 Subject: [PATCH 098/134] add output redirection test --- tests/test_app.py | 1190 ++++++++++++++++++++++++--------------------- 1 file changed, 636 insertions(+), 554 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 2ec4baa7..e20e4e2d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,7 +1,7 @@ from path import Path import os, sys, asyncio -if sys.platform == 'win32': +if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) from multiprocessing import Process @@ -17,61 +17,54 @@ from cq_editor.widgets.editor import Editor from cq_editor.cq_utils import export, get_occ_color -code = \ -'''import cadquery as cq +code = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) -result = result.edges("|Z").fillet(0.125)''' +result = result.edges("|Z").fillet(0.125)""" -code_bigger_object = \ -'''import cadquery as cq +code_bigger_object = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(20, 20, 0.5) result = result.edges("|Z").fillet(0.125) -''' +""" -code_show_Workplane = \ -'''import cadquery as cq +code_show_Workplane = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) show_object(result) -''' +""" -code_show_Workplane_named = \ -'''import cadquery as cq +code_show_Workplane_named = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) log('test') show_object(result,name='test') -''' +""" -code_show_Shape = \ -'''import cadquery as cq +code_show_Shape = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) show_object(result.val()) -''' +""" -code_debug_Workplane = \ -'''import cadquery as cq +code_debug_Workplane = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) debug(result) -''' +""" -code_multi = \ -'''import cadquery as cq +code_multi = """import cadquery as cq result1 = cq.Workplane("XY" ).box(3, 3, 0.5) result2 = cq.Workplane("XY" ).box(3, 3, 0.5).translate((0,15,0)) -''' +""" code_nested_top = """import test_nested_bottom """ @@ -91,31 +84,35 @@ sk = cq.Sketch().rect(1,1) """ + def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) def modify_file(code, path="test.py"): - p = Process(target=_modify_file, args=(code,path)) + p = Process(target=_modify_file, args=(code, path)) p.start() p.join() + def get_center(widget): pos = widget.pos() - pos.setX(pos.x()+widget.width()//2) - pos.setY(pos.y()+widget.height()//2) + pos.setX(pos.x() + widget.width() // 2) + pos.setY(pos.y() + widget.height() // 2) return pos + def get_bottom_left(widget): pos = widget.pos() - pos.setY(pos.y()+widget.height()) + pos.setY(pos.y() + widget.height()) return pos + def get_rgba(ais): alpha = ais.Transparency() @@ -123,28 +120,30 @@ def get_rgba(ais): return color.redF(), color.greenF(), color.blueF(), alpha + @pytest.fixture -def main(qtbot,mocker): +def main(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) win = MainWindow() win.show() qtbot.addWidget(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code) - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() return qtbot, win + @pytest.fixture -def main_clean(qtbot,mocker): +def main_clean(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) win = MainWindow() win.show() @@ -152,15 +151,16 @@ def main_clean(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code) return qtbot, win + @pytest.fixture -def main_clean_do_not_close(qtbot,mocker): +def main_clean_do_not_close(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.No) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.No) win = MainWindow() win.show() @@ -168,16 +168,17 @@ def main_clean_do_not_close(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code) return qtbot, win + @pytest.fixture -def main_multi(qtbot,mocker): +def main_multi(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.step", "")) win = MainWindow() win.show() @@ -185,116 +186,120 @@ def main_multi(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code_multi) - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() return qtbot, win + def test_render(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] - log = win.components['log'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] + log = win.components["log"] # enable CQ reloading - debugger.preferences['Reload CQ'] = True + debugger.preferences["Reload CQ"] = True # check that object was rendered - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_Workplane) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that cq.Shape object was rendered using explicit show_object call editor.set_text(code_show_Shape) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # test rendering via console console.execute(code_show_Workplane) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 console.execute(code_show_Shape) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # check object rendering using show_object call with a name specified and # debug call editor.set_text(code_show_Workplane_named) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.child(0).text(0) == 'test') - assert('test' in log.toPlainText().splitlines()[-1]) + assert obj_tree_comp.CQ.child(0).text(0) == "test" + assert "test" in log.toPlainText().splitlines()[-1] # cq reloading check obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 editor.set_text(code_reload_issue) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.childCount() == 3) + assert obj_tree_comp.CQ.childCount() == 3 - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.childCount() == 3) + assert obj_tree_comp.CQ.childCount() == 3 -def test_export(main,mocker): + +def test_export(main, mocker): qtbot, win = main - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() - #set focus - obj_tree = win.components['object_tree'].tree - obj_tree_comp = win.components['object_tree'] + # set focus + obj_tree = win.components["object_tree"].tree + obj_tree_comp = win.components["object_tree"] qtbot.mouseClick(obj_tree, Qt.LeftButton) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Down) - #export STL - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.stl','')) + # export STL + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.stl", "")) obj_tree_comp._export_STL_action.triggered.emit() - assert(os.path.isfile('out.stl')) + assert os.path.isfile("out.stl") - #export STEP - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) + # export STEP + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.step", "")) obj_tree_comp._export_STEP_action.triggered.emit() - assert(os.path.isfile('out.step')) + assert os.path.isfile("out.step") + + # clean + os.remove("out.step") + os.remove("out.stl") - #clean - os.remove('out.step') - os.remove('out.stl') def number_visible_items(viewer): from OCP.AIS import AIS_ListOfInteractive + l = AIS_ListOfInteractive() viewer_ctx = viewer._get_context() @@ -302,186 +307,223 @@ def number_visible_items(viewer): return l.Extent() + def test_inspect(main): qtbot, win = main - #set focus and make invisible - obj_tree = win.components['object_tree'].tree + # set focus and make invisible + obj_tree = win.components["object_tree"].tree qtbot.mouseClick(obj_tree, Qt.LeftButton) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Space) - #enable object inspector - insp = win.components['cq_object_inspector'] + # enable object inspector + insp = win.components["cq_object_inspector"] insp._toolbar_actions[0].toggled.emit(True) - #check if all stack items are visible in the tree - assert(insp.root.childCount() == 3) + # check if all stack items are visible in the tree + assert insp.root.childCount() == 3 - #check if correct number of items is displayed - viewer = win.components['viewer'] + # check if correct number of items is displayed + viewer = win.components["viewer"] insp.setCurrentItem(insp.root.child(0)) - assert(number_visible_items(viewer) == 4) + assert number_visible_items(viewer) == 4 insp.setCurrentItem(insp.root.child(1)) - assert(number_visible_items(viewer) == 7) + assert number_visible_items(viewer) == 7 insp.setCurrentItem(insp.root.child(2)) - assert(number_visible_items(viewer) == 4) + assert number_visible_items(viewer) == 4 insp._toolbar_actions[0].toggled.emit(False) - assert(number_visible_items(viewer) == 3) + assert number_visible_items(viewer) == 3 + class event_loop(object): - '''Used to mock the QEventLoop for the debugger component - ''' + """Used to mock the QEventLoop for the debugger component""" - def __init__(self,callbacks): + def __init__(self, callbacks): self.callbacks = callbacks self.i = 0 def exec_(self): - if self.i 0) - assert(conv_line_ends(editor.get_text_with_eol()) == code) + # check that loading from file works properly + editor.load_from_file("test.py") + assert len(editor.get_text_with_eol()) > 0 + assert conv_line_ends(editor.get_text_with_eol()) == code - #check that loading from file works properly + # check that loading from file works properly editor.new() - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" - #monkeypatch QFileDialog methods + # monkeypatch QFileDialog methods def filename(*args, **kwargs): - return 'test.py',None + return "test.py", None def filename2(*args, **kwargs): - return 'test2.py',None + return "test2.py", None - monkeypatch.setattr(QFileDialog, 'getOpenFileName', - staticmethod(filename)) + monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename)) - monkeypatch.setattr(QFileDialog, 'getSaveFileName', - staticmethod(filename2)) + monkeypatch.setattr(QFileDialog, "getSaveFileName", staticmethod(filename2)) - #check that open file works properly + # check that open file works properly editor.open() - assert(conv_line_ends(editor.get_text_with_eol()) == code) + assert conv_line_ends(editor.get_text_with_eol()) == code - #check that save file works properly + # check that save file works properly editor.new() qtbot.mouseClick(editor, Qt.LeftButton) - qtbot.keyClick(editor,Qt.Key_A) + qtbot.keyClick(editor, Qt.Key_A) - assert(editor.document().isModified() == True) + assert editor.document().isModified() == True - editor.filename = 'test2.py' + editor.filename = "test2.py" editor.save() - assert(editor.document().isModified() == False) + assert editor.document().isModified() == False - monkeypatch.setattr(QFileDialog, 'getOpenFileName', - staticmethod(filename2)) + monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename2)) editor.open() - assert(editor.get_text_with_eol() == 'a') + assert editor.get_text_with_eol() == "a" - #check that save as works properly - os.remove('test2.py') + # check that save as works properly + os.remove("test2.py") editor.save_as() - assert(os.path.exists(filename2()[0])) + assert os.path.exists(filename2()[0]) - #test persistance - settings = QSettings('test') + # test persistance + settings = QSettings("test") editor.saveComponentState(settings) editor.new() - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" editor.restoreComponentState(settings) - assert(editor.get_text_with_eol() == 'a') + assert editor.get_text_with_eol() == "a" - #test error handling - os.remove('test2.py') - assert(not os.path.exists('test2.py')) + # test error handling + os.remove("test2.py") + assert not os.path.exists("test2.py") editor.restoreComponentState(settings) + @pytest.mark.repeat(1) -def test_editor_autoreload(monkeypatch,editor): +def test_editor_autoreload(monkeypatch, editor): qtbot, editor = editor @@ -668,13 +709,13 @@ def test_editor_autoreload(monkeypatch,editor): # start out with autoreload enabled editor.autoreload(True) - with open('test.py','w') as f: + with open("test.py", "w") as f: f.write(code) - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" - editor.load_from_file('test.py') - assert(len(editor.get_text_with_eol()) > 0) + editor.load_from_file("test.py") + assert len(editor.get_text_with_eol()) > 0 # wait for reload. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): @@ -682,7 +723,7 @@ def test_editor_autoreload(monkeypatch,editor): modify_file(code_bigger_object) # check that editor has updated file contents - assert(code_bigger_object.splitlines()[2] in editor.get_text_with_eol()) + assert code_bigger_object.splitlines()[2] in editor.get_text_with_eol() # disable autoreload editor.autoreload(False) @@ -696,7 +737,7 @@ def test_editor_autoreload(monkeypatch,editor): modify_file(code) # editor should continue showing old contents since autoreload is disabled. - assert(code_bigger_object.splitlines()[2] in editor.get_text_with_eol()) + assert code_bigger_object.splitlines()[2] in editor.get_text_with_eol() # Saving a file with autoreload disabled should not trigger a rerender. with pytest.raises(pytestqt.exceptions.TimeoutError): @@ -709,6 +750,7 @@ def test_editor_autoreload(monkeypatch,editor): with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): editor.save() + def test_autoreload_nested(editor): qtbot, editor = editor @@ -716,156 +758,158 @@ def test_autoreload_nested(editor): TIMEOUT = 500 editor.autoreload(True) - editor.preferences['Autoreload: watch imported modules'] = True + editor.preferences["Autoreload: watch imported modules"] = True - with open('test_nested_top.py','w') as f: + with open("test_nested_top.py", "w") as f: f.write(code_nested_top) - with open('test_nested_bottom.py','w') as f: + with open("test_nested_bottom.py", "w") as f: f.write("") - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" - editor.load_from_file('test_nested_top.py') - assert(len(editor.get_text_with_eol()) > 0) + editor.load_from_file("test_nested_top.py") + assert len(editor.get_text_with_eol()) > 0 # wait for reload. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): # modify file - NB: separate process is needed to avoid Windows quirks - modify_file(code_nested_bottom, 'test_nested_bottom.py') + modify_file(code_nested_bottom, "test_nested_bottom.py") + def test_console(main): qtbot, win = main - console = win.components['console'] + console = win.components["console"] # test execute_command a = [] - console.push_vars({'a' : a}) - console.execute_command('a.append(1)') - assert(len(a) == 1) + console.push_vars({"a": a}) + console.execute_command("a.append(1)") + assert len(a) == 1 # test print_text pos_orig = console._prompt_pos - console.print_text('a') - assert(console._prompt_pos == pos_orig + len('a')) + console.print_text("a") + assert console._prompt_pos == pos_orig + len("a") + def test_viewer(main): qtbot, win = main - viewer = win.components['viewer'] + viewer = win.components["viewer"] + + # not sure how to test this, so only smoke tests + + # trigger all 'View' actions + actions = viewer._actions["View"] + for a in actions: + a.trigger() - #not sure how to test this, so only smoke tests - #trigger all 'View' actions - actions = viewer._actions['View'] - for a in actions: a.trigger() +code_module = """def dummy(): return True""" -code_module = \ -'''def dummy(): return True''' +code_import = """from module import dummy +assert(dummy())""" -code_import = \ -'''from module import dummy -assert(dummy())''' def test_module_import(main): qtbot, win = main - editor = win.components['editor'] - debugger = win.components['debugger'] - traceback_view = win.components['traceback_viewer'] + editor = win.components["editor"] + debugger = win.components["debugger"] + traceback_view = win.components["traceback_viewer"] - #save the dummy module - with open('module.py','w') as f: + # save the dummy module + with open("module.py", "w") as f: f.write(code_module) - #run the code importing this module + # run the code importing this module editor.set_text(code_import) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() + + # verify that no exception was generated + assert traceback_view.current_exception.text() == "" - #verify that no exception was generated - assert(traceback_view.current_exception.text() == '') def test_auto_fit_view(main_clean): - def concat(eye,proj,scale): - return eye+proj+(scale,) + def concat(eye, proj, scale): + return eye + proj + (scale,) - def approx_view_properties(eye,proj,scale): + def approx_view_properties(eye, proj, scale): - return pytest.approx(eye+proj+(scale,)) + return pytest.approx(eye + proj + (scale,)) qtbot, win = main_clean - editor = win.components['editor'] - debugger = win.components['debugger'] - viewer = win.components['viewer'] - object_tree = win.components['object_tree'] + editor = win.components["editor"] + debugger = win.components["debugger"] + viewer = win.components["viewer"] + object_tree = win.components["object_tree"] view = viewer.canvas.view - viewer.preferences['Fit automatically'] = False - eye0,proj0,scale0 = view.Eye(),view.Proj(),view.Scale() + viewer.preferences["Fit automatically"] = False + eye0, proj0, scale0 = view.Eye(), view.Proj(), view.Scale() # check if camera position is adjusted automatically when rendering for the # first time debugger.render() - eye1,proj1,scale1 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye0,proj0,scale0) != \ - approx_view_properties(eye1,proj1,scale1) ) + eye1, proj1, scale1 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye0, proj0, scale0) != approx_view_properties(eye1, proj1, scale1) # check if camera position is not changed fter code change editor.set_text(code_bigger_object) debugger.render() - eye2,proj2,scale2 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye1,proj1,scale1) == \ - approx_view_properties(eye2,proj2,scale2) ) + eye2, proj2, scale2 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye1, proj1, scale1) == approx_view_properties(eye2, proj2, scale2) # check if position is adjusted automatically after erasing all objects object_tree.removeObjects() debugger.render() - eye3,proj3,scale3 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye2,proj2,scale2) != \ - approx_view_properties(eye3,proj3,scale3) ) + eye3, proj3, scale3 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye2, proj2, scale2) != approx_view_properties(eye3, proj3, scale3) # check if position is adjusted automatically if settings are changed - viewer.preferences['Fit automatically'] = True + viewer.preferences["Fit automatically"] = True editor.set_text(code) debugger.render() - eye4,proj4,scale4 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye3,proj3,scale3) != \ - approx_view_properties(eye4,proj4,scale4) ) + eye4, proj4, scale4 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye3, proj3, scale3) != approx_view_properties(eye4, proj4, scale4) + def test_preserve_properties(main): qtbot, win = main - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() - object_tree = win.components['object_tree'] - object_tree.preferences['Preserve properties on reload'] = True + object_tree = win.components["object_tree"] + object_tree.preferences["Preserve properties on reload"] = True - assert(object_tree.CQ.childCount() == 1) + assert object_tree.CQ.childCount() == 1 props = object_tree.CQ.child(0).properties - props['Visible'] = False - props['Color'] = '#caffee' - props['Alpha'] = 0.5 + props["Visible"] = False + props["Color"] = "#caffee" + props["Alpha"] = 0.5 - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(object_tree.CQ.childCount() == 1) + assert object_tree.CQ.childCount() == 1 props = object_tree.CQ.child(0).properties - assert(props['Visible'] == False) - assert(props['Color'].name() == '#caffee') - assert(props['Alpha'] == 0.5) + assert props["Visible"] == False + assert props["Color"].name() == "#caffee" + assert props["Alpha"] == 0.5 + -def test_selection(main_multi,mocker): +def test_selection(main_multi, mocker): qtbot, win = main_multi - viewer = win.components['viewer'] - object_tree = win.components['object_tree'] + viewer = win.components["viewer"] + object_tree = win.components["object_tree"] CQ = object_tree.CQ obj1 = CQ.child(0) @@ -876,23 +920,23 @@ def test_selection(main_multi,mocker): obj2.setSelected(True) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep('out.step') - assert(len(imported.solids().vals()) == 2) + imported = cq.importers.importStep("out.step") + assert len(imported.solids().vals()) == 2 # export with one selected objects obj2.setSelected(False) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep('out.step') - assert(len(imported.solids().vals()) == 1) + imported = cq.importers.importStep("out.step") + assert len(imported.solids().vals()) == 1 # export with one selected objects obj1.setSelected(False) CQ.setSelected(True) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep('out.step') - assert(len(imported.solids().vals()) == 2) + imported = cq.importers.importStep("out.step") + assert len(imported.solids().vals()) == 2 # check if viewer and object tree are properly connected CQ.setSelected(False) @@ -905,15 +949,15 @@ def test_selection(main_multi,mocker): while ctx.MoreSelected(): shapes.append(ctx.SelectedShape()) ctx.NextSelected() - assert(len(shapes) == 2) + assert len(shapes) == 2 viewer.fit() qtbot.mouseClick(viewer.canvas, Qt.LeftButton) - assert(len(object_tree.tree.selectedItems()) == 0) + assert len(object_tree.tree.selectedItems()) == 0 viewer.sigObjectSelected.emit([obj1.shape_display.wrapped]) - assert(len(object_tree.tree.selectedItems()) == 1) + assert len(object_tree.tree.selectedItems()) == 1 # go through different handleSelection paths qtbot.mouseClick(object_tree.tree, Qt.LeftButton) @@ -922,111 +966,121 @@ def test_selection(main_multi,mocker): qtbot.keyClick(object_tree.tree, Qt.Key_Down) qtbot.keyClick(object_tree.tree, Qt.Key_Down) - assert(object_tree._export_STL_action.isEnabled() == False) - assert(object_tree._export_STEP_action.isEnabled() == False) - assert(object_tree._clear_current_action.isEnabled() == False) - assert(object_tree.properties_editor.isEnabled() == False) + assert object_tree._export_STL_action.isEnabled() == False + assert object_tree._export_STEP_action.isEnabled() == False + assert object_tree._clear_current_action.isEnabled() == False + assert object_tree.properties_editor.isEnabled() == False + def test_closing(main_clean_do_not_close): - qtbot,win = main_clean_do_not_close + qtbot, win = main_clean_do_not_close - editor = win.components['editor'] + editor = win.components["editor"] # make sure that windows is visible - assert(win.isVisible()) + assert win.isVisible() # should not quit win.close() - assert(win.isVisible()) + assert win.isVisible() # should quit editor.reset_modified() win.close() - assert(not win.isVisible()) + assert not win.isVisible() -def test_check_for_updates(main,mocker): - qtbot,win = main +def test_check_for_updates(main, mocker): + + qtbot, win = main # patch requests import requests - mocker.patch.object(requests.models.Response,'json', - return_value=[{'tag_name' : '0.0.2','draft' : False}]) + + mocker.patch.object( + requests.models.Response, + "json", + return_value=[{"tag_name": "0.0.2", "draft": False}], + ) # stub QMessageBox about about_stub = mocker.stub() - mocker.patch.object(QMessageBox, 'about', about_stub) + mocker.patch.object(QMessageBox, "about", about_stub) import cadquery - cadquery.__version__ = '0.0.1' + cadquery.__version__ = "0.0.1" win.check_for_cq_updates() - assert(about_stub.call_args[0][1] == 'Updates available') + assert about_stub.call_args[0][1] == "Updates available" - cadquery.__version__ = '0.0.3' + cadquery.__version__ = "0.0.3" win.check_for_cq_updates() - assert(about_stub.call_args[0][1] == 'No updates available') + assert about_stub.call_args[0][1] == "No updates available" -@pytest.mark.skipif(sys.platform.startswith('linux'),reason='Segfault workaround for linux') -def test_screenshot(main,mocker): - qtbot,win = main +@pytest.mark.skipif( + sys.platform.startswith("linux"), reason="Segfault workaround for linux" +) +def test_screenshot(main, mocker): - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.png','')) + qtbot, win = main + + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.png", "")) + + viewer = win.components["viewer"] + viewer._actions["Tools"][0].triggered.emit() - viewer = win.components['viewer'] - viewer._actions['Tools'][0].triggered.emit() + assert os.path.exists("out.png") - assert(os.path.exists('out.png')) def test_resize(main): - qtbot,win = main - editor = win.components['editor'] + qtbot, win = main + editor = win.components["editor"] editor.hide() qtbot.wait(50) editor.show() qtbot.wait(50) -code_simple_step = \ -'''import cadquery as cq + +code_simple_step = """import cadquery as cq imported = cq.importers.importStep('shape.step') -''' +""" + def test_relative_references(main): # create code with a relative reference in a subdirectory - p = Path('test_relative_references') + p = Path("test_relative_references") p.mkdir_p() - p_code = p.joinpath('code.py') + p_code = p.joinpath("code.py") p_code.write_text(code_simple_step) # create the referenced step file shape = cq.Workplane("XY").box(1, 1, 1) - p_step = p.joinpath('shape.step') + p_step = p.joinpath("shape.step") export(shape, "step", p_step) # open code qtbot, win = main - editor = win.components['editor'] + editor = win.components["editor"] editor.load_from_file(p_code) # render - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() # assert no errors - traceback_view = win.components['traceback_viewer'] - assert(traceback_view.current_exception.text() == '') + traceback_view = win.components["traceback_viewer"] + assert traceback_view.current_exception.text() == "" # assert one object has been rendered - obj_tree_comp = win.components['object_tree'] - assert(obj_tree_comp.CQ.childCount() == 1) + obj_tree_comp = win.components["object_tree"] + assert obj_tree_comp.CQ.childCount() == 1 # clean up p_code.remove_p() p_step.remove_p() p.rmdir_p() -code_color = \ -''' +code_color = """ import cadquery as cq result = cq.Workplane("XY" ).box(1, 1, 1) @@ -1037,19 +1091,20 @@ def test_relative_references(main): show_object(result, name ='5', options=dict(alpha=0.5,color=(1.,0,0))) show_object(result, name ='6', options=dict(rgba=(1.,0,0,.5))) show_object(result, name ='7', options=dict(color=('ff','cc','dd'))) -''' +""" + def test_render_colors(main_clean): qtbot, win = main_clean - obj_tree = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - log = win.components['log'] + obj_tree = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + log = win.components["log"] editor.set_text(code_color) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() CQ = obj_tree.CQ @@ -1057,42 +1112,43 @@ def test_render_colors(main_clean): assert not CQ.child(0).ais.HasColor() # object 2 - r,g,b,a = get_rgba(CQ.child(1).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) - assert( g == 0.0 ) + r, g, b, a = get_rgba(CQ.child(1).ais) + assert a == 0.5 + assert r == 1.0 + assert g == 0.0 # object 3 - r,g,b,a = get_rgba(CQ.child(2).ais) - assert( a == 0.5) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(2).ais) + assert a == 0.5 + assert r == 1.0 # object 4 - r,g,b,a = get_rgba(CQ.child(3).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(3).ais) + assert a == 0.5 + assert r == 1.0 # object 5 - r,g,b,a = get_rgba(CQ.child(4).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(4).ais) + assert a == 0.5 + assert r == 1.0 # object 6 - r,g,b,a = get_rgba(CQ.child(5).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(5).ais) + assert a == 0.5 + assert r == 1.0 # check if error occured qtbot.wait(100) - assert('Unknown color format' in log.toPlainText().splitlines()[-1]) + assert "Unknown color format" in log.toPlainText().splitlines()[-1] + def test_render_colors_console(main_clean): qtbot, win = main_clean - obj_tree = win.components['object_tree'] - log = win.components['log'] - console = win.components['console'] + obj_tree = win.components["object_tree"] + log = win.components["log"] + console = win.components["console"] console.execute_command(code_color) @@ -1102,54 +1158,55 @@ def test_render_colors_console(main_clean): assert not CQ.child(0).ais.HasColor() # object 2 - r,g,b,a = get_rgba(CQ.child(1).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(1).ais) + assert a == 0.5 + assert r == 1.0 # object 3 - r,g,b,a = get_rgba(CQ.child(2).ais) - assert( a == 0.5) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(2).ais) + assert a == 0.5 + assert r == 1.0 # object 4 - r,g,b,a = get_rgba(CQ.child(3).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(3).ais) + assert a == 0.5 + assert r == 1.0 # object 5 - r,g,b,a = get_rgba(CQ.child(4).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(4).ais) + assert a == 0.5 + assert r == 1.0 # object 6 - r,g,b,a = get_rgba(CQ.child(5).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(5).ais) + assert a == 0.5 + assert r == 1.0 # check if error occured qtbot.wait(100) - assert('Unknown color format' in log.toPlainText().splitlines()[-1]) + assert "Unknown color format" in log.toPlainText().splitlines()[-1] + -code_shading = \ -''' +code_shading = """ import cadquery as cq res1 = cq.Workplane('XY').box(5, 7, 5) res2 = cq.Workplane('XY').box(8, 5, 4) show_object(res1) show_object(res2,options={"alpha":0}) -''' +""" + def test_shading_aspect(main_clean): qtbot, win = main_clean - obj_tree = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] + obj_tree = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] editor.set_text(code_shading) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() CQ = obj_tree.CQ @@ -1160,147 +1217,148 @@ def test_shading_aspect(main_clean): # verify that they are the same assert ma1.Shininess() == ma2.Shininess() -def test_confirm_new(monkeypatch,editor): + +def test_confirm_new(monkeypatch, editor): qtbot, editor = editor - #check that initial state is as expected - assert(editor.modified == False) + # check that initial state is as expected + assert editor.modified == False editor.document().setPlainText(code) - assert(editor.modified == True) + assert editor.modified == True - #monkeypatch the confirmation dialog and run both scenarios + # monkeypatch the confirmation dialog and run both scenarios def cancel(*args, **kwargs): return QMessageBox.No def ok(*args, **kwargs): return QMessageBox.Yes - monkeypatch.setattr(QMessageBox, 'question', - staticmethod(cancel)) + monkeypatch.setattr(QMessageBox, "question", staticmethod(cancel)) editor.new() - assert(editor.modified == True) - assert(conv_line_ends(editor.get_text_with_eol()) == code) + assert editor.modified == True + assert conv_line_ends(editor.get_text_with_eol()) == code - monkeypatch.setattr(QMessageBox, 'question', - staticmethod(ok)) + monkeypatch.setattr(QMessageBox, "question", staticmethod(ok)) editor.new() - assert(editor.modified == False) - assert(editor.get_text_with_eol() == '') + assert editor.modified == False + assert editor.get_text_with_eol() == "" + -code_show_topods = \ -''' +code_show_topods = """ import cadquery as cq result = cq.Workplane("XY" ).box(1, 1, 1) show_object(result.val().wrapped) -''' +""" + def test_render_topods(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was rendered - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_topods) - debugger._actions['Run'][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 1) + debugger._actions["Run"][0].triggered.emit() + assert obj_tree_comp.CQ.childCount() == 1 # test rendering of topods object via console - console.execute('show(result.val().wrapped)') - assert(obj_tree_comp.CQ.childCount() == 2) + console.execute("show(result.val().wrapped)") + assert obj_tree_comp.CQ.childCount() == 2 # test rendering of list of topods object via console - console.execute('show([result.val().wrapped,result.val().wrapped])') - assert(obj_tree_comp.CQ.childCount() == 3) + console.execute("show([result.val().wrapped,result.val().wrapped])") + assert obj_tree_comp.CQ.childCount() == 3 -code_show_shape_list = \ -''' +code_show_shape_list = """ import cadquery as cq result1 = cq.Workplane("XY" ).box(1, 1, 1).val() result2 = cq.Workplane("XY",origin=(0,1,1)).box(1, 1, 1).val() show_object(result1) show_object([result1,result2]) -''' +""" + def test_render_shape_list(main): qtbot, win = main - log = win.components['log'] + log = win.components["log"] - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_shape_list) - debugger._actions['Run'][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 2) + debugger._actions["Run"][0].triggered.emit() + assert obj_tree_comp.CQ.childCount() == 2 # test rendering of Shape via console - console.execute('show(result1)') - console.execute('show([result1,result2])') - assert(obj_tree_comp.CQ.childCount() == 4) + console.execute("show(result1)") + console.execute("show([result1,result2])") + assert obj_tree_comp.CQ.childCount() == 4 # smoke test exception in show console.execute('show("a")') -code_show_assy = \ -'''import cadquery as cq + +code_show_assy = """import cadquery as cq result1 = cq.Workplane("XY" ).box(3, 3, 0.5) assy = cq.Assembly(result1) show_object(assy) -''' +""" + def test_render_assy(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_assy) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # test rendering via console - console.execute('show(assy)') + console.execute("show(assy)") qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 2) + assert obj_tree_comp.CQ.childCount() == 2 -code_show_ais = \ -'''import cadquery as cq + +code_show_ais = """import cadquery as cq from cadquery.occ_impl.assembly import toCAF import OCP @@ -1312,101 +1370,107 @@ def test_render_assy(main): ais = OCP.XCAFPrs.XCAFPrs_AISObject(lab) show_object(ais) -''' +""" + def test_render_ais(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_ais) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # test rendering via console - console.execute('show(ais)') + console.execute("show(ais)") qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 2) + assert obj_tree_comp.CQ.childCount() == 2 + -code_show_sketch = \ -'''import cadquery as cq +code_show_sketch = """import cadquery as cq s1 = cq.Sketch().rect(1,1) s2 = cq.Sketch().segment((0,0), (0,3.),"s1") show_object(s1) show_object(s2) -''' +""" + def test_render_sketch(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_sketch) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 2) + assert obj_tree_comp.CQ.childCount() == 2 # test rendering via console - console.execute('show(s1); show(s2)') + console.execute("show(s1); show(s2)") qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 4) + assert obj_tree_comp.CQ.childCount() == 4 + def test_window_title(monkeypatch, main): - fname = 'test_window_title.py' + fname = "test_window_title.py" - with open(fname, 'w') as f: + with open(fname, "w") as f: f.write(code) qtbot, win = main - #monkeypatch QFileDialog methods + # monkeypatch QFileDialog methods def filename(*args, **kwargs): return fname, None - monkeypatch.setattr(QFileDialog, 'getOpenFileName', - staticmethod(filename)) + monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename)) win.components["editor"].open() - assert(win.windowTitle().endswith(fname)) + assert win.windowTitle().endswith(fname) # handle a new file win.components["editor"].new() # I don't really care what the title is, as long as it's not a filename - assert(not win.windowTitle().endswith('.py')) + assert not win.windowTitle().endswith(".py") + def test_module_discovery(tmp_path, editor): qtbot, editor = editor - with open(tmp_path.joinpath('main.py'), 'w') as f: - f.write('import b') + with open(tmp_path.joinpath("main.py"), "w") as f: + f.write("import b") - assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [] + assert editor.get_imported_module_paths(str(tmp_path.joinpath("main.py"))) == [] - tmp_path.joinpath('b.py').touch() + tmp_path.joinpath("b.py").touch() + + assert editor.get_imported_module_paths(str(tmp_path.joinpath("main.py"))) == [ + str(tmp_path.joinpath("b.py")) + ] - assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [str(tmp_path.joinpath('b.py'))] def test_launch_syntax_error(tmp_path): @@ -1421,23 +1485,23 @@ def test_launch_syntax_error(tmp_path): editor.load_from_file(inputfile) win.show() - assert(win.isVisible()) + assert win.isVisible() -code_import_module_makebox = \ -""" + +code_import_module_makebox = """ from module_makebox import * z = 1 r = makebox(z) """ -code_module_makebox = \ -""" +code_module_makebox = """ import cadquery as cq def makebox(z): zval = z + 1 return cq.Workplane().box(1, 1, zval) """ + def test_reload_import_handle_error(tmp_path, main): TIMEOUT = 500 @@ -1458,18 +1522,18 @@ def test_reload_import_handle_error(tmp_path, main): # run, verify that no exception was generated editor.load_from_file(script) debugger._actions["Run"][0].triggered.emit() - assert(traceback_view.current_exception.text() == "") + assert traceback_view.current_exception.text() == "" # save the module with an error with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): lines = code_module_makebox.splitlines() - lines.remove(" zval = z + 1") # introduce NameError + lines.remove(" zval = z + 1") # introduce NameError lines = "\n".join(lines) modify_file(lines, module_file) # verify NameError is generated debugger._actions["Run"][0].triggered.emit() - assert("NameError" in traceback_view.current_exception.text()) + assert "NameError" in traceback_view.current_exception.text() # revert the error, verify rerender is triggered with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): @@ -1477,7 +1541,8 @@ def test_reload_import_handle_error(tmp_path, main): # verify that no exception was generated debugger._actions["Run"][0].triggered.emit() - assert(traceback_view.current_exception.text() == "") + assert traceback_view.current_exception.text() == "" + def test_modulefinder(tmp_path, main): @@ -1486,7 +1551,7 @@ def test_modulefinder(tmp_path, main): editor = win.components["editor"] debugger = win.components["debugger"] traceback_view = win.components["traceback_viewer"] - log = win.components['log'] + log = win.components["log"] editor.autoreload(True) editor.preferences["Autoreload: watch imported modules"] = True @@ -1499,56 +1564,58 @@ def test_modulefinder(tmp_path, main): modify_file("import emptydir", script) qtbot.wait(100) - assert("Cannot determine imported modules" in log.toPlainText().splitlines()[-1]) + assert "Cannot determine imported modules" in log.toPlainText().splitlines()[-1] + def test_show_all(main): qtbot, win = main - editor = win.components['editor'] - debugger = win.components['debugger'] - object_tree = win.components['object_tree'] + editor = win.components["editor"] + debugger = win.components["debugger"] + object_tree = win.components["object_tree"] # remove all objects object_tree.removeObjects() - assert(object_tree.CQ.childCount() == 0) + assert object_tree.CQ.childCount() == 0 # add code wtih Shape, Workplane, Assy, Sketch editor.set_text(code_show_all) # Run and check if all are shown - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(object_tree.CQ.childCount() == 4) + assert object_tree.CQ.childCount() == 4 -code_randcolor = \ -"""import cadquery as cq + +code_randcolor = """import cadquery as cq b = cq.Workplane().box(8, 3, 4) for i in range(10): show_object(b.translate((0,5*i,0)), options=rand_color(alpha=0)) show_object(b.translate((0,5*i,0)), options=rand_color(0, True)) """ + def test_randcolor(main): - + qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_randcolor) - debugger._actions['Run'][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 2*10) + debugger._actions["Run"][0].triggered.emit() + assert obj_tree_comp.CQ.childCount() == 2 * 10 -code_show_wo_name = \ -""" + +code_show_wo_name = """ import cadquery as cq res = cq.Workplane().box(1,1,1) @@ -1557,28 +1624,43 @@ def test_randcolor(main): show_object(cq.Workplane().box(1,1,1)) """ + def test_show_without_name(main): qtbot, win = main - editor = win.components['editor'] - debugger = win.components['debugger'] - object_tree = win.components['object_tree'] + editor = win.components["editor"] + debugger = win.components["debugger"] + object_tree = win.components["object_tree"] # remove all objects object_tree.removeObjects() - assert(object_tree.CQ.childCount() == 0) + assert object_tree.CQ.childCount() == 0 # add code wtih Shape, Workplane, Assy, Sketch editor.set_text(code_show_wo_name) # Run and check if all are shown - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(object_tree.CQ.childCount() == 2) + assert object_tree.CQ.childCount() == 2 # Check the name of the first object - assert(object_tree.CQ.child(0).text(0) == "res") + assert object_tree.CQ.child(0).text(0) == "res" # Check that the name of the seconf object is an int int(object_tree.CQ.child(1).text(0)) + + +def test_print_redirect(main): + qtbot, win = main + + editor = win.components["editor"] + debugger = win.components["debugger"] + log = win.components["log"] + + editor.set_text(r"""print('foo\nbar')""") + debugger._actions["Run"][0].triggered.emit() + + qtbot.wait(100) + assert "foo\nbar" in log.toPlainText() From 52f45b00770b2ecb3dd61a86a82b5b55d9ed6796 Mon Sep 17 00:00:00 2001 From: snoyer Date: Thu, 13 Feb 2025 08:30:15 +0400 Subject: [PATCH 099/134] undo formatting --- tests/test_app.py | 1176 +++++++++++++++++++++------------------------ 1 file changed, 554 insertions(+), 622 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index e20e4e2d..c82b928e 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,7 +1,7 @@ from path import Path import os, sys, asyncio -if sys.platform == "win32": +if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) from multiprocessing import Process @@ -17,54 +17,61 @@ from cq_editor.widgets.editor import Editor from cq_editor.cq_utils import export, get_occ_color -code = """import cadquery as cq +code = \ +'''import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) -result = result.edges("|Z").fillet(0.125)""" +result = result.edges("|Z").fillet(0.125)''' -code_bigger_object = """import cadquery as cq +code_bigger_object = \ +'''import cadquery as cq result = cq.Workplane("XY" ) result = result.box(20, 20, 0.5) result = result.edges("|Z").fillet(0.125) -""" +''' -code_show_Workplane = """import cadquery as cq +code_show_Workplane = \ +'''import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) show_object(result) -""" +''' -code_show_Workplane_named = """import cadquery as cq +code_show_Workplane_named = \ +'''import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) log('test') show_object(result,name='test') -""" +''' -code_show_Shape = """import cadquery as cq +code_show_Shape = \ +'''import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) show_object(result.val()) -""" +''' -code_debug_Workplane = """import cadquery as cq +code_debug_Workplane = \ +'''import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) debug(result) -""" +''' -code_multi = """import cadquery as cq +code_multi = \ +'''import cadquery as cq result1 = cq.Workplane("XY" ).box(3, 3, 0.5) result2 = cq.Workplane("XY" ).box(3, 3, 0.5).translate((0,15,0)) -""" +''' code_nested_top = """import test_nested_bottom """ @@ -84,35 +91,31 @@ sk = cq.Sketch().rect(1,1) """ - def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) def modify_file(code, path="test.py"): - p = Process(target=_modify_file, args=(code, path)) + p = Process(target=_modify_file, args=(code,path)) p.start() p.join() - def get_center(widget): pos = widget.pos() - pos.setX(pos.x() + widget.width() // 2) - pos.setY(pos.y() + widget.height() // 2) + pos.setX(pos.x()+widget.width()//2) + pos.setY(pos.y()+widget.height()//2) return pos - def get_bottom_left(widget): pos = widget.pos() - pos.setY(pos.y() + widget.height()) + pos.setY(pos.y()+widget.height()) return pos - def get_rgba(ais): alpha = ais.Transparency() @@ -120,30 +123,28 @@ def get_rgba(ais): return color.redF(), color.greenF(), color.blueF(), alpha - @pytest.fixture -def main(qtbot, mocker): +def main(qtbot,mocker): - mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) + mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) win = MainWindow() win.show() qtbot.addWidget(win) - editor = win.components["editor"] + editor = win.components['editor'] editor.set_text(code) - debugger = win.components["debugger"] - debugger._actions["Run"][0].triggered.emit() + debugger = win.components['debugger'] + debugger._actions['Run'][0].triggered.emit() return qtbot, win - @pytest.fixture -def main_clean(qtbot, mocker): +def main_clean(qtbot,mocker): - mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) + mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) win = MainWindow() win.show() @@ -151,16 +152,15 @@ def main_clean(qtbot, mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components["editor"] + editor = win.components['editor'] editor.set_text(code) return qtbot, win - @pytest.fixture -def main_clean_do_not_close(qtbot, mocker): +def main_clean_do_not_close(qtbot,mocker): - mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.No) + mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.No) win = MainWindow() win.show() @@ -168,17 +168,16 @@ def main_clean_do_not_close(qtbot, mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components["editor"] + editor = win.components['editor'] editor.set_text(code) return qtbot, win - @pytest.fixture -def main_multi(qtbot, mocker): +def main_multi(qtbot,mocker): - mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) - mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.step", "")) + mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) + mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) win = MainWindow() win.show() @@ -186,120 +185,116 @@ def main_multi(qtbot, mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components["editor"] + editor = win.components['editor'] editor.set_text(code_multi) - debugger = win.components["debugger"] - debugger._actions["Run"][0].triggered.emit() + debugger = win.components['debugger'] + debugger._actions['Run'][0].triggered.emit() return qtbot, win - def test_render(main): qtbot, win = main - obj_tree_comp = win.components["object_tree"] - editor = win.components["editor"] - debugger = win.components["debugger"] - console = win.components["console"] - log = win.components["log"] + obj_tree_comp = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + console = win.components['console'] + log = win.components['log'] # enable CQ reloading - debugger.preferences["Reload CQ"] = True + debugger.preferences['Reload CQ'] = True # check that object was rendered - assert obj_tree_comp.CQ.childCount() == 1 + assert(obj_tree_comp.CQ.childCount() == 1) # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) # check that object was rendered usin explicit show_object call editor.set_text(code_show_Workplane) - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 1 + assert(obj_tree_comp.CQ.childCount() == 1) obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) # check that cq.Shape object was rendered using explicit show_object call editor.set_text(code_show_Shape) - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 1 + assert(obj_tree_comp.CQ.childCount() == 1) obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) # test rendering via console console.execute(code_show_Workplane) - assert obj_tree_comp.CQ.childCount() == 1 + assert(obj_tree_comp.CQ.childCount() == 1) obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) console.execute(code_show_Shape) - assert obj_tree_comp.CQ.childCount() == 1 + assert(obj_tree_comp.CQ.childCount() == 1) # check object rendering using show_object call with a name specified and # debug call editor.set_text(code_show_Workplane_named) - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() qtbot.wait(100) - assert obj_tree_comp.CQ.child(0).text(0) == "test" - assert "test" in log.toPlainText().splitlines()[-1] + assert(obj_tree_comp.CQ.child(0).text(0) == 'test') + assert('test' in log.toPlainText().splitlines()[-1]) # cq reloading check obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) editor.set_text(code_reload_issue) - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() qtbot.wait(100) - assert obj_tree_comp.CQ.childCount() == 3 + assert(obj_tree_comp.CQ.childCount() == 3) - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() qtbot.wait(100) - assert obj_tree_comp.CQ.childCount() == 3 + assert(obj_tree_comp.CQ.childCount() == 3) - -def test_export(main, mocker): +def test_export(main,mocker): qtbot, win = main - debugger = win.components["debugger"] - debugger._actions["Run"][0].triggered.emit() + debugger = win.components['debugger'] + debugger._actions['Run'][0].triggered.emit() - # set focus - obj_tree = win.components["object_tree"].tree - obj_tree_comp = win.components["object_tree"] + #set focus + obj_tree = win.components['object_tree'].tree + obj_tree_comp = win.components['object_tree'] qtbot.mouseClick(obj_tree, Qt.LeftButton) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Down) - # export STL - mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.stl", "")) + #export STL + mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.stl','')) obj_tree_comp._export_STL_action.triggered.emit() - assert os.path.isfile("out.stl") + assert(os.path.isfile('out.stl')) - # export STEP - mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.step", "")) + #export STEP + mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) obj_tree_comp._export_STEP_action.triggered.emit() - assert os.path.isfile("out.step") - - # clean - os.remove("out.step") - os.remove("out.stl") + assert(os.path.isfile('out.step')) + #clean + os.remove('out.step') + os.remove('out.stl') def number_visible_items(viewer): from OCP.AIS import AIS_ListOfInteractive - l = AIS_ListOfInteractive() viewer_ctx = viewer._get_context() @@ -307,223 +302,186 @@ def number_visible_items(viewer): return l.Extent() - def test_inspect(main): qtbot, win = main - # set focus and make invisible - obj_tree = win.components["object_tree"].tree + #set focus and make invisible + obj_tree = win.components['object_tree'].tree qtbot.mouseClick(obj_tree, Qt.LeftButton) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Space) - # enable object inspector - insp = win.components["cq_object_inspector"] + #enable object inspector + insp = win.components['cq_object_inspector'] insp._toolbar_actions[0].toggled.emit(True) - # check if all stack items are visible in the tree - assert insp.root.childCount() == 3 + #check if all stack items are visible in the tree + assert(insp.root.childCount() == 3) - # check if correct number of items is displayed - viewer = win.components["viewer"] + #check if correct number of items is displayed + viewer = win.components['viewer'] insp.setCurrentItem(insp.root.child(0)) - assert number_visible_items(viewer) == 4 + assert(number_visible_items(viewer) == 4) insp.setCurrentItem(insp.root.child(1)) - assert number_visible_items(viewer) == 7 + assert(number_visible_items(viewer) == 7) insp.setCurrentItem(insp.root.child(2)) - assert number_visible_items(viewer) == 4 + assert(number_visible_items(viewer) == 4) insp._toolbar_actions[0].toggled.emit(False) - assert number_visible_items(viewer) == 3 - + assert(number_visible_items(viewer) == 3) class event_loop(object): - """Used to mock the QEventLoop for the debugger component""" + '''Used to mock the QEventLoop for the debugger component + ''' - def __init__(self, callbacks): + def __init__(self,callbacks): self.callbacks = callbacks self.i = 0 def exec_(self): - if self.i < len(self.callbacks): + if self.i 0 - assert conv_line_ends(editor.get_text_with_eol()) == code + #check that loading from file works properly + editor.load_from_file('test.py') + assert(len(editor.get_text_with_eol()) > 0) + assert(conv_line_ends(editor.get_text_with_eol()) == code) - # check that loading from file works properly + #check that loading from file works properly editor.new() - assert editor.get_text_with_eol() == "" + assert(editor.get_text_with_eol() == '') - # monkeypatch QFileDialog methods + #monkeypatch QFileDialog methods def filename(*args, **kwargs): - return "test.py", None + return 'test.py',None def filename2(*args, **kwargs): - return "test2.py", None + return 'test2.py',None - monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename)) + monkeypatch.setattr(QFileDialog, 'getOpenFileName', + staticmethod(filename)) - monkeypatch.setattr(QFileDialog, "getSaveFileName", staticmethod(filename2)) + monkeypatch.setattr(QFileDialog, 'getSaveFileName', + staticmethod(filename2)) - # check that open file works properly + #check that open file works properly editor.open() - assert conv_line_ends(editor.get_text_with_eol()) == code + assert(conv_line_ends(editor.get_text_with_eol()) == code) - # check that save file works properly + #check that save file works properly editor.new() qtbot.mouseClick(editor, Qt.LeftButton) - qtbot.keyClick(editor, Qt.Key_A) + qtbot.keyClick(editor,Qt.Key_A) - assert editor.document().isModified() == True + assert(editor.document().isModified() == True) - editor.filename = "test2.py" + editor.filename = 'test2.py' editor.save() - assert editor.document().isModified() == False + assert(editor.document().isModified() == False) - monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename2)) + monkeypatch.setattr(QFileDialog, 'getOpenFileName', + staticmethod(filename2)) editor.open() - assert editor.get_text_with_eol() == "a" + assert(editor.get_text_with_eol() == 'a') - # check that save as works properly - os.remove("test2.py") + #check that save as works properly + os.remove('test2.py') editor.save_as() - assert os.path.exists(filename2()[0]) + assert(os.path.exists(filename2()[0])) - # test persistance - settings = QSettings("test") + #test persistance + settings = QSettings('test') editor.saveComponentState(settings) editor.new() - assert editor.get_text_with_eol() == "" + assert(editor.get_text_with_eol() == '') editor.restoreComponentState(settings) - assert editor.get_text_with_eol() == "a" + assert(editor.get_text_with_eol() == 'a') - # test error handling - os.remove("test2.py") - assert not os.path.exists("test2.py") + #test error handling + os.remove('test2.py') + assert(not os.path.exists('test2.py')) editor.restoreComponentState(settings) - @pytest.mark.repeat(1) -def test_editor_autoreload(monkeypatch, editor): +def test_editor_autoreload(monkeypatch,editor): qtbot, editor = editor @@ -709,13 +668,13 @@ def test_editor_autoreload(monkeypatch, editor): # start out with autoreload enabled editor.autoreload(True) - with open("test.py", "w") as f: + with open('test.py','w') as f: f.write(code) - assert editor.get_text_with_eol() == "" + assert(editor.get_text_with_eol() == '') - editor.load_from_file("test.py") - assert len(editor.get_text_with_eol()) > 0 + editor.load_from_file('test.py') + assert(len(editor.get_text_with_eol()) > 0) # wait for reload. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): @@ -723,7 +682,7 @@ def test_editor_autoreload(monkeypatch, editor): modify_file(code_bigger_object) # check that editor has updated file contents - assert code_bigger_object.splitlines()[2] in editor.get_text_with_eol() + assert(code_bigger_object.splitlines()[2] in editor.get_text_with_eol()) # disable autoreload editor.autoreload(False) @@ -737,7 +696,7 @@ def test_editor_autoreload(monkeypatch, editor): modify_file(code) # editor should continue showing old contents since autoreload is disabled. - assert code_bigger_object.splitlines()[2] in editor.get_text_with_eol() + assert(code_bigger_object.splitlines()[2] in editor.get_text_with_eol()) # Saving a file with autoreload disabled should not trigger a rerender. with pytest.raises(pytestqt.exceptions.TimeoutError): @@ -750,7 +709,6 @@ def test_editor_autoreload(monkeypatch, editor): with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): editor.save() - def test_autoreload_nested(editor): qtbot, editor = editor @@ -758,158 +716,156 @@ def test_autoreload_nested(editor): TIMEOUT = 500 editor.autoreload(True) - editor.preferences["Autoreload: watch imported modules"] = True + editor.preferences['Autoreload: watch imported modules'] = True - with open("test_nested_top.py", "w") as f: + with open('test_nested_top.py','w') as f: f.write(code_nested_top) - with open("test_nested_bottom.py", "w") as f: + with open('test_nested_bottom.py','w') as f: f.write("") - assert editor.get_text_with_eol() == "" + assert(editor.get_text_with_eol() == '') - editor.load_from_file("test_nested_top.py") - assert len(editor.get_text_with_eol()) > 0 + editor.load_from_file('test_nested_top.py') + assert(len(editor.get_text_with_eol()) > 0) # wait for reload. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): # modify file - NB: separate process is needed to avoid Windows quirks - modify_file(code_nested_bottom, "test_nested_bottom.py") - + modify_file(code_nested_bottom, 'test_nested_bottom.py') def test_console(main): qtbot, win = main - console = win.components["console"] + console = win.components['console'] # test execute_command a = [] - console.push_vars({"a": a}) - console.execute_command("a.append(1)") - assert len(a) == 1 + console.push_vars({'a' : a}) + console.execute_command('a.append(1)') + assert(len(a) == 1) # test print_text pos_orig = console._prompt_pos - console.print_text("a") - assert console._prompt_pos == pos_orig + len("a") - + console.print_text('a') + assert(console._prompt_pos == pos_orig + len('a')) def test_viewer(main): qtbot, win = main - viewer = win.components["viewer"] - - # not sure how to test this, so only smoke tests + viewer = win.components['viewer'] - # trigger all 'View' actions - actions = viewer._actions["View"] - for a in actions: - a.trigger() + #not sure how to test this, so only smoke tests + #trigger all 'View' actions + actions = viewer._actions['View'] + for a in actions: a.trigger() -code_module = """def dummy(): return True""" - -code_import = """from module import dummy -assert(dummy())""" +code_module = \ +'''def dummy(): return True''' +code_import = \ +'''from module import dummy +assert(dummy())''' def test_module_import(main): qtbot, win = main - editor = win.components["editor"] - debugger = win.components["debugger"] - traceback_view = win.components["traceback_viewer"] + editor = win.components['editor'] + debugger = win.components['debugger'] + traceback_view = win.components['traceback_viewer'] - # save the dummy module - with open("module.py", "w") as f: + #save the dummy module + with open('module.py','w') as f: f.write(code_module) - # run the code importing this module + #run the code importing this module editor.set_text(code_import) - debugger._actions["Run"][0].triggered.emit() - - # verify that no exception was generated - assert traceback_view.current_exception.text() == "" + debugger._actions['Run'][0].triggered.emit() + #verify that no exception was generated + assert(traceback_view.current_exception.text() == '') def test_auto_fit_view(main_clean): - def concat(eye, proj, scale): - return eye + proj + (scale,) + def concat(eye,proj,scale): + return eye+proj+(scale,) - def approx_view_properties(eye, proj, scale): + def approx_view_properties(eye,proj,scale): - return pytest.approx(eye + proj + (scale,)) + return pytest.approx(eye+proj+(scale,)) qtbot, win = main_clean - editor = win.components["editor"] - debugger = win.components["debugger"] - viewer = win.components["viewer"] - object_tree = win.components["object_tree"] + editor = win.components['editor'] + debugger = win.components['debugger'] + viewer = win.components['viewer'] + object_tree = win.components['object_tree'] view = viewer.canvas.view - viewer.preferences["Fit automatically"] = False - eye0, proj0, scale0 = view.Eye(), view.Proj(), view.Scale() + viewer.preferences['Fit automatically'] = False + eye0,proj0,scale0 = view.Eye(),view.Proj(),view.Scale() # check if camera position is adjusted automatically when rendering for the # first time debugger.render() - eye1, proj1, scale1 = view.Eye(), view.Proj(), view.Scale() - assert concat(eye0, proj0, scale0) != approx_view_properties(eye1, proj1, scale1) + eye1,proj1,scale1 = view.Eye(),view.Proj(),view.Scale() + assert( concat(eye0,proj0,scale0) != \ + approx_view_properties(eye1,proj1,scale1) ) # check if camera position is not changed fter code change editor.set_text(code_bigger_object) debugger.render() - eye2, proj2, scale2 = view.Eye(), view.Proj(), view.Scale() - assert concat(eye1, proj1, scale1) == approx_view_properties(eye2, proj2, scale2) + eye2,proj2,scale2 = view.Eye(),view.Proj(),view.Scale() + assert( concat(eye1,proj1,scale1) == \ + approx_view_properties(eye2,proj2,scale2) ) # check if position is adjusted automatically after erasing all objects object_tree.removeObjects() debugger.render() - eye3, proj3, scale3 = view.Eye(), view.Proj(), view.Scale() - assert concat(eye2, proj2, scale2) != approx_view_properties(eye3, proj3, scale3) + eye3,proj3,scale3 = view.Eye(),view.Proj(),view.Scale() + assert( concat(eye2,proj2,scale2) != \ + approx_view_properties(eye3,proj3,scale3) ) # check if position is adjusted automatically if settings are changed - viewer.preferences["Fit automatically"] = True + viewer.preferences['Fit automatically'] = True editor.set_text(code) debugger.render() - eye4, proj4, scale4 = view.Eye(), view.Proj(), view.Scale() - assert concat(eye3, proj3, scale3) != approx_view_properties(eye4, proj4, scale4) - + eye4,proj4,scale4 = view.Eye(),view.Proj(),view.Scale() + assert( concat(eye3,proj3,scale3) != \ + approx_view_properties(eye4,proj4,scale4) ) def test_preserve_properties(main): qtbot, win = main - debugger = win.components["debugger"] - debugger._actions["Run"][0].triggered.emit() + debugger = win.components['debugger'] + debugger._actions['Run'][0].triggered.emit() - object_tree = win.components["object_tree"] - object_tree.preferences["Preserve properties on reload"] = True + object_tree = win.components['object_tree'] + object_tree.preferences['Preserve properties on reload'] = True - assert object_tree.CQ.childCount() == 1 + assert(object_tree.CQ.childCount() == 1) props = object_tree.CQ.child(0).properties - props["Visible"] = False - props["Color"] = "#caffee" - props["Alpha"] = 0.5 + props['Visible'] = False + props['Color'] = '#caffee' + props['Alpha'] = 0.5 - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() - assert object_tree.CQ.childCount() == 1 + assert(object_tree.CQ.childCount() == 1) props = object_tree.CQ.child(0).properties - assert props["Visible"] == False - assert props["Color"].name() == "#caffee" - assert props["Alpha"] == 0.5 + assert(props['Visible'] == False) + assert(props['Color'].name() == '#caffee') + assert(props['Alpha'] == 0.5) - -def test_selection(main_multi, mocker): +def test_selection(main_multi,mocker): qtbot, win = main_multi - viewer = win.components["viewer"] - object_tree = win.components["object_tree"] + viewer = win.components['viewer'] + object_tree = win.components['object_tree'] CQ = object_tree.CQ obj1 = CQ.child(0) @@ -920,23 +876,23 @@ def test_selection(main_multi, mocker): obj2.setSelected(True) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep("out.step") - assert len(imported.solids().vals()) == 2 + imported = cq.importers.importStep('out.step') + assert(len(imported.solids().vals()) == 2) # export with one selected objects obj2.setSelected(False) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep("out.step") - assert len(imported.solids().vals()) == 1 + imported = cq.importers.importStep('out.step') + assert(len(imported.solids().vals()) == 1) # export with one selected objects obj1.setSelected(False) CQ.setSelected(True) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep("out.step") - assert len(imported.solids().vals()) == 2 + imported = cq.importers.importStep('out.step') + assert(len(imported.solids().vals()) == 2) # check if viewer and object tree are properly connected CQ.setSelected(False) @@ -949,15 +905,15 @@ def test_selection(main_multi, mocker): while ctx.MoreSelected(): shapes.append(ctx.SelectedShape()) ctx.NextSelected() - assert len(shapes) == 2 + assert(len(shapes) == 2) viewer.fit() qtbot.mouseClick(viewer.canvas, Qt.LeftButton) - assert len(object_tree.tree.selectedItems()) == 0 + assert(len(object_tree.tree.selectedItems()) == 0) viewer.sigObjectSelected.emit([obj1.shape_display.wrapped]) - assert len(object_tree.tree.selectedItems()) == 1 + assert(len(object_tree.tree.selectedItems()) == 1) # go through different handleSelection paths qtbot.mouseClick(object_tree.tree, Qt.LeftButton) @@ -966,121 +922,111 @@ def test_selection(main_multi, mocker): qtbot.keyClick(object_tree.tree, Qt.Key_Down) qtbot.keyClick(object_tree.tree, Qt.Key_Down) - assert object_tree._export_STL_action.isEnabled() == False - assert object_tree._export_STEP_action.isEnabled() == False - assert object_tree._clear_current_action.isEnabled() == False - assert object_tree.properties_editor.isEnabled() == False - + assert(object_tree._export_STL_action.isEnabled() == False) + assert(object_tree._export_STEP_action.isEnabled() == False) + assert(object_tree._clear_current_action.isEnabled() == False) + assert(object_tree.properties_editor.isEnabled() == False) def test_closing(main_clean_do_not_close): - qtbot, win = main_clean_do_not_close + qtbot,win = main_clean_do_not_close - editor = win.components["editor"] + editor = win.components['editor'] # make sure that windows is visible - assert win.isVisible() + assert(win.isVisible()) # should not quit win.close() - assert win.isVisible() + assert(win.isVisible()) # should quit editor.reset_modified() win.close() - assert not win.isVisible() - + assert(not win.isVisible()) -def test_check_for_updates(main, mocker): +def test_check_for_updates(main,mocker): - qtbot, win = main + qtbot,win = main # patch requests import requests - - mocker.patch.object( - requests.models.Response, - "json", - return_value=[{"tag_name": "0.0.2", "draft": False}], - ) + mocker.patch.object(requests.models.Response,'json', + return_value=[{'tag_name' : '0.0.2','draft' : False}]) # stub QMessageBox about about_stub = mocker.stub() - mocker.patch.object(QMessageBox, "about", about_stub) + mocker.patch.object(QMessageBox, 'about', about_stub) import cadquery - cadquery.__version__ = "0.0.1" + cadquery.__version__ = '0.0.1' win.check_for_cq_updates() - assert about_stub.call_args[0][1] == "Updates available" + assert(about_stub.call_args[0][1] == 'Updates available') - cadquery.__version__ = "0.0.3" + cadquery.__version__ = '0.0.3' win.check_for_cq_updates() - assert about_stub.call_args[0][1] == "No updates available" - + assert(about_stub.call_args[0][1] == 'No updates available') -@pytest.mark.skipif( - sys.platform.startswith("linux"), reason="Segfault workaround for linux" -) -def test_screenshot(main, mocker): +@pytest.mark.skipif(sys.platform.startswith('linux'),reason='Segfault workaround for linux') +def test_screenshot(main,mocker): - qtbot, win = main - - mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.png", "")) + qtbot,win = main - viewer = win.components["viewer"] - viewer._actions["Tools"][0].triggered.emit() + mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.png','')) - assert os.path.exists("out.png") + viewer = win.components['viewer'] + viewer._actions['Tools'][0].triggered.emit() + assert(os.path.exists('out.png')) def test_resize(main): - qtbot, win = main - editor = win.components["editor"] + qtbot,win = main + editor = win.components['editor'] editor.hide() qtbot.wait(50) editor.show() qtbot.wait(50) - -code_simple_step = """import cadquery as cq +code_simple_step = \ +'''import cadquery as cq imported = cq.importers.importStep('shape.step') -""" - +''' def test_relative_references(main): # create code with a relative reference in a subdirectory - p = Path("test_relative_references") + p = Path('test_relative_references') p.mkdir_p() - p_code = p.joinpath("code.py") + p_code = p.joinpath('code.py') p_code.write_text(code_simple_step) # create the referenced step file shape = cq.Workplane("XY").box(1, 1, 1) - p_step = p.joinpath("shape.step") + p_step = p.joinpath('shape.step') export(shape, "step", p_step) # open code qtbot, win = main - editor = win.components["editor"] + editor = win.components['editor'] editor.load_from_file(p_code) # render - debugger = win.components["debugger"] - debugger._actions["Run"][0].triggered.emit() + debugger = win.components['debugger'] + debugger._actions['Run'][0].triggered.emit() # assert no errors - traceback_view = win.components["traceback_viewer"] - assert traceback_view.current_exception.text() == "" + traceback_view = win.components['traceback_viewer'] + assert(traceback_view.current_exception.text() == '') # assert one object has been rendered - obj_tree_comp = win.components["object_tree"] - assert obj_tree_comp.CQ.childCount() == 1 + obj_tree_comp = win.components['object_tree'] + assert(obj_tree_comp.CQ.childCount() == 1) # clean up p_code.remove_p() p_step.remove_p() p.rmdir_p() -code_color = """ +code_color = \ +''' import cadquery as cq result = cq.Workplane("XY" ).box(1, 1, 1) @@ -1091,20 +1037,19 @@ def test_relative_references(main): show_object(result, name ='5', options=dict(alpha=0.5,color=(1.,0,0))) show_object(result, name ='6', options=dict(rgba=(1.,0,0,.5))) show_object(result, name ='7', options=dict(color=('ff','cc','dd'))) -""" - +''' def test_render_colors(main_clean): qtbot, win = main_clean - obj_tree = win.components["object_tree"] - editor = win.components["editor"] - debugger = win.components["debugger"] - log = win.components["log"] + obj_tree = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + log = win.components['log'] editor.set_text(code_color) - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() CQ = obj_tree.CQ @@ -1112,43 +1057,42 @@ def test_render_colors(main_clean): assert not CQ.child(0).ais.HasColor() # object 2 - r, g, b, a = get_rgba(CQ.child(1).ais) - assert a == 0.5 - assert r == 1.0 - assert g == 0.0 + r,g,b,a = get_rgba(CQ.child(1).ais) + assert( a == 0.5 ) + assert( r == 1.0 ) + assert( g == 0.0 ) # object 3 - r, g, b, a = get_rgba(CQ.child(2).ais) - assert a == 0.5 - assert r == 1.0 + r,g,b,a = get_rgba(CQ.child(2).ais) + assert( a == 0.5) + assert( r == 1.0 ) # object 4 - r, g, b, a = get_rgba(CQ.child(3).ais) - assert a == 0.5 - assert r == 1.0 + r,g,b,a = get_rgba(CQ.child(3).ais) + assert( a == 0.5 ) + assert( r == 1.0 ) # object 5 - r, g, b, a = get_rgba(CQ.child(4).ais) - assert a == 0.5 - assert r == 1.0 + r,g,b,a = get_rgba(CQ.child(4).ais) + assert( a == 0.5 ) + assert( r == 1.0 ) # object 6 - r, g, b, a = get_rgba(CQ.child(5).ais) - assert a == 0.5 - assert r == 1.0 + r,g,b,a = get_rgba(CQ.child(5).ais) + assert( a == 0.5 ) + assert( r == 1.0 ) # check if error occured qtbot.wait(100) - assert "Unknown color format" in log.toPlainText().splitlines()[-1] - + assert('Unknown color format' in log.toPlainText().splitlines()[-1]) def test_render_colors_console(main_clean): qtbot, win = main_clean - obj_tree = win.components["object_tree"] - log = win.components["log"] - console = win.components["console"] + obj_tree = win.components['object_tree'] + log = win.components['log'] + console = win.components['console'] console.execute_command(code_color) @@ -1158,55 +1102,54 @@ def test_render_colors_console(main_clean): assert not CQ.child(0).ais.HasColor() # object 2 - r, g, b, a = get_rgba(CQ.child(1).ais) - assert a == 0.5 - assert r == 1.0 + r,g,b,a = get_rgba(CQ.child(1).ais) + assert( a == 0.5 ) + assert( r == 1.0 ) # object 3 - r, g, b, a = get_rgba(CQ.child(2).ais) - assert a == 0.5 - assert r == 1.0 + r,g,b,a = get_rgba(CQ.child(2).ais) + assert( a == 0.5) + assert( r == 1.0 ) # object 4 - r, g, b, a = get_rgba(CQ.child(3).ais) - assert a == 0.5 - assert r == 1.0 + r,g,b,a = get_rgba(CQ.child(3).ais) + assert( a == 0.5 ) + assert( r == 1.0 ) # object 5 - r, g, b, a = get_rgba(CQ.child(4).ais) - assert a == 0.5 - assert r == 1.0 + r,g,b,a = get_rgba(CQ.child(4).ais) + assert( a == 0.5 ) + assert( r == 1.0 ) # object 6 - r, g, b, a = get_rgba(CQ.child(5).ais) - assert a == 0.5 - assert r == 1.0 + r,g,b,a = get_rgba(CQ.child(5).ais) + assert( a == 0.5 ) + assert( r == 1.0 ) # check if error occured qtbot.wait(100) - assert "Unknown color format" in log.toPlainText().splitlines()[-1] + assert('Unknown color format' in log.toPlainText().splitlines()[-1]) - -code_shading = """ +code_shading = \ +''' import cadquery as cq res1 = cq.Workplane('XY').box(5, 7, 5) res2 = cq.Workplane('XY').box(8, 5, 4) show_object(res1) show_object(res2,options={"alpha":0}) -""" - +''' def test_shading_aspect(main_clean): qtbot, win = main_clean - obj_tree = win.components["object_tree"] - editor = win.components["editor"] - debugger = win.components["debugger"] + obj_tree = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] editor.set_text(code_shading) - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() CQ = obj_tree.CQ @@ -1217,148 +1160,147 @@ def test_shading_aspect(main_clean): # verify that they are the same assert ma1.Shininess() == ma2.Shininess() - -def test_confirm_new(monkeypatch, editor): +def test_confirm_new(monkeypatch,editor): qtbot, editor = editor - # check that initial state is as expected - assert editor.modified == False + #check that initial state is as expected + assert(editor.modified == False) editor.document().setPlainText(code) - assert editor.modified == True + assert(editor.modified == True) - # monkeypatch the confirmation dialog and run both scenarios + #monkeypatch the confirmation dialog and run both scenarios def cancel(*args, **kwargs): return QMessageBox.No def ok(*args, **kwargs): return QMessageBox.Yes - monkeypatch.setattr(QMessageBox, "question", staticmethod(cancel)) + monkeypatch.setattr(QMessageBox, 'question', + staticmethod(cancel)) editor.new() - assert editor.modified == True - assert conv_line_ends(editor.get_text_with_eol()) == code + assert(editor.modified == True) + assert(conv_line_ends(editor.get_text_with_eol()) == code) - monkeypatch.setattr(QMessageBox, "question", staticmethod(ok)) + monkeypatch.setattr(QMessageBox, 'question', + staticmethod(ok)) editor.new() - assert editor.modified == False - assert editor.get_text_with_eol() == "" + assert(editor.modified == False) + assert(editor.get_text_with_eol() == '') - -code_show_topods = """ +code_show_topods = \ +''' import cadquery as cq result = cq.Workplane("XY" ).box(1, 1, 1) show_object(result.val().wrapped) -""" - +''' def test_render_topods(main): qtbot, win = main - obj_tree_comp = win.components["object_tree"] - editor = win.components["editor"] - debugger = win.components["debugger"] - console = win.components["console"] + obj_tree_comp = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + console = win.components['console'] # check that object was rendered - assert obj_tree_comp.CQ.childCount() == 1 + assert(obj_tree_comp.CQ.childCount() == 1) # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) # check that object was rendered usin explicit show_object call editor.set_text(code_show_topods) - debugger._actions["Run"][0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 1 + debugger._actions['Run'][0].triggered.emit() + assert(obj_tree_comp.CQ.childCount() == 1) # test rendering of topods object via console - console.execute("show(result.val().wrapped)") - assert obj_tree_comp.CQ.childCount() == 2 + console.execute('show(result.val().wrapped)') + assert(obj_tree_comp.CQ.childCount() == 2) # test rendering of list of topods object via console - console.execute("show([result.val().wrapped,result.val().wrapped])") - assert obj_tree_comp.CQ.childCount() == 3 + console.execute('show([result.val().wrapped,result.val().wrapped])') + assert(obj_tree_comp.CQ.childCount() == 3) -code_show_shape_list = """ +code_show_shape_list = \ +''' import cadquery as cq result1 = cq.Workplane("XY" ).box(1, 1, 1).val() result2 = cq.Workplane("XY",origin=(0,1,1)).box(1, 1, 1).val() show_object(result1) show_object([result1,result2]) -""" - +''' def test_render_shape_list(main): qtbot, win = main - log = win.components["log"] + log = win.components['log'] - obj_tree_comp = win.components["object_tree"] - editor = win.components["editor"] - debugger = win.components["debugger"] - console = win.components["console"] + obj_tree_comp = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + console = win.components['console'] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) # check that object was rendered usin explicit show_object call editor.set_text(code_show_shape_list) - debugger._actions["Run"][0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 2 + debugger._actions['Run'][0].triggered.emit() + assert(obj_tree_comp.CQ.childCount() == 2) # test rendering of Shape via console - console.execute("show(result1)") - console.execute("show([result1,result2])") - assert obj_tree_comp.CQ.childCount() == 4 + console.execute('show(result1)') + console.execute('show([result1,result2])') + assert(obj_tree_comp.CQ.childCount() == 4) # smoke test exception in show console.execute('show("a")') - -code_show_assy = """import cadquery as cq +code_show_assy = \ +'''import cadquery as cq result1 = cq.Workplane("XY" ).box(3, 3, 0.5) assy = cq.Assembly(result1) show_object(assy) -""" - +''' def test_render_assy(main): qtbot, win = main - obj_tree_comp = win.components["object_tree"] - editor = win.components["editor"] - debugger = win.components["debugger"] - console = win.components["console"] + obj_tree_comp = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + console = win.components['console'] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) # check that object was rendered usin explicit show_object call editor.set_text(code_show_assy) - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() qtbot.wait(500) - assert obj_tree_comp.CQ.childCount() == 1 + assert(obj_tree_comp.CQ.childCount() == 1) # test rendering via console - console.execute("show(assy)") + console.execute('show(assy)') qtbot.wait(500) - assert obj_tree_comp.CQ.childCount() == 2 + assert(obj_tree_comp.CQ.childCount() == 2) - -code_show_ais = """import cadquery as cq +code_show_ais = \ +'''import cadquery as cq from cadquery.occ_impl.assembly import toCAF import OCP @@ -1370,107 +1312,101 @@ def test_render_assy(main): ais = OCP.XCAFPrs.XCAFPrs_AISObject(lab) show_object(ais) -""" - +''' def test_render_ais(main): qtbot, win = main - obj_tree_comp = win.components["object_tree"] - editor = win.components["editor"] - debugger = win.components["debugger"] - console = win.components["console"] + obj_tree_comp = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + console = win.components['console'] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) # check that object was rendered usin explicit show_object call editor.set_text(code_show_ais) - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() qtbot.wait(500) - assert obj_tree_comp.CQ.childCount() == 1 + assert(obj_tree_comp.CQ.childCount() == 1) # test rendering via console - console.execute("show(ais)") + console.execute('show(ais)') qtbot.wait(500) - assert obj_tree_comp.CQ.childCount() == 2 - + assert(obj_tree_comp.CQ.childCount() == 2) -code_show_sketch = """import cadquery as cq +code_show_sketch = \ +'''import cadquery as cq s1 = cq.Sketch().rect(1,1) s2 = cq.Sketch().segment((0,0), (0,3.),"s1") show_object(s1) show_object(s2) -""" - +''' def test_render_sketch(main): qtbot, win = main - obj_tree_comp = win.components["object_tree"] - editor = win.components["editor"] - debugger = win.components["debugger"] - console = win.components["console"] + obj_tree_comp = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + console = win.components['console'] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) # check that object was rendered usin explicit show_object call editor.set_text(code_show_sketch) - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() qtbot.wait(500) - assert obj_tree_comp.CQ.childCount() == 2 + assert(obj_tree_comp.CQ.childCount() == 2) # test rendering via console - console.execute("show(s1); show(s2)") + console.execute('show(s1); show(s2)') qtbot.wait(500) - assert obj_tree_comp.CQ.childCount() == 4 - + assert(obj_tree_comp.CQ.childCount() == 4) def test_window_title(monkeypatch, main): - fname = "test_window_title.py" + fname = 'test_window_title.py' - with open(fname, "w") as f: + with open(fname, 'w') as f: f.write(code) qtbot, win = main - # monkeypatch QFileDialog methods + #monkeypatch QFileDialog methods def filename(*args, **kwargs): return fname, None - monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename)) + monkeypatch.setattr(QFileDialog, 'getOpenFileName', + staticmethod(filename)) win.components["editor"].open() - assert win.windowTitle().endswith(fname) + assert(win.windowTitle().endswith(fname)) # handle a new file win.components["editor"].new() # I don't really care what the title is, as long as it's not a filename - assert not win.windowTitle().endswith(".py") - + assert(not win.windowTitle().endswith('.py')) def test_module_discovery(tmp_path, editor): qtbot, editor = editor - with open(tmp_path.joinpath("main.py"), "w") as f: - f.write("import b") - - assert editor.get_imported_module_paths(str(tmp_path.joinpath("main.py"))) == [] + with open(tmp_path.joinpath('main.py'), 'w') as f: + f.write('import b') - tmp_path.joinpath("b.py").touch() + assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [] - assert editor.get_imported_module_paths(str(tmp_path.joinpath("main.py"))) == [ - str(tmp_path.joinpath("b.py")) - ] + tmp_path.joinpath('b.py').touch() + assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [str(tmp_path.joinpath('b.py'))] def test_launch_syntax_error(tmp_path): @@ -1485,23 +1421,23 @@ def test_launch_syntax_error(tmp_path): editor.load_from_file(inputfile) win.show() - assert win.isVisible() + assert(win.isVisible()) - -code_import_module_makebox = """ +code_import_module_makebox = \ +""" from module_makebox import * z = 1 r = makebox(z) """ -code_module_makebox = """ +code_module_makebox = \ +""" import cadquery as cq def makebox(z): zval = z + 1 return cq.Workplane().box(1, 1, zval) """ - def test_reload_import_handle_error(tmp_path, main): TIMEOUT = 500 @@ -1522,18 +1458,18 @@ def test_reload_import_handle_error(tmp_path, main): # run, verify that no exception was generated editor.load_from_file(script) debugger._actions["Run"][0].triggered.emit() - assert traceback_view.current_exception.text() == "" + assert(traceback_view.current_exception.text() == "") # save the module with an error with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): lines = code_module_makebox.splitlines() - lines.remove(" zval = z + 1") # introduce NameError + lines.remove(" zval = z + 1") # introduce NameError lines = "\n".join(lines) modify_file(lines, module_file) # verify NameError is generated debugger._actions["Run"][0].triggered.emit() - assert "NameError" in traceback_view.current_exception.text() + assert("NameError" in traceback_view.current_exception.text()) # revert the error, verify rerender is triggered with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): @@ -1541,8 +1477,7 @@ def test_reload_import_handle_error(tmp_path, main): # verify that no exception was generated debugger._actions["Run"][0].triggered.emit() - assert traceback_view.current_exception.text() == "" - + assert(traceback_view.current_exception.text() == "") def test_modulefinder(tmp_path, main): @@ -1551,7 +1486,7 @@ def test_modulefinder(tmp_path, main): editor = win.components["editor"] debugger = win.components["debugger"] traceback_view = win.components["traceback_viewer"] - log = win.components["log"] + log = win.components['log'] editor.autoreload(True) editor.preferences["Autoreload: watch imported modules"] = True @@ -1564,58 +1499,56 @@ def test_modulefinder(tmp_path, main): modify_file("import emptydir", script) qtbot.wait(100) - assert "Cannot determine imported modules" in log.toPlainText().splitlines()[-1] - + assert("Cannot determine imported modules" in log.toPlainText().splitlines()[-1]) def test_show_all(main): qtbot, win = main - editor = win.components["editor"] - debugger = win.components["debugger"] - object_tree = win.components["object_tree"] + editor = win.components['editor'] + debugger = win.components['debugger'] + object_tree = win.components['object_tree'] # remove all objects object_tree.removeObjects() - assert object_tree.CQ.childCount() == 0 + assert(object_tree.CQ.childCount() == 0) # add code wtih Shape, Workplane, Assy, Sketch editor.set_text(code_show_all) # Run and check if all are shown - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() - assert object_tree.CQ.childCount() == 4 + assert(object_tree.CQ.childCount() == 4) - -code_randcolor = """import cadquery as cq +code_randcolor = \ +"""import cadquery as cq b = cq.Workplane().box(8, 3, 4) for i in range(10): show_object(b.translate((0,5*i,0)), options=rand_color(alpha=0)) show_object(b.translate((0,5*i,0)), options=rand_color(0, True)) """ - def test_randcolor(main): - + qtbot, win = main - obj_tree_comp = win.components["object_tree"] - editor = win.components["editor"] - debugger = win.components["debugger"] - console = win.components["console"] + obj_tree_comp = win.components['object_tree'] + editor = win.components['editor'] + debugger = win.components['debugger'] + console = win.components['console'] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 0 + assert(obj_tree_comp.CQ.childCount() == 0) # check that object was rendered usin explicit show_object call editor.set_text(code_randcolor) - debugger._actions["Run"][0].triggered.emit() - assert obj_tree_comp.CQ.childCount() == 2 * 10 - + debugger._actions['Run'][0].triggered.emit() + assert(obj_tree_comp.CQ.childCount() == 2*10) -code_show_wo_name = """ +code_show_wo_name = \ +""" import cadquery as cq res = cq.Workplane().box(1,1,1) @@ -1624,29 +1557,28 @@ def test_randcolor(main): show_object(cq.Workplane().box(1,1,1)) """ - def test_show_without_name(main): qtbot, win = main - editor = win.components["editor"] - debugger = win.components["debugger"] - object_tree = win.components["object_tree"] + editor = win.components['editor'] + debugger = win.components['debugger'] + object_tree = win.components['object_tree'] # remove all objects object_tree.removeObjects() - assert object_tree.CQ.childCount() == 0 + assert(object_tree.CQ.childCount() == 0) # add code wtih Shape, Workplane, Assy, Sketch editor.set_text(code_show_wo_name) # Run and check if all are shown - debugger._actions["Run"][0].triggered.emit() + debugger._actions['Run'][0].triggered.emit() - assert object_tree.CQ.childCount() == 2 + assert(object_tree.CQ.childCount() == 2) # Check the name of the first object - assert object_tree.CQ.child(0).text(0) == "res" + assert(object_tree.CQ.child(0).text(0) == "res") # Check that the name of the seconf object is an int int(object_tree.CQ.child(1).text(0)) From dadb1ed7bf4af3523aabfe4e5824f96d4dfea945 Mon Sep 17 00:00:00 2001 From: snoyer Date: Thu, 13 Feb 2025 08:41:14 +0400 Subject: [PATCH 100/134] use signal --- cq_editor/main_window.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index b8169aad..493e53e2 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -27,6 +27,8 @@ class MainWindow(QMainWindow,MainMixin): name = 'CQ-Editor' org = 'CadQuery' + sigStdoutWrite = pyqtSignal(str) + def __init__(self,parent=None, filename=None): super(MainWindow,self).__init__(parent) @@ -147,12 +149,16 @@ def prepare_panes(self): def new_stdout_write(text): original_stdout_write(text) + self.sigStdoutWrite.emit(text) + + sys.stdout.write = new_stdout_write + def append_to_log_viewer(text): log_viewer = self.components['log'] log_viewer.moveCursor(QtGui.QTextCursor.End) log_viewer.insertPlainText(text) - - sys.stdout.write = new_stdout_write + + self.sigStdoutWrite.connect(append_to_log_viewer) def prepare_menubar(self): From d6cbad2e6ae7f8baaf450630bc0ca894c150be8c Mon Sep 17 00:00:00 2001 From: snoyer Date: Thu, 13 Feb 2025 20:32:39 +0400 Subject: [PATCH 101/134] use singleton --- cq_editor/main_window.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 493e53e2..eedc2b90 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -22,13 +22,28 @@ from .preferences import PreferencesWidget +class PrintRedirectorSingleton(QObject): + sigStdoutWrite = pyqtSignal(str) + + def __init__(self): + super().__init__() + + original_stdout_write = sys.stdout.write + + def new_stdout_write(text): + original_stdout_write(text) + self.sigStdoutWrite.emit(text) + + sys.stdout.write = new_stdout_write + + +PRINT_REDIRECTOR = PrintRedirectorSingleton() + class MainWindow(QMainWindow,MainMixin): name = 'CQ-Editor' org = 'CadQuery' - sigStdoutWrite = pyqtSignal(str) - def __init__(self,parent=None, filename=None): super(MainWindow,self).__init__(parent) @@ -144,21 +159,12 @@ def prepare_panes(self): for d in self.docks.values(): d.show() - # Handle the stdout redirection - original_stdout_write = sys.stdout.write - - def new_stdout_write(text): - original_stdout_write(text) - self.sigStdoutWrite.emit(text) - - sys.stdout.write = new_stdout_write - def append_to_log_viewer(text): log_viewer = self.components['log'] log_viewer.moveCursor(QtGui.QTextCursor.End) log_viewer.insertPlainText(text) - - self.sigStdoutWrite.connect(append_to_log_viewer) + + PRINT_REDIRECTOR.sigStdoutWrite.connect(append_to_log_viewer) def prepare_menubar(self): From b6ea9d4fd8024baac83e31ae75eddc522b73b1eb Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 13 Feb 2025 15:50:31 -0500 Subject: [PATCH 102/134] Added lint check --- .github/workflows/lint.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..800b51e2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,16 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: | + python -m pip install --upgrade pip + python -m pip install -e .[dev] + black --diff --check . From 50702f3b9c912d330e2e164664b318c3b6b0ba79 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 13 Feb 2025 16:09:01 -0500 Subject: [PATCH 103/134] Exclude auto-generated icon resource file --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 800b51e2..bccb61fd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,4 +13,4 @@ jobs: - run: | python -m pip install --upgrade pip python -m pip install -e .[dev] - black --diff --check . + black --diff --check . --exclude icons_res.py From a194e6766d98eff64dc78c58489a94b57c762834 Mon Sep 17 00:00:00 2001 From: snoyer Date: Fri, 14 Feb 2025 06:10:53 +0400 Subject: [PATCH 104/134] minor improvements --- cq_editor/main_window.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index eedc2b90..5f3908dd 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -22,7 +22,11 @@ from .preferences import PreferencesWidget -class PrintRedirectorSingleton(QObject): +class _PrintRedirectorSingleton(QObject): + """This class monkey-patches `sys.stdout.write` to emit a signal. + It is instanciated as `.main_window.PRINT_REDIRECTOR` and should not be instanciated again. + """ + sigStdoutWrite = pyqtSignal(str) def __init__(self): @@ -30,14 +34,14 @@ def __init__(self): original_stdout_write = sys.stdout.write - def new_stdout_write(text): - original_stdout_write(text) + def new_stdout_write(text: str): self.sigStdoutWrite.emit(text) + return original_stdout_write(text) sys.stdout.write = new_stdout_write -PRINT_REDIRECTOR = PrintRedirectorSingleton() +PRINT_REDIRECTOR = _PrintRedirectorSingleton() class MainWindow(QMainWindow,MainMixin): From e35bd4c7a98ffa1ce0626dac773df5854fec4d21 Mon Sep 17 00:00:00 2001 From: snoyer Date: Fri, 14 Feb 2025 06:11:16 +0400 Subject: [PATCH 105/134] strip escape sequences --- cq_editor/main_window.py | 4 ++-- tests/test_app.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 5f3908dd..97657b5e 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -13,7 +13,7 @@ from .widgets.traceback_viewer import TracebackPane from .widgets.debugger import Debugger, LocalsView from .widgets.cq_object_inspector import CQObjectInspector -from .widgets.log import LogViewer +from .widgets.log import LogViewer, strip_escape_sequences from . import __version__ from .utils import dock, add_actions, open_url, about_dialog, check_gtihub_for_updates, confirm @@ -166,7 +166,7 @@ def prepare_panes(self): def append_to_log_viewer(text): log_viewer = self.components['log'] log_viewer.moveCursor(QtGui.QTextCursor.End) - log_viewer.insertPlainText(text) + log_viewer.insertPlainText(strip_escape_sequences(text)) PRINT_REDIRECTOR.sigStdoutWrite.connect(append_to_log_viewer) diff --git a/tests/test_app.py b/tests/test_app.py index c82b928e..db0c9a02 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1591,7 +1591,7 @@ def test_print_redirect(main): debugger = win.components["debugger"] log = win.components["log"] - editor.set_text(r"""print('foo\nbar')""") + editor.set_text(r"""print("\x1b[1mfoo\x1b[0m\nbar")""") debugger._actions["Run"][0].triggered.emit() qtbot.wait(100) From c3a161f4d560a57f2db5def5cdbc00c58b2f3baa Mon Sep 17 00:00:00 2001 From: snoyer Date: Fri, 14 Feb 2025 08:21:53 +0400 Subject: [PATCH 106/134] use single entry point for both printing and logging --- cq_editor/main_window.py | 10 ++-------- cq_editor/widgets/log.py | 27 ++++++++++++--------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 97657b5e..4948289d 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,6 +1,5 @@ import sys -from PyQt5 import QtGui from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction) from logbook import Logger @@ -13,7 +12,7 @@ from .widgets.traceback_viewer import TracebackPane from .widgets.debugger import Debugger, LocalsView from .widgets.cq_object_inspector import CQObjectInspector -from .widgets.log import LogViewer, strip_escape_sequences +from .widgets.log import LogViewer from . import __version__ from .utils import dock, add_actions, open_url, about_dialog, check_gtihub_for_updates, confirm @@ -163,12 +162,7 @@ def prepare_panes(self): for d in self.docks.values(): d.show() - def append_to_log_viewer(text): - log_viewer = self.components['log'] - log_viewer.moveCursor(QtGui.QTextCursor.End) - log_viewer.insertPlainText(strip_escape_sequences(text)) - - PRINT_REDIRECTOR.sigStdoutWrite.connect(append_to_log_viewer) + PRINT_REDIRECTOR.sigStdoutWrite.connect(lambda text: self.components['log'].append(text)) def prepare_menubar(self): diff --git a/cq_editor/widgets/log.py b/cq_editor/widgets/log.py index 18d5162c..6d2da57c 100644 --- a/cq_editor/widgets/log.py +++ b/cq_editor/widgets/log.py @@ -1,9 +1,9 @@ import logbook as logging -import sys import re +from PyQt5 import QtGui +from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtWidgets import QPlainTextEdit -from PyQt5 import QtCore from ..mixins import ComponentMixin @@ -16,6 +16,10 @@ def strip_escape_sequences(input_string): return clean_string +class _QtLogHandlerQObject(QObject): + sigRecordEmit = pyqtSignal(str) + + class QtLogHandler(logging.Handler,logging.StringFormatterHandlerMixin): def __init__(self, log_widget,*args,**kwargs): @@ -26,19 +30,11 @@ def __init__(self, log_widget,*args,**kwargs): logging.StringFormatterHandlerMixin.__init__(self,log_format_string) - self.log_widget = log_widget + self._qobject = _QtLogHandlerQObject() + self._qobject.sigRecordEmit.connect(log_widget.append) def emit(self, record): - - msg = self.format(record) - - msg = strip_escape_sequences(msg) - - QtCore.QMetaObject\ - .invokeMethod(self.log_widget, - 'appendPlainText', - QtCore.Qt.QueuedConnection, - QtCore.Q_ARG(str, msg)) + self._qobject.sigRecordEmit.emit(self.format(record) + "\n") class LogViewer(QPlainTextEdit, ComponentMixin): @@ -56,5 +52,6 @@ def __init__(self,*args,**kwargs): self.handler = QtLogHandler(self) def append(self,msg): - - self.appendPlainText(msg) + """Append text to the panel with ANSI escape sequences stipped.""" + self.moveCursor(QtGui.QTextCursor.End) + self.insertPlainText(strip_escape_sequences(msg)) From fff414d3b67d0e3c5da662f4b418fa3329706ec4 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Fri, 14 Feb 2025 09:18:26 -0500 Subject: [PATCH 107/134] Lint check on source code --- bundle.py | 22 +- collect_icons.py | 27 +- cq_editor/__main__.py | 10 +- cq_editor/cq_utils.py | 101 +- cq_editor/cqe_run.py | 10 +- cq_editor/icons.py | 96 +- cq_editor/main_window.py | 455 +++++---- cq_editor/mixins.py | 58 +- cq_editor/preferences.py | 124 +-- cq_editor/utils.py | 203 ++-- cq_editor/widgets/console.py | 39 +- cq_editor/widgets/cq_object_inspector.py | 129 +-- cq_editor/widgets/debugger.py | 302 +++--- cq_editor/widgets/editor.py | 214 ++-- cq_editor/widgets/log.py | 49 +- cq_editor/widgets/object_tree.py | 332 +++--- cq_editor/widgets/occt_widget.py | 162 ++- cq_editor/widgets/traceback_viewer.py | 118 +-- cq_editor/widgets/viewer.py | 380 ++++--- pyinstaller/pyi_rth_fontconfig.py | 6 +- pyinstaller/pyi_rth_occ.py | 8 +- run.py | 8 +- setup.py | 27 +- tests/test_app.py | 1176 ++++++++++++---------- 24 files changed, 2211 insertions(+), 1845 deletions(-) diff --git a/bundle.py b/bundle.py index 6cfaf381..abb6aeb3 100644 --- a/bundle.py +++ b/bundle.py @@ -4,21 +4,21 @@ from shutil import make_archive from cq_editor import __version__ as version -out_p = Path('dist/CQ-editor') +out_p = Path("dist/CQ-editor") out_p.rmtree_p() -build_p = Path('build') +build_p = Path("build") build_p.rmtree_p() system("pyinstaller pyinstaller.spec") -if platform == 'linux': +if platform == "linux": with out_p: - p = Path('.').glob('libpython*')[0] - p.symlink(p.split(".so")[0]+".so") - - make_archive(f'CQ-editor-{version}-linux64','bztar', out_p / '..', 'CQ-editor') - -elif platform == 'win32': - - make_archive(f'CQ-editor-{version}-win64','zip', out_p / '..', 'CQ-editor') + p = Path(".").glob("libpython*")[0] + p.symlink(p.split(".so")[0] + ".so") + + make_archive(f"CQ-editor-{version}-linux64", "bztar", out_p / "..", "CQ-editor") + +elif platform == "win32": + + make_archive(f"CQ-editor-{version}-win64", "zip", out_p / "..", "CQ-editor") diff --git a/collect_icons.py b/collect_icons.py index e252236b..3f8b6b20 100644 --- a/collect_icons.py +++ b/collect_icons.py @@ -2,29 +2,28 @@ from subprocess import call from os import remove -TEMPLATE = \ -''' +TEMPLATE = """ {} -''' +""" -ITEM_TEMPLATE = '{}' +ITEM_TEMPLATE = "{}" -QRC_OUT = 'icons.qrc' -RES_OUT = 'src/icons_res.py' -TOOL = 'pyrcc5' +QRC_OUT = "icons.qrc" +RES_OUT = "src/icons_res.py" +TOOL = "pyrcc5" items = [] -for i in glob('icons/*.svg'): +for i in glob("icons/*.svg"): items.append(ITEM_TEMPLATE.format(i)) - - -qrc_text = TEMPLATE.format('\n'.join(items)) -with open(QRC_OUT,'w') as f: + +qrc_text = TEMPLATE.format("\n".join(items)) + +with open(QRC_OUT, "w") as f: f.write(qrc_text) - -call([TOOL,QRC_OUT,'-o',RES_OUT]) + +call([TOOL, QRC_OUT, "-o", RES_OUT]) remove(QRC_OUT) diff --git a/cq_editor/__main__.py b/cq_editor/__main__.py index 0fc8f700..2298ea56 100644 --- a/cq_editor/__main__.py +++ b/cq_editor/__main__.py @@ -3,18 +3,18 @@ from PyQt5.QtWidgets import QApplication -NAME = 'CQ-editor' +NAME = "CQ-editor" -#need to initialize QApp here, otherewise svg icons do not work on windows -app = QApplication(sys.argv, - applicationName=NAME) +# need to initialize QApp here, otherewise svg icons do not work on windows +app = QApplication(sys.argv, applicationName=NAME) from .main_window import MainWindow + def main(): parser = argparse.ArgumentParser(description=NAME) - parser.add_argument('filename',nargs='?',default=None) + parser.add_argument("filename", nargs="?", default=None) args = parser.parse_args(app.arguments()[1:]) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 2378a82c..88c64483 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -8,8 +8,11 @@ from OCP.XCAFPrs import XCAFPrs_AISObject from OCP.TopoDS import TopoDS_Shape from OCP.AIS import AIS_InteractiveObject, AIS_Shape -from OCP.Quantity import \ - Quantity_TOC_RGB as TOC_RGB, Quantity_Color, Quantity_NOC_GOLD as GOLD +from OCP.Quantity import ( + Quantity_TOC_RGB as TOC_RGB, + Quantity_Color, + Quantity_NOC_GOLD as GOLD, +) from OCP.Graphic3d import Graphic3d_NOM_JADE, Graphic3d_MaterialAspect from PyQt5.QtGui import QColor @@ -25,26 +28,33 @@ def is_cq_obj(obj): return isinstance(obj, (Workplane, Shape, Assembly, Sketch)) -def find_cq_objects(results : dict): +def find_cq_objects(results: dict): - return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if is_cq_obj(v)} + return { + k: SimpleNamespace(shape=v, options={}) + for k, v in results.items() + if is_cq_obj(v) + } -def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch]): +def to_compound( + obj: Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch], +): vals = [] - if isinstance(obj,cq.Workplane): + if isinstance(obj, cq.Workplane): vals.extend(obj.vals()) - elif isinstance(obj,cq.Shape): + elif isinstance(obj, cq.Shape): vals.append(obj) - elif isinstance(obj,list) and isinstance(obj[0],cq.Workplane): - for o in obj: vals.extend(o.vals()) - elif isinstance(obj,list) and isinstance(obj[0],cq.Shape): + elif isinstance(obj, list) and isinstance(obj[0], cq.Workplane): + for o in obj: + vals.extend(o.vals()) + elif isinstance(obj, list) and isinstance(obj[0], cq.Shape): vals.extend(obj) elif isinstance(obj, TopoDS_Shape): vals.append(cq.Shape.cast(obj)) - elif isinstance(obj,list) and isinstance(obj[0],TopoDS_Shape): + elif isinstance(obj, list) and isinstance(obj[0], TopoDS_Shape): vals.extend(cq.Shape.cast(o) for o in obj) elif isinstance(obj, cq.Sketch): if obj._faces: @@ -52,21 +62,32 @@ def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq. else: vals.extend(obj._edges) else: - raise ValueError(f'Invalid type {type(obj)}') + raise ValueError(f"Invalid type {type(obj)}") return cq.Compound.makeCompound(vals) -def to_workplane(obj : cq.Shape): +def to_workplane(obj: cq.Shape): - rv = cq.Workplane('XY') - rv.objects = [obj,] + rv = cq.Workplane("XY") + rv.objects = [ + obj, + ] return rv -def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly, AIS_InteractiveObject], - options={}): +def make_AIS( + obj: Union[ + cq.Workplane, + List[cq.Workplane], + cq.Shape, + List[cq.Shape], + cq.Assembly, + AIS_InteractiveObject, + ], + options={}, +): shape = None @@ -82,28 +103,29 @@ def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Sha set_material(ais, DEFAULT_MATERIAL) set_color(ais, DEFAULT_FACE_COLOR) - if 'alpha' in options: - set_transparency(ais, options['alpha']) - if 'color' in options: - set_color(ais, to_occ_color(options['color'])) - if 'rgba' in options: - r,g,b,a = options['rgba'] - set_color(ais, to_occ_color((r,g,b))) + if "alpha" in options: + set_transparency(ais, options["alpha"]) + if "color" in options: + set_color(ais, to_occ_color(options["color"])) + if "rgba" in options: + r, g, b, a = options["rgba"] + set_color(ais, to_occ_color((r, g, b))) set_transparency(ais, a) - return ais,shape + return ais, shape -def export(obj : Union[cq.Workplane, List[cq.Workplane]], type : str, - file, precision=1e-1): +def export( + obj: Union[cq.Workplane, List[cq.Workplane]], type: str, file, precision=1e-1 +): comp = to_compound(obj) - if type == 'stl': + if type == "stl": comp.exportStl(file, tolerance=precision) - elif type == 'step': + elif type == "step": comp.exportStep(file) - elif type == 'brep': + elif type == "brep": comp.exportBrep(file) @@ -116,17 +138,14 @@ def to_occ_color(color) -> Quantity_Color: elif isinstance(color[0], float): color = QColor.fromRgbF(*color) else: - raise ValueError('Unknown color format') + raise ValueError("Unknown color format") else: color = QColor(color) - return Quantity_Color(color.redF(), - color.greenF(), - color.blueF(), - TOC_RGB) + return Quantity_Color(color.redF(), color.greenF(), color.blueF(), TOC_RGB) -def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: +def get_occ_color(obj: Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: if isinstance(obj, AIS_InteractiveObject): color = Quantity_Color() @@ -137,7 +156,7 @@ def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: return QColor.fromRgbF(color.Red(), color.Green(), color.Blue()) -def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape: +def set_color(ais: AIS_Shape, color: Quantity_Color) -> AIS_Shape: drawer = ais.Attributes() drawer.SetupOwnShadingAspect() @@ -146,7 +165,7 @@ def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape: return ais -def set_material(ais : AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Shape: +def set_material(ais: AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Shape: drawer = ais.Attributes() drawer.SetupOwnShadingAspect() @@ -155,7 +174,7 @@ def set_material(ais : AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Sha return ais -def set_transparency(ais : AIS_Shape, alpha: float) -> AIS_Shape: +def set_transparency(ais: AIS_Shape, alpha: float) -> AIS_Shape: drawer = ais.Attributes() drawer.SetupOwnShadingAspect() @@ -184,13 +203,13 @@ def reload_cq(): reload(cq.occ_impl.exporters.dxf) reload(cq.occ_impl.exporters.amf) reload(cq.occ_impl.exporters.json) - #reload(cq.occ_impl.exporters.assembly) + # reload(cq.occ_impl.exporters.assembly) reload(cq.occ_impl.exporters) reload(cq.assembly) reload(cq) -def is_obj_empty(obj : Union[cq.Workplane,cq.Shape]) -> bool: +def is_obj_empty(obj: Union[cq.Workplane, cq.Shape]) -> bool: rv = False diff --git a/cq_editor/cqe_run.py b/cq_editor/cqe_run.py index bbd79682..038f7d31 100644 --- a/cq_editor/cqe_run.py +++ b/cq_editor/cqe_run.py @@ -1,13 +1,13 @@ import os, sys, asyncio -if 'CASROOT' in os.environ: - del os.environ['CASROOT'] +if "CASROOT" in os.environ: + del os.environ["CASROOT"] -if sys.platform == 'win32': +if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) from cq_editor.__main__ import main -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/cq_editor/icons.py b/cq_editor/icons.py index 572e4a01..4679eaa5 100644 --- a/cq_editor/icons.py +++ b/cq_editor/icons.py @@ -9,52 +9,76 @@ from PyQt5.QtGui import QIcon from . import icons_res -_icons = { - 'app' : QIcon(":/images/icons/cadquery_logo_dark.svg") - } + +_icons = {"app": QIcon(":/images/icons/cadquery_logo_dark.svg")} import qtawesome as qta _icons_specs = { - 'new' : (('fa.file-o',),{}), - 'open' : (('fa.folder-open-o',),{}), + "new": (("fa.file-o",), {}), + "open": (("fa.folder-open-o",), {}), # borrowed from spider-ide - 'autoreload': [('fa.repeat', 'fa.clock-o'), {'options': [{'scale_factor': 0.75, 'offset': (-0.1, -0.1)}, {'scale_factor': 0.5, 'offset': (0.25, 0.25)}]}], - 'save' : (('fa.save',),{}), - 'save_as': (('fa.save','fa.pencil'), - {'options':[{'scale_factor': 1,}, - {'scale_factor': 0.8, - 'offset': (0.2, 0.2)}]}), - 'run' : (('fa.play',),{}), - 'delete' : (('fa.trash',),{}), - 'delete-many' : (('fa.trash','fa.trash',), - {'options' : \ - [{'scale_factor': 0.8, - 'offset': (0.2, 0.2), - 'color': 'gray'}, - {'scale_factor': 0.8}]}), - 'help' : (('fa.life-ring',),{}), - 'about': (('fa.info',),{}), - 'preferences' : (('fa.cogs',),{}), - 'inspect' : (('fa.cubes','fa.search'), - {'options' : \ - [{'scale_factor': 0.8, - 'offset': (0,0), - 'color': 'gray'},{}]}), - 'screenshot' : (('fa.camera',),{}), - 'screenshot-save' : (('fa.save','fa.camera'), - {'options' : \ - [{'scale_factor': 0.8}, - {'scale_factor': 0.8, - 'offset': (.2,.2)}]}), - 'toggle-comment' : (('fa.hashtag',),{}), + "autoreload": [ + ("fa.repeat", "fa.clock-o"), + { + "options": [ + {"scale_factor": 0.75, "offset": (-0.1, -0.1)}, + {"scale_factor": 0.5, "offset": (0.25, 0.25)}, + ] + }, + ], + "save": (("fa.save",), {}), + "save_as": ( + ("fa.save", "fa.pencil"), + { + "options": [ + { + "scale_factor": 1, + }, + {"scale_factor": 0.8, "offset": (0.2, 0.2)}, + ] + }, + ), + "run": (("fa.play",), {}), + "delete": (("fa.trash",), {}), + "delete-many": ( + ( + "fa.trash", + "fa.trash", + ), + { + "options": [ + {"scale_factor": 0.8, "offset": (0.2, 0.2), "color": "gray"}, + {"scale_factor": 0.8}, + ] + }, + ), + "help": (("fa.life-ring",), {}), + "about": (("fa.info",), {}), + "preferences": (("fa.cogs",), {}), + "inspect": ( + ("fa.cubes", "fa.search"), + {"options": [{"scale_factor": 0.8, "offset": (0, 0), "color": "gray"}, {}]}, + ), + "screenshot": (("fa.camera",), {}), + "screenshot-save": ( + ("fa.save", "fa.camera"), + { + "options": [ + {"scale_factor": 0.8}, + {"scale_factor": 0.8, "offset": (0.2, 0.2)}, + ] + }, + ), + "toggle-comment": (("fa.hashtag",), {}), } + def icon(name): if name in _icons: return _icons[name] - args,kwargs = _icons_specs[name] + args, kwargs = _icons_specs[name] - return qta.icon(*args,**kwargs) \ No newline at end of file + return qta.icon(*args, **kwargs) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 4948289d..9b1e7907 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,7 +1,7 @@ import sys from PyQt5.QtCore import QObject, pyqtSignal -from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction) +from PyQt5.QtWidgets import QLabel, QMainWindow, QToolBar, QDockWidget, QAction from logbook import Logger import cadquery as cq @@ -15,7 +15,14 @@ from .widgets.log import LogViewer from . import __version__ -from .utils import dock, add_actions, open_url, about_dialog, check_gtihub_for_updates, confirm +from .utils import ( + dock, + add_actions, + open_url, + about_dialog, + check_gtihub_for_updates, + confirm, +) from .mixins import MainMixin from .icons import icon from .preferences import PreferencesWidget @@ -42,36 +49,38 @@ def new_stdout_write(text: str): PRINT_REDIRECTOR = _PrintRedirectorSingleton() -class MainWindow(QMainWindow,MainMixin): - name = 'CQ-Editor' - org = 'CadQuery' +class MainWindow(QMainWindow, MainMixin): - def __init__(self,parent=None, filename=None): + name = "CQ-Editor" + org = "CadQuery" - super(MainWindow,self).__init__(parent) + def __init__(self, parent=None, filename=None): + + super(MainWindow, self).__init__(parent) MainMixin.__init__(self) - self.setWindowIcon(icon('app')) + self.setWindowIcon(icon("app")) # Windows workaround - makes the correct task bar icon show up. if sys.platform == "win32": import ctypes - myappid = 'cq-editor' # arbitrary string + + myappid = "cq-editor" # arbitrary string ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) self.viewer = OCCViewer(self) self.setCentralWidget(self.viewer.canvas) self.prepare_panes() - self.registerComponent('viewer',self.viewer) + self.registerComponent("viewer", self.viewer) self.prepare_toolbar() self.prepare_menubar() self.prepare_statusbar() self.prepare_actions() - - self.components['object_tree'].addLines() + + self.components["object_tree"].addLines() self.prepare_console() @@ -83,112 +92,110 @@ def __init__(self,parent=None, filename=None): self.restoreWindow() # Let the user know when the file has been modified - self.components['editor'].document().modificationChanged.connect(self.update_window_title) + self.components["editor"].document().modificationChanged.connect( + self.update_window_title + ) if filename: - self.components['editor'].load_from_file(filename) + self.components["editor"].load_from_file(filename) self.restoreComponentState() - def closeEvent(self,event): + def closeEvent(self, event): self.saveWindow() self.savePreferences() self.saveComponentState() - if self.components['editor'].document().isModified(): + if self.components["editor"].document().isModified(): + + rv = confirm(self, "Confirm close", "Close without saving?") - rv = confirm(self, 'Confirm close', 'Close without saving?') - if rv: event.accept() - super(MainWindow,self).closeEvent(event) + super(MainWindow, self).closeEvent(event) else: event.ignore() else: - super(MainWindow,self).closeEvent(event) + super(MainWindow, self).closeEvent(event) def prepare_panes(self): - self.registerComponent('editor', - Editor(self), - lambda c : dock(c, - 'Editor', - self, - defaultArea='left')) - - self.registerComponent('object_tree', - ObjectTree(self), - lambda c: dock(c, - 'Objects', - self, - defaultArea='right')) - - self.registerComponent('console', - ConsoleWidget(self), - lambda c: dock(c, - 'Console', - self, - defaultArea='bottom')) - - self.registerComponent('traceback_viewer', - TracebackPane(self), - lambda c: dock(c, - 'Current traceback', - self, - defaultArea='bottom')) - - self.registerComponent('debugger',Debugger(self)) - - self.registerComponent('variables_viewer',LocalsView(self), - lambda c: dock(c, - 'Variables', - self, - defaultArea='right')) - - self.registerComponent('cq_object_inspector', - CQObjectInspector(self), - lambda c: dock(c, - 'CQ object inspector', - self, - defaultArea='right')) - self.registerComponent('log', - LogViewer(self), - lambda c: dock(c, - 'Log viewer', - self, - defaultArea='bottom')) + self.registerComponent( + "editor", + Editor(self), + lambda c: dock(c, "Editor", self, defaultArea="left"), + ) + + self.registerComponent( + "object_tree", + ObjectTree(self), + lambda c: dock(c, "Objects", self, defaultArea="right"), + ) + + self.registerComponent( + "console", + ConsoleWidget(self), + lambda c: dock(c, "Console", self, defaultArea="bottom"), + ) + + self.registerComponent( + "traceback_viewer", + TracebackPane(self), + lambda c: dock(c, "Current traceback", self, defaultArea="bottom"), + ) + + self.registerComponent("debugger", Debugger(self)) + + self.registerComponent( + "variables_viewer", + LocalsView(self), + lambda c: dock(c, "Variables", self, defaultArea="right"), + ) + + self.registerComponent( + "cq_object_inspector", + CQObjectInspector(self), + lambda c: dock(c, "CQ object inspector", self, defaultArea="right"), + ) + self.registerComponent( + "log", + LogViewer(self), + lambda c: dock(c, "Log viewer", self, defaultArea="bottom"), + ) for d in self.docks.values(): d.show() - PRINT_REDIRECTOR.sigStdoutWrite.connect(lambda text: self.components['log'].append(text)) - + PRINT_REDIRECTOR.sigStdoutWrite.connect( + lambda text: self.components["log"].append(text) + ) def prepare_menubar(self): menu = self.menuBar() - menu_file = menu.addMenu('&File') - menu_edit = menu.addMenu('&Edit') - menu_tools = menu.addMenu('&Tools') - menu_run = menu.addMenu('&Run') - menu_view = menu.addMenu('&View') - menu_help = menu.addMenu('&Help') - - #per component menu elements - menus = {'File' : menu_file, - 'Edit' : menu_edit, - 'Run' : menu_run, - 'Tools': menu_tools, - 'View' : menu_view, - 'Help' : menu_help} + menu_file = menu.addMenu("&File") + menu_edit = menu.addMenu("&Edit") + menu_tools = menu.addMenu("&Tools") + menu_run = menu.addMenu("&Run") + menu_view = menu.addMenu("&View") + menu_help = menu.addMenu("&Help") + + # per component menu elements + menus = { + "File": menu_file, + "Edit": menu_edit, + "Run": menu_run, + "Tools": menu_tools, + "View": menu_view, + "Help": menu_help, + } for comp in self.components.values(): - self.prepare_menubar_component(menus, - comp.menuActions()) + self.prepare_menubar_component(menus, comp.menuActions()) - #global menu elements + # global menu elements menu_view.addSeparator() for d in self.findChildren(QDockWidget): menu_view.addAction(d.toggleViewAction()) @@ -197,133 +204,168 @@ def prepare_menubar(self): for t in self.findChildren(QToolBar): menu_view.addAction(t.toggleViewAction()) - menu_edit.addAction( \ - QAction(icon('toggle-comment'), - 'Toggle Comment', - self, - shortcut='ctrl+/', - triggered=self.components['editor'].toggle_comment)) - menu_edit.addAction( \ - QAction(icon('preferences'), - 'Preferences', - self,triggered=self.edit_preferences)) - - menu_help.addAction( \ - QAction(icon('help'), - 'Documentation', - self,triggered=self.documentation)) - - menu_help.addAction( \ - QAction('CQ documentation', - self,triggered=self.cq_documentation)) - - menu_help.addAction( \ - QAction(icon('about'), - 'About', - self,triggered=self.about)) - - menu_help.addAction( \ - QAction('Check for CadQuery updates', - self,triggered=self.check_for_cq_updates)) - - def prepare_menubar_component(self,menus,comp_menu_dict): - - for name,action in comp_menu_dict.items(): + menu_edit.addAction( + QAction( + icon("toggle-comment"), + "Toggle Comment", + self, + shortcut="ctrl+/", + triggered=self.components["editor"].toggle_comment, + ) + ) + menu_edit.addAction( + QAction( + icon("preferences"), + "Preferences", + self, + triggered=self.edit_preferences, + ) + ) + + menu_help.addAction( + QAction(icon("help"), "Documentation", self, triggered=self.documentation) + ) + + menu_help.addAction( + QAction("CQ documentation", self, triggered=self.cq_documentation) + ) + + menu_help.addAction(QAction(icon("about"), "About", self, triggered=self.about)) + + menu_help.addAction( + QAction( + "Check for CadQuery updates", self, triggered=self.check_for_cq_updates + ) + ) + + def prepare_menubar_component(self, menus, comp_menu_dict): + + for name, action in comp_menu_dict.items(): menus[name].addActions(action) def prepare_toolbar(self): - self.toolbar = QToolBar('Main toolbar',self,objectName='Main toolbar') + self.toolbar = QToolBar("Main toolbar", self, objectName="Main toolbar") for c in self.components.values(): - add_actions(self.toolbar,c.toolbarActions()) + add_actions(self.toolbar, c.toolbarActions()) self.addToolBar(self.toolbar) def prepare_statusbar(self): - self.status_label = QLabel('',parent=self) + self.status_label = QLabel("", parent=self) self.statusBar().insertPermanentWidget(0, self.status_label) def prepare_actions(self): - self.components['debugger'].sigRendered\ - .connect(self.components['object_tree'].addObjects) - self.components['debugger'].sigTraceback\ - .connect(self.components['traceback_viewer'].addTraceback) - self.components['debugger'].sigLocals\ - .connect(self.components['variables_viewer'].update_frame) - self.components['debugger'].sigLocals\ - .connect(self.components['console'].push_vars) - - self.components['object_tree'].sigObjectsAdded[list]\ - .connect(self.components['viewer'].display_many) - self.components['object_tree'].sigObjectsAdded[list,bool]\ - .connect(self.components['viewer'].display_many) - self.components['object_tree'].sigItemChanged.\ - connect(self.components['viewer'].update_item) - self.components['object_tree'].sigObjectsRemoved\ - .connect(self.components['viewer'].remove_items) - self.components['object_tree'].sigCQObjectSelected\ - .connect(self.components['cq_object_inspector'].setObject) - self.components['object_tree'].sigObjectPropertiesChanged\ - .connect(self.components['viewer'].redraw) - self.components['object_tree'].sigAISObjectsSelected\ - .connect(self.components['viewer'].set_selected) - - self.components['viewer'].sigObjectSelected\ - .connect(self.components['object_tree'].handleGraphicalSelection) - - self.components['traceback_viewer'].sigHighlightLine\ - .connect(self.components['editor'].go_to_line) - - self.components['cq_object_inspector'].sigDisplayObjects\ - .connect(self.components['viewer'].display_many) - self.components['cq_object_inspector'].sigRemoveObjects\ - .connect(self.components['viewer'].remove_items) - self.components['cq_object_inspector'].sigShowPlane\ - .connect(self.components['viewer'].toggle_grid) - self.components['cq_object_inspector'].sigShowPlane[bool,float]\ - .connect(self.components['viewer'].toggle_grid) - self.components['cq_object_inspector'].sigChangePlane\ - .connect(self.components['viewer'].set_grid_orientation) - - self.components['debugger'].sigLocalsChanged\ - .connect(self.components['variables_viewer'].update_frame) - self.components['debugger'].sigLineChanged\ - .connect(self.components['editor'].go_to_line) - self.components['debugger'].sigDebugging\ - .connect(self.components['object_tree'].stashObjects) - self.components['debugger'].sigCQChanged\ - .connect(self.components['object_tree'].addObjects) - self.components['debugger'].sigTraceback\ - .connect(self.components['traceback_viewer'].addTraceback) + self.components["debugger"].sigRendered.connect( + self.components["object_tree"].addObjects + ) + self.components["debugger"].sigTraceback.connect( + self.components["traceback_viewer"].addTraceback + ) + self.components["debugger"].sigLocals.connect( + self.components["variables_viewer"].update_frame + ) + self.components["debugger"].sigLocals.connect( + self.components["console"].push_vars + ) + + self.components["object_tree"].sigObjectsAdded[list].connect( + self.components["viewer"].display_many + ) + self.components["object_tree"].sigObjectsAdded[list, bool].connect( + self.components["viewer"].display_many + ) + self.components["object_tree"].sigItemChanged.connect( + self.components["viewer"].update_item + ) + self.components["object_tree"].sigObjectsRemoved.connect( + self.components["viewer"].remove_items + ) + self.components["object_tree"].sigCQObjectSelected.connect( + self.components["cq_object_inspector"].setObject + ) + self.components["object_tree"].sigObjectPropertiesChanged.connect( + self.components["viewer"].redraw + ) + self.components["object_tree"].sigAISObjectsSelected.connect( + self.components["viewer"].set_selected + ) + + self.components["viewer"].sigObjectSelected.connect( + self.components["object_tree"].handleGraphicalSelection + ) + + self.components["traceback_viewer"].sigHighlightLine.connect( + self.components["editor"].go_to_line + ) + + self.components["cq_object_inspector"].sigDisplayObjects.connect( + self.components["viewer"].display_many + ) + self.components["cq_object_inspector"].sigRemoveObjects.connect( + self.components["viewer"].remove_items + ) + self.components["cq_object_inspector"].sigShowPlane.connect( + self.components["viewer"].toggle_grid + ) + self.components["cq_object_inspector"].sigShowPlane[bool, float].connect( + self.components["viewer"].toggle_grid + ) + self.components["cq_object_inspector"].sigChangePlane.connect( + self.components["viewer"].set_grid_orientation + ) + + self.components["debugger"].sigLocalsChanged.connect( + self.components["variables_viewer"].update_frame + ) + self.components["debugger"].sigLineChanged.connect( + self.components["editor"].go_to_line + ) + self.components["debugger"].sigDebugging.connect( + self.components["object_tree"].stashObjects + ) + self.components["debugger"].sigCQChanged.connect( + self.components["object_tree"].addObjects + ) + self.components["debugger"].sigTraceback.connect( + self.components["traceback_viewer"].addTraceback + ) # trigger re-render when file is modified externally or saved - self.components['editor'].triggerRerender \ - .connect(self.components['debugger'].render) - self.components['editor'].sigFilenameChanged\ - .connect(self.handle_filename_change) + self.components["editor"].triggerRerender.connect( + self.components["debugger"].render + ) + self.components["editor"].sigFilenameChanged.connect( + self.handle_filename_change + ) def prepare_console(self): - console = self.components['console'] - obj_tree = self.components['object_tree'] - - #application related items - console.push_vars({'self' : self}) - - #CQ related items - console.push_vars({'show' : obj_tree.addObject, - 'show_object' : obj_tree.addObject, - 'rand_color' : self.components['debugger']._rand_color, - 'cq' : cq, - 'log' : Logger(self.name).info}) + console = self.components["console"] + obj_tree = self.components["object_tree"] + + # application related items + console.push_vars({"self": self}) + + # CQ related items + console.push_vars( + { + "show": obj_tree.addObject, + "show_object": obj_tree.addObject, + "rand_color": self.components["debugger"]._rand_color, + "cq": cq, + "log": Logger(self.name).info, + } + ) def fill_dummy(self): - self.components['editor']\ - .set_text('import cadquery as cq\nresult = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125)\nshow_object(result)') + self.components["editor"].set_text( + 'import cadquery as cq\nresult = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125)\nshow_object(result)' + ) def setup_logging(self): @@ -331,8 +373,8 @@ def setup_logging(self): from logbook import INFO, Logger redirect_logging() - self.components['log'].handler.level = INFO - self.components['log'].handler.push_application() + self.components["log"].handler.level = INFO + self.components["log"].handler.push_application() self._logger = Logger(self.name) @@ -342,36 +384,37 @@ def handle_exception(exc_type, exc_value, exc_traceback): sys.__excepthook__(exc_type, exc_value, exc_traceback) return - self._logger.error("Uncaught exception occurred", - exc_info=(exc_type, exc_value, exc_traceback)) + self._logger.error( + "Uncaught exception occurred", + exc_info=(exc_type, exc_value, exc_traceback), + ) sys.excepthook = handle_exception - def edit_preferences(self): - prefs = PreferencesWidget(self,self.components) + prefs = PreferencesWidget(self, self.components) prefs.exec_() def about(self): about_dialog( self, - f'About CQ-editor', - f'PyQt GUI for CadQuery.\nVersion: {__version__}.\nSource Code: https://github.com/CadQuery/CQ-editor', + f"About CQ-editor", + f"PyQt GUI for CadQuery.\nVersion: {__version__}.\nSource Code: https://github.com/CadQuery/CQ-editor", ) - + def check_for_cq_updates(self): - - check_gtihub_for_updates(self,cq) + + check_gtihub_for_updates(self, cq) def documentation(self): - open_url('https://github.com/CadQuery') + open_url("https://github.com/CadQuery") def cq_documentation(self): - open_url('https://cadquery.readthedocs.io/en/latest/') + open_url("https://cadquery.readthedocs.io/en/latest/") def handle_filename_change(self, fname): @@ -382,9 +425,9 @@ def update_window_title(self, modified): """ Allows updating the window title to show that the document has been modified. """ - title = self.windowTitle().rstrip('*') + title = self.windowTitle().rstrip("*") if modified: - title += '*' + title += "*" self.setWindowTitle(title) diff --git a/cq_editor/mixins.py b/cq_editor/mixins.py index f48f0d23..f614ffe6 100644 --- a/cq_editor/mixins.py +++ b/cq_editor/mixins.py @@ -12,10 +12,11 @@ from PyQt5.QtCore import pyqtSlot, QSettings + class MainMixin(object): - name = 'Main' - org = 'Unknown' + name = "Main" + org = "Unknown" components = {} docks = {} @@ -23,9 +24,9 @@ class MainMixin(object): def __init__(self): - self.settings = QSettings(self.org,self.name) + self.settings = QSettings(self.org, self.name) - def registerComponent(self,name,component,dock=None): + def registerComponent(self, name, component, dock=None): self.components[name] = component @@ -34,38 +35,40 @@ def registerComponent(self,name,component,dock=None): def saveWindow(self): - self.settings.setValue('geometry',self.saveGeometry()) - self.settings.setValue('windowState',self.saveState()) + self.settings.setValue("geometry", self.saveGeometry()) + self.settings.setValue("windowState", self.saveState()) def restoreWindow(self): - if self.settings.value('geometry'): - self.restoreGeometry(self.settings.value('geometry')) - if self.settings.value('windowState'): - self.restoreState(self.settings.value('windowState')) + if self.settings.value("geometry"): + self.restoreGeometry(self.settings.value("geometry")) + if self.settings.value("windowState"): + self.restoreState(self.settings.value("windowState")) def savePreferences(self): settings = self.settings if self.preferences: - settings.setValue('General',self.preferences.saveState()) + settings.setValue("General", self.preferences.saveState()) for comp in (c for c in self.components.values() if c.preferences): - settings.setValue(comp.name,comp.preferences.saveState()) + settings.setValue(comp.name, comp.preferences.saveState()) def restorePreferences(self): settings = self.settings - if self.preferences and settings.value('General'): - self.preferences.restoreState(settings.value('General'), - removeChildren=False) + if self.preferences and settings.value("General"): + self.preferences.restoreState( + settings.value("General"), removeChildren=False + ) for comp in (c for c in self.components.values() if c.preferences): if settings.value(comp.name): - comp.preferences.restoreState(settings.value(comp.name), - removeChildren=False) + comp.preferences.restoreState( + settings.value(comp.name), removeChildren=False + ) def saveComponentState(self): @@ -84,19 +87,16 @@ def restoreComponentState(self): class ComponentMixin(object): - - name = 'Component' + name = "Component" preferences = None _actions = {} - def __init__(self): if self.preferences: - self.preferences.sigTreeStateChanged.\ - connect(self.updatePreferences) - + self.preferences.sigTreeStateChanged.connect(self.updatePreferences) + self._logger = Logger(self.name) def menuActions(self): @@ -106,19 +106,19 @@ def menuActions(self): def toolbarActions(self): if len(self._actions) > 0: - return reduce(add,[a for a in self._actions.values()]) + return reduce(add, [a for a in self._actions.values()]) else: return [] - @pyqtSlot(object,object) - def updatePreferences(self,*args): + @pyqtSlot(object, object) + def updatePreferences(self, *args): pass - def saveComponentState(self,store): + def saveComponentState(self, store): pass - def restoreComponentState(self,store): + def restoreComponentState(self, store): - pass \ No newline at end of file + pass diff --git a/cq_editor/preferences.py b/cq_editor/preferences.py index f312caae..4591a6d2 100644 --- a/cq_editor/preferences.py +++ b/cq_editor/preferences.py @@ -1,5 +1,4 @@ -from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, - QStackedWidget, QDialog) +from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QStackedWidget, QDialog from PyQt5.QtCore import pyqtSlot, Qt from pyqtgraph.parametertree import ParameterTree @@ -8,77 +7,90 @@ class PreferencesTreeItem(QTreeWidgetItem): - - def __init__(self,name,widget,): - - super(PreferencesTreeItem,self).__init__(name) + + def __init__( + self, + name, + widget, + ): + + super(PreferencesTreeItem, self).__init__(name) self.widget = widget + class PreferencesWidget(QDialog): - - def __init__(self,parent,components): - - super(PreferencesWidget,self).__init__( - parent, - Qt.Window | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint, - windowTitle='Preferences') - + + def __init__(self, parent, components): + + super(PreferencesWidget, self).__init__( + parent, + Qt.Window | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint, + windowTitle="Preferences", + ) + self.stacked = QStackedWidget(self) - self.preferences_tree = QTreeWidget(self, - headerHidden=True, - itemsExpandable=False, - rootIsDecorated=False, - columnCount=1) - + self.preferences_tree = QTreeWidget( + self, + headerHidden=True, + itemsExpandable=False, + rootIsDecorated=False, + columnCount=1, + ) + self.root = self.preferences_tree.invisibleRootItem() - - self.add('General', - parent) - + + self.add("General", parent) + for v in parent.components.values(): - self.add(v.name,v) - - self.splitter = splitter((self.preferences_tree,self.stacked),(2,5)) - layout(self,(self.splitter,),self) - + self.add(v.name, v) + + self.splitter = splitter((self.preferences_tree, self.stacked), (2, 5)) + layout(self, (self.splitter,), self) + self.preferences_tree.currentItemChanged.connect(self.handleSelection) - def add(self,name,component): - + def add(self, name, component): + if component.preferences: widget = ParameterTree() widget.setHeaderHidden(True) - widget.setParameters(component.preferences,showTop=False) - self.root.addChild(PreferencesTreeItem((name,), - widget)) - + widget.setParameters(component.preferences, showTop=False) + self.root.addChild(PreferencesTreeItem((name,), widget)) + self.stacked.addWidget(widget) # PyQtGraph is not setting items in drop down lists properly, so we do it manually for child in component.preferences.children(): # Fill the editor color scheme drop down list - if child.name() == 'Color scheme': - child.setLimits(['Spyder','Monokai','Zenburn']) + if child.name() == "Color scheme": + child.setLimits(["Spyder", "Monokai", "Zenburn"]) # Fill the camera projection type - elif child.name() == 'Projection Type': - child.setLimits(['Orthographic', - 'Perspective', - 'Stereo', - 'MonoLeftEye', - 'MonoRightEye']) + elif child.name() == "Projection Type": + child.setLimits( + [ + "Orthographic", + "Perspective", + "Stereo", + "MonoLeftEye", + "MonoRightEye", + ] + ) # Fill the stereo mode, or lack thereof - elif child.name() == 'Stereo Mode': - child.setLimits(['QuadBuffer', - 'Anaglyph', - 'RowInterlaced', - 'ColumnInterlaced', - 'ChessBoard', - 'SideBySide', - 'OverUnder']) - - @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem) - def handleSelection(self,item,*args): - + elif child.name() == "Stereo Mode": + child.setLimits( + [ + "QuadBuffer", + "Anaglyph", + "RowInterlaced", + "ColumnInterlaced", + "ChessBoard", + "SideBySide", + "OverUnder", + ] + ) + + @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) + def handleSelection(self, item, *args): + if item: self.stacked.setCurrentWidget(item.widget) - diff --git a/cq_editor/utils.py b/cq_editor/utils.py index 6e4cebcd..dde99ff0 100644 --- a/cq_editor/utils.py +++ b/cq_editor/utils.py @@ -7,17 +7,23 @@ from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QFileDialog, QMessageBox -DOCK_POSITIONS = {'right' : QtCore.Qt.RightDockWidgetArea, - 'left' : QtCore.Qt.LeftDockWidgetArea, - 'top' : QtCore.Qt.TopDockWidgetArea, - 'bottom' : QtCore.Qt.BottomDockWidgetArea} - -def layout(parent,items, - top_widget = None, - layout_type = QtWidgets.QVBoxLayout, - margin = 2, - spacing = 0): - +DOCK_POSITIONS = { + "right": QtCore.Qt.RightDockWidgetArea, + "left": QtCore.Qt.LeftDockWidgetArea, + "top": QtCore.Qt.TopDockWidgetArea, + "bottom": QtCore.Qt.BottomDockWidgetArea, +} + + +def layout( + parent, + items, + top_widget=None, + layout_type=QtWidgets.QVBoxLayout, + margin=2, + spacing=0, +): + if not top_widget: top_widget = QtWidgets.QWidget(parent) top_widget_was_none = True @@ -25,110 +31,133 @@ def layout(parent,items, top_widget_was_none = False layout = layout_type(top_widget) top_widget.setLayout(layout) - - for item in items: layout.addWidget(item) + + for item in items: + layout.addWidget(item) layout.setSpacing(spacing) - layout.setContentsMargins(margin,margin,margin,margin) - + layout.setContentsMargins(margin, margin, margin, margin) + if top_widget_was_none: return top_widget else: return layout - -def splitter(items, - stretch_factors = None, - orientation=QtCore.Qt.Horizontal): - + + +def splitter(items, stretch_factors=None, orientation=QtCore.Qt.Horizontal): + sp = QtWidgets.QSplitter(orientation) - - for item in items: sp.addWidget(item) - + + for item in items: + sp.addWidget(item) + if stretch_factors: - for i,s in enumerate(stretch_factors): - sp.setStretchFactor(i,s) - - + for i, s in enumerate(stretch_factors): + sp.setStretchFactor(i, s) + return sp -def dock(widget, - title, - parent, - allowedAreas = QtCore.Qt.AllDockWidgetAreas, - defaultArea = 'right', - name=None, - icon = None): - - dock = QtWidgets.QDockWidget(title,parent,objectName=title) - - if name: dock.setObjectName(name) - if icon: dock.toggleViewAction().setIcon(icon) - + +def dock( + widget, + title, + parent, + allowedAreas=QtCore.Qt.AllDockWidgetAreas, + defaultArea="right", + name=None, + icon=None, +): + + dock = QtWidgets.QDockWidget(title, parent, objectName=title) + + if name: + dock.setObjectName(name) + if icon: + dock.toggleViewAction().setIcon(icon) + dock.setAllowedAreas(allowedAreas) dock.setWidget(widget) action = dock.toggleViewAction() action.setText(title) - - dock.setFeatures(QtWidgets.QDockWidget.DockWidgetFeatures(\ - QtWidgets.QDockWidget.AllDockWidgetFeatures)) - - parent.addDockWidget(DOCK_POSITIONS[defaultArea], - dock) - + + dock.setFeatures( + QtWidgets.QDockWidget.DockWidgetFeatures( + QtWidgets.QDockWidget.AllDockWidgetFeatures + ) + ) + + parent.addDockWidget(DOCK_POSITIONS[defaultArea], dock) + return dock -def add_actions(menu,actions): - + +def add_actions(menu, actions): + if len(actions) > 0: menu.addActions(actions) menu.addSeparator() - + + def open_url(url): - - QDesktopServices.openUrl(QUrl(url)) - -def about_dialog(parent,title,text): - - QtWidgets.QMessageBox.about(parent,title,text) - + + QDesktopServices.openUrl(QUrl(url)) + + +def about_dialog(parent, title, text): + + QtWidgets.QMessageBox.about(parent, title, text) + + def get_save_filename(suffix): - - rv,_ = QFileDialog.getSaveFileName(filter='*.{}'.format(suffix)) - if rv != '' and not rv.endswith(suffix): rv += '.'+suffix - + + rv, _ = QFileDialog.getSaveFileName(filter="*.{}".format(suffix)) + if rv != "" and not rv.endswith(suffix): + rv += "." + suffix + return rv + def get_open_filename(suffix, curr_dir): - - rv,_ = QFileDialog.getOpenFileName(directory=curr_dir, filter='*.{}'.format(suffix)) - if rv != '' and not rv.endswith(suffix): rv += '.'+suffix - + + rv, _ = QFileDialog.getOpenFileName( + directory=curr_dir, filter="*.{}".format(suffix) + ) + if rv != "" and not rv.endswith(suffix): + rv += "." + suffix + return rv -def check_gtihub_for_updates(parent, - mod, - github_org='cadquery', - github_proj='cadquery'): - - url = f'https://api.github.com/repos/{github_org}/{github_proj}/releases' + +def check_gtihub_for_updates( + parent, mod, github_org="cadquery", github_proj="cadquery" +): + + url = f"https://api.github.com/repos/{github_org}/{github_proj}/releases" resp = requests.get(url).json() - - newer = [el['tag_name'] for el in resp if not el['draft'] and \ - parse_version(el['tag_name']) > parse_version(mod.__version__)] - + + newer = [ + el["tag_name"] + for el in resp + if not el["draft"] + and parse_version(el["tag_name"]) > parse_version(mod.__version__) + ] + if newer: - title='Updates available' - text=f'There are newer versions of {github_proj} ' \ - f'available on github:\n' + '\n'.join(newer) - + title = "Updates available" + text = ( + f"There are newer versions of {github_proj} " + f"available on github:\n" + "\n".join(newer) + ) + else: - title='No updates available' - text=f'You are already using the latest version of {github_proj}' - - QtWidgets.QMessageBox.about(parent,title,text) - -def confirm(parent,title,msg): - + title = "No updates available" + text = f"You are already using the latest version of {github_proj}" + + QtWidgets.QMessageBox.about(parent, title, text) + + +def confirm(parent, title, msg): + rv = QMessageBox.question(parent, title, msg, QMessageBox.Yes, QMessageBox.No) - + return True if rv == QMessageBox.Yes else False diff --git a/cq_editor/widgets/console.py b/cq_editor/widgets/console.py index 77fd1dcc..3ed51a74 100644 --- a/cq_editor/widgets/console.py +++ b/cq_editor/widgets/console.py @@ -6,22 +6,23 @@ from ..mixins import ComponentMixin -class ConsoleWidget(RichJupyterWidget,ComponentMixin): - - name = 'Console' + +class ConsoleWidget(RichJupyterWidget, ComponentMixin): + + name = "Console" def __init__(self, customBanner=None, namespace=dict(), *args, **kwargs): super(ConsoleWidget, self).__init__(*args, **kwargs) -# if not customBanner is None: -# self.banner = customBanner + # if not customBanner is None: + # self.banner = customBanner self.font_size = 6 self.kernel_manager = kernel_manager = QtInProcessKernelManager() kernel_manager.start_kernel(show_banner=False) - kernel_manager.kernel.gui = 'qt' + kernel_manager.kernel.gui = "qt" kernel_manager.kernel.shell.banner1 = "" - + self.kernel_client = kernel_client = self._kernel_manager.client() kernel_client.start_channels() @@ -31,9 +32,9 @@ def stop(): QApplication.instance().exit() self.exit_requested.connect(stop) - + self.clear() - + self.push_vars(namespace) @pyqtSlot(dict) @@ -50,7 +51,6 @@ def clear(self): """ self._control.clear() - def print_text(self, text): """ Prints some plain text to the console @@ -62,20 +62,19 @@ def execute_command(self, command): Execute a command in the frame of the console widget """ self._execute(command, False) - + def _banner_default(self): - - return '' - + return "" + + if __name__ == "__main__": - - + import sys - + app = QApplication(sys.argv) - - console = ConsoleWidget(customBanner='IPython console test') + + console = ConsoleWidget(customBanner="IPython console test") console.show() - + sys.exit(app.exec_()) diff --git a/cq_editor/widgets/cq_object_inspector.py b/cq_editor/widgets/cq_object_inspector.py index c8c3d37c..b9faac01 100644 --- a/cq_editor/widgets/cq_object_inspector.py +++ b/cq_editor/widgets/cq_object_inspector.py @@ -10,63 +10,70 @@ from ..icons import icon - class CQChildItem(QTreeWidgetItem): - - def __init__(self,cq_item,**kwargs): - - super(CQChildItem,self).\ - __init__([type(cq_item).__name__,str(cq_item)],**kwargs) - + + def __init__(self, cq_item, **kwargs): + + super(CQChildItem, self).__init__( + [type(cq_item).__name__, str(cq_item)], **kwargs + ) + self.cq_item = cq_item + class CQStackItem(QTreeWidgetItem): - - def __init__(self,name,workplane=None,**kwargs): - - super(CQStackItem,self).__init__([name,''],**kwargs) - + + def __init__(self, name, workplane=None, **kwargs): + + super(CQStackItem, self).__init__([name, ""], **kwargs) + self.workplane = workplane -class CQObjectInspector(QTreeWidget,ComponentMixin): - - name = 'CQ Object Inspector' - +class CQObjectInspector(QTreeWidget, ComponentMixin): + + name = "CQ Object Inspector" + sigRemoveObjects = pyqtSignal(list) - sigDisplayObjects = pyqtSignal(list,bool) - sigShowPlane = pyqtSignal([bool],[bool,float]) + sigDisplayObjects = pyqtSignal(list, bool) + sigShowPlane = pyqtSignal([bool], [bool, float]) sigChangePlane = pyqtSignal(gp_Ax3) - - def __init__(self,parent): - - super(CQObjectInspector,self).__init__(parent) + + def __init__(self, parent): + + super(CQObjectInspector, self).__init__(parent) self.setHeaderHidden(False) self.setRootIsDecorated(True) self.setContextMenuPolicy(Qt.ActionsContextMenu) self.setColumnCount(2) - self.setHeaderLabels(['Type','Value']) - + self.setHeaderLabels(["Type", "Value"]) + self.root = self.invisibleRootItem() self.inspected_items = [] - - self._toolbar_actions = \ - [QAction(icon('inspect'),'Inspect CQ object',self,\ - toggled=self.inspect,checkable=True)] - + + self._toolbar_actions = [ + QAction( + icon("inspect"), + "Inspect CQ object", + self, + toggled=self.inspect, + checkable=True, + ) + ] + self.addActions(self._toolbar_actions) - + def menuActions(self): - - return {'Tools' : self._toolbar_actions} - + + return {"Tools": self._toolbar_actions} + def toolbarActions(self): - + return self._toolbar_actions - + @pyqtSlot(bool) - def inspect(self,value): - + def inspect(self, value): + if value: self.itemSelectionChanged.connect(self.handleSelection) self.itemSelectionChanged.emit() @@ -74,56 +81,54 @@ def inspect(self,value): self.itemSelectionChanged.disconnect(self.handleSelection) self.sigRemoveObjects.emit(self.inspected_items) self.sigShowPlane.emit(False) - - @pyqtSlot() + + @pyqtSlot() def handleSelection(self): - + inspected_items = self.inspected_items self.sigRemoveObjects.emit(inspected_items) inspected_items.clear() - + items = self.selectedItems() if len(items) == 0: return - + item = items[-1] if type(item) is CQStackItem: cq_plane = item.workplane.plane dim = item.workplane.largestDimension() - plane = gp_Ax3(cq_plane.origin.toPnt(), - cq_plane.zDir.toDir(), - cq_plane.xDir.toDir()) + plane = gp_Ax3( + cq_plane.origin.toPnt(), cq_plane.zDir.toDir(), cq_plane.xDir.toDir() + ) self.sigChangePlane.emit(plane) - self.sigShowPlane[bool,float].emit(True,dim) - + self.sigShowPlane[bool, float].emit(True, dim) + for child in (item.child(i) for i in range(item.childCount())): obj = child.cq_item - if hasattr(obj,'wrapped') and type(obj) != Vector: + if hasattr(obj, "wrapped") and type(obj) != Vector: ais = AIS_ColoredShape(obj.wrapped) inspected_items.append(ais) - + else: self.sigShowPlane.emit(False) obj = item.cq_item - if hasattr(obj,'wrapped') and type(obj) != Vector: + if hasattr(obj, "wrapped") and type(obj) != Vector: ais = AIS_ColoredShape(obj.wrapped) inspected_items.append(ais) - - self.sigDisplayObjects.emit(inspected_items,False) - + + self.sigDisplayObjects.emit(inspected_items, False) + @pyqtSlot(object) - def setObject(self,cq_obj): - + def setObject(self, cq_obj): + self.root.takeChildren() - + # iterate through parent objects if they exist - while getattr(cq_obj, 'parent', None): - current_frame = CQStackItem(str(cq_obj.plane.origin),workplane=cq_obj) + while getattr(cq_obj, "parent", None): + current_frame = CQStackItem(str(cq_obj.plane.origin), workplane=cq_obj) self.root.addChild(current_frame) - + for obj in cq_obj.objects: current_frame.addChild(CQChildItem(obj)) - + cq_obj = cq_obj.parent - - \ No newline at end of file diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 70d5795f..b911f55d 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -8,19 +8,26 @@ import cadquery as cq from PyQt5 import QtCore -from PyQt5.QtCore import Qt, QObject, pyqtSlot, pyqtSignal, QEventLoop, QAbstractTableModel +from PyQt5.QtCore import ( + Qt, + QObject, + pyqtSlot, + pyqtSignal, + QEventLoop, + QAbstractTableModel, +) from PyQt5.QtWidgets import QAction, QTableView from logbook import info from path import Path from pyqtgraph.parametertree import Parameter from spyder.utils.icon_manager import icon -from random import randrange as rrr,seed +from random import randrange as rrr, seed from ..cq_utils import find_cq_objects, reload_cq from ..mixins import ComponentMixin -DUMMY_FILE = '' +DUMMY_FILE = "" class DbgState(Enum): @@ -30,35 +37,39 @@ class DbgState(Enum): STEP_IN = auto() RETURN = auto() + class DbgEevent(object): - LINE = 'line' - CALL = 'call' - RETURN = 'return' + LINE = "line" + CALL = "call" + RETURN = "return" + class LocalsModel(QAbstractTableModel): - HEADER = ('Name','Type', 'Value') + HEADER = ("Name", "Type", "Value") - def __init__(self,parent): + def __init__(self, parent): - super(LocalsModel,self).__init__(parent) + super(LocalsModel, self).__init__(parent) self.frame = None - def update_frame(self,frame): - - self.frame = \ - [(k,type(v).__name__, str(v)) for k,v in frame.items() if not k.startswith('_')] + def update_frame(self, frame): + self.frame = [ + (k, type(v).__name__, str(v)) + for k, v in frame.items() + if not k.startswith("_") + ] - def rowCount(self,parent=QtCore.QModelIndex()): + def rowCount(self, parent=QtCore.QModelIndex()): if self.frame: return len(self.frame) else: return 0 - def columnCount(self,parent=QtCore.QModelIndex()): + def columnCount(self, parent=QtCore.QModelIndex()): return 3 @@ -76,13 +87,13 @@ def data(self, index, role): return QtCore.QVariant() -class LocalsView(QTableView,ComponentMixin): +class LocalsView(QTableView, ComponentMixin): - name = 'Variables' + name = "Variables" - def __init__(self,parent): + def __init__(self, parent): - super(LocalsView,self).__init__(parent) + super(LocalsView, self).__init__(parent) ComponentMixin.__init__(self) header = self.horizontalHeader() @@ -92,98 +103,109 @@ def __init__(self,parent): vheader.setVisible(False) @pyqtSlot(dict) - def update_frame(self,frame): + def update_frame(self, frame): model = LocalsModel(self) model.update_frame(frame) self.setModel(model) -class Debugger(QObject,ComponentMixin): - name = 'Debugger' +class Debugger(QObject, ComponentMixin): - preferences = Parameter.create(name='Preferences',children=[ - {'name': 'Reload CQ', 'type': 'bool', 'value': False}, - {'name': 'Add script dir to path','type': 'bool', 'value': True}, - {'name': 'Change working dir to script dir','type': 'bool', 'value': True}, - {'name': 'Reload imported modules', 'type': 'bool', 'value': True}, - ]) + name = "Debugger" + preferences = Parameter.create( + name="Preferences", + children=[ + {"name": "Reload CQ", "type": "bool", "value": False}, + {"name": "Add script dir to path", "type": "bool", "value": True}, + {"name": "Change working dir to script dir", "type": "bool", "value": True}, + {"name": "Reload imported modules", "type": "bool", "value": True}, + ], + ) sigRendered = pyqtSignal(dict) sigLocals = pyqtSignal(dict) - sigTraceback = pyqtSignal(object,str) + sigTraceback = pyqtSignal(object, str) sigFrameChanged = pyqtSignal(object) sigLineChanged = pyqtSignal(int) sigLocalsChanged = pyqtSignal(dict) - sigCQChanged = pyqtSignal(dict,bool) + sigCQChanged = pyqtSignal(dict, bool) sigDebugging = pyqtSignal(bool) - _frames : List[FrameType] - _stop_debugging : bool + _frames: List[FrameType] + _stop_debugging: bool - def __init__(self,parent): + def __init__(self, parent): - super(Debugger,self).__init__(parent) + super(Debugger, self).__init__(parent) ComponentMixin.__init__(self) self.inner_event_loop = QEventLoop(self) - self._actions = \ - {'Run' : [QAction(icon('run'), - 'Render', - self, - shortcut='F5', - triggered=self.render), - QAction(icon('debug'), - 'Debug', - self, - checkable=True, - shortcut='ctrl+F5', - triggered=self.debug), - QAction(icon('arrow-step-over'), - 'Step', - self, - shortcut='ctrl+F10', - triggered=lambda: self.debug_cmd(DbgState.STEP)), - QAction(icon('arrow-step-in'), - 'Step in', - self, - shortcut='ctrl+F11', - triggered=lambda: self.debug_cmd(DbgState.STEP_IN)), - QAction(icon('arrow-continue'), - 'Continue', - self, - shortcut='ctrl+F12', - triggered=lambda: self.debug_cmd(DbgState.CONT)) - ]} + self._actions = { + "Run": [ + QAction( + icon("run"), "Render", self, shortcut="F5", triggered=self.render + ), + QAction( + icon("debug"), + "Debug", + self, + checkable=True, + shortcut="ctrl+F5", + triggered=self.debug, + ), + QAction( + icon("arrow-step-over"), + "Step", + self, + shortcut="ctrl+F10", + triggered=lambda: self.debug_cmd(DbgState.STEP), + ), + QAction( + icon("arrow-step-in"), + "Step in", + self, + shortcut="ctrl+F11", + triggered=lambda: self.debug_cmd(DbgState.STEP_IN), + ), + QAction( + icon("arrow-continue"), + "Continue", + self, + shortcut="ctrl+F12", + triggered=lambda: self.debug_cmd(DbgState.CONT), + ), + ] + } self._frames = [] self._stop_debugging = False def get_current_script(self): - return self.parent().components['editor'].get_text_with_eol() - + return self.parent().components["editor"].get_text_with_eol() + def get_current_script_path(self): - + filename = self.parent().components["editor"].filename if filename: return Path(filename).absolute() def get_breakpoints(self): - return self.parent().components['editor'].debugger.get_breakpoints() + return self.parent().components["editor"].debugger.get_breakpoints() def compile_code(self, cq_script, cq_script_path=None): try: - module = ModuleType('__cq_main__') + module = ModuleType("__cq_main__") if cq_script_path: module.__dict__["__file__"] = cq_script_path - cq_code = compile(cq_script, DUMMY_FILE, 'exec') + cq_code = compile(cq_script, DUMMY_FILE, "exec") return cq_code, module except Exception: self.sigTraceback.emit(sys.exc_info(), cq_script) @@ -194,100 +216,103 @@ def _exec(self, code, locals_dict, globals_dict): with ExitStack() as stack: p = (self.get_current_script_path() or Path("")).absolute().dirname() - if self.preferences['Add script dir to path'] and p.exists(): - sys.path.insert(0,p) + if self.preferences["Add script dir to path"] and p.exists(): + sys.path.insert(0, p) stack.callback(sys.path.remove, p) - if self.preferences['Change working dir to script dir'] and p.exists(): + if self.preferences["Change working dir to script dir"] and p.exists(): stack.enter_context(p) - if self.preferences['Reload imported modules']: + if self.preferences["Reload imported modules"]: stack.enter_context(module_manager()) exec(code, locals_dict, globals_dict) @staticmethod - def _rand_color(alpha = 0., cfloat=False): - #helper function to generate a random color dict - #for CQ-editor's show_object function + def _rand_color(alpha=0.0, cfloat=False): + # helper function to generate a random color dict + # for CQ-editor's show_object function lower = 10 - upper = 100 #not too high to keep color brightness in check - if cfloat: #for two output types depending on need + upper = 100 # not too high to keep color brightness in check + if cfloat: # for two output types depending on need return ( - (rrr(lower,upper)/255), - (rrr(lower,upper)/255), - (rrr(lower,upper)/255), - alpha, - ) - return {"alpha": alpha, - "color": ( - rrr(lower,upper), - rrr(lower,upper), - rrr(lower,upper), - )} - - def _inject_locals(self,module): + (rrr(lower, upper) / 255), + (rrr(lower, upper) / 255), + (rrr(lower, upper) / 255), + alpha, + ) + return { + "alpha": alpha, + "color": ( + rrr(lower, upper), + rrr(lower, upper), + rrr(lower, upper), + ), + } + + def _inject_locals(self, module): cq_objects = {} - def _show_object(obj,name=None, options={}): + def _show_object(obj, name=None, options={}): if name: - cq_objects.update({name : SimpleNamespace(shape=obj,options=options)}) + cq_objects.update({name: SimpleNamespace(shape=obj, options=options)}) else: - #get locals of the enclosing scope + # get locals of the enclosing scope d = currentframe().f_back.f_locals - #try to find the name + # try to find the name try: name = list(d.keys())[list(d.values()).index(obj)] except ValueError: - #use id if not found + # use id if not found name = str(id(obj)) - cq_objects.update({name : SimpleNamespace(shape=obj,options=options)}) + cq_objects.update({name: SimpleNamespace(shape=obj, options=options)}) - def _debug(obj,name=None): + def _debug(obj, name=None): - _show_object(obj,name,options=dict(color='red',alpha=0.2)) + _show_object(obj, name, options=dict(color="red", alpha=0.2)) - module.__dict__['show_object'] = _show_object - module.__dict__['debug'] = _debug - module.__dict__['rand_color'] = self._rand_color - module.__dict__['log'] = lambda x: info(str(x)) - module.__dict__['cq'] = cq + module.__dict__["show_object"] = _show_object + module.__dict__["debug"] = _debug + module.__dict__["rand_color"] = self._rand_color + module.__dict__["log"] = lambda x: info(str(x)) + module.__dict__["cq"] = cq - return cq_objects, set(module.__dict__)-{'cq'} + return cq_objects, set(module.__dict__) - {"cq"} - def _cleanup_locals(self,module,injected_names): + def _cleanup_locals(self, module, injected_names): - for name in injected_names: module.__dict__.pop(name) + for name in injected_names: + module.__dict__.pop(name) @pyqtSlot(bool) def render(self): seed(59798267586177) - if self.preferences['Reload CQ']: + if self.preferences["Reload CQ"]: reload_cq() cq_script = self.get_current_script() cq_script_path = self.get_current_script_path() - cq_code,module = self.compile_code(cq_script, cq_script_path) + cq_code, module = self.compile_code(cq_script, cq_script_path) - if cq_code is None: return + if cq_code is None: + return - cq_objects,injected_names = self._inject_locals(module) + cq_objects, injected_names = self._inject_locals(module) try: self._exec(cq_code, module.__dict__, module.__dict__) - #remove the special methods - self._cleanup_locals(module,injected_names) + # remove the special methods + self._cleanup_locals(module, injected_names) - #collect all CQ objects if no explicit show_object was called + # collect all CQ objects if no explicit show_object was called if len(cq_objects) == 0: cq_objects = find_cq_objects(module.__dict__) self.sigRendered.emit(cq_objects) - self.sigTraceback.emit(None, - cq_script) + self.sigTraceback.emit(None, cq_script) self.sigLocals.emit(module.__dict__) except Exception: exc_info = sys.exc_info() @@ -296,10 +321,10 @@ def render(self): @property def breakpoints(self): - return [ el[0] for el in self.get_breakpoints()] + return [el[0] for el in self.get_breakpoints()] @pyqtSlot(bool) - def debug(self,value): + def debug(self, value): # used to stop the debugging session early self._stop_debugging = False @@ -312,39 +337,37 @@ def debug(self,value): self.script = self.get_current_script() cq_script_path = self.get_current_script_path() - code,module = self.compile_code(self.script, cq_script_path) + code, module = self.compile_code(self.script, cq_script_path) if code is None: self.sigDebugging.emit(False) - self._actions['Run'][1].setChecked(False) + self._actions["Run"][1].setChecked(False) return - cq_objects,injected_names = self._inject_locals(module) + cq_objects, injected_names = self._inject_locals(module) - #clear possible traceback - self.sigTraceback.emit(None, - self.script) + # clear possible traceback + self.sigTraceback.emit(None, self.script) try: sys.settrace(self.trace_callback) - exec(code,module.__dict__,module.__dict__) + exec(code, module.__dict__, module.__dict__) except BdbQuit: pass except Exception: exc_info = sys.exc_info() sys.last_traceback = exc_info[-1] - self.sigTraceback.emit(exc_info, - self.script) + self.sigTraceback.emit(exc_info, self.script) finally: sys.settrace(previous_trace) self.sigDebugging.emit(False) - self._actions['Run'][1].setChecked(False) + self._actions["Run"][1].setChecked(False) if len(cq_objects) == 0: cq_objects = find_cq_objects(module.__dict__) self.sigRendered.emit(cq_objects) - self._cleanup_locals(module,injected_names) + self._cleanup_locals(module, injected_names) self.sigLocals.emit(module.__dict__) self._frames = [] @@ -353,32 +376,33 @@ def debug(self,value): self._stop_debugging = True self.inner_event_loop.exit(0) - def debug_cmd(self,state=DbgState.STEP): + def debug_cmd(self, state=DbgState.STEP): self.state = state self.inner_event_loop.exit(0) - - def trace_callback(self,frame,event,arg): + def trace_callback(self, frame, event, arg): filename = frame.f_code.co_filename - if filename==DUMMY_FILE: + if filename == DUMMY_FILE: if not self._frames: self._frames.append(frame) - self.trace_local(frame,event,arg) + self.trace_local(frame, event, arg) return self.trace_callback else: return None - def trace_local(self,frame,event,arg): + def trace_local(self, frame, event, arg): lineno = frame.f_lineno if event in (DbgEevent.LINE,): - if (self.state in (DbgState.STEP, DbgState.STEP_IN) and frame is self._frames[-1]) \ - or (lineno in self.breakpoints): + if ( + self.state in (DbgState.STEP, DbgState.STEP_IN) + and frame is self._frames[-1] + ) or (lineno in self.breakpoints): if lineno in self.breakpoints: self._frames.append(frame) @@ -386,7 +410,7 @@ def trace_local(self,frame,event,arg): self.sigLineChanged.emit(lineno) self.sigFrameChanged.emit(frame) self.sigLocalsChanged.emit(frame.f_locals) - self.sigCQChanged.emit(find_cq_objects(frame.f_locals),True) + self.sigCQChanged.emit(find_cq_objects(frame.f_locals), True) self.inner_event_loop.exec_() @@ -403,12 +427,12 @@ def trace_local(self,frame,event,arg): self._frames.append(frame) if self._stop_debugging: - raise BdbQuit #stop debugging if requested + raise BdbQuit # stop debugging if requested @contextmanager def module_manager(): - """ unloads any modules loaded while the context manager is active """ + """unloads any modules loaded while the context manager is active""" loaded_modules = set(sys.modules.keys()) try: diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 0e6b2c98..2ed9b788 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -17,85 +17,101 @@ from ..icons import icon -class Editor(CodeEditor,ComponentMixin): - name = 'Code Editor' +class Editor(CodeEditor, ComponentMixin): + + name = "Code Editor" # This signal is emitted whenever the currently-open file changes and # autoreload is enabled. triggerRerender = pyqtSignal(bool) sigFilenameChanged = pyqtSignal(str) - preferences = Parameter.create(name='Preferences', children=[ - {'name': 'Font size', 'type': 'int', 'value': 12}, - {'name': 'Autoreload', 'type': 'bool', 'value': False}, - {'name': 'Autoreload delay', 'type': 'int', 'value': 50}, - {'name': 'Autoreload: watch imported modules', 'type': 'bool', 'value': False}, - {'name': 'Line wrap', 'type': 'bool', 'value': False}, - {'name': 'Color scheme', 'type': 'list', - 'values': ['Spyder','Monokai','Zenburn'], 'value': 'Spyder'}]) - - EXTENSIONS = 'py' - - def __init__(self,parent=None): + preferences = Parameter.create( + name="Preferences", + children=[ + {"name": "Font size", "type": "int", "value": 12}, + {"name": "Autoreload", "type": "bool", "value": False}, + {"name": "Autoreload delay", "type": "int", "value": 50}, + { + "name": "Autoreload: watch imported modules", + "type": "bool", + "value": False, + }, + {"name": "Line wrap", "type": "bool", "value": False}, + { + "name": "Color scheme", + "type": "list", + "values": ["Spyder", "Monokai", "Zenburn"], + "value": "Spyder", + }, + ], + ) + + EXTENSIONS = "py" + + def __init__(self, parent=None): self._watched_file = None - super(Editor,self).__init__(parent) + super(Editor, self).__init__(parent) ComponentMixin.__init__(self) - self.setup_editor(linenumbers=True, - markers=True, - edge_line=False, - tab_mode=False, - show_blanks=True, - font=QFontDatabase.systemFont(QFontDatabase.FixedFont), - language='Python', - filename='') - - self._actions = \ - {'File' : [QAction(icon('new'), - 'New', - self, - shortcut='ctrl+N', - triggered=self.new), - QAction(icon('open'), - 'Open', - self, - shortcut='ctrl+O', - triggered=self.open), - QAction(icon('save'), - 'Save', - self, - shortcut='ctrl+S', - triggered=self.save), - QAction(icon('save_as'), - 'Save as', - self, - shortcut='ctrl+shift+S', - triggered=self.save_as), - QAction(icon('autoreload'), - 'Automatic reload and preview', - self,triggered=self.autoreload, - checkable=True, - checked=False, - objectName='autoreload'), - ]} + self.setup_editor( + linenumbers=True, + markers=True, + edge_line=False, + tab_mode=False, + show_blanks=True, + font=QFontDatabase.systemFont(QFontDatabase.FixedFont), + language="Python", + filename="", + ) + + self._actions = { + "File": [ + QAction( + icon("new"), "New", self, shortcut="ctrl+N", triggered=self.new + ), + QAction( + icon("open"), "Open", self, shortcut="ctrl+O", triggered=self.open + ), + QAction( + icon("save"), "Save", self, shortcut="ctrl+S", triggered=self.save + ), + QAction( + icon("save_as"), + "Save as", + self, + shortcut="ctrl+shift+S", + triggered=self.save_as, + ), + QAction( + icon("autoreload"), + "Automatic reload and preview", + self, + triggered=self.autoreload, + checkable=True, + checked=False, + objectName="autoreload", + ), + ] + } for a in self._actions.values(): self.addActions(a) - self._fixContextMenu() # autoreload support self._file_watcher = QFileSystemWatcher(self) # we wait for 50ms after a file change for the file to be written completely self._file_watch_timer = QTimer(self) - self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) + self._file_watch_timer.setInterval(self.preferences["Autoreload delay"]) self._file_watch_timer.setSingleShot(True) self._file_watcher.fileChanged.connect( - lambda val: self._file_watch_timer.start()) + lambda val: self._file_watch_timer.start() + ) self._file_watch_timer.timeout.connect(self._file_changed) self.updatePreferences() @@ -109,20 +125,19 @@ def _fixContextMenu(self): menu.removeAction(self.run_selection_action) menu.removeAction(self.re_run_last_cell_action) - def updatePreferences(self,*args): + def updatePreferences(self, *args): - self.set_color_scheme(self.preferences['Color scheme']) + self.set_color_scheme(self.preferences["Color scheme"]) font = self.font() - font.setPointSize(self.preferences['Font size']) + font.setPointSize(self.preferences["Font size"]) self.set_font(font) - self.findChild(QAction, 'autoreload') \ - .setChecked(self.preferences['Autoreload']) + self.findChild(QAction, "autoreload").setChecked(self.preferences["Autoreload"]) - self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) + self._file_watch_timer.setInterval(self.preferences["Autoreload delay"]) - self.toggle_wrap_mode(self.preferences['Line wrap']) + self.toggle_wrap_mode(self.preferences["Line wrap"]) self._clear_watched_paths() self._watch_paths() @@ -130,7 +145,11 @@ def updatePreferences(self,*args): def confirm_discard(self): if self.modified: - rv = confirm(self,'Please confirm','Current document is not saved - do you want to continue?') + rv = confirm( + self, + "Please confirm", + "Current document is not saved - do you want to continue?", + ) else: rv = True @@ -138,22 +157,24 @@ def confirm_discard(self): def new(self): - if not self.confirm_discard(): return + if not self.confirm_discard(): + return - self.set_text('') - self.filename = '' + self.set_text("") + self.filename = "" self.reset_modified() def open(self): - - if not self.confirm_discard(): return + + if not self.confirm_discard(): + return curr_dir = Path(self.filename).absolute().dirname() fname = get_open_filename(self.EXTENSIONS, curr_dir) - if fname != '': + if fname != "": self.load_from_file(fname) - def load_from_file(self,fname): + def load_from_file(self, fname): self.set_text_from_file(fname) self.filename = fname @@ -165,8 +186,8 @@ def save(self): save-as dialog. """ - if self._filename != '': - with open(self._filename, 'w', encoding='utf-8') as f: + if self._filename != "": + with open(self._filename, "w", encoding="utf-8") as f: f.write(self.toPlainText()) # Let the editor and the rest of the app know that the file is no longer dirty @@ -178,8 +199,8 @@ def save(self): def save_as(self): fname = get_save_filename(self.EXTENSIONS) - if fname != '': - with open(fname, 'w', encoding='utf-8') as f: + if fname != "": + with open(fname, "w", encoding="utf-8") as f: f.write(self.toPlainText()) self.filename = fname @@ -189,14 +210,20 @@ def toggle_comment(self): """ Allows us to mark the document as modified when the user toggles a comment. """ - super(Editor,self).toggle_comment() + super(Editor, self).toggle_comment() self.document().setModified(True) def _update_filewatcher(self): - if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']): + if self._watched_file and ( + self._watched_file != self.filename or not self.preferences["Autoreload"] + ): self._clear_watched_paths() self._watched_file = None - if self.preferences['Autoreload'] and self.filename and self.filename != self._watched_file: + if ( + self.preferences["Autoreload"] + and self.filename + and self.filename != self._watched_file + ): self._watched_file = self._filename self._watch_paths() @@ -218,8 +245,8 @@ def _clear_watched_paths(self): def _watch_paths(self): if Path(self._filename).exists(): self._file_watcher.addPath(self._filename) - if self.preferences['Autoreload: watch imported modules']: - module_paths = self.get_imported_module_paths(self._filename) + if self.preferences["Autoreload: watch imported modules"]: + module_paths = self.get_imported_module_paths(self._filename) if module_paths: self._file_watcher.addPaths(module_paths) @@ -233,33 +260,32 @@ def _file_changed(self): # Turn autoreload on/off. def autoreload(self, enabled): - self.preferences['Autoreload'] = enabled + self.preferences["Autoreload"] = enabled self._update_filewatcher() def reset_modified(self): self.document().setModified(False) - + @property def modified(self): - + return self.document().isModified() - def saveComponentState(self,store): + def saveComponentState(self, store): - if self.filename != '': - store.setValue(self.name+'/state',self.filename) + if self.filename != "": + store.setValue(self.name + "/state", self.filename) - def restoreComponentState(self,store): + def restoreComponentState(self, store): - filename = store.value(self.name+'/state') + filename = store.value(self.name + "/state") - if filename and self.filename == '': + if filename and self.filename == "": try: self.load_from_file(filename) except IOError: - self._logger.warning(f'could not open {filename}') - + self._logger.warning(f"could not open {filename}") def get_imported_module_paths(self, module_path): @@ -269,15 +295,15 @@ def get_imported_module_paths(self, module_path): try: finder.run_script(module_path) except SyntaxError as err: - self._logger.warning(f'Syntax error in {module_path}: {err}') + self._logger.warning(f"Syntax error in {module_path}: {err}") except Exception as err: self._logger.warning( - f'Cannot determine imported modules in {module_path}: {type(err).__name__} {err}' + f"Cannot determine imported modules in {module_path}: {type(err).__name__} {err}" ) else: for module_name, module in finder.modules.items(): - if module_name != '__main__': - path = getattr(module, '__file__', None) + if module_name != "__main__": + path = getattr(module, "__file__", None) if path is not None and os.path.isfile(path): imported_modules.append(path) diff --git a/cq_editor/widgets/log.py b/cq_editor/widgets/log.py index 6d2da57c..a962c918 100644 --- a/cq_editor/widgets/log.py +++ b/cq_editor/widgets/log.py @@ -7,51 +7,56 @@ from ..mixins import ComponentMixin + def strip_escape_sequences(input_string): # Regular expression pattern to match ANSI escape codes - escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + escape_pattern = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") # Use re.sub to replace escape codes with an empty string - clean_string = re.sub(escape_pattern, '', input_string) + clean_string = re.sub(escape_pattern, "", input_string) return clean_string + class _QtLogHandlerQObject(QObject): sigRecordEmit = pyqtSignal(str) -class QtLogHandler(logging.Handler,logging.StringFormatterHandlerMixin): - - def __init__(self, log_widget,*args,**kwargs): - - super(QtLogHandler,self).__init__(*args,**kwargs) - - log_format_string = '[{record.time:%H:%M:%S%z}] {record.level_name}: {record.message}' - - logging.StringFormatterHandlerMixin.__init__(self,log_format_string) - +class QtLogHandler(logging.Handler, logging.StringFormatterHandlerMixin): + + def __init__(self, log_widget, *args, **kwargs): + + super(QtLogHandler, self).__init__(*args, **kwargs) + + log_format_string = ( + "[{record.time:%H:%M:%S%z}] {record.level_name}: {record.message}" + ) + + logging.StringFormatterHandlerMixin.__init__(self, log_format_string) + self._qobject = _QtLogHandlerQObject() self._qobject.sigRecordEmit.connect(log_widget.append) def emit(self, record): self._qobject.sigRecordEmit.emit(self.format(record) + "\n") + class LogViewer(QPlainTextEdit, ComponentMixin): - - name = 'Log viewer' - - def __init__(self,*args,**kwargs): - - super(LogViewer,self).__init__(*args,**kwargs) + + name = "Log viewer" + + def __init__(self, *args, **kwargs): + + super(LogViewer, self).__init__(*args, **kwargs) self._MAX_ROWS = 500 - + self.setReadOnly(True) self.setMaximumBlockCount(self._MAX_ROWS) self.setLineWrapMode(QPlainTextEdit.NoWrap) - + self.handler = QtLogHandler(self) - - def append(self,msg): + + def append(self, msg): """Append text to the panel with ANSI escape sequences stipped.""" self.moveCursor(QtGui.QTextCursor.End) self.insertPlainText(strip_escape_sequences(msg)) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 1778bfd5..8725d50a 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -1,4 +1,11 @@ -from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAction, QMenu, QWidget, QAbstractItemView +from PyQt5.QtWidgets import ( + QTreeWidget, + QTreeWidgetItem, + QAction, + QMenu, + QWidget, + QAbstractItemView, +) from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal from pyqtgraph.parametertree import Parameter, ParameterTree @@ -9,107 +16,129 @@ from ..mixins import ComponentMixin from ..icons import icon -from ..cq_utils import make_AIS, export, to_occ_color, is_obj_empty, get_occ_color, set_color +from ..cq_utils import ( + make_AIS, + export, + to_occ_color, + is_obj_empty, + get_occ_color, + set_color, +) from .viewer import DEFAULT_FACE_COLOR from ..utils import splitter, layout, get_save_filename + class TopTreeItem(QTreeWidgetItem): - def __init__(self,*args,**kwargs): + def __init__(self, *args, **kwargs): + + super(TopTreeItem, self).__init__(*args, **kwargs) - super(TopTreeItem,self).__init__(*args,**kwargs) class ObjectTreeItem(QTreeWidgetItem): - props = [{'name': 'Name', 'type': 'str', 'value': ''}, - {'name': 'Color', 'type': 'color', 'value': "#f4a824"}, - {'name': 'Alpha', 'type': 'float', 'value': 0, 'limits': (0,1), 'step': 1e-1}, - {'name': 'Visible', 'type': 'bool','value': True}] - - def __init__(self, - name, - ais=None, - shape=None, - shape_display=None, - sig=None, - alpha=0., - color='#f4a824', - **kwargs): - - super(ObjectTreeItem,self).__init__([name],**kwargs) - self.setFlags( self.flags() | Qt.ItemIsUserCheckable) - self.setCheckState(0,Qt.Checked) + props = [ + {"name": "Name", "type": "str", "value": ""}, + {"name": "Color", "type": "color", "value": "#f4a824"}, + {"name": "Alpha", "type": "float", "value": 0, "limits": (0, 1), "step": 1e-1}, + {"name": "Visible", "type": "bool", "value": True}, + ] + + def __init__( + self, + name, + ais=None, + shape=None, + shape_display=None, + sig=None, + alpha=0.0, + color="#f4a824", + **kwargs, + ): + + super(ObjectTreeItem, self).__init__([name], **kwargs) + self.setFlags(self.flags() | Qt.ItemIsUserCheckable) + self.setCheckState(0, Qt.Checked) self.ais = ais self.shape = shape self.shape_display = shape_display self.sig = sig - self.properties = Parameter.create(name='Properties', - children=self.props) + self.properties = Parameter.create(name="Properties", children=self.props) - self.properties['Name'] = name - self.properties['Alpha'] = ais.Transparency() - self.properties['Color'] = get_occ_color(ais) if ais and ais.HasColor() else get_occ_color(DEFAULT_FACE_COLOR) + self.properties["Name"] = name + self.properties["Alpha"] = ais.Transparency() + self.properties["Color"] = ( + get_occ_color(ais) + if ais and ais.HasColor() + else get_occ_color(DEFAULT_FACE_COLOR) + ) self.properties.sigTreeStateChanged.connect(self.propertiesChanged) def propertiesChanged(self, properties, changed): changed_prop = changed[0][0] - self.setData(0,0,self.properties['Name']) - self.ais.SetTransparency(self.properties['Alpha']) + self.setData(0, 0, self.properties["Name"]) + self.ais.SetTransparency(self.properties["Alpha"]) - if changed_prop.name() == 'Color': - set_color(self.ais, to_occ_color(self.properties['Color'])) + if changed_prop.name() == "Color": + set_color(self.ais, to_occ_color(self.properties["Color"])) self.ais.Redisplay() - if self.properties['Visible']: - self.setCheckState(0,Qt.Checked) + if self.properties["Visible"]: + self.setCheckState(0, Qt.Checked) else: - self.setCheckState(0,Qt.Unchecked) + self.setCheckState(0, Qt.Unchecked) if self.sig: self.sig.emit() + class CQRootItem(TopTreeItem): - def __init__(self,*args,**kwargs): + def __init__(self, *args, **kwargs): - super(CQRootItem,self).__init__(['CQ models'],*args,**kwargs) + super(CQRootItem, self).__init__(["CQ models"], *args, **kwargs) class HelpersRootItem(TopTreeItem): - def __init__(self,*args,**kwargs): + def __init__(self, *args, **kwargs): - super(HelpersRootItem,self).__init__(['Helpers'],*args,**kwargs) + super(HelpersRootItem, self).__init__(["Helpers"], *args, **kwargs) -class ObjectTree(QWidget,ComponentMixin): +class ObjectTree(QWidget, ComponentMixin): - name = 'Object Tree' + name = "Object Tree" _stash = [] - preferences = Parameter.create(name='Preferences',children=[ - {'name': 'Preserve properties on reload', 'type': 'bool', 'value': False}, - {'name': 'Clear all before each run', 'type': 'bool', 'value': True}, - {'name': 'STL precision','type': 'float', 'value': .1}]) + preferences = Parameter.create( + name="Preferences", + children=[ + {"name": "Preserve properties on reload", "type": "bool", "value": False}, + {"name": "Clear all before each run", "type": "bool", "value": True}, + {"name": "STL precision", "type": "float", "value": 0.1}, + ], + ) - sigObjectsAdded = pyqtSignal([list],[list,bool]) + sigObjectsAdded = pyqtSignal([list], [list, bool]) sigObjectsRemoved = pyqtSignal(list) sigCQObjectSelected = pyqtSignal(object) sigAISObjectsSelected = pyqtSignal(list) - sigItemChanged = pyqtSignal(QTreeWidgetItem,int) + sigItemChanged = pyqtSignal(QTreeWidgetItem, int) sigObjectPropertiesChanged = pyqtSignal() - def __init__(self,parent): + def __init__(self, parent): - super(ObjectTree,self).__init__(parent) + super(ObjectTree, self).__init__(parent) - self.tree = tree = QTreeWidget(self, - selectionMode=QAbstractItemView.ExtendedSelection) + self.tree = tree = QTreeWidget( + self, selectionMode=QAbstractItemView.ExtendedSelection + ) self.properties_editor = ParameterTree(self) tree.setHeaderHidden(True) @@ -117,10 +146,9 @@ def __init__(self,parent): tree.setRootIsDecorated(False) tree.setContextMenuPolicy(Qt.ActionsContextMenu) - #forward itemChanged singal - tree.itemChanged.connect(\ - lambda item,col: self.sigItemChanged.emit(item,col)) - #handle visibility changes form tree + # forward itemChanged singal + tree.itemChanged.connect(lambda item, col: self.sigItemChanged.emit(item, col)) + # handle visibility changes form tree tree.itemChanged.connect(self.handleChecked) self.CQ = CQRootItem() @@ -129,33 +157,34 @@ def __init__(self,parent): root = tree.invisibleRootItem() root.addChild(self.CQ) root.addChild(self.Helpers) - + tree.expandToDepth(1) - self._export_STL_action = \ - QAction('Export as STL', - self, - enabled=False, - triggered=lambda: \ - self.export('stl', - self.preferences['STL precision'])) - - self._export_STEP_action = \ - QAction('Export as STEP', - self, - enabled=False, - triggered=lambda: \ - self.export('step')) - - self._clear_current_action = QAction(icon('delete'), - 'Clear current', - self, - enabled=False, - triggered=self.removeSelected) - - self._toolbar_actions = \ - [QAction(icon('delete-many'),'Clear all',self,triggered=self.removeObjects), - self._clear_current_action,] + self._export_STL_action = QAction( + "Export as STL", + self, + enabled=False, + triggered=lambda: self.export("stl", self.preferences["STL precision"]), + ) + + self._export_STEP_action = QAction( + "Export as STEP", self, enabled=False, triggered=lambda: self.export("step") + ) + + self._clear_current_action = QAction( + icon("delete"), + "Clear current", + self, + enabled=False, + triggered=self.removeSelected, + ) + + self._toolbar_actions = [ + QAction( + icon("delete-many"), "Clear all", self, triggered=self.removeObjects + ), + self._clear_current_action, + ] self.prepareMenu() @@ -164,34 +193,34 @@ def __init__(self,parent): self.prepareLayout() - def prepareMenu(self): self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self._context_menu = QMenu(self) self._context_menu.addActions(self._toolbar_actions) - self._context_menu.addActions((self._export_STL_action, - self._export_STEP_action)) + self._context_menu.addActions( + (self._export_STL_action, self._export_STEP_action) + ) def prepareLayout(self): - self._splitter = splitter((self.tree,self.properties_editor), - stretch_factors = (2,1), - orientation=Qt.Vertical) - layout(self,(self._splitter,),top_widget=self) + self._splitter = splitter( + (self.tree, self.properties_editor), + stretch_factors=(2, 1), + orientation=Qt.Vertical, + ) + layout(self, (self._splitter,), top_widget=self) self._splitter.show() - def showMenu(self,position): + def showMenu(self, position): self._context_menu.exec_(self.tree.viewport().mapToGlobal(position)) - def menuActions(self): - return {'Tools' : [self._export_STL_action, - self._export_STEP_action]} + return {"Tools": [self._export_STL_action, self._export_STEP_action]} def toolbarActions(self): @@ -199,19 +228,19 @@ def toolbarActions(self): def addLines(self): - origin = (0,0,0) + origin = (0, 0, 0) ais_list = [] - for name,color,direction in zip(('X','Y','Z'), - ('red','lawngreen','blue'), - ((1,0,0),(0,1,0),(0,0,1))): - line_placement = Geom_Line(gp_Ax1(gp_Pnt(*origin), - gp_Dir(*direction))) + for name, color, direction in zip( + ("X", "Y", "Z"), + ("red", "lawngreen", "blue"), + ((1, 0, 0), (0, 1, 0), (0, 0, 1)), + ): + line_placement = Geom_Line(gp_Ax1(gp_Pnt(*origin), gp_Dir(*direction))) line = AIS_Line(line_placement) line.SetColor(to_occ_color(color)) - - self.Helpers.addChild(ObjectTreeItem(name, - ais=line)) + + self.Helpers.addChild(ObjectTreeItem(name, ais=line)) ais_list.append(line) @@ -222,78 +251,85 @@ def _current_properties(self): current_params = {} for i in range(self.CQ.childCount()): child = self.CQ.child(i) - current_params[child.properties['Name']] = child.properties + current_params[child.properties["Name"]] = child.properties return current_params - def _restore_properties(self,obj,properties): + def _restore_properties(self, obj, properties): - for p in properties[obj.properties['Name']]: + for p in properties[obj.properties["Name"]]: obj.properties[p.name()] = p.value() - @pyqtSlot(dict,bool) + @pyqtSlot(dict, bool) @pyqtSlot(dict) - def addObjects(self,objects,clean=False,root=None): + def addObjects(self, objects, clean=False, root=None): if root is None: root = self.CQ request_fit_view = True if root.childCount() == 0 else False - preserve_props = self.preferences['Preserve properties on reload'] - + preserve_props = self.preferences["Preserve properties on reload"] + if preserve_props: current_props = self._current_properties() - if clean or self.preferences['Clear all before each run']: + if clean or self.preferences["Clear all before each run"]: self.removeObjects() ais_list = [] - #remove empty objects - objects_f = {k:v for k,v in objects.items() if not is_obj_empty(v.shape)} - - for name,obj in objects_f.items(): - ais,shape_display = make_AIS(obj.shape,obj.options) - - child = ObjectTreeItem(name, - shape=obj.shape, - shape_display=shape_display, - ais=ais, - sig=self.sigObjectPropertiesChanged) - + # remove empty objects + objects_f = {k: v for k, v in objects.items() if not is_obj_empty(v.shape)} + + for name, obj in objects_f.items(): + ais, shape_display = make_AIS(obj.shape, obj.options) + + child = ObjectTreeItem( + name, + shape=obj.shape, + shape_display=shape_display, + ais=ais, + sig=self.sigObjectPropertiesChanged, + ) + if preserve_props and name in current_props: - self._restore_properties(child,current_props) - - if child.properties['Visible']: + self._restore_properties(child, current_props) + + if child.properties["Visible"]: ais_list.append(ais) - + root.addChild(child) if request_fit_view: - self.sigObjectsAdded[list,bool].emit(ais_list,True) + self.sigObjectsAdded[list, bool].emit(ais_list, True) else: self.sigObjectsAdded[list].emit(ais_list) - @pyqtSlot(object,str,object) - def addObject(self,obj,name='',options=None): + @pyqtSlot(object, str, object) + def addObject(self, obj, name="", options=None): - if options is None: options={} + if options is None: + options = {} root = self.CQ - ais,shape_display = make_AIS(obj, options) + ais, shape_display = make_AIS(obj, options) - root.addChild(ObjectTreeItem(name, - shape=obj, - shape_display=shape_display, - ais=ais, - sig=self.sigObjectPropertiesChanged)) + root.addChild( + ObjectTreeItem( + name, + shape=obj, + shape_display=shape_display, + ais=ais, + sig=self.sigObjectPropertiesChanged, + ) + ) self.sigObjectsAdded.emit([ais]) @pyqtSlot(list) @pyqtSlot() - def removeObjects(self,objects=None): + def removeObjects(self, objects=None): if objects: removed_items_ais = [self.CQ.takeChild(i).ais for i in objects] @@ -303,7 +339,7 @@ def removeObjects(self,objects=None): self.sigObjectsRemoved.emit(removed_items_ais) @pyqtSlot(bool) - def stashObjects(self,action : bool): + def stashObjects(self, action: bool): if action: self._stash = self.CQ.takeChildren() @@ -323,7 +359,7 @@ def removeSelected(self): self.removeObjects(rows) - def export(self,export_type,precision=None): + def export(self, export_type, precision=None): items = self.tree.selectedItems() @@ -336,13 +372,13 @@ def export(self,export_type,precision=None): shapes = [item.shape for item in items if item.parent() is self.CQ] fname = get_save_filename(export_type) - if fname != '': - export(shapes,export_type,fname,precision) + if fname != "": + export(shapes, export_type, fname, precision) @pyqtSlot() def handleSelection(self): - items =self.tree.selectedItems() + items = self.tree.selectedItems() if len(items) == 0: self._export_STL_action.setEnabled(False) self._export_STEP_action.setEnabled(False) @@ -359,10 +395,9 @@ def handleSelection(self): self._export_STEP_action.setEnabled(True) self._clear_current_action.setEnabled(True) self.sigCQObjectSelected.emit(item.shape) - self.properties_editor.setParameters(item.properties, - showTop=False) + self.properties_editor.setParameters(item.properties, showTop=False) self.properties_editor.setEnabled(True) - elif item is self.CQ and item.childCount()>0: + elif item is self.CQ and item.childCount() > 0: self._export_STL_action.setEnabled(True) self._export_STEP_action.setEnabled(True) else: @@ -373,7 +408,7 @@ def handleSelection(self): self.properties_editor.clear() @pyqtSlot(list) - def handleGraphicalSelection(self,shapes): + def handleGraphicalSelection(self, shapes): self.tree.clearSelection() @@ -384,14 +419,11 @@ def handleGraphicalSelection(self,shapes): if item.ais.Shape().IsEqual(shape): item.setSelected(True) - @pyqtSlot(QTreeWidgetItem,int) - def handleChecked(self,item,col): + @pyqtSlot(QTreeWidgetItem, int) + def handleChecked(self, item, col): if type(item) is ObjectTreeItem: if item.checkState(0): - item.properties['Visible'] = True + item.properties["Visible"] = True else: - item.properties['Visible'] = False - - - + item.properties["Visible"] = False diff --git a/cq_editor/widgets/occt_widget.py b/cq_editor/widgets/occt_widget.py index 172755ea..cd8215f3 100755 --- a/cq_editor/widgets/occt_widget.py +++ b/cq_editor/widgets/occt_widget.py @@ -15,159 +15,157 @@ ZOOM_STEP = 0.9 - + class OCCTWidget(QWidget): - + sigObjectSelected = pyqtSignal(list) - - def __init__(self,parent=None): - - super(OCCTWidget,self).__init__(parent) - + + def __init__(self, parent=None): + + super(OCCTWidget, self).__init__(parent) + self.setAttribute(Qt.WA_NativeWindow) self.setAttribute(Qt.WA_PaintOnScreen) self.setAttribute(Qt.WA_NoSystemBackground) - + self._initialized = False self._needs_update = False - - #OCCT secific things + + # OCCT secific things self.display_connection = Aspect_DisplayConnection() self.graphics_driver = OpenGl_GraphicDriver(self.display_connection) - + self.viewer = V3d_Viewer(self.graphics_driver) self.view = self.viewer.CreateView() self.context = AIS_InteractiveContext(self.viewer) - - #Trihedorn, lights, etc + + # Trihedorn, lights, etc self.prepare_display() - + def prepare_display(self): - + view = self.view - + params = view.ChangeRenderingParams() params.NbMsaaSamples = 8 params.IsAntialiasingEnabled = True - + view.TriedronDisplay( - Aspect_TypeOfTriedronPosition.Aspect_TOTP_RIGHT_LOWER, - Quantity_Color(), 0.1) - + Aspect_TypeOfTriedronPosition.Aspect_TOTP_RIGHT_LOWER, Quantity_Color(), 0.1 + ) + viewer = self.viewer - + viewer.SetDefaultLights() viewer.SetLightOn() - + ctx = self.context - + ctx.SetDisplayMode(AIS_DisplayMode.AIS_Shaded, True) ctx.DefaultDrawer().SetFaceBoundaryDraw(True) - + def wheelEvent(self, event): - + delta = event.angleDelta().y() - factor = ZOOM_STEP if delta<0 else 1/ZOOM_STEP - + factor = ZOOM_STEP if delta < 0 else 1 / ZOOM_STEP + self.view.SetZoom(factor) - - def mousePressEvent(self,event): - + + def mousePressEvent(self, event): + pos = event.pos() - + if event.button() == Qt.LeftButton: self.view.StartRotation(pos.x(), pos.y()) elif event.button() == Qt.RightButton: self.view.StartZoomAtPoint(pos.x(), pos.y()) - + self.old_pos = pos - - def mouseMoveEvent(self,event): - + + def mouseMoveEvent(self, event): + pos = event.pos() - x,y = pos.x(),pos.y() - + x, y = pos.x(), pos.y() + if event.buttons() == Qt.LeftButton: - self.view.Rotation(x,y) - + self.view.Rotation(x, y) + elif event.buttons() == Qt.MiddleButton: - self.view.Pan(x - self.old_pos.x(), - self.old_pos.y() - y, theToStart=True) - + self.view.Pan(x - self.old_pos.x(), self.old_pos.y() - y, theToStart=True) + elif event.buttons() == Qt.RightButton: - self.view.ZoomAtPoint(self.old_pos.x(), y, - x, self.old_pos.y()) - + self.view.ZoomAtPoint(self.old_pos.x(), y, x, self.old_pos.y()) + self.old_pos = pos - - def mouseReleaseEvent(self,event): - + + def mouseReleaseEvent(self, event): + if event.button() == Qt.LeftButton: pos = event.pos() - x,y = pos.x(),pos.y() - - self.context.MoveTo(x,y,self.view,True) - + x, y = pos.x(), pos.y() + + self.context.MoveTo(x, y, self.view, True) + self._handle_selection() - + def _handle_selection(self): - + self.context.Select(True) self.context.InitSelected() - + selected = [] if self.context.HasSelectedShape(): selected.append(self.context.SelectedShape()) - + self.sigObjectSelected.emit(selected) def paintEngine(self): - + return None - + def paintEvent(self, event): - + if not self._initialized: self._initialize() else: self.view.Redraw() def showEvent(self, event): - - super(OCCTWidget,self).showEvent(event) - + + super(OCCTWidget, self).showEvent(event) + def resizeEvent(self, event): - - super(OCCTWidget,self).resizeEvent(event) - + + super(OCCTWidget, self).resizeEvent(event) + self.view.MustBeResized() - + def _initialize(self): wins = { - 'darwin' : self._get_window_osx, - 'linux' : self._get_window_linux, - 'win32': self._get_window_win + "darwin": self._get_window_osx, + "linux": self._get_window_linux, + "win32": self._get_window_win, } - self.view.SetWindow(wins.get(platform,self._get_window_linux)(self.winId())) + self.view.SetWindow(wins.get(platform, self._get_window_linux)(self.winId())) self._initialized = True - - def _get_window_win(self,wid): - + + def _get_window_win(self, wid): + from OCP.WNT import WNT_Window - + return WNT_Window(wid.ascapsule()) - def _get_window_linux(self,wid): - + def _get_window_linux(self, wid): + from OCP.Xw import Xw_Window - - return Xw_Window(self.display_connection,int(wid)) - - def _get_window_osx(self,wid): - + + return Xw_Window(self.display_connection, int(wid)) + + def _get_window_osx(self, wid): + from OCP.Cocoa import Cocoa_Window - + return Cocoa_Window(wid.ascapsule()) diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index 244d9acc..b02f378f 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -1,50 +1,46 @@ from traceback import extract_tb, format_exception_only from itertools import dropwhile -from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction, - QLabel) +from PyQt5.QtWidgets import QWidget, QTreeWidget, QTreeWidgetItem, QAction, QLabel from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal from PyQt5.QtGui import QFontMetrics from ..mixins import ComponentMixin from ..utils import layout + class TracebackTree(QTreeWidget): - name = 'Traceback Viewer' - - def __init__(self,parent): - - super(TracebackTree,self).__init__(parent) + name = "Traceback Viewer" + + def __init__(self, parent): + + super(TracebackTree, self).__init__(parent) self.setHeaderHidden(False) self.setItemsExpandable(False) self.setRootIsDecorated(False) self.setContextMenuPolicy(Qt.ActionsContextMenu) - + self.setColumnCount(3) - self.setHeaderLabels(['File','Line','Code']) - - + self.setHeaderLabels(["File", "Line", "Code"]) + self.root = self.invisibleRootItem() -class TracebackPane(QWidget,ComponentMixin): - + +class TracebackPane(QWidget, ComponentMixin): + sigHighlightLine = pyqtSignal(int) - - def __init__(self,parent): - - super(TracebackPane,self).__init__(parent) - + + def __init__(self, parent): + + super(TracebackPane, self).__init__(parent) + self.tree = TracebackTree(self) self.current_exception = QLabel(self) - self.current_exception.setStyleSheet(\ - "QLabel {color : red; }") - - layout(self, - (self.current_exception, - self.tree), - self) - + self.current_exception.setStyleSheet("QLabel {color : red; }") + + layout(self, (self.current_exception, self.tree), self) + self.tree.currentItemChanged.connect(self.handleSelection) def truncate_text(self, text, max_length=100): @@ -52,60 +48,64 @@ def truncate_text(self, text, max_length=100): Used to prevent the label from expanding the window width off the screen. """ metrics = QFontMetrics(self.current_exception.font()) - elided_text = metrics.elidedText(text, Qt.ElideRight, self.current_exception.width() - 75) + elided_text = metrics.elidedText( + text, Qt.ElideRight, self.current_exception.width() - 75 + ) return elided_text - @pyqtSlot(object,str) - def addTraceback(self,exc_info,code): - + @pyqtSlot(object, str) + def addTraceback(self, exc_info, code): + self.tree.clear() - + if exc_info: - t,exc,tb = exc_info - + t, exc, tb = exc_info + root = self.tree.root code = code.splitlines() for el in dropwhile( - lambda el: 'string>' not in el.filename, extract_tb(tb) + lambda el: "string>" not in el.filename, extract_tb(tb) ): - #workaround of the traceback module - if el.line == '': - line = code[el.lineno-1].strip() + # workaround of the traceback module + if el.line == "": + line = code[el.lineno - 1].strip() else: line = el.line - root.addChild(QTreeWidgetItem([el.filename, - str(el.lineno), - line])) + root.addChild(QTreeWidgetItem([el.filename, str(el.lineno), line])) exc_name = t.__name__ exc_msg = str(exc) - exc_msg = exc_msg.replace('<', '<').replace('>', '>') #replace <> + exc_msg = exc_msg.replace("<", "<").replace(">", ">") # replace <> truncated_msg = self.truncate_text(exc_msg) - self.current_exception.setText('{}: {}'.format(exc_name,truncated_msg)) + self.current_exception.setText( + "{}: {}".format(exc_name, truncated_msg) + ) self.current_exception.setToolTip(exc_msg) - + # handle the special case of a SyntaxError - if t is SyntaxError: - root.addChild(QTreeWidgetItem( - [exc.filename, - str(exc.lineno), - exc.text.strip() if exc.text else ''] - )) + if t is SyntaxError: + root.addChild( + QTreeWidgetItem( + [ + exc.filename, + str(exc.lineno), + exc.text.strip() if exc.text else "", + ] + ) + ) else: - self.current_exception.setText('') - self.current_exception.setToolTip('') + self.current_exception.setText("") + self.current_exception.setToolTip("") + + @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) + def handleSelection(self, item, *args): - @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem) - def handleSelection(self,item,*args): - if item: - f,line = item.data(0,0),int(item.data(1,0)) - - if '' in f: + f, line = item.data(0, 0), int(item.data(1, 0)) + + if "" in f: self.sigHighlightLine.emit(line) - - diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 2f54eaca..0750d626 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -3,12 +3,19 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal from PyQt5.QtGui import QIcon -from OCP.Graphic3d import Graphic3d_Camera, Graphic3d_StereoMode, Graphic3d_NOM_JADE,\ - Graphic3d_MaterialAspect -from OCP.AIS import AIS_Shaded,AIS_WireFrame, AIS_ColoredShape, AIS_Axis +from OCP.Graphic3d import ( + Graphic3d_Camera, + Graphic3d_StereoMode, + Graphic3d_NOM_JADE, + Graphic3d_MaterialAspect, +) +from OCP.AIS import AIS_Shaded, AIS_WireFrame, AIS_ColoredShape, AIS_Axis from OCP.Aspect import Aspect_GDM_Lines, Aspect_GT_Rectangular -from OCP.Quantity import Quantity_NOC_BLACK as BLACK, Quantity_TOC_RGB as TOC_RGB,\ - Quantity_Color +from OCP.Quantity import ( + Quantity_NOC_BLACK as BLACK, + Quantity_TOC_RGB as TOC_RGB, + Quantity_Color, +) from OCP.Geom import Geom_Axis1Placement from OCP.gp import gp_Ax3, gp_Dir, gp_Pnt, gp_Ax1 @@ -23,33 +30,70 @@ import qtawesome as qta - DEFAULT_EDGE_COLOR = Quantity_Color(BLACK) DEFAULT_EDGE_WIDTH = 2 -class OCCViewer(QWidget,ComponentMixin): - - name = '3D Viewer' - preferences = Parameter.create(name='Pref', children=[ - {'name': 'Fit automatically', 'type': 'bool', 'value': True}, - {'name': 'Use gradient', 'type': 'bool', 'value': False}, - {'name': 'Background color', 'type': 'color', 'value': (95,95,95)}, - {'name': 'Background color (aux)', 'type': 'color', 'value': (30,30,30)}, - {'name': 'Deviation', 'type': 'float', 'value': 1e-5, 'dec': True, 'step': 1}, - {'name': 'Angular deviation', 'type': 'float', 'value': 0.1, 'dec': True, 'step': 1}, - {'name': 'Projection Type', 'type': 'list', 'value': 'Orthographic', - 'values': ['Orthographic', 'Perspective', 'Stereo', 'MonoLeftEye', 'MonoRightEye']}, - {'name': 'Stereo Mode', 'type': 'list', 'value': 'QuadBuffer', - 'values': ['QuadBuffer', 'Anaglyph', 'RowInterlaced', 'ColumnInterlaced', - 'ChessBoard', 'SideBySide', 'OverUnder']}]) - IMAGE_EXTENSIONS = 'png' +class OCCViewer(QWidget, ComponentMixin): + + name = "3D Viewer" + + preferences = Parameter.create( + name="Pref", + children=[ + {"name": "Fit automatically", "type": "bool", "value": True}, + {"name": "Use gradient", "type": "bool", "value": False}, + {"name": "Background color", "type": "color", "value": (95, 95, 95)}, + {"name": "Background color (aux)", "type": "color", "value": (30, 30, 30)}, + { + "name": "Deviation", + "type": "float", + "value": 1e-5, + "dec": True, + "step": 1, + }, + { + "name": "Angular deviation", + "type": "float", + "value": 0.1, + "dec": True, + "step": 1, + }, + { + "name": "Projection Type", + "type": "list", + "value": "Orthographic", + "values": [ + "Orthographic", + "Perspective", + "Stereo", + "MonoLeftEye", + "MonoRightEye", + ], + }, + { + "name": "Stereo Mode", + "type": "list", + "value": "QuadBuffer", + "values": [ + "QuadBuffer", + "Anaglyph", + "RowInterlaced", + "ColumnInterlaced", + "ChessBoard", + "SideBySide", + "OverUnder", + ], + }, + ], + ) + IMAGE_EXTENSIONS = "png" sigObjectSelected = pyqtSignal(list) - def __init__(self,parent=None): + def __init__(self, parent=None): - super(OCCViewer,self).__init__(parent) + super(OCCViewer, self).__init__(parent) ComponentMixin.__init__(self) self.canvas = OCCTWidget() @@ -57,10 +101,14 @@ def __init__(self,parent=None): self.create_actions(self) - self.layout_ = layout(self, - [self.canvas,], - top_widget=self, - margin=0) + self.layout_ = layout( + self, + [ + self.canvas, + ], + top_widget=self, + margin=0, + ) self.setup_default_drawer() self.updatePreferences() @@ -79,95 +127,129 @@ def setup_default_drawer(self): line_aspect.SetWidth(DEFAULT_EDGE_WIDTH) line_aspect.SetColor(DEFAULT_EDGE_COLOR) - def updatePreferences(self,*args): + def updatePreferences(self, *args): - color1 = to_occ_color(self.preferences['Background color']) - color2 = to_occ_color(self.preferences['Background color (aux)']) + color1 = to_occ_color(self.preferences["Background color"]) + color2 = to_occ_color(self.preferences["Background color (aux)"]) - if not self.preferences['Use gradient']: + if not self.preferences["Use gradient"]: color2 = color1 - self.canvas.view.SetBgGradientColors(color1,color2,theToUpdate=True) + self.canvas.view.SetBgGradientColors(color1, color2, theToUpdate=True) self.canvas.update() ctx = self.canvas.context - ctx.SetDeviationCoefficient(self.preferences['Deviation']) - ctx.SetDeviationAngle(self.preferences['Angular deviation']) + ctx.SetDeviationCoefficient(self.preferences["Deviation"]) + ctx.SetDeviationAngle(self.preferences["Angular deviation"]) v = self._get_view() camera = v.Camera() - projection_type = self.preferences['Projection Type'] - camera.SetProjectionType(getattr(Graphic3d_Camera, f'Projection_{projection_type}', - Graphic3d_Camera.Projection_Orthographic)) + projection_type = self.preferences["Projection Type"] + camera.SetProjectionType( + getattr( + Graphic3d_Camera, + f"Projection_{projection_type}", + Graphic3d_Camera.Projection_Orthographic, + ) + ) # onle relevant for stereo projection - stereo_mode = self.preferences['Stereo Mode'] + stereo_mode = self.preferences["Stereo Mode"] params = v.ChangeRenderingParams() - params.StereoMode = getattr(Graphic3d_StereoMode, f'Graphic3d_StereoMode_{stereo_mode}', - Graphic3d_StereoMode.Graphic3d_StereoMode_QuadBuffer) - - def create_actions(self,parent): - - self._actions = \ - {'View' : [QAction(qta.icon('fa.arrows-alt'), - 'Fit (Shift+F1)', - parent, - shortcut='shift+F1', - triggered=self.fit), - QAction(QIcon(':/images/icons/isometric_view.svg'), - 'Iso (Shift+F2)', - parent, - shortcut='shift+F2', - triggered=self.iso_view), - QAction(QIcon(':/images/icons/top_view.svg'), - 'Top (Shift+F3)', - parent, - shortcut='shift+F3', - triggered=self.top_view), - QAction(QIcon(':/images/icons/bottom_view.svg'), - 'Bottom (Shift+F4)', - parent, - shortcut='shift+F4', - triggered=self.bottom_view), - QAction(QIcon(':/images/icons/front_view.svg'), - 'Front (Shift+F5)', - parent, - shortcut='shift+F5', - triggered=self.front_view), - QAction(QIcon(':/images/icons/back_view.svg'), - 'Back (Shift+F6)', - parent, - shortcut='shift+F6', - triggered=self.back_view), - QAction(QIcon(':/images/icons/left_side_view.svg'), - 'Left (Shift+F7)', - parent, - shortcut='shift+F7', - triggered=self.left_view), - QAction(QIcon(':/images/icons/right_side_view.svg'), - 'Right (Shift+F8)', - parent, - shortcut='shift+F8', - triggered=self.right_view), - QAction(qta.icon('fa.square-o'), - 'Wireframe (Shift+F9)', - parent, - shortcut='shift+F9', - triggered=self.wireframe_view), - QAction(qta.icon('fa.square'), - 'Shaded (Shift+F10)', - parent, - shortcut='shift+F10', - triggered=self.shaded_view)], - 'Tools' : [QAction(icon('screenshot'), - 'Screenshot', - parent, - triggered=self.save_screenshot)]} + params.StereoMode = getattr( + Graphic3d_StereoMode, + f"Graphic3d_StereoMode_{stereo_mode}", + Graphic3d_StereoMode.Graphic3d_StereoMode_QuadBuffer, + ) + + def create_actions(self, parent): + + self._actions = { + "View": [ + QAction( + qta.icon("fa.arrows-alt"), + "Fit (Shift+F1)", + parent, + shortcut="shift+F1", + triggered=self.fit, + ), + QAction( + QIcon(":/images/icons/isometric_view.svg"), + "Iso (Shift+F2)", + parent, + shortcut="shift+F2", + triggered=self.iso_view, + ), + QAction( + QIcon(":/images/icons/top_view.svg"), + "Top (Shift+F3)", + parent, + shortcut="shift+F3", + triggered=self.top_view, + ), + QAction( + QIcon(":/images/icons/bottom_view.svg"), + "Bottom (Shift+F4)", + parent, + shortcut="shift+F4", + triggered=self.bottom_view, + ), + QAction( + QIcon(":/images/icons/front_view.svg"), + "Front (Shift+F5)", + parent, + shortcut="shift+F5", + triggered=self.front_view, + ), + QAction( + QIcon(":/images/icons/back_view.svg"), + "Back (Shift+F6)", + parent, + shortcut="shift+F6", + triggered=self.back_view, + ), + QAction( + QIcon(":/images/icons/left_side_view.svg"), + "Left (Shift+F7)", + parent, + shortcut="shift+F7", + triggered=self.left_view, + ), + QAction( + QIcon(":/images/icons/right_side_view.svg"), + "Right (Shift+F8)", + parent, + shortcut="shift+F8", + triggered=self.right_view, + ), + QAction( + qta.icon("fa.square-o"), + "Wireframe (Shift+F9)", + parent, + shortcut="shift+F9", + triggered=self.wireframe_view, + ), + QAction( + qta.icon("fa.square"), + "Shaded (Shift+F10)", + parent, + shortcut="shift+F10", + triggered=self.shaded_view, + ), + ], + "Tools": [ + QAction( + icon("screenshot"), + "Screenshot", + parent, + triggered=self.save_screenshot, + ) + ], + } def toolbarActions(self): - return self._actions['View'] - + return self._actions["View"] def clear(self): @@ -178,50 +260,52 @@ def clear(self): context.PurgeDisplay() context.RemoveAll(True) - def _display(self,shape): + def _display(self, shape): ais = make_AIS(shape) - self.canvas.context.Display(shape,True) + self.canvas.context.Display(shape, True) self.displayed_shapes.append(shape) self.displayed_ais.append(ais) - #self.canvas._display.Repaint() + # self.canvas._display.Repaint() @pyqtSlot(object) - def display(self,ais): + def display(self, ais): context = self._get_context() - context.Display(ais,True) + context.Display(ais, True) - if self.preferences['Fit automatically']: self.fit() + if self.preferences["Fit automatically"]: + self.fit() @pyqtSlot(list) - @pyqtSlot(list,bool) - def display_many(self,ais_list,fit=None): + @pyqtSlot(list, bool) + def display_many(self, ais_list, fit=None): context = self._get_context() for ais in ais_list: - context.Display(ais,True) + context.Display(ais, True) - if self.preferences['Fit automatically'] and fit is None: + if self.preferences["Fit automatically"] and fit is None: self.fit() elif fit: self.fit() - @pyqtSlot(QTreeWidgetItem,int) - def update_item(self,item,col): + @pyqtSlot(QTreeWidgetItem, int) + def update_item(self, item, col): ctx = self._get_context() if item.checkState(0): - ctx.Display(item.ais,True) + ctx.Display(item.ais, True) else: - ctx.Erase(item.ais,True) + ctx.Erase(item.ais, True) @pyqtSlot(list) - def remove_items(self,ais_items): + def remove_items(self, ais_items): ctx = self._get_context() - for ais in ais_items: ctx.Erase(ais,True) + for ais in ais_items: + ctx.Erase(ais, True) @pyqtSlot() def redraw(self): @@ -235,43 +319,43 @@ def fit(self): def iso_view(self): v = self._get_view() - v.SetProj(1,-1,1) + v.SetProj(1, -1, 1) v.SetTwist(0) def bottom_view(self): v = self._get_view() - v.SetProj(0,0,-1) + v.SetProj(0, 0, -1) v.SetTwist(0) def top_view(self): v = self._get_view() - v.SetProj(0,0,1) + v.SetProj(0, 0, 1) v.SetTwist(0) def front_view(self): v = self._get_view() - v.SetProj(0,1,0) + v.SetProj(0, 1, 0) v.SetTwist(0) def back_view(self): v = self._get_view() - v.SetProj(0,-1,0) + v.SetProj(0, -1, 0) v.SetTwist(0) def left_view(self): v = self._get_view() - v.SetProj(-1,0,0) + v.SetProj(-1, 0, 0) v.SetTwist(0) def right_view(self): v = self._get_view() - v.SetProj(1,0,0) + v.SetProj(1, 0, 0) v.SetTwist(0) def shaded_view(self): @@ -284,61 +368,55 @@ def wireframe_view(self): c = self._get_context() c.SetDisplayMode(AIS_WireFrame, True) - def show_grid(self, - step=1., - size=10.+1e-6, - color1=(.7,.7,.7), - color2=(0,0,0)): + def show_grid( + self, step=1.0, size=10.0 + 1e-6, color1=(0.7, 0.7, 0.7), color2=(0, 0, 0) + ): viewer = self._get_viewer() - viewer.ActivateGrid(Aspect_GT_Rectangular, - Aspect_GDM_Lines) + viewer.ActivateGrid(Aspect_GT_Rectangular, Aspect_GDM_Lines) viewer.SetRectangularGridGraphicValues(size, size, 0) viewer.SetRectangularGridValues(0, 0, step, step, 0) grid = viewer.Grid() - grid.SetColors(Quantity_Color(*color1,TOC_RGB), - Quantity_Color(*color2,TOC_RGB)) + grid.SetColors( + Quantity_Color(*color1, TOC_RGB), Quantity_Color(*color2, TOC_RGB) + ) def hide_grid(self): viewer = self._get_viewer() viewer.DeactivateGrid() - @pyqtSlot(bool,float) + @pyqtSlot(bool, float) @pyqtSlot(bool) - def toggle_grid(self, - value : bool, - dim : float = 10.): + def toggle_grid(self, value: bool, dim: float = 10.0): if value: - self.show_grid(step=dim/20,size=dim+1e-9) + self.show_grid(step=dim / 20, size=dim + 1e-9) else: self.hide_grid() @pyqtSlot(gp_Ax3) - def set_grid_orientation(self,orientation : gp_Ax3): + def set_grid_orientation(self, orientation: gp_Ax3): viewer = self._get_viewer() viewer.SetPrivilegedPlane(orientation) - def show_axis(self,origin = (0,0,0), direction=(0,0,1)): + def show_axis(self, origin=(0, 0, 0), direction=(0, 0, 1)): - ax_placement = Geom_Axis1Placement(gp_Ax1(gp_Pnt(*origin), - gp_Dir(*direction))) + ax_placement = Geom_Axis1Placement(gp_Ax1(gp_Pnt(*origin), gp_Dir(*direction))) ax = AIS_Axis(ax_placement) self._display_ais(ax) def save_screenshot(self): fname = get_save_filename(self.IMAGE_EXTENSIONS) - if fname != '': - self._get_view().Dump(fname) + if fname != "": + self._get_view().Dump(fname) - def _display_ais(self,ais): + def _display_ais(self, ais): self._get_context().Display(ais) - def _get_view(self): return self.canvas.view @@ -352,18 +430,18 @@ def _get_context(self): return self.canvas.context @pyqtSlot(list) - def handle_selection(self,obj): + def handle_selection(self, obj): self.sigObjectSelected.emit(obj) @pyqtSlot(list) - def set_selected(self,ais): + def set_selected(self, ais): ctx = self._get_context() ctx.ClearSelected(False) for obj in ais: - ctx.AddOrRemoveSelected(obj,False) + ctx.AddOrRemoveSelected(obj, False) self.redraw() @@ -380,10 +458,10 @@ def set_selected(self,ais): dlg.setFixedHeight(400) dlg.setFixedWidth(600) - layout(dlg,(viewer,),dlg) + layout(dlg, (viewer,), dlg) dlg.show() - box = BRepPrimAPI_MakeBox(20,20,30) + box = BRepPrimAPI_MakeBox(20, 20, 30) box_ais = AIS_ColoredShape(box.Shape()) viewer.display(box_ais) diff --git a/pyinstaller/pyi_rth_fontconfig.py b/pyinstaller/pyi_rth_fontconfig.py index c406f67d..93326e22 100644 --- a/pyinstaller/pyi_rth_fontconfig.py +++ b/pyinstaller/pyi_rth_fontconfig.py @@ -1,6 +1,6 @@ import os import sys -if sys.platform.startswith('linux'): - os.environ['FONTCONFIG_FILE'] = '/etc/fonts/fonts.conf' - os.environ['FONTCONFIG_PATH'] = '/etc/fonts/' +if sys.platform.startswith("linux"): + os.environ["FONTCONFIG_FILE"] = "/etc/fonts/fonts.conf" + os.environ["FONTCONFIG_PATH"] = "/etc/fonts/" diff --git a/pyinstaller/pyi_rth_occ.py b/pyinstaller/pyi_rth_occ.py index f10104d2..9dc0932c 100644 --- a/pyinstaller/pyi_rth_occ.py +++ b/pyinstaller/pyi_rth_occ.py @@ -1,7 +1,7 @@ from os import environ as env -env['CASROOT'] = 'opencascade' +env["CASROOT"] = "opencascade" -env['CSF_ShadersDirectory'] = 'opencascade/src/Shaders' -env['CSF_UnitsLexicon'] = 'opencascade/src/UnitsAPI/Lexi_Expr.dat' -env['CSF_UnitsDefinition'] = 'opencascade/src/UnitsAPI/Units.dat' +env["CSF_ShadersDirectory"] = "opencascade/src/Shaders" +env["CSF_UnitsLexicon"] = "opencascade/src/UnitsAPI/Lexi_Expr.dat" +env["CSF_UnitsDefinition"] = "opencascade/src/UnitsAPI/Units.dat" diff --git a/run.py b/run.py index 8c0badf6..606ac60f 100644 --- a/run.py +++ b/run.py @@ -3,14 +3,14 @@ faulthandler.enable() -if 'CASROOT' in os.environ: - del os.environ['CASROOT'] +if "CASROOT" in os.environ: + del os.environ["CASROOT"] -if sys.platform == 'win32': +if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) from cq_editor.__main__ import main -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/setup.py b/setup.py index 8943e67f..1141c444 100644 --- a/setup.py +++ b/setup.py @@ -3,25 +3,30 @@ from setuptools import setup, find_packages + def read(rel_path): here = os.path.abspath(os.path.dirname(__file__)) - with codecs.open(os.path.join(here, rel_path), 'r') as fp: + with codecs.open(os.path.join(here, rel_path), "r") as fp: return fp.read() + def get_version(rel_path): for line in read(rel_path).splitlines(): - if line.startswith('__version__'): + if line.startswith("__version__"): delim = '"' if '"' in line else "'" return line.split(delim)[1] else: raise RuntimeError("Unable to find version string.") -setup(name='CQ-editor', - version=get_version('cq_editor/_version.py'), - packages=find_packages(), - entry_points={ - 'gui_scripts': [ - 'cq-editor = cq_editor.__main__:main', - 'CQ-editor = cq_editor.__main__:main' - ]} - ) + +setup( + name="CQ-editor", + version=get_version("cq_editor/_version.py"), + packages=find_packages(), + entry_points={ + "gui_scripts": [ + "cq-editor = cq_editor.__main__:main", + "CQ-editor = cq_editor.__main__:main", + ] + }, +) diff --git a/tests/test_app.py b/tests/test_app.py index db0c9a02..ef547c91 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,7 +1,7 @@ from path import Path import os, sys, asyncio -if sys.platform == 'win32': +if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) from multiprocessing import Process @@ -17,61 +17,54 @@ from cq_editor.widgets.editor import Editor from cq_editor.cq_utils import export, get_occ_color -code = \ -'''import cadquery as cq +code = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) -result = result.edges("|Z").fillet(0.125)''' +result = result.edges("|Z").fillet(0.125)""" -code_bigger_object = \ -'''import cadquery as cq +code_bigger_object = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(20, 20, 0.5) result = result.edges("|Z").fillet(0.125) -''' +""" -code_show_Workplane = \ -'''import cadquery as cq +code_show_Workplane = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) show_object(result) -''' +""" -code_show_Workplane_named = \ -'''import cadquery as cq +code_show_Workplane_named = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) log('test') show_object(result,name='test') -''' +""" -code_show_Shape = \ -'''import cadquery as cq +code_show_Shape = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) show_object(result.val()) -''' +""" -code_debug_Workplane = \ -'''import cadquery as cq +code_debug_Workplane = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) debug(result) -''' +""" -code_multi = \ -'''import cadquery as cq +code_multi = """import cadquery as cq result1 = cq.Workplane("XY" ).box(3, 3, 0.5) result2 = cq.Workplane("XY" ).box(3, 3, 0.5).translate((0,15,0)) -''' +""" code_nested_top = """import test_nested_bottom """ @@ -91,31 +84,35 @@ sk = cq.Sketch().rect(1,1) """ + def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) def modify_file(code, path="test.py"): - p = Process(target=_modify_file, args=(code,path)) + p = Process(target=_modify_file, args=(code, path)) p.start() p.join() + def get_center(widget): pos = widget.pos() - pos.setX(pos.x()+widget.width()//2) - pos.setY(pos.y()+widget.height()//2) + pos.setX(pos.x() + widget.width() // 2) + pos.setY(pos.y() + widget.height() // 2) return pos + def get_bottom_left(widget): pos = widget.pos() - pos.setY(pos.y()+widget.height()) + pos.setY(pos.y() + widget.height()) return pos + def get_rgba(ais): alpha = ais.Transparency() @@ -123,28 +120,30 @@ def get_rgba(ais): return color.redF(), color.greenF(), color.blueF(), alpha + @pytest.fixture -def main(qtbot,mocker): +def main(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) win = MainWindow() win.show() qtbot.addWidget(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code) - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() return qtbot, win + @pytest.fixture -def main_clean(qtbot,mocker): +def main_clean(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) win = MainWindow() win.show() @@ -152,15 +151,16 @@ def main_clean(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code) return qtbot, win + @pytest.fixture -def main_clean_do_not_close(qtbot,mocker): +def main_clean_do_not_close(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.No) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.No) win = MainWindow() win.show() @@ -168,16 +168,17 @@ def main_clean_do_not_close(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code) return qtbot, win + @pytest.fixture -def main_multi(qtbot,mocker): +def main_multi(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.step", "")) win = MainWindow() win.show() @@ -185,116 +186,120 @@ def main_multi(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code_multi) - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() return qtbot, win + def test_render(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] - log = win.components['log'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] + log = win.components["log"] # enable CQ reloading - debugger.preferences['Reload CQ'] = True + debugger.preferences["Reload CQ"] = True # check that object was rendered - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_Workplane) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that cq.Shape object was rendered using explicit show_object call editor.set_text(code_show_Shape) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # test rendering via console console.execute(code_show_Workplane) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 console.execute(code_show_Shape) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # check object rendering using show_object call with a name specified and # debug call editor.set_text(code_show_Workplane_named) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.child(0).text(0) == 'test') - assert('test' in log.toPlainText().splitlines()[-1]) + assert obj_tree_comp.CQ.child(0).text(0) == "test" + assert "test" in log.toPlainText().splitlines()[-1] # cq reloading check obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 editor.set_text(code_reload_issue) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.childCount() == 3) + assert obj_tree_comp.CQ.childCount() == 3 - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.childCount() == 3) + assert obj_tree_comp.CQ.childCount() == 3 -def test_export(main,mocker): + +def test_export(main, mocker): qtbot, win = main - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() - #set focus - obj_tree = win.components['object_tree'].tree - obj_tree_comp = win.components['object_tree'] + # set focus + obj_tree = win.components["object_tree"].tree + obj_tree_comp = win.components["object_tree"] qtbot.mouseClick(obj_tree, Qt.LeftButton) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Down) - #export STL - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.stl','')) + # export STL + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.stl", "")) obj_tree_comp._export_STL_action.triggered.emit() - assert(os.path.isfile('out.stl')) + assert os.path.isfile("out.stl") - #export STEP - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) + # export STEP + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.step", "")) obj_tree_comp._export_STEP_action.triggered.emit() - assert(os.path.isfile('out.step')) + assert os.path.isfile("out.step") + + # clean + os.remove("out.step") + os.remove("out.stl") - #clean - os.remove('out.step') - os.remove('out.stl') def number_visible_items(viewer): from OCP.AIS import AIS_ListOfInteractive + l = AIS_ListOfInteractive() viewer_ctx = viewer._get_context() @@ -302,186 +307,223 @@ def number_visible_items(viewer): return l.Extent() + def test_inspect(main): qtbot, win = main - #set focus and make invisible - obj_tree = win.components['object_tree'].tree + # set focus and make invisible + obj_tree = win.components["object_tree"].tree qtbot.mouseClick(obj_tree, Qt.LeftButton) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Space) - #enable object inspector - insp = win.components['cq_object_inspector'] + # enable object inspector + insp = win.components["cq_object_inspector"] insp._toolbar_actions[0].toggled.emit(True) - #check if all stack items are visible in the tree - assert(insp.root.childCount() == 3) + # check if all stack items are visible in the tree + assert insp.root.childCount() == 3 - #check if correct number of items is displayed - viewer = win.components['viewer'] + # check if correct number of items is displayed + viewer = win.components["viewer"] insp.setCurrentItem(insp.root.child(0)) - assert(number_visible_items(viewer) == 4) + assert number_visible_items(viewer) == 4 insp.setCurrentItem(insp.root.child(1)) - assert(number_visible_items(viewer) == 7) + assert number_visible_items(viewer) == 7 insp.setCurrentItem(insp.root.child(2)) - assert(number_visible_items(viewer) == 4) + assert number_visible_items(viewer) == 4 insp._toolbar_actions[0].toggled.emit(False) - assert(number_visible_items(viewer) == 3) + assert number_visible_items(viewer) == 3 + class event_loop(object): - '''Used to mock the QEventLoop for the debugger component - ''' + """Used to mock the QEventLoop for the debugger component""" - def __init__(self,callbacks): + def __init__(self, callbacks): self.callbacks = callbacks self.i = 0 def exec_(self): - if self.i 0) - assert(conv_line_ends(editor.get_text_with_eol()) == code) + # check that loading from file works properly + editor.load_from_file("test.py") + assert len(editor.get_text_with_eol()) > 0 + assert conv_line_ends(editor.get_text_with_eol()) == code - #check that loading from file works properly + # check that loading from file works properly editor.new() - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" - #monkeypatch QFileDialog methods + # monkeypatch QFileDialog methods def filename(*args, **kwargs): - return 'test.py',None + return "test.py", None def filename2(*args, **kwargs): - return 'test2.py',None + return "test2.py", None - monkeypatch.setattr(QFileDialog, 'getOpenFileName', - staticmethod(filename)) + monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename)) - monkeypatch.setattr(QFileDialog, 'getSaveFileName', - staticmethod(filename2)) + monkeypatch.setattr(QFileDialog, "getSaveFileName", staticmethod(filename2)) - #check that open file works properly + # check that open file works properly editor.open() - assert(conv_line_ends(editor.get_text_with_eol()) == code) + assert conv_line_ends(editor.get_text_with_eol()) == code - #check that save file works properly + # check that save file works properly editor.new() qtbot.mouseClick(editor, Qt.LeftButton) - qtbot.keyClick(editor,Qt.Key_A) + qtbot.keyClick(editor, Qt.Key_A) - assert(editor.document().isModified() == True) + assert editor.document().isModified() == True - editor.filename = 'test2.py' + editor.filename = "test2.py" editor.save() - assert(editor.document().isModified() == False) + assert editor.document().isModified() == False - monkeypatch.setattr(QFileDialog, 'getOpenFileName', - staticmethod(filename2)) + monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename2)) editor.open() - assert(editor.get_text_with_eol() == 'a') + assert editor.get_text_with_eol() == "a" - #check that save as works properly - os.remove('test2.py') + # check that save as works properly + os.remove("test2.py") editor.save_as() - assert(os.path.exists(filename2()[0])) + assert os.path.exists(filename2()[0]) - #test persistance - settings = QSettings('test') + # test persistance + settings = QSettings("test") editor.saveComponentState(settings) editor.new() - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" editor.restoreComponentState(settings) - assert(editor.get_text_with_eol() == 'a') + assert editor.get_text_with_eol() == "a" - #test error handling - os.remove('test2.py') - assert(not os.path.exists('test2.py')) + # test error handling + os.remove("test2.py") + assert not os.path.exists("test2.py") editor.restoreComponentState(settings) + @pytest.mark.repeat(1) -def test_editor_autoreload(monkeypatch,editor): +def test_editor_autoreload(monkeypatch, editor): qtbot, editor = editor @@ -668,13 +709,13 @@ def test_editor_autoreload(monkeypatch,editor): # start out with autoreload enabled editor.autoreload(True) - with open('test.py','w') as f: + with open("test.py", "w") as f: f.write(code) - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" - editor.load_from_file('test.py') - assert(len(editor.get_text_with_eol()) > 0) + editor.load_from_file("test.py") + assert len(editor.get_text_with_eol()) > 0 # wait for reload. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): @@ -682,7 +723,7 @@ def test_editor_autoreload(monkeypatch,editor): modify_file(code_bigger_object) # check that editor has updated file contents - assert(code_bigger_object.splitlines()[2] in editor.get_text_with_eol()) + assert code_bigger_object.splitlines()[2] in editor.get_text_with_eol() # disable autoreload editor.autoreload(False) @@ -696,7 +737,7 @@ def test_editor_autoreload(monkeypatch,editor): modify_file(code) # editor should continue showing old contents since autoreload is disabled. - assert(code_bigger_object.splitlines()[2] in editor.get_text_with_eol()) + assert code_bigger_object.splitlines()[2] in editor.get_text_with_eol() # Saving a file with autoreload disabled should not trigger a rerender. with pytest.raises(pytestqt.exceptions.TimeoutError): @@ -709,6 +750,7 @@ def test_editor_autoreload(monkeypatch,editor): with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): editor.save() + def test_autoreload_nested(editor): qtbot, editor = editor @@ -716,156 +758,158 @@ def test_autoreload_nested(editor): TIMEOUT = 500 editor.autoreload(True) - editor.preferences['Autoreload: watch imported modules'] = True + editor.preferences["Autoreload: watch imported modules"] = True - with open('test_nested_top.py','w') as f: + with open("test_nested_top.py", "w") as f: f.write(code_nested_top) - with open('test_nested_bottom.py','w') as f: + with open("test_nested_bottom.py", "w") as f: f.write("") - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" - editor.load_from_file('test_nested_top.py') - assert(len(editor.get_text_with_eol()) > 0) + editor.load_from_file("test_nested_top.py") + assert len(editor.get_text_with_eol()) > 0 # wait for reload. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): # modify file - NB: separate process is needed to avoid Windows quirks - modify_file(code_nested_bottom, 'test_nested_bottom.py') + modify_file(code_nested_bottom, "test_nested_bottom.py") + def test_console(main): qtbot, win = main - console = win.components['console'] + console = win.components["console"] # test execute_command a = [] - console.push_vars({'a' : a}) - console.execute_command('a.append(1)') - assert(len(a) == 1) + console.push_vars({"a": a}) + console.execute_command("a.append(1)") + assert len(a) == 1 # test print_text pos_orig = console._prompt_pos - console.print_text('a') - assert(console._prompt_pos == pos_orig + len('a')) + console.print_text("a") + assert console._prompt_pos == pos_orig + len("a") + def test_viewer(main): qtbot, win = main - viewer = win.components['viewer'] + viewer = win.components["viewer"] + + # not sure how to test this, so only smoke tests - #not sure how to test this, so only smoke tests + # trigger all 'View' actions + actions = viewer._actions["View"] + for a in actions: + a.trigger() - #trigger all 'View' actions - actions = viewer._actions['View'] - for a in actions: a.trigger() -code_module = \ -'''def dummy(): return True''' +code_module = """def dummy(): return True""" + +code_import = """from module import dummy +assert(dummy())""" -code_import = \ -'''from module import dummy -assert(dummy())''' def test_module_import(main): qtbot, win = main - editor = win.components['editor'] - debugger = win.components['debugger'] - traceback_view = win.components['traceback_viewer'] + editor = win.components["editor"] + debugger = win.components["debugger"] + traceback_view = win.components["traceback_viewer"] - #save the dummy module - with open('module.py','w') as f: + # save the dummy module + with open("module.py", "w") as f: f.write(code_module) - #run the code importing this module + # run the code importing this module editor.set_text(code_import) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() + + # verify that no exception was generated + assert traceback_view.current_exception.text() == "" - #verify that no exception was generated - assert(traceback_view.current_exception.text() == '') def test_auto_fit_view(main_clean): - def concat(eye,proj,scale): - return eye+proj+(scale,) + def concat(eye, proj, scale): + return eye + proj + (scale,) - def approx_view_properties(eye,proj,scale): + def approx_view_properties(eye, proj, scale): - return pytest.approx(eye+proj+(scale,)) + return pytest.approx(eye + proj + (scale,)) qtbot, win = main_clean - editor = win.components['editor'] - debugger = win.components['debugger'] - viewer = win.components['viewer'] - object_tree = win.components['object_tree'] + editor = win.components["editor"] + debugger = win.components["debugger"] + viewer = win.components["viewer"] + object_tree = win.components["object_tree"] view = viewer.canvas.view - viewer.preferences['Fit automatically'] = False - eye0,proj0,scale0 = view.Eye(),view.Proj(),view.Scale() + viewer.preferences["Fit automatically"] = False + eye0, proj0, scale0 = view.Eye(), view.Proj(), view.Scale() # check if camera position is adjusted automatically when rendering for the # first time debugger.render() - eye1,proj1,scale1 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye0,proj0,scale0) != \ - approx_view_properties(eye1,proj1,scale1) ) + eye1, proj1, scale1 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye0, proj0, scale0) != approx_view_properties(eye1, proj1, scale1) # check if camera position is not changed fter code change editor.set_text(code_bigger_object) debugger.render() - eye2,proj2,scale2 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye1,proj1,scale1) == \ - approx_view_properties(eye2,proj2,scale2) ) + eye2, proj2, scale2 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye1, proj1, scale1) == approx_view_properties(eye2, proj2, scale2) # check if position is adjusted automatically after erasing all objects object_tree.removeObjects() debugger.render() - eye3,proj3,scale3 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye2,proj2,scale2) != \ - approx_view_properties(eye3,proj3,scale3) ) + eye3, proj3, scale3 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye2, proj2, scale2) != approx_view_properties(eye3, proj3, scale3) # check if position is adjusted automatically if settings are changed - viewer.preferences['Fit automatically'] = True + viewer.preferences["Fit automatically"] = True editor.set_text(code) debugger.render() - eye4,proj4,scale4 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye3,proj3,scale3) != \ - approx_view_properties(eye4,proj4,scale4) ) + eye4, proj4, scale4 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye3, proj3, scale3) != approx_view_properties(eye4, proj4, scale4) + def test_preserve_properties(main): qtbot, win = main - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() - object_tree = win.components['object_tree'] - object_tree.preferences['Preserve properties on reload'] = True + object_tree = win.components["object_tree"] + object_tree.preferences["Preserve properties on reload"] = True - assert(object_tree.CQ.childCount() == 1) + assert object_tree.CQ.childCount() == 1 props = object_tree.CQ.child(0).properties - props['Visible'] = False - props['Color'] = '#caffee' - props['Alpha'] = 0.5 + props["Visible"] = False + props["Color"] = "#caffee" + props["Alpha"] = 0.5 - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(object_tree.CQ.childCount() == 1) + assert object_tree.CQ.childCount() == 1 props = object_tree.CQ.child(0).properties - assert(props['Visible'] == False) - assert(props['Color'].name() == '#caffee') - assert(props['Alpha'] == 0.5) + assert props["Visible"] == False + assert props["Color"].name() == "#caffee" + assert props["Alpha"] == 0.5 -def test_selection(main_multi,mocker): + +def test_selection(main_multi, mocker): qtbot, win = main_multi - viewer = win.components['viewer'] - object_tree = win.components['object_tree'] + viewer = win.components["viewer"] + object_tree = win.components["object_tree"] CQ = object_tree.CQ obj1 = CQ.child(0) @@ -876,23 +920,23 @@ def test_selection(main_multi,mocker): obj2.setSelected(True) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep('out.step') - assert(len(imported.solids().vals()) == 2) + imported = cq.importers.importStep("out.step") + assert len(imported.solids().vals()) == 2 # export with one selected objects obj2.setSelected(False) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep('out.step') - assert(len(imported.solids().vals()) == 1) + imported = cq.importers.importStep("out.step") + assert len(imported.solids().vals()) == 1 # export with one selected objects obj1.setSelected(False) CQ.setSelected(True) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep('out.step') - assert(len(imported.solids().vals()) == 2) + imported = cq.importers.importStep("out.step") + assert len(imported.solids().vals()) == 2 # check if viewer and object tree are properly connected CQ.setSelected(False) @@ -905,15 +949,15 @@ def test_selection(main_multi,mocker): while ctx.MoreSelected(): shapes.append(ctx.SelectedShape()) ctx.NextSelected() - assert(len(shapes) == 2) + assert len(shapes) == 2 viewer.fit() qtbot.mouseClick(viewer.canvas, Qt.LeftButton) - assert(len(object_tree.tree.selectedItems()) == 0) + assert len(object_tree.tree.selectedItems()) == 0 viewer.sigObjectSelected.emit([obj1.shape_display.wrapped]) - assert(len(object_tree.tree.selectedItems()) == 1) + assert len(object_tree.tree.selectedItems()) == 1 # go through different handleSelection paths qtbot.mouseClick(object_tree.tree, Qt.LeftButton) @@ -922,111 +966,121 @@ def test_selection(main_multi,mocker): qtbot.keyClick(object_tree.tree, Qt.Key_Down) qtbot.keyClick(object_tree.tree, Qt.Key_Down) - assert(object_tree._export_STL_action.isEnabled() == False) - assert(object_tree._export_STEP_action.isEnabled() == False) - assert(object_tree._clear_current_action.isEnabled() == False) - assert(object_tree.properties_editor.isEnabled() == False) + assert object_tree._export_STL_action.isEnabled() == False + assert object_tree._export_STEP_action.isEnabled() == False + assert object_tree._clear_current_action.isEnabled() == False + assert object_tree.properties_editor.isEnabled() == False + def test_closing(main_clean_do_not_close): - qtbot,win = main_clean_do_not_close + qtbot, win = main_clean_do_not_close - editor = win.components['editor'] + editor = win.components["editor"] # make sure that windows is visible - assert(win.isVisible()) + assert win.isVisible() # should not quit win.close() - assert(win.isVisible()) + assert win.isVisible() # should quit editor.reset_modified() win.close() - assert(not win.isVisible()) + assert not win.isVisible() + -def test_check_for_updates(main,mocker): +def test_check_for_updates(main, mocker): - qtbot,win = main + qtbot, win = main # patch requests import requests - mocker.patch.object(requests.models.Response,'json', - return_value=[{'tag_name' : '0.0.2','draft' : False}]) + + mocker.patch.object( + requests.models.Response, + "json", + return_value=[{"tag_name": "0.0.2", "draft": False}], + ) # stub QMessageBox about about_stub = mocker.stub() - mocker.patch.object(QMessageBox, 'about', about_stub) + mocker.patch.object(QMessageBox, "about", about_stub) import cadquery - cadquery.__version__ = '0.0.1' + cadquery.__version__ = "0.0.1" win.check_for_cq_updates() - assert(about_stub.call_args[0][1] == 'Updates available') + assert about_stub.call_args[0][1] == "Updates available" - cadquery.__version__ = '0.0.3' + cadquery.__version__ = "0.0.3" win.check_for_cq_updates() - assert(about_stub.call_args[0][1] == 'No updates available') + assert about_stub.call_args[0][1] == "No updates available" + -@pytest.mark.skipif(sys.platform.startswith('linux'),reason='Segfault workaround for linux') -def test_screenshot(main,mocker): +@pytest.mark.skipif( + sys.platform.startswith("linux"), reason="Segfault workaround for linux" +) +def test_screenshot(main, mocker): - qtbot,win = main + qtbot, win = main + + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.png", "")) - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.png','')) + viewer = win.components["viewer"] + viewer._actions["Tools"][0].triggered.emit() - viewer = win.components['viewer'] - viewer._actions['Tools'][0].triggered.emit() + assert os.path.exists("out.png") - assert(os.path.exists('out.png')) def test_resize(main): - qtbot,win = main - editor = win.components['editor'] + qtbot, win = main + editor = win.components["editor"] editor.hide() qtbot.wait(50) editor.show() qtbot.wait(50) -code_simple_step = \ -'''import cadquery as cq + +code_simple_step = """import cadquery as cq imported = cq.importers.importStep('shape.step') -''' +""" + def test_relative_references(main): # create code with a relative reference in a subdirectory - p = Path('test_relative_references') + p = Path("test_relative_references") p.mkdir_p() - p_code = p.joinpath('code.py') + p_code = p.joinpath("code.py") p_code.write_text(code_simple_step) # create the referenced step file shape = cq.Workplane("XY").box(1, 1, 1) - p_step = p.joinpath('shape.step') + p_step = p.joinpath("shape.step") export(shape, "step", p_step) # open code qtbot, win = main - editor = win.components['editor'] + editor = win.components["editor"] editor.load_from_file(p_code) # render - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() # assert no errors - traceback_view = win.components['traceback_viewer'] - assert(traceback_view.current_exception.text() == '') + traceback_view = win.components["traceback_viewer"] + assert traceback_view.current_exception.text() == "" # assert one object has been rendered - obj_tree_comp = win.components['object_tree'] - assert(obj_tree_comp.CQ.childCount() == 1) + obj_tree_comp = win.components["object_tree"] + assert obj_tree_comp.CQ.childCount() == 1 # clean up p_code.remove_p() p_step.remove_p() p.rmdir_p() -code_color = \ -''' +code_color = """ import cadquery as cq result = cq.Workplane("XY" ).box(1, 1, 1) @@ -1037,19 +1091,20 @@ def test_relative_references(main): show_object(result, name ='5', options=dict(alpha=0.5,color=(1.,0,0))) show_object(result, name ='6', options=dict(rgba=(1.,0,0,.5))) show_object(result, name ='7', options=dict(color=('ff','cc','dd'))) -''' +""" + def test_render_colors(main_clean): qtbot, win = main_clean - obj_tree = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - log = win.components['log'] + obj_tree = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + log = win.components["log"] editor.set_text(code_color) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() CQ = obj_tree.CQ @@ -1057,42 +1112,43 @@ def test_render_colors(main_clean): assert not CQ.child(0).ais.HasColor() # object 2 - r,g,b,a = get_rgba(CQ.child(1).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) - assert( g == 0.0 ) + r, g, b, a = get_rgba(CQ.child(1).ais) + assert a == 0.5 + assert r == 1.0 + assert g == 0.0 # object 3 - r,g,b,a = get_rgba(CQ.child(2).ais) - assert( a == 0.5) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(2).ais) + assert a == 0.5 + assert r == 1.0 # object 4 - r,g,b,a = get_rgba(CQ.child(3).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(3).ais) + assert a == 0.5 + assert r == 1.0 # object 5 - r,g,b,a = get_rgba(CQ.child(4).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(4).ais) + assert a == 0.5 + assert r == 1.0 # object 6 - r,g,b,a = get_rgba(CQ.child(5).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(5).ais) + assert a == 0.5 + assert r == 1.0 # check if error occured qtbot.wait(100) - assert('Unknown color format' in log.toPlainText().splitlines()[-1]) + assert "Unknown color format" in log.toPlainText().splitlines()[-1] + def test_render_colors_console(main_clean): qtbot, win = main_clean - obj_tree = win.components['object_tree'] - log = win.components['log'] - console = win.components['console'] + obj_tree = win.components["object_tree"] + log = win.components["log"] + console = win.components["console"] console.execute_command(code_color) @@ -1102,54 +1158,55 @@ def test_render_colors_console(main_clean): assert not CQ.child(0).ais.HasColor() # object 2 - r,g,b,a = get_rgba(CQ.child(1).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(1).ais) + assert a == 0.5 + assert r == 1.0 # object 3 - r,g,b,a = get_rgba(CQ.child(2).ais) - assert( a == 0.5) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(2).ais) + assert a == 0.5 + assert r == 1.0 # object 4 - r,g,b,a = get_rgba(CQ.child(3).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(3).ais) + assert a == 0.5 + assert r == 1.0 # object 5 - r,g,b,a = get_rgba(CQ.child(4).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(4).ais) + assert a == 0.5 + assert r == 1.0 # object 6 - r,g,b,a = get_rgba(CQ.child(5).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(5).ais) + assert a == 0.5 + assert r == 1.0 # check if error occured qtbot.wait(100) - assert('Unknown color format' in log.toPlainText().splitlines()[-1]) + assert "Unknown color format" in log.toPlainText().splitlines()[-1] -code_shading = \ -''' + +code_shading = """ import cadquery as cq res1 = cq.Workplane('XY').box(5, 7, 5) res2 = cq.Workplane('XY').box(8, 5, 4) show_object(res1) show_object(res2,options={"alpha":0}) -''' +""" + def test_shading_aspect(main_clean): qtbot, win = main_clean - obj_tree = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] + obj_tree = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] editor.set_text(code_shading) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() CQ = obj_tree.CQ @@ -1160,147 +1217,148 @@ def test_shading_aspect(main_clean): # verify that they are the same assert ma1.Shininess() == ma2.Shininess() -def test_confirm_new(monkeypatch,editor): + +def test_confirm_new(monkeypatch, editor): qtbot, editor = editor - #check that initial state is as expected - assert(editor.modified == False) + # check that initial state is as expected + assert editor.modified == False editor.document().setPlainText(code) - assert(editor.modified == True) + assert editor.modified == True - #monkeypatch the confirmation dialog and run both scenarios + # monkeypatch the confirmation dialog and run both scenarios def cancel(*args, **kwargs): return QMessageBox.No def ok(*args, **kwargs): return QMessageBox.Yes - monkeypatch.setattr(QMessageBox, 'question', - staticmethod(cancel)) + monkeypatch.setattr(QMessageBox, "question", staticmethod(cancel)) editor.new() - assert(editor.modified == True) - assert(conv_line_ends(editor.get_text_with_eol()) == code) + assert editor.modified == True + assert conv_line_ends(editor.get_text_with_eol()) == code - monkeypatch.setattr(QMessageBox, 'question', - staticmethod(ok)) + monkeypatch.setattr(QMessageBox, "question", staticmethod(ok)) editor.new() - assert(editor.modified == False) - assert(editor.get_text_with_eol() == '') + assert editor.modified == False + assert editor.get_text_with_eol() == "" -code_show_topods = \ -''' + +code_show_topods = """ import cadquery as cq result = cq.Workplane("XY" ).box(1, 1, 1) show_object(result.val().wrapped) -''' +""" + def test_render_topods(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was rendered - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_topods) - debugger._actions['Run'][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 1) + debugger._actions["Run"][0].triggered.emit() + assert obj_tree_comp.CQ.childCount() == 1 # test rendering of topods object via console - console.execute('show(result.val().wrapped)') - assert(obj_tree_comp.CQ.childCount() == 2) + console.execute("show(result.val().wrapped)") + assert obj_tree_comp.CQ.childCount() == 2 # test rendering of list of topods object via console - console.execute('show([result.val().wrapped,result.val().wrapped])') - assert(obj_tree_comp.CQ.childCount() == 3) + console.execute("show([result.val().wrapped,result.val().wrapped])") + assert obj_tree_comp.CQ.childCount() == 3 -code_show_shape_list = \ -''' +code_show_shape_list = """ import cadquery as cq result1 = cq.Workplane("XY" ).box(1, 1, 1).val() result2 = cq.Workplane("XY",origin=(0,1,1)).box(1, 1, 1).val() show_object(result1) show_object([result1,result2]) -''' +""" + def test_render_shape_list(main): qtbot, win = main - log = win.components['log'] + log = win.components["log"] - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_shape_list) - debugger._actions['Run'][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 2) + debugger._actions["Run"][0].triggered.emit() + assert obj_tree_comp.CQ.childCount() == 2 # test rendering of Shape via console - console.execute('show(result1)') - console.execute('show([result1,result2])') - assert(obj_tree_comp.CQ.childCount() == 4) + console.execute("show(result1)") + console.execute("show([result1,result2])") + assert obj_tree_comp.CQ.childCount() == 4 # smoke test exception in show console.execute('show("a")') -code_show_assy = \ -'''import cadquery as cq + +code_show_assy = """import cadquery as cq result1 = cq.Workplane("XY" ).box(3, 3, 0.5) assy = cq.Assembly(result1) show_object(assy) -''' +""" + def test_render_assy(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_assy) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # test rendering via console - console.execute('show(assy)') + console.execute("show(assy)") qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 2) + assert obj_tree_comp.CQ.childCount() == 2 -code_show_ais = \ -'''import cadquery as cq + +code_show_ais = """import cadquery as cq from cadquery.occ_impl.assembly import toCAF import OCP @@ -1312,101 +1370,107 @@ def test_render_assy(main): ais = OCP.XCAFPrs.XCAFPrs_AISObject(lab) show_object(ais) -''' +""" + def test_render_ais(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_ais) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # test rendering via console - console.execute('show(ais)') + console.execute("show(ais)") qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 2) + assert obj_tree_comp.CQ.childCount() == 2 + -code_show_sketch = \ -'''import cadquery as cq +code_show_sketch = """import cadquery as cq s1 = cq.Sketch().rect(1,1) s2 = cq.Sketch().segment((0,0), (0,3.),"s1") show_object(s1) show_object(s2) -''' +""" + def test_render_sketch(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_sketch) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 2) + assert obj_tree_comp.CQ.childCount() == 2 # test rendering via console - console.execute('show(s1); show(s2)') + console.execute("show(s1); show(s2)") qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 4) + assert obj_tree_comp.CQ.childCount() == 4 + def test_window_title(monkeypatch, main): - fname = 'test_window_title.py' + fname = "test_window_title.py" - with open(fname, 'w') as f: + with open(fname, "w") as f: f.write(code) qtbot, win = main - #monkeypatch QFileDialog methods + # monkeypatch QFileDialog methods def filename(*args, **kwargs): return fname, None - monkeypatch.setattr(QFileDialog, 'getOpenFileName', - staticmethod(filename)) + monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename)) win.components["editor"].open() - assert(win.windowTitle().endswith(fname)) + assert win.windowTitle().endswith(fname) # handle a new file win.components["editor"].new() # I don't really care what the title is, as long as it's not a filename - assert(not win.windowTitle().endswith('.py')) + assert not win.windowTitle().endswith(".py") + def test_module_discovery(tmp_path, editor): qtbot, editor = editor - with open(tmp_path.joinpath('main.py'), 'w') as f: - f.write('import b') + with open(tmp_path.joinpath("main.py"), "w") as f: + f.write("import b") + + assert editor.get_imported_module_paths(str(tmp_path.joinpath("main.py"))) == [] - assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [] + tmp_path.joinpath("b.py").touch() - tmp_path.joinpath('b.py').touch() + assert editor.get_imported_module_paths(str(tmp_path.joinpath("main.py"))) == [ + str(tmp_path.joinpath("b.py")) + ] - assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [str(tmp_path.joinpath('b.py'))] def test_launch_syntax_error(tmp_path): @@ -1421,23 +1485,23 @@ def test_launch_syntax_error(tmp_path): editor.load_from_file(inputfile) win.show() - assert(win.isVisible()) + assert win.isVisible() -code_import_module_makebox = \ -""" + +code_import_module_makebox = """ from module_makebox import * z = 1 r = makebox(z) """ -code_module_makebox = \ -""" +code_module_makebox = """ import cadquery as cq def makebox(z): zval = z + 1 return cq.Workplane().box(1, 1, zval) """ + def test_reload_import_handle_error(tmp_path, main): TIMEOUT = 500 @@ -1458,18 +1522,18 @@ def test_reload_import_handle_error(tmp_path, main): # run, verify that no exception was generated editor.load_from_file(script) debugger._actions["Run"][0].triggered.emit() - assert(traceback_view.current_exception.text() == "") + assert traceback_view.current_exception.text() == "" # save the module with an error with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): lines = code_module_makebox.splitlines() - lines.remove(" zval = z + 1") # introduce NameError + lines.remove(" zval = z + 1") # introduce NameError lines = "\n".join(lines) modify_file(lines, module_file) # verify NameError is generated debugger._actions["Run"][0].triggered.emit() - assert("NameError" in traceback_view.current_exception.text()) + assert "NameError" in traceback_view.current_exception.text() # revert the error, verify rerender is triggered with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): @@ -1477,7 +1541,8 @@ def test_reload_import_handle_error(tmp_path, main): # verify that no exception was generated debugger._actions["Run"][0].triggered.emit() - assert(traceback_view.current_exception.text() == "") + assert traceback_view.current_exception.text() == "" + def test_modulefinder(tmp_path, main): @@ -1486,7 +1551,7 @@ def test_modulefinder(tmp_path, main): editor = win.components["editor"] debugger = win.components["debugger"] traceback_view = win.components["traceback_viewer"] - log = win.components['log'] + log = win.components["log"] editor.autoreload(True) editor.preferences["Autoreload: watch imported modules"] = True @@ -1499,56 +1564,58 @@ def test_modulefinder(tmp_path, main): modify_file("import emptydir", script) qtbot.wait(100) - assert("Cannot determine imported modules" in log.toPlainText().splitlines()[-1]) + assert "Cannot determine imported modules" in log.toPlainText().splitlines()[-1] + def test_show_all(main): qtbot, win = main - editor = win.components['editor'] - debugger = win.components['debugger'] - object_tree = win.components['object_tree'] + editor = win.components["editor"] + debugger = win.components["debugger"] + object_tree = win.components["object_tree"] # remove all objects object_tree.removeObjects() - assert(object_tree.CQ.childCount() == 0) + assert object_tree.CQ.childCount() == 0 # add code wtih Shape, Workplane, Assy, Sketch editor.set_text(code_show_all) # Run and check if all are shown - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(object_tree.CQ.childCount() == 4) + assert object_tree.CQ.childCount() == 4 -code_randcolor = \ -"""import cadquery as cq + +code_randcolor = """import cadquery as cq b = cq.Workplane().box(8, 3, 4) for i in range(10): show_object(b.translate((0,5*i,0)), options=rand_color(alpha=0)) show_object(b.translate((0,5*i,0)), options=rand_color(0, True)) """ + def test_randcolor(main): - + qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_randcolor) - debugger._actions['Run'][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 2*10) + debugger._actions["Run"][0].triggered.emit() + assert obj_tree_comp.CQ.childCount() == 2 * 10 -code_show_wo_name = \ -""" + +code_show_wo_name = """ import cadquery as cq res = cq.Workplane().box(1,1,1) @@ -1557,28 +1624,29 @@ def test_randcolor(main): show_object(cq.Workplane().box(1,1,1)) """ + def test_show_without_name(main): qtbot, win = main - editor = win.components['editor'] - debugger = win.components['debugger'] - object_tree = win.components['object_tree'] + editor = win.components["editor"] + debugger = win.components["debugger"] + object_tree = win.components["object_tree"] # remove all objects object_tree.removeObjects() - assert(object_tree.CQ.childCount() == 0) + assert object_tree.CQ.childCount() == 0 # add code wtih Shape, Workplane, Assy, Sketch editor.set_text(code_show_wo_name) # Run and check if all are shown - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(object_tree.CQ.childCount() == 2) + assert object_tree.CQ.childCount() == 2 # Check the name of the first object - assert(object_tree.CQ.child(0).text(0) == "res") + assert object_tree.CQ.child(0).text(0) == "res" # Check that the name of the seconf object is an int int(object_tree.CQ.child(1).text(0)) From 27ae6b627b8d142b5a342fabc5dd5d65293eaccc Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Mon, 17 Feb 2025 08:43:53 -0500 Subject: [PATCH 108/134] Setting up for the 0.4.0 release --- README.md | 14 +++++++++----- changes.md | 11 +++++++++++ cq_editor/_version.py | 2 +- pyproject.toml | 2 +- 4 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 changes.md diff --git a/README.md b/README.md index 3a128889..afc24e5e 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,6 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. ## Installation - Pre-Built Packages (Recommended) -~~### Release Packages~~ - -~~Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download installer for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly.~~ - ### Development Packages Development builds are also available, but can be unstable and should be used at your own risk. You can download the newest build [here](https://github.com/CadQuery/CQ-editor/releases/tag/nightly). Install and run the `run.sh` (Linux/MacOS) or `run.bat` (Windows) script in the root CQ-editor directory. The CQ-editor window should launch. @@ -59,7 +55,7 @@ micromamba install -n base -c cadquery cq-editor micromamba run -n base cq-editor ``` -On some linux distributions (e.g. `Ubuntu 18.04`) it might be necessary to install additonal packages: +On some linux distributions (e.g. `Ubuntu 18.04`+) it might be necessary to install additonal packages: ``` sudo apt install libglu1-mesa libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev ``` @@ -68,6 +64,14 @@ On Fedora 29 the packages can be installed as follows: dnf install -y mesa-libGLU mesa-libGL mesa-libGLU-devel ``` +## Installation (pip) + +A newer installation option (starting with 0.3.0) is to install via pip. Being a newer option, there may be issues that are not present with the conda installation method. It has worked well in testing so far though. + +``` +pip install CQ-editor +``` + ## Usage ### Showing Objects diff --git a/changes.md b/changes.md new file mode 100644 index 00000000..9d507c4b --- /dev/null +++ b/changes.md @@ -0,0 +1,11 @@ +# Release 0.4.0 + +* Updated version pins in order to fix some issues, including segfaults on Python 3.12 +* Changed to forcing UTF-8 when saving (#480) +* Added `Toggle Comment` Edit menu item with a `Ctrl+/` hotkey (#481) +* Fixed the case where long exceptions would force the window to expand (#481) +* Add stdout redirect so that print statements could be used and passed to log viewer (#481) +* Improvements in stdout redirect method (#483 and #485) +* Fixed preferences drop downs not populating (#484) +* Fixed double-render calls on saves in some cases (#486) +* Added a lint check to CI and linted codebase (#487) diff --git a/cq_editor/_version.py b/cq_editor/_version.py index 116884f3..6a9beea8 100644 --- a/cq_editor/_version.py +++ b/cq_editor/_version.py @@ -1 +1 @@ -__version__ = "0.4.dev0" +__version__ = "0.4.0" diff --git a/pyproject.toml b/pyproject.toml index 693a18e7..cfa8146e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "CQ-editor" -version = "0.4.dev0" +version = "0.4.0" dependencies = [ "cadquery", "pyqtgraph", From e3430a61ba3c788185351e668203f0b320f23232 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Mon, 17 Feb 2025 14:11:35 -0500 Subject: [PATCH 109/134] Moved back to development version --- cq_editor/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cq_editor/_version.py b/cq_editor/_version.py index 6a9beea8..9aea0d23 100644 --- a/cq_editor/_version.py +++ b/cq_editor/_version.py @@ -1 +1 @@ -__version__ = "0.4.0" +__version__ = "0.5.dev0" diff --git a/pyproject.toml b/pyproject.toml index cfa8146e..0a4a39b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "CQ-editor" -version = "0.4.0" +version = "0.5.dev0" dependencies = [ "cadquery", "pyqtgraph", From f2fc7d4e02744a895588c5d4d9bb33a4d7651c0e Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Mon, 17 Feb 2025 17:33:40 -0500 Subject: [PATCH 110/134] Initial attempt at dark theme --- cq_editor/main_window.py | 72 +++++++++++++++++++++++++++++++++++- cq_editor/preferences.py | 3 ++ cq_editor/widgets/console.py | 29 +++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 9b1e7907..478ec546 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,7 +1,8 @@ import sys from PyQt5.QtCore import QObject, pyqtSignal -from PyQt5.QtWidgets import QLabel, QMainWindow, QToolBar, QDockWidget, QAction +from PyQt5.QtGui import QPalette, QColor +from PyQt5.QtWidgets import QLabel, QMainWindow, QToolBar, QDockWidget, QAction, QApplication from logbook import Logger import cadquery as cq @@ -25,6 +26,7 @@ ) from .mixins import MainMixin from .icons import icon +from pyqtgraph.parametertree import Parameter from .preferences import PreferencesWidget @@ -55,6 +57,21 @@ class MainWindow(QMainWindow, MainMixin): name = "CQ-Editor" org = "CadQuery" + preferences = Parameter.create( + name="Preferences", + children=[ + { + "name": "Light/Dark Theme", + "type": "list", + "value": "Light", + "values": [ + "Light", + "Dark", + ], + }, + ], + ) + def __init__(self, parent=None, filename=None): super(MainWindow, self).__init__(parent) @@ -88,6 +105,9 @@ def __init__(self, parent=None, filename=None): self.setup_logging() + # Allows us to react to the top-level settings for this window being changed + self.preferences.sigTreeStateChanged.connect(self.preferencesChanged) + self.restorePreferences() self.restoreWindow() @@ -101,6 +121,56 @@ def __init__(self, parent=None, filename=None): self.restoreComponentState() + + def preferencesChanged(self, param, changes): + """ + Triggered when the preferences for this window are changed. + """ + + # Use the default light theme/palette + if self.preferences['Light/Dark Theme'] == 'Light': + QApplication.instance().setStyleSheet("") + QApplication.instance().setPalette(QApplication.style().standardPalette()) + + # The console theme needs to be changed separately + self.components['console'].app_theme_changed('Light') + # Use the dark theme/palette + elif self.preferences['Light/Dark Theme'] == 'Dark': + QApplication.instance().setStyle("Fusion") + + # Now use a palette to switch to dark colors: + white_color = QColor(255, 255, 255) + black_color = QColor(0, 0, 0) + red_color = QColor(255, 0, 0) + palette = QPalette() + palette.setColor(QPalette.Window, QColor(53, 53, 53)) + palette.setColor(QPalette.WindowText, white_color) + palette.setColor(QPalette.Base, QColor(25, 25, 25)) + palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) + palette.setColor(QPalette.ToolTipBase, black_color) + palette.setColor(QPalette.ToolTipText, white_color) + palette.setColor(QPalette.Text, white_color) + palette.setColor(QPalette.Button, QColor(53, 53, 53)) + palette.setColor(QPalette.ButtonText, white_color) + palette.setColor(QPalette.BrightText, red_color) + palette.setColor(QPalette.Link, QColor(42, 130, 218)) + palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) + palette.setColor(QPalette.HighlightedText, black_color) + QApplication.instance().setPalette(palette) + + # The console theme needs to be changed separately + self.components['console'].app_theme_changed('Dark') + + # We alter the color of the toolbar separately to avoid having separate dark theme icons + p = self.toolbar.palette() + if self.preferences['Light/Dark Theme'] == 'Dark': + p.setColor(QPalette.Button, QColor(120, 120, 120)) + p.setColor(QPalette.Background, QColor(120, 120, 120)) + else: + p = QApplication.instance().style().standardPalette() + + self.toolbar.setPalette(p) + def closeEvent(self, event): self.saveWindow() diff --git a/cq_editor/preferences.py b/cq_editor/preferences.py index 4591a6d2..65f68ef5 100644 --- a/cq_editor/preferences.py +++ b/cq_editor/preferences.py @@ -88,6 +88,9 @@ def add(self, name, component): "OverUnder", ] ) + # Fill the light/dark theme in the general settings + elif child.name() == "Light/Dark Theme": + child.setLimits(["Light", "Dark"]) @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) def handleSelection(self, item, *args): diff --git a/cq_editor/widgets/console.py b/cq_editor/widgets/console.py index 3ed51a74..85c362fb 100644 --- a/cq_editor/widgets/console.py +++ b/cq_editor/widgets/console.py @@ -18,6 +18,26 @@ def __init__(self, customBanner=None, namespace=dict(), *args, **kwargs): # self.banner = customBanner self.font_size = 6 + self.style_sheet = ''' + ''' + self.syntax_style = 'zenburn' + self.kernel_manager = kernel_manager = QtInProcessKernelManager() kernel_manager.start_kernel(show_banner=False) kernel_manager.kernel.gui = "qt" @@ -67,6 +87,15 @@ def _banner_default(self): return "" + def app_theme_changed(self, theme): + """ + Allows this console to be changed to match the light or dark theme of the rest of the app. + """ + + if theme == 'Dark': + self.set_default_style('linux') + else: + self.set_default_style('lightbg') if __name__ == "__main__": From b436fea872245f2f60ce5efa138e239c7dd899ae Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Mon, 17 Feb 2025 17:34:25 -0500 Subject: [PATCH 111/134] Lint pass --- cq_editor/main_window.py | 20 +++++++++++++------- cq_editor/widgets/console.py | 13 +++++++------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 478ec546..9bb3fc5b 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -2,7 +2,14 @@ from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtGui import QPalette, QColor -from PyQt5.QtWidgets import QLabel, QMainWindow, QToolBar, QDockWidget, QAction, QApplication +from PyQt5.QtWidgets import ( + QLabel, + QMainWindow, + QToolBar, + QDockWidget, + QAction, + QApplication, +) from logbook import Logger import cadquery as cq @@ -121,21 +128,20 @@ def __init__(self, parent=None, filename=None): self.restoreComponentState() - def preferencesChanged(self, param, changes): """ Triggered when the preferences for this window are changed. """ # Use the default light theme/palette - if self.preferences['Light/Dark Theme'] == 'Light': + if self.preferences["Light/Dark Theme"] == "Light": QApplication.instance().setStyleSheet("") QApplication.instance().setPalette(QApplication.style().standardPalette()) # The console theme needs to be changed separately - self.components['console'].app_theme_changed('Light') + self.components["console"].app_theme_changed("Light") # Use the dark theme/palette - elif self.preferences['Light/Dark Theme'] == 'Dark': + elif self.preferences["Light/Dark Theme"] == "Dark": QApplication.instance().setStyle("Fusion") # Now use a palette to switch to dark colors: @@ -159,11 +165,11 @@ def preferencesChanged(self, param, changes): QApplication.instance().setPalette(palette) # The console theme needs to be changed separately - self.components['console'].app_theme_changed('Dark') + self.components["console"].app_theme_changed("Dark") # We alter the color of the toolbar separately to avoid having separate dark theme icons p = self.toolbar.palette() - if self.preferences['Light/Dark Theme'] == 'Dark': + if self.preferences["Light/Dark Theme"] == "Dark": p.setColor(QPalette.Button, QColor(120, 120, 120)) p.setColor(QPalette.Background, QColor(120, 120, 120)) else: diff --git a/cq_editor/widgets/console.py b/cq_editor/widgets/console.py index 85c362fb..bea77af7 100644 --- a/cq_editor/widgets/console.py +++ b/cq_editor/widgets/console.py @@ -18,7 +18,7 @@ def __init__(self, customBanner=None, namespace=dict(), *args, **kwargs): # self.banner = customBanner self.font_size = 6 - self.style_sheet = ''' - ''' - self.syntax_style = 'zenburn' + """ + self.syntax_style = "zenburn" self.kernel_manager = kernel_manager = QtInProcessKernelManager() kernel_manager.start_kernel(show_banner=False) @@ -92,10 +92,11 @@ def app_theme_changed(self, theme): Allows this console to be changed to match the light or dark theme of the rest of the app. """ - if theme == 'Dark': - self.set_default_style('linux') + if theme == "Dark": + self.set_default_style("linux") else: - self.set_default_style('lightbg') + self.set_default_style("lightbg") + if __name__ == "__main__": From f8c3e0847da0bed686585f65d3b8fad07e35a1f8 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 18 Feb 2025 07:02:52 -0500 Subject: [PATCH 112/134] Added a test for light-dark theme switching --- tests/test_app.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_app.py b/tests/test_app.py index ef547c91..0e255f6f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1664,3 +1664,34 @@ def test_print_redirect(main): qtbot.wait(100) assert "foo\nbar" in log.toPlainText() + + +def test_light_dark_mode(main): + """ + Tests that the app does switch between light and dark mode. + """ + from PyQt5.QtGui import QPalette + + qtbot, win = main + + # Change to dark mode + win.preferences["Light/Dark Theme"] = "Dark" + win.preferencesChanged(None, None) + + # Retireve the toolbar so that we can check its style + toolbar = win.toolbar + + # Get the dark mode stylesheet for the toolbar + dark_pal = win.toolbar.palette() + dark_bg = dark_pal.color(QPalette.Background).rgb() + + # Change to light mode + win.preferences["Light/Dark Theme"] = "Light" + win.preferencesChanged(None, None) + + # Get the light mode stylesheet for the toolbar + light_pal = win.toolbar.palette() + light_bg = light_pal.color(QPalette.Background).rgb() + + # Check that the dark mode stylesheet is different from the light mode stylesheet + assert dark_bg != light_bg From e1cc945cae982da8d143c600dcf1ec28a147d196 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 18 Feb 2025 11:32:12 -0500 Subject: [PATCH 113/134] Trying to pin the version of OCP to see if it fixes the test CI --- cqgui_env.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/cqgui_env.yml b/cqgui_env.yml index dd7351e9..78159ed4 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -11,4 +11,5 @@ dependencies: - logbook - requests - cadquery=master + - ocp=7.8.1.1 - qtconsole >=5.5.1,<5.6.0 From eecd0afec7735520ee6e9be55ea069ae9904e150 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 18 Feb 2025 11:47:08 -0500 Subject: [PATCH 114/134] Pinning 7.8.1.1 did not help, making cadquery pin match pyproject.toml --- cqgui_env.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cqgui_env.yml b/cqgui_env.yml index 78159ed4..9121df2a 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -10,6 +10,5 @@ dependencies: - path - logbook - requests - - cadquery=master - - ocp=7.8.1.1 + - cadquery - qtconsole >=5.5.1,<5.6.0 From 56b9cf7e45352bbe2f2f97efc3ec7c90ae52c136 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 18 Feb 2025 17:50:34 -0500 Subject: [PATCH 115/134] Updating Linux and Windows runner versions --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 60ee6084..2824bb6d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,8 +1,8 @@ shallow_clone: false image: - - Ubuntu2004 - - Visual Studio 2015 + - Ubuntu2204 + - Visual Studio 2019 environment: matrix: From 6cfee42d9569b0bb31dc2d9383986c5f08f09285 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 20 Feb 2025 12:40:39 -0500 Subject: [PATCH 116/134] The cursor no longer resets to the beginning of the document on save --- cq_editor/widgets/editor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 2ed9b788..9cb8667e 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -5,7 +5,7 @@ from spyder.plugins.editor.widgets.codeeditor import CodeEditor from PyQt5.QtCore import pyqtSignal, QFileSystemWatcher, QTimer from PyQt5.QtWidgets import QAction, QFileDialog -from PyQt5.QtGui import QFontDatabase +from PyQt5.QtGui import QFontDatabase, QTextCursor from path import Path import sys @@ -254,7 +254,21 @@ def _watch_paths(self): def _file_changed(self): # neovim writes a file by removing it first so must re-add each time self._watch_paths() + + # Save the current cursor position and selection + cursor = self.textCursor() + cursor_position = cursor.position() + anchor_position = cursor.anchor() + + # Reload the file in case it was modified by an external editor self.set_text_from_file(self._filename) + + # Restore the cursor position and selection + cursor.setPosition(anchor_position) + cursor.setPosition(cursor_position, QTextCursor.KeepAnchor) + self.setTextCursor(cursor) + + # Reset the dirty state and trigger a 3D render self.reset_modified() self.triggerRerender.emit(True) From 9fd1f0fa1fb8660ebaa1f100b5d2e7c78967a85d Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 27 Feb 2025 20:47:19 -0500 Subject: [PATCH 117/134] Added max line length setting and set it to 88 by default to match black --- cq_editor/widgets/editor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 9cb8667e..b73c6bf1 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -45,6 +45,7 @@ class Editor(CodeEditor, ComponentMixin): "values": ["Spyder", "Monokai", "Zenburn"], "value": "Spyder", }, + {"name": "Maximum line length", "type": "int", "value": 88}, ], ) @@ -60,7 +61,7 @@ def __init__(self, parent=None): self.setup_editor( linenumbers=True, markers=True, - edge_line=False, + edge_line=self.preferences["Maximum line length"], tab_mode=False, show_blanks=True, font=QFontDatabase.systemFont(QFontDatabase.FixedFont), @@ -139,6 +140,10 @@ def updatePreferences(self, *args): self.toggle_wrap_mode(self.preferences["Line wrap"]) + # Update the edge line (maximum line length) + self.edge_line.set_enabled(True) + self.edge_line.set_columns(self.preferences["Maximum line length"]) + self._clear_watched_paths() self._watch_paths() From 9a6c401d1c36332d9f48d2dda6e7166f3d22e0e1 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Fri, 28 Feb 2025 08:54:53 -0500 Subject: [PATCH 118/134] Fixed scroll position jumping during file reload --- cq_editor/widgets/editor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index b73c6bf1..b2a07c24 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -265,6 +265,10 @@ def _file_changed(self): cursor_position = cursor.position() anchor_position = cursor.anchor() + # Save the current scroll position + vertical_scroll_pos = self.verticalScrollBar().value() + horizontal_scroll_pos = self.horizontalScrollBar().value() + # Reload the file in case it was modified by an external editor self.set_text_from_file(self._filename) @@ -273,6 +277,10 @@ def _file_changed(self): cursor.setPosition(cursor_position, QTextCursor.KeepAnchor) self.setTextCursor(cursor) + # Restore the scroll position + self.verticalScrollBar().setValue(vertical_scroll_pos) + self.horizontalScrollBar().setValue(horizontal_scroll_pos) + # Reset the dirty state and trigger a 3D render self.reset_modified() self.triggerRerender.emit(True) From 0c2216b62d25749d74a4844ea05a91565cabf1d1 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Fri, 28 Feb 2025 09:03:53 -0500 Subject: [PATCH 119/134] Added Wayland work-around to README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index afc24e5e..b1f6d83d 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,17 @@ A newer installation option (starting with 0.3.0) is to install via pip. Being a pip install CQ-editor ``` +## Running + +Whether CQ-editor was installed via conda or pip, it should now be possible to start the application using the following command. +```bash +CQ-editor +``` +If you are running a Linux distribution which uses Wayland, you may get an error. The following command line is a work-around for Wayland issues. +```bash +CQ-editor --platform xcb +``` + ## Usage ### Showing Objects From 4158df07517e883378dcbc1477bd6b757dad6758 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Fri, 28 Feb 2025 21:11:09 -0500 Subject: [PATCH 120/134] Accidental Drag Select Fix (#498) * Fixed accidental drag selects --- cq_editor/widgets/occt_widget.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cq_editor/widgets/occt_widget.py b/cq_editor/widgets/occt_widget.py index cd8215f3..f40fbc0e 100755 --- a/cq_editor/widgets/occt_widget.py +++ b/cq_editor/widgets/occt_widget.py @@ -76,6 +76,10 @@ def mousePressEvent(self, event): pos = event.pos() if event.button() == Qt.LeftButton: + # Used to prevent drag selection of objects + self.pending_select = True + self.left_press = pos + self.view.StartRotation(pos.x(), pos.y()) elif event.button() == Qt.RightButton: self.view.StartZoomAtPoint(pos.x(), pos.y()) @@ -90,6 +94,10 @@ def mouseMoveEvent(self, event): if event.buttons() == Qt.LeftButton: self.view.Rotation(x, y) + # If the user moves the mouse at all, the selection will not happen + if abs(x - self.left_press.x()) > 2 or abs(y - self.left_press.y()) > 2: + self.pending_select = False + elif event.buttons() == Qt.MiddleButton: self.view.Pan(x - self.old_pos.x(), self.old_pos.y() - y, theToStart=True) @@ -104,9 +112,10 @@ def mouseReleaseEvent(self, event): pos = event.pos() x, y = pos.x(), pos.y() - self.context.MoveTo(x, y, self.view, True) - - self._handle_selection() + # Only make the selection if the user has not moved the mouse + if self.pending_select: + self.context.MoveTo(x, y, self.view, True) + self._handle_selection() def _handle_selection(self): From 50fd73a33bbdc51b30a9d1274d23bf89d381df38 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sat, 1 Mar 2025 21:20:12 -0500 Subject: [PATCH 121/134] Fixed alpha setting being backwards in show_object (#499) * Fixed alpha setting being backwards in show_object * Trying to fix tests affected by color/alpha changes * Fixed another test --- cq_editor/cq_utils.py | 2 +- cq_editor/widgets/object_tree.py | 30 +++++++++++++++++------------- tests/test_app.py | 10 +++++----- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 88c64483..0439c7d3 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -178,7 +178,7 @@ def set_transparency(ais: AIS_Shape, alpha: float) -> AIS_Shape: drawer = ais.Attributes() drawer.SetupOwnShadingAspect() - drawer.ShadingAspect().SetTransparency(alpha) + drawer.ShadingAspect().SetTransparency(1.0 - alpha) return ais diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 8725d50a..f3dcb57b 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -38,9 +38,9 @@ def __init__(self, *args, **kwargs): class ObjectTreeItem(QTreeWidgetItem): props = [ - {"name": "Name", "type": "str", "value": ""}, - {"name": "Color", "type": "color", "value": "#f4a824"}, - {"name": "Alpha", "type": "float", "value": 0, "limits": (0, 1), "step": 1e-1}, + {"name": "Name", "type": "str", "value": "", "readonly": True}, + # {"name": "Color", "type": "color", "value": "#f4a824"}, + # {"name": "Alpha", "type": "float", "value": 0, "limits": (0, 1), "step": 1e-1}, {"name": "Visible", "type": "bool", "value": True}, ] @@ -68,12 +68,14 @@ def __init__( self.properties = Parameter.create(name="Properties", children=self.props) self.properties["Name"] = name - self.properties["Alpha"] = ais.Transparency() - self.properties["Color"] = ( - get_occ_color(ais) - if ais and ais.HasColor() - else get_occ_color(DEFAULT_FACE_COLOR) - ) + # Alpha and Color from this panel fight with the options in show_object and so they are + # disabled for now until a better solution is found + # self.properties["Alpha"] = ais.Transparency() + # self.properties["Color"] = ( + # get_occ_color(ais) + # if ais and ais.HasColor() + # else get_occ_color(DEFAULT_FACE_COLOR) + # ) self.properties.sigTreeStateChanged.connect(self.propertiesChanged) def propertiesChanged(self, properties, changed): @@ -81,12 +83,14 @@ def propertiesChanged(self, properties, changed): changed_prop = changed[0][0] self.setData(0, 0, self.properties["Name"]) - self.ais.SetTransparency(self.properties["Alpha"]) - if changed_prop.name() == "Color": - set_color(self.ais, to_occ_color(self.properties["Color"])) + # if changed_prop.name() == "Alpha": + # self.ais.SetTransparency(self.properties["Alpha"]) + + # if changed_prop.name() == "Color": + # set_color(self.ais, to_occ_color(self.properties["Color"])) - self.ais.Redisplay() + # self.ais.Redisplay() if self.properties["Visible"]: self.setCheckState(0, Qt.Checked) diff --git a/tests/test_app.py b/tests/test_app.py index 0e255f6f..862bb42c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -533,7 +533,7 @@ def check_no_error_occured(): # object 1 (defualt color) r, g, b, a = get_rgba(CQ.child(0).ais) - assert a == pytest.approx(0.2) + # assert a == pytest.approx(0.2) assert r == 1.0 assert variables.model().rowCount() == 2 @@ -892,16 +892,16 @@ def test_preserve_properties(main): assert object_tree.CQ.childCount() == 1 props = object_tree.CQ.child(0).properties props["Visible"] = False - props["Color"] = "#caffee" - props["Alpha"] = 0.5 + # props["Color"] = "#caffee" + # props["Alpha"] = 0.5 debugger._actions["Run"][0].triggered.emit() assert object_tree.CQ.childCount() == 1 props = object_tree.CQ.child(0).properties assert props["Visible"] == False - assert props["Color"].name() == "#caffee" - assert props["Alpha"] == 0.5 + # assert props["Color"].name() == "#caffee" + # assert props["Alpha"] == 0.5 def test_selection(main_multi, mocker): From 2198069e3fde13b660124496f32c6d68b9bf5b7e Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 5 Mar 2025 16:16:26 -0500 Subject: [PATCH 122/134] Added log and console clear and lighter menus (#500) * Added log and console clear and lighter menus * Fixed an indentation bug related to the mouse buttons * Better way to clear the console and fixed theme switching bug * Separate reset from clear * Simplified test of console reset --- cq_editor/main_window.py | 28 ++++++++++++++++++++-------- cq_editor/widgets/console.py | 17 ++++++++++++++++- cq_editor/widgets/log.py | 16 +++++++++++++++- cq_editor/widgets/occt_widget.py | 6 +++--- tests/test_app.py | 15 +++++++++++++++ 5 files changed, 69 insertions(+), 13 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 9bb3fc5b..683e59cb 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -9,6 +9,7 @@ QDockWidget, QAction, QApplication, + QMenu, ) from logbook import Logger import cadquery as cq @@ -170,10 +171,21 @@ def preferencesChanged(self, param, changes): # We alter the color of the toolbar separately to avoid having separate dark theme icons p = self.toolbar.palette() if self.preferences["Light/Dark Theme"] == "Dark": - p.setColor(QPalette.Button, QColor(120, 120, 120)) p.setColor(QPalette.Background, QColor(120, 120, 120)) + + # TWeak the QMenu items palette for dark theme + menu_palette = self.menuBar().palette() + menu_palette.setColor(QPalette.Base, QColor(80, 80, 80)) + for menu in self.menuBar().findChildren(QMenu): + menu.setPalette(menu_palette) else: - p = QApplication.instance().style().standardPalette() + p.setColor(QPalette.Background, QColor(240, 240, 240)) + + # Revert the QMenu items palette for dark theme + menu_palette = self.menuBar().palette() + menu_palette.setColor(QPalette.Base, QColor(240, 240, 240)) + for menu in self.menuBar().findChildren(QMenu): + menu.setPalette(menu_palette) self.toolbar.setPalette(p) @@ -209,12 +221,6 @@ def prepare_panes(self): lambda c: dock(c, "Objects", self, defaultArea="right"), ) - self.registerComponent( - "console", - ConsoleWidget(self), - lambda c: dock(c, "Console", self, defaultArea="bottom"), - ) - self.registerComponent( "traceback_viewer", TracebackPane(self), @@ -223,6 +229,12 @@ def prepare_panes(self): self.registerComponent("debugger", Debugger(self)) + self.registerComponent( + "console", + ConsoleWidget(self), + lambda c: dock(c, "Console", self, defaultArea="bottom"), + ) + self.registerComponent( "variables_viewer", LocalsView(self), diff --git a/cq_editor/widgets/console.py b/cq_editor/widgets/console.py index bea77af7..fea588e5 100644 --- a/cq_editor/widgets/console.py +++ b/cq_editor/widgets/console.py @@ -1,4 +1,4 @@ -from PyQt5.QtWidgets import QApplication +from PyQt5.QtWidgets import QApplication, QAction from PyQt5.QtCore import pyqtSlot from qtconsole.rich_jupyter_widget import RichJupyterWidget @@ -6,6 +6,8 @@ from ..mixins import ComponentMixin +from ..icons import icon + class ConsoleWidget(RichJupyterWidget, ComponentMixin): @@ -17,6 +19,13 @@ def __init__(self, customBanner=None, namespace=dict(), *args, **kwargs): # if not customBanner is None: # self.banner = customBanner + self._actions = { + "Run": [ + QAction( + icon("delete"), "Clear Console", self, triggered=self.reset_console + ), + ] + } self.font_size = 6 self.style_sheet = """