├── .coveragerc
├── .github
└── workflows
│ ├── appimage-build.yml
│ ├── pyinstaller-build-pip.yml
│ └── pyinstaller-builds-actions.yml
├── .gitignore
├── LICENSE
├── README.md
├── appimage
├── CQ-editor.desktop
├── appimage-build.sh
└── cq_logo.png
├── appveyor.yml
├── azure-pipelines.yml
├── bundle.py
├── collect_icons.py
├── conda
├── construct.yaml
├── meta.yaml
├── run.bat
└── run.sh
├── cq_editor
├── __init__.py
├── __main__.py
├── _version.py
├── cq_utils.py
├── icons.py
├── icons_res.py
├── main_window.py
├── mixins.py
├── preferences.py
├── utils.py
└── widgets
│ ├── __init__.py
│ ├── console.py
│ ├── cq_object_inspector.py
│ ├── debugger.py
│ ├── editor.py
│ ├── log.py
│ ├── object_tree.py
│ ├── occt_widget.py
│ ├── traceback_viewer.py
│ └── viewer.py
├── cqgui_env.yml
├── environment.yml
├── icons
├── back_view.svg
├── bottom_view.svg
├── cadquery_logo_dark.ico
├── cadquery_logo_dark.svg
├── front_view.svg
├── isometric_view.svg
├── left_side_view.svg
├── right_side_view.svg
└── top_view.svg
├── pyinstaller.spec
├── pyinstaller
├── CQ-editor.cmd
├── CQ-editor.sh
├── extrahooks
│ └── hook-casadi.py
├── pyi_rth_fontconfig.py
└── pyi_rth_occ.py
├── pyinstaller_pip.spec
├── pytest.ini
├── run.py
├── runtests_locally.sh
├── screenshots
├── screenshot1.png
├── screenshot2.png
├── screenshot3.png
└── screenshot4.png
├── setup.py
└── tests
└── test_app.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | timid = True
3 | branch = True
4 | source = src
5 |
6 | [report]
7 | exclude_lines =
8 | if __name__ == .__main__.:
9 |
--------------------------------------------------------------------------------
/.github/workflows/appimage-build.yml:
--------------------------------------------------------------------------------
1 | name: appimage-build
2 | on:
3 | schedule:
4 | - cron: '0 0 * * 1'
5 | workflow_dispatch:
6 |
7 | jobs:
8 | build-linux:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: conda-incubator/setup-miniconda@v2
13 | with:
14 | # mamba-version: "*"
15 | miniconda-version: latest
16 | channels: conda-forge,defaults
17 | channel-priority: true
18 | auto-update-conda: true
19 | python-version: 3.9
20 | activate-environment: test
21 | - name: Install CadQuery, CQ-editor and pyinstaller
22 | shell: bash --login {0}
23 | run: |
24 | sudo apt install -y libblas-dev libblas3 libblas64-3 libblas64-dev
25 | sudo apt install -y libxkbcommon0
26 | sudo apt install -y libxkbcommon-x11-0
27 | sudo apt install -y libxcb-xinerama0
28 | conda info
29 | conda install -c cadquery -c conda-forge cq-editor=master cadquery=master ipython=8.4.0 jedi=0.17.2 pyqtgraph=0.12.4 python=3.9
30 | conda install -c conda-forge pyinstaller=4.10
31 | conda uninstall --force -y importlib_resources
32 | pip install path
33 | pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
34 | pip install git+https://github.com/meadiode/cq_gears.git@main
35 | pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache"
36 | - name: Run build
37 | shell: bash --login {0}
38 | run: |
39 | conda info
40 | /home/runner/work/CQ-editor/CQ-editor/appimage/appimage-build.sh
41 | ls /home/runner/work/CQ-editor/CQ-editor/AppDir
42 | # ls /home/runner/work/CQ-editor/CQ-editor/CQ-editor-0.3-dev-x86_64.AppImage
43 | - uses: actions/upload-artifact@v2
44 | with:
45 | name: CQ-editor-dev-Linux-x86_64
46 | path: CQ-editor-0.3-dev-x86_64.AppImage
--------------------------------------------------------------------------------
/.github/workflows/pyinstaller-build-pip.yml:
--------------------------------------------------------------------------------
1 | name: build-pip
2 | on:
3 | schedule:
4 | - cron: '0 0 * * 1'
5 | workflow_dispatch:
6 | inputs:
7 | type:
8 | description: 'Whether to build a single file (onefile) or directory (dir) dist'
9 | required: true
10 | default: 'dir'
11 | jobs:
12 | build-linux:
13 | runs-on: ubuntu-18.04
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: mamba-org/provision-with-micromamba@main
17 | with:
18 | #miniconda-version: "latest"
19 | #auto-update-conda: true
20 | environment-name: test
21 | environment-file: environment.yml
22 | extra-specs:
23 | python=3.9
24 | - name: Mamba install CadQuery and pyinstaller
25 | shell: bash --login {0}
26 | run: |
27 | sudo apt install -y libblas3 libblas-dev
28 | sudo apt install -y libxkbcommon0
29 | sudo apt install -y libxkbcommon-x11-0
30 | sudo apt install -y libxcb-xinerama0
31 | micromamba info
32 | python -m pip install --upgrade pip
33 | pip install git+https://github.com/jmwright/CQ-editor
34 | pip install --pre git+https://github.com/CadQuery/cadquery casadi==3.5.5
35 | pip install pyinstaller==4.10
36 | pip install path
37 | pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
38 | pip install git+https://github.com/meadiode/cq_gears.git@main
39 | pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache"
40 | pip install git+https://github.com/gumyr/build123d.git#egg=build123d
41 | pip install git+https://github.com/JustinSDK/cqMore
42 | pip list
43 | - name: Run build
44 | shell: bash --login {0}
45 | run: |
46 | micromamba info
47 | pyinstaller pyinstaller_pip.spec ${{ github.event.inputs.type }}
48 | cp /home/runner/work/CQ-editor/CQ-editor/pyinstaller/CQ-editor.sh /home/runner/work/CQ-editor/CQ-editor/dist/
49 | - uses: actions/upload-artifact@v2
50 | with:
51 | name: CQ-editor-Linux-x86_64
52 | path: dist
53 | build-macos:
54 | runs-on: macos-latest
55 | steps:
56 | - uses: actions/checkout@v2
57 | - uses: mamba-org/provision-with-micromamba@main
58 | with:
59 | #miniconda-version: "latest"
60 | #auto-update-conda: true
61 | environment-name: test
62 | environment-file: environment.yml
63 | extra-specs:
64 | python=3.9
65 | - name: Mamba install CadQuery and pyinstaller
66 | shell: bash --login {0}
67 | run: |
68 | micromamba info
69 | pip install git+https://github.com/jmwright/CQ-editor
70 | pip install --pre git+https://github.com/CadQuery/cadquery casadi==3.5.5
71 | pip install pyinstaller==4.10
72 | pip install path
73 | pip uninstall -y PyQt5
74 | pip install PyQt5==5.15.7
75 | pip install PyQtWebEngine==5.15.6
76 | pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
77 | pip install git+https://github.com/meadiode/cq_gears.git@main
78 | pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache"
79 | pip install git+https://github.com/gumyr/build123d.git#egg=build123d
80 | pip install git+https://github.com/JustinSDK/cqMore
81 | pip list
82 | - name: Run build
83 | shell: bash --login {0}
84 | run: |
85 | micromamba info
86 | pyinstaller pyinstaller_pip.spec ${{ github.event.inputs.type }}
87 | cp /Users/runner/work/CQ-editor/CQ-editor/pyinstaller/CQ-editor.sh /Users/runner/work/CQ-editor/CQ-editor/dist/
88 | - uses: actions/upload-artifact@v2
89 | with:
90 | name: CQ-editor-MacOS
91 | path: dist
92 | build-windows:
93 | runs-on: windows-latest
94 | steps:
95 | - uses: actions/checkout@v2
96 | - uses: mamba-org/provision-with-micromamba@main
97 | with:
98 | #miniconda-version: "latest"
99 | #auto-update-conda: true
100 | environment-name: test
101 | environment-file: environment.yml
102 | extra-specs:
103 | python=3.9
104 | - name: pip install cadquery CQ-editor ... etc
105 | shell: powershell
106 | run: |
107 | micromamba info
108 | pip install git+https://github.com/jmwright/CQ-editor
109 | pip install --pre git+https://github.com/CadQuery/cadquery casadi==3.5.5
110 | pip install pyinstaller==4.10
111 | pip install path
112 | pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
113 | pip install git+https://github.com/meadiode/cq_gears.git@main
114 | pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache"
115 | pip install git+https://github.com/gumyr/build123d.git#egg=build123d
116 | pip install git+https://github.com/JustinSDK/cqMore
117 | pip list
118 | - name: Run build
119 | shell: powershell
120 | run: |
121 | micromamba info
122 | pyinstaller --debug all pyinstaller_pip.spec ${{ github.event.inputs.type }}
123 | Copy-Item D:\a\CQ-editor\CQ-editor\pyinstaller\CQ-editor.cmd D:\a\CQ-editor\CQ-editor\dist\
124 | - uses: actions/upload-artifact@v2
125 | with:
126 | name: CQ-editor-Windows
127 | path: dist
128 |
--------------------------------------------------------------------------------
/.github/workflows/pyinstaller-builds-actions.yml:
--------------------------------------------------------------------------------
1 | name: build
2 | on:
3 | schedule:
4 | - cron: '0 0 * * 1'
5 | workflow_dispatch:
6 | inputs:
7 | type:
8 | description: 'Whether to build a single file (onefile) or directory (dir) dist'
9 | required: true
10 | default: 'dir'
11 | jobs:
12 | build-linux:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: conda-incubator/setup-miniconda@v2
17 | with:
18 | # mamba-version: "*"
19 | # miniconda-version: latest
20 | miniforge-version: latest
21 | channels: conda-forge,defaults
22 | channel-priority: true
23 | auto-update-conda: true
24 | python-version: 3.9
25 | activate-environment: test
26 | - name: Install CadQuery, CQ-editor and pyinstaller
27 | shell: bash --login {0}
28 | run: |
29 | sudo apt install -y libblas-dev libblas3 libblas64-3 libblas64-dev
30 | sudo apt install -y libxkbcommon0
31 | sudo apt install -y libxkbcommon-x11-0
32 | sudo apt install -y libxcb-xinerama0
33 | mamba info
34 | mamba install -c cadquery -c conda-forge cq-editor=master cadquery=master ipython=8.4.0 jedi=0.17.2 pyqtgraph=0.12.4 python=3.9
35 | mamba install -c conda-forge pyinstaller=4.10
36 | # mamba uninstall --force -y importlib_resources
37 | pip install path
38 | pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
39 | pip install git+https://github.com/meadiode/cq_gears.git@main
40 | pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache"
41 | - name: Run build
42 | shell: bash --login {0}
43 | run: |
44 | mamba info
45 | pyinstaller pyinstaller.spec ${{ github.event.inputs.type }}
46 | cp /home/runner/work/CQ-editor/CQ-editor/pyinstaller/CQ-editor.sh /home/runner/work/CQ-editor/CQ-editor/dist/
47 | # rm /home/runner/work/CQ-editor/CQ-editor/dist/CQ-editor/libstdc++.so.6
48 | - uses: actions/upload-artifact@v2
49 | with:
50 | name: CQ-editor-Linux-x86_64
51 | path: dist
52 | build-macos:
53 | runs-on: macos-latest
54 | steps:
55 | - uses: actions/checkout@v2
56 | - uses: conda-incubator/setup-miniconda@v2
57 | with:
58 | # mamba-version: "*"
59 | miniconda-version: latest
60 | channels: conda-forge,defaults
61 | auto-update-conda: true
62 | python-version: 3.9
63 | activate-environment: test
64 | - name: Install CadQuery, CQ-editor and pyinstaller=4.10
65 | shell: bash --login {0}
66 | run: |
67 | conda info
68 | conda install -c cadquery -c conda-forge cq-editor=master cadquery=master ipython=8.4.0 jedi=0.17.2 pyqtgraph=0.12.4 python=3.9
69 | conda install -c conda-forge pyinstaller
70 | conda uninstall --force -y importlib_resources
71 | pip install path
72 | pip uninstall -y PyQt5
73 | pip install PyQt5==5.15.7
74 | pip install PyQtWebEngine==5.15.6
75 | pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
76 | pip install git+https://github.com/meadiode/cq_gears.git@main
77 | pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache"
78 | - name: Run build
79 | shell: bash --login {0}
80 | run: |
81 | conda info
82 | pyinstaller pyinstaller.spec ${{ github.event.inputs.type }}
83 | cp /Users/runner/work/CQ-editor/CQ-editor/pyinstaller/CQ-editor.sh /Users/runner/work/CQ-editor/CQ-editor/dist/
84 | - uses: actions/upload-artifact@v2
85 | with:
86 | name: CQ-editor-MacOS
87 | path: dist
88 | build-windows:
89 | runs-on: windows-2019
90 | steps:
91 | - uses: actions/checkout@v2
92 | - uses: conda-incubator/setup-miniconda@v2
93 | with:
94 | miniconda-version: "latest"
95 | channels: conda-forge,defaults
96 | auto-update-conda: true
97 | python-version: 3.9
98 | activate-environment: test
99 | - name: Install CadQuery and pyinstaller
100 | shell: powershell
101 | run: |
102 | conda install -c cadquery -c conda-forge cq-editor=master cadquery=master ipython=8.4.0 jedi=0.17.2 pyqtgraph=0.12.4 python=3.9
103 | conda install -c conda-forge pyinstaller=4.10
104 | pip install pipwin
105 | pipwin install numpy
106 | pip install path
107 | pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
108 | pip install git+https://github.com/meadiode/cq_gears.git@main
109 | pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache"
110 | - name: Run build
111 | shell: powershell
112 | run: |
113 | conda info
114 | pyinstaller --debug all pyinstaller.spec ${{ github.event.inputs.type }}
115 | # Get-ChildItem -Path C:\ -Filter libssl-1_1-x64.dll -Recurse
116 | Copy-Item "C:\Program Files\OpenSSL\bin\libssl-1_1-x64.dll" D:\a\CQ-editor\CQ-editor\dist\CQ-editor\
117 | Copy-Item "C:\Program Files\OpenSSL\bin\libcrypto-1_1-x64.dll" D:\a\CQ-editor\CQ-editor\dist\CQ-editor\
118 | Copy-Item D:\a\CQ-editor\CQ-editor\pyinstaller\CQ-editor.cmd D:\a\CQ-editor\CQ-editor\dist\
119 | - uses: actions/upload-artifact@v2
120 | with:
121 | name: CQ-editor-Windows
122 | path: dist
123 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | # *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CadQuery editor
2 |
3 | [](https://ci.appveyor.com/project/adam-urbanczyk/cq-editor/branch/master)
4 | [](https://codecov.io/gh/CadQuery/CQ-editor)
5 | [](https://dev.azure.com/cadquery/CQ-editor/_build/latest?definitionId=3&branchName=master)
6 | [](https://zenodo.org/badge/latestdoi/136604983)
7 |
8 | CadQuery GUI editor based on PyQT supports Linux, Windows and Mac.
9 |
10 |
11 |
12 |
13 |
14 | ## Notable features
15 |
16 | * Automatic code reloading - you can use your favourite editor
17 | * OCCT based
18 | * Graphical debugger for CadQuery scripts
19 | * Step through script and watch how your model changes
20 | * CadQuery object stack inspector
21 | * Visual inspection of current workplane and selected items
22 | * Insight into evolution of the model
23 | * Export to various formats
24 | * STL
25 | * STEP
26 |
27 | ## Installation - Pre-Built Packages (Recommended)
28 |
29 | ### Release Packages
30 |
31 | 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.
32 |
33 | ### Development Packages
34 |
35 | 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.
36 |
37 | ### MacOS workarounds
38 |
39 | On later MacOS versions you may also need `xattr -r -d com.apple.quarantine path/to/CQ-editor-MacOS`.
40 |
41 | ## Installation (conda/mamba)
42 |
43 | Use conda or mamba to install:
44 | ```
45 | mamba install -c cadquery -c conda-forge cq-editor=master
46 | ```
47 | and then simply type `cq-editor` to run it. This installs the latest version built directly from the HEAD of this repository.
48 |
49 | Alternatively clone this git repository and set up the following conda environment:
50 | ```
51 | mamba env create -f cqgui_env.yml -n cqgui
52 | mamba activate cqgui
53 | python run.py
54 | ```
55 |
56 | 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):
57 | ```
58 | micromamba install -n base -c cadquery cq-editor
59 | micromamba run -n base cq-editor
60 | ```
61 |
62 | On some linux distributions (e.g. `Ubuntu 18.04`) it might be necessary to install additonal packages:
63 | ```
64 | sudo apt install libglu1-mesa libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev
65 | ```
66 | On Fedora 29 the packages can be installed as follows:
67 | ```
68 | dnf install -y mesa-libGLU mesa-libGL mesa-libGLU-devel
69 | ```
70 |
71 | ## Usage
72 |
73 | ### Showing Objects
74 |
75 | 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.
76 |
77 | ```python
78 | show_object(result, name="somename", options={"alpha":0.5, "color": (64, 164, 223)})
79 | ```
80 |
81 | Note that `show_object` works for `Shape` and `TopoDS_Shape` objects too. In order to display objects from the embedded Python console use `show`.
82 |
83 | ### Rotate, Pan and Zoom the 3D View
84 |
85 | 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.
86 |
87 | * _Left Mouse Button_ + _Drag_ = Rotate
88 | * _Middle Mouse Button_ + _Drag_ = Pan
89 | * _Right Mouse Button_ + _Drag_ = Zoom
90 | * _Mouse Wheel_ = Zoom
91 |
92 | ### Debugging Objects
93 |
94 | 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.
95 |
96 | * `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.
97 | * `Step` (Ctrl + F10) - Will move execution of the script to the next non-empty line.
98 | * `Step in` (Ctrl + F11) - Will follow the flow of execution to the inside of a user-created function defined within the script.
99 | * `Continue` (Ctrl + F12) - Completes execution of the script, starting from the current line that is being debugged.
100 |
101 | 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()`.
102 | ```python
103 | import cadquery as cq
104 |
105 | result = cq.Workplane().box(10, 10, 10)
106 |
107 | highlight = result.faces('>Z')
108 |
109 | show_object(result, name='box')
110 | debug(highlight)
111 | ```
112 | 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.
113 |
114 | ### Console Logging
115 |
116 | 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.
117 |
118 | ### Using an External Code Editor
119 |
120 | 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.
121 |
122 | 1. Open the Preferences dialog by clicking `Edit->Preferences`.
123 | 2. Make sure that `Code Editor` is selected in the left pane.
124 | 3. Check `Autoreload` in the right pane.
125 | 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.
126 |
127 | ### Exporting an Object
128 |
129 | Any object can be exported to either STEP or STL format. The steps for doing so are listed below.
130 |
131 | 1. Highlight the object to be exported in the _Objects_ panel.
132 | 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.
133 |
134 | Clicking either _Export_ item will present a file dialog that allows the file name and location of the export file to be set.
135 |
136 | ### Displaying All Wires for Debugging
137 |
138 | **NOTE:** This is intended for debugging purposes, and if not removed, could interfere with the execution of your model in some cases.
139 |
140 | 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.
141 |
142 | ```python
143 | import cadquery as cq
144 | res = cq.Workplane().rect(1,1).rect(3,3).consolidateWires()
145 | show_object(res)
146 | ```
147 |
148 | ### Highlighting a Specific Face
149 |
150 | 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.
151 |
152 | ```python
153 | import cadquery as cq
154 |
155 | result = cq.Workplane().box(10, 10, 10)
156 |
157 | highlight = result.faces('>Z')
158 |
159 | show_object(result)
160 | show_object(highlight,'highlight',options=dict(alpha=0.1,color=(1.,0,0)))
161 | ```
162 |
163 | ### Naming an Object
164 |
165 | 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.
166 |
167 | ```python
168 | import cadquery as cq
169 |
170 | result = cq.Workplane().box(10, 10, 10)
171 |
172 | show_object(result, name='box')
173 | ```
174 |
--------------------------------------------------------------------------------
/appimage/CQ-editor.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Type=Application
3 | Name=CQ-editor
4 | Comment=CadQuery IDE
5 | Categories=Utility;
6 | Exec=CQ-editor
7 | Icon=cq_logo
8 | StartupNotify=false
9 | Terminal=false
10 |
--------------------------------------------------------------------------------
/appimage/appimage-build.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | BUILD_DIR=$(mktemp -d -p "/tmp/" CQeditor-AppImage-build-XXXXXX)
4 |
5 | # Clean up the build directory
6 | cleanup () {
7 | if [ -d "$BUILD_DIR" ]; then
8 | rm -rf "$BUILD_DIR"
9 | fi
10 | }
11 | trap cleanup EXIT
12 |
13 | # Save the repo root directory
14 | REPO_ROOT="$(readlink -f $(dirname $(dirname "$0")))"
15 | OLD_CWD="$(readlink -f .)"
16 |
17 | export UPD_INFO="gh-releases-zsync|jmright|CQ-editor|latest|CQeditor*x86_64.AppImage.zsync"
18 |
19 | echo $REPO_ROOT
20 | echo $OLD_CWD
21 | echo $UPD_INFO
22 |
23 | # Set up LinuxDeploy and its conda plugin
24 | wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
25 | wget https://raw.githubusercontent.com/TheAssassin/linuxdeploy-plugin-conda/e714783a1ca6fffeeb9dd15bbfce83831bb196f8/linuxdeploy-plugin-conda.sh
26 | chmod +x linuxdeploy*.{sh,AppImage}
27 |
28 | # LinuxDeploy plugin config
29 | export CONDA_CHANNELS=conda-forge
30 | export CONDA_PACKAGES=xorg-libxi
31 | export PIP_REQUIREMENTS="."
32 | export PIP_WORKDIR="$REPO_ROOT"
33 | export PIP_VERBOSE=1
34 |
35 | mkdir -p AppDir/usr/share/metainfo/
36 | cp "$REPO_ROOT"/*.appdata.xml AppDir/usr/share/metainfo/
37 | cp "$REPO_ROOT"/appimage/cq_logo.png AppDir/
38 | cp "$REPO_ROOT"/appimage/AppRun.sh AppDir/
39 |
40 | ./linuxdeploy-x86_64.AppImage --appdir AppDir --plugin conda -d "$REPO_ROOT"/appimage/CQ-editor.desktop --custom-apprun AppRun.sh
41 |
42 | export VERSION="0.3-dev"
43 |
44 | chmod +x CQ-editor*.AppImage*
45 |
46 | # Install the AppImage tool
47 | wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
48 | chmod +x appimagetool*.AppImage
49 | ./appimagetool*.AppImage AppDir -u "$UPD_INFO"
50 |
--------------------------------------------------------------------------------
/appimage/cq_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmwright/CQ-editor/2d5b00140258b330f50ac5be829a252ebd51b2af/appimage/cq_logo.png
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | shallow_clone: false
2 |
3 | image:
4 | - Ubuntu2004
5 | - Visual Studio 2015
6 |
7 | environment:
8 | matrix:
9 | - PYTEST_QT_API: pyqt5
10 | CODECOV_TOKEN:
11 | secure: ZggK9wgDeFdTp0pu0MEV+SY4i/i1Ls0xrEC2MxSQOQ0JQV+TkpzJJzI4au7L8TpD
12 | MINICONDA_DIRNAME: C:\FreshMiniconda
13 |
14 | install:
15 | - 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
16 | - 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
17 | - 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
18 | - sh: bash miniconda.sh -b -p $HOME/miniconda
19 | - sh: source $HOME/miniconda/bin/activate
20 | - cmd: curl -fsSL -o miniconda.exe https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Windows-x86_64.exe
21 | - cmd: miniconda.exe /S /InstallationType=JustMe /D=%MINICONDA_DIRNAME%
22 | - cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%"
23 | - cmd: activate
24 | - mamba info
25 | - mamba env create --name cqgui -f cqgui_env.yml
26 | - sh: source activate cqgui
27 | - cmd: activate cqgui
28 | - mamba list
29 | - mamba install -y pytest pluggy pytest-qt
30 | - mamba install -y pytest-mock pytest-cov pytest-repeat codecov pyvirtualdisplay
31 |
32 | build: false
33 |
34 | before_test:
35 | - sh: ulimit -c unlimited -S
36 | - sh: sudo rm -f /cores/core.*
37 |
38 | test_script:
39 | - sh: export PYTHONPATH=$(pwd)
40 | - cmd: set PYTHONPATH=%cd%
41 | - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then xvfb-run -s '-screen 0 1920x1080x24 +iglx' pytest -v --cov=cq_editor; fi
42 | - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then pytest -v --cov=cq_editor; fi
43 | - cmd: pytest -v --cov=cq_editor
44 |
45 | on_success:
46 | - codecov
47 |
48 | #on_failure:
49 | # - qtdiag
50 | # - ls /cores/core.*
51 | # - lldb --core `ls /cores/core.*` --batch --one-line "bt"
52 |
53 | on_finish:
54 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
55 | # - sh: export APPVEYOR_SSH_BLOCK=true
56 | # - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -
57 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | branches:
3 | include:
4 | - master
5 | - refs/tags/*
6 | exclude:
7 | - refs/tags/nightly
8 |
9 | pr:
10 | - master
11 |
12 | resources:
13 | repositories:
14 | - repository: templates
15 | type: github
16 | name: jmwright/conda-packages
17 | endpoint: CadQuery
18 |
19 | parameters:
20 | - name: minor
21 | type: object
22 | default:
23 | - 11
24 |
25 | stages:
26 | - stage: build_conda_package
27 | jobs:
28 | - ${{ each minor in parameters.minor }}:
29 | - template: conda-build.yml@templates
30 | parameters:
31 | name: Linux
32 | vmImage: 'ubuntu-latest'
33 | py_maj: 3
34 | py_min: ${{minor}}
35 | conda_bld: 3.21.6
36 |
37 | - stage: build_installers
38 | jobs:
39 | - template: constructor-build.yml@templates
40 | parameters:
41 | name: linux
42 | vmImage: 'ubuntu-latest'
43 | - template: constructor-build.yml@templates
44 | parameters:
45 | name: win
46 | vmImage: 'windows-latest'
47 | - template: constructor-build.yml@templates
48 | parameters:
49 | name: macos
50 | vmImage: 'macOS-latest'
51 |
52 | - stage: upload_installers
53 | jobs:
54 | - job: upload_to_github
55 | condition: ne(variables['Build.Reason'], 'PullRequest')
56 | pool:
57 | vmImage: ubuntu-latest
58 | steps:
59 | - download: current
60 | artifact: installer_ubuntu-latest
61 | - download: current
62 | artifact: installer_windows-latest
63 | - download: current
64 | artifact: installer_macOS-latest
65 | - bash: cp $(Pipeline.Workspace)/installer*/*.* .
66 | - task: GitHubRelease@1
67 | inputs:
68 | gitHubConnection: github.com_oauth
69 | assets: CQ-editor-*.*
70 | action: edit
71 | tag: nightly
72 | target: d8e247d15001bf785ef7498d922b4b5aa017a9c9
73 | addChangeLog: false
74 | assetUploadMode: replace
75 | isPreRelease: true
76 |
77 | # stage left for debugging, disabled by default
78 | - stage: verify
79 | condition: False
80 | jobs:
81 | - job: verify_linux
82 | pool:
83 | vmImage: ubuntu-latest
84 | steps:
85 | - download: current
86 | artifact: installer_ubuntu-latest
87 | - bash: cp $(Pipeline.Workspace)/installer*/*.* .
88 | - bash: sh ./CQ-editor-master-Linux-x86_64.sh -b -p dummy && cd dummy && ./run.sh
89 |
--------------------------------------------------------------------------------
/bundle.py:
--------------------------------------------------------------------------------
1 | from sys import platform
2 | from path import Path
3 | from os import system
4 | from shutil import make_archive
5 | from cq_editor import __version__ as version
6 |
7 | out_p = Path('dist/CQ-editor')
8 | out_p.rmtree_p()
9 |
10 | build_p = Path('build')
11 | build_p.rmtree_p()
12 |
13 | system("pyinstaller pyinstaller.spec")
14 |
15 | if platform == 'linux':
16 | with out_p:
17 | p = Path('.').glob('libpython*')[0]
18 | p.symlink(p.split(".so")[0]+".so")
19 |
20 | make_archive(f'CQ-editor-{version}-linux64','bztar', out_p / '..', 'CQ-editor')
21 |
22 | elif platform == 'win32':
23 |
24 | make_archive(f'CQ-editor-{version}-win64','zip', out_p / '..', 'CQ-editor')
25 |
--------------------------------------------------------------------------------
/collect_icons.py:
--------------------------------------------------------------------------------
1 | from glob import glob
2 | from subprocess import call
3 | from os import remove
4 |
5 | TEMPLATE = \
6 | '''
7 |
8 | {}
9 |
10 | '''
11 |
12 | ITEM_TEMPLATE = '{}'
13 |
14 | QRC_OUT = 'icons.qrc'
15 | RES_OUT = 'src/icons_res.py'
16 | TOOL = 'pyrcc5'
17 |
18 | items = []
19 |
20 | for i in glob('icons/*.svg'):
21 | items.append(ITEM_TEMPLATE.format(i))
22 |
23 |
24 | qrc_text = TEMPLATE.format('\n'.join(items))
25 |
26 | with open(QRC_OUT,'w') as f:
27 | f.write(qrc_text)
28 |
29 | call([TOOL,QRC_OUT,'-o',RES_OUT])
30 | remove(QRC_OUT)
31 |
--------------------------------------------------------------------------------
/conda/construct.yaml:
--------------------------------------------------------------------------------
1 | name: CQ-editor
2 | company: Cadquery
3 | version: master
4 | icon_image: ../icons/cadquery_logo_dark.ico
5 | license_file: ../LICENSE
6 | register_python: False
7 | initialize_conda: False
8 | keep_pkgs: False
9 |
10 | channels:
11 | - conda-forge
12 | - cadquery
13 |
14 | specs:
15 | - cq-editor=master
16 | - cadquery=master
17 | - nomkl
18 | - mamba
19 |
20 | menu_packages: []
21 |
22 | extra_files:
23 | - run.sh # [unix]
24 | - run.bat # [win]
25 |
--------------------------------------------------------------------------------
/conda/meta.yaml:
--------------------------------------------------------------------------------
1 | package:
2 | name: cq-editor
3 | version: {{ environ.get('PACKAGE_VERSION') }}
4 |
5 | source:
6 | path: ..
7 |
8 | build:
9 | string: {{ GIT_DESCRIBE_TAG }}_{{ GIT_BUILD_STR }}
10 | noarch: python
11 | script: python setup.py install --single-version-externally-managed --record=record.txt
12 | entry_points:
13 | - cq-editor = cq_editor.__main__:main
14 | - CQ-editor = cq_editor.__main__:main
15 | requirements:
16 | build:
17 | - python >=3.8
18 | - setuptools
19 |
20 | run:
21 | - python >=3.8
22 | - cadquery=master
23 | - ocp
24 | - logbook
25 | - pyqt=5.*
26 | - pyqtgraph
27 | - spyder=5.*
28 | - path
29 | - logbook
30 | - requests
31 | - qtconsole=5.4.1
32 | test:
33 | imports:
34 | - cq_editor
35 |
36 | about:
37 | summary: GUI for CadQuery 2
38 |
--------------------------------------------------------------------------------
/conda/run.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | start /B Scripts/CQ-editor.exe
3 |
--------------------------------------------------------------------------------
/conda/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exec bin/cq-editor
3 |
--------------------------------------------------------------------------------
/cq_editor/__init__.py:
--------------------------------------------------------------------------------
1 | from ._version import __version__
2 |
--------------------------------------------------------------------------------
/cq_editor/__main__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import argparse
3 |
4 | from PyQt5.QtWidgets import QApplication
5 |
6 | NAME = 'CQ-editor'
7 |
8 | #need to initialize QApp here, otherewise svg icons do not work on windows
9 | app = QApplication(sys.argv,
10 | applicationName=NAME)
11 |
12 | from .main_window import MainWindow
13 |
14 | def main():
15 |
16 | parser = argparse.ArgumentParser(description=NAME)
17 | parser.add_argument('filename',nargs='?',default=None)
18 |
19 | args = parser.parse_args(app.arguments()[1:])
20 |
21 | win = MainWindow(filename=args.filename if args.filename else None)
22 | win.show()
23 | sys.exit(app.exec_())
24 |
25 |
26 | if __name__ == "__main__":
27 |
28 | main()
29 |
--------------------------------------------------------------------------------
/cq_editor/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.3.0dev"
2 |
--------------------------------------------------------------------------------
/cq_editor/cq_utils.py:
--------------------------------------------------------------------------------
1 | import cadquery as cq
2 | from cadquery.occ_impl.assembly import toCAF
3 |
4 | from typing import List, Union
5 | from imp import reload
6 | from types import SimpleNamespace
7 |
8 | from OCP.XCAFPrs import XCAFPrs_AISObject
9 | from OCP.TopoDS import TopoDS_Shape
10 | from OCP.AIS import AIS_InteractiveObject, AIS_Shape
11 | from OCP.Quantity import \
12 | Quantity_TOC_RGB as TOC_RGB, Quantity_Color, Quantity_NOC_GOLD as GOLD
13 | from OCP.Graphic3d import Graphic3d_NOM_JADE, Graphic3d_MaterialAspect
14 |
15 | from PyQt5.QtGui import QColor
16 |
17 | DEFAULT_FACE_COLOR = Quantity_Color(GOLD)
18 | DEFAULT_MATERIAL = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE)
19 |
20 | def find_cq_objects(results : dict):
21 |
22 | return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if isinstance(v,cq.Workplane)}
23 |
24 | def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch]):
25 |
26 | vals = []
27 |
28 | if isinstance(obj,cq.Workplane):
29 | vals.extend(obj.vals())
30 | elif isinstance(obj,cq.Shape):
31 | vals.append(obj)
32 | elif isinstance(obj,list) and isinstance(obj[0],cq.Workplane):
33 | for o in obj: vals.extend(o.vals())
34 | elif isinstance(obj,list) and isinstance(obj[0],cq.Shape):
35 | vals.extend(obj)
36 | elif isinstance(obj, TopoDS_Shape):
37 | vals.append(cq.Shape.cast(obj))
38 | elif isinstance(obj,list) and isinstance(obj[0],TopoDS_Shape):
39 | vals.extend(cq.Shape.cast(o) for o in obj)
40 | elif isinstance(obj, cq.Sketch):
41 | if obj._faces:
42 | vals.append(obj._faces)
43 | else:
44 | vals.extend(obj._edges)
45 | else:
46 | raise ValueError(f'Invalid type {type(obj)}')
47 |
48 | return cq.Compound.makeCompound(vals)
49 |
50 | def to_workplane(obj : cq.Shape):
51 |
52 | rv = cq.Workplane('XY')
53 | rv.objects = [obj,]
54 |
55 | return rv
56 |
57 | def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly, AIS_InteractiveObject],
58 | options={}):
59 |
60 | shape = None
61 |
62 | if isinstance(obj, cq.Assembly):
63 | label, shape = toCAF(obj)
64 | ais = XCAFPrs_AISObject(label)
65 | elif isinstance(obj, AIS_InteractiveObject):
66 | ais = obj
67 | else:
68 | shape = to_compound(obj)
69 | ais = AIS_Shape(shape.wrapped)
70 |
71 | set_material(ais, DEFAULT_MATERIAL)
72 | set_color(ais, DEFAULT_FACE_COLOR)
73 |
74 | if 'alpha' in options:
75 | set_transparency(ais, options['alpha'])
76 | if 'color' in options:
77 | set_color(ais, to_occ_color(options['color']))
78 | if 'rgba' in options:
79 | r,g,b,a = options['rgba']
80 | set_color(ais, to_occ_color((r,g,b)))
81 | set_transparency(ais, a)
82 |
83 | return ais,shape
84 |
85 | def export(obj : Union[cq.Workplane, List[cq.Workplane]], type : str,
86 | file, precision=1e-1):
87 |
88 | comp = to_compound(obj)
89 |
90 | if type == 'stl':
91 | comp.exportStl(file, tolerance=precision)
92 | elif type == 'step':
93 | comp.exportStep(file)
94 | elif type == 'brep':
95 | comp.exportBrep(file)
96 |
97 | def to_occ_color(color) -> Quantity_Color:
98 |
99 | if not isinstance(color, QColor):
100 | if isinstance(color, tuple):
101 | if isinstance(color[0], int):
102 | color = QColor(*color)
103 | elif isinstance(color[0], float):
104 | color = QColor.fromRgbF(*color)
105 | else:
106 | raise ValueError('Unknown color format')
107 | else:
108 | color = QColor(color)
109 |
110 | return Quantity_Color(color.redF(),
111 | color.greenF(),
112 | color.blueF(),
113 | TOC_RGB)
114 |
115 | def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor:
116 |
117 | if isinstance(obj, AIS_InteractiveObject):
118 | color = Quantity_Color()
119 | obj.Color(color)
120 | else:
121 | color = obj
122 |
123 | return QColor.fromRgbF(color.Red(), color.Green(), color.Blue())
124 |
125 | def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape:
126 |
127 | drawer = ais.Attributes()
128 | drawer.SetupOwnShadingAspect()
129 | drawer.ShadingAspect().SetColor(color)
130 |
131 | return ais
132 |
133 | def set_material(ais : AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Shape:
134 |
135 | drawer = ais.Attributes()
136 | drawer.SetupOwnShadingAspect()
137 | drawer.ShadingAspect().SetMaterial(material)
138 |
139 | return ais
140 |
141 | def set_transparency(ais : AIS_Shape, alpha: float) -> AIS_Shape:
142 |
143 | drawer = ais.Attributes()
144 | drawer.SetupOwnShadingAspect()
145 | drawer.ShadingAspect().SetTransparency(alpha)
146 |
147 | return ais
148 |
149 | def reload_cq():
150 |
151 | # NB: order of reloads is important
152 | reload(cq.types)
153 | reload(cq.occ_impl.geom)
154 | reload(cq.occ_impl.shapes)
155 | reload(cq.occ_impl.shapes)
156 | reload(cq.occ_impl.importers.dxf)
157 | reload(cq.occ_impl.importers)
158 | reload(cq.occ_impl.solver)
159 | reload(cq.occ_impl.assembly)
160 | reload(cq.occ_impl.sketch_solver)
161 | reload(cq.hull)
162 | reload(cq.selectors)
163 | reload(cq.sketch)
164 | reload(cq.occ_impl.exporters.svg)
165 | reload(cq.cq)
166 | reload(cq.occ_impl.exporters.utils)
167 | reload(cq.occ_impl.exporters.dxf)
168 | reload(cq.occ_impl.exporters.amf)
169 | reload(cq.occ_impl.exporters.json)
170 | #reload(cq.occ_impl.exporters.assembly)
171 | reload(cq.occ_impl.exporters)
172 | reload(cq.assembly)
173 | reload(cq)
174 |
175 |
176 | def is_obj_empty(obj : Union[cq.Workplane,cq.Shape]) -> bool:
177 |
178 | rv = False
179 |
180 | if isinstance(obj, cq.Workplane):
181 | rv = True if isinstance(obj.val(), cq.Vector) else False
182 |
183 | return rv
184 |
--------------------------------------------------------------------------------
/cq_editor/icons.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Created on Fri May 25 14:47:10 2018
5 |
6 | @author: adam
7 | """
8 |
9 | from PyQt5.QtGui import QIcon
10 |
11 | from . import icons_res
12 | _icons = {
13 | 'app' : QIcon(":/images/icons/cadquery_logo_dark.svg")
14 | }
15 |
16 | import qtawesome as qta
17 |
18 | _icons_specs = {
19 | 'new' : (('fa.file-o',),{}),
20 | 'open' : (('fa.folder-open-o',),{}),
21 | # borrowed from spider-ide
22 | '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)}]}],
23 | 'save' : (('fa.save',),{}),
24 | 'save_as': (('fa.save','fa.pencil'),
25 | {'options':[{'scale_factor': 1,},
26 | {'scale_factor': 0.8,
27 | 'offset': (0.2, 0.2)}]}),
28 | 'run' : (('fa.play',),{}),
29 | 'delete' : (('fa.trash',),{}),
30 | 'delete-many' : (('fa.trash','fa.trash',),
31 | {'options' : \
32 | [{'scale_factor': 0.8,
33 | 'offset': (0.2, 0.2),
34 | 'color': 'gray'},
35 | {'scale_factor': 0.8}]}),
36 | 'help' : (('fa.life-ring',),{}),
37 | 'about': (('fa.info',),{}),
38 | 'preferences' : (('fa.cogs',),{}),
39 | 'inspect' : (('fa.cubes','fa.search'),
40 | {'options' : \
41 | [{'scale_factor': 0.8,
42 | 'offset': (0,0),
43 | 'color': 'gray'},{}]}),
44 | 'screenshot' : (('fa.camera',),{}),
45 | 'screenshot-save' : (('fa.save','fa.camera'),
46 | {'options' : \
47 | [{'scale_factor': 0.8},
48 | {'scale_factor': 0.8,
49 | 'offset': (.2,.2)}]})
50 | }
51 |
52 | def icon(name):
53 |
54 | if name in _icons:
55 | return _icons[name]
56 |
57 | args,kwargs = _icons_specs[name]
58 |
59 | return qta.icon(*args,**kwargs)
--------------------------------------------------------------------------------
/cq_editor/main_window.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction)
4 | from logbook import Logger
5 | import cadquery as cq
6 |
7 | from .widgets.editor import Editor
8 | from .widgets.viewer import OCCViewer
9 | from .widgets.console import ConsoleWidget
10 | from .widgets.object_tree import ObjectTree
11 | from .widgets.traceback_viewer import TracebackPane
12 | from .widgets.debugger import Debugger, LocalsView
13 | from .widgets.cq_object_inspector import CQObjectInspector
14 | from .widgets.log import LogViewer
15 |
16 | from . import __version__
17 | from .utils import dock, add_actions, open_url, about_dialog, check_gtihub_for_updates, confirm
18 | from .mixins import MainMixin
19 | from .icons import icon
20 | from .preferences import PreferencesWidget
21 |
22 |
23 | class MainWindow(QMainWindow,MainMixin):
24 |
25 | name = 'CQ-Editor'
26 | org = 'CadQuery'
27 |
28 | def __init__(self,parent=None, filename=None):
29 |
30 | super(MainWindow,self).__init__(parent)
31 | MainMixin.__init__(self)
32 |
33 | self.setWindowIcon(icon('app'))
34 |
35 | self.viewer = OCCViewer(self)
36 | self.setCentralWidget(self.viewer.canvas)
37 |
38 | self.prepare_panes()
39 | self.registerComponent('viewer',self.viewer)
40 | self.prepare_toolbar()
41 | self.prepare_menubar()
42 |
43 | self.prepare_statusbar()
44 | self.prepare_actions()
45 |
46 | self.components['object_tree'].addLines()
47 |
48 | self.prepare_console()
49 |
50 | self.fill_dummy()
51 |
52 | self.setup_logging()
53 |
54 | self.restorePreferences()
55 | self.restoreWindow()
56 |
57 | if filename:
58 | self.components['editor'].load_from_file(filename)
59 |
60 | self.restoreComponentState()
61 |
62 | def closeEvent(self,event):
63 |
64 | self.saveWindow()
65 | self.savePreferences()
66 | self.saveComponentState()
67 |
68 | if self.components['editor'].document().isModified():
69 |
70 | rv = confirm(self, 'Confirm close', 'Close without saving?')
71 |
72 | if rv:
73 | event.accept()
74 | super(MainWindow,self).closeEvent(event)
75 | else:
76 | event.ignore()
77 | else:
78 | super(MainWindow,self).closeEvent(event)
79 |
80 | def prepare_panes(self):
81 |
82 | self.registerComponent('editor',
83 | Editor(self),
84 | lambda c : dock(c,
85 | 'Editor',
86 | self,
87 | defaultArea='left'))
88 |
89 | self.registerComponent('object_tree',
90 | ObjectTree(self),
91 | lambda c: dock(c,
92 | 'Objects',
93 | self,
94 | defaultArea='right'))
95 |
96 | self.registerComponent('console',
97 | ConsoleWidget(self),
98 | lambda c: dock(c,
99 | 'Console',
100 | self,
101 | defaultArea='bottom'))
102 |
103 | self.registerComponent('traceback_viewer',
104 | TracebackPane(self),
105 | lambda c: dock(c,
106 | 'Current traceback',
107 | self,
108 | defaultArea='bottom'))
109 |
110 | self.registerComponent('debugger',Debugger(self))
111 |
112 | self.registerComponent('variables_viewer',LocalsView(self),
113 | lambda c: dock(c,
114 | 'Variables',
115 | self,
116 | defaultArea='right'))
117 |
118 | self.registerComponent('cq_object_inspector',
119 | CQObjectInspector(self),
120 | lambda c: dock(c,
121 | 'CQ object inspector',
122 | self,
123 | defaultArea='right'))
124 | self.registerComponent('log',
125 | LogViewer(self),
126 | lambda c: dock(c,
127 | 'Log viewer',
128 | self,
129 | defaultArea='bottom'))
130 |
131 | for d in self.docks.values():
132 | d.show()
133 |
134 | def prepare_menubar(self):
135 |
136 | menu = self.menuBar()
137 |
138 | menu_file = menu.addMenu('&File')
139 | menu_edit = menu.addMenu('&Edit')
140 | menu_tools = menu.addMenu('&Tools')
141 | menu_run = menu.addMenu('&Run')
142 | menu_view = menu.addMenu('&View')
143 | menu_help = menu.addMenu('&Help')
144 |
145 | #per component menu elements
146 | menus = {'File' : menu_file,
147 | 'Edit' : menu_edit,
148 | 'Run' : menu_run,
149 | 'Tools': menu_tools,
150 | 'View' : menu_view,
151 | 'Help' : menu_help}
152 |
153 | for comp in self.components.values():
154 | self.prepare_menubar_component(menus,
155 | comp.menuActions())
156 |
157 | #global menu elements
158 | menu_view.addSeparator()
159 | for d in self.findChildren(QDockWidget):
160 | menu_view.addAction(d.toggleViewAction())
161 |
162 | menu_view.addSeparator()
163 | for t in self.findChildren(QToolBar):
164 | menu_view.addAction(t.toggleViewAction())
165 |
166 | menu_edit.addAction( \
167 | QAction(icon('preferences'),
168 | 'Preferences',
169 | self,triggered=self.edit_preferences))
170 |
171 | menu_help.addAction( \
172 | QAction(icon('help'),
173 | 'Documentation',
174 | self,triggered=self.documentation))
175 |
176 | menu_help.addAction( \
177 | QAction('CQ documentation',
178 | self,triggered=self.cq_documentation))
179 |
180 | menu_help.addAction( \
181 | QAction(icon('about'),
182 | 'About',
183 | self,triggered=self.about))
184 |
185 | menu_help.addAction( \
186 | QAction('Check for CadQuery updates',
187 | self,triggered=self.check_for_cq_updates))
188 |
189 | def prepare_menubar_component(self,menus,comp_menu_dict):
190 |
191 | for name,action in comp_menu_dict.items():
192 | menus[name].addActions(action)
193 |
194 | def prepare_toolbar(self):
195 |
196 | self.toolbar = QToolBar('Main toolbar',self,objectName='Main toolbar')
197 |
198 | for c in self.components.values():
199 | add_actions(self.toolbar,c.toolbarActions())
200 |
201 | self.addToolBar(self.toolbar)
202 |
203 | def prepare_statusbar(self):
204 |
205 | self.status_label = QLabel('',parent=self)
206 | self.statusBar().insertPermanentWidget(0, self.status_label)
207 |
208 | def prepare_actions(self):
209 |
210 | self.components['debugger'].sigRendered\
211 | .connect(self.components['object_tree'].addObjects)
212 | self.components['debugger'].sigTraceback\
213 | .connect(self.components['traceback_viewer'].addTraceback)
214 | self.components['debugger'].sigLocals\
215 | .connect(self.components['variables_viewer'].update_frame)
216 | self.components['debugger'].sigLocals\
217 | .connect(self.components['console'].push_vars)
218 |
219 | self.components['object_tree'].sigObjectsAdded[list]\
220 | .connect(self.components['viewer'].display_many)
221 | self.components['object_tree'].sigObjectsAdded[list,bool]\
222 | .connect(self.components['viewer'].display_many)
223 | self.components['object_tree'].sigItemChanged.\
224 | connect(self.components['viewer'].update_item)
225 | self.components['object_tree'].sigObjectsRemoved\
226 | .connect(self.components['viewer'].remove_items)
227 | self.components['object_tree'].sigCQObjectSelected\
228 | .connect(self.components['cq_object_inspector'].setObject)
229 | self.components['object_tree'].sigObjectPropertiesChanged\
230 | .connect(self.components['viewer'].redraw)
231 | self.components['object_tree'].sigAISObjectsSelected\
232 | .connect(self.components['viewer'].set_selected)
233 |
234 | self.components['viewer'].sigObjectSelected\
235 | .connect(self.components['object_tree'].handleGraphicalSelection)
236 |
237 | self.components['traceback_viewer'].sigHighlightLine\
238 | .connect(self.components['editor'].go_to_line)
239 |
240 | self.components['cq_object_inspector'].sigDisplayObjects\
241 | .connect(self.components['viewer'].display_many)
242 | self.components['cq_object_inspector'].sigRemoveObjects\
243 | .connect(self.components['viewer'].remove_items)
244 | self.components['cq_object_inspector'].sigShowPlane\
245 | .connect(self.components['viewer'].toggle_grid)
246 | self.components['cq_object_inspector'].sigShowPlane[bool,float]\
247 | .connect(self.components['viewer'].toggle_grid)
248 | self.components['cq_object_inspector'].sigChangePlane\
249 | .connect(self.components['viewer'].set_grid_orientation)
250 |
251 | self.components['debugger'].sigLocalsChanged\
252 | .connect(self.components['variables_viewer'].update_frame)
253 | self.components['debugger'].sigLineChanged\
254 | .connect(self.components['editor'].go_to_line)
255 | self.components['debugger'].sigDebugging\
256 | .connect(self.components['object_tree'].stashObjects)
257 | self.components['debugger'].sigCQChanged\
258 | .connect(self.components['object_tree'].addObjects)
259 | self.components['debugger'].sigTraceback\
260 | .connect(self.components['traceback_viewer'].addTraceback)
261 |
262 | # trigger re-render when file is modified externally or saved
263 | self.components['editor'].triggerRerender \
264 | .connect(self.components['debugger'].render)
265 | self.components['editor'].sigFilenameChanged\
266 | .connect(self.handle_filename_change)
267 |
268 | def prepare_console(self):
269 |
270 | console = self.components['console']
271 | obj_tree = self.components['object_tree']
272 |
273 | #application related items
274 | console.push_vars({'self' : self})
275 |
276 | #CQ related items
277 | console.push_vars({'show' : obj_tree.addObject,
278 | 'show_object' : obj_tree.addObject,
279 | 'cq' : cq,
280 | 'log' : Logger(self.name).info})
281 |
282 | def fill_dummy(self):
283 |
284 | self.components['editor']\
285 | .set_text('import cadquery as cq\nresult = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125)')
286 |
287 | def setup_logging(self):
288 |
289 | from logbook.compat import redirect_logging
290 | from logbook import INFO, Logger
291 |
292 | redirect_logging()
293 | self.components['log'].handler.level = INFO
294 | self.components['log'].handler.push_application()
295 |
296 | self._logger = Logger(self.name)
297 |
298 | def handle_exception(exc_type, exc_value, exc_traceback):
299 |
300 | if issubclass(exc_type, KeyboardInterrupt):
301 | sys.__excepthook__(exc_type, exc_value, exc_traceback)
302 | return
303 |
304 | self._logger.error("Uncaught exception occurred",
305 | exc_info=(exc_type, exc_value, exc_traceback))
306 |
307 | sys.excepthook = handle_exception
308 |
309 |
310 | def edit_preferences(self):
311 |
312 | prefs = PreferencesWidget(self,self.components)
313 | prefs.exec_()
314 |
315 | def about(self):
316 |
317 | about_dialog(
318 | self,
319 | f'About CQ-editor',
320 | f'PyQt GUI for CadQuery.\nVersion: {__version__}.\nSource Code: https://github.com/CadQuery/CQ-editor',
321 | )
322 |
323 | def check_for_cq_updates(self):
324 |
325 | check_gtihub_for_updates(self,cq)
326 |
327 | def documentation(self):
328 |
329 | open_url('https://github.com/CadQuery')
330 |
331 | def cq_documentation(self):
332 |
333 | open_url('https://cadquery.readthedocs.io/en/latest/')
334 |
335 | def handle_filename_change(self, fname):
336 |
337 | new_title = fname if fname else "*"
338 | self.setWindowTitle(f"{self.name}: {new_title}")
339 |
340 | if __name__ == "__main__":
341 |
342 | pass
343 |
--------------------------------------------------------------------------------
/cq_editor/mixins.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Created on Wed May 23 22:02:30 2018
5 |
6 | @author: adam
7 | """
8 |
9 | from functools import reduce
10 | from operator import add
11 | from logbook import Logger
12 |
13 | from PyQt5.QtCore import pyqtSlot, QSettings
14 |
15 | class MainMixin(object):
16 |
17 | name = 'Main'
18 | org = 'Unknown'
19 |
20 | components = {}
21 | docks = {}
22 | preferences = None
23 |
24 | def __init__(self):
25 |
26 | self.settings = QSettings(self.org,self.name)
27 |
28 | def registerComponent(self,name,component,dock=None):
29 |
30 | self.components[name] = component
31 |
32 | if dock:
33 | self.docks[name] = dock(component)
34 |
35 | def saveWindow(self):
36 |
37 | self.settings.setValue('geometry',self.saveGeometry())
38 | self.settings.setValue('windowState',self.saveState())
39 |
40 | def restoreWindow(self):
41 |
42 | if self.settings.value('geometry'):
43 | self.restoreGeometry(self.settings.value('geometry'))
44 | if self.settings.value('windowState'):
45 | self.restoreState(self.settings.value('windowState'))
46 |
47 | def savePreferences(self):
48 |
49 | settings = self.settings
50 |
51 | if self.preferences:
52 | settings.setValue('General',self.preferences.saveState())
53 |
54 | for comp in (c for c in self.components.values() if c.preferences):
55 | settings.setValue(comp.name,comp.preferences.saveState())
56 |
57 | def restorePreferences(self):
58 |
59 | settings = self.settings
60 |
61 | if self.preferences and settings.value('General'):
62 | self.preferences.restoreState(settings.value('General'),
63 | removeChildren=False)
64 |
65 | for comp in (c for c in self.components.values() if c.preferences):
66 | if settings.value(comp.name):
67 | comp.preferences.restoreState(settings.value(comp.name),
68 | removeChildren=False)
69 |
70 | def saveComponentState(self):
71 |
72 | settings = self.settings
73 |
74 | for comp in self.components.values():
75 | comp.saveComponentState(settings)
76 |
77 | def restoreComponentState(self):
78 |
79 | settings = self.settings
80 |
81 | for comp in self.components.values():
82 | comp.restoreComponentState(settings)
83 |
84 |
85 | class ComponentMixin(object):
86 |
87 |
88 | name = 'Component'
89 | preferences = None
90 |
91 | _actions = {}
92 |
93 |
94 | def __init__(self):
95 |
96 | if self.preferences:
97 | self.preferences.sigTreeStateChanged.\
98 | connect(self.updatePreferences)
99 |
100 | self._logger = Logger(self.name)
101 |
102 | def menuActions(self):
103 |
104 | return self._actions
105 |
106 | def toolbarActions(self):
107 |
108 | if len(self._actions) > 0:
109 | return reduce(add,[a for a in self._actions.values()])
110 | else:
111 | return []
112 |
113 | @pyqtSlot(object,object)
114 | def updatePreferences(self,*args):
115 |
116 | pass
117 |
118 | def saveComponentState(self,store):
119 |
120 | pass
121 |
122 | def restoreComponentState(self,store):
123 |
124 | pass
--------------------------------------------------------------------------------
/cq_editor/preferences.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem,
2 | QStackedWidget, QDialog)
3 | from PyQt5.QtCore import pyqtSlot, Qt
4 |
5 | from pyqtgraph.parametertree import ParameterTree
6 |
7 | from .utils import splitter, layout
8 |
9 |
10 | class PreferencesTreeItem(QTreeWidgetItem):
11 |
12 | def __init__(self,name,widget,):
13 |
14 | super(PreferencesTreeItem,self).__init__(name)
15 | self.widget = widget
16 |
17 | class PreferencesWidget(QDialog):
18 |
19 | def __init__(self,parent,components):
20 |
21 | super(PreferencesWidget,self).__init__(
22 | parent,
23 | Qt.Window | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint,
24 | windowTitle='Preferences')
25 |
26 | self.stacked = QStackedWidget(self)
27 | self.preferences_tree = QTreeWidget(self,
28 | headerHidden=True,
29 | itemsExpandable=False,
30 | rootIsDecorated=False,
31 | columnCount=1)
32 |
33 | self.root = self.preferences_tree.invisibleRootItem()
34 |
35 | self.add('General',
36 | parent)
37 |
38 | for v in parent.components.values():
39 | self.add(v.name,v)
40 |
41 | self.splitter = splitter((self.preferences_tree,self.stacked),(2,5))
42 | layout(self,(self.splitter,),self)
43 |
44 | self.preferences_tree.currentItemChanged.connect(self.handleSelection)
45 |
46 | def add(self,name,component):
47 |
48 | if component.preferences:
49 | widget = ParameterTree()
50 | widget.setHeaderHidden(True)
51 | widget.setParameters(component.preferences,showTop=False)
52 | self.root.addChild(PreferencesTreeItem((name,),
53 | widget))
54 |
55 | self.stacked.addWidget(widget)
56 |
57 | @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem)
58 | def handleSelection(self,item,*args):
59 |
60 | if item:
61 | self.stacked.setCurrentWidget(item.widget)
62 |
63 |
--------------------------------------------------------------------------------
/cq_editor/utils.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from pkg_resources import parse_version
4 |
5 | from PyQt5 import QtCore, QtWidgets
6 | from PyQt5.QtGui import QDesktopServices
7 | from PyQt5.QtCore import QUrl
8 | from PyQt5.QtWidgets import QFileDialog, QMessageBox
9 |
10 | DOCK_POSITIONS = {'right' : QtCore.Qt.RightDockWidgetArea,
11 | 'left' : QtCore.Qt.LeftDockWidgetArea,
12 | 'top' : QtCore.Qt.TopDockWidgetArea,
13 | 'bottom' : QtCore.Qt.BottomDockWidgetArea}
14 |
15 | def layout(parent,items,
16 | top_widget = None,
17 | layout_type = QtWidgets.QVBoxLayout,
18 | margin = 2,
19 | spacing = 0):
20 |
21 | if not top_widget:
22 | top_widget = QtWidgets.QWidget(parent)
23 | top_widget_was_none = True
24 | else:
25 | top_widget_was_none = False
26 | layout = layout_type(top_widget)
27 | top_widget.setLayout(layout)
28 |
29 | for item in items: layout.addWidget(item)
30 |
31 | layout.setSpacing(spacing)
32 | layout.setContentsMargins(margin,margin,margin,margin)
33 |
34 | if top_widget_was_none:
35 | return top_widget
36 | else:
37 | return layout
38 |
39 | def splitter(items,
40 | stretch_factors = None,
41 | orientation=QtCore.Qt.Horizontal):
42 |
43 | sp = QtWidgets.QSplitter(orientation)
44 |
45 | for item in items: sp.addWidget(item)
46 |
47 | if stretch_factors:
48 | for i,s in enumerate(stretch_factors):
49 | sp.setStretchFactor(i,s)
50 |
51 |
52 | return sp
53 |
54 | def dock(widget,
55 | title,
56 | parent,
57 | allowedAreas = QtCore.Qt.AllDockWidgetAreas,
58 | defaultArea = 'right',
59 | name=None,
60 | icon = None):
61 |
62 | dock = QtWidgets.QDockWidget(title,parent,objectName=title)
63 |
64 | if name: dock.setObjectName(name)
65 | if icon: dock.toggleViewAction().setIcon(icon)
66 |
67 | dock.setAllowedAreas(allowedAreas)
68 | dock.setWidget(widget)
69 | action = dock.toggleViewAction()
70 | action.setText(title)
71 |
72 | dock.setFeatures(QtWidgets.QDockWidget.DockWidgetFeatures(\
73 | QtWidgets.QDockWidget.AllDockWidgetFeatures))
74 |
75 | parent.addDockWidget(DOCK_POSITIONS[defaultArea],
76 | dock)
77 |
78 | return dock
79 |
80 | def add_actions(menu,actions):
81 |
82 | if len(actions) > 0:
83 | menu.addActions(actions)
84 | menu.addSeparator()
85 |
86 | def open_url(url):
87 |
88 | QDesktopServices.openUrl(QUrl(url))
89 |
90 | def about_dialog(parent,title,text):
91 |
92 | QtWidgets.QMessageBox.about(parent,title,text)
93 |
94 | def get_save_filename(suffix):
95 |
96 | rv,_ = QFileDialog.getSaveFileName(filter='*.{}'.format(suffix))
97 | if rv != '' and not rv.endswith(suffix): rv += '.'+suffix
98 |
99 | return rv
100 |
101 | def get_open_filename(suffix, curr_dir):
102 |
103 | rv,_ = QFileDialog.getOpenFileName(directory=curr_dir, filter='*.{}'.format(suffix))
104 | if rv != '' and not rv.endswith(suffix): rv += '.'+suffix
105 |
106 | return rv
107 |
108 | def check_gtihub_for_updates(parent,
109 | mod,
110 | github_org='cadquery',
111 | github_proj='cadquery'):
112 |
113 | url = f'https://api.github.com/repos/{github_org}/{github_proj}/releases'
114 | resp = requests.get(url).json()
115 |
116 | newer = [el['tag_name'] for el in resp if not el['draft'] and \
117 | parse_version(el['tag_name']) > parse_version(mod.__version__)]
118 |
119 | if newer:
120 | title='Updates available'
121 | text=f'There are newer versions of {github_proj} ' \
122 | f'available on github:\n' + '\n'.join(newer)
123 |
124 | else:
125 | title='No updates available'
126 | text=f'You are already using the latest version of {github_proj}'
127 |
128 | QtWidgets.QMessageBox.about(parent,title,text)
129 |
130 | def confirm(parent,title,msg):
131 |
132 | rv = QMessageBox.question(parent, title, msg, QMessageBox.Yes, QMessageBox.No)
133 |
134 | return True if rv == QMessageBox.Yes else False
135 |
--------------------------------------------------------------------------------
/cq_editor/widgets/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmwright/CQ-editor/2d5b00140258b330f50ac5be829a252ebd51b2af/cq_editor/widgets/__init__.py
--------------------------------------------------------------------------------
/cq_editor/widgets/console.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import QApplication
2 | from PyQt5.QtCore import pyqtSlot
3 |
4 | from qtconsole.rich_jupyter_widget import RichJupyterWidget
5 | from qtconsole.inprocess import QtInProcessKernelManager
6 |
7 | from ..mixins import ComponentMixin
8 |
9 | class ConsoleWidget(RichJupyterWidget,ComponentMixin):
10 |
11 | name = 'Console'
12 |
13 | def __init__(self, customBanner=None, namespace=dict(), *args, **kwargs):
14 | super(ConsoleWidget, self).__init__(*args, **kwargs)
15 |
16 | # if not customBanner is None:
17 | # self.banner = customBanner
18 |
19 | self.font_size = 6
20 | self.kernel_manager = kernel_manager = QtInProcessKernelManager()
21 | kernel_manager.start_kernel(show_banner=False)
22 | kernel_manager.kernel.gui = 'qt'
23 | kernel_manager.kernel.shell.banner1 = ""
24 |
25 | self.kernel_client = kernel_client = self._kernel_manager.client()
26 | kernel_client.start_channels()
27 |
28 | def stop():
29 | kernel_client.stop_channels()
30 | kernel_manager.shutdown_kernel()
31 | QApplication.instance().exit()
32 |
33 | self.exit_requested.connect(stop)
34 |
35 | self.clear()
36 |
37 | self.push_vars(namespace)
38 |
39 | @pyqtSlot(dict)
40 | def push_vars(self, variableDict):
41 | """
42 | Given a dictionary containing name / value pairs, push those variables
43 | to the Jupyter console widget
44 | """
45 | self.kernel_manager.kernel.shell.push(variableDict)
46 |
47 | def clear(self):
48 | """
49 | Clears the terminal
50 | """
51 | self._control.clear()
52 |
53 |
54 | def print_text(self, text):
55 | """
56 | Prints some plain text to the console
57 | """
58 | self._append_plain_text(text)
59 |
60 | def execute_command(self, command):
61 | """
62 | Execute a command in the frame of the console widget
63 | """
64 | self._execute(command, False)
65 |
66 | def _banner_default(self):
67 |
68 | return ''
69 |
70 |
71 | if __name__ == "__main__":
72 |
73 |
74 | import sys
75 |
76 | app = QApplication(sys.argv)
77 |
78 | console = ConsoleWidget(customBanner='IPython console test')
79 | console.show()
80 |
81 | sys.exit(app.exec_())
82 |
--------------------------------------------------------------------------------
/cq_editor/widgets/cq_object_inspector.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAction
2 | from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal
3 |
4 | from OCP.AIS import AIS_ColoredShape
5 | from OCP.gp import gp_Ax3
6 |
7 | from cadquery import Vector
8 |
9 | from ..mixins import ComponentMixin
10 | from ..icons import icon
11 |
12 |
13 |
14 | class CQChildItem(QTreeWidgetItem):
15 |
16 | def __init__(self,cq_item,**kwargs):
17 |
18 | super(CQChildItem,self).\
19 | __init__([type(cq_item).__name__,str(cq_item)],**kwargs)
20 |
21 | self.cq_item = cq_item
22 |
23 | class CQStackItem(QTreeWidgetItem):
24 |
25 | def __init__(self,name,workplane=None,**kwargs):
26 |
27 | super(CQStackItem,self).__init__([name,''],**kwargs)
28 |
29 | self.workplane = workplane
30 |
31 |
32 | class CQObjectInspector(QTreeWidget,ComponentMixin):
33 |
34 | name = 'CQ Object Inspector'
35 |
36 | sigRemoveObjects = pyqtSignal(list)
37 | sigDisplayObjects = pyqtSignal(list,bool)
38 | sigShowPlane = pyqtSignal([bool],[bool,float])
39 | sigChangePlane = pyqtSignal(gp_Ax3)
40 |
41 | def __init__(self,parent):
42 |
43 | super(CQObjectInspector,self).__init__(parent)
44 | self.setHeaderHidden(False)
45 | self.setRootIsDecorated(True)
46 | self.setContextMenuPolicy(Qt.ActionsContextMenu)
47 | self.setColumnCount(2)
48 | self.setHeaderLabels(['Type','Value'])
49 |
50 | self.root = self.invisibleRootItem()
51 | self.inspected_items = []
52 |
53 | self._toolbar_actions = \
54 | [QAction(icon('inspect'),'Inspect CQ object',self,\
55 | toggled=self.inspect,checkable=True)]
56 |
57 | self.addActions(self._toolbar_actions)
58 |
59 | def menuActions(self):
60 |
61 | return {'Tools' : self._toolbar_actions}
62 |
63 | def toolbarActions(self):
64 |
65 | return self._toolbar_actions
66 |
67 | @pyqtSlot(bool)
68 | def inspect(self,value):
69 |
70 | if value:
71 | self.itemSelectionChanged.connect(self.handleSelection)
72 | self.itemSelectionChanged.emit()
73 | else:
74 | self.itemSelectionChanged.disconnect(self.handleSelection)
75 | self.sigRemoveObjects.emit(self.inspected_items)
76 | self.sigShowPlane.emit(False)
77 |
78 | @pyqtSlot()
79 | def handleSelection(self):
80 |
81 | inspected_items = self.inspected_items
82 | self.sigRemoveObjects.emit(inspected_items)
83 | inspected_items.clear()
84 |
85 | items = self.selectedItems()
86 | if len(items) == 0:
87 | return
88 |
89 | item = items[-1]
90 | if type(item) is CQStackItem:
91 | cq_plane = item.workplane.plane
92 | dim = item.workplane.largestDimension()
93 | plane = gp_Ax3(cq_plane.origin.toPnt(),
94 | cq_plane.zDir.toDir(),
95 | cq_plane.xDir.toDir())
96 | self.sigChangePlane.emit(plane)
97 | self.sigShowPlane[bool,float].emit(True,dim)
98 |
99 | for child in (item.child(i) for i in range(item.childCount())):
100 | obj = child.cq_item
101 | if hasattr(obj,'wrapped') and type(obj) != Vector:
102 | ais = AIS_ColoredShape(obj.wrapped)
103 | inspected_items.append(ais)
104 |
105 | else:
106 | self.sigShowPlane.emit(False)
107 | obj = item.cq_item
108 | if hasattr(obj,'wrapped') and type(obj) != Vector:
109 | ais = AIS_ColoredShape(obj.wrapped)
110 | inspected_items.append(ais)
111 |
112 | self.sigDisplayObjects.emit(inspected_items,False)
113 |
114 | @pyqtSlot(object)
115 | def setObject(self,cq_obj):
116 |
117 | self.root.takeChildren()
118 |
119 | # iterate through parent objects if they exist
120 | while getattr(cq_obj, 'parent', None):
121 | current_frame = CQStackItem(str(cq_obj.plane.origin),workplane=cq_obj)
122 | self.root.addChild(current_frame)
123 |
124 | for obj in cq_obj.objects:
125 | current_frame.addChild(CQChildItem(obj))
126 |
127 | cq_obj = cq_obj.parent
128 |
129 |
--------------------------------------------------------------------------------
/cq_editor/widgets/debugger.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from contextlib import ExitStack, contextmanager
3 | from enum import Enum, auto
4 | from types import SimpleNamespace, FrameType, ModuleType
5 | from typing import List
6 | from bdb import BdbQuit
7 |
8 | import cadquery as cq
9 | from PyQt5 import QtCore
10 | from PyQt5.QtCore import Qt, QObject, pyqtSlot, pyqtSignal, QEventLoop, QAbstractTableModel
11 | from PyQt5.QtWidgets import QAction, QTableView
12 |
13 | from logbook import info
14 | from path import Path
15 | from pyqtgraph.parametertree import Parameter
16 | from spyder.utils.icon_manager import icon
17 |
18 | from ..cq_utils import find_cq_objects, reload_cq
19 | from ..mixins import ComponentMixin
20 |
21 | DUMMY_FILE = ''
22 |
23 |
24 | class DbgState(Enum):
25 |
26 | STEP = auto()
27 | CONT = auto()
28 | STEP_IN = auto()
29 | RETURN = auto()
30 |
31 | class DbgEevent(object):
32 |
33 | LINE = 'line'
34 | CALL = 'call'
35 | RETURN = 'return'
36 |
37 | class LocalsModel(QAbstractTableModel):
38 |
39 | HEADER = ('Name','Type', 'Value')
40 |
41 | def __init__(self,parent):
42 |
43 | super(LocalsModel,self).__init__(parent)
44 | self.frame = None
45 |
46 | def update_frame(self,frame):
47 |
48 | self.frame = \
49 | [(k,type(v).__name__, str(v)) for k,v in frame.items() if not k.startswith('_')]
50 |
51 |
52 | def rowCount(self,parent=QtCore.QModelIndex()):
53 |
54 | if self.frame:
55 | return len(self.frame)
56 | else:
57 | return 0
58 |
59 | def columnCount(self,parent=QtCore.QModelIndex()):
60 |
61 | return 3
62 |
63 | def headerData(self, section, orientation, role=Qt.DisplayRole):
64 | if role == Qt.DisplayRole and orientation == Qt.Horizontal:
65 | return self.HEADER[section]
66 | return QAbstractTableModel.headerData(self, section, orientation, role)
67 |
68 | def data(self, index, role):
69 | if role == QtCore.Qt.DisplayRole:
70 | i = index.row()
71 | j = index.column()
72 | return self.frame[i][j]
73 | else:
74 | return QtCore.QVariant()
75 |
76 |
77 | class LocalsView(QTableView,ComponentMixin):
78 |
79 | name = 'Variables'
80 |
81 | def __init__(self,parent):
82 |
83 | super(LocalsView,self).__init__(parent)
84 | ComponentMixin.__init__(self)
85 |
86 | header = self.horizontalHeader()
87 | header.setStretchLastSection(True)
88 |
89 | vheader = self.verticalHeader()
90 | vheader.setVisible(False)
91 |
92 | @pyqtSlot(dict)
93 | def update_frame(self,frame):
94 |
95 | model = LocalsModel(self)
96 | model.update_frame(frame)
97 |
98 | self.setModel(model)
99 |
100 | class Debugger(QObject,ComponentMixin):
101 |
102 | name = 'Debugger'
103 |
104 | preferences = Parameter.create(name='Preferences',children=[
105 | {'name': 'Reload CQ', 'type': 'bool', 'value': False},
106 | {'name': 'Add script dir to path','type': 'bool', 'value': True},
107 | {'name': 'Change working dir to script dir','type': 'bool', 'value': True},
108 | {'name': 'Reload imported modules', 'type': 'bool', 'value': True},
109 | ])
110 |
111 |
112 | sigRendered = pyqtSignal(dict)
113 | sigLocals = pyqtSignal(dict)
114 | sigTraceback = pyqtSignal(object,str)
115 |
116 | sigFrameChanged = pyqtSignal(object)
117 | sigLineChanged = pyqtSignal(int)
118 | sigLocalsChanged = pyqtSignal(dict)
119 | sigCQChanged = pyqtSignal(dict,bool)
120 | sigDebugging = pyqtSignal(bool)
121 |
122 | _frames : List[FrameType]
123 | _stop_debugging : bool
124 |
125 | def __init__(self,parent):
126 |
127 | super(Debugger,self).__init__(parent)
128 | ComponentMixin.__init__(self)
129 |
130 | self.inner_event_loop = QEventLoop(self)
131 |
132 | self._actions = \
133 | {'Run' : [QAction(icon('run'),
134 | 'Render',
135 | self,
136 | shortcut='F5',
137 | triggered=self.render),
138 | QAction(icon('debug'),
139 | 'Debug',
140 | self,
141 | checkable=True,
142 | shortcut='ctrl+F5',
143 | triggered=self.debug),
144 | QAction(icon('arrow-step-over'),
145 | 'Step',
146 | self,
147 | shortcut='ctrl+F10',
148 | triggered=lambda: self.debug_cmd(DbgState.STEP)),
149 | QAction(icon('arrow-step-in'),
150 | 'Step in',
151 | self,
152 | shortcut='ctrl+F11',
153 | triggered=lambda: self.debug_cmd(DbgState.STEP_IN)),
154 | QAction(icon('arrow-continue'),
155 | 'Continue',
156 | self,
157 | shortcut='ctrl+F12',
158 | triggered=lambda: self.debug_cmd(DbgState.CONT))
159 | ]}
160 |
161 | self._frames = []
162 | self._stop_debugging = False
163 |
164 | def get_current_script(self):
165 |
166 | return self.parent().components['editor'].get_text_with_eol()
167 |
168 | def get_current_script_path(self):
169 |
170 | filename = self.parent().components["editor"].filename
171 | if filename:
172 | return Path(filename).abspath()
173 |
174 | def get_breakpoints(self):
175 |
176 | return self.parent().components['editor'].debugger.get_breakpoints()
177 |
178 | def compile_code(self, cq_script, cq_script_path=None):
179 |
180 | try:
181 | module = ModuleType('__cq_main__')
182 | if cq_script_path:
183 | module.__dict__["__file__"] = cq_script_path
184 | cq_code = compile(cq_script, DUMMY_FILE, 'exec')
185 | return cq_code, module
186 | except Exception:
187 | self.sigTraceback.emit(sys.exc_info(), cq_script)
188 | return None, None
189 |
190 | def _exec(self, code, locals_dict, globals_dict):
191 |
192 | with ExitStack() as stack:
193 | p = (self.get_current_script_path() or Path("")).abspath().dirname()
194 |
195 | if self.preferences['Add script dir to path'] and p.exists():
196 | sys.path.insert(0,p)
197 | stack.callback(sys.path.remove, p)
198 | if self.preferences['Change working dir to script dir'] and p.exists():
199 | stack.enter_context(p)
200 | if self.preferences['Reload imported modules']:
201 | stack.enter_context(module_manager())
202 |
203 | exec(code, locals_dict, globals_dict)
204 |
205 | def _inject_locals(self,module):
206 |
207 | cq_objects = {}
208 |
209 | def _show_object(obj,name=None, options={}):
210 |
211 | if name:
212 | cq_objects.update({name : SimpleNamespace(shape=obj,options=options)})
213 | else:
214 | cq_objects.update({str(id(obj)) : SimpleNamespace(shape=obj,options=options)})
215 |
216 | def _debug(obj,name=None):
217 |
218 | _show_object(obj,name,options=dict(color='red',alpha=0.2))
219 |
220 | module.__dict__['show_object'] = _show_object
221 | module.__dict__['debug'] = _debug
222 | module.__dict__['log'] = lambda x: info(str(x))
223 | module.__dict__['cq'] = cq
224 |
225 | return cq_objects, set(module.__dict__)-{'cq'}
226 |
227 | def _cleanup_locals(self,module,injected_names):
228 |
229 | for name in injected_names: module.__dict__.pop(name)
230 |
231 | @pyqtSlot(bool)
232 | def render(self):
233 |
234 | if self.preferences['Reload CQ']:
235 | reload_cq()
236 |
237 | cq_script = self.get_current_script()
238 | cq_script_path = self.get_current_script_path()
239 | cq_code,module = self.compile_code(cq_script, cq_script_path)
240 |
241 | if cq_code is None: return
242 |
243 | cq_objects,injected_names = self._inject_locals(module)
244 |
245 | try:
246 | self._exec(cq_code, module.__dict__, module.__dict__)
247 |
248 | #remove the special methods
249 | self._cleanup_locals(module,injected_names)
250 |
251 | #collect all CQ objects if no explicit show_object was called
252 | if len(cq_objects) == 0:
253 | cq_objects = find_cq_objects(module.__dict__)
254 | self.sigRendered.emit(cq_objects)
255 | self.sigTraceback.emit(None,
256 | cq_script)
257 | self.sigLocals.emit(module.__dict__)
258 | except Exception:
259 | exc_info = sys.exc_info()
260 | sys.last_traceback = exc_info[-1]
261 | self.sigTraceback.emit(exc_info, cq_script)
262 |
263 | @property
264 | def breakpoints(self):
265 | return [ el[0] for el in self.get_breakpoints()]
266 |
267 | @pyqtSlot(bool)
268 | def debug(self,value):
269 |
270 | # used to stop the debugging session early
271 | self._stop_debugging = False
272 |
273 | if value:
274 | self.previous_trace = previous_trace = sys.gettrace()
275 |
276 | self.sigDebugging.emit(True)
277 | self.state = DbgState.STEP
278 |
279 | self.script = self.get_current_script()
280 | cq_script_path = self.get_current_script_path()
281 | code,module = self.compile_code(self.script, cq_script_path)
282 |
283 | if code is None:
284 | self.sigDebugging.emit(False)
285 | self._actions['Run'][1].setChecked(False)
286 | return
287 |
288 | cq_objects,injected_names = self._inject_locals(module)
289 |
290 | #clear possible traceback
291 | self.sigTraceback.emit(None,
292 | self.script)
293 |
294 | try:
295 | sys.settrace(self.trace_callback)
296 | exec(code,module.__dict__,module.__dict__)
297 | except BdbQuit:
298 | pass
299 | except Exception:
300 | exc_info = sys.exc_info()
301 | sys.last_traceback = exc_info[-1]
302 | self.sigTraceback.emit(exc_info,
303 | self.script)
304 | finally:
305 | sys.settrace(previous_trace)
306 | self.sigDebugging.emit(False)
307 | self._actions['Run'][1].setChecked(False)
308 |
309 | if len(cq_objects) == 0:
310 | cq_objects = find_cq_objects(module.__dict__)
311 | self.sigRendered.emit(cq_objects)
312 |
313 | self._cleanup_locals(module,injected_names)
314 | self.sigLocals.emit(module.__dict__)
315 |
316 | self._frames = []
317 | self.inner_event_loop.exit(0)
318 | else:
319 | self._stop_debugging = True
320 | self.inner_event_loop.exit(0)
321 |
322 | def debug_cmd(self,state=DbgState.STEP):
323 |
324 | self.state = state
325 | self.inner_event_loop.exit(0)
326 |
327 |
328 | def trace_callback(self,frame,event,arg):
329 |
330 | filename = frame.f_code.co_filename
331 |
332 | if filename==DUMMY_FILE:
333 | if not self._frames:
334 | self._frames.append(frame)
335 | self.trace_local(frame,event,arg)
336 | return self.trace_callback
337 |
338 | else:
339 | return None
340 |
341 | def trace_local(self,frame,event,arg):
342 |
343 | lineno = frame.f_lineno
344 |
345 | if event in (DbgEevent.LINE,):
346 | if (self.state in (DbgState.STEP, DbgState.STEP_IN) and frame is self._frames[-1]) \
347 | or (lineno in self.breakpoints):
348 |
349 | if lineno in self.breakpoints:
350 | self._frames.append(frame)
351 |
352 | self.sigLineChanged.emit(lineno)
353 | self.sigFrameChanged.emit(frame)
354 | self.sigLocalsChanged.emit(frame.f_locals)
355 | self.sigCQChanged.emit(find_cq_objects(frame.f_locals),True)
356 |
357 | self.inner_event_loop.exec_()
358 |
359 | elif event in (DbgEevent.RETURN):
360 | self.sigLocalsChanged.emit(frame.f_locals)
361 | self._frames.pop()
362 |
363 | elif event == DbgEevent.CALL:
364 | func_filename = frame.f_code.co_filename
365 | if self.state == DbgState.STEP_IN and func_filename == DUMMY_FILE:
366 | self.sigLineChanged.emit(lineno)
367 | self.sigFrameChanged.emit(frame)
368 | self.state = DbgState.STEP
369 | self._frames.append(frame)
370 |
371 | if self._stop_debugging:
372 | raise BdbQuit #stop debugging if requested
373 |
374 |
375 | @contextmanager
376 | def module_manager():
377 | """ unloads any modules loaded while the context manager is active """
378 | loaded_modules = set(sys.modules.keys())
379 |
380 | try:
381 | yield
382 | finally:
383 | new_modules = set(sys.modules.keys()) - loaded_modules
384 | for module_name in new_modules:
385 | del sys.modules[module_name]
386 |
--------------------------------------------------------------------------------
/cq_editor/widgets/editor.py:
--------------------------------------------------------------------------------
1 | import os
2 | import spyder.utils.encoding
3 | from modulefinder import ModuleFinder
4 |
5 | from spyder.plugins.editor.widgets.codeeditor import CodeEditor
6 | from PyQt5.QtCore import pyqtSignal, QFileSystemWatcher, QTimer
7 | from PyQt5.QtWidgets import QAction, QFileDialog
8 | from PyQt5.QtGui import QFontDatabase
9 | from path import Path
10 |
11 | import sys
12 |
13 | from pyqtgraph.parametertree import Parameter
14 |
15 | from ..mixins import ComponentMixin
16 | from ..utils import get_save_filename, get_open_filename, confirm
17 |
18 | from ..icons import icon
19 |
20 | class Editor(CodeEditor,ComponentMixin):
21 |
22 | name = 'Code Editor'
23 |
24 | # This signal is emitted whenever the currently-open file changes and
25 | # autoreload is enabled.
26 | triggerRerender = pyqtSignal(bool)
27 | sigFilenameChanged = pyqtSignal(str)
28 |
29 | preferences = Parameter.create(name='Preferences',children=[
30 | {'name': 'Font size', 'type': 'int', 'value': 12},
31 | {'name': 'Autoreload', 'type': 'bool', 'value': False},
32 | {'name': 'Autoreload delay', 'type': 'int', 'value': 50},
33 | {'name': 'Autoreload: watch imported modules', 'type': 'bool', 'value': False},
34 | {'name': 'Line wrap', 'type': 'bool', 'value': False},
35 | {'name': 'Color scheme', 'type': 'list',
36 | 'values': ['Spyder','Monokai','Zenburn'], 'value': 'Spyder'}])
37 |
38 | EXTENSIONS = 'py'
39 |
40 | def __init__(self,parent=None):
41 |
42 | self._watched_file = None
43 |
44 | super(Editor,self).__init__(parent)
45 | ComponentMixin.__init__(self)
46 |
47 | self.setup_editor(linenumbers=True,
48 | markers=True,
49 | edge_line=False,
50 | tab_mode=False,
51 | show_blanks=True,
52 | font=QFontDatabase.systemFont(QFontDatabase.FixedFont),
53 | language='Python',
54 | filename='')
55 |
56 | self._actions = \
57 | {'File' : [QAction(icon('new'),
58 | 'New',
59 | self,
60 | shortcut='ctrl+N',
61 | triggered=self.new),
62 | QAction(icon('open'),
63 | 'Open',
64 | self,
65 | shortcut='ctrl+O',
66 | triggered=self.open),
67 | QAction(icon('save'),
68 | 'Save',
69 | self,
70 | shortcut='ctrl+S',
71 | triggered=self.save),
72 | QAction(icon('save_as'),
73 | 'Save as',
74 | self,
75 | shortcut='ctrl+shift+S',
76 | triggered=self.save_as),
77 | QAction(icon('autoreload'),
78 | 'Automatic reload and preview',
79 | self,triggered=self.autoreload,
80 | checkable=True,
81 | checked=False,
82 | objectName='autoreload'),
83 | ]}
84 |
85 | for a in self._actions.values():
86 | self.addActions(a)
87 |
88 |
89 | self._fixContextMenu()
90 |
91 | # autoreload support
92 | self._file_watcher = QFileSystemWatcher(self)
93 | # we wait for 50ms after a file change for the file to be written completely
94 | self._file_watch_timer = QTimer(self)
95 | self._file_watch_timer.setInterval(self.preferences['Autoreload delay'])
96 | self._file_watch_timer.setSingleShot(True)
97 | self._file_watcher.fileChanged.connect(
98 | lambda val: self._file_watch_timer.start())
99 | self._file_watch_timer.timeout.connect(self._file_changed)
100 |
101 | self.updatePreferences()
102 |
103 | def _fixContextMenu(self):
104 |
105 | menu = self.menu
106 |
107 | menu.removeAction(self.run_cell_action)
108 | menu.removeAction(self.run_cell_and_advance_action)
109 | menu.removeAction(self.run_selection_action)
110 | menu.removeAction(self.re_run_last_cell_action)
111 |
112 | def updatePreferences(self,*args):
113 |
114 | self.set_color_scheme(self.preferences['Color scheme'])
115 |
116 | font = self.font()
117 | font.setPointSize(self.preferences['Font size'])
118 | self.set_font(font)
119 |
120 | self.findChild(QAction, 'autoreload') \
121 | .setChecked(self.preferences['Autoreload'])
122 |
123 | self._file_watch_timer.setInterval(self.preferences['Autoreload delay'])
124 |
125 | self.toggle_wrap_mode(self.preferences['Line wrap'])
126 |
127 | self._clear_watched_paths()
128 | self._watch_paths()
129 |
130 | def confirm_discard(self):
131 |
132 | if self.modified:
133 | rv = confirm(self,'Please confirm','Current document is not saved - do you want to continue?')
134 | else:
135 | rv = True
136 |
137 | return rv
138 |
139 | def new(self):
140 |
141 | if not self.confirm_discard(): return
142 |
143 | self.set_text('')
144 | self.filename = ''
145 | self.reset_modified()
146 |
147 | def open(self):
148 |
149 | if not self.confirm_discard(): return
150 |
151 | curr_dir = Path(self.filename).abspath().dirname()
152 | fname = get_open_filename(self.EXTENSIONS, curr_dir)
153 | if fname != '':
154 | self.load_from_file(fname)
155 |
156 | def load_from_file(self,fname):
157 |
158 | self.set_text_from_file(fname)
159 | self.filename = fname
160 | self.reset_modified()
161 |
162 | def determine_encoding(self, fname):
163 | if os.path.exists(fname):
164 | # this function returns the encoding spyder used to read the file
165 | _, encoding = spyder.utils.encoding.read(fname)
166 | # spyder returns a -guessed suffix in some cases
167 | return encoding.replace('-guessed', '')
168 | else:
169 | return 'utf-8'
170 |
171 | def save(self):
172 |
173 | if self._filename != '':
174 |
175 | if self.preferences['Autoreload']:
176 | self._file_watcher.blockSignals(True)
177 | self._file_watch_timer.stop()
178 |
179 | encoding = self.determine_encoding(self._filename)
180 | encoded = self.toPlainText().encode(encoding)
181 | with open(self._filename, 'wb') as f:
182 | f.write(encoded)
183 |
184 | if self.preferences['Autoreload']:
185 | self._file_watcher.blockSignals(False)
186 | self.triggerRerender.emit(True)
187 |
188 | self.reset_modified()
189 |
190 | else:
191 | self.save_as()
192 |
193 | def save_as(self):
194 |
195 | fname = get_save_filename(self.EXTENSIONS)
196 | if fname != '':
197 | encoded = self.toPlainText().encode('utf-8')
198 | with open(fname, 'wb') as f:
199 | f.write(encoded)
200 | self.filename = fname
201 |
202 | self.reset_modified()
203 |
204 | def _update_filewatcher(self):
205 | if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']):
206 | self._clear_watched_paths()
207 | self._watched_file = None
208 | if self.preferences['Autoreload'] and self.filename and self.filename != self._watched_file:
209 | self._watched_file = self._filename
210 | self._watch_paths()
211 |
212 | @property
213 | def filename(self):
214 | return self._filename
215 |
216 | @filename.setter
217 | def filename(self, fname):
218 | self._filename = fname
219 | self._update_filewatcher()
220 | self.sigFilenameChanged.emit(fname)
221 |
222 | def _clear_watched_paths(self):
223 | paths = self._file_watcher.files()
224 | if paths:
225 | self._file_watcher.removePaths(paths)
226 |
227 | def _watch_paths(self):
228 | if Path(self._filename).exists():
229 | self._file_watcher.addPath(self._filename)
230 | if self.preferences['Autoreload: watch imported modules']:
231 | module_paths = self.get_imported_module_paths(self._filename)
232 | if module_paths:
233 | self._file_watcher.addPaths(module_paths)
234 |
235 | # callback triggered by QFileSystemWatcher
236 | def _file_changed(self):
237 | # neovim writes a file by removing it first so must re-add each time
238 | self._watch_paths()
239 | self.set_text_from_file(self._filename)
240 | self.triggerRerender.emit(True)
241 |
242 | # Turn autoreload on/off.
243 | def autoreload(self, enabled):
244 | self.preferences['Autoreload'] = enabled
245 | self._update_filewatcher()
246 |
247 | def reset_modified(self):
248 |
249 | self.document().setModified(False)
250 |
251 | @property
252 | def modified(self):
253 |
254 | return self.document().isModified()
255 |
256 | def saveComponentState(self,store):
257 |
258 | if self.filename != '':
259 | store.setValue(self.name+'/state',self.filename)
260 |
261 | def restoreComponentState(self,store):
262 |
263 | filename = store.value(self.name+'/state')
264 |
265 | if filename and self.filename == '':
266 | try:
267 | self.load_from_file(filename)
268 | except IOError:
269 | self._logger.warning(f'could not open {filename}')
270 |
271 |
272 | def get_imported_module_paths(self, module_path):
273 |
274 | finder = ModuleFinder([os.path.dirname(module_path)])
275 | imported_modules = []
276 |
277 | try:
278 | finder.run_script(module_path)
279 | except SyntaxError as err:
280 | self._logger.warning(f'Syntax error in {module_path}: {err}')
281 | except Exception as err:
282 | self._logger.warning(
283 | f'Cannot determine imported modules in {module_path}: {type(err).__name__} {err}'
284 | )
285 | else:
286 | for module_name, module in finder.modules.items():
287 | if module_name != '__main__':
288 | path = getattr(module, '__file__', None)
289 | if path is not None and os.path.isfile(path):
290 | imported_modules.append(path)
291 |
292 | return imported_modules
293 |
294 |
295 | if __name__ == "__main__":
296 |
297 | from PyQt5.QtWidgets import QApplication
298 |
299 | app = QApplication(sys.argv)
300 | editor = Editor()
301 | editor.show()
302 |
303 | sys.exit(app.exec_())
304 |
--------------------------------------------------------------------------------
/cq_editor/widgets/log.py:
--------------------------------------------------------------------------------
1 | import logbook as logging
2 |
3 | from PyQt5.QtWidgets import QPlainTextEdit
4 | from PyQt5 import QtCore
5 |
6 | from ..mixins import ComponentMixin
7 |
8 | class QtLogHandler(logging.Handler,logging.StringFormatterHandlerMixin):
9 |
10 | def __init__(self, log_widget,*args,**kwargs):
11 |
12 | super(QtLogHandler,self).__init__(*args,**kwargs)
13 | logging.StringFormatterHandlerMixin.__init__(self,None)
14 |
15 | self.log_widget = log_widget
16 |
17 | def emit(self, record):
18 |
19 | msg = self.format(record)
20 | QtCore.QMetaObject\
21 | .invokeMethod(self.log_widget,
22 | 'appendPlainText',
23 | QtCore.Qt.QueuedConnection,
24 | QtCore.Q_ARG(str, msg))
25 |
26 | class LogViewer(QPlainTextEdit, ComponentMixin):
27 |
28 | name = 'Log viewer'
29 |
30 | def __init__(self,*args,**kwargs):
31 |
32 | super(LogViewer,self).__init__(*args,**kwargs)
33 | self._MAX_ROWS = 500
34 |
35 | self.setReadOnly(True)
36 | self.setMaximumBlockCount(self._MAX_ROWS)
37 | self.setLineWrapMode(QPlainTextEdit.NoWrap)
38 |
39 | self.handler = QtLogHandler(self)
40 |
41 | def append(self,msg):
42 |
43 | self.appendPlainText(msg)
--------------------------------------------------------------------------------
/cq_editor/widgets/object_tree.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAction, QMenu, QWidget, QAbstractItemView
2 | from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal
3 |
4 | from pyqtgraph.parametertree import Parameter, ParameterTree
5 |
6 | from OCP.AIS import AIS_Line
7 | from OCP.Geom import Geom_Line
8 | from OCP.gp import gp_Dir, gp_Pnt, gp_Ax1
9 |
10 | from ..mixins import ComponentMixin
11 | from ..icons import icon
12 | from ..cq_utils import make_AIS, export, to_occ_color, is_obj_empty, get_occ_color, set_color
13 | from .viewer import DEFAULT_FACE_COLOR
14 | from ..utils import splitter, layout, get_save_filename
15 |
16 | class TopTreeItem(QTreeWidgetItem):
17 |
18 | def __init__(self,*args,**kwargs):
19 |
20 | super(TopTreeItem,self).__init__(*args,**kwargs)
21 |
22 | class ObjectTreeItem(QTreeWidgetItem):
23 |
24 | props = [{'name': 'Name', 'type': 'str', 'value': ''},
25 | {'name': 'Color', 'type': 'color', 'value': "#f4a824"},
26 | {'name': 'Alpha', 'type': 'float', 'value': 0, 'limits': (0,1), 'step': 1e-1},
27 | {'name': 'Visible', 'type': 'bool','value': True}]
28 |
29 | def __init__(self,
30 | name,
31 | ais=None,
32 | shape=None,
33 | shape_display=None,
34 | sig=None,
35 | alpha=0.,
36 | color='#f4a824',
37 | **kwargs):
38 |
39 | super(ObjectTreeItem,self).__init__([name],**kwargs)
40 | self.setFlags( self.flags() | Qt.ItemIsUserCheckable)
41 | self.setCheckState(0,Qt.Checked)
42 |
43 | self.ais = ais
44 | self.shape = shape
45 | self.shape_display = shape_display
46 | self.sig = sig
47 |
48 | self.properties = Parameter.create(name='Properties',
49 | children=self.props)
50 |
51 | self.properties['Name'] = name
52 | self.properties['Alpha'] = ais.Transparency()
53 | self.properties['Color'] = get_occ_color(ais) if ais and ais.HasColor() else get_occ_color(DEFAULT_FACE_COLOR)
54 | self.properties.sigTreeStateChanged.connect(self.propertiesChanged)
55 |
56 | def propertiesChanged(self, properties, changed):
57 |
58 | changed_prop = changed[0][0]
59 |
60 | self.setData(0,0,self.properties['Name'])
61 | self.ais.SetTransparency(self.properties['Alpha'])
62 |
63 | if changed_prop.name() == 'Color':
64 | set_color(self.ais, to_occ_color(self.properties['Color']))
65 |
66 | self.ais.Redisplay()
67 |
68 | if self.properties['Visible']:
69 | self.setCheckState(0,Qt.Checked)
70 | else:
71 | self.setCheckState(0,Qt.Unchecked)
72 |
73 | if self.sig:
74 | self.sig.emit()
75 |
76 | class CQRootItem(TopTreeItem):
77 |
78 | def __init__(self,*args,**kwargs):
79 |
80 | super(CQRootItem,self).__init__(['CQ models'],*args,**kwargs)
81 |
82 |
83 | class HelpersRootItem(TopTreeItem):
84 |
85 | def __init__(self,*args,**kwargs):
86 |
87 | super(HelpersRootItem,self).__init__(['Helpers'],*args,**kwargs)
88 |
89 |
90 | class ObjectTree(QWidget,ComponentMixin):
91 |
92 | name = 'Object Tree'
93 | _stash = []
94 |
95 | preferences = Parameter.create(name='Preferences',children=[
96 | {'name': 'Preserve properties on reload', 'type': 'bool', 'value': False},
97 | {'name': 'Clear all before each run', 'type': 'bool', 'value': True},
98 | {'name': 'STL precision','type': 'float', 'value': .1}])
99 |
100 | sigObjectsAdded = pyqtSignal([list],[list,bool])
101 | sigObjectsRemoved = pyqtSignal(list)
102 | sigCQObjectSelected = pyqtSignal(object)
103 | sigAISObjectsSelected = pyqtSignal(list)
104 | sigItemChanged = pyqtSignal(QTreeWidgetItem,int)
105 | sigObjectPropertiesChanged = pyqtSignal()
106 |
107 | def __init__(self,parent):
108 |
109 | super(ObjectTree,self).__init__(parent)
110 |
111 | self.tree = tree = QTreeWidget(self,
112 | selectionMode=QAbstractItemView.ExtendedSelection)
113 | self.properties_editor = ParameterTree(self)
114 |
115 | tree.setHeaderHidden(True)
116 | tree.setItemsExpandable(False)
117 | tree.setRootIsDecorated(False)
118 | tree.setContextMenuPolicy(Qt.ActionsContextMenu)
119 |
120 | #forward itemChanged singal
121 | tree.itemChanged.connect(\
122 | lambda item,col: self.sigItemChanged.emit(item,col))
123 | #handle visibility changes form tree
124 | tree.itemChanged.connect(self.handleChecked)
125 |
126 | self.CQ = CQRootItem()
127 | self.Helpers = HelpersRootItem()
128 |
129 | root = tree.invisibleRootItem()
130 | root.addChild(self.CQ)
131 | root.addChild(self.Helpers)
132 |
133 | tree.expandToDepth(1)
134 |
135 | self._export_STL_action = \
136 | QAction('Export as STL',
137 | self,
138 | enabled=False,
139 | triggered=lambda: \
140 | self.export('stl',
141 | self.preferences['STL precision']))
142 |
143 | self._export_STEP_action = \
144 | QAction('Export as STEP',
145 | self,
146 | enabled=False,
147 | triggered=lambda: \
148 | self.export('step'))
149 |
150 | self._clear_current_action = QAction(icon('delete'),
151 | 'Clear current',
152 | self,
153 | enabled=False,
154 | triggered=self.removeSelected)
155 |
156 | self._toolbar_actions = \
157 | [QAction(icon('delete-many'),'Clear all',self,triggered=self.removeObjects),
158 | self._clear_current_action,]
159 |
160 | self.prepareMenu()
161 |
162 | tree.itemSelectionChanged.connect(self.handleSelection)
163 | tree.customContextMenuRequested.connect(self.showMenu)
164 |
165 | self.prepareLayout()
166 |
167 |
168 | def prepareMenu(self):
169 |
170 | self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
171 |
172 | self._context_menu = QMenu(self)
173 | self._context_menu.addActions(self._toolbar_actions)
174 | self._context_menu.addActions((self._export_STL_action,
175 | self._export_STEP_action))
176 |
177 | def prepareLayout(self):
178 |
179 | self._splitter = splitter((self.tree,self.properties_editor),
180 | stretch_factors = (2,1),
181 | orientation=Qt.Vertical)
182 | layout(self,(self._splitter,),top_widget=self)
183 |
184 | self._splitter.show()
185 |
186 | def showMenu(self,position):
187 |
188 | self._context_menu.exec_(self.tree.viewport().mapToGlobal(position))
189 |
190 |
191 | def menuActions(self):
192 |
193 | return {'Tools' : [self._export_STL_action,
194 | self._export_STEP_action]}
195 |
196 | def toolbarActions(self):
197 |
198 | return self._toolbar_actions
199 |
200 | def addLines(self):
201 |
202 | origin = (0,0,0)
203 | ais_list = []
204 |
205 | for name,color,direction in zip(('X','Y','Z'),
206 | ('red','lawngreen','blue'),
207 | ((1,0,0),(0,1,0),(0,0,1))):
208 | line_placement = Geom_Line(gp_Ax1(gp_Pnt(*origin),
209 | gp_Dir(*direction)))
210 | line = AIS_Line(line_placement)
211 | line.SetColor(to_occ_color(color))
212 |
213 | self.Helpers.addChild(ObjectTreeItem(name,
214 | ais=line))
215 |
216 | ais_list.append(line)
217 |
218 | self.sigObjectsAdded.emit(ais_list)
219 |
220 | def _current_properties(self):
221 |
222 | current_params = {}
223 | for i in range(self.CQ.childCount()):
224 | child = self.CQ.child(i)
225 | current_params[child.properties['Name']] = child.properties
226 |
227 | return current_params
228 |
229 | def _restore_properties(self,obj,properties):
230 |
231 | for p in properties[obj.properties['Name']]:
232 | obj.properties[p.name()] = p.value()
233 |
234 | @pyqtSlot(dict,bool)
235 | @pyqtSlot(dict)
236 | def addObjects(self,objects,clean=False,root=None):
237 |
238 | if root is None:
239 | root = self.CQ
240 |
241 | request_fit_view = True if root.childCount() == 0 else False
242 | preserve_props = self.preferences['Preserve properties on reload']
243 |
244 | if preserve_props:
245 | current_props = self._current_properties()
246 |
247 | if clean or self.preferences['Clear all before each run']:
248 | self.removeObjects()
249 |
250 | ais_list = []
251 |
252 | #remove empty objects
253 | objects_f = {k:v for k,v in objects.items() if not is_obj_empty(v.shape)}
254 |
255 | for name,obj in objects_f.items():
256 | ais,shape_display = make_AIS(obj.shape,obj.options)
257 |
258 | child = ObjectTreeItem(name,
259 | shape=obj.shape,
260 | shape_display=shape_display,
261 | ais=ais,
262 | sig=self.sigObjectPropertiesChanged)
263 |
264 | if preserve_props and name in current_props:
265 | self._restore_properties(child,current_props)
266 |
267 | if child.properties['Visible']:
268 | ais_list.append(ais)
269 |
270 | root.addChild(child)
271 |
272 | if request_fit_view:
273 | self.sigObjectsAdded[list,bool].emit(ais_list,True)
274 | else:
275 | self.sigObjectsAdded[list].emit(ais_list)
276 |
277 | @pyqtSlot(object,str,object)
278 | def addObject(self,obj,name='',options={}):
279 |
280 | root = self.CQ
281 |
282 | ais,shape_display = make_AIS(obj, options)
283 |
284 | root.addChild(ObjectTreeItem(name,
285 | shape=obj,
286 | shape_display=shape_display,
287 | ais=ais,
288 | sig=self.sigObjectPropertiesChanged))
289 |
290 | self.sigObjectsAdded.emit([ais])
291 |
292 | @pyqtSlot(list)
293 | @pyqtSlot()
294 | def removeObjects(self,objects=None):
295 |
296 | if objects:
297 | removed_items_ais = [self.CQ.takeChild(i).ais for i in objects]
298 | else:
299 | removed_items_ais = [ch.ais for ch in self.CQ.takeChildren()]
300 |
301 | self.sigObjectsRemoved.emit(removed_items_ais)
302 |
303 | @pyqtSlot(bool)
304 | def stashObjects(self,action : bool):
305 |
306 | if action:
307 | self._stash = self.CQ.takeChildren()
308 | removed_items_ais = [ch.ais for ch in self._stash]
309 | self.sigObjectsRemoved.emit(removed_items_ais)
310 | else:
311 | self.removeObjects()
312 | self.CQ.addChildren(self._stash)
313 | ais_list = [el.ais for el in self._stash]
314 | self.sigObjectsAdded.emit(ais_list)
315 |
316 | @pyqtSlot()
317 | def removeSelected(self):
318 |
319 | ixs = self.tree.selectedIndexes()
320 | rows = [ix.row() for ix in ixs]
321 |
322 | self.removeObjects(rows)
323 |
324 | def export(self,export_type,precision=None):
325 |
326 | items = self.tree.selectedItems()
327 |
328 | # if CQ models is selected get all children
329 | if [item for item in items if item is self.CQ]:
330 | CQ = self.CQ
331 | shapes = [CQ.child(i).shape for i in range(CQ.childCount())]
332 | # otherwise collect all selected children of CQ
333 | else:
334 | shapes = [item.shape for item in items if item.parent() is self.CQ]
335 |
336 | fname = get_save_filename(export_type)
337 | if fname != '':
338 | export(shapes,export_type,fname,precision)
339 |
340 | @pyqtSlot()
341 | def handleSelection(self):
342 |
343 | items =self.tree.selectedItems()
344 | if len(items) == 0:
345 | self._export_STL_action.setEnabled(False)
346 | self._export_STEP_action.setEnabled(False)
347 | return
348 |
349 | # emit list of all selected ais objects (might be empty)
350 | ais_objects = [item.ais for item in items if item.parent() is self.CQ]
351 | self.sigAISObjectsSelected.emit(ais_objects)
352 |
353 | # handle context menu and emit last selected CQ object (if present)
354 | item = items[-1]
355 | if item.parent() is self.CQ:
356 | self._export_STL_action.setEnabled(True)
357 | self._export_STEP_action.setEnabled(True)
358 | self._clear_current_action.setEnabled(True)
359 | self.sigCQObjectSelected.emit(item.shape)
360 | self.properties_editor.setParameters(item.properties,
361 | showTop=False)
362 | self.properties_editor.setEnabled(True)
363 | elif item is self.CQ and item.childCount()>0:
364 | self._export_STL_action.setEnabled(True)
365 | self._export_STEP_action.setEnabled(True)
366 | else:
367 | self._export_STL_action.setEnabled(False)
368 | self._export_STEP_action.setEnabled(False)
369 | self._clear_current_action.setEnabled(False)
370 | self.properties_editor.setEnabled(False)
371 | self.properties_editor.clear()
372 |
373 | @pyqtSlot(list)
374 | def handleGraphicalSelection(self,shapes):
375 |
376 | self.tree.clearSelection()
377 |
378 | CQ = self.CQ
379 | for i in range(CQ.childCount()):
380 | item = CQ.child(i)
381 | for shape in shapes:
382 | if item.ais.Shape().IsEqual(shape):
383 | item.setSelected(True)
384 |
385 | @pyqtSlot(QTreeWidgetItem,int)
386 | def handleChecked(self,item,col):
387 |
388 | if type(item) is ObjectTreeItem:
389 | if item.checkState(0):
390 | item.properties['Visible'] = True
391 | else:
392 | item.properties['Visible'] = False
393 |
394 |
395 |
396 |
--------------------------------------------------------------------------------
/cq_editor/widgets/occt_widget.py:
--------------------------------------------------------------------------------
1 | from sys import platform
2 |
3 |
4 | from PyQt5.QtWidgets import QWidget, QApplication
5 | from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QEvent
6 |
7 | import OCP
8 |
9 | from OCP.Aspect import Aspect_DisplayConnection, Aspect_TypeOfTriedronPosition
10 | from OCP.OpenGl import OpenGl_GraphicDriver
11 | from OCP.V3d import V3d_Viewer
12 | from OCP.AIS import AIS_InteractiveContext, AIS_DisplayMode
13 | from OCP.Quantity import Quantity_Color
14 |
15 |
16 | ZOOM_STEP = 0.9
17 |
18 |
19 | class OCCTWidget(QWidget):
20 |
21 | sigObjectSelected = pyqtSignal(list)
22 |
23 | def __init__(self,parent=None):
24 |
25 | super(OCCTWidget,self).__init__(parent)
26 |
27 | self.setAttribute(Qt.WA_NativeWindow)
28 | self.setAttribute(Qt.WA_PaintOnScreen)
29 | self.setAttribute(Qt.WA_NoSystemBackground)
30 |
31 | self._initialized = False
32 | self._needs_update = False
33 |
34 | #OCCT secific things
35 | self.display_connection = Aspect_DisplayConnection()
36 | self.graphics_driver = OpenGl_GraphicDriver(self.display_connection)
37 |
38 | self.viewer = V3d_Viewer(self.graphics_driver)
39 | self.view = self.viewer.CreateView()
40 | self.context = AIS_InteractiveContext(self.viewer)
41 |
42 | #Trihedorn, lights, etc
43 | self.prepare_display()
44 |
45 | def prepare_display(self):
46 |
47 | view = self.view
48 |
49 | params = view.ChangeRenderingParams()
50 | params.NbMsaaSamples = 8
51 | params.IsAntialiasingEnabled = True
52 |
53 | view.TriedronDisplay(
54 | Aspect_TypeOfTriedronPosition.Aspect_TOTP_RIGHT_LOWER,
55 | Quantity_Color(), 0.1)
56 |
57 | viewer = self.viewer
58 |
59 | viewer.SetDefaultLights()
60 | viewer.SetLightOn()
61 |
62 | ctx = self.context
63 |
64 | ctx.SetDisplayMode(AIS_DisplayMode.AIS_Shaded, True)
65 | ctx.DefaultDrawer().SetFaceBoundaryDraw(True)
66 |
67 | def wheelEvent(self, event):
68 |
69 | delta = event.angleDelta().y()
70 | factor = ZOOM_STEP if delta<0 else 1/ZOOM_STEP
71 |
72 | self.view.SetZoom(factor)
73 |
74 | def mousePressEvent(self,event):
75 |
76 | pos = event.pos()
77 |
78 | if event.button() == Qt.LeftButton:
79 | self.view.StartRotation(pos.x(), pos.y())
80 | elif event.button() == Qt.RightButton:
81 | self.view.StartZoomAtPoint(pos.x(), pos.y())
82 |
83 | self.old_pos = pos
84 |
85 | def mouseMoveEvent(self,event):
86 |
87 | pos = event.pos()
88 | x,y = pos.x(),pos.y()
89 |
90 | if event.buttons() == Qt.LeftButton:
91 | self.view.Rotation(x,y)
92 |
93 | elif event.buttons() == Qt.MiddleButton:
94 | self.view.Pan(x - self.old_pos.x(),
95 | self.old_pos.y() - y, theToStart=True)
96 |
97 | elif event.buttons() == Qt.RightButton:
98 | self.view.ZoomAtPoint(self.old_pos.x(), y,
99 | x, self.old_pos.y())
100 |
101 | self.old_pos = pos
102 |
103 | def mouseReleaseEvent(self,event):
104 |
105 | if event.button() == Qt.LeftButton:
106 | pos = event.pos()
107 | x,y = pos.x(),pos.y()
108 |
109 | self.context.MoveTo(x,y,self.view,True)
110 |
111 | self._handle_selection()
112 |
113 | def _handle_selection(self):
114 |
115 | self.context.Select(True)
116 | self.context.InitSelected()
117 |
118 | selected = []
119 | if self.context.HasSelectedShape():
120 | selected.append(self.context.SelectedShape())
121 |
122 | self.sigObjectSelected.emit(selected)
123 |
124 | def paintEngine(self):
125 |
126 | return None
127 |
128 | def paintEvent(self, event):
129 |
130 | if not self._initialized:
131 | self._initialize()
132 | else:
133 | self.view.Redraw()
134 |
135 | def showEvent(self, event):
136 |
137 | super(OCCTWidget,self).showEvent(event)
138 |
139 | def resizeEvent(self, event):
140 |
141 | super(OCCTWidget,self).resizeEvent(event)
142 |
143 | self.view.MustBeResized()
144 |
145 | def _initialize(self):
146 |
147 | wins = {
148 | 'darwin' : self._get_window_osx,
149 | 'linux' : self._get_window_linux,
150 | 'win32': self._get_window_win
151 | }
152 |
153 | self.view.SetWindow(wins.get(platform,self._get_window_linux)(self.winId()))
154 |
155 | self._initialized = True
156 |
157 | def _get_window_win(self,wid):
158 |
159 | from OCP.WNT import WNT_Window
160 |
161 | return WNT_Window(wid.ascapsule())
162 |
163 | def _get_window_linux(self,wid):
164 |
165 | from OCP.Xw import Xw_Window
166 |
167 | return Xw_Window(self.display_connection,int(wid))
168 |
169 | def _get_window_osx(self,wid):
170 |
171 | from OCP.Cocoa import Cocoa_Window
172 |
173 | return Cocoa_Window(wid.ascapsule())
174 |
--------------------------------------------------------------------------------
/cq_editor/widgets/traceback_viewer.py:
--------------------------------------------------------------------------------
1 | from traceback import extract_tb, format_exception_only
2 |
3 | from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction,
4 | QLabel)
5 | from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal
6 |
7 | from ..mixins import ComponentMixin
8 | from ..utils import layout
9 |
10 | class TracebackTree(QTreeWidget):
11 |
12 | name = 'Traceback Viewer'
13 |
14 | def __init__(self,parent):
15 |
16 | super(TracebackTree,self).__init__(parent)
17 | self.setHeaderHidden(False)
18 | self.setItemsExpandable(False)
19 | self.setRootIsDecorated(False)
20 | self.setContextMenuPolicy(Qt.ActionsContextMenu)
21 |
22 | self.setColumnCount(3)
23 | self.setHeaderLabels(['File','Line','Code'])
24 |
25 |
26 | self.root = self.invisibleRootItem()
27 |
28 | class TracebackPane(QWidget,ComponentMixin):
29 |
30 | sigHighlightLine = pyqtSignal(int)
31 |
32 | def __init__(self,parent):
33 |
34 | super(TracebackPane,self).__init__(parent)
35 |
36 | self.tree = TracebackTree(self)
37 | self.current_exception = QLabel(self)
38 | self.current_exception.setStyleSheet(\
39 | "QLabel {color : red; }");
40 |
41 | layout(self,
42 | (self.current_exception,
43 | self.tree),
44 | self)
45 |
46 | self.tree.currentItemChanged.connect(self.handleSelection)
47 |
48 | @pyqtSlot(object,str)
49 | def addTraceback(self,exc_info,code):
50 |
51 | self.tree.clear()
52 |
53 | if exc_info:
54 | t,exc,tb = exc_info
55 |
56 | root = self.tree.root
57 | code = code.splitlines()
58 | tb = [t for t in extract_tb(tb) if '' in t.filename] #ignore highest frames (debug, exec)
59 |
60 | for el in tb:
61 | #workaround of the traceback module
62 | if el.line == '':
63 | line = code[el.lineno-1].strip()
64 | else:
65 | line = el.line
66 |
67 | root.addChild(QTreeWidgetItem([el.filename,
68 | str(el.lineno),
69 | line]))
70 |
71 | exc_name = t.__name__
72 | exc_msg = str(exc)
73 | exc_msg = exc_msg.replace('<', '<').replace('>', '>') #replace <>
74 |
75 | self.current_exception.\
76 | setText('{}: {}'.format(exc_name,exc_msg))
77 |
78 | # handle the special case of a SyntaxError
79 | if t is SyntaxError:
80 | root.addChild(QTreeWidgetItem(
81 | [exc.filename,
82 | str(exc.lineno),
83 | exc.text.strip() if exc.text else '']
84 | ))
85 | else:
86 | self.current_exception.setText('')
87 |
88 | @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem)
89 | def handleSelection(self,item,*args):
90 |
91 | if item:
92 | f,line = item.data(0,0),int(item.data(1,0))
93 |
94 | if '' in f:
95 | self.sigHighlightLine.emit(line)
96 |
97 |
98 |
--------------------------------------------------------------------------------
/cq_editor/widgets/viewer.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import QWidget, QDialog, QTreeWidgetItem, QApplication, QAction
2 |
3 | from PyQt5.QtCore import pyqtSlot, pyqtSignal
4 | from PyQt5.QtGui import QIcon
5 |
6 | from OCP.Graphic3d import Graphic3d_Camera, Graphic3d_StereoMode, Graphic3d_NOM_JADE,\
7 | Graphic3d_MaterialAspect
8 | from OCP.AIS import AIS_Shaded,AIS_WireFrame, AIS_ColoredShape, AIS_Axis
9 | from OCP.Aspect import Aspect_GDM_Lines, Aspect_GT_Rectangular
10 | from OCP.Quantity import Quantity_NOC_BLACK as BLACK, Quantity_TOC_RGB as TOC_RGB,\
11 | Quantity_Color
12 | from OCP.Geom import Geom_Axis1Placement
13 | from OCP.gp import gp_Ax3, gp_Dir, gp_Pnt, gp_Ax1
14 |
15 | from ..utils import layout, get_save_filename
16 | from ..mixins import ComponentMixin
17 | from ..icons import icon
18 | from ..cq_utils import to_occ_color, make_AIS, DEFAULT_FACE_COLOR
19 |
20 | from .occt_widget import OCCTWidget
21 |
22 | from pyqtgraph.parametertree import Parameter
23 | import qtawesome as qta
24 |
25 |
26 |
27 | DEFAULT_EDGE_COLOR = Quantity_Color(BLACK)
28 | DEFAULT_EDGE_WIDTH = 2
29 |
30 | class OCCViewer(QWidget,ComponentMixin):
31 |
32 | name = '3D Viewer'
33 |
34 | preferences = Parameter.create(name='Pref',children=[
35 | {'name': 'Fit automatically', 'type': 'bool', 'value': True},
36 | {'name': 'Use gradient', 'type': 'bool', 'value': False},
37 | {'name': 'Background color', 'type': 'color', 'value': (95,95,95)},
38 | {'name': 'Background color (aux)', 'type': 'color', 'value': (30,30,30)},
39 | {'name': 'Default object color', 'type': 'color', 'value': "#FF0"},
40 | {'name': 'Deviation', 'type': 'float', 'value': 1e-5, 'dec': True, 'step': 1},
41 | {'name': 'Angular deviation', 'type': 'float', 'value': 0.1, 'dec': True, 'step': 1},
42 | {'name': 'Projection Type', 'type': 'list', 'value': 'Orthographic',
43 | 'values': ['Orthographic', 'Perspective', 'Stereo', 'MonoLeftEye', 'MonoRightEye']},
44 | {'name': 'Stereo Mode', 'type': 'list', 'value': 'QuadBuffer',
45 | 'values': ['QuadBuffer', 'Anaglyph', 'RowInterlaced', 'ColumnInterlaced',
46 | 'ChessBoard', 'SideBySide', 'OverUnder']}])
47 | IMAGE_EXTENSIONS = 'png'
48 |
49 | sigObjectSelected = pyqtSignal(list)
50 |
51 | def __init__(self,parent=None):
52 |
53 | super(OCCViewer,self).__init__(parent)
54 | ComponentMixin.__init__(self)
55 |
56 | self.canvas = OCCTWidget()
57 | self.canvas.sigObjectSelected.connect(self.handle_selection)
58 |
59 | self.create_actions(self)
60 |
61 | self.layout_ = layout(self,
62 | [self.canvas,],
63 | top_widget=self,
64 | margin=0)
65 |
66 | self.setup_default_drawer()
67 | self.updatePreferences()
68 |
69 | def setup_default_drawer(self):
70 |
71 | # set the default color and material
72 | material = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE)
73 |
74 | shading_aspect = self.canvas.context.DefaultDrawer().ShadingAspect()
75 | shading_aspect.SetMaterial(material)
76 | shading_aspect.SetColor(DEFAULT_FACE_COLOR)
77 |
78 | # face edge lw
79 | line_aspect = self.canvas.context.DefaultDrawer().FaceBoundaryAspect()
80 | line_aspect.SetWidth(DEFAULT_EDGE_WIDTH)
81 | line_aspect.SetColor(DEFAULT_EDGE_COLOR)
82 |
83 | def updatePreferences(self,*args):
84 |
85 | color1 = to_occ_color(self.preferences['Background color'])
86 | color2 = to_occ_color(self.preferences['Background color (aux)'])
87 |
88 | if not self.preferences['Use gradient']:
89 | color2 = color1
90 | self.canvas.view.SetBgGradientColors(color1,color2,theToUpdate=True)
91 |
92 | self.canvas.update()
93 |
94 | ctx = self.canvas.context
95 | ctx.SetDeviationCoefficient(self.preferences['Deviation'])
96 | ctx.SetDeviationAngle(self.preferences['Angular deviation'])
97 |
98 | v = self._get_view()
99 | camera = v.Camera()
100 | projection_type = self.preferences['Projection Type']
101 | camera.SetProjectionType(getattr(Graphic3d_Camera, f'Projection_{projection_type}',
102 | Graphic3d_Camera.Projection_Orthographic))
103 |
104 | # onle relevant for stereo projection
105 | stereo_mode = self.preferences['Stereo Mode']
106 | params = v.ChangeRenderingParams()
107 | params.StereoMode = getattr(Graphic3d_StereoMode, f'Graphic3d_StereoMode_{stereo_mode}',
108 | Graphic3d_StereoMode.Graphic3d_StereoMode_QuadBuffer)
109 |
110 | def create_actions(self,parent):
111 |
112 | self._actions = \
113 | {'View' : [QAction(qta.icon('fa.arrows-alt'),
114 | 'Fit (Shift+F1)',
115 | parent,
116 | shortcut='shift+F1',
117 | triggered=self.fit),
118 | QAction(QIcon(':/images/icons/isometric_view.svg'),
119 | 'Iso (Shift+F2)',
120 | parent,
121 | shortcut='shift+F2',
122 | triggered=self.iso_view),
123 | QAction(QIcon(':/images/icons/top_view.svg'),
124 | 'Top (Shift+F3)',
125 | parent,
126 | shortcut='shift+F3',
127 | triggered=self.top_view),
128 | QAction(QIcon(':/images/icons/bottom_view.svg'),
129 | 'Bottom (Shift+F4)',
130 | parent,
131 | shortcut='shift+F4',
132 | triggered=self.bottom_view),
133 | QAction(QIcon(':/images/icons/front_view.svg'),
134 | 'Front (Shift+F5)',
135 | parent,
136 | shortcut='shift+F5',
137 | triggered=self.front_view),
138 | QAction(QIcon(':/images/icons/back_view.svg'),
139 | 'Back (Shift+F6)',
140 | parent,
141 | shortcut='shift+F6',
142 | triggered=self.back_view),
143 | QAction(QIcon(':/images/icons/left_side_view.svg'),
144 | 'Left (Shift+F7)',
145 | parent,
146 | shortcut='shift+F7',
147 | triggered=self.left_view),
148 | QAction(QIcon(':/images/icons/right_side_view.svg'),
149 | 'Right (Shift+F8)',
150 | parent,
151 | shortcut='shift+F8',
152 | triggered=self.right_view),
153 | QAction(qta.icon('fa.square-o'),
154 | 'Wireframe (Shift+F9)',
155 | parent,
156 | shortcut='shift+F9',
157 | triggered=self.wireframe_view),
158 | QAction(qta.icon('fa.square'),
159 | 'Shaded (Shift+F10)',
160 | parent,
161 | shortcut='shift+F10',
162 | triggered=self.shaded_view)],
163 | 'Tools' : [QAction(icon('screenshot'),
164 | 'Screenshot',
165 | parent,
166 | triggered=self.save_screenshot)]}
167 |
168 | def toolbarActions(self):
169 |
170 | return self._actions['View']
171 |
172 |
173 | def clear(self):
174 |
175 | self.displayed_shapes = []
176 | self.displayed_ais = []
177 | self.canvas.context.EraseAll(True)
178 | context = self._get_context()
179 | context.PurgeDisplay()
180 | context.RemoveAll(True)
181 |
182 | def _display(self,shape):
183 |
184 | ais = make_AIS(shape)
185 | self.canvas.context.Display(shape,True)
186 |
187 | self.displayed_shapes.append(shape)
188 | self.displayed_ais.append(ais)
189 |
190 | #self.canvas._display.Repaint()
191 |
192 | @pyqtSlot(object)
193 | def display(self,ais):
194 |
195 | context = self._get_context()
196 | context.Display(ais,True)
197 |
198 | if self.preferences['Fit automatically']: self.fit()
199 |
200 | @pyqtSlot(list)
201 | @pyqtSlot(list,bool)
202 | def display_many(self,ais_list,fit=None):
203 |
204 | context = self._get_context()
205 | for ais in ais_list:
206 | context.Display(ais,True)
207 |
208 | if self.preferences['Fit automatically'] and fit is None:
209 | self.fit()
210 | elif fit:
211 | self.fit()
212 |
213 | @pyqtSlot(QTreeWidgetItem,int)
214 | def update_item(self,item,col):
215 |
216 | ctx = self._get_context()
217 | if item.checkState(0):
218 | ctx.Display(item.ais,True)
219 | else:
220 | ctx.Erase(item.ais,True)
221 |
222 | @pyqtSlot(list)
223 | def remove_items(self,ais_items):
224 |
225 | ctx = self._get_context()
226 | for ais in ais_items: ctx.Erase(ais,True)
227 |
228 | @pyqtSlot()
229 | def redraw(self):
230 |
231 | self._get_viewer().Redraw()
232 |
233 | def fit(self):
234 |
235 | self.canvas.view.FitAll()
236 |
237 | def iso_view(self):
238 |
239 | v = self._get_view()
240 | v.SetProj(1,-1,1)
241 | v.SetTwist(0)
242 |
243 | def bottom_view(self):
244 |
245 | v = self._get_view()
246 | v.SetProj(0,0,-1)
247 | v.SetTwist(0)
248 |
249 | def top_view(self):
250 |
251 | v = self._get_view()
252 | v.SetProj(0,0,1)
253 | v.SetTwist(0)
254 |
255 | def front_view(self):
256 |
257 | v = self._get_view()
258 | v.SetProj(0,1,0)
259 | v.SetTwist(0)
260 |
261 | def back_view(self):
262 |
263 | v = self._get_view()
264 | v.SetProj(0,-1,0)
265 | v.SetTwist(0)
266 |
267 | def left_view(self):
268 |
269 | v = self._get_view()
270 | v.SetProj(-1,0,0)
271 | v.SetTwist(0)
272 |
273 | def right_view(self):
274 |
275 | v = self._get_view()
276 | v.SetProj(1,0,0)
277 | v.SetTwist(0)
278 |
279 | def shaded_view(self):
280 |
281 | c = self._get_context()
282 | c.SetDisplayMode(AIS_Shaded, True)
283 |
284 | def wireframe_view(self):
285 |
286 | c = self._get_context()
287 | c.SetDisplayMode(AIS_WireFrame, True)
288 |
289 | def show_grid(self,
290 | step=1.,
291 | size=10.+1e-6,
292 | color1=(.7,.7,.7),
293 | color2=(0,0,0)):
294 |
295 | viewer = self._get_viewer()
296 | viewer.ActivateGrid(Aspect_GT_Rectangular,
297 | Aspect_GDM_Lines)
298 | viewer.SetRectangularGridGraphicValues(size, size, 0)
299 | viewer.SetRectangularGridValues(0, 0, step, step, 0)
300 | grid = viewer.Grid()
301 | grid.SetColors(Quantity_Color(*color1,TOC_RGB),
302 | Quantity_Color(*color2,TOC_RGB))
303 |
304 | def hide_grid(self):
305 |
306 | viewer = self._get_viewer()
307 | viewer.DeactivateGrid()
308 |
309 | @pyqtSlot(bool,float)
310 | @pyqtSlot(bool)
311 | def toggle_grid(self,
312 | value : bool,
313 | dim : float = 10.):
314 |
315 | if value:
316 | self.show_grid(step=dim/20,size=dim+1e-9)
317 | else:
318 | self.hide_grid()
319 |
320 | @pyqtSlot(gp_Ax3)
321 | def set_grid_orientation(self,orientation : gp_Ax3):
322 |
323 | viewer = self._get_viewer()
324 | viewer.SetPrivilegedPlane(orientation)
325 |
326 | def show_axis(self,origin = (0,0,0), direction=(0,0,1)):
327 |
328 | ax_placement = Geom_Axis1Placement(gp_Ax1(gp_Pnt(*origin),
329 | gp_Dir(*direction)))
330 | ax = AIS_Axis(ax_placement)
331 | self._display_ais(ax)
332 |
333 | def save_screenshot(self):
334 |
335 | fname = get_save_filename(self.IMAGE_EXTENSIONS)
336 | if fname != '':
337 | self._get_view().Dump(fname)
338 |
339 | def _display_ais(self,ais):
340 |
341 | self._get_context().Display(ais)
342 |
343 |
344 | def _get_view(self):
345 |
346 | return self.canvas.view
347 |
348 | def _get_viewer(self):
349 |
350 | return self.canvas.viewer
351 |
352 | def _get_context(self):
353 |
354 | return self.canvas.context
355 |
356 | @pyqtSlot(list)
357 | def handle_selection(self,obj):
358 |
359 | self.sigObjectSelected.emit(obj)
360 |
361 | @pyqtSlot(list)
362 | def set_selected(self,ais):
363 |
364 | ctx = self._get_context()
365 | ctx.ClearSelected(False)
366 |
367 | for obj in ais:
368 | ctx.AddOrRemoveSelected(obj,False)
369 |
370 | self.redraw()
371 |
372 |
373 | if __name__ == "__main__":
374 |
375 | import sys
376 | from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
377 |
378 | app = QApplication(sys.argv)
379 | viewer = OCCViewer()
380 |
381 | dlg = QDialog()
382 | dlg.setFixedHeight(400)
383 | dlg.setFixedWidth(600)
384 |
385 | layout(dlg,(viewer,),dlg)
386 | dlg.show()
387 |
388 | box = BRepPrimAPI_MakeBox(20,20,30)
389 | box_ais = AIS_ColoredShape(box.Shape())
390 | viewer.display(box_ais)
391 |
392 | sys.exit(app.exec_())
393 |
--------------------------------------------------------------------------------
/cqgui_env.yml:
--------------------------------------------------------------------------------
1 | name: cq-occ-conda-test-py3
2 | channels:
3 | - CadQuery
4 | - conda-forge
5 | dependencies:
6 | - pyqt=5
7 | - pyqtgraph
8 | - python=3.10
9 | - spyder=5
10 | - ipython=8.4.0
11 | - jedi=0.17.2
12 | - path
13 | - logbook
14 | - requests
15 | - cadquery=master
16 | - qtconsole=5.4.1
17 |
--------------------------------------------------------------------------------
/environment.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | channels:
3 | - conda-forge
4 | - defaults
5 | dependencies:
6 | - python=3.9
--------------------------------------------------------------------------------
/icons/back_view.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
132 |
--------------------------------------------------------------------------------
/icons/bottom_view.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
132 |
--------------------------------------------------------------------------------
/icons/cadquery_logo_dark.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmwright/CQ-editor/2d5b00140258b330f50ac5be829a252ebd51b2af/icons/cadquery_logo_dark.ico
--------------------------------------------------------------------------------
/icons/cadquery_logo_dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
79 |
--------------------------------------------------------------------------------
/icons/front_view.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
132 |
--------------------------------------------------------------------------------
/pyinstaller.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python -*-
2 |
3 | import sys, site, os
4 | from path import Path
5 | from PyInstaller.utils.hooks import collect_all, collect_submodules
6 |
7 | block_cipher = None
8 |
9 | spyder_data = Path(site.getsitepackages()[-1]) / 'spyder'
10 | parso_grammar = (Path(site.getsitepackages()[-1]) / 'parso/python').glob('grammar*')
11 | cqw_path = Path(site.getsitepackages()[-1]) / 'cq_warehouse'
12 |
13 | if sys.platform == 'linux':
14 | occt_dir = os.path.join(Path(sys.prefix), 'share', 'opencascade')
15 | ocp_path = (os.path.join(HOMEPATH, 'OCP.cpython-39-x86_64-linux-gnu.so'), '.')
16 | elif sys.platform == 'darwin':
17 | occt_dir = os.path.join(Path(sys.prefix), 'share', 'opencascade')
18 | ocp_path = (os.path.join(HOMEPATH, 'OCP.cpython-39-darwin.so'), '.')
19 | elif sys.platform == 'win32':
20 | occt_dir = os.path.join(Path(sys.prefix), 'Library', 'share', 'opencascade')
21 | ocp_path = (os.path.join(HOMEPATH, 'OCP.cp39-win_amd64.pyd'), '.')
22 |
23 | datas1, binaries1, hiddenimports1 = collect_all('debugpy')
24 | hiddenimports2 = collect_submodules('xmlrpc')
25 |
26 | a = Analysis(['run.py'],
27 | pathex=['.'],
28 | binaries=[ocp_path] + binaries1,
29 | datas=[(spyder_data, 'spyder'),
30 | (occt_dir, 'opencascade'),
31 | (cqw_path, 'cq_warehouse')] +
32 | [(p, 'parso/python') for p in parso_grammar] + datas1,
33 | hiddenimports=['ipykernel.datapub', 'vtkmodules', 'vtkmodules.all',
34 | 'pyqtgraph.graphicsItems.ViewBox.axisCtrlTemplate_pyqt5',
35 | 'pyqtgraph.graphicsItems.PlotItem.plotConfigTemplate_pyqt5',
36 | 'pyqtgraph.imageview.ImageViewTemplate_pyqt5', 'debugpy', 'xmlrpc',
37 | 'zmq.backend', 'cq_warehouse', 'cq_warehouse.bearing', 'cq_warehouse.chain',
38 | 'cq_warehouse.drafting', 'cq_warehouse.extensions', 'cq_warehouse.fastener',
39 | 'cq_warehouse.sprocket', 'cq_warehouse.thread', 'cq_gears', 'cq_cache'] + hiddenimports1 + hiddenimports2,
40 | hookspath=[],
41 | runtime_hooks=['pyinstaller/pyi_rth_occ.py',
42 | 'pyinstaller/pyi_rth_fontconfig.py'],
43 | excludes=['_tkinter'],
44 | win_no_prefer_redirects=False,
45 | win_private_assemblies=False,
46 | cipher=block_cipher,
47 | noarchive=False)
48 |
49 | # There is an issue that keeps the OpenSSL libraries from being copied to the output directory.
50 | # This should work if nothing else, but does not with GitHub Actions
51 | if sys.platform == 'win32':
52 | from PyInstaller.depend.bindepend import getfullnameof
53 | rel_data_path = ['PyQt5', 'Qt', 'bin']
54 | a.datas += [
55 | (getfullnameof('libssl-1_1-x64.dll'), os.path.join(*rel_data_path), 'DATA'),
56 | (getfullnameof('libcrypto-1_1-x64.dll'), os.path.join(*rel_data_path), 'DATA'),
57 | ]
58 |
59 |
60 | pyz = PYZ(a.pure, a.zipped_data,
61 | cipher=block_cipher)
62 | exe = EXE(pyz,
63 | a.scripts,
64 | [],
65 | exclude_binaries=True,
66 | name='CQ-editor',
67 | debug=False,
68 | bootloader_ignore_signals=False,
69 | strip=False,
70 | upx=True,
71 | console=True,
72 | icon='icons/cadquery_logo_dark.ico')
73 |
74 | exclude = ()
75 | #exclude = ('libGL','libEGL','libbsd')
76 | a.binaries = TOC([x for x in a.binaries if not x[0].startswith(exclude)])
77 |
78 | coll = COLLECT(exe,
79 | a.binaries,
80 | a.zipfiles,
81 | a.datas,
82 | strip=False,
83 | upx=True,
84 | name='CQ-editor')
85 |
--------------------------------------------------------------------------------
/pyinstaller/CQ-editor.cmd:
--------------------------------------------------------------------------------
1 | call .\CQ-editor\CQ-editor.exe
--------------------------------------------------------------------------------
/pyinstaller/CQ-editor.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | xattr -r -d com.apple.quarantine .
3 | export QT_MAC_WANTS_LAYER=1
4 | chmod u+x ./CQ-editor/CQ-editor
5 | ./CQ-editor/CQ-editor
6 |
--------------------------------------------------------------------------------
/pyinstaller/extrahooks/hook-casadi.py:
--------------------------------------------------------------------------------
1 | # hook-casadi.py
2 | from PyInstaller.utils.hooks import collect_dynamic_libs
3 |
4 | binaries = collect_dynamic_libs('casadi')
5 |
6 | # Something about legacy import codepaths in casadi.casadi causes PyInstaller's analysis to pick up
7 | # casadi._casadi as a top-level _casadi module, which is wrong.
8 | hiddenimports = ['casadi._casadi']
9 | excludedimports = ['_casadi']
10 |
--------------------------------------------------------------------------------
/pyinstaller/pyi_rth_fontconfig.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | if sys.platform.startswith('linux'):
5 | os.environ['FONTCONFIG_FILE'] = '/etc/fonts/fonts.conf'
6 | os.environ['FONTCONFIG_PATH'] = '/etc/fonts/'
7 |
--------------------------------------------------------------------------------
/pyinstaller/pyi_rth_occ.py:
--------------------------------------------------------------------------------
1 | from os import environ as env
2 |
3 | env['CASROOT'] = 'opencascade'
4 |
5 | env['CSF_ShadersDirectory'] = 'opencascade/src/Shaders'
6 | env['CSF_UnitsLexicon'] = 'opencascade/src/UnitsAPI/Lexi_Expr.dat'
7 | env['CSF_UnitsDefinition'] = 'opencascade/src/UnitsAPI/Units.dat'
8 |
--------------------------------------------------------------------------------
/pyinstaller_pip.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python -*-
2 |
3 | import sys, site, os
4 | from path import Path
5 | from PyInstaller.utils.hooks import collect_all, collect_submodules
6 |
7 | block_cipher = None
8 |
9 | spyder_data = Path(site.getsitepackages()[-1]) / 'spyder'
10 | parso_grammar = (Path(site.getsitepackages()[-1]) / 'parso/python').glob('grammar*')
11 | cqw_path = Path(site.getsitepackages()[-1]) / 'cq_warehouse'
12 | cq_path = Path(site.getsitepackages()[-1]) / 'cadquery'
13 |
14 | if sys.platform == 'linux':
15 | occt_dir = os.path.join(Path(sys.prefix), 'share', 'opencascade')
16 | ocp_path = [(os.path.join(HOMEPATH, 'OCP.cpython-39-x86_64-linux-gnu.so'), '.')]
17 | elif sys.platform == 'darwin':
18 | occt_dir = os.path.join(Path(sys.prefix), 'share', 'opencascade')
19 | ocp_path = [(os.path.join(HOMEPATH, 'OCP.cpython-39-darwin.so'), '.')]
20 | elif sys.platform == 'win32':
21 | occt_dir = os.path.join(Path(sys.prefix), 'Library', 'share', 'opencascade')
22 | ocp_path = [(os.path.join(HOMEPATH, 'OCP.cp39-win_amd64.pyd'), '.')]
23 |
24 | datas1, binaries1, hiddenimports1 = collect_all('debugpy')
25 | hiddenimports2 = collect_submodules('xmlrpc')
26 |
27 | a = Analysis(['run.py'],
28 | pathex=['.'],
29 | binaries=ocp_path + binaries1,
30 | datas=[(spyder_data, 'spyder'),
31 | (cqw_path, 'cq_warehouse'),
32 | (cq_path, 'cadquery')] +
33 | [(p, 'parso/python') for p in parso_grammar] + datas1,
34 | hiddenimports=['ipykernel.datapub', 'debugpy', 'vtkmodules', 'vtkmodules.all',
35 | 'pyqtgraph.graphicsItems.ViewBox.axisCtrlTemplate_pyqt5',
36 | 'pyqtgraph.graphicsItems.PlotItem.plotConfigTemplate_pyqt5',
37 | 'pyqtgraph.imageview.ImageViewTemplate_pyqt5', 'xmlrpc',
38 | 'zmq.backend', 'cq_warehouse', 'cq_warehouse.bearing', 'cq_warehouse.chain',
39 | 'cq_warehouse.drafting', 'cq_warehouse.extensions', 'cq_warehouse.fastener',
40 | 'cq_warehouse.sprocket', 'cq_warehouse.thread', 'cq_gears', 'cq_cache',
41 | 'build123d', 'cqmore'] + hiddenimports1 + hiddenimports2,
42 | hookspath=['pyinstaller/extrahooks/'],
43 | runtime_hooks=['pyinstaller/pyi_rth_occ.py',
44 | 'pyinstaller/pyi_rth_fontconfig.py'],
45 | excludes=['_tkinter'],
46 | win_no_prefer_redirects=False,
47 | win_private_assemblies=False,
48 | cipher=block_cipher,
49 | noarchive=False)
50 |
51 | # There is an issue that keeps the OpenSSL libraries from being copied to the output directory.
52 | # This should work if nothing else, but does not with GitHub Actions
53 | if sys.platform == 'win32':
54 | from PyInstaller.depend.bindepend import getfullnameof
55 | rel_data_path = ['PyQt5', 'Qt', 'bin']
56 | a.datas += [
57 | (getfullnameof('libssl-1_1-x64.dll'), os.path.join(*rel_data_path), 'DATA'),
58 | (getfullnameof('libcrypto-1_1-x64.dll'), os.path.join(*rel_data_path), 'DATA'),
59 | ]
60 |
61 |
62 | pyz = PYZ(a.pure, a.zipped_data,
63 | cipher=block_cipher)
64 | exe = EXE(pyz,
65 | a.scripts,
66 | [],
67 | exclude_binaries=True,
68 | name='CQ-editor',
69 | debug=False,
70 | bootloader_ignore_signals=False,
71 | strip=False,
72 | upx=True,
73 | console=True,
74 | icon='icons/cadquery_logo_dark.ico')
75 |
76 | exclude = ()
77 | #exclude = ('libGL','libEGL','libbsd')
78 | a.binaries = TOC([x for x in a.binaries if not x[0].startswith(exclude)])
79 |
80 | coll = COLLECT(exe,
81 | a.binaries,
82 | a.zipfiles,
83 | a.datas,
84 | strip=False,
85 | upx=True,
86 | name='CQ-editor')
87 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | xvfb_args=-ac +extension GLX +render
3 | log_level=DEBUG
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | import os, sys, asyncio
2 | import faulthandler
3 |
4 | faulthandler.enable()
5 |
6 | if 'CASROOT' in os.environ:
7 | del os.environ['CASROOT']
8 |
9 | if sys.platform == 'win32':
10 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
11 |
12 | from cq_editor.__main__ import main
13 |
14 |
15 | if __name__ == '__main__':
16 | main()
17 |
--------------------------------------------------------------------------------
/runtests_locally.sh:
--------------------------------------------------------------------------------
1 | python -m pytest --no-xvfb -s
2 |
--------------------------------------------------------------------------------
/screenshots/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmwright/CQ-editor/2d5b00140258b330f50ac5be829a252ebd51b2af/screenshots/screenshot1.png
--------------------------------------------------------------------------------
/screenshots/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmwright/CQ-editor/2d5b00140258b330f50ac5be829a252ebd51b2af/screenshots/screenshot2.png
--------------------------------------------------------------------------------
/screenshots/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmwright/CQ-editor/2d5b00140258b330f50ac5be829a252ebd51b2af/screenshots/screenshot3.png
--------------------------------------------------------------------------------
/screenshots/screenshot4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmwright/CQ-editor/2d5b00140258b330f50ac5be829a252ebd51b2af/screenshots/screenshot4.png
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import codecs
2 | import os.path
3 |
4 | from setuptools import setup, find_packages
5 |
6 | def read(rel_path):
7 | here = os.path.abspath(os.path.dirname(__file__))
8 | with codecs.open(os.path.join(here, rel_path), 'r') as fp:
9 | return fp.read()
10 |
11 | def get_version(rel_path):
12 | for line in read(rel_path).splitlines():
13 | if line.startswith('__version__'):
14 | delim = '"' if '"' in line else "'"
15 | return line.split(delim)[1]
16 | else:
17 | raise RuntimeError("Unable to find version string.")
18 |
19 | setup(
20 | name="CQ-editor",
21 | version=get_version("cq_editor/_version.py"),
22 | packages=find_packages(),
23 | entry_points={
24 | "gui_scripts": [
25 | "cq-editor = cq_editor.__main__:main",
26 | "CQ-editor = cq_editor.__main__:main",
27 | ]
28 | },
29 | python_requires=">=3.8,<3.11",
30 | install_requires=[
31 | "logbook>=1",
32 | "ipython==8.4.0",
33 | "jedi==0.17.2",
34 | "path>=16",
35 | "PyQt5>=5",
36 | "requests>=2,<3",
37 | "spyder>=5,<6",
38 | "pyqtgraph==0.12.4",
39 | ],
40 | )
41 |
--------------------------------------------------------------------------------