diff --git a/.github/workflows/pyinstaller-builds-actions-PIP-TAR.yml b/.github/workflows/pyinstaller-builds-actions-PIP-TAR.yml new file mode 100644 index 00000000..24957639 --- /dev/null +++ b/.github/workflows/pyinstaller-builds-actions-PIP-TAR.yml @@ -0,0 +1,177 @@ +name: build-PIP-TAR +on: + schedule: + - cron: '0 0 * * 1' + workflow_dispatch: + inputs: + type: + description: 'Whether to build a single file (onefile) or directory (dir) dist' + required: true + default: 'dir' +jobs: + build-linux-x86_64: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: mamba-org/setup-micromamba@v1 + with: + environment-name: test + environment-file: environment.yml + init-shell: >- + bash + # create-args: >- + - name: pip install cadquery CQ-editor ... etc + shell: bash --login {0} + run: | + sudo apt install -y libblas-dev libblas3 libblas64-3 libblas64-dev + sudo apt install -y libxkbcommon0 libxkbcommon-x11-0 libxcb-xinerama0 + sudo apt install -y qtbase5-dev qt5-qmake + micromamba info + pip install pyopengl + pip install git+https://github.com/jdegenstein/jmwright-CQ-Editor + pip install -vvv --pre git+https://github.com/cadquery/cadquery casadi + pip install pyinstaller>=5.6 + pip install path + pip install jupyter-rfb + pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse + pip install git+https://github.com/gumyr/bd_warehouse + pip install git+https://github.com/meadiode/cq_gears.git@main + pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache" + pip install git+https://github.com/gumyr/build123d + pip install git+https://github.com/JustinSDK/cqMore + pip list + - name: Run build + shell: bash --login {0} + run: | + micromamba activate test + micromamba info + echo $LD_LIBRARY_PATH + export LD_LIBRARY_PATH=/home/runner/micromamba/envs/test/lib + echo $LD_LIBRARY_PATH + pyinstaller --log-level=DEBUG pyinstaller_pip.spec ${{ github.event.inputs.type }} + cp /home/runner/work/jmwright-CQ-Editor/jmwright-CQ-Editor/pyinstaller/CQ-editor.sh /home/runner/work/jmwright-CQ-Editor/jmwright-CQ-Editor/dist/ + - uses: actions/upload-artifact@v4 + with: + name: CQ-editor-Linux-x86_64 + path: dist + build-macos-x86_64: + runs-on: macos-13 + steps: + - uses: actions/checkout@v4 + - uses: mamba-org/setup-micromamba@v1 + with: + environment-name: test + environment-file: environment.yml + init-shell: >- + bash + - name: pip install cadquery CQ-editor ... etc + shell: bash --login {0} + run: | + micromamba info + pip install git+https://github.com/jdegenstein/jmwright-CQ-Editor + pip install --pre git+https://github.com/cadquery/cadquery casadi + pip install pyinstaller>=5.6 + pip install path + pip uninstall -y PyQt5 + pip install PyQt5==5.15.7 + pip install PyQtWebEngine==5.15.6 + pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse + pip install git+https://github.com/gumyr/bd_warehouse + pip install git+https://github.com/meadiode/cq_gears.git@main + pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache" + pip install git+https://github.com/gumyr/build123d + pip install git+https://github.com/JustinSDK/cqMore + pip list + - name: Run build + shell: bash --login {0} + run: | + micromamba activate test + micromamba info + pyinstaller pyinstaller_pip.spec ${{ github.event.inputs.type }} + cp /Users/runner/work/jmwright-CQ-Editor/jmwright-CQ-Editor/pyinstaller/CQ-editor-mac.sh /Users/runner/work/jmwright-CQ-Editor/jmwright-CQ-Editor/dist/CQ-editor.sh + - uses: actions/upload-artifact@v4 + with: + name: CQ-editor-MacOS-x86_64 + path: dist + build-macos-arm64: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + - uses: mamba-org/setup-micromamba@v1 + with: + environment-name: test + environment-file: environment.yml + init-shell: >- + bash + - name: pip install cadquery CQ-editor ... etc + shell: bash --login {0} + run: | + micromamba info + pip install git+https://github.com/jdegenstein/jmwright-CQ-Editor + pip install https://github.com/CadQuery/ocp-build-system/releases/download/7.7.2.0/cadquery_ocp-7.7.2-cp311-cp311-macosx_11_0_arm64.whl + pip install https://github.com/jdegenstein/nlopt-python/releases/download/2.7.1.3/nlopt-2.7.1-cp311-cp311-macosx_11_0_arm64.whl + pip install --pre git+https://github.com/cadquery/cadquery casadi + pip install pyinstaller>=5.6 + pip install path + pip uninstall -y PyQt5 + pip install PyQt5==5.15.10 + pip install PyQtWebEngine==5.15.6 + pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse + pip install git+https://github.com/gumyr/bd_warehouse + pip install git+https://github.com/meadiode/cq_gears.git@main + pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache" + pip install git+https://github.com/gumyr/build123d + pip install git+https://github.com/JustinSDK/cqMore + pip list + - name: Run build + shell: bash --login {0} + run: | + micromamba activate test + micromamba info + pyinstaller pyinstaller_pip.spec ${{ github.event.inputs.type }} + cp /Users/runner/work/jmwright-CQ-Editor/jmwright-CQ-Editor/pyinstaller/CQ-editor-mac.sh /Users/runner/work/jmwright-CQ-Editor/jmwright-CQ-Editor/dist/CQ-editor.sh + - uses: actions/upload-artifact@v4 + with: + name: CQ-editor-MacOS-arm64 + path: dist + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: mamba-org/setup-micromamba@v1 + with: + environment-name: test + environment-file: environment.yml + init-shell: >- + bash + - name: pip install cadquery CQ-editor ... etc + shell: bash --login {0} + run: | + micromamba info + pip install git+https://github.com/jdegenstein/jmwright-CQ-Editor + pip install --pre git+https://github.com/cadquery/cadquery casadi + pip install pyinstaller>=5.6 + pip install path + pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse + pip install git+https://github.com/gumyr/bd_warehouse + pip install git+https://github.com/meadiode/cq_gears.git@main + pip install -e "git+https://github.com/CadQuery/cadquery-plugins.git#egg=cq_cache&subdirectory=plugins/cq_cache" + pip install git+https://github.com/gumyr/build123d + pip install git+https://github.com/JustinSDK/cqMore + pip list + - name: Run build + shell: bash --login {0} + run: | + micromamba activate test + micromamba info + pyinstaller pyinstaller_pip.spec ${{ github.event.inputs.type }} + cp D:/a/jmwright-CQ-Editor/jmwright-CQ-Editor/pyinstaller/CQ-editor.cmd D:/a/jmwright-CQ-Editor/jmwright-CQ-Editor/dist/ + + - name: 7zip artifact (workaround for too many files during artifact upload) + shell: bash --login {0} + run: 7z a release.zip ./dist/* + + - uses: actions/upload-artifact@v4 + with: + name: CQ-editor-Windows + path: release.zip diff --git a/README.md b/README.md index 3a128889..7a0a5679 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,16 @@ # CadQuery editor +<<<<<<< HEAD +This is a fork of [jmwright's fork](https://github.com/jmwright/CQ-editor) of [CadQuery/CQ-editor](https://github.com/CadQuery/CQ-editor). This fork includes changes that enable dark mode for CQ-editor (see screenshot below). Under the GitHub Actions menu this fork also contains static builds of CQ-editor for Linux/MacOS/Windows that include the [cq_gears](https://github.com/meadiode/cq_gears), [cq_cache](https://github.com/CadQuery/cadquery-plugins/tree/main/plugins/cq_cache), [cq_more](https://github.com/JustinSDK/cqMore), [cq_warehouse](https://github.com/gumyr/cq_warehouse), [bd_warehouse](https://github.com/gumyr/bd_warehouse), and [build123d](https://github.com/gumyr/build123d) libraries. Note you need to change color preferences to enable dark mode for all panes (see Edit -> Preferences). + +This fork also contains additional changes to the `show_object` function in CQ-editor that make it easier to display and export build123d objects and object lists. + +Running into issues? Please click here to join the [***CadQuery, CQ-Editor, and build123d Discord***](https://discord.com/invite/Bj9AQPsCfx) + +![image](https://user-images.githubusercontent.com/16868537/191054760-a2cac297-3488-48d4-b9f6-52747dffcce3.png) + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b [![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) @@ -13,7 +24,10 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. ## Notable features +<<<<<<< HEAD +======= * Automatic code reloading - you can use your favourite editor +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b * OCCT based * Graphical debugger for CadQuery scripts * Step through script and watch how your model changes @@ -26,6 +40,27 @@ CadQuery GUI editor based on PyQT supports Linux, Windows and Mac. ## Installation - Pre-Built Packages (Recommended) +<<<<<<< HEAD +### Release Packages + +Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/jdegenstein/jmwright-CQ-editor/releases). Download the zip file for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. On later MacOS versions you may also need `xattr -r -d com.apple.quarantine path/to/CQ-editor-MacOS`. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly. + +### Development Packages + +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/jdegenstein/jmwright-CQ-editor/actions), 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 (pip) + +Additional packages for Linux (known as needed on Ubuntu 22.04): +``` +sudo apt install qtbase5-dev qt5-qmake +``` +All platforms (Windows/Mac/Linux): +``` +pip install git+https://github.com/jdegenstein/jmwright-CQ-Editor +pip install --pre "cadquery>=2.2" +pip install git+https://github.com/gumyr/build123d +======= ~~### Release Packages~~ ~~Stable release builds which do not require Anaconda are attached to the [latest release](https://github.com/CadQuery/CQ-editor/releases). Download installer for your operating system, extract it, and run the CQ-editor script for your OS (CQ-editor.cmd for Windows, CQ-editor.sh for Linux and MacOS). On Windows you should be able to simply double-click on CQ-editor.cmd. On Linux and MacOS you may need to make the script executable with `chmod +x CQ-editor.sh` and run the script from the command line. The script contains an environment variable export that may be required to get CQ-editor to launch correctly on MacOS Big Sur, so it is better to use the script than to launch CQ-editor directly.~~ @@ -66,6 +101,7 @@ 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 +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b ``` ## Usage @@ -76,6 +112,11 @@ By default, CQ-editor will display a 3D representation of all `Workplane` object ```python show_object(result, name="somename", options={"alpha":0.5, "color": (64, 164, 223)}) +<<<<<<< HEAD +# or using rand_color: +show_object(result, name="somename", options=rand_color(alpha=.5)) +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b ``` Note that `show_object` works for `Shape` and `TopoDS_Shape` objects too. In order to display objects from the embedded Python console use `show`. diff --git a/appveyor.yml b/appveyor.yml index 60ee6084..d99eb304 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,6 +2,10 @@ shallow_clone: false image: - Ubuntu2004 +<<<<<<< HEAD + - Ubuntu1804 +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b - Visual Studio 2015 environment: @@ -13,11 +17,19 @@ environment: install: - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then sudo apt update; sudo apt -y --force-yes install libglu1-mesa xvfb libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev; fi +<<<<<<< HEAD + - 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 +======= - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh; fi - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Darwin-x86_64.sh; fi - sh: bash miniconda.sh -b -p $HOME/miniconda - sh: source $HOME/miniconda/bin/activate - cmd: curl -fsSL -o miniconda.exe https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Windows-x86_64.exe +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b - cmd: miniconda.exe /S /InstallationType=JustMe /D=%MINICONDA_DIRNAME% - cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - cmd: activate diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 77e02d43..c0891187 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,8 +3,11 @@ trigger: include: - master - refs/tags/* +<<<<<<< HEAD +======= exclude: - refs/tags/nightly +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b pr: - master @@ -13,13 +16,48 @@ resources: repositories: - repository: templates type: github +<<<<<<< HEAD + name: jmwright/conda-packages +======= name: CadQuery/conda-packages +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b endpoint: CadQuery parameters: - name: minor type: object default: +<<<<<<< HEAD + - 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 +======= - 11 stages: @@ -86,3 +124,4 @@ stages: artifact: installer_ubuntu-latest - bash: cp $(Pipeline.Workspace)/installer*/*.* . - bash: sh ./CQ-editor-master-Linux-x86_64.sh -b -p dummy && cd dummy && ./run.sh +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b diff --git a/conda/meta.yaml b/conda/meta.yaml index b6a95f53..3b2553c2 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -6,19 +6,31 @@ source: path: .. build: +<<<<<<< HEAD + string: {{ 'py'+environ.get('PYTHON_VERSION')}} +======= string: {{ GIT_DESCRIBE_TAG }}_{{ GIT_BUILD_STR }} noarch: python +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b 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: +<<<<<<< HEAD + - python {{ environ.get('PYTHON_VERSION') }} + - setuptools + + run: + - python {{ environ.get('PYTHON_VERSION') }} +======= - python >=3.8 - setuptools run: - python >=3.8 +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b - cadquery=master - ocp - logbook @@ -28,7 +40,11 @@ requirements: - path - logbook - requests +<<<<<<< HEAD + +======= - qtconsole=5.4.1 +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b test: imports: - cq_editor diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 2378a82c..035205fe 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -1,3 +1,238 @@ +<<<<<<< HEAD +import cadquery as cq +from cadquery.occ_impl.assembly import toCAF + +from typing import List, Union +from importlib 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 +from OCP.Quantity import ( + Quantity_TOC_RGB as TOC_RGB, + Quantity_Color, + Quantity_NOC_GOLD as GOLD, +) +from OCP.Graphic3d import Graphic3d_NOM_JADE, Graphic3d_MaterialAspect + +from PyQt5.QtGui import QColor + +DEFAULT_FACE_COLOR = Quantity_Color(GOLD) +DEFAULT_MATERIAL = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE) + + +def find_cq_objects(results: dict): + + return { + k: SimpleNamespace(shape=v, options={}) + for k, v in results.items() + if isinstance(v, cq.Workplane) + } + + +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 hasattr(obj, "wrapped") and isinstance(obj.wrapped, TopoDS_Shape): + vals.append(cq.Shape.cast(obj.wrapped)) + elif ( + isinstance(obj, list) + and hasattr(obj[0], "wrapped") + and isinstance(obj[0].wrapped, TopoDS_Shape) + ): + vals.extend(o for o in obj) + elif ( + hasattr(obj, "_obj") + and hasattr(obj._obj, "wrapped") + and isinstance(obj._obj.wrapped, TopoDS_Shape) + ): + vals.append(cq.Shape.cast(obj._obj.wrapped)) + elif ( + isinstance(obj, list) + and hasattr(obj[0], "_obj") + and hasattr(obj[0]._obj, "wrapped") + and isinstance(obj[0]._obj.wrapped, TopoDS_Shape) + ): + vals.append(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_Shape(shape.wrapped) + + set_material(ais, DEFAULT_MATERIAL) + set_color(ais, DEFAULT_FACE_COLOR) + + if "alpha" in options: + set_transparency(ais, options["alpha"]) + if "color" in options: + set_color(ais, to_occ_color(options["color"])) + if "rgba" in options: + r, g, b, a = options["rgba"] + set_color(ais, to_occ_color((r, g, b))) + set_transparency(ais, 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(obj: Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: + + if isinstance(obj, AIS_InteractiveObject): + color = Quantity_Color() + obj.Color(color) + else: + color = obj + + return QColor.fromRgbF(color.Red(), color.Green(), color.Blue()) + + +def set_color(ais: AIS_Shape, color: Quantity_Color) -> AIS_Shape: + + drawer = ais.Attributes() + drawer.SetupOwnShadingAspect() + drawer.ShadingAspect().SetColor(color) + + return ais + + +def set_material(ais: AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Shape: + + drawer = ais.Attributes() + drawer.SetupOwnShadingAspect() + drawer.ShadingAspect().SetMaterial(material) + + return ais + + +def set_transparency(ais: AIS_Shape, alpha: float) -> AIS_Shape: + + drawer = ais.Attributes() + drawer.SetupOwnShadingAspect() + drawer.ShadingAspect().SetTransparency(alpha) + + return ais + + +def reload_cq(): + + # NB: order of reloads is important + reload(cq.types) + reload(cq.occ_impl.geom) + 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 +======= import cadquery as cq from cadquery.occ_impl.assembly import toCAF @@ -198,3 +433,4 @@ def is_obj_empty(obj : Union[cq.Workplane,cq.Shape]) -> bool: rv = True if isinstance(obj.val(), cq.Vector) else False return rv +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b diff --git a/cq_editor/editor.py b/cq_editor/editor.py new file mode 100644 index 00000000..30b4629d --- /dev/null +++ b/cq_editor/editor.py @@ -0,0 +1,303 @@ +import os +import spyder.utils.encoding +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).absolute().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 determine_encoding(self, fname): + if os.path.exists(fname): + # this function returns the encoding spyder used to read the file + _, encoding = spyder.utils.encoding.read(fname) + # spyder returns a -guessed suffix in some cases + return encoding.replace('-guessed', '') + else: + return 'utf-8' + + def save(self): + + if self._filename != '': + + if self.preferences['Autoreload']: + self._file_watcher.blockSignals(True) + self._file_watch_timer.stop() + + encoding = self.determine_encoding(self._filename) + encoded = self.toPlainText().encode(encoding) + with open(self._filename, 'wb') as f: + f.write(encoded) + + 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 != '': + encoded = self.toPlainText().encode('utf-8') + with open(fname, 'wb') as f: + f.write(encoded) + 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_()) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 4bbee280..61f3c89c 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -19,6 +19,32 @@ from .icons import icon from .preferences import PreferencesWidget +<<<<<<< HEAD +#DARKMODE edits: https://stackoverflow.com/questions/48256772/dark-theme-for-qt-widgets +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QApplication +from PyQt5.QtGui import QPalette, QColor +app = QApplication([]) +# Force the style to be the same on all OSs: +app.setStyle("Fusion") +# Now use a palette to switch to dark colors: +palette = QPalette() +palette.setColor(QPalette.Window, QColor(53, 53, 53)) +palette.setColor(QPalette.WindowText, Qt.white) +palette.setColor(QPalette.Base, QColor(25, 25, 25)) +palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) +palette.setColor(QPalette.ToolTipBase, Qt.black) +palette.setColor(QPalette.ToolTipText, Qt.white) +palette.setColor(QPalette.Text, Qt.white) +palette.setColor(QPalette.Button, QColor(53, 53, 53)) +palette.setColor(QPalette.ButtonText, Qt.white) +palette.setColor(QPalette.BrightText, Qt.red) +palette.setColor(QPalette.Link, QColor(42, 130, 218)) +palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) +palette.setColor(QPalette.HighlightedText, Qt.black) +app.setPalette(palette) +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b class MainWindow(QMainWindow,MainMixin): @@ -32,12 +58,15 @@ def __init__(self,parent=None, filename=None): self.setWindowIcon(icon('app')) +<<<<<<< HEAD +======= # Windows workaround - makes the correct task bar icon show up. if sys.platform == "win32": import ctypes myappid = 'cq-editor' # arbitrary string ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b self.viewer = OCCViewer(self) self.setCentralWidget(self.viewer.canvas) @@ -285,7 +314,11 @@ def prepare_console(self): 'rand_color' : self.components['debugger']._rand_color, 'cq' : cq, 'log' : Logger(self.name).info}) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b def fill_dummy(self): self.components['editor']\ diff --git a/cq_editor/widgets/console.py b/cq_editor/widgets/console.py index 77fd1dcc..c2b45a02 100644 --- a/cq_editor/widgets/console.py +++ b/cq_editor/widgets/console.py @@ -17,6 +17,29 @@ def __init__(self, customBanner=None, namespace=dict(), *args, **kwargs): # self.banner = customBanner self.font_size = 6 +<<<<<<< HEAD + self.style_sheet = ''' + ''' + self.syntax_style = 'zenburn' #CHANGES FOR DARKMODE + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b self.kernel_manager = kernel_manager = QtInProcessKernelManager() kernel_manager.start_kernel(show_banner=False) kernel_manager.kernel.gui = 'qt' diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 70d5795f..ecc5dcab 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -1,3 +1,418 @@ +<<<<<<< HEAD +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 random import randrange as rrr, seed + +from ..cq_utils import find_cq_objects, reload_cq +from ..mixins import ComponentMixin + +DUMMY_FILE = "" + + +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, "", "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 "").absolute().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 _rand_color(self, alpha=0.0, cfloat=False): + # helper function to generate a random color dict + # for CQ-editor's show_object function + lower = 10 + upper = 100 # not too high to keep color brightness in check + if cfloat: # for two output types depending on need + return ( + (rrr(lower, upper) / 255), + (rrr(lower, upper) / 255), + (rrr(lower, upper) / 255), + alpha, + ) + return { + "alpha": alpha, + "color": ( + rrr(lower, upper), + rrr(lower, upper), + rrr(lower, upper), + ), + } + + def _inject_locals(self, module): + cq_objects = {} + + def _show_object( + obj, + name=None, + options={}, # all following inputs are ignored by cq-editor + parent=1, + clear=True, + port=3939, + axes=False, + axes0=False, + grid=False, + ticks=10, + ortho=True, + transparent=False, + default_color=(232, 176, 36), + reset_camera=True, + zoom=1.0, + default_edgecolor=(128, 128, 128), + render_edges=True, + render_normals=False, + render_mates=False, + mate_scale=1.0, + deviation=0.1, + angular_tolerance=0.2, + edge_accuracy=5.0, + ambient_intensity=1.0, + direct_intensity=0.12, + ): + 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__["rand_color"] = self._rand_color + 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): + seed( + 59798267586177 + ) # reset the seed every time render is called (preserves colors run to run) + 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] +======= import sys from contextlib import ExitStack, contextmanager from enum import Enum, auto @@ -417,3 +832,4 @@ def module_manager(): new_modules = set(sys.modules.keys()) - loaded_modules for module_name in new_modules: del sys.modules[module_name] +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 20bee81f..a5963a47 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -1,5 +1,8 @@ import os +<<<<<<< HEAD +======= import spyder.utils.encoding +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b from modulefinder import ModuleFinder from spyder.plugins.editor.widgets.codeeditor import CodeEditor @@ -159,6 +162,8 @@ def load_from_file(self,fname): self.filename = fname self.reset_modified() +<<<<<<< HEAD +======= def determine_encoding(self, fname): if os.path.exists(fname): # this function returns the encoding spyder used to read the file @@ -168,6 +173,7 @@ def determine_encoding(self, fname): else: return 'utf-8' +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b def save(self): if self._filename != '': @@ -176,10 +182,15 @@ def save(self): self._file_watcher.blockSignals(True) self._file_watch_timer.stop() +<<<<<<< HEAD + with open(self._filename, 'w') as f: + f.write(self.toPlainText()) +======= encoding = self.determine_encoding(self._filename) encoded = self.toPlainText().encode(encoding) with open(self._filename, 'wb') as f: f.write(encoded) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b if self.preferences['Autoreload']: self._file_watcher.blockSignals(False) @@ -194,9 +205,14 @@ def save_as(self): fname = get_save_filename(self.EXTENSIONS) if fname != '': +<<<<<<< HEAD + with open(fname,'w') as f: + f.write(self.toPlainText()) +======= encoded = self.toPlainText().encode('utf-8') with open(fname, 'wb') as f: f.write(encoded) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b self.filename = fname self.reset_modified() @@ -237,7 +253,10 @@ 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) +<<<<<<< HEAD +======= self.reset_modified() +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b self.triggerRerender.emit(True) # Turn autoreload on/off. diff --git a/cq_editor/widgets/log.py b/cq_editor/widgets/log.py index 18d5162c..823e9b84 100644 --- a/cq_editor/widgets/log.py +++ b/cq_editor/widgets/log.py @@ -1,12 +1,31 @@ import logbook as logging +<<<<<<< HEAD +======= import sys import re +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b from PyQt5.QtWidgets import QPlainTextEdit from PyQt5 import QtCore from ..mixins import ComponentMixin +<<<<<<< HEAD +class QtLogHandler(logging.Handler,logging.StringFormatterHandlerMixin): + + def __init__(self, log_widget,*args,**kwargs): + + super(QtLogHandler,self).__init__(*args,**kwargs) + + log_format_string = '[{record.time:%H:%M:%S.%f%z}] {record.level_name}: {record.message}' + + logging.StringFormatterHandlerMixin.__init__(self,log_format_string) + self.log_widget = log_widget + + def emit(self, record): + + msg = self.format(record) +======= def strip_escape_sequences(input_string): # Regular expression pattern to match ANSI escape codes escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') @@ -34,6 +53,7 @@ def emit(self, record): msg = strip_escape_sequences(msg) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b QtCore.QMetaObject\ .invokeMethod(self.log_widget, 'appendPlainText', diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 1778bfd5..e65d0dd6 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -1,3 +1,429 @@ +<<<<<<< HEAD +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, + set_color, +) +from .viewer import DEFAULT_FACE_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.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 and ais.HasColor() + else get_occ_color(DEFAULT_FACE_COLOR) + ) + self.properties.sigTreeStateChanged.connect(self.propertiesChanged) + + def propertiesChanged(self, properties, changed): + changed_prop = changed[0][0] + + self.setData(0, 0, self.properties["Name"]) + self.ais.SetTransparency(self.properties["Alpha"]) + + if changed_prop.name() == "Color": + set_color(self.ais, to_occ_color(self.properties["Color"])) + + self.ais.Redisplay() + + if self.properties["Visible"]: + self.setCheckState(0, Qt.Checked) + 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": 0.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"), + ((0.2, 0, 0), "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={}, # all following inputs are ignored by cq-editor + parent=1, + clear=True, + port=3939, + axes=False, + axes0=False, + grid=False, + ticks=10, + ortho=True, + transparent=False, + default_color=(232, 176, 36), + reset_camera=True, + zoom=1.0, + default_edgecolor=(128, 128, 128), + render_edges=True, + render_normals=False, + render_mates=False, + mate_scale=1.0, + deviation=0.1, + angular_tolerance=0.2, + edge_accuracy=5.0, + ambient_intensity=1.0, + direct_intensity=0.12, + ): + 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 +======= from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAction, QMenu, QWidget, QAbstractItemView from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal @@ -395,3 +821,4 @@ def handleChecked(self,item,col): +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b diff --git a/cq_editor/widgets/occt_widget.py b/cq_editor/widgets/occt_widget.py old mode 100755 new mode 100644 index 172755ea..1a7dc8b5 --- a/cq_editor/widgets/occt_widget.py +++ b/cq_editor/widgets/occt_widget.py @@ -10,7 +10,11 @@ from OCP.OpenGl import OpenGl_GraphicDriver from OCP.V3d import V3d_Viewer from OCP.AIS import AIS_InteractiveContext, AIS_DisplayMode +<<<<<<< HEAD +from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB as TOC_RGB +======= from OCP.Quantity import Quantity_Color +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b ZOOM_STEP = 0.9 @@ -54,6 +58,11 @@ def prepare_display(self): Aspect_TypeOfTriedronPosition.Aspect_TOTP_RIGHT_LOWER, Quantity_Color(), 0.1) +<<<<<<< HEAD + view.ZBufferTriedronSetup(Quantity_Color(*(0.2, 0.0, 0.0), TOC_RGB)) + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b viewer = self.viewer viewer.SetDefaultLights() @@ -95,8 +104,13 @@ def mouseMoveEvent(self,event): self.old_pos.y() - y, theToStart=True) elif event.buttons() == Qt.RightButton: +<<<<<<< HEAD + self.view.Pan(x - self.old_pos.x(), + self.old_pos.y() - y, theToStart=True) +======= self.view.ZoomAtPoint(self.old_pos.x(), y, x, self.old_pos.y()) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b self.old_pos = pos diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index 7d1051a0..5cfb0642 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -1,5 +1,8 @@ from traceback import extract_tb, format_exception_only +<<<<<<< HEAD +======= from itertools import dropwhile +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction, QLabel) @@ -56,16 +59,26 @@ def addTraceback(self,exc_info,code): root = self.tree.root code = code.splitlines() +<<<<<<< HEAD + tb = [t for t in extract_tb(tb) if '' in t.filename] #ignore highest frames (debug, exec) + + for el in tb: +======= for el in dropwhile( lambda el: 'string>' not in el.filename, extract_tb(tb) ): +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b #workaround of the traceback module if el.line == '': line = code[el.lineno-1].strip() else: line = el.line +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b root.addChild(QTreeWidgetItem([el.filename, str(el.lineno), line])) diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 9c5d620b..f6ecefc6 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -22,8 +22,11 @@ from pyqtgraph.parametertree import Parameter import qtawesome as qta +<<<<<<< HEAD +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b DEFAULT_EDGE_COLOR = Quantity_Color(BLACK) DEFAULT_EDGE_WIDTH = 2 @@ -62,10 +65,16 @@ def __init__(self,parent=None): [self.canvas,], top_widget=self, margin=0) +<<<<<<< HEAD + self.setup_default_drawer() #misspelled in original + self.updatePreferences() + +======= self.setup_default_drawer() self.updatePreferences() +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b def setup_default_drawer(self): # set the default color and material @@ -79,7 +88,11 @@ def setup_default_drawer(self): line_aspect = self.canvas.context.DefaultDrawer().FaceBoundaryAspect() line_aspect.SetWidth(DEFAULT_EDGE_WIDTH) line_aspect.SetColor(DEFAULT_EDGE_COLOR) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b def updatePreferences(self,*args): color1 = to_occ_color(self.preferences['Background color']) @@ -88,9 +101,15 @@ def updatePreferences(self,*args): if not self.preferences['Use gradient']: color2 = color1 self.canvas.view.SetBgGradientColors(color1,color2,theToUpdate=True) +<<<<<<< HEAD + + self.canvas.update() + +======= self.canvas.update() +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b ctx = self.canvas.context ctx.SetDeviationCoefficient(self.preferences['Deviation']) ctx.SetDeviationAngle(self.preferences['Angular deviation']) @@ -255,13 +274,21 @@ def top_view(self): def front_view(self): v = self._get_view() +<<<<<<< HEAD + v.SetProj(0,-1,0) +======= v.SetProj(0,1,0) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b v.SetTwist(0) def back_view(self): v = self._get_view() +<<<<<<< HEAD + v.SetProj(0,1,0) +======= v.SetProj(0,-1,0) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b v.SetTwist(0) def left_view(self): diff --git a/cqgui_env.yml b/cqgui_env.yml index 98cae565..b5c84af4 100644 --- a/cqgui_env.yml +++ b/cqgui_env.yml @@ -5,10 +5,17 @@ channels: dependencies: - pyqt=5 - pyqtgraph +<<<<<<< HEAD + - python=3.11 +======= - python=3.10 +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b - spyder=5 - path - logbook - requests - cadquery=master +<<<<<<< HEAD +======= - qtconsole=5.4.1 +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..992943fc --- /dev/null +++ b/environment.yml @@ -0,0 +1,6 @@ +name: test +channels: + - conda-forge + - defaults +dependencies: + - python=3.11 diff --git a/pyinstaller/CQ-editor-mac.sh b/pyinstaller/CQ-editor-mac.sh new file mode 100644 index 00000000..476f9582 --- /dev/null +++ b/pyinstaller/CQ-editor-mac.sh @@ -0,0 +1,4 @@ +#!/bin/sh +export QT_MAC_WANTS_LAYER=1 +chmod u+x ./CQ-editor/CQ-editor +QT_QPA_PLATFORM=cocoa PYOPENGL_PLATFORM=x11 ./CQ-editor/CQ-editor diff --git a/pyinstaller/CQ-editor.cmd b/pyinstaller/CQ-editor.cmd new file mode 100644 index 00000000..954e27b1 --- /dev/null +++ b/pyinstaller/CQ-editor.cmd @@ -0,0 +1 @@ +call .\CQ-editor\CQ-editor.exe \ No newline at end of file diff --git a/pyinstaller/CQ-editor.sh b/pyinstaller/CQ-editor.sh new file mode 100644 index 00000000..f1a5abc6 --- /dev/null +++ b/pyinstaller/CQ-editor.sh @@ -0,0 +1,4 @@ +#!/bin/sh +export QT_MAC_WANTS_LAYER=1 +chmod u+x ./CQ-editor/CQ-editor +QT_QPA_PLATFORM=xcb PYOPENGL_PLATFORM=x11 ./CQ-editor/CQ-editor diff --git a/pyinstaller/extrahooks/hook-casadi.py b/pyinstaller/extrahooks/hook-casadi.py new file mode 100644 index 00000000..47db8b12 --- /dev/null +++ b/pyinstaller/extrahooks/hook-casadi.py @@ -0,0 +1,9 @@ +# hook-casadi.py +from PyInstaller.utils.hooks import collect_dynamic_libs + +binaries = collect_dynamic_libs('casadi') + +# Something about legacy import codepaths in casadi.casadi causes PyInstaller's analysis to pick up +# casadi._casadi as a top-level _casadi module, which is wrong. +hiddenimports = ['casadi._casadi'] +excludedimports = ['_casadi'] diff --git a/pyinstaller/extrahooks/hook-py_lib3mf.py b/pyinstaller/extrahooks/hook-py_lib3mf.py new file mode 100644 index 00000000..8c665e91 --- /dev/null +++ b/pyinstaller/extrahooks/hook-py_lib3mf.py @@ -0,0 +1,4 @@ +# hook-py_lib3mf.py +from PyInstaller.utils.hooks import collect_dynamic_libs + +binaries = collect_dynamic_libs('py_lib3mf') diff --git a/pyinstaller_pip.spec b/pyinstaller_pip.spec new file mode 100644 index 00000000..52b7331e --- /dev/null +++ b/pyinstaller_pip.spec @@ -0,0 +1,89 @@ +# -*- 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*') +cqw_path = Path(site.getsitepackages()[-1]) / 'cq_warehouse' +bdw_path = Path(site.getsitepackages()[-1]) / 'bd_warehouse' +cq_path = Path(site.getsitepackages()[-1]) / 'cadquery' + +if sys.platform == 'linux': + occt_dir = os.path.join(Path(sys.prefix), 'share', 'opencascade') + ocp_path = [(os.path.join(HOMEPATH, 'OCP.cpython-311-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-311-darwin.so'), '.')] +elif sys.platform == 'win32': + occt_dir = os.path.join(Path(sys.prefix), 'Library', 'share', 'opencascade') + ocp_path = [(os.path.join(HOMEPATH, 'OCP.cp311-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'), + (cqw_path, 'cq_warehouse'), + (bdw_path, 'bd_warehouse'), + (cq_path, 'cadquery')] + + [(p, 'parso/python') for p in parso_grammar] + datas1, + hiddenimports=['ipykernel.datapub', 'debugpy', 'vtkmodules', 'vtkmodules.all', + 'pyqtgraph.graphicsItems.ViewBox.axisCtrlTemplate_pyqt5', + 'pyqtgraph.graphicsItems.PlotItem.plotConfigTemplate_pyqt5', + 'pyqtgraph.imageview.ImageViewTemplate_pyqt5', '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', + 'build123d', 'cqmore', 'bd_warehouse', 'bd_warehouse.pipe', 'bd_warehouse.flange', + 'bd_warehouse.thread', 'bd_warehouse.gears'] + hiddenimports1 + hiddenimports2, + hookspath=['pyinstaller/extrahooks/'], + 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') diff --git a/setup.py b/setup.py index 8943e67f..68c51e6a 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,18 @@ from setuptools import setup, find_packages +<<<<<<< HEAD + +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__"): +======= def read(rel_path): here = os.path.abspath(os.path.dirname(__file__)) with codecs.open(os.path.join(here, rel_path), 'r') as fp: @@ -11,11 +23,37 @@ def read(rel_path): def get_version(rel_path): for line in read(rel_path).splitlines(): if line.startswith('__version__'): +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b delim = '"' if '"' in line else "'" return line.split(delim)[1] else: raise RuntimeError("Unable to find version string.") +<<<<<<< HEAD + +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", + ] + }, + python_requires=">=3.10,<3.14", + install_requires=[ + "logbook>=1", + "ipython", + "path>=16", + "PyQt5>=5", + "requests>=2,<3", + "spyder>=5,<6", + "pyqtgraph", + "numpy >= 2, <3", + ], +) +======= setup(name='CQ-editor', version=get_version('cq_editor/_version.py'), packages=find_packages(), @@ -25,3 +63,4 @@ def get_version(rel_path): 'CQ-editor = cq_editor.__main__:main' ]} ) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b diff --git a/tests/test_app.py b/tests/test_app.py index 2ec4baa7..67fd5996 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -84,6 +84,8 @@ r1 = cq.Workplane(solid1).translate((10, 0, 0)) """ +<<<<<<< HEAD +======= code_show_all = """import cadquery as cq b = cq.Workplane().box(1,1,1) sh = b.val() @@ -91,6 +93,7 @@ sk = cq.Sketch().rect(1,1) """ +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) @@ -117,10 +120,17 @@ def get_bottom_left(widget): return pos def get_rgba(ais): +<<<<<<< HEAD + + alpha = ais.Transparency() + color = get_occ_color(ais) + +======= alpha = ais.Transparency() color = get_occ_color(ais) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b return color.redF(), color.greenF(), color.blueF(), alpha @pytest.fixture @@ -151,7 +161,11 @@ def main_clean(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b editor = win.components['editor'] editor.set_text(code) @@ -167,7 +181,11 @@ def main_clean_do_not_close(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b editor = win.components['editor'] editor.set_text(code) @@ -184,7 +202,11 @@ def main_multi(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b editor = win.components['editor'] editor.set_text(code_multi) @@ -202,7 +224,11 @@ def test_render(main): debugger = win.components['debugger'] console = win.components['console'] log = win.components['log'] +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b # enable CQ reloading debugger.preferences['Reload CQ'] = True @@ -249,6 +275,22 @@ def test_render(main): qtbot.wait(100) assert(obj_tree_comp.CQ.child(0).text(0) == 'test') assert('test' in log.toPlainText().splitlines()[-1]) +<<<<<<< HEAD + + # cq reloading check + obj_tree_comp._toolbar_actions[0].triggered.emit() + assert(obj_tree_comp.CQ.childCount() == 0) + + editor.set_text(code_reload_issue) + debugger._actions['Run'][0].triggered.emit() + + qtbot.wait(100) + assert(obj_tree_comp.CQ.childCount() == 1) + + debugger._actions['Run'][0].triggered.emit() + qtbot.wait(100) + assert(obj_tree_comp.CQ.childCount() == 1) +======= # cq reloading check obj_tree_comp._toolbar_actions[0].triggered.emit() @@ -263,6 +305,7 @@ def test_render(main): debugger._actions['Run'][0].triggered.emit() qtbot.wait(100) assert(obj_tree_comp.CQ.childCount() == 3) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b def test_export(main,mocker): @@ -384,6 +427,8 @@ def assert_func(x): variables = win.components['variables_viewer'] +<<<<<<< HEAD +======= traceback_view = win.components['traceback_viewer'] def check_no_error_occured(): @@ -391,6 +436,7 @@ def check_no_error_occured(): ''' assert( '' == traceback_view.current_exception.text()) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b viewer = win.components['viewer'] assert(number_visible_items(viewer) == 3) @@ -401,31 +447,49 @@ def check_no_error_occured(): assert(debugger._frames == []) #test step through +<<<<<<< HEAD + ev = event_loop([lambda: (assert_func(variables.model().rowCount() == 4), + assert_func(number_visible_items(viewer) == 3), + step.triggered.emit()), + lambda: (assert_func(variables.model().rowCount() == 4), +======= ev = event_loop([lambda: (assert_func(variables.model().rowCount() == 5), +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b assert_func(number_visible_items(viewer) == 3), step.triggered.emit()), lambda: (assert_func(variables.model().rowCount() == 5), assert_func(number_visible_items(viewer) == 3), step.triggered.emit()), +<<<<<<< HEAD + lambda: (assert_func(variables.model().rowCount() == 5), +======= lambda: (assert_func(variables.model().rowCount() == 6), assert_func(number_visible_items(viewer) == 3), step.triggered.emit()), lambda: (assert_func(variables.model().rowCount() == 6), +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b assert_func(number_visible_items(viewer) == 4), cont.triggered.emit())]) patch_debugger(debugger,ev) debug.triggered.emit(True) +<<<<<<< HEAD +======= check_no_error_occured() +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b assert(variables.model().rowCount() == 2) assert(number_visible_items(viewer) == 4) #test exit debug ev = event_loop([lambda: (step.triggered.emit(),), +<<<<<<< HEAD + lambda: (assert_func(variables.model().rowCount() == 1), +======= lambda: (assert_func(variables.model().rowCount() == 5), +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b assert_func(number_visible_items(viewer) == 3), debug.triggered.emit(False),)]) @@ -433,14 +497,21 @@ def check_no_error_occured(): debug.triggered.emit(True) +<<<<<<< HEAD +======= check_no_error_occured() +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b assert(variables.model().rowCount() == 1) assert(number_visible_items(viewer) == 3) #test breakpoint ev = event_loop([lambda: (cont.triggered.emit(),), +<<<<<<< HEAD + lambda: (assert_func(variables.model().rowCount() == 5), +======= lambda: (assert_func(variables.model().rowCount() == 6), +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b assert_func(number_visible_items(viewer) == 4), cont.triggered.emit(),)]) @@ -450,6 +521,14 @@ def check_no_error_occured(): debug.triggered.emit(True) +<<<<<<< HEAD + assert(variables.model().rowCount() == 2) + assert(number_visible_items(viewer) == 4) + + #test breakpoint without using singals + ev = event_loop([lambda: (cont.triggered.emit(),), + lambda: (assert_func(variables.model().rowCount() == 5), +======= check_no_error_occured() assert(variables.model().rowCount() == 2) @@ -458,6 +537,7 @@ def check_no_error_occured(): #test breakpoint without using singals ev = event_loop([lambda: (cont.triggered.emit(),), lambda: (assert_func(variables.model().rowCount() == 6), +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b assert_func(number_visible_items(viewer) == 4), cont.triggered.emit(),)]) @@ -467,6 +547,14 @@ def check_no_error_occured(): debugger.debug(True) +<<<<<<< HEAD + assert(variables.model().rowCount() == 2) + assert(number_visible_items(viewer) == 4) + + #test debug() without using singals + ev = event_loop([lambda: (cont.triggered.emit(),), + lambda: (assert_func(variables.model().rowCount() == 5), +======= check_no_error_occured() assert(variables.model().rowCount() == 2) @@ -475,6 +563,7 @@ def check_no_error_occured(): #test debug() without using singals ev = event_loop([lambda: (cont.triggered.emit(),), lambda: (assert_func(variables.model().rowCount() == 6), +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b assert_func(number_visible_items(viewer) == 4), cont.triggered.emit(),)]) @@ -484,11 +573,17 @@ def check_no_error_occured(): editor.debugger.set_breakpoints([(4,None)]) debugger.debug(True) +<<<<<<< HEAD + + CQ = obj_tree.CQ + +======= check_no_error_occured() CQ = obj_tree.CQ +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b # object 1 (defualt color) r,g,b,a = get_rgba(CQ.child(0).ais) assert( a == pytest.approx(0.2) ) @@ -511,10 +606,13 @@ def check_no_error_occured(): result = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125) f() ''' +<<<<<<< HEAD +======= code_err3 =\ '''import cadquery as cq result = cq.Workplane("XY" ).box(3, 3, 0) ''' +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b def test_traceback(main): @@ -545,6 +643,21 @@ def test_traceback(main): assert('NameError' in traceback_view.current_exception.text()) assert(hasattr(sys, 'last_traceback')) +<<<<<<< HEAD + + del sys.last_traceback + assert(not hasattr(sys, 'last_traceback')) + + + #test last_traceback with debug + ev = event_loop([lambda: (cont.triggered.emit(),)]) + patch_debugger(debugger,ev) + + debugger.debug(True) + + assert('NameError' in traceback_view.current_exception.text()) + assert(hasattr(sys, 'last_traceback')) +======= del sys.last_traceback assert(not hasattr(sys, 'last_traceback')) @@ -559,10 +672,13 @@ def test_traceback(main): assert('NameError' in traceback_view.current_exception.text()) assert(hasattr(sys, 'last_traceback')) assert(traceback_view.tree.root.childCount() == 1) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b # restore the tracing function sys.settrace(trace_function) +<<<<<<< HEAD +======= # check if errors deeper in CQ are reported too editor.set_text(code_err3) run.triggered.emit() @@ -570,6 +686,7 @@ def test_traceback(main): assert('Standard_DomainError' in traceback_view.current_exception.text()) assert(traceback_view.tree.root.childCount() == 3) # 1 in user code + 2 in CQ code +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b @pytest.fixture def editor(qtbot): @@ -581,7 +698,11 @@ def editor(qtbot): return qtbot, win def conv_line_ends(text): +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b return '\n'.join(text.splitlines()) def test_editor(monkeypatch,editor): @@ -708,9 +829,15 @@ def test_editor_autoreload(monkeypatch,editor): # Saving a file with autoreload enabled should trigger a rerender. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): editor.save() +<<<<<<< HEAD + +def test_autoreload_nested(editor): + +======= def test_autoreload_nested(editor): +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b qtbot, editor = editor TIMEOUT = 500 @@ -798,8 +925,13 @@ def approx_view_properties(eye,proj,scale): return pytest.approx(eye+proj+(scale,)) +<<<<<<< HEAD + qtbot, win = main_clean + +======= qtbot, win = main_clean +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b editor = win.components['editor'] debugger = win.components['debugger'] viewer = win.components['viewer'] @@ -901,7 +1033,11 @@ def test_selection(main_multi,mocker): ctx = viewer._get_context() ctx.InitSelected() shapes = [] +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b while ctx.MoreSelected(): shapes.append(ctx.SelectedShape()) ctx.NextSelected() @@ -974,7 +1110,11 @@ def test_screenshot(main,mocker): qtbot,win = main mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.png','')) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b viewer = win.components['viewer'] viewer._actions['Tools'][0].triggered.emit() @@ -982,9 +1122,15 @@ def test_screenshot(main,mocker): def test_resize(main): +<<<<<<< HEAD + qtbot,win = main + editor = win.components['editor'] + +======= qtbot,win = main editor = win.components['editor'] +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b editor.hide() qtbot.wait(50) editor.show() @@ -1050,9 +1196,15 @@ def test_render_colors(main_clean): editor.set_text(code_color) debugger._actions['Run'][0].triggered.emit() +<<<<<<< HEAD + + CQ = obj_tree.CQ + +======= CQ = obj_tree.CQ +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b # object 1 (defualt color) assert not CQ.child(0).ais.HasColor() @@ -1085,7 +1237,11 @@ def test_render_colors(main_clean): # check if error occured qtbot.wait(100) assert('Unknown color format' in log.toPlainText().splitlines()[-1]) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b def test_render_colors_console(main_clean): qtbot, win = main_clean @@ -1095,12 +1251,21 @@ def test_render_colors_console(main_clean): console = win.components['console'] console.execute_command(code_color) +<<<<<<< HEAD + + CQ = obj_tree.CQ + + # object 1 (defualt color) + assert not CQ.child(0).ais.HasColor() + +======= CQ = obj_tree.CQ # object 1 (defualt color) assert not CQ.child(0).ais.HasColor() +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b # object 2 r,g,b,a = get_rgba(CQ.child(1).ais) assert( a == 0.5 ) @@ -1125,7 +1290,11 @@ def test_render_colors_console(main_clean): r,g,b,a = get_rgba(CQ.child(5).ais) assert( a == 0.5 ) assert( r == 1.0 ) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b # check if error occured qtbot.wait(100) assert('Unknown color format' in log.toPlainText().splitlines()[-1]) @@ -1141,7 +1310,11 @@ def test_render_colors_console(main_clean): ''' def test_shading_aspect(main_clean): +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b qtbot, win = main_clean obj_tree = win.components['object_tree'] @@ -1169,7 +1342,11 @@ def test_confirm_new(monkeypatch,editor): editor.document().setPlainText(code) assert(editor.modified == True) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b #monkeypatch the confirmation dialog and run both scenarios def cancel(*args, **kwargs): return QMessageBox.No @@ -1179,6 +1356,20 @@ def ok(*args, **kwargs): monkeypatch.setattr(QMessageBox, 'question', staticmethod(cancel)) +<<<<<<< HEAD + + editor.new() + assert(editor.modified == True) + assert(conv_line_ends(editor.get_text_with_eol()) == code) + + monkeypatch.setattr(QMessageBox, 'question', + staticmethod(ok)) + + editor.new() + assert(editor.modified == False) + assert(editor.get_text_with_eol() == '') + +======= editor.new() assert(editor.modified == True) @@ -1191,6 +1382,7 @@ def ok(*args, **kwargs): assert(editor.modified == False) assert(editor.get_text_with_eol() == '') +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b code_show_topods = \ ''' import cadquery as cq @@ -1219,6 +1411,18 @@ def test_render_topods(main): editor.set_text(code_show_topods) debugger._actions['Run'][0].triggered.emit() assert(obj_tree_comp.CQ.childCount() == 1) +<<<<<<< HEAD + + # test rendering of topods object via console + console.execute('show(result.val().wrapped)') + assert(obj_tree_comp.CQ.childCount() == 2) + + # test rendering of list of topods object via console + console.execute('show([result.val().wrapped,result.val().wrapped])') + assert(obj_tree_comp.CQ.childCount() == 3) + + +======= # test rendering of topods object via console console.execute('show(result.val().wrapped)') @@ -1229,6 +1433,7 @@ def test_render_topods(main): assert(obj_tree_comp.CQ.childCount() == 3) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b code_show_shape_list = \ ''' import cadquery as cq @@ -1242,7 +1447,11 @@ def test_render_topods(main): def test_render_shape_list(main): qtbot, win = main +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b log = win.components['log'] obj_tree_comp = win.components['object_tree'] @@ -1258,7 +1467,11 @@ def test_render_shape_list(main): editor.set_text(code_show_shape_list) debugger._actions['Run'][0].triggered.emit() assert(obj_tree_comp.CQ.childCount() == 2) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b # test rendering of Shape via console console.execute('show(result1)') console.execute('show([result1,result2])') @@ -1337,7 +1550,11 @@ def test_render_ais(main): console.execute('show(ais)') qtbot.wait(500) assert(obj_tree_comp.CQ.childCount() == 2) +<<<<<<< HEAD + +======= +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b code_show_sketch = \ '''import cadquery as cq @@ -1500,6 +1717,8 @@ def test_modulefinder(tmp_path, main): qtbot.wait(100) assert("Cannot determine imported modules" in log.toPlainText().splitlines()[-1]) +<<<<<<< HEAD +======= def test_show_all(main): @@ -1582,3 +1801,4 @@ def test_show_without_name(main): # Check that the name of the seconf object is an int int(object_tree.CQ.child(1).text(0)) +>>>>>>> d746c8e8e6d80f53dc931ddc4910d4c791d7218b