from jmwright 72b67da

This commit is contained in:
jdegenstein
2022-09-16 13:52:44 -05:00
committed by GitHub
parent 04978a6f78
commit fffb6db0f2
50 changed files with 14900 additions and 0 deletions

162
README.md Normal file
View File

@@ -0,0 +1,162 @@
# CadQuery editor
[![Build status](https://ci.appveyor.com/api/projects/status/g98rs7la393mgy91/branch/master?svg=true)](https://ci.appveyor.com/project/adam-urbanczyk/cq-editor/branch/master)
[![codecov](https://codecov.io/gh/CadQuery/CQ-editor/branch/master/graph/badge.svg)](https://codecov.io/gh/CadQuery/CQ-editor)
[![Build Status](https://dev.azure.com/cadquery/CQ-editor/_apis/build/status/CadQuery.CQ-editor?branchName=master)](https://dev.azure.com/cadquery/CQ-editor/_build/latest?definitionId=3&branchName=master)
[![DOI](https://zenodo.org/badge/136604983.svg)](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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
from ._version import __version__

28
cq_editor/__main__.py Normal file
View 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
View File

@@ -0,0 +1 @@
__version__ = "0.3.0dev"

148
cq_editor/cq_utils.py Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

341
cq_editor/main_window.py Normal file
View 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
View 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
View 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
View 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

View File

View 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_())

View 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

View 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
View 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
View 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)

View 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

View 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())

View 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('<', '&lt;').replace('>', '&gt;') #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
View 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
View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

131
icons/bottom_view.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

131
icons/isometric_view.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

131
icons/left_side_view.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

131
icons/right_side_view.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

131
icons/top_view.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

82
pyinstaller.spec Normal file
View 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')

View File

@@ -0,0 +1 @@
call .\CQ-editor\CQ-editor.exe

4
pyinstaller/CQ-editor.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
export QT_MAC_WANTS_LAYER=1
chmod u+x ./CQ-editor/CQ-editor
./CQ-editor/CQ-editor

View 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/'

View 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
View File

@@ -0,0 +1,3 @@
[pytest]
xvfb_args=-ac +extension GLX +render
log_level=DEBUG

16
run.py Normal file
View 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
View File

@@ -0,0 +1 @@
python -m pytest --no-xvfb -s

BIN
screenshots/screenshot1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

BIN
screenshots/screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

BIN
screenshots/screenshot3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
screenshots/screenshot4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

27
setup.py Normal file
View 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'
]}
)

1427
tests/test_app.py Normal file

File diff suppressed because it is too large Load Diff