from jmwright 72b67da
162
README.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# CadQuery editor
|
||||
|
||||
[](https://ci.appveyor.com/project/adam-urbanczyk/cq-editor/branch/master)
|
||||
[](https://codecov.io/gh/CadQuery/CQ-editor)
|
||||
[](https://dev.azure.com/cadquery/CQ-editor/_build/latest?definitionId=3&branchName=master)
|
||||
[](https://zenodo.org/badge/latestdoi/136604983)
|
||||
|
||||
CadQuery GUI editor based on PyQT supports Linux, Windows and Mac.
|
||||
|
||||
<img src="https://github.com/CadQuery/CQ-editor/raw/master/screenshots/screenshot2.png" alt="Screenshot" width="70%" >
|
||||
<img src="https://github.com/CadQuery/CQ-editor/raw/master/screenshots/screenshot3.png" alt="Screenshot" width="70%" >
|
||||
<img src="https://github.com/CadQuery/CQ-editor/raw/master/screenshots/screenshot4.png" alt="Screenshot" width="70%" >
|
||||
|
||||
## Notable features
|
||||
|
||||
* OCCT based
|
||||
* Graphical debugger for CadQuery scripts
|
||||
* Step through script and watch how your model changes
|
||||
* CadQuery object stack inspector
|
||||
* Visual inspection of current workplane and selected items
|
||||
* Insight into evolution of the model
|
||||
* Export to various formats
|
||||
* STL
|
||||
* STEP
|
||||
|
||||
## Installation - Pre-Built Packages (Recommended)
|
||||
|
||||
### Release Packages
|
||||
|
||||
Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download the zip file for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly.
|
||||
|
||||
### Development Packages
|
||||
|
||||
Development builds are also available, but can be unstable and should be used at your own risk. Click on the newest build with a green checkmark [here](https://github.com/jmwright/CQ-editor/actions?query=workflow%3Abuild), wait for the _Artifacts_ section at the bottom of the page to load, and then click on the appropriate download for your operating system. Extract the archive file and run the shell (Linux/MacOS) or cmd (Windows) script in the root CQ-editor directory. The CQ-editor window should launch.
|
||||
|
||||
## Installation (Anaconda)
|
||||
|
||||
Use conda to install:
|
||||
```
|
||||
conda install -c cadquery -c conda-forge cq-editor=master
|
||||
```
|
||||
and then simply type `cq-editor` to run it. This installs the latest version built directly from the HEAD of this repository.
|
||||
|
||||
Alternatively clone this git repository and set up the following conda environment:
|
||||
```
|
||||
conda env create -f cqgui_env.yml -n cqgui
|
||||
conda activate cqgui
|
||||
python run.py
|
||||
```
|
||||
|
||||
On some linux distributions (e.g. `Ubuntu 18.04`) it might be necessary to install additonal packages:
|
||||
```
|
||||
sudo apt install libglu1-mesa libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev
|
||||
```
|
||||
On Fedora 29 the packages can be installed as follows:
|
||||
```
|
||||
dnf install -y mesa-libGLU mesa-libGL mesa-libGLU-devel
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Showing Objects
|
||||
|
||||
By default, CQ-editor will display a 3D representation of all `Workplane` objects in a script with a default color and alpha (transparency). To have more control over what is shown, and what the color and alpha settings are, the `show_object` method can be used. `show_object` tells CQ-editor to explicity display an object, and accepts the `options` parameter. The `options` parameter is a dictionary of rendering options named `alpha` and `color`. `alpha` is scaled between 0.0 and 1.0, with 0.0 being completely opaque and 1.0 being completely transparent. The color is set using R (red), G (green) and B (blue) values, and each one is scaled from 0 to 255. Either option or both can be omitted.
|
||||
|
||||
```python
|
||||
show_object(result, options={"alpha":0.5, "color": (64, 164, 223)})
|
||||
```
|
||||
|
||||
Note that `show_object` works for `Shape` and `TopoDS_Shape` objects too. In order to display objects from the embedded Python console use `show`.
|
||||
|
||||
### Rotate, Pan and Zoom the 3D View
|
||||
|
||||
The following mouse controls can be used to alter the view of the 3D object, and should be familiar to CAD users, even if the mouse buttons used may differ.
|
||||
|
||||
* _Left Mouse Button_ + _Drag_ = Rotate
|
||||
* _Middle Mouse Button_ + _Drag_ = Pan
|
||||
* _Right Mouse Button_ + _Drag_ = Zoom
|
||||
* _Mouse Wheel_ = Zoom
|
||||
|
||||
### Debugging Objects
|
||||
|
||||
There are multiple menu options to help in debugging a CadQuery script. They are included in the `Run` menu, with corresponding buttons in the toolbar. Below is a listing of what each menu item does.
|
||||
|
||||
* `Debug` (Ctrl + F5) - Instead of running the script completely through as with the `Render` item, it begins executing the script but stops at the first non-empty line, waiting for the user to continue execution manually.
|
||||
* `Step` (Ctrl + F10) - Will move execution of the script to the next non-empty line.
|
||||
* `Step in` (Ctrl + F11) - Will follow the flow of execution to the inside of a user-created function defined within the script.
|
||||
* `Continue` (Ctrl + F12) - Completes execution of the script, starting from the current line that is being debugged.
|
||||
|
||||
It is also possible to do visual debugging of objects. This is possible by using the `debug()` function to display an object instead of `show_object()`. An alternative method for the following code snippet is shown below for highlighting a specific face, but it demonstrates one use of `debug()`.
|
||||
```python
|
||||
import cadquery as cq
|
||||
|
||||
result = cq.Workplane().box(10, 10, 10)
|
||||
|
||||
highlight = result.faces('>Z')
|
||||
|
||||
show_object(result, name='box')
|
||||
debug(highlight)
|
||||
```
|
||||
Objects displayed with `debug()` are colored in red and have their alpha set so they are semi-transparent. This can be useful for checking for interference, clearance, or whether the expected face is being selected, as in the code above.
|
||||
|
||||
### Console Logging
|
||||
|
||||
Python's standard `print()` function will not output to the CQ-editor GUI, and `log()` should be used instead. `log()` will output the provided text to the _Log viewer_ panel, providing another way to debug CadQuery scripts. If you started CQ-editor from the command line, the `print()` function will output text back to it.
|
||||
|
||||
### Using an External Code Editor
|
||||
|
||||
Some users prefer to use an external code editor instead of the built-in Spyder-based editor that comes stock with CQ-editor. The steps below should allow CQ-editor to work alongside most text editors.
|
||||
|
||||
1. Open the Preferences dialog by clicking `Edit->Preferences`.
|
||||
2. Make sure that `Code Editor` is selected in the left pane.
|
||||
3. Check `Autoreload` in the right pane.
|
||||
4. If CQ-editor is not catching the saves from your external editor, increasing `Autoreload delay` in the right pane may help. This issue has been reported when using vim or emacs.
|
||||
|
||||
### Exporting an Object
|
||||
|
||||
Any object can be exported to either STEP or STL format. The steps for doing so are listed below.
|
||||
|
||||
1. Highlight the object to be exported in the _Objects_ panel.
|
||||
2. Click either `Export as STL` or `Export as STEP` from the `Tools` menu, depending on which file format you want to export. Both of these options will be disabled if an object is not selected in the _Objects_ panel.
|
||||
|
||||
Clicking either _Export_ item will present a file dialog that allows the file name and location of the export file to be set.
|
||||
|
||||
### Displaying All Wires for Debugging
|
||||
|
||||
**NOTE:** This is intended for debugging purposes, and if not removed, could interfere with the execution of your model in some cases.
|
||||
|
||||
Using `consolidateWires()` is a quick way to combine all wires so that they will display together in CQ-editor's viewer. In the following code, it is used to make sure that both rects are displayed. This technique can make it easier to debug in-progress 2D sketches.
|
||||
|
||||
```python
|
||||
import cadquery as cq
|
||||
res = cq.Workplane().rect(1,1).rect(3,3).consolidateWires()
|
||||
show_object(res)
|
||||
```
|
||||
|
||||
### Highlighting a Specific Face
|
||||
|
||||
Highlighting a specific face in a different color can be useful when debugging, or when trying to learn CadQuery selectors. The following code creates a separate, highlighted object to show the selected face in red. This is an alternative to using a `debug()` object, and in most cases `debug()` will provide the same result with less code. However, this method will allow the color and alpha of the highlight object to be customized.
|
||||
|
||||
```python
|
||||
import cadquery as cq
|
||||
|
||||
result = cq.Workplane().box(10, 10, 10)
|
||||
|
||||
highlight = result.faces('>Z')
|
||||
|
||||
show_object(result)
|
||||
show_object(highlight,'highlight',options=dict(alpha=0.1,color=(1.,0,0)))
|
||||
```
|
||||
|
||||
### Naming an Object
|
||||
|
||||
By default, objects have a randomly generated ID in the object inspector. However, it can be useful to name objects so that it is easier to identify them. The `name` parameter of `show_object()` can be used to do this.
|
||||
|
||||
```python
|
||||
import cadquery as cq
|
||||
|
||||
result = cq.Workplane().box(10, 10, 10)
|
||||
|
||||
show_object(result, name='box')
|
||||
```
|
||||
57
appveyor.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
shallow_clone: false
|
||||
|
||||
image:
|
||||
- Ubuntu2004
|
||||
- Ubuntu1804
|
||||
- Visual Studio 2015
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- PYTEST_QT_API: pyqt5
|
||||
CODECOV_TOKEN:
|
||||
secure: ZggK9wgDeFdTp0pu0MEV+SY4i/i1Ls0xrEC2MxSQOQ0JQV+TkpzJJzI4au7L8TpD
|
||||
MINICONDA_DIRNAME: C:\FreshMiniconda
|
||||
|
||||
install:
|
||||
- sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then sudo apt update; sudo apt -y --force-yes install libglu1-mesa xvfb libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev; fi
|
||||
- sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Linux-x86_64.sh; fi
|
||||
- sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-MacOSX-x86_64.sh; fi
|
||||
- sh: bash miniconda.sh -b -p $HOME/miniconda
|
||||
- sh: source $HOME/miniconda/bin/activate
|
||||
- cmd: curl -fsSL -o miniconda.exe https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Windows-x86_64.exe
|
||||
- cmd: miniconda.exe /S /InstallationType=JustMe /D=%MINICONDA_DIRNAME%
|
||||
- cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%"
|
||||
- cmd: activate
|
||||
- mamba info
|
||||
- mamba env create --name cqgui -f cqgui_env.yml
|
||||
- sh: source activate cqgui
|
||||
- cmd: activate cqgui
|
||||
- mamba list
|
||||
- mamba install -y pytest pluggy pytest-qt
|
||||
- mamba install -y pytest-mock pytest-cov pytest-repeat codecov pyvirtualdisplay
|
||||
|
||||
build: false
|
||||
|
||||
before_test:
|
||||
- sh: ulimit -c unlimited -S
|
||||
- sh: sudo rm -f /cores/core.*
|
||||
|
||||
test_script:
|
||||
- sh: export PYTHONPATH=$(pwd)
|
||||
- cmd: set PYTHONPATH=%cd%
|
||||
- sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then xvfb-run -s '-screen 0 1920x1080x24 +iglx' pytest -v --cov=cq_editor; fi
|
||||
- sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then pytest -v --cov=cq_editor; fi
|
||||
- cmd: pytest -v --cov=cq_editor
|
||||
|
||||
on_success:
|
||||
- codecov
|
||||
|
||||
#on_failure:
|
||||
# - qtdiag
|
||||
# - ls /cores/core.*
|
||||
# - lldb --core `ls /cores/core.*` --batch --one-line "bt"
|
||||
|
||||
on_finish:
|
||||
# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
||||
# - sh: export APPVEYOR_SSH_BLOCK=true
|
||||
# - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -
|
||||
49
azure-pipelines.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
- refs/tags/*
|
||||
|
||||
pr:
|
||||
- master
|
||||
|
||||
resources:
|
||||
repositories:
|
||||
- repository: templates
|
||||
type: github
|
||||
name: jmwright/conda-packages
|
||||
endpoint: CadQuery
|
||||
|
||||
parameters:
|
||||
- name: minor
|
||||
type: object
|
||||
default:
|
||||
- 8
|
||||
- 9
|
||||
- 10
|
||||
|
||||
jobs:
|
||||
- ${{ each minor in parameters.minor }}:
|
||||
- template: conda-build.yml@templates
|
||||
parameters:
|
||||
name: Linux
|
||||
vmImage: 'ubuntu-18.04'
|
||||
py_maj: 3
|
||||
py_min: ${{minor}}
|
||||
conda_bld: 3.21.6
|
||||
|
||||
- template: conda-build.yml@templates
|
||||
parameters:
|
||||
name: macOS
|
||||
vmImage: 'macOS-10.15'
|
||||
py_maj: 3
|
||||
py_min: ${{minor}}
|
||||
conda_bld: 3.21.6
|
||||
|
||||
- template: conda-build.yml@templates
|
||||
parameters:
|
||||
name: Windows
|
||||
vmImage: 'windows-latest'
|
||||
py_maj: 3
|
||||
py_min: ${{minor}}
|
||||
conda_bld: 3.21.6
|
||||
24
bundle.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from sys import platform
|
||||
from path import Path
|
||||
from os import system
|
||||
from shutil import make_archive
|
||||
from cq_editor import __version__ as version
|
||||
|
||||
out_p = Path('dist/CQ-editor')
|
||||
out_p.rmtree_p()
|
||||
|
||||
build_p = Path('build')
|
||||
build_p.rmtree_p()
|
||||
|
||||
system("pyinstaller pyinstaller.spec")
|
||||
|
||||
if platform == 'linux':
|
||||
with out_p:
|
||||
p = Path('.').glob('libpython*')[0]
|
||||
p.symlink(p.split(".so")[0]+".so")
|
||||
|
||||
make_archive(f'CQ-editor-{version}-linux64','bztar', out_p / '..', 'CQ-editor')
|
||||
|
||||
elif platform == 'win32':
|
||||
|
||||
make_archive(f'CQ-editor-{version}-win64','zip', out_p / '..', 'CQ-editor')
|
||||
30
collect_icons.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from glob import glob
|
||||
from subprocess import call
|
||||
from os import remove
|
||||
|
||||
TEMPLATE = \
|
||||
'''<RCC>
|
||||
<qresource prefix="/images">
|
||||
{}
|
||||
</qresource>
|
||||
</RCC>'''
|
||||
|
||||
ITEM_TEMPLATE = '<file>{}</file>'
|
||||
|
||||
QRC_OUT = 'icons.qrc'
|
||||
RES_OUT = 'src/icons_res.py'
|
||||
TOOL = 'pyrcc5'
|
||||
|
||||
items = []
|
||||
|
||||
for i in glob('icons/*.svg'):
|
||||
items.append(ITEM_TEMPLATE.format(i))
|
||||
|
||||
|
||||
qrc_text = TEMPLATE.format('\n'.join(items))
|
||||
|
||||
with open(QRC_OUT,'w') as f:
|
||||
f.write(qrc_text)
|
||||
|
||||
call([TOOL,QRC_OUT,'-o',RES_OUT])
|
||||
remove(QRC_OUT)
|
||||
36
conda/meta.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
package:
|
||||
name: cq-editor
|
||||
version: {{ environ.get('PACKAGE_VERSION') }}
|
||||
|
||||
source:
|
||||
path: ..
|
||||
|
||||
build:
|
||||
string: {{ 'py'+environ.get('PYTHON_VERSION')}}
|
||||
script: python setup.py install --single-version-externally-managed --record=record.txt
|
||||
entry_points:
|
||||
- cq-editor = cq_editor.__main__:main
|
||||
- CQ-editor = cq_editor.__main__:main
|
||||
requirements:
|
||||
build:
|
||||
- python {{ environ.get('PYTHON_VERSION') }}
|
||||
- setuptools
|
||||
|
||||
run:
|
||||
- python {{ environ.get('PYTHON_VERSION') }}
|
||||
- cadquery=master
|
||||
- ocp
|
||||
- logbook
|
||||
- pyqt=5.*
|
||||
- pyqtgraph
|
||||
- spyder=5.*
|
||||
- path
|
||||
- logbook
|
||||
- requests
|
||||
|
||||
test:
|
||||
imports:
|
||||
- cq_editor
|
||||
|
||||
about:
|
||||
summary: GUI for CadQuery 2
|
||||
1
cq_editor/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from ._version import __version__
|
||||
28
cq_editor/__main__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
NAME = 'CQ-editor'
|
||||
|
||||
#need to initialize QApp here, otherewise svg icons do not work on windows
|
||||
app = QApplication(sys.argv,
|
||||
applicationName=NAME)
|
||||
|
||||
from .main_window import MainWindow
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser(description=NAME)
|
||||
parser.add_argument('filename',nargs='?',default=None)
|
||||
|
||||
args = parser.parse_args(app.arguments()[1:])
|
||||
|
||||
win = MainWindow(filename=args.filename if args.filename else None)
|
||||
win.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
main()
|
||||
1
cq_editor/_version.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = "0.3.0dev"
|
||||
148
cq_editor/cq_utils.py
Normal file
@@ -0,0 +1,148 @@
|
||||
import cadquery as cq
|
||||
from cadquery.occ_impl.assembly import toCAF
|
||||
|
||||
from typing import List, Union
|
||||
from imp import reload
|
||||
from types import SimpleNamespace
|
||||
|
||||
from OCP.XCAFPrs import XCAFPrs_AISObject
|
||||
from OCP.TopoDS import TopoDS_Shape
|
||||
from OCP.AIS import AIS_InteractiveObject, AIS_Shape, AIS_ColoredShape
|
||||
from OCP.Quantity import \
|
||||
Quantity_TOC_RGB as TOC_RGB, Quantity_Color
|
||||
|
||||
from PyQt5.QtGui import QColor
|
||||
|
||||
def find_cq_objects(results : dict):
|
||||
|
||||
return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if isinstance(v,cq.Workplane)}
|
||||
|
||||
def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch]):
|
||||
|
||||
vals = []
|
||||
|
||||
if isinstance(obj,cq.Workplane):
|
||||
vals.extend(obj.vals())
|
||||
elif isinstance(obj,cq.Shape):
|
||||
vals.append(obj)
|
||||
elif isinstance(obj,list) and isinstance(obj[0],cq.Workplane):
|
||||
for o in obj: vals.extend(o.vals())
|
||||
elif isinstance(obj,list) and isinstance(obj[0],cq.Shape):
|
||||
vals.extend(obj)
|
||||
elif isinstance(obj, TopoDS_Shape):
|
||||
vals.append(cq.Shape.cast(obj))
|
||||
elif isinstance(obj,list) and isinstance(obj[0],TopoDS_Shape):
|
||||
vals.extend(cq.Shape.cast(o) for o in obj)
|
||||
elif isinstance(obj, cq.Sketch):
|
||||
if obj._faces:
|
||||
vals.append(obj._faces)
|
||||
else:
|
||||
vals.extend(obj._edges)
|
||||
else:
|
||||
raise ValueError(f'Invalid type {type(obj)}')
|
||||
|
||||
return cq.Compound.makeCompound(vals)
|
||||
|
||||
def to_workplane(obj : cq.Shape):
|
||||
|
||||
rv = cq.Workplane('XY')
|
||||
rv.objects = [obj,]
|
||||
|
||||
return rv
|
||||
|
||||
def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly, AIS_InteractiveObject],
|
||||
options={}):
|
||||
|
||||
shape = None
|
||||
|
||||
if isinstance(obj, cq.Assembly):
|
||||
label, shape = toCAF(obj)
|
||||
ais = XCAFPrs_AISObject(label)
|
||||
elif isinstance(obj, AIS_InteractiveObject):
|
||||
ais = obj
|
||||
else:
|
||||
shape = to_compound(obj)
|
||||
ais = AIS_ColoredShape(shape.wrapped)
|
||||
|
||||
if 'alpha' in options:
|
||||
ais.SetTransparency(options['alpha'])
|
||||
if 'color' in options:
|
||||
ais.SetColor(to_occ_color(options['color']))
|
||||
if 'rgba' in options:
|
||||
r,g,b,a = options['rgba']
|
||||
ais.SetColor(to_occ_color((r,g,b)))
|
||||
ais.SetTransparency(a)
|
||||
|
||||
return ais,shape
|
||||
|
||||
def export(obj : Union[cq.Workplane, List[cq.Workplane]], type : str,
|
||||
file, precision=1e-1):
|
||||
|
||||
comp = to_compound(obj)
|
||||
|
||||
if type == 'stl':
|
||||
comp.exportStl(file, tolerance=precision)
|
||||
elif type == 'step':
|
||||
comp.exportStep(file)
|
||||
elif type == 'brep':
|
||||
comp.exportBrep(file)
|
||||
|
||||
def to_occ_color(color) -> Quantity_Color:
|
||||
|
||||
if not isinstance(color, QColor):
|
||||
if isinstance(color, tuple):
|
||||
if isinstance(color[0], int):
|
||||
color = QColor(*color)
|
||||
elif isinstance(color[0], float):
|
||||
color = QColor.fromRgbF(*color)
|
||||
else:
|
||||
raise ValueError('Unknown color format')
|
||||
else:
|
||||
color = QColor(color)
|
||||
|
||||
return Quantity_Color(color.redF(),
|
||||
color.greenF(),
|
||||
color.blueF(),
|
||||
TOC_RGB)
|
||||
|
||||
def get_occ_color(ais : AIS_ColoredShape) -> QColor:
|
||||
|
||||
color = Quantity_Color()
|
||||
ais.Color(color)
|
||||
|
||||
return QColor.fromRgbF(color.Red(), color.Green(), color.Blue())
|
||||
|
||||
def reload_cq():
|
||||
|
||||
# NB: order of reloads is important
|
||||
reload(cq.types)
|
||||
reload(cq.occ_impl.geom)
|
||||
reload(cq.occ_impl.shapes)
|
||||
reload(cq.occ_impl.importers.dxf)
|
||||
reload(cq.occ_impl.importers)
|
||||
reload(cq.occ_impl.solver)
|
||||
reload(cq.occ_impl.assembly)
|
||||
reload(cq.occ_impl.sketch_solver)
|
||||
reload(cq.hull)
|
||||
reload(cq.selectors)
|
||||
reload(cq.sketch)
|
||||
reload(cq.occ_impl.exporters.svg)
|
||||
reload(cq.cq)
|
||||
reload(cq.occ_impl.exporters.utils)
|
||||
reload(cq.occ_impl.exporters.dxf)
|
||||
reload(cq.occ_impl.exporters.amf)
|
||||
reload(cq.occ_impl.exporters.json)
|
||||
#reload(cq.occ_impl.exporters.assembly)
|
||||
reload(cq.occ_impl.exporters)
|
||||
reload(cq.assembly)
|
||||
reload(cq)
|
||||
|
||||
|
||||
def is_obj_empty(obj : Union[cq.Workplane,cq.Shape]) -> bool:
|
||||
|
||||
rv = False
|
||||
|
||||
if isinstance(obj, cq.Workplane):
|
||||
rv = True if isinstance(obj.val(), cq.Vector) else False
|
||||
|
||||
return rv
|
||||
59
cq_editor/icons.py
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Fri May 25 14:47:10 2018
|
||||
|
||||
@author: adam
|
||||
"""
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from . import icons_res
|
||||
_icons = {
|
||||
'app' : QIcon(":/images/icons/cadquery_logo_dark.svg")
|
||||
}
|
||||
|
||||
import qtawesome as qta
|
||||
|
||||
_icons_specs = {
|
||||
'new' : (('fa.file-o',),{}),
|
||||
'open' : (('fa.folder-open-o',),{}),
|
||||
# borrowed from spider-ide
|
||||
'autoreload': [('fa.repeat', 'fa.clock-o'), {'options': [{'scale_factor': 0.75, 'offset': (-0.1, -0.1)}, {'scale_factor': 0.5, 'offset': (0.25, 0.25)}]}],
|
||||
'save' : (('fa.save',),{}),
|
||||
'save_as': (('fa.save','fa.pencil'),
|
||||
{'options':[{'scale_factor': 1,},
|
||||
{'scale_factor': 0.8,
|
||||
'offset': (0.2, 0.2)}]}),
|
||||
'run' : (('fa.play',),{}),
|
||||
'delete' : (('fa.trash',),{}),
|
||||
'delete-many' : (('fa.trash','fa.trash',),
|
||||
{'options' : \
|
||||
[{'scale_factor': 0.8,
|
||||
'offset': (0.2, 0.2),
|
||||
'color': 'gray'},
|
||||
{'scale_factor': 0.8}]}),
|
||||
'help' : (('fa.life-ring',),{}),
|
||||
'about': (('fa.info',),{}),
|
||||
'preferences' : (('fa.cogs',),{}),
|
||||
'inspect' : (('fa.cubes','fa.search'),
|
||||
{'options' : \
|
||||
[{'scale_factor': 0.8,
|
||||
'offset': (0,0),
|
||||
'color': 'gray'},{}]}),
|
||||
'screenshot' : (('fa.camera',),{}),
|
||||
'screenshot-save' : (('fa.save','fa.camera'),
|
||||
{'options' : \
|
||||
[{'scale_factor': 0.8},
|
||||
{'scale_factor': 0.8,
|
||||
'offset': (.2,.2)}]})
|
||||
}
|
||||
|
||||
def icon(name):
|
||||
|
||||
if name in _icons:
|
||||
return _icons[name]
|
||||
|
||||
args,kwargs = _icons_specs[name]
|
||||
|
||||
return qta.icon(*args,**kwargs)
|
||||
9116
cq_editor/icons_res.py
Normal file
341
cq_editor/main_window.py
Normal file
@@ -0,0 +1,341 @@
|
||||
import sys
|
||||
|
||||
from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction)
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
from .widgets.editor import Editor
|
||||
from .widgets.viewer import OCCViewer
|
||||
from .widgets.console import ConsoleWidget
|
||||
from .widgets.object_tree import ObjectTree
|
||||
from .widgets.traceback_viewer import TracebackPane
|
||||
from .widgets.debugger import Debugger, LocalsView
|
||||
from .widgets.cq_object_inspector import CQObjectInspector
|
||||
from .widgets.log import LogViewer
|
||||
|
||||
from . import __version__
|
||||
from .utils import dock, add_actions, open_url, about_dialog, check_gtihub_for_updates, confirm
|
||||
from .mixins import MainMixin
|
||||
from .icons import icon
|
||||
from .preferences import PreferencesWidget
|
||||
|
||||
|
||||
class MainWindow(QMainWindow,MainMixin):
|
||||
|
||||
name = 'CQ-Editor'
|
||||
org = 'CadQuery'
|
||||
|
||||
def __init__(self,parent=None, filename=None):
|
||||
|
||||
super(MainWindow,self).__init__(parent)
|
||||
MainMixin.__init__(self)
|
||||
|
||||
self.setWindowIcon(icon('app'))
|
||||
|
||||
self.viewer = OCCViewer(self)
|
||||
self.setCentralWidget(self.viewer.canvas)
|
||||
|
||||
self.prepare_panes()
|
||||
self.registerComponent('viewer',self.viewer)
|
||||
self.prepare_toolbar()
|
||||
self.prepare_menubar()
|
||||
|
||||
self.prepare_statusbar()
|
||||
self.prepare_actions()
|
||||
|
||||
self.components['object_tree'].addLines()
|
||||
|
||||
self.prepare_console()
|
||||
|
||||
self.fill_dummy()
|
||||
|
||||
self.setup_logging()
|
||||
|
||||
self.restorePreferences()
|
||||
self.restoreWindow()
|
||||
|
||||
if filename:
|
||||
self.components['editor'].load_from_file(filename)
|
||||
|
||||
self.restoreComponentState()
|
||||
|
||||
def closeEvent(self,event):
|
||||
|
||||
self.saveWindow()
|
||||
self.savePreferences()
|
||||
self.saveComponentState()
|
||||
|
||||
if self.components['editor'].document().isModified():
|
||||
|
||||
rv = confirm(self, 'Confirm close', 'Close without saving?')
|
||||
|
||||
if rv:
|
||||
event.accept()
|
||||
super(MainWindow,self).closeEvent(event)
|
||||
else:
|
||||
event.ignore()
|
||||
else:
|
||||
super(MainWindow,self).closeEvent(event)
|
||||
|
||||
def prepare_panes(self):
|
||||
|
||||
self.registerComponent('editor',
|
||||
Editor(self),
|
||||
lambda c : dock(c,
|
||||
'Editor',
|
||||
self,
|
||||
defaultArea='left'))
|
||||
|
||||
self.registerComponent('object_tree',
|
||||
ObjectTree(self),
|
||||
lambda c: dock(c,
|
||||
'Objects',
|
||||
self,
|
||||
defaultArea='right'))
|
||||
|
||||
self.registerComponent('console',
|
||||
ConsoleWidget(self),
|
||||
lambda c: dock(c,
|
||||
'Console',
|
||||
self,
|
||||
defaultArea='bottom'))
|
||||
|
||||
self.registerComponent('traceback_viewer',
|
||||
TracebackPane(self),
|
||||
lambda c: dock(c,
|
||||
'Current traceback',
|
||||
self,
|
||||
defaultArea='bottom'))
|
||||
|
||||
self.registerComponent('debugger',Debugger(self))
|
||||
|
||||
self.registerComponent('variables_viewer',LocalsView(self),
|
||||
lambda c: dock(c,
|
||||
'Variables',
|
||||
self,
|
||||
defaultArea='right'))
|
||||
|
||||
self.registerComponent('cq_object_inspector',
|
||||
CQObjectInspector(self),
|
||||
lambda c: dock(c,
|
||||
'CQ object inspector',
|
||||
self,
|
||||
defaultArea='right'))
|
||||
self.registerComponent('log',
|
||||
LogViewer(self),
|
||||
lambda c: dock(c,
|
||||
'Log viewer',
|
||||
self,
|
||||
defaultArea='bottom'))
|
||||
|
||||
for d in self.docks.values():
|
||||
d.show()
|
||||
|
||||
def prepare_menubar(self):
|
||||
|
||||
menu = self.menuBar()
|
||||
|
||||
menu_file = menu.addMenu('&File')
|
||||
menu_edit = menu.addMenu('&Edit')
|
||||
menu_tools = menu.addMenu('&Tools')
|
||||
menu_run = menu.addMenu('&Run')
|
||||
menu_view = menu.addMenu('&View')
|
||||
menu_help = menu.addMenu('&Help')
|
||||
|
||||
#per component menu elements
|
||||
menus = {'File' : menu_file,
|
||||
'Edit' : menu_edit,
|
||||
'Run' : menu_run,
|
||||
'Tools': menu_tools,
|
||||
'View' : menu_view,
|
||||
'Help' : menu_help}
|
||||
|
||||
for comp in self.components.values():
|
||||
self.prepare_menubar_component(menus,
|
||||
comp.menuActions())
|
||||
|
||||
#global menu elements
|
||||
menu_view.addSeparator()
|
||||
for d in self.findChildren(QDockWidget):
|
||||
menu_view.addAction(d.toggleViewAction())
|
||||
|
||||
menu_view.addSeparator()
|
||||
for t in self.findChildren(QToolBar):
|
||||
menu_view.addAction(t.toggleViewAction())
|
||||
|
||||
menu_edit.addAction( \
|
||||
QAction(icon('preferences'),
|
||||
'Preferences',
|
||||
self,triggered=self.edit_preferences))
|
||||
|
||||
menu_help.addAction( \
|
||||
QAction(icon('help'),
|
||||
'Documentation',
|
||||
self,triggered=self.documentation))
|
||||
|
||||
menu_help.addAction( \
|
||||
QAction('CQ documentation',
|
||||
self,triggered=self.cq_documentation))
|
||||
|
||||
menu_help.addAction( \
|
||||
QAction(icon('about'),
|
||||
'About',
|
||||
self,triggered=self.about))
|
||||
|
||||
menu_help.addAction( \
|
||||
QAction('Check for CadQuery updates',
|
||||
self,triggered=self.check_for_cq_updates))
|
||||
|
||||
def prepare_menubar_component(self,menus,comp_menu_dict):
|
||||
|
||||
for name,action in comp_menu_dict.items():
|
||||
menus[name].addActions(action)
|
||||
|
||||
def prepare_toolbar(self):
|
||||
|
||||
self.toolbar = QToolBar('Main toolbar',self,objectName='Main toolbar')
|
||||
|
||||
for c in self.components.values():
|
||||
add_actions(self.toolbar,c.toolbarActions())
|
||||
|
||||
self.addToolBar(self.toolbar)
|
||||
|
||||
def prepare_statusbar(self):
|
||||
|
||||
self.status_label = QLabel('',parent=self)
|
||||
self.statusBar().insertPermanentWidget(0, self.status_label)
|
||||
|
||||
def prepare_actions(self):
|
||||
|
||||
self.components['debugger'].sigRendered\
|
||||
.connect(self.components['object_tree'].addObjects)
|
||||
self.components['debugger'].sigTraceback\
|
||||
.connect(self.components['traceback_viewer'].addTraceback)
|
||||
self.components['debugger'].sigLocals\
|
||||
.connect(self.components['variables_viewer'].update_frame)
|
||||
self.components['debugger'].sigLocals\
|
||||
.connect(self.components['console'].push_vars)
|
||||
|
||||
self.components['object_tree'].sigObjectsAdded[list]\
|
||||
.connect(self.components['viewer'].display_many)
|
||||
self.components['object_tree'].sigObjectsAdded[list,bool]\
|
||||
.connect(self.components['viewer'].display_many)
|
||||
self.components['object_tree'].sigItemChanged.\
|
||||
connect(self.components['viewer'].update_item)
|
||||
self.components['object_tree'].sigObjectsRemoved\
|
||||
.connect(self.components['viewer'].remove_items)
|
||||
self.components['object_tree'].sigCQObjectSelected\
|
||||
.connect(self.components['cq_object_inspector'].setObject)
|
||||
self.components['object_tree'].sigObjectPropertiesChanged\
|
||||
.connect(self.components['viewer'].redraw)
|
||||
self.components['object_tree'].sigAISObjectsSelected\
|
||||
.connect(self.components['viewer'].set_selected)
|
||||
|
||||
self.components['viewer'].sigObjectSelected\
|
||||
.connect(self.components['object_tree'].handleGraphicalSelection)
|
||||
|
||||
self.components['traceback_viewer'].sigHighlightLine\
|
||||
.connect(self.components['editor'].go_to_line)
|
||||
|
||||
self.components['cq_object_inspector'].sigDisplayObjects\
|
||||
.connect(self.components['viewer'].display_many)
|
||||
self.components['cq_object_inspector'].sigRemoveObjects\
|
||||
.connect(self.components['viewer'].remove_items)
|
||||
self.components['cq_object_inspector'].sigShowPlane\
|
||||
.connect(self.components['viewer'].toggle_grid)
|
||||
self.components['cq_object_inspector'].sigShowPlane[bool,float]\
|
||||
.connect(self.components['viewer'].toggle_grid)
|
||||
self.components['cq_object_inspector'].sigChangePlane\
|
||||
.connect(self.components['viewer'].set_grid_orientation)
|
||||
|
||||
self.components['debugger'].sigLocalsChanged\
|
||||
.connect(self.components['variables_viewer'].update_frame)
|
||||
self.components['debugger'].sigLineChanged\
|
||||
.connect(self.components['editor'].go_to_line)
|
||||
self.components['debugger'].sigDebugging\
|
||||
.connect(self.components['object_tree'].stashObjects)
|
||||
self.components['debugger'].sigCQChanged\
|
||||
.connect(self.components['object_tree'].addObjects)
|
||||
self.components['debugger'].sigTraceback\
|
||||
.connect(self.components['traceback_viewer'].addTraceback)
|
||||
|
||||
# trigger re-render when file is modified externally or saved
|
||||
self.components['editor'].triggerRerender \
|
||||
.connect(self.components['debugger'].render)
|
||||
self.components['editor'].sigFilenameChanged\
|
||||
.connect(self.handle_filename_change)
|
||||
|
||||
def prepare_console(self):
|
||||
|
||||
console = self.components['console']
|
||||
obj_tree = self.components['object_tree']
|
||||
|
||||
#application related items
|
||||
console.push_vars({'self' : self})
|
||||
|
||||
#CQ related items
|
||||
console.push_vars({'show' : obj_tree.addObject,
|
||||
'show_object' : obj_tree.addObject,
|
||||
'cq' : cq})
|
||||
|
||||
def fill_dummy(self):
|
||||
|
||||
self.components['editor']\
|
||||
.set_text('import cadquery as cq\nresult = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125)')
|
||||
|
||||
def setup_logging(self):
|
||||
|
||||
from logbook.compat import redirect_logging
|
||||
from logbook import INFO, Logger
|
||||
|
||||
redirect_logging()
|
||||
self.components['log'].handler.level = INFO
|
||||
self.components['log'].handler.push_application()
|
||||
|
||||
self._logger = Logger(self.name)
|
||||
|
||||
def handle_exception(exc_type, exc_value, exc_traceback):
|
||||
|
||||
if issubclass(exc_type, KeyboardInterrupt):
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
return
|
||||
|
||||
self._logger.error("Uncaught exception occurred",
|
||||
exc_info=(exc_type, exc_value, exc_traceback))
|
||||
|
||||
sys.excepthook = handle_exception
|
||||
|
||||
|
||||
def edit_preferences(self):
|
||||
|
||||
prefs = PreferencesWidget(self,self.components)
|
||||
prefs.exec_()
|
||||
|
||||
def about(self):
|
||||
|
||||
about_dialog(
|
||||
self,
|
||||
f'About CQ-editor',
|
||||
f'PyQt GUI for CadQuery.\nVersion: {__version__}.\nSource Code: https://github.com/CadQuery/CQ-editor',
|
||||
)
|
||||
|
||||
def check_for_cq_updates(self):
|
||||
|
||||
check_gtihub_for_updates(self,cq)
|
||||
|
||||
def documentation(self):
|
||||
|
||||
open_url('https://github.com/CadQuery')
|
||||
|
||||
def cq_documentation(self):
|
||||
|
||||
open_url('https://cadquery.readthedocs.io/en/latest/')
|
||||
|
||||
def handle_filename_change(self, fname):
|
||||
|
||||
new_title = fname if fname else "*"
|
||||
self.setWindowTitle(f"{self.name}: {new_title}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
124
cq_editor/mixins.py
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Wed May 23 22:02:30 2018
|
||||
|
||||
@author: adam
|
||||
"""
|
||||
|
||||
from functools import reduce
|
||||
from operator import add
|
||||
from logbook import Logger
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QSettings
|
||||
|
||||
class MainMixin(object):
|
||||
|
||||
name = 'Main'
|
||||
org = 'Unknown'
|
||||
|
||||
components = {}
|
||||
docks = {}
|
||||
preferences = None
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.settings = QSettings(self.org,self.name)
|
||||
|
||||
def registerComponent(self,name,component,dock=None):
|
||||
|
||||
self.components[name] = component
|
||||
|
||||
if dock:
|
||||
self.docks[name] = dock(component)
|
||||
|
||||
def saveWindow(self):
|
||||
|
||||
self.settings.setValue('geometry',self.saveGeometry())
|
||||
self.settings.setValue('windowState',self.saveState())
|
||||
|
||||
def restoreWindow(self):
|
||||
|
||||
if self.settings.value('geometry'):
|
||||
self.restoreGeometry(self.settings.value('geometry'))
|
||||
if self.settings.value('windowState'):
|
||||
self.restoreState(self.settings.value('windowState'))
|
||||
|
||||
def savePreferences(self):
|
||||
|
||||
settings = self.settings
|
||||
|
||||
if self.preferences:
|
||||
settings.setValue('General',self.preferences.saveState())
|
||||
|
||||
for comp in (c for c in self.components.values() if c.preferences):
|
||||
settings.setValue(comp.name,comp.preferences.saveState())
|
||||
|
||||
def restorePreferences(self):
|
||||
|
||||
settings = self.settings
|
||||
|
||||
if self.preferences and settings.value('General'):
|
||||
self.preferences.restoreState(settings.value('General'),
|
||||
removeChildren=False)
|
||||
|
||||
for comp in (c for c in self.components.values() if c.preferences):
|
||||
if settings.value(comp.name):
|
||||
comp.preferences.restoreState(settings.value(comp.name),
|
||||
removeChildren=False)
|
||||
|
||||
def saveComponentState(self):
|
||||
|
||||
settings = self.settings
|
||||
|
||||
for comp in self.components.values():
|
||||
comp.saveComponentState(settings)
|
||||
|
||||
def restoreComponentState(self):
|
||||
|
||||
settings = self.settings
|
||||
|
||||
for comp in self.components.values():
|
||||
comp.restoreComponentState(settings)
|
||||
|
||||
|
||||
class ComponentMixin(object):
|
||||
|
||||
|
||||
name = 'Component'
|
||||
preferences = None
|
||||
|
||||
_actions = {}
|
||||
|
||||
|
||||
def __init__(self):
|
||||
|
||||
if self.preferences:
|
||||
self.preferences.sigTreeStateChanged.\
|
||||
connect(self.updatePreferences)
|
||||
|
||||
self._logger = Logger(self.name)
|
||||
|
||||
def menuActions(self):
|
||||
|
||||
return self._actions
|
||||
|
||||
def toolbarActions(self):
|
||||
|
||||
if len(self._actions) > 0:
|
||||
return reduce(add,[a for a in self._actions.values()])
|
||||
else:
|
||||
return []
|
||||
|
||||
@pyqtSlot(object,object)
|
||||
def updatePreferences(self,*args):
|
||||
|
||||
pass
|
||||
|
||||
def saveComponentState(self,store):
|
||||
|
||||
pass
|
||||
|
||||
def restoreComponentState(self,store):
|
||||
|
||||
pass
|
||||
62
cq_editor/preferences.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem,
|
||||
QStackedWidget, QDialog)
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from pyqtgraph.parametertree import ParameterTree
|
||||
|
||||
from .utils import splitter, layout
|
||||
|
||||
|
||||
class PreferencesTreeItem(QTreeWidgetItem):
|
||||
|
||||
def __init__(self,name,widget,):
|
||||
|
||||
super(PreferencesTreeItem,self).__init__(name)
|
||||
self.widget = widget
|
||||
|
||||
class PreferencesWidget(QDialog):
|
||||
|
||||
def __init__(self,parent,components):
|
||||
|
||||
super(PreferencesWidget,self).__init__(
|
||||
parent,
|
||||
Qt.Window | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint,
|
||||
windowTitle='Preferences')
|
||||
|
||||
self.stacked = QStackedWidget(self)
|
||||
self.preferences_tree = QTreeWidget(self,
|
||||
headerHidden=True,
|
||||
itemsExpandable=False,
|
||||
rootIsDecorated=False,
|
||||
columnCount=1)
|
||||
|
||||
self.root = self.preferences_tree.invisibleRootItem()
|
||||
|
||||
self.add('General',
|
||||
parent)
|
||||
|
||||
for v in parent.components.values():
|
||||
self.add(v.name,v)
|
||||
|
||||
self.splitter = splitter((self.preferences_tree,self.stacked),(2,5))
|
||||
layout(self,(self.splitter,),self)
|
||||
|
||||
self.preferences_tree.currentItemChanged.connect(self.handleSelection)
|
||||
|
||||
def add(self,name,component):
|
||||
|
||||
if component.preferences:
|
||||
widget = ParameterTree()
|
||||
widget.setHeaderHidden(True)
|
||||
widget.setParameters(component.preferences,showTop=False)
|
||||
self.root.addChild(PreferencesTreeItem((name,),
|
||||
widget))
|
||||
|
||||
self.stacked.addWidget(widget)
|
||||
|
||||
@pyqtSlot(QTreeWidgetItem,QTreeWidgetItem)
|
||||
def handleSelection(self,item,*args):
|
||||
|
||||
if item:
|
||||
self.stacked.setCurrentWidget(item.widget)
|
||||
|
||||
134
cq_editor/utils.py
Normal file
@@ -0,0 +1,134 @@
|
||||
import requests
|
||||
|
||||
from pkg_resources import parse_version
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox
|
||||
|
||||
DOCK_POSITIONS = {'right' : QtCore.Qt.RightDockWidgetArea,
|
||||
'left' : QtCore.Qt.LeftDockWidgetArea,
|
||||
'top' : QtCore.Qt.TopDockWidgetArea,
|
||||
'bottom' : QtCore.Qt.BottomDockWidgetArea}
|
||||
|
||||
def layout(parent,items,
|
||||
top_widget = None,
|
||||
layout_type = QtWidgets.QVBoxLayout,
|
||||
margin = 2,
|
||||
spacing = 0):
|
||||
|
||||
if not top_widget:
|
||||
top_widget = QtWidgets.QWidget(parent)
|
||||
top_widget_was_none = True
|
||||
else:
|
||||
top_widget_was_none = False
|
||||
layout = layout_type(top_widget)
|
||||
top_widget.setLayout(layout)
|
||||
|
||||
for item in items: layout.addWidget(item)
|
||||
|
||||
layout.setSpacing(spacing)
|
||||
layout.setContentsMargins(margin,margin,margin,margin)
|
||||
|
||||
if top_widget_was_none:
|
||||
return top_widget
|
||||
else:
|
||||
return layout
|
||||
|
||||
def splitter(items,
|
||||
stretch_factors = None,
|
||||
orientation=QtCore.Qt.Horizontal):
|
||||
|
||||
sp = QtWidgets.QSplitter(orientation)
|
||||
|
||||
for item in items: sp.addWidget(item)
|
||||
|
||||
if stretch_factors:
|
||||
for i,s in enumerate(stretch_factors):
|
||||
sp.setStretchFactor(i,s)
|
||||
|
||||
|
||||
return sp
|
||||
|
||||
def dock(widget,
|
||||
title,
|
||||
parent,
|
||||
allowedAreas = QtCore.Qt.AllDockWidgetAreas,
|
||||
defaultArea = 'right',
|
||||
name=None,
|
||||
icon = None):
|
||||
|
||||
dock = QtWidgets.QDockWidget(title,parent,objectName=title)
|
||||
|
||||
if name: dock.setObjectName(name)
|
||||
if icon: dock.toggleViewAction().setIcon(icon)
|
||||
|
||||
dock.setAllowedAreas(allowedAreas)
|
||||
dock.setWidget(widget)
|
||||
action = dock.toggleViewAction()
|
||||
action.setText(title)
|
||||
|
||||
dock.setFeatures(QtWidgets.QDockWidget.DockWidgetFeatures(\
|
||||
QtWidgets.QDockWidget.AllDockWidgetFeatures))
|
||||
|
||||
parent.addDockWidget(DOCK_POSITIONS[defaultArea],
|
||||
dock)
|
||||
|
||||
return dock
|
||||
|
||||
def add_actions(menu,actions):
|
||||
|
||||
if len(actions) > 0:
|
||||
menu.addActions(actions)
|
||||
menu.addSeparator()
|
||||
|
||||
def open_url(url):
|
||||
|
||||
QDesktopServices.openUrl(QUrl(url))
|
||||
|
||||
def about_dialog(parent,title,text):
|
||||
|
||||
QtWidgets.QMessageBox.about(parent,title,text)
|
||||
|
||||
def get_save_filename(suffix):
|
||||
|
||||
rv,_ = QFileDialog.getSaveFileName(filter='*.{}'.format(suffix))
|
||||
if rv != '' and not rv.endswith(suffix): rv += '.'+suffix
|
||||
|
||||
return rv
|
||||
|
||||
def get_open_filename(suffix, curr_dir):
|
||||
|
||||
rv,_ = QFileDialog.getOpenFileName(directory=curr_dir, filter='*.{}'.format(suffix))
|
||||
if rv != '' and not rv.endswith(suffix): rv += '.'+suffix
|
||||
|
||||
return rv
|
||||
|
||||
def check_gtihub_for_updates(parent,
|
||||
mod,
|
||||
github_org='cadquery',
|
||||
github_proj='cadquery'):
|
||||
|
||||
url = f'https://api.github.com/repos/{github_org}/{github_proj}/releases'
|
||||
resp = requests.get(url).json()
|
||||
|
||||
newer = [el['tag_name'] for el in resp if not el['draft'] and \
|
||||
parse_version(el['tag_name']) > parse_version(mod.__version__)]
|
||||
|
||||
if newer:
|
||||
title='Updates available'
|
||||
text=f'There are newer versions of {github_proj} ' \
|
||||
f'available on github:\n' + '\n'.join(newer)
|
||||
|
||||
else:
|
||||
title='No updates available'
|
||||
text=f'You are already using the latest version of {github_proj}'
|
||||
|
||||
QtWidgets.QMessageBox.about(parent,title,text)
|
||||
|
||||
def confirm(parent,title,msg):
|
||||
|
||||
rv = QMessageBox.question(parent, title, msg, QMessageBox.Yes, QMessageBox.No)
|
||||
|
||||
return True if rv == QMessageBox.Yes else False
|
||||
0
cq_editor/widgets/__init__.py
Normal file
81
cq_editor/widgets/console.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
||||
from qtconsole.inprocess import QtInProcessKernelManager
|
||||
|
||||
from ..mixins import ComponentMixin
|
||||
|
||||
class ConsoleWidget(RichJupyterWidget,ComponentMixin):
|
||||
|
||||
name = 'Console'
|
||||
|
||||
def __init__(self, customBanner=None, namespace=dict(), *args, **kwargs):
|
||||
super(ConsoleWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
# if not customBanner is None:
|
||||
# self.banner = customBanner
|
||||
|
||||
self.font_size = 6
|
||||
self.kernel_manager = kernel_manager = QtInProcessKernelManager()
|
||||
kernel_manager.start_kernel(show_banner=False)
|
||||
kernel_manager.kernel.gui = 'qt'
|
||||
kernel_manager.kernel.shell.banner1 = ""
|
||||
|
||||
self.kernel_client = kernel_client = self._kernel_manager.client()
|
||||
kernel_client.start_channels()
|
||||
|
||||
def stop():
|
||||
kernel_client.stop_channels()
|
||||
kernel_manager.shutdown_kernel()
|
||||
QApplication.instance().exit()
|
||||
|
||||
self.exit_requested.connect(stop)
|
||||
|
||||
self.clear()
|
||||
|
||||
self.push_vars(namespace)
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def push_vars(self, variableDict):
|
||||
"""
|
||||
Given a dictionary containing name / value pairs, push those variables
|
||||
to the Jupyter console widget
|
||||
"""
|
||||
self.kernel_manager.kernel.shell.push(variableDict)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clears the terminal
|
||||
"""
|
||||
self._control.clear()
|
||||
|
||||
|
||||
def print_text(self, text):
|
||||
"""
|
||||
Prints some plain text to the console
|
||||
"""
|
||||
self._append_plain_text(text)
|
||||
|
||||
def execute_command(self, command):
|
||||
"""
|
||||
Execute a command in the frame of the console widget
|
||||
"""
|
||||
self._execute(command, False)
|
||||
|
||||
def _banner_default(self):
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
console = ConsoleWidget(customBanner='IPython console test')
|
||||
console.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
129
cq_editor/widgets/cq_object_inspector.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAction
|
||||
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal
|
||||
|
||||
from OCP.AIS import AIS_ColoredShape
|
||||
from OCP.gp import gp_Ax3
|
||||
|
||||
from cadquery import Vector
|
||||
|
||||
from ..mixins import ComponentMixin
|
||||
from ..icons import icon
|
||||
|
||||
|
||||
|
||||
class CQChildItem(QTreeWidgetItem):
|
||||
|
||||
def __init__(self,cq_item,**kwargs):
|
||||
|
||||
super(CQChildItem,self).\
|
||||
__init__([type(cq_item).__name__,str(cq_item)],**kwargs)
|
||||
|
||||
self.cq_item = cq_item
|
||||
|
||||
class CQStackItem(QTreeWidgetItem):
|
||||
|
||||
def __init__(self,name,workplane=None,**kwargs):
|
||||
|
||||
super(CQStackItem,self).__init__([name,''],**kwargs)
|
||||
|
||||
self.workplane = workplane
|
||||
|
||||
|
||||
class CQObjectInspector(QTreeWidget,ComponentMixin):
|
||||
|
||||
name = 'CQ Object Inspector'
|
||||
|
||||
sigRemoveObjects = pyqtSignal(list)
|
||||
sigDisplayObjects = pyqtSignal(list,bool)
|
||||
sigShowPlane = pyqtSignal([bool],[bool,float])
|
||||
sigChangePlane = pyqtSignal(gp_Ax3)
|
||||
|
||||
def __init__(self,parent):
|
||||
|
||||
super(CQObjectInspector,self).__init__(parent)
|
||||
self.setHeaderHidden(False)
|
||||
self.setRootIsDecorated(True)
|
||||
self.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
self.setColumnCount(2)
|
||||
self.setHeaderLabels(['Type','Value'])
|
||||
|
||||
self.root = self.invisibleRootItem()
|
||||
self.inspected_items = []
|
||||
|
||||
self._toolbar_actions = \
|
||||
[QAction(icon('inspect'),'Inspect CQ object',self,\
|
||||
toggled=self.inspect,checkable=True)]
|
||||
|
||||
self.addActions(self._toolbar_actions)
|
||||
|
||||
def menuActions(self):
|
||||
|
||||
return {'Tools' : self._toolbar_actions}
|
||||
|
||||
def toolbarActions(self):
|
||||
|
||||
return self._toolbar_actions
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def inspect(self,value):
|
||||
|
||||
if value:
|
||||
self.itemSelectionChanged.connect(self.handleSelection)
|
||||
self.itemSelectionChanged.emit()
|
||||
else:
|
||||
self.itemSelectionChanged.disconnect(self.handleSelection)
|
||||
self.sigRemoveObjects.emit(self.inspected_items)
|
||||
self.sigShowPlane.emit(False)
|
||||
|
||||
@pyqtSlot()
|
||||
def handleSelection(self):
|
||||
|
||||
inspected_items = self.inspected_items
|
||||
self.sigRemoveObjects.emit(inspected_items)
|
||||
inspected_items.clear()
|
||||
|
||||
items = self.selectedItems()
|
||||
if len(items) == 0:
|
||||
return
|
||||
|
||||
item = items[-1]
|
||||
if type(item) is CQStackItem:
|
||||
cq_plane = item.workplane.plane
|
||||
dim = item.workplane.largestDimension()
|
||||
plane = gp_Ax3(cq_plane.origin.toPnt(),
|
||||
cq_plane.zDir.toDir(),
|
||||
cq_plane.xDir.toDir())
|
||||
self.sigChangePlane.emit(plane)
|
||||
self.sigShowPlane[bool,float].emit(True,dim)
|
||||
|
||||
for child in (item.child(i) for i in range(item.childCount())):
|
||||
obj = child.cq_item
|
||||
if hasattr(obj,'wrapped') and type(obj) != Vector:
|
||||
ais = AIS_ColoredShape(obj.wrapped)
|
||||
inspected_items.append(ais)
|
||||
|
||||
else:
|
||||
self.sigShowPlane.emit(False)
|
||||
obj = item.cq_item
|
||||
if hasattr(obj,'wrapped') and type(obj) != Vector:
|
||||
ais = AIS_ColoredShape(obj.wrapped)
|
||||
inspected_items.append(ais)
|
||||
|
||||
self.sigDisplayObjects.emit(inspected_items,False)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def setObject(self,cq_obj):
|
||||
|
||||
self.root.takeChildren()
|
||||
|
||||
# iterate through parent objects if they exist
|
||||
while getattr(cq_obj, 'parent', None):
|
||||
current_frame = CQStackItem(str(cq_obj.plane.origin),workplane=cq_obj)
|
||||
self.root.addChild(current_frame)
|
||||
|
||||
for obj in cq_obj.objects:
|
||||
current_frame.addChild(CQChildItem(obj))
|
||||
|
||||
cq_obj = cq_obj.parent
|
||||
|
||||
|
||||
365
cq_editor/widgets/debugger.py
Normal file
@@ -0,0 +1,365 @@
|
||||
import sys
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from enum import Enum, auto
|
||||
from types import SimpleNamespace, FrameType, ModuleType
|
||||
from typing import List
|
||||
|
||||
import cadquery as cq
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5.QtCore import Qt, QObject, pyqtSlot, pyqtSignal, QEventLoop, QAbstractTableModel
|
||||
from PyQt5.QtWidgets import (QAction,
|
||||
QTableView)
|
||||
from logbook import info
|
||||
from path import Path
|
||||
from pyqtgraph.parametertree import Parameter
|
||||
from spyder.utils.icon_manager import icon
|
||||
|
||||
from ..cq_utils import find_cq_objects, reload_cq
|
||||
from ..mixins import ComponentMixin
|
||||
|
||||
DUMMY_FILE = '<string>'
|
||||
|
||||
|
||||
class DbgState(Enum):
|
||||
|
||||
STEP = auto()
|
||||
CONT = auto()
|
||||
STEP_IN = auto()
|
||||
RETURN = auto()
|
||||
|
||||
class DbgEevent(object):
|
||||
|
||||
LINE = 'line'
|
||||
CALL = 'call'
|
||||
RETURN = 'return'
|
||||
|
||||
class LocalsModel(QAbstractTableModel):
|
||||
|
||||
HEADER = ('Name','Type', 'Value')
|
||||
|
||||
def __init__(self,parent):
|
||||
|
||||
super(LocalsModel,self).__init__(parent)
|
||||
self.frame = None
|
||||
|
||||
def update_frame(self,frame):
|
||||
|
||||
self.frame = \
|
||||
[(k,type(v).__name__, str(v)) for k,v in frame.items() if not k.startswith('_')]
|
||||
|
||||
|
||||
def rowCount(self,parent=QtCore.QModelIndex()):
|
||||
|
||||
if self.frame:
|
||||
return len(self.frame)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def columnCount(self,parent=QtCore.QModelIndex()):
|
||||
|
||||
return 3
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||
return self.HEADER[section]
|
||||
return QAbstractTableModel.headerData(self, section, orientation, role)
|
||||
|
||||
def data(self, index, role):
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
i = index.row()
|
||||
j = index.column()
|
||||
return self.frame[i][j]
|
||||
else:
|
||||
return QtCore.QVariant()
|
||||
|
||||
|
||||
class LocalsView(QTableView,ComponentMixin):
|
||||
|
||||
name = 'Variables'
|
||||
|
||||
def __init__(self,parent):
|
||||
|
||||
super(LocalsView,self).__init__(parent)
|
||||
ComponentMixin.__init__(self)
|
||||
|
||||
header = self.horizontalHeader()
|
||||
header.setStretchLastSection(True)
|
||||
|
||||
vheader = self.verticalHeader()
|
||||
vheader.setVisible(False)
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def update_frame(self,frame):
|
||||
|
||||
model = LocalsModel(self)
|
||||
model.update_frame(frame)
|
||||
|
||||
self.setModel(model)
|
||||
|
||||
class Debugger(QObject,ComponentMixin):
|
||||
|
||||
name = 'Debugger'
|
||||
|
||||
preferences = Parameter.create(name='Preferences',children=[
|
||||
{'name': 'Reload CQ', 'type': 'bool', 'value': False},
|
||||
{'name': 'Add script dir to path','type': 'bool', 'value': True},
|
||||
{'name': 'Change working dir to script dir','type': 'bool', 'value': True},
|
||||
{'name': 'Reload imported modules', 'type': 'bool', 'value': True},
|
||||
])
|
||||
|
||||
|
||||
sigRendered = pyqtSignal(dict)
|
||||
sigLocals = pyqtSignal(dict)
|
||||
sigTraceback = pyqtSignal(object,str)
|
||||
|
||||
sigFrameChanged = pyqtSignal(object)
|
||||
sigLineChanged = pyqtSignal(int)
|
||||
sigLocalsChanged = pyqtSignal(dict)
|
||||
sigCQChanged = pyqtSignal(dict,bool)
|
||||
sigDebugging = pyqtSignal(bool)
|
||||
|
||||
_frames : List[FrameType]
|
||||
|
||||
def __init__(self,parent):
|
||||
|
||||
super(Debugger,self).__init__(parent)
|
||||
ComponentMixin.__init__(self)
|
||||
|
||||
self.inner_event_loop = QEventLoop(self)
|
||||
|
||||
self._actions = \
|
||||
{'Run' : [QAction(icon('run'),
|
||||
'Render',
|
||||
self,
|
||||
shortcut='F5',
|
||||
triggered=self.render),
|
||||
QAction(icon('debug'),
|
||||
'Debug',
|
||||
self,
|
||||
checkable=True,
|
||||
shortcut='ctrl+F5',
|
||||
triggered=self.debug),
|
||||
QAction(icon('arrow-step-over'),
|
||||
'Step',
|
||||
self,
|
||||
shortcut='ctrl+F10',
|
||||
triggered=lambda: self.debug_cmd(DbgState.STEP)),
|
||||
QAction(icon('arrow-step-in'),
|
||||
'Step in',
|
||||
self,
|
||||
shortcut='ctrl+F11',
|
||||
triggered=lambda: self.debug_cmd(DbgState.STEP_IN)),
|
||||
QAction(icon('arrow-continue'),
|
||||
'Continue',
|
||||
self,
|
||||
shortcut='ctrl+F12',
|
||||
triggered=lambda: self.debug_cmd(DbgState.CONT))
|
||||
]}
|
||||
|
||||
self._frames = []
|
||||
|
||||
def get_current_script(self):
|
||||
|
||||
return self.parent().components['editor'].get_text_with_eol()
|
||||
|
||||
def get_breakpoints(self):
|
||||
|
||||
return self.parent().components['editor'].debugger.get_breakpoints()
|
||||
|
||||
def compile_code(self, cq_script):
|
||||
|
||||
try:
|
||||
module = ModuleType('temp')
|
||||
cq_code = compile(cq_script, '<string>', 'exec')
|
||||
return cq_code, module
|
||||
except Exception:
|
||||
self.sigTraceback.emit(sys.exc_info(), cq_script)
|
||||
return None, None
|
||||
|
||||
def _exec(self, code, locals_dict, globals_dict):
|
||||
|
||||
with ExitStack() as stack:
|
||||
fname = self.parent().components['editor'].filename
|
||||
p = Path(fname if fname else '').abspath().dirname()
|
||||
|
||||
if self.preferences['Add script dir to path'] and p.exists():
|
||||
sys.path.insert(0,p)
|
||||
stack.callback(sys.path.remove, p)
|
||||
if self.preferences['Change working dir to script dir'] and p.exists():
|
||||
stack.enter_context(p)
|
||||
if self.preferences['Reload imported modules']:
|
||||
stack.enter_context(module_manager())
|
||||
|
||||
exec(code, locals_dict, globals_dict)
|
||||
|
||||
def _inject_locals(self,module):
|
||||
|
||||
cq_objects = {}
|
||||
|
||||
def _show_object(obj,name=None, options={}):
|
||||
|
||||
if name:
|
||||
cq_objects.update({name : SimpleNamespace(shape=obj,options=options)})
|
||||
else:
|
||||
cq_objects.update({str(id(obj)) : SimpleNamespace(shape=obj,options=options)})
|
||||
|
||||
def _debug(obj,name=None):
|
||||
|
||||
_show_object(obj,name,options=dict(color='red',alpha=0.2))
|
||||
|
||||
module.__dict__['show_object'] = _show_object
|
||||
module.__dict__['debug'] = _debug
|
||||
module.__dict__['log'] = lambda x: info(str(x))
|
||||
module.__dict__['cq'] = cq
|
||||
|
||||
return cq_objects, set(module.__dict__)-{'cq'}
|
||||
|
||||
def _cleanup_locals(self,module,injected_names):
|
||||
|
||||
for name in injected_names: module.__dict__.pop(name)
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def render(self):
|
||||
|
||||
if self.preferences['Reload CQ']:
|
||||
reload_cq()
|
||||
|
||||
cq_script = self.get_current_script()
|
||||
cq_code,module = self.compile_code(cq_script)
|
||||
|
||||
if cq_code is None: return
|
||||
|
||||
cq_objects,injected_names = self._inject_locals(module)
|
||||
|
||||
try:
|
||||
self._exec(cq_code, module.__dict__, module.__dict__)
|
||||
|
||||
#remove the special methods
|
||||
self._cleanup_locals(module,injected_names)
|
||||
|
||||
#collect all CQ objects if no explicit show_object was called
|
||||
if len(cq_objects) == 0:
|
||||
cq_objects = find_cq_objects(module.__dict__)
|
||||
self.sigRendered.emit(cq_objects)
|
||||
self.sigTraceback.emit(None,
|
||||
cq_script)
|
||||
self.sigLocals.emit(module.__dict__)
|
||||
except Exception:
|
||||
exc_info = sys.exc_info()
|
||||
sys.last_traceback = exc_info[-1]
|
||||
self.sigTraceback.emit(exc_info, cq_script)
|
||||
|
||||
@property
|
||||
def breakpoints(self):
|
||||
return [ el[0] for el in self.get_breakpoints()]
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def debug(self,value):
|
||||
|
||||
previous_trace = sys.gettrace()
|
||||
|
||||
if value:
|
||||
self.sigDebugging.emit(True)
|
||||
self.state = DbgState.STEP
|
||||
|
||||
self.script = self.get_current_script()
|
||||
code,module = self.compile_code(self.script)
|
||||
|
||||
if code is None:
|
||||
self.sigDebugging.emit(False)
|
||||
self._actions['Run'][1].setChecked(False)
|
||||
return
|
||||
|
||||
cq_objects,injected_names = self._inject_locals(module)
|
||||
|
||||
#clear possible traceback
|
||||
self.sigTraceback.emit(None,
|
||||
self.script)
|
||||
|
||||
try:
|
||||
sys.settrace(self.trace_callback)
|
||||
exec(code,module.__dict__,module.__dict__)
|
||||
except Exception:
|
||||
exc_info = sys.exc_info()
|
||||
sys.last_traceback = exc_info[-1]
|
||||
self.sigTraceback.emit(exc_info,
|
||||
self.script)
|
||||
finally:
|
||||
sys.settrace(previous_trace)
|
||||
self.sigDebugging.emit(False)
|
||||
self._actions['Run'][1].setChecked(False)
|
||||
|
||||
if len(cq_objects) == 0:
|
||||
cq_objects = find_cq_objects(module.__dict__)
|
||||
self.sigRendered.emit(cq_objects)
|
||||
|
||||
self._cleanup_locals(module,injected_names)
|
||||
self.sigLocals.emit(module.__dict__)
|
||||
|
||||
self._frames = []
|
||||
else:
|
||||
sys.settrace(previous_trace)
|
||||
self.inner_event_loop.exit(0)
|
||||
|
||||
|
||||
def debug_cmd(self,state=DbgState.STEP):
|
||||
|
||||
self.state = state
|
||||
self.inner_event_loop.exit(0)
|
||||
|
||||
|
||||
def trace_callback(self,frame,event,arg):
|
||||
|
||||
filename = frame.f_code.co_filename
|
||||
|
||||
if filename==DUMMY_FILE:
|
||||
if not self._frames:
|
||||
self._frames.append(frame)
|
||||
self.trace_local(frame,event,arg)
|
||||
return self.trace_callback
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
def trace_local(self,frame,event,arg):
|
||||
|
||||
lineno = frame.f_lineno
|
||||
|
||||
if event in (DbgEevent.LINE,):
|
||||
if (self.state in (DbgState.STEP, DbgState.STEP_IN) and frame is self._frames[-1]) \
|
||||
or (lineno in self.breakpoints):
|
||||
|
||||
if lineno in self.breakpoints:
|
||||
self._frames.append(frame)
|
||||
|
||||
self.sigLineChanged.emit(lineno)
|
||||
self.sigFrameChanged.emit(frame)
|
||||
self.sigLocalsChanged.emit(frame.f_locals)
|
||||
self.sigCQChanged.emit(find_cq_objects(frame.f_locals),True)
|
||||
|
||||
self.inner_event_loop.exec_()
|
||||
|
||||
elif event in (DbgEevent.RETURN):
|
||||
self.sigLocalsChanged.emit(frame.f_locals)
|
||||
self._frames.pop()
|
||||
|
||||
elif event == DbgEevent.CALL:
|
||||
func_filename = frame.f_code.co_filename
|
||||
if self.state == DbgState.STEP_IN and func_filename == DUMMY_FILE:
|
||||
self.sigLineChanged.emit(lineno)
|
||||
self.sigFrameChanged.emit(frame)
|
||||
self.state = DbgState.STEP
|
||||
self._frames.append(frame)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def module_manager():
|
||||
""" unloads any modules loaded while the context manager is active """
|
||||
loaded_modules = set(sys.modules.keys())
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
new_modules = set(sys.modules.keys()) - loaded_modules
|
||||
for module_name in new_modules:
|
||||
del sys.modules[module_name]
|
||||
290
cq_editor/widgets/editor.py
Normal file
@@ -0,0 +1,290 @@
|
||||
import os
|
||||
from modulefinder import ModuleFinder
|
||||
|
||||
from spyder.plugins.editor.widgets.codeeditor import CodeEditor
|
||||
from PyQt5.QtCore import pyqtSignal, QFileSystemWatcher, QTimer
|
||||
from PyQt5.QtWidgets import QAction, QFileDialog
|
||||
from PyQt5.QtGui import QFontDatabase
|
||||
from path import Path
|
||||
|
||||
import sys
|
||||
|
||||
from pyqtgraph.parametertree import Parameter
|
||||
|
||||
from ..mixins import ComponentMixin
|
||||
from ..utils import get_save_filename, get_open_filename, confirm
|
||||
|
||||
from ..icons import icon
|
||||
|
||||
class Editor(CodeEditor,ComponentMixin):
|
||||
|
||||
name = 'Code Editor'
|
||||
|
||||
# This signal is emitted whenever the currently-open file changes and
|
||||
# autoreload is enabled.
|
||||
triggerRerender = pyqtSignal(bool)
|
||||
sigFilenameChanged = pyqtSignal(str)
|
||||
|
||||
preferences = Parameter.create(name='Preferences',children=[
|
||||
{'name': 'Font size', 'type': 'int', 'value': 12},
|
||||
{'name': 'Autoreload', 'type': 'bool', 'value': False},
|
||||
{'name': 'Autoreload delay', 'type': 'int', 'value': 50},
|
||||
{'name': 'Autoreload: watch imported modules', 'type': 'bool', 'value': False},
|
||||
{'name': 'Line wrap', 'type': 'bool', 'value': False},
|
||||
{'name': 'Color scheme', 'type': 'list',
|
||||
'values': ['Spyder','Monokai','Zenburn'], 'value': 'Spyder'}])
|
||||
|
||||
EXTENSIONS = 'py'
|
||||
|
||||
def __init__(self,parent=None):
|
||||
|
||||
self._watched_file = None
|
||||
|
||||
super(Editor,self).__init__(parent)
|
||||
ComponentMixin.__init__(self)
|
||||
|
||||
self.setup_editor(linenumbers=True,
|
||||
markers=True,
|
||||
edge_line=False,
|
||||
tab_mode=False,
|
||||
show_blanks=True,
|
||||
font=QFontDatabase.systemFont(QFontDatabase.FixedFont),
|
||||
language='Python',
|
||||
filename='')
|
||||
|
||||
self._actions = \
|
||||
{'File' : [QAction(icon('new'),
|
||||
'New',
|
||||
self,
|
||||
shortcut='ctrl+N',
|
||||
triggered=self.new),
|
||||
QAction(icon('open'),
|
||||
'Open',
|
||||
self,
|
||||
shortcut='ctrl+O',
|
||||
triggered=self.open),
|
||||
QAction(icon('save'),
|
||||
'Save',
|
||||
self,
|
||||
shortcut='ctrl+S',
|
||||
triggered=self.save),
|
||||
QAction(icon('save_as'),
|
||||
'Save as',
|
||||
self,
|
||||
shortcut='ctrl+shift+S',
|
||||
triggered=self.save_as),
|
||||
QAction(icon('autoreload'),
|
||||
'Automatic reload and preview',
|
||||
self,triggered=self.autoreload,
|
||||
checkable=True,
|
||||
checked=False,
|
||||
objectName='autoreload'),
|
||||
]}
|
||||
|
||||
for a in self._actions.values():
|
||||
self.addActions(a)
|
||||
|
||||
|
||||
self._fixContextMenu()
|
||||
|
||||
# autoreload support
|
||||
self._file_watcher = QFileSystemWatcher(self)
|
||||
# we wait for 50ms after a file change for the file to be written completely
|
||||
self._file_watch_timer = QTimer(self)
|
||||
self._file_watch_timer.setInterval(self.preferences['Autoreload delay'])
|
||||
self._file_watch_timer.setSingleShot(True)
|
||||
self._file_watcher.fileChanged.connect(
|
||||
lambda val: self._file_watch_timer.start())
|
||||
self._file_watch_timer.timeout.connect(self._file_changed)
|
||||
|
||||
self.updatePreferences()
|
||||
|
||||
def _fixContextMenu(self):
|
||||
|
||||
menu = self.menu
|
||||
|
||||
menu.removeAction(self.run_cell_action)
|
||||
menu.removeAction(self.run_cell_and_advance_action)
|
||||
menu.removeAction(self.run_selection_action)
|
||||
menu.removeAction(self.re_run_last_cell_action)
|
||||
|
||||
def updatePreferences(self,*args):
|
||||
|
||||
self.set_color_scheme(self.preferences['Color scheme'])
|
||||
|
||||
font = self.font()
|
||||
font.setPointSize(self.preferences['Font size'])
|
||||
self.set_font(font)
|
||||
|
||||
self.findChild(QAction, 'autoreload') \
|
||||
.setChecked(self.preferences['Autoreload'])
|
||||
|
||||
self._file_watch_timer.setInterval(self.preferences['Autoreload delay'])
|
||||
|
||||
self.toggle_wrap_mode(self.preferences['Line wrap'])
|
||||
|
||||
self._clear_watched_paths()
|
||||
self._watch_paths()
|
||||
|
||||
def confirm_discard(self):
|
||||
|
||||
if self.modified:
|
||||
rv = confirm(self,'Please confirm','Current document is not saved - do you want to continue?')
|
||||
else:
|
||||
rv = True
|
||||
|
||||
return rv
|
||||
|
||||
def new(self):
|
||||
|
||||
if not self.confirm_discard(): return
|
||||
|
||||
self.set_text('')
|
||||
self.filename = ''
|
||||
self.reset_modified()
|
||||
|
||||
def open(self):
|
||||
|
||||
if not self.confirm_discard(): return
|
||||
|
||||
curr_dir = Path(self.filename).abspath().dirname()
|
||||
fname = get_open_filename(self.EXTENSIONS, curr_dir)
|
||||
if fname != '':
|
||||
self.load_from_file(fname)
|
||||
|
||||
def load_from_file(self,fname):
|
||||
|
||||
self.set_text_from_file(fname)
|
||||
self.filename = fname
|
||||
self.reset_modified()
|
||||
|
||||
def save(self):
|
||||
|
||||
if self._filename != '':
|
||||
|
||||
if self.preferences['Autoreload']:
|
||||
self._file_watcher.blockSignals(True)
|
||||
self._file_watch_timer.stop()
|
||||
|
||||
with open(self._filename, 'w') as f:
|
||||
f.write(self.toPlainText())
|
||||
|
||||
if self.preferences['Autoreload']:
|
||||
self._file_watcher.blockSignals(False)
|
||||
self.triggerRerender.emit(True)
|
||||
|
||||
self.reset_modified()
|
||||
|
||||
else:
|
||||
self.save_as()
|
||||
|
||||
def save_as(self):
|
||||
|
||||
fname = get_save_filename(self.EXTENSIONS)
|
||||
if fname != '':
|
||||
with open(fname,'w') as f:
|
||||
f.write(self.toPlainText())
|
||||
self.filename = fname
|
||||
|
||||
self.reset_modified()
|
||||
|
||||
def _update_filewatcher(self):
|
||||
if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']):
|
||||
self._clear_watched_paths()
|
||||
self._watched_file = None
|
||||
if self.preferences['Autoreload'] and self.filename and self.filename != self._watched_file:
|
||||
self._watched_file = self._filename
|
||||
self._watch_paths()
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
return self._filename
|
||||
|
||||
@filename.setter
|
||||
def filename(self, fname):
|
||||
self._filename = fname
|
||||
self._update_filewatcher()
|
||||
self.sigFilenameChanged.emit(fname)
|
||||
|
||||
def _clear_watched_paths(self):
|
||||
paths = self._file_watcher.files()
|
||||
if paths:
|
||||
self._file_watcher.removePaths(paths)
|
||||
|
||||
def _watch_paths(self):
|
||||
if Path(self._filename).exists():
|
||||
self._file_watcher.addPath(self._filename)
|
||||
if self.preferences['Autoreload: watch imported modules']:
|
||||
module_paths = self.get_imported_module_paths(self._filename)
|
||||
if module_paths:
|
||||
self._file_watcher.addPaths(module_paths)
|
||||
|
||||
# callback triggered by QFileSystemWatcher
|
||||
def _file_changed(self):
|
||||
# neovim writes a file by removing it first so must re-add each time
|
||||
self._watch_paths()
|
||||
self.set_text_from_file(self._filename)
|
||||
self.triggerRerender.emit(True)
|
||||
|
||||
# Turn autoreload on/off.
|
||||
def autoreload(self, enabled):
|
||||
self.preferences['Autoreload'] = enabled
|
||||
self._update_filewatcher()
|
||||
|
||||
def reset_modified(self):
|
||||
|
||||
self.document().setModified(False)
|
||||
|
||||
@property
|
||||
def modified(self):
|
||||
|
||||
return self.document().isModified()
|
||||
|
||||
def saveComponentState(self,store):
|
||||
|
||||
if self.filename != '':
|
||||
store.setValue(self.name+'/state',self.filename)
|
||||
|
||||
def restoreComponentState(self,store):
|
||||
|
||||
filename = store.value(self.name+'/state')
|
||||
|
||||
if filename and self.filename == '':
|
||||
try:
|
||||
self.load_from_file(filename)
|
||||
except IOError:
|
||||
self._logger.warning(f'could not open {filename}')
|
||||
|
||||
|
||||
def get_imported_module_paths(self, module_path):
|
||||
|
||||
finder = ModuleFinder([os.path.dirname(module_path)])
|
||||
imported_modules = []
|
||||
|
||||
try:
|
||||
finder.run_script(module_path)
|
||||
except SyntaxError as err:
|
||||
self._logger.warning(f'Syntax error in {module_path}: {err}')
|
||||
except Exception as err:
|
||||
self._logger.warning(
|
||||
f'Cannot determine imported modules in {module_path}: {type(err).__name__} {err}'
|
||||
)
|
||||
else:
|
||||
for module_name, module in finder.modules.items():
|
||||
if module_name != '__main__':
|
||||
path = getattr(module, '__file__', None)
|
||||
if path is not None and os.path.isfile(path):
|
||||
imported_modules.append(path)
|
||||
|
||||
return imported_modules
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
editor = Editor()
|
||||
editor.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
43
cq_editor/widgets/log.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import logbook as logging
|
||||
|
||||
from PyQt5.QtWidgets import QPlainTextEdit
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from ..mixins import ComponentMixin
|
||||
|
||||
class QtLogHandler(logging.Handler,logging.StringFormatterHandlerMixin):
|
||||
|
||||
def __init__(self, log_widget,*args,**kwargs):
|
||||
|
||||
super(QtLogHandler,self).__init__(*args,**kwargs)
|
||||
logging.StringFormatterHandlerMixin.__init__(self,None)
|
||||
|
||||
self.log_widget = log_widget
|
||||
|
||||
def emit(self, record):
|
||||
|
||||
msg = self.format(record)
|
||||
QtCore.QMetaObject\
|
||||
.invokeMethod(self.log_widget,
|
||||
'appendPlainText',
|
||||
QtCore.Qt.QueuedConnection,
|
||||
QtCore.Q_ARG(str, msg))
|
||||
|
||||
class LogViewer(QPlainTextEdit, ComponentMixin):
|
||||
|
||||
name = 'Log viewer'
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
|
||||
super(LogViewer,self).__init__(*args,**kwargs)
|
||||
self._MAX_ROWS = 500
|
||||
|
||||
self.setReadOnly(True)
|
||||
self.setMaximumBlockCount(self._MAX_ROWS)
|
||||
self.setLineWrapMode(QPlainTextEdit.NoWrap)
|
||||
|
||||
self.handler = QtLogHandler(self)
|
||||
|
||||
def append(self,msg):
|
||||
|
||||
self.appendPlainText(msg)
|
||||
389
cq_editor/widgets/object_tree.py
Normal file
@@ -0,0 +1,389 @@
|
||||
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAction, QMenu, QWidget, QAbstractItemView
|
||||
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal
|
||||
|
||||
from pyqtgraph.parametertree import Parameter, ParameterTree
|
||||
|
||||
from OCP.AIS import AIS_Line
|
||||
from OCP.Geom import Geom_Line
|
||||
from OCP.gp import gp_Dir, gp_Pnt, gp_Ax1
|
||||
|
||||
from ..mixins import ComponentMixin
|
||||
from ..icons import icon
|
||||
from ..cq_utils import make_AIS, export, to_occ_color, is_obj_empty, get_occ_color
|
||||
from ..utils import splitter, layout, get_save_filename
|
||||
|
||||
class TopTreeItem(QTreeWidgetItem):
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
|
||||
super(TopTreeItem,self).__init__(*args,**kwargs)
|
||||
|
||||
class ObjectTreeItem(QTreeWidgetItem):
|
||||
|
||||
props = [{'name': 'Name', 'type': 'str', 'value': ''},
|
||||
{'name': 'Color', 'type': 'color', 'value': "f4a824"},
|
||||
{'name': 'Alpha', 'type': 'float', 'value': 0, 'limits': (0,1), 'step': 1e-1},
|
||||
{'name': 'Visible', 'type': 'bool','value': True}]
|
||||
|
||||
def __init__(self,
|
||||
name,
|
||||
ais=None,
|
||||
shape=None,
|
||||
shape_display=None,
|
||||
sig=None,
|
||||
alpha=0.,
|
||||
color='f4a824',
|
||||
**kwargs):
|
||||
|
||||
super(ObjectTreeItem,self).__init__([name],**kwargs)
|
||||
self.setFlags( self.flags() | Qt.ItemIsUserCheckable)
|
||||
self.setCheckState(0,Qt.Checked)
|
||||
|
||||
self.ais = ais
|
||||
self.shape = shape
|
||||
self.shape_display = shape_display
|
||||
self.sig = sig
|
||||
|
||||
self.properties = Parameter.create(name='Properties',
|
||||
children=self.props)
|
||||
|
||||
self.properties['Name'] = name
|
||||
self.properties['Alpha'] = ais.Transparency()
|
||||
self.properties['Color'] = get_occ_color(ais) if ais else color
|
||||
self.properties.sigTreeStateChanged.connect(self.propertiesChanged)
|
||||
|
||||
def propertiesChanged(self,*args):
|
||||
|
||||
self.setData(0,0,self.properties['Name'])
|
||||
self.ais.SetTransparency(self.properties['Alpha'])
|
||||
self.ais.SetColor(to_occ_color(self.properties['Color']))
|
||||
self.ais.Redisplay()
|
||||
|
||||
if self.properties['Visible']:
|
||||
self.setCheckState(0,Qt.Checked)
|
||||
else:
|
||||
self.setCheckState(0,Qt.Unchecked)
|
||||
|
||||
if self.sig:
|
||||
self.sig.emit()
|
||||
|
||||
class CQRootItem(TopTreeItem):
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
|
||||
super(CQRootItem,self).__init__(['CQ models'],*args,**kwargs)
|
||||
|
||||
|
||||
class HelpersRootItem(TopTreeItem):
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
|
||||
super(HelpersRootItem,self).__init__(['Helpers'],*args,**kwargs)
|
||||
|
||||
|
||||
class ObjectTree(QWidget,ComponentMixin):
|
||||
|
||||
name = 'Object Tree'
|
||||
_stash = []
|
||||
|
||||
preferences = Parameter.create(name='Preferences',children=[
|
||||
{'name': 'Preserve properties on reload', 'type': 'bool', 'value': False},
|
||||
{'name': 'Clear all before each run', 'type': 'bool', 'value': True},
|
||||
{'name': 'STL precision','type': 'float', 'value': .1}])
|
||||
|
||||
sigObjectsAdded = pyqtSignal([list],[list,bool])
|
||||
sigObjectsRemoved = pyqtSignal(list)
|
||||
sigCQObjectSelected = pyqtSignal(object)
|
||||
sigAISObjectsSelected = pyqtSignal(list)
|
||||
sigItemChanged = pyqtSignal(QTreeWidgetItem,int)
|
||||
sigObjectPropertiesChanged = pyqtSignal()
|
||||
|
||||
def __init__(self,parent):
|
||||
|
||||
super(ObjectTree,self).__init__(parent)
|
||||
|
||||
self.tree = tree = QTreeWidget(self,
|
||||
selectionMode=QAbstractItemView.ExtendedSelection)
|
||||
self.properties_editor = ParameterTree(self)
|
||||
|
||||
tree.setHeaderHidden(True)
|
||||
tree.setItemsExpandable(False)
|
||||
tree.setRootIsDecorated(False)
|
||||
tree.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
|
||||
#forward itemChanged singal
|
||||
tree.itemChanged.connect(\
|
||||
lambda item,col: self.sigItemChanged.emit(item,col))
|
||||
#handle visibility changes form tree
|
||||
tree.itemChanged.connect(self.handleChecked)
|
||||
|
||||
self.CQ = CQRootItem()
|
||||
self.Helpers = HelpersRootItem()
|
||||
|
||||
root = tree.invisibleRootItem()
|
||||
root.addChild(self.CQ)
|
||||
root.addChild(self.Helpers)
|
||||
|
||||
tree.expandToDepth(1)
|
||||
|
||||
self._export_STL_action = \
|
||||
QAction('Export as STL',
|
||||
self,
|
||||
enabled=False,
|
||||
triggered=lambda: \
|
||||
self.export('stl',
|
||||
self.preferences['STL precision']))
|
||||
|
||||
self._export_STEP_action = \
|
||||
QAction('Export as STEP',
|
||||
self,
|
||||
enabled=False,
|
||||
triggered=lambda: \
|
||||
self.export('step'))
|
||||
|
||||
self._clear_current_action = QAction(icon('delete'),
|
||||
'Clear current',
|
||||
self,
|
||||
enabled=False,
|
||||
triggered=self.removeSelected)
|
||||
|
||||
self._toolbar_actions = \
|
||||
[QAction(icon('delete-many'),'Clear all',self,triggered=self.removeObjects),
|
||||
self._clear_current_action,]
|
||||
|
||||
self.prepareMenu()
|
||||
|
||||
tree.itemSelectionChanged.connect(self.handleSelection)
|
||||
tree.customContextMenuRequested.connect(self.showMenu)
|
||||
|
||||
self.prepareLayout()
|
||||
|
||||
|
||||
def prepareMenu(self):
|
||||
|
||||
self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
|
||||
self._context_menu = QMenu(self)
|
||||
self._context_menu.addActions(self._toolbar_actions)
|
||||
self._context_menu.addActions((self._export_STL_action,
|
||||
self._export_STEP_action))
|
||||
|
||||
def prepareLayout(self):
|
||||
|
||||
self._splitter = splitter((self.tree,self.properties_editor),
|
||||
stretch_factors = (2,1),
|
||||
orientation=Qt.Vertical)
|
||||
layout(self,(self._splitter,),top_widget=self)
|
||||
|
||||
self._splitter.show()
|
||||
|
||||
def showMenu(self,position):
|
||||
|
||||
self._context_menu.exec_(self.tree.viewport().mapToGlobal(position))
|
||||
|
||||
|
||||
def menuActions(self):
|
||||
|
||||
return {'Tools' : [self._export_STL_action,
|
||||
self._export_STEP_action]}
|
||||
|
||||
def toolbarActions(self):
|
||||
|
||||
return self._toolbar_actions
|
||||
|
||||
def addLines(self):
|
||||
|
||||
origin = (0,0,0)
|
||||
ais_list = []
|
||||
|
||||
for name,color,direction in zip(('X','Y','Z'),
|
||||
('red','lawngreen','blue'),
|
||||
((1,0,0),(0,1,0),(0,0,1))):
|
||||
line_placement = Geom_Line(gp_Ax1(gp_Pnt(*origin),
|
||||
gp_Dir(*direction)))
|
||||
line = AIS_Line(line_placement)
|
||||
line.SetColor(to_occ_color(color))
|
||||
|
||||
self.Helpers.addChild(ObjectTreeItem(name,
|
||||
ais=line))
|
||||
|
||||
ais_list.append(line)
|
||||
|
||||
self.sigObjectsAdded.emit(ais_list)
|
||||
|
||||
def _current_properties(self):
|
||||
|
||||
current_params = {}
|
||||
for i in range(self.CQ.childCount()):
|
||||
child = self.CQ.child(i)
|
||||
current_params[child.properties['Name']] = child.properties
|
||||
|
||||
return current_params
|
||||
|
||||
def _restore_properties(self,obj,properties):
|
||||
|
||||
for p in properties[obj.properties['Name']]:
|
||||
obj.properties[p.name()] = p.value()
|
||||
|
||||
@pyqtSlot(dict,bool)
|
||||
@pyqtSlot(dict)
|
||||
def addObjects(self,objects,clean=False,root=None):
|
||||
|
||||
if root is None:
|
||||
root = self.CQ
|
||||
|
||||
request_fit_view = True if root.childCount() == 0 else False
|
||||
preserve_props = self.preferences['Preserve properties on reload']
|
||||
|
||||
if preserve_props:
|
||||
current_props = self._current_properties()
|
||||
|
||||
if clean or self.preferences['Clear all before each run']:
|
||||
self.removeObjects()
|
||||
|
||||
ais_list = []
|
||||
|
||||
#remove empty objects
|
||||
objects_f = {k:v for k,v in objects.items() if not is_obj_empty(v.shape)}
|
||||
|
||||
for name,obj in objects_f.items():
|
||||
ais,shape_display = make_AIS(obj.shape,obj.options)
|
||||
|
||||
child = ObjectTreeItem(name,
|
||||
shape=obj.shape,
|
||||
shape_display=shape_display,
|
||||
ais=ais,
|
||||
sig=self.sigObjectPropertiesChanged)
|
||||
|
||||
if preserve_props and name in current_props:
|
||||
self._restore_properties(child,current_props)
|
||||
|
||||
if child.properties['Visible']:
|
||||
ais_list.append(ais)
|
||||
|
||||
root.addChild(child)
|
||||
|
||||
if request_fit_view:
|
||||
self.sigObjectsAdded[list,bool].emit(ais_list,True)
|
||||
else:
|
||||
self.sigObjectsAdded[list].emit(ais_list)
|
||||
|
||||
@pyqtSlot(object,str,object)
|
||||
def addObject(self,obj,name='',options={}):
|
||||
|
||||
root = self.CQ
|
||||
|
||||
ais,shape_display = make_AIS(obj, options)
|
||||
|
||||
root.addChild(ObjectTreeItem(name,
|
||||
shape=obj,
|
||||
shape_display=shape_display,
|
||||
ais=ais,
|
||||
sig=self.sigObjectPropertiesChanged))
|
||||
|
||||
self.sigObjectsAdded.emit([ais])
|
||||
|
||||
@pyqtSlot(list)
|
||||
@pyqtSlot()
|
||||
def removeObjects(self,objects=None):
|
||||
|
||||
if objects:
|
||||
removed_items_ais = [self.CQ.takeChild(i).ais for i in objects]
|
||||
else:
|
||||
removed_items_ais = [ch.ais for ch in self.CQ.takeChildren()]
|
||||
|
||||
self.sigObjectsRemoved.emit(removed_items_ais)
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def stashObjects(self,action : bool):
|
||||
|
||||
if action:
|
||||
self._stash = self.CQ.takeChildren()
|
||||
removed_items_ais = [ch.ais for ch in self._stash]
|
||||
self.sigObjectsRemoved.emit(removed_items_ais)
|
||||
else:
|
||||
self.removeObjects()
|
||||
self.CQ.addChildren(self._stash)
|
||||
ais_list = [el.ais for el in self._stash]
|
||||
self.sigObjectsAdded.emit(ais_list)
|
||||
|
||||
@pyqtSlot()
|
||||
def removeSelected(self):
|
||||
|
||||
ixs = self.tree.selectedIndexes()
|
||||
rows = [ix.row() for ix in ixs]
|
||||
|
||||
self.removeObjects(rows)
|
||||
|
||||
def export(self,export_type,precision=None):
|
||||
|
||||
items = self.tree.selectedItems()
|
||||
|
||||
# if CQ models is selected get all children
|
||||
if [item for item in items if item is self.CQ]:
|
||||
CQ = self.CQ
|
||||
shapes = [CQ.child(i).shape for i in range(CQ.childCount())]
|
||||
# otherwise collect all selected children of CQ
|
||||
else:
|
||||
shapes = [item.shape for item in items if item.parent() is self.CQ]
|
||||
|
||||
fname = get_save_filename(export_type)
|
||||
if fname != '':
|
||||
export(shapes,export_type,fname,precision)
|
||||
|
||||
@pyqtSlot()
|
||||
def handleSelection(self):
|
||||
|
||||
items =self.tree.selectedItems()
|
||||
if len(items) == 0:
|
||||
self._export_STL_action.setEnabled(False)
|
||||
self._export_STEP_action.setEnabled(False)
|
||||
return
|
||||
|
||||
# emit list of all selected ais objects (might be empty)
|
||||
ais_objects = [item.ais for item in items if item.parent() is self.CQ]
|
||||
self.sigAISObjectsSelected.emit(ais_objects)
|
||||
|
||||
# handle context menu and emit last selected CQ object (if present)
|
||||
item = items[-1]
|
||||
if item.parent() is self.CQ:
|
||||
self._export_STL_action.setEnabled(True)
|
||||
self._export_STEP_action.setEnabled(True)
|
||||
self._clear_current_action.setEnabled(True)
|
||||
self.sigCQObjectSelected.emit(item.shape)
|
||||
self.properties_editor.setParameters(item.properties,
|
||||
showTop=False)
|
||||
self.properties_editor.setEnabled(True)
|
||||
elif item is self.CQ and item.childCount()>0:
|
||||
self._export_STL_action.setEnabled(True)
|
||||
self._export_STEP_action.setEnabled(True)
|
||||
else:
|
||||
self._export_STL_action.setEnabled(False)
|
||||
self._export_STEP_action.setEnabled(False)
|
||||
self._clear_current_action.setEnabled(False)
|
||||
self.properties_editor.setEnabled(False)
|
||||
self.properties_editor.clear()
|
||||
|
||||
@pyqtSlot(list)
|
||||
def handleGraphicalSelection(self,shapes):
|
||||
|
||||
self.tree.clearSelection()
|
||||
|
||||
CQ = self.CQ
|
||||
for i in range(CQ.childCount()):
|
||||
item = CQ.child(i)
|
||||
for shape in shapes:
|
||||
if item.ais.Shape().IsEqual(shape):
|
||||
item.setSelected(True)
|
||||
|
||||
@pyqtSlot(QTreeWidgetItem,int)
|
||||
def handleChecked(self,item,col):
|
||||
|
||||
if type(item) is ObjectTreeItem:
|
||||
if item.checkState(0):
|
||||
item.properties['Visible'] = True
|
||||
else:
|
||||
item.properties['Visible'] = False
|
||||
|
||||
|
||||
|
||||
173
cq_editor/widgets/occt_widget.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from sys import platform
|
||||
|
||||
|
||||
from PyQt5.QtWidgets import QWidget, QApplication
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QEvent
|
||||
|
||||
import OCP
|
||||
|
||||
from OCP.Aspect import Aspect_DisplayConnection, Aspect_TypeOfTriedronPosition
|
||||
from OCP.OpenGl import OpenGl_GraphicDriver
|
||||
from OCP.V3d import V3d_Viewer
|
||||
from OCP.AIS import AIS_InteractiveContext, AIS_DisplayMode
|
||||
from OCP.Quantity import Quantity_Color
|
||||
|
||||
|
||||
ZOOM_STEP = 0.9
|
||||
|
||||
|
||||
class OCCTWidget(QWidget):
|
||||
|
||||
sigObjectSelected = pyqtSignal(list)
|
||||
|
||||
def __init__(self,parent=None):
|
||||
|
||||
super(OCCTWidget,self).__init__(parent)
|
||||
|
||||
self.setAttribute(Qt.WA_NativeWindow)
|
||||
self.setAttribute(Qt.WA_PaintOnScreen)
|
||||
self.setAttribute(Qt.WA_NoSystemBackground)
|
||||
|
||||
self._initialized = False
|
||||
self._needs_update = False
|
||||
|
||||
#OCCT secific things
|
||||
self.display_connection = Aspect_DisplayConnection()
|
||||
self.graphics_driver = OpenGl_GraphicDriver(self.display_connection)
|
||||
|
||||
self.viewer = V3d_Viewer(self.graphics_driver)
|
||||
self.view = self.viewer.CreateView()
|
||||
self.context = AIS_InteractiveContext(self.viewer)
|
||||
|
||||
#Trihedorn, lights, etc
|
||||
self.prepare_display()
|
||||
|
||||
def prepare_display(self):
|
||||
|
||||
view = self.view
|
||||
|
||||
params = view.ChangeRenderingParams()
|
||||
params.NbMsaaSamples = 8
|
||||
params.IsAntialiasingEnabled = True
|
||||
|
||||
view.TriedronDisplay(
|
||||
Aspect_TypeOfTriedronPosition.Aspect_TOTP_RIGHT_LOWER,
|
||||
Quantity_Color(), 0.1)
|
||||
|
||||
viewer = self.viewer
|
||||
|
||||
viewer.SetDefaultLights()
|
||||
viewer.SetLightOn()
|
||||
|
||||
ctx = self.context
|
||||
|
||||
ctx.SetDisplayMode(AIS_DisplayMode.AIS_Shaded, True)
|
||||
ctx.DefaultDrawer().SetFaceBoundaryDraw(True)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
|
||||
delta = event.angleDelta().y()
|
||||
factor = ZOOM_STEP if delta<0 else 1/ZOOM_STEP
|
||||
|
||||
self.view.SetZoom(factor)
|
||||
|
||||
def mousePressEvent(self,event):
|
||||
|
||||
pos = event.pos()
|
||||
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.view.StartRotation(pos.x(), pos.y())
|
||||
elif event.button() == Qt.RightButton:
|
||||
self.view.StartZoomAtPoint(pos.x(), pos.y())
|
||||
|
||||
self.old_pos = pos
|
||||
|
||||
def mouseMoveEvent(self,event):
|
||||
|
||||
pos = event.pos()
|
||||
x,y = pos.x(),pos.y()
|
||||
|
||||
if event.buttons() == Qt.LeftButton:
|
||||
self.view.Rotation(x,y)
|
||||
|
||||
elif event.buttons() == Qt.MiddleButton:
|
||||
self.view.Pan(x - self.old_pos.x(),
|
||||
self.old_pos.y() - y, theToStart=True)
|
||||
|
||||
elif event.buttons() == Qt.RightButton:
|
||||
self.view.ZoomAtPoint(self.old_pos.x(), y,
|
||||
x, self.old_pos.y())
|
||||
|
||||
self.old_pos = pos
|
||||
|
||||
def mouseReleaseEvent(self,event):
|
||||
|
||||
if event.button() == Qt.LeftButton:
|
||||
pos = event.pos()
|
||||
x,y = pos.x(),pos.y()
|
||||
|
||||
self.context.MoveTo(x,y,self.view,True)
|
||||
|
||||
self._handle_selection()
|
||||
|
||||
def _handle_selection(self):
|
||||
|
||||
self.context.Select(True)
|
||||
self.context.InitSelected()
|
||||
|
||||
selected = []
|
||||
if self.context.HasSelectedShape():
|
||||
selected.append(self.context.SelectedShape())
|
||||
|
||||
self.sigObjectSelected.emit(selected)
|
||||
|
||||
def paintEngine(self):
|
||||
|
||||
return None
|
||||
|
||||
def paintEvent(self, event):
|
||||
|
||||
if not self._initialized:
|
||||
self._initialize()
|
||||
else:
|
||||
self.view.Redraw()
|
||||
|
||||
def showEvent(self, event):
|
||||
|
||||
super(OCCTWidget,self).showEvent(event)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
|
||||
super(OCCTWidget,self).resizeEvent(event)
|
||||
|
||||
self.view.MustBeResized()
|
||||
|
||||
def _initialize(self):
|
||||
|
||||
wins = {
|
||||
'darwin' : self._get_window_osx,
|
||||
'linux' : self._get_window_linux,
|
||||
'win32': self._get_window_win
|
||||
}
|
||||
|
||||
self.view.SetWindow(wins.get(platform,self._get_window_linux)(self.winId()))
|
||||
|
||||
self._initialized = True
|
||||
|
||||
def _get_window_win(self,wid):
|
||||
|
||||
from OCP.WNT import WNT_Window
|
||||
|
||||
return WNT_Window(wid.ascapsule())
|
||||
|
||||
def _get_window_linux(self,wid):
|
||||
|
||||
from OCP.Xw import Xw_Window
|
||||
|
||||
return Xw_Window(self.display_connection,int(wid))
|
||||
|
||||
def _get_window_osx(self,wid):
|
||||
|
||||
from OCP.Cocoa import Cocoa_Window
|
||||
|
||||
return Cocoa_Window(wid.ascapsule())
|
||||
97
cq_editor/widgets/traceback_viewer.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from traceback import extract_tb, format_exception_only
|
||||
|
||||
from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction,
|
||||
QLabel)
|
||||
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal
|
||||
|
||||
from ..mixins import ComponentMixin
|
||||
from ..utils import layout
|
||||
|
||||
class TracebackTree(QTreeWidget):
|
||||
|
||||
name = 'Traceback Viewer'
|
||||
|
||||
def __init__(self,parent):
|
||||
|
||||
super(TracebackTree,self).__init__(parent)
|
||||
self.setHeaderHidden(False)
|
||||
self.setItemsExpandable(False)
|
||||
self.setRootIsDecorated(False)
|
||||
self.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
|
||||
self.setColumnCount(3)
|
||||
self.setHeaderLabels(['File','Line','Code'])
|
||||
|
||||
|
||||
self.root = self.invisibleRootItem()
|
||||
|
||||
class TracebackPane(QWidget,ComponentMixin):
|
||||
|
||||
sigHighlightLine = pyqtSignal(int)
|
||||
|
||||
def __init__(self,parent):
|
||||
|
||||
super(TracebackPane,self).__init__(parent)
|
||||
|
||||
self.tree = TracebackTree(self)
|
||||
self.current_exception = QLabel(self)
|
||||
self.current_exception.setStyleSheet(\
|
||||
"QLabel {color : red; }");
|
||||
|
||||
layout(self,
|
||||
(self.current_exception,
|
||||
self.tree),
|
||||
self)
|
||||
|
||||
self.tree.currentItemChanged.connect(self.handleSelection)
|
||||
|
||||
@pyqtSlot(object,str)
|
||||
def addTraceback(self,exc_info,code):
|
||||
|
||||
self.tree.clear()
|
||||
|
||||
if exc_info:
|
||||
t,exc,tb = exc_info
|
||||
|
||||
root = self.tree.root
|
||||
code = code.splitlines()
|
||||
tb = [t for t in extract_tb(tb) if '<string>' in t.filename] #ignore highest frames (debug, exec)
|
||||
|
||||
for el in tb:
|
||||
#workaround of the traceback module
|
||||
if el.line == '':
|
||||
line = code[el.lineno-1].strip()
|
||||
else:
|
||||
line = el.line
|
||||
|
||||
root.addChild(QTreeWidgetItem([el.filename,
|
||||
str(el.lineno),
|
||||
line]))
|
||||
|
||||
exc_name = t.__name__
|
||||
exc_msg = str(exc)
|
||||
exc_msg = exc_msg.replace('<', '<').replace('>', '>') #replace <>
|
||||
|
||||
self.current_exception.\
|
||||
setText('<b>{}</b>: {}'.format(exc_name,exc_msg))
|
||||
|
||||
# handle the special case of a SyntaxError
|
||||
if t is SyntaxError:
|
||||
root.addChild(QTreeWidgetItem(
|
||||
[exc.filename,
|
||||
str(exc.lineno),
|
||||
exc.text.strip() if exc.text else '']
|
||||
))
|
||||
else:
|
||||
self.current_exception.setText('')
|
||||
|
||||
@pyqtSlot(QTreeWidgetItem,QTreeWidgetItem)
|
||||
def handleSelection(self,item,*args):
|
||||
|
||||
if item:
|
||||
f,line = item.data(0,0),int(item.data(1,0))
|
||||
|
||||
if '<string>' in f:
|
||||
self.sigHighlightLine.emit(line)
|
||||
|
||||
|
||||
379
cq_editor/widgets/viewer.py
Normal file
@@ -0,0 +1,379 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from OCP.Graphic3d import Graphic3d_Camera, Graphic3d_StereoMode
|
||||
from PyQt5.QtWidgets import (QWidget, QPushButton, QDialog, QTreeWidget,
|
||||
QTreeWidgetItem, QVBoxLayout, QFileDialog,
|
||||
QHBoxLayout, QFrame, QLabel, QApplication,
|
||||
QToolBar, QAction)
|
||||
|
||||
from PyQt5.QtCore import QSize, pyqtSlot, pyqtSignal, QMetaObject, Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from OCP.AIS import AIS_Shaded,AIS_WireFrame, AIS_ColoredShape, \
|
||||
AIS_Axis, AIS_Line
|
||||
from OCP.Aspect import Aspect_GDM_Lines, Aspect_GT_Rectangular, Aspect_GFM_VER
|
||||
from OCP.Quantity import Quantity_NOC_BLACK as BLACK, \
|
||||
Quantity_TOC_RGB as TOC_RGB, Quantity_Color
|
||||
from OCP.Geom import Geom_CylindricalSurface, Geom_Plane, Geom_Circle,\
|
||||
Geom_TrimmedCurve, Geom_Axis1Placement, Geom_Axis2Placement, Geom_Line
|
||||
from OCP.gp import gp_Trsf, gp_Vec, gp_Ax3, gp_Dir, gp_Pnt, gp_Ax1
|
||||
|
||||
from ..utils import layout, get_save_filename
|
||||
from ..mixins import ComponentMixin
|
||||
from ..icons import icon
|
||||
from ..cq_utils import to_occ_color, make_AIS
|
||||
|
||||
from .occt_widget import OCCTWidget
|
||||
|
||||
from pyqtgraph.parametertree import Parameter
|
||||
import qtawesome as qta
|
||||
|
||||
|
||||
class OCCViewer(QWidget,ComponentMixin):
|
||||
|
||||
name = '3D Viewer'
|
||||
|
||||
preferences = Parameter.create(name='Pref',children=[
|
||||
{'name': 'Fit automatically', 'type': 'bool', 'value': True},
|
||||
{'name': 'Use gradient', 'type': 'bool', 'value': False},
|
||||
{'name': 'Background color', 'type': 'color', 'value': (95,95,95)},
|
||||
{'name': 'Background color (aux)', 'type': 'color', 'value': (30,30,30)},
|
||||
{'name': 'Default object color', 'type': 'color', 'value': "FF0"},
|
||||
{'name': 'Deviation', 'type': 'float', 'value': 1e-5, 'dec': True, 'step': 1},
|
||||
{'name': 'Angular deviation', 'type': 'float', 'value': 0.1, 'dec': True, 'step': 1},
|
||||
{'name': 'Projection Type', 'type': 'list', 'value': 'Orthographic',
|
||||
'values': ['Orthographic', 'Perspective', 'Stereo', 'MonoLeftEye', 'MonoRightEye']},
|
||||
{'name': 'Stereo Mode', 'type': 'list', 'value': 'QuadBuffer',
|
||||
'values': ['QuadBuffer', 'Anaglyph', 'RowInterlaced', 'ColumnInterlaced',
|
||||
'ChessBoard', 'SideBySide', 'OverUnder']}])
|
||||
IMAGE_EXTENSIONS = 'png'
|
||||
|
||||
sigObjectSelected = pyqtSignal(list)
|
||||
|
||||
def __init__(self,parent=None):
|
||||
|
||||
super(OCCViewer,self).__init__(parent)
|
||||
ComponentMixin.__init__(self)
|
||||
|
||||
self.canvas = OCCTWidget()
|
||||
self.canvas.sigObjectSelected.connect(self.handle_selection)
|
||||
|
||||
self.create_actions(self)
|
||||
|
||||
self.layout_ = layout(self,
|
||||
[self.canvas,],
|
||||
top_widget=self,
|
||||
margin=0)
|
||||
|
||||
self.updatePreferences()
|
||||
|
||||
def updatePreferences(self,*args):
|
||||
|
||||
color1 = to_occ_color(self.preferences['Background color'])
|
||||
color2 = to_occ_color(self.preferences['Background color (aux)'])
|
||||
|
||||
if not self.preferences['Use gradient']:
|
||||
color2 = color1
|
||||
self.canvas.view.SetBgGradientColors(color1,color2,theToUpdate=True)
|
||||
|
||||
self.canvas.update()
|
||||
|
||||
ctx = self.canvas.context
|
||||
ctx.SetDeviationCoefficient(self.preferences['Deviation'])
|
||||
ctx.SetDeviationAngle(self.preferences['Angular deviation'])
|
||||
|
||||
v = self._get_view()
|
||||
camera = v.Camera()
|
||||
projection_type = self.preferences['Projection Type']
|
||||
camera.SetProjectionType(getattr(Graphic3d_Camera, f'Projection_{projection_type}',
|
||||
Graphic3d_Camera.Projection_Orthographic))
|
||||
|
||||
# onle relevant for stereo projection
|
||||
stereo_mode = self.preferences['Stereo Mode']
|
||||
params = v.ChangeRenderingParams()
|
||||
params.StereoMode = getattr(Graphic3d_StereoMode, f'Graphic3d_StereoMode_{stereo_mode}',
|
||||
Graphic3d_StereoMode.Graphic3d_StereoMode_QuadBuffer)
|
||||
|
||||
def create_actions(self,parent):
|
||||
|
||||
self._actions = \
|
||||
{'View' : [QAction(qta.icon('fa.arrows-alt'),
|
||||
'Fit (Shift+F1)',
|
||||
parent,
|
||||
shortcut='shift+F1',
|
||||
triggered=self.fit),
|
||||
QAction(QIcon(':/images/icons/isometric_view.svg'),
|
||||
'Iso (Shift+F2)',
|
||||
parent,
|
||||
shortcut='shift+F2',
|
||||
triggered=self.iso_view),
|
||||
QAction(QIcon(':/images/icons/top_view.svg'),
|
||||
'Top (Shift+F3)',
|
||||
parent,
|
||||
shortcut='shift+F3',
|
||||
triggered=self.top_view),
|
||||
QAction(QIcon(':/images/icons/bottom_view.svg'),
|
||||
'Bottom (Shift+F4)',
|
||||
parent,
|
||||
shortcut='shift+F4',
|
||||
triggered=self.bottom_view),
|
||||
QAction(QIcon(':/images/icons/front_view.svg'),
|
||||
'Front (Shift+F5)',
|
||||
parent,
|
||||
shortcut='shift+F5',
|
||||
triggered=self.front_view),
|
||||
QAction(QIcon(':/images/icons/back_view.svg'),
|
||||
'Back (Shift+F6)',
|
||||
parent,
|
||||
shortcut='shift+F6',
|
||||
triggered=self.back_view),
|
||||
QAction(QIcon(':/images/icons/left_side_view.svg'),
|
||||
'Left (Shift+F7)',
|
||||
parent,
|
||||
shortcut='shift+F7',
|
||||
triggered=self.left_view),
|
||||
QAction(QIcon(':/images/icons/right_side_view.svg'),
|
||||
'Right (Shift+F8)',
|
||||
parent,
|
||||
shortcut='shift+F8',
|
||||
triggered=self.right_view),
|
||||
QAction(qta.icon('fa.square-o'),
|
||||
'Wireframe (Shift+F9)',
|
||||
parent,
|
||||
shortcut='shift+F9',
|
||||
triggered=self.wireframe_view),
|
||||
QAction(qta.icon('fa.square'),
|
||||
'Shaded (Shift+F10)',
|
||||
parent,
|
||||
shortcut='shift+F10',
|
||||
triggered=self.shaded_view)],
|
||||
'Tools' : [QAction(icon('screenshot'),
|
||||
'Screenshot',
|
||||
parent,
|
||||
triggered=self.save_screenshot)]}
|
||||
|
||||
def toolbarActions(self):
|
||||
|
||||
return self._actions['View']
|
||||
|
||||
|
||||
def clear(self):
|
||||
|
||||
self.displayed_shapes = []
|
||||
self.displayed_ais = []
|
||||
self.canvas.context.EraseAll(True)
|
||||
context = self._get_context()
|
||||
context.PurgeDisplay()
|
||||
context.RemoveAll(True)
|
||||
|
||||
def _display(self,shape):
|
||||
|
||||
ais = make_AIS(shape)
|
||||
self.canvas.context.Display(shape,True)
|
||||
|
||||
self.displayed_shapes.append(shape)
|
||||
self.displayed_ais.append(ais)
|
||||
|
||||
#self.canvas._display.Repaint()
|
||||
|
||||
@pyqtSlot(object)
|
||||
def display(self,ais):
|
||||
|
||||
context = self._get_context()
|
||||
context.Display(ais,True)
|
||||
|
||||
if self.preferences['Fit automatically']: self.fit()
|
||||
|
||||
@pyqtSlot(list)
|
||||
@pyqtSlot(list,bool)
|
||||
def display_many(self,ais_list,fit=None):
|
||||
|
||||
context = self._get_context()
|
||||
for ais in ais_list:
|
||||
context.Display(ais,True)
|
||||
|
||||
if self.preferences['Fit automatically'] and fit is None:
|
||||
self.fit()
|
||||
elif fit:
|
||||
self.fit()
|
||||
|
||||
@pyqtSlot(QTreeWidgetItem,int)
|
||||
def update_item(self,item,col):
|
||||
|
||||
ctx = self._get_context()
|
||||
if item.checkState(0):
|
||||
ctx.Display(item.ais,True)
|
||||
else:
|
||||
ctx.Erase(item.ais,True)
|
||||
|
||||
@pyqtSlot(list)
|
||||
def remove_items(self,ais_items):
|
||||
|
||||
ctx = self._get_context()
|
||||
for ais in ais_items: ctx.Erase(ais,True)
|
||||
|
||||
@pyqtSlot()
|
||||
def redraw(self):
|
||||
|
||||
self._get_viewer().Redraw()
|
||||
|
||||
def fit(self):
|
||||
|
||||
self.canvas.view.FitAll()
|
||||
|
||||
def iso_view(self):
|
||||
|
||||
v = self._get_view()
|
||||
v.SetProj(1,-1,1)
|
||||
v.SetTwist(0)
|
||||
|
||||
def bottom_view(self):
|
||||
|
||||
v = self._get_view()
|
||||
v.SetProj(0,0,-1)
|
||||
v.SetTwist(0)
|
||||
|
||||
def top_view(self):
|
||||
|
||||
v = self._get_view()
|
||||
v.SetProj(0,0,1)
|
||||
v.SetTwist(0)
|
||||
|
||||
def front_view(self):
|
||||
|
||||
v = self._get_view()
|
||||
v.SetProj(0,1,0)
|
||||
v.SetTwist(0)
|
||||
|
||||
def back_view(self):
|
||||
|
||||
v = self._get_view()
|
||||
v.SetProj(0,-1,0)
|
||||
v.SetTwist(0)
|
||||
|
||||
def left_view(self):
|
||||
|
||||
v = self._get_view()
|
||||
v.SetProj(-1,0,0)
|
||||
v.SetTwist(0)
|
||||
|
||||
def right_view(self):
|
||||
|
||||
v = self._get_view()
|
||||
v.SetProj(1,0,0)
|
||||
v.SetTwist(0)
|
||||
|
||||
def shaded_view(self):
|
||||
|
||||
c = self._get_context()
|
||||
c.SetDisplayMode(AIS_Shaded, True)
|
||||
|
||||
def wireframe_view(self):
|
||||
|
||||
c = self._get_context()
|
||||
c.SetDisplayMode(AIS_WireFrame, True)
|
||||
|
||||
def show_grid(self,
|
||||
step=1.,
|
||||
size=10.+1e-6,
|
||||
color1=(.7,.7,.7),
|
||||
color2=(0,0,0)):
|
||||
|
||||
viewer = self._get_viewer()
|
||||
viewer.ActivateGrid(Aspect_GT_Rectangular,
|
||||
Aspect_GDM_Lines)
|
||||
viewer.SetRectangularGridGraphicValues(size, size, 0)
|
||||
viewer.SetRectangularGridValues(0, 0, step, step, 0)
|
||||
grid = viewer.Grid()
|
||||
grid.SetColors(Quantity_Color(*color1,TOC_RGB),
|
||||
Quantity_Color(*color2,TOC_RGB))
|
||||
|
||||
def hide_grid(self):
|
||||
|
||||
viewer = self._get_viewer()
|
||||
viewer.DeactivateGrid()
|
||||
|
||||
@pyqtSlot(bool,float)
|
||||
@pyqtSlot(bool)
|
||||
def toggle_grid(self,
|
||||
value : bool,
|
||||
dim : float = 10.):
|
||||
|
||||
if value:
|
||||
self.show_grid(step=dim/20,size=dim+1e-9)
|
||||
else:
|
||||
self.hide_grid()
|
||||
|
||||
@pyqtSlot(gp_Ax3)
|
||||
def set_grid_orientation(self,orientation : gp_Ax3):
|
||||
|
||||
viewer = self._get_viewer()
|
||||
viewer.SetPrivilegedPlane(orientation)
|
||||
|
||||
def show_axis(self,origin = (0,0,0), direction=(0,0,1)):
|
||||
|
||||
ax_placement = Geom_Axis1Placement(gp_Ax1(gp_Pnt(*origin),
|
||||
gp_Dir(*direction)))
|
||||
ax = AIS_Axis(ax_placement)
|
||||
self._display_ais(ax)
|
||||
|
||||
def save_screenshot(self):
|
||||
|
||||
fname = get_save_filename(self.IMAGE_EXTENSIONS)
|
||||
if fname != '':
|
||||
self._get_view().Dump(fname)
|
||||
|
||||
def _display_ais(self,ais):
|
||||
|
||||
self._get_context().Display(ais)
|
||||
|
||||
|
||||
def _get_view(self):
|
||||
|
||||
return self.canvas.view
|
||||
|
||||
def _get_viewer(self):
|
||||
|
||||
return self.canvas.viewer
|
||||
|
||||
def _get_context(self):
|
||||
|
||||
return self.canvas.context
|
||||
|
||||
@pyqtSlot(list)
|
||||
def handle_selection(self,obj):
|
||||
|
||||
self.sigObjectSelected.emit(obj)
|
||||
|
||||
@pyqtSlot(list)
|
||||
def set_selected(self,ais):
|
||||
|
||||
ctx = self._get_context()
|
||||
ctx.ClearSelected(False)
|
||||
|
||||
for obj in ais:
|
||||
ctx.AddOrRemoveSelected(obj,False)
|
||||
|
||||
self.redraw()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
viewer = OCCViewer()
|
||||
|
||||
dlg = QDialog()
|
||||
dlg.setFixedHeight(400)
|
||||
dlg.setFixedWidth(600)
|
||||
|
||||
layout(dlg,(viewer,),dlg)
|
||||
dlg.show()
|
||||
|
||||
box = BRepPrimAPI_MakeBox(20,20,30)
|
||||
box_ais = AIS_ColoredShape(box.Shape())
|
||||
viewer.display(box_ais)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
13
cqgui_env.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
name: cq-occ-conda-test-py3
|
||||
channels:
|
||||
- CadQuery
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- pyqt=5
|
||||
- pyqtgraph
|
||||
- python=3.10
|
||||
- spyder=5
|
||||
- path
|
||||
- logbook
|
||||
- requests
|
||||
- cadquery=master
|
||||
131
icons/back_view.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
131
icons/bottom_view.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
icons/cadquery_logo_dark.ico
Normal file
|
After Width: | Height: | Size: 301 KiB |
78
icons/cadquery_logo_dark.svg
Normal file
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="26.731115mm"
|
||||
height="24.375559mm"
|
||||
viewBox="0 0 94.71655 86.37009"
|
||||
id="svg5495"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="cadquery_logo_dark.svg">
|
||||
<defs
|
||||
id="defs5497" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="4.3709362"
|
||||
inkscape:cx="47.358275"
|
||||
inkscape:cy="43.185045"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="1855"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="65"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5500">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-284.0703,-477.74859)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2980b9;fill-opacity:1;fill-rule:evenodd;stroke:#2980b9;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 285.5703,479.24859 91.71655,0 0,83.37009 -91.71655,0 z"
|
||||
id="path4266" />
|
||||
<g
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text4873">
|
||||
<path
|
||||
d="m 313.43298,529.37701 q 2.73438,0 4.44336,-1.5039 1.70899,-1.50391 1.77735,-3.99903 l 9.26269,0 q -0.0342,3.75977 -2.05078,6.9043 -2.0166,3.11035 -5.53711,4.85352 -3.48633,1.70898 -7.72461,1.70898 -7.92969,0 -12.50976,-5.02441 -4.58008,-5.0586 -4.58008,-13.94532 l 0,-0.64941 q 0,-8.54492 4.5459,-13.63769 4.54589,-5.09278 12.47558,-5.09278 6.93848,0 11.1084,3.96485 4.2041,3.93066 4.27246,10.49316 l -9.26269,0 q -0.0684,-2.87109 -1.77735,-4.64844 -1.70898,-1.81152 -4.51172,-1.81152 -3.45214,0 -5.22949,2.5293 -1.74316,2.49511 -1.74316,8.13476 l 0,1.02539 q 0,5.70801 1.74316,8.20313 1.74317,2.49511 5.29785,2.49511 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:70px;font-family:Roboto;-inkscape-font-specification:'Roboto Bold';fill:#ffffff;fill-opacity:1"
|
||||
id="path4241" />
|
||||
<path
|
||||
d="m 333.01794,517.82428 q 0,-8.71582 3.86231,-13.77441 3.89648,-5.0586 10.66406,-5.0586 5.94727,0 9.46777,4.5459 l 0.64942,-3.8623 8.68164,0 0,51.20117 -9.91211,0 0,-17.39746 q -3.41797,3.8623 -8.95508,3.8623 -6.59668,0 -10.52734,-5.12695 -3.93067,-5.12695 -3.93067,-14.38965 z m 9.87793,0.71777 q 0,5.29785 1.84571,8.06641 1.87988,2.76855 5.26367,2.76855 4.5459,0 6.42578,-3.62304 l 0,-15.27832 q -1.8457,-3.48633 -6.35742,-3.48633 -3.41797,0 -5.29785,2.76855 -1.87989,2.76856 -1.87989,8.78418 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:70px;font-family:Roboto;-inkscape-font-specification:'Roboto Bold';fill:#ffffff;fill-opacity:1"
|
||||
id="path4243" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
131
icons/front_view.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
131
icons/isometric_view.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
131
icons/left_side_view.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
131
icons/right_side_view.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
131
icons/top_view.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
82
pyinstaller.spec
Normal file
@@ -0,0 +1,82 @@
|
||||
# -*- mode: python -*-
|
||||
|
||||
import sys, site, os
|
||||
from path import Path
|
||||
from PyInstaller.utils.hooks import collect_all, collect_submodules
|
||||
|
||||
block_cipher = None
|
||||
|
||||
spyder_data = Path(site.getsitepackages()[-1]) / 'spyder'
|
||||
parso_grammar = (Path(site.getsitepackages()[-1]) / 'parso/python').glob('grammar*')
|
||||
|
||||
if sys.platform == 'linux':
|
||||
occt_dir = os.path.join(Path(sys.prefix), 'share', 'opencascade')
|
||||
ocp_path = (os.path.join(HOMEPATH, 'OCP.cpython-39-x86_64-linux-gnu.so'), '.')
|
||||
elif sys.platform == 'darwin':
|
||||
occt_dir = os.path.join(Path(sys.prefix), 'share', 'opencascade')
|
||||
ocp_path = (os.path.join(HOMEPATH, 'OCP.cpython-39-darwin.so'), '.')
|
||||
elif sys.platform == 'win32':
|
||||
occt_dir = os.path.join(Path(sys.prefix), 'Library', 'share', 'opencascade')
|
||||
ocp_path = (os.path.join(HOMEPATH, 'OCP.cp39-win_amd64.pyd'), '.')
|
||||
|
||||
datas1, binaries1, hiddenimports1 = collect_all('debugpy')
|
||||
hiddenimports2 = collect_submodules('xmlrpc')
|
||||
|
||||
a = Analysis(['run.py'],
|
||||
pathex=['.'],
|
||||
binaries=[ocp_path] + binaries1,
|
||||
datas=[(spyder_data, 'spyder'),
|
||||
(occt_dir, 'opencascade')] +
|
||||
[(p, 'parso/python') for p in parso_grammar] + datas1,
|
||||
hiddenimports=['ipykernel.datapub', 'vtkmodules', 'vtkmodules.all',
|
||||
'pyqtgraph.graphicsItems.ViewBox.axisCtrlTemplate_pyqt5',
|
||||
'pyqtgraph.graphicsItems.PlotItem.plotConfigTemplate_pyqt5',
|
||||
'pyqtgraph.imageview.ImageViewTemplate_pyqt5', 'debugpy', 'xmlrpc',
|
||||
'zmq.backend', 'cq_warehouse', 'cq_warehouse.bearing', 'cq_warehouse.chain',
|
||||
'cq_warehouse.drafting', 'cq_warehouse.extensions', 'cq_warehouse.fastener',
|
||||
'cq_warehouse.sprocket', 'cq_warehouse.thread', 'cq_gears', 'cq_cache'] + hiddenimports1 + hiddenimports2,
|
||||
hookspath=[],
|
||||
runtime_hooks=['pyinstaller/pyi_rth_occ.py',
|
||||
'pyinstaller/pyi_rth_fontconfig.py'],
|
||||
excludes=['_tkinter'],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
|
||||
# There is an issue that keeps the OpenSSL libraries from being copied to the output directory.
|
||||
# This should work if nothing else, but does not with GitHub Actions
|
||||
if sys.platform == 'win32':
|
||||
from PyInstaller.depend.bindepend import getfullnameof
|
||||
rel_data_path = ['PyQt5', 'Qt', 'bin']
|
||||
a.datas += [
|
||||
(getfullnameof('libssl-1_1-x64.dll'), os.path.join(*rel_data_path), 'DATA'),
|
||||
(getfullnameof('libcrypto-1_1-x64.dll'), os.path.join(*rel_data_path), 'DATA'),
|
||||
]
|
||||
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='CQ-editor',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True,
|
||||
icon='icons/cadquery_logo_dark.ico')
|
||||
|
||||
exclude = ()
|
||||
#exclude = ('libGL','libEGL','libbsd')
|
||||
a.binaries = TOC([x for x in a.binaries if not x[0].startswith(exclude)])
|
||||
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
name='CQ-editor')
|
||||
1
pyinstaller/CQ-editor.cmd
Normal file
@@ -0,0 +1 @@
|
||||
call .\CQ-editor\CQ-editor.exe
|
||||
4
pyinstaller/CQ-editor.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
export QT_MAC_WANTS_LAYER=1
|
||||
chmod u+x ./CQ-editor/CQ-editor
|
||||
./CQ-editor/CQ-editor
|
||||
6
pyinstaller/pyi_rth_fontconfig.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
if sys.platform.startswith('linux'):
|
||||
os.environ['FONTCONFIG_FILE'] = '/etc/fonts/fonts.conf'
|
||||
os.environ['FONTCONFIG_PATH'] = '/etc/fonts/'
|
||||
7
pyinstaller/pyi_rth_occ.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from os import environ as env
|
||||
|
||||
env['CASROOT'] = 'opencascade'
|
||||
|
||||
env['CSF_ShadersDirectory'] = 'opencascade/src/Shaders'
|
||||
env['CSF_UnitsLexicon'] = 'opencascade/src/UnitsAPI/Lexi_Expr.dat'
|
||||
env['CSF_UnitsDefinition'] = 'opencascade/src/UnitsAPI/Units.dat'
|
||||
3
pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
xvfb_args=-ac +extension GLX +render
|
||||
log_level=DEBUG
|
||||
16
run.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import os, sys, asyncio
|
||||
import faulthandler
|
||||
|
||||
faulthandler.enable()
|
||||
|
||||
if 'CASROOT' in os.environ:
|
||||
del os.environ['CASROOT']
|
||||
|
||||
if sys.platform == 'win32':
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
from cq_editor.__main__ import main
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1
runtests_locally.sh
Normal file
@@ -0,0 +1 @@
|
||||
python -m pytest --no-xvfb -s
|
||||
BIN
screenshots/screenshot1.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
screenshots/screenshot2.png
Normal file
|
After Width: | Height: | Size: 365 KiB |
BIN
screenshots/screenshot3.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/screenshot4.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
27
setup.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import codecs
|
||||
import os.path
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
def read(rel_path):
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
with codecs.open(os.path.join(here, rel_path), 'r') as fp:
|
||||
return fp.read()
|
||||
|
||||
def get_version(rel_path):
|
||||
for line in read(rel_path).splitlines():
|
||||
if line.startswith('__version__'):
|
||||
delim = '"' if '"' in line else "'"
|
||||
return line.split(delim)[1]
|
||||
else:
|
||||
raise RuntimeError("Unable to find version string.")
|
||||
|
||||
setup(name='CQ-editor',
|
||||
version=get_version('cq_editor/_version.py'),
|
||||
packages=find_packages(),
|
||||
entry_points={
|
||||
'gui_scripts': [
|
||||
'cq-editor = cq_editor.__main__:main',
|
||||
'CQ-editor = cq_editor.__main__:main'
|
||||
]}
|
||||
)
|
||||