├── .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 | [![Build status](https://ci.appveyor.com/api/projects/status/g98rs7la393mgy91/branch/master?svg=true)](https://ci.appveyor.com/project/adam-urbanczyk/cq-editor/branch/master) 4 | [![codecov](https://codecov.io/gh/CadQuery/CQ-editor/branch/master/graph/badge.svg)](https://codecov.io/gh/CadQuery/CQ-editor) 5 | [![Build Status](https://dev.azure.com/cadquery/CQ-editor/_apis/build/status/CadQuery.CQ-editor?branchName=master)](https://dev.azure.com/cadquery/CQ-editor/_build/latest?definitionId=3&branchName=master) 6 | [![DOI](https://zenodo.org/badge/136604983.svg)](https://zenodo.org/badge/latestdoi/136604983) 7 | 8 | CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. 9 | 10 | Screenshot 11 | Screenshot 12 | Screenshot 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 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /icons/bottom_view.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 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 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 65 | 68 | 72 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /icons/front_view.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 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 | --------------------------------------------------------------------------------