├── .coveragerc ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── appveyor.yml ├── azure-pipelines.yml ├── bundle.py ├── collect_icons.py ├── conda └── meta.yaml ├── cq_editor ├── __init__.py ├── __main__.py ├── _version.py ├── cq_utils.py ├── icons.py ├── icons_res.py ├── main_window.py ├── mixins.py ├── preferences.py ├── utils.py └── widgets │ ├── __init__.py │ ├── console.py │ ├── cq_object_inspector.py │ ├── debugger.py │ ├── editor.py │ ├── log.py │ ├── object_tree.py │ ├── occt_widget.py │ ├── traceback_viewer.py │ └── viewer.py ├── environment.yaml ├── icons ├── back_view.svg ├── bottom_view.svg ├── cadquery_logo_dark.ico ├── cadquery_logo_dark.svg ├── front_view.svg ├── isometric_view.svg ├── left_side_view.svg ├── right_side_view.svg └── top_view.svg ├── lattice_scripts ├── BCC_heterogeneous_lattice.py ├── FBCC_heterogeneous_lattice.py ├── FCC_heterogeneous_lattice.py ├── adaptive_Ls.py ├── changing_cross_section.py ├── conform-surface.py ├── cubic.py ├── diamond.py ├── gyroid.py ├── hetero-shwarz.py ├── heterogeneous_gyroid.py ├── heterogeneous_schwartz.py ├── homogeneous_lattice.py ├── lego_brick.py ├── martensite.py ├── rco.py ├── reverse_gyroid.py ├── schwartz-d.py ├── simple_cubic.py ├── support_plate.py ├── t_cubic.py ├── tco.py ├── tetra_lattice.py ├── tire.py ├── tpms_test.py ├── tpms_transition.py ├── unit_cell.py ├── varying_Ls.py ├── varying_truncation.py ├── voronoi.py └── wavy_circle.py ├── lq ├── commons.py └── topologies │ ├── __init__.py │ ├── bcc.py │ ├── bcc_old.py │ ├── conformal.py │ ├── cubic.py │ ├── diamond.py │ ├── fbcc.py │ ├── fcc.py │ ├── gyroid.py │ ├── martensite.py │ ├── rco.py │ ├── schwartz.py │ ├── tco.py │ ├── tcubic.py │ └── tpms_transition.py ├── lqgui_env.yml ├── pyinstaller.spec ├── pyinstaller ├── pyi_rth_fontconfig.py └── pyi_rth_occ.py ├── pytest.ini ├── run.py ├── runtests_locally.sh ├── screenshots ├── hetero-schwartz.png ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png └── screenshot4.png ├── setup.py ├── tests └── test_app.py └── topologies ├── BCC └── unit_cell.py └── fblgen_helper.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | timid = True 3 | branch = True 4 | source = src 5 | 6 | [report] 7 | exclude_lines = 8 | if __name__ == .__main__.: 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # macOS generated files 107 | .DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "docwriter.style": "Google" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LatticeQuery - An open-source software for modeling of lattice structures 2 | 3 | [![DOI](https://zenodo.org/badge/291864023.svg)](https://zenodo.org/badge/latestdoi/291864023) 4 | 5 | The tool allows modeling of heterogeneous lattice structures, both beam-based and surface-based. The tool is based on the [CadQuery GUI](https://github.com/CadQuery/CQ-editor.git) editor which allows parametric modeling with [OpenCASCADE](https://www.opencascade.com/) and PyQT and supports Linux, Windows and MacOS. 6 | 7 | For the CadQuery documentation, please address [its repository](https://github.com/CadQuery/cadquery) and [official documentation](https://cadquery.readthedocs.io/en/latest/). 8 | 9 | For the description of the methodology, the [corresponding research paper](https://doi.org/10.1093/jcde/qwac076) is suggested. 10 | 11 | ## Installation 12 | 13 | The installation of this tool requires [Anaconda](https://www.anaconda.com/) installed. Once installed, you can create a virtual conda environment as follows: 14 | ```bash 15 | conda env create -f lqgui_env.yml -n lq 16 | conda activate lq 17 | ``` 18 | 19 | You can also simply use the binary versions in the [latest release](https://github.com/jalovisko/LatticeQuery/releases/latest). 20 | 21 | ## Usage 22 | Most of the functionality is located in the `lq` folder (stands for 'LatticeQuery'). Within the installed virtual environment, run the main Python script as follows: 23 | ```bash 24 | python run.py 25 | ``` 26 | 27 | The topologies that are implemented include: 28 | * Beam-based 29 | * Simple cubic 30 | * BCC 31 | * FCC 32 | * S-FCC 33 | * BCCz 34 | * FCCz 35 | * S-FCCz 36 | * FBCC 37 | * S-FBCC 38 | * S-FBCCz 39 | * Diamond 40 | * Rhombicuboctahedron 41 | * Truncated cube 42 | * TPMS 43 | * Gyroid 44 | * Schwarz 'Primitive' (P) 45 | * Schwarz 'Diamond' (D) 46 | 47 | For example, modeling of a heterogeneous Schwarz P lattice with the thickness linearly changing from 0.1 to 7 is possible as follows 48 | ```python 49 | # Python 50 | import cadquery as cq 51 | from lq.topologies.schwartz import schwartz_p_heterogeneous_lattice 52 | cq.Workplane.schwartz_p_heterogeneous_lattice = schwartz_p_heterogeneous_lattice 53 | 54 | # BEGIN USER INPUT 55 | unit_cell_size = 4 56 | Nx = 10 57 | Ny = 10 58 | Nz = 10 59 | min_thickness = 0.1 60 | max_thickness = 7 61 | # END USER INPUT 62 | 63 | schwartz = schwartz_p_heterogeneous_lattice(unit_cell_size, min_thickness, max_thickness, 64 | Nx, Ny, Nz 65 | ) 66 | ``` 67 | As you can see, a single function handles requires geometric arguments and handles all the modeling. The result is the following: 68 | ![Heterogeneous Schwartz P lattice](/screenshots/hetero-schwartz.png) 69 | 70 | ## Other examples 71 | This and many more examples of the implementation are located in the `lattce_scripts` directory. 72 | An example is a Python script that can be imported from within the editor (the window you see when running `run.py`). 73 | These examples include the geometric modeling of: 74 | * A homogeneous gyroid lattice (`gyroid.py`) 75 | * A conformal heterogeneous lattice filling a cylindrical tube (`tire.py`) 76 | * A heterogeneous FCC lattice with the linearly changing beam thickness (`FCC_heterogeneous_lattice.py`) 77 | * A heterogeneous BCC lattice with the beam thickness changing according to the parabolic distribution (`BCC_heterogeneous_lattice.py`) 78 | * A heterogeneous FCC lattice with the beam cross-section gradually changing from square to circle (`changing_cross_section.py`) 79 | * A heterogeneous diamond lattice with the linearly changing beam thickness (`diamond.py`) 80 | * A heterogeneous FBCC lattice with the linearly changing beam thickness (`FBCC_heterogeneous_lattice.py`) 81 | * A heterogeneous gyroid lattice with the linearly changing thickness (`heterogeneous_gyroid.py`) 82 | * A heterogeneous Schwarz D and P lattices with the linearly changing thickness (`heterogeneous_schwartz.py`) 83 | * A heterogeneous Schwarz D lattice with the thickness changing according to the periodic sine distribution (`schwartz-d.py`) 84 | 85 | 86 | ## Known issues 87 | Sometimes the modeling would fail with an error like `Brep: command not done`. This is often solved by passing a float argument to the function rather than an integer one. You can also try to increase the unit cell size, let's say, 10 times, and then scale it down 10 times. 88 | 89 | The connection between some of the TPMS based topologies seems abrupt and has gaps in some cases. This effect should be investigated further. 90 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | shallow_clone: false 2 | 3 | image: 4 | # - macOS-mojave 5 | # - macOS 6 | - Ubuntu 7 | - Ubuntu1804 8 | - Visual Studio 2015 9 | 10 | environment: 11 | matrix: 12 | - PYTEST_QT_API: pyqt5 13 | CODECOV_TOKEN: 14 | secure: ZggK9wgDeFdTp0pu0MEV+SY4i/i1Ls0xrEC2MxSQOQ0JQV+TkpzJJzI4au7L8TpD 15 | MINICONDA_DIRNAME: C:\FreshMiniconda 16 | 17 | install: 18 | - 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 19 | - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then curl -o miniconda.sh curl -o miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh; fi 20 | - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then curl -o miniconda.sh curl -o miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-4.7.10-MacOSX-x86_64.sh; fi 21 | - sh: bash miniconda.sh -b -p $HOME/miniconda 22 | - sh: source $HOME/miniconda/bin/activate 23 | - cmd: appveyor DownloadFile https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe 24 | - cmd: Miniconda3-latest-Windows-x86_64.exe /S /InstallationType=JustMe /D=%MINICONDA_DIRNAME% 25 | - cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" 26 | - cmd: activate 27 | - conda config --set always_yes yes 28 | - conda install -c conda-forge python=3.7 29 | - conda info 30 | - conda env create --name cqgui -f cqgui_env.yml 31 | - sh: source activate cqgui 32 | - cmd: activate cqgui 33 | - conda list 34 | - pip install pytest pluggy pytest-qt 35 | - pip install pytest-mock pytest-cov pytest-repeat codecov pyvirtualdisplay==0.2.1 36 | 37 | build: false 38 | 39 | before_test: 40 | - sh: ulimit -c unlimited -S 41 | - sh: sudo rm -f /cores/core.* 42 | 43 | test_script: 44 | - sh: export PYTHONPATH=$(pwd) 45 | - cmd: set PYTHONPATH=%cd% 46 | - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then xvfb-run -s '-screen 0 1920x1080x24 +iglx' pytest -v --cov=cq_editor; fi 47 | - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then pytest -v --cov=cq_editor; fi 48 | - cmd: pytest -v --cov=cq_editor 49 | 50 | on_success: 51 | - codecov 52 | 53 | #on_failure: 54 | # - qtdiag 55 | # - ls /cores/core.* 56 | # - lldb --core `ls /cores/core.*` --batch --one-line "bt" 57 | 58 | on_finish: 59 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 60 | # - sh: export APPVEYOR_SSH_BLOCK=true 61 | # - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - 62 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - master 5 | - refs/tags/* 6 | 7 | pr: 8 | - master 9 | 10 | resources: 11 | repositories: 12 | - repository: templates 13 | type: github 14 | name: CadQuery/conda-packages 15 | endpoint: CadQuery 16 | 17 | jobs: 18 | - template: conda-build.yml@templates 19 | parameters: 20 | name: Linux 21 | vmImage: 'ubuntu-16.04' 22 | py_maj: 3 23 | py_min: 8 24 | conda_bld: '3.20.3' 25 | 26 | - template: conda-build.yml@templates 27 | parameters: 28 | name: macOS 29 | vmImage: 'macOS-10.15' 30 | py_maj: 3 31 | py_min: 8 32 | conda_bld: '3.20.3' 33 | 34 | - template: conda-build.yml@templates 35 | parameters: 36 | name: Windows 37 | vmImage: 'vs2017-win2016' 38 | py_maj: 3 39 | py_min: 8 40 | conda_bld: '3.20.3' 41 | -------------------------------------------------------------------------------- /bundle.py: -------------------------------------------------------------------------------- 1 | from sys import platform 2 | from path import Path 3 | from os import system 4 | from shutil import make_archive 5 | from cq_editor import __version__ as version 6 | 7 | out_p = Path('dist/CQ-editor') 8 | out_p.rmtree_p() 9 | 10 | build_p = Path('build') 11 | build_p.rmtree_p() 12 | 13 | system("pyinstaller pyinstaller.spec") 14 | 15 | if platform == 'linux': 16 | with out_p: 17 | p = Path('.').glob('libpython*')[0] 18 | p.symlink(p.split(".so")[0]+".so") 19 | 20 | make_archive(f'CQ-editor-{version}-linux64','bztar', out_p / '..', 'CQ-editor') 21 | 22 | elif platform == 'win32': 23 | 24 | make_archive(f'CQ-editor-{version}-win64','zip', out_p / '..', 'CQ-editor') 25 | -------------------------------------------------------------------------------- /collect_icons.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | from subprocess import call 3 | from os import remove 4 | 5 | TEMPLATE = \ 6 | ''' 7 | 8 | {} 9 | 10 | ''' 11 | 12 | ITEM_TEMPLATE = '{}' 13 | 14 | QRC_OUT = 'icons.qrc' 15 | RES_OUT = 'src/icons_res.py' 16 | TOOL = 'pyrcc5' 17 | 18 | items = [] 19 | 20 | for i in glob('icons/*.svg'): 21 | items.append(ITEM_TEMPLATE.format(i)) 22 | 23 | 24 | qrc_text = TEMPLATE.format('\n'.join(items)) 25 | 26 | with open(QRC_OUT,'w') as f: 27 | f.write(qrc_text) 28 | 29 | call([TOOL,QRC_OUT,'-o',RES_OUT]) 30 | remove(QRC_OUT) 31 | -------------------------------------------------------------------------------- /conda/meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: cq-editor 3 | version: {{ environ.get('PACKAGE_VERSION') }} 4 | 5 | source: 6 | path: .. 7 | 8 | build: 9 | string: {{ 'py'+environ.get('PYTHON_VERSION')}} 10 | script: python setup.py install --single-version-externally-managed --record=record.txt 11 | entry_points: 12 | - cq-editor = cq_editor.__main__:main 13 | - CQ-editor = cq_editor.__main__:main 14 | requirements: 15 | build: 16 | - python {{ environ.get('PYTHON_VERSION') }} 17 | - setuptools 18 | 19 | run: 20 | - python {{ environ.get('PYTHON_VERSION') }} 21 | - cadquery=master 22 | - ocp 23 | - logbook 24 | - pyqt=5.* 25 | - pyqtgraph 26 | - spyder=4.* 27 | - path.py 28 | - logbook 29 | - requests 30 | 31 | test: 32 | imports: 33 | - cq_editor 34 | 35 | about: 36 | summary: GUI for CadQuery 2.0 37 | -------------------------------------------------------------------------------- /cq_editor/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | -------------------------------------------------------------------------------- /cq_editor/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | 4 | from PyQt5.QtWidgets import QApplication 5 | 6 | NAME = 'FBLGen' 7 | 8 | #need to initialize QApp here, otherewise svg icons do not work on windows 9 | app = QApplication(sys.argv, 10 | applicationName=NAME) 11 | 12 | from .main_window import MainWindow 13 | 14 | def main(): 15 | 16 | win = MainWindow() 17 | 18 | parser = argparse.ArgumentParser(description=NAME) 19 | parser.add_argument('filename',nargs='?',default=None) 20 | 21 | args = parser.parse_args(app.arguments()[1:]) 22 | print(args) 23 | if args.filename: 24 | win.components['editor'].load_from_file(args.filename) 25 | 26 | win.show() 27 | sys.exit(app.exec_()) 28 | 29 | 30 | if __name__ == "__main__": 31 | 32 | main() 33 | -------------------------------------------------------------------------------- /cq_editor/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.1" 2 | -------------------------------------------------------------------------------- /cq_editor/cq_utils.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | from cadquery.occ_impl.assembly import toCAF 3 | 4 | from typing import List, Union, Tuple 5 | from imp import reload 6 | from types import SimpleNamespace 7 | 8 | from OCP.XCAFPrs import XCAFPrs_AISObject 9 | from OCP.TopoDS import TopoDS_Shape 10 | from OCP.AIS import AIS_ColoredShape 11 | from OCP.Quantity import \ 12 | Quantity_TOC_RGB as TOC_RGB, Quantity_Color 13 | 14 | from PyQt5.QtGui import QColor 15 | 16 | def find_cq_objects(results : dict): 17 | 18 | return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if isinstance(v,cq.Workplane)} 19 | 20 | def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape]]): 21 | 22 | vals = [] 23 | 24 | if isinstance(obj,cq.Workplane): 25 | vals.extend(obj.vals()) 26 | elif isinstance(obj,cq.Shape): 27 | vals.append(obj) 28 | elif isinstance(obj,list) and isinstance(obj[0],cq.Workplane): 29 | for o in obj: vals.extend(o.vals()) 30 | elif isinstance(obj,list) and isinstance(obj[0],cq.Shape): 31 | vals.extend(obj) 32 | elif isinstance(obj, TopoDS_Shape): 33 | vals.append(cq.Shape.cast(obj)) 34 | elif isinstance(obj,list) and isinstance(obj[0],TopoDS_Shape): 35 | vals.extend(cq.Shape.cast(o) for o in obj) 36 | else: 37 | raise ValueError(f'Invalid type {type(obj)}') 38 | 39 | return cq.Compound.makeCompound(vals) 40 | 41 | def to_workplane(obj : cq.Shape): 42 | 43 | rv = cq.Workplane('XY') 44 | rv.objects = [obj,] 45 | 46 | return rv 47 | 48 | def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly], 49 | options={}): 50 | 51 | if isinstance(obj, cq.Assembly): 52 | ais = XCAFPrs_AISObject(toCAF(obj)[0]) 53 | shape = None#cq.Shape(ais.Shape()) 54 | else: 55 | shape = to_compound(obj) 56 | ais = AIS_ColoredShape(shape.wrapped) 57 | 58 | if 'alpha' in options: 59 | ais.SetTransparency(options['alpha']) 60 | if 'color' in options: 61 | ais.SetColor(to_occ_color(options['color'])) 62 | if 'rgba' in options: 63 | r,g,b,a = options['rgba'] 64 | ais.SetColor(to_occ_color((r,g,b))) 65 | ais.SetTransparency(a) 66 | 67 | return ais,shape 68 | 69 | def export(obj : Union[cq.Workplane, List[cq.Workplane]], type : str, 70 | file, precision=1e-1): 71 | 72 | comp = to_compound(obj) 73 | 74 | if type == 'stl': 75 | comp.exportStl(file, tolerance=precision) 76 | elif type == 'step': 77 | comp.exportStep(file) 78 | elif type == 'brep': 79 | comp.exportBrep(file) 80 | 81 | def to_occ_color(color) -> Quantity_Color: 82 | 83 | if not isinstance(color, QColor): 84 | if isinstance(color, tuple): 85 | if isinstance(color[0], int): 86 | color = QColor(*color) 87 | elif isinstance(color[0], float): 88 | color = QColor.fromRgbF(*color) 89 | else: 90 | raise ValueError('Unknown color format') 91 | else: 92 | color = QColor(color) 93 | 94 | return Quantity_Color(color.redF(), 95 | color.greenF(), 96 | color.blueF(), 97 | TOC_RGB) 98 | 99 | def get_occ_color(ais : AIS_ColoredShape) -> QColor: 100 | 101 | color = Quantity_Color() 102 | ais.Color(color) 103 | 104 | return QColor.fromRgbF(color.Red(), color.Green(), color.Blue()) 105 | 106 | def reload_cq(): 107 | 108 | # NB: order of reloads is important 109 | reload(cq.occ_impl.geom) 110 | reload(cq.occ_impl.shapes) 111 | reload(cq.occ_impl.importers) 112 | reload(cq.occ_impl.solver) 113 | reload(cq.occ_impl.assembly) 114 | reload(cq.selectors) 115 | reload(cq.occ_impl.exporters.svg) 116 | reload(cq.cq) 117 | reload(cq.occ_impl.exporters.utils) 118 | reload(cq.occ_impl.exporters.dxf) 119 | reload(cq.occ_impl.exporters.amf) 120 | reload(cq.occ_impl.exporters.json) 121 | #reload(cq.occ_impl.exporters.assembly) 122 | reload(cq.occ_impl.exporters) 123 | reload(cq.assembly) 124 | reload(cq) 125 | 126 | 127 | def is_obj_empty(obj : Union[cq.Workplane,cq.Shape]) -> bool: 128 | 129 | rv = False 130 | 131 | if isinstance(obj, cq.Workplane): 132 | rv = True if isinstance(obj.val(), cq.Vector) else False 133 | 134 | return rv 135 | -------------------------------------------------------------------------------- /cq_editor/icons.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Fri May 25 14:47:10 2018 5 | 6 | @author: adam 7 | """ 8 | 9 | from PyQt5.QtGui import QIcon 10 | 11 | from . import icons_res 12 | _icons = { 13 | 'app' : QIcon(":/images/icons/cadquery_logo_dark.svg") 14 | } 15 | 16 | import qtawesome as qta 17 | 18 | _icons_specs = { 19 | 'new' : (('fa.file-o',),{}), 20 | 'open' : (('fa.folder-open-o',),{}), 21 | # borrowed from spider-ide 22 | 'autoreload': [('fa.repeat', 'fa.clock-o'), {'options': [{'scale_factor': 0.75, 'offset': (-0.1, -0.1)}, {'scale_factor': 0.5, 'offset': (0.25, 0.25)}]}], 23 | 'save' : (('fa.save',),{}), 24 | 'save_as': (('fa.save','fa.pencil'), 25 | {'options':[{'scale_factor': 1,}, 26 | {'scale_factor': 0.8, 27 | 'offset': (0.2, 0.2)}]}), 28 | 'run' : (('fa.play',),{}), 29 | 'delete' : (('fa.trash',),{}), 30 | 'delete-many' : (('fa.trash','fa.trash',), 31 | {'options' : \ 32 | [{'scale_factor': 0.8, 33 | 'offset': (0.2, 0.2), 34 | 'color': 'gray'}, 35 | {'scale_factor': 0.8}]}), 36 | 'help' : (('fa.life-ring',),{}), 37 | 'about': (('fa.info',),{}), 38 | 'preferences' : (('fa.cogs',),{}), 39 | 'inspect' : (('fa.cubes','fa.search'), 40 | {'options' : \ 41 | [{'scale_factor': 0.8, 42 | 'offset': (0,0), 43 | 'color': 'gray'},{}]}), 44 | 'screenshot' : (('fa.camera',),{}), 45 | 'screenshot-save' : (('fa.save','fa.camera'), 46 | {'options' : \ 47 | [{'scale_factor': 0.8}, 48 | {'scale_factor': 0.8, 49 | 'offset': (.2,.2)}]}) 50 | } 51 | 52 | def icon(name): 53 | 54 | if name in _icons: 55 | return _icons[name] 56 | 57 | args,kwargs = _icons_specs[name] 58 | 59 | return qta.icon(*args,**kwargs) -------------------------------------------------------------------------------- /cq_editor/mixins.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Wed May 23 22:02:30 2018 5 | 6 | @author: adam 7 | """ 8 | 9 | from functools import reduce 10 | from operator import add 11 | from logbook import Logger 12 | 13 | from PyQt5.QtCore import pyqtSlot, QSettings 14 | 15 | class MainMixin(object): 16 | 17 | name = 'Main' 18 | org = 'Unknown' 19 | 20 | components = {} 21 | docks = {} 22 | preferences = None 23 | 24 | def __init__(self): 25 | 26 | self.settings = QSettings(self.org,self.name) 27 | 28 | def registerComponent(self,name,component,dock=None): 29 | 30 | self.components[name] = component 31 | 32 | if dock: 33 | self.docks[name] = dock(component) 34 | 35 | def saveWindow(self): 36 | 37 | self.settings.setValue('geometry',self.saveGeometry()) 38 | self.settings.setValue('windowState',self.saveState()) 39 | 40 | def restoreWindow(self): 41 | 42 | if self.settings.value('geometry'): 43 | self.restoreGeometry(self.settings.value('geometry')) 44 | if self.settings.value('windowState'): 45 | self.restoreState(self.settings.value('windowState')) 46 | 47 | def savePreferences(self): 48 | 49 | settings = self.settings 50 | 51 | if self.preferences: 52 | settings.setValue('General',self.preferences.saveState()) 53 | 54 | for comp in (c for c in self.components.values() if c.preferences): 55 | settings.setValue(comp.name,comp.preferences.saveState()) 56 | 57 | def restorePreferences(self): 58 | 59 | settings = self.settings 60 | 61 | if self.preferences and settings.value('General'): 62 | self.preferences.restoreState(settings.value('General'), 63 | removeChildren=False) 64 | 65 | for comp in (c for c in self.components.values() if c.preferences): 66 | if settings.value(comp.name): 67 | comp.preferences.restoreState(settings.value(comp.name), 68 | removeChildren=False) 69 | 70 | def saveComponentState(self): 71 | 72 | settings = self.settings 73 | 74 | for comp in self.components.values(): 75 | comp.saveComponentState(settings) 76 | 77 | def restoreComponentState(self): 78 | 79 | settings = self.settings 80 | 81 | for comp in self.components.values(): 82 | comp.restoreComponentState(settings) 83 | 84 | 85 | class ComponentMixin(object): 86 | 87 | 88 | name = 'Component' 89 | preferences = None 90 | 91 | _actions = {} 92 | 93 | 94 | def __init__(self): 95 | 96 | if self.preferences: 97 | self.preferences.sigTreeStateChanged.\ 98 | connect(self.updatePreferences) 99 | 100 | self._logger = Logger(self.name) 101 | 102 | def menuActions(self): 103 | 104 | return self._actions 105 | 106 | def toolbarActions(self): 107 | 108 | if len(self._actions) > 0: 109 | return reduce(add,[a for a in self._actions.values()]) 110 | else: 111 | return [] 112 | 113 | @pyqtSlot(object,object) 114 | def updatePreferences(self,*args): 115 | 116 | pass 117 | 118 | def saveComponentState(self,store): 119 | 120 | pass 121 | 122 | def restoreComponentState(self,store): 123 | 124 | pass -------------------------------------------------------------------------------- /cq_editor/preferences.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, 2 | QStackedWidget, QDialog) 3 | from PyQt5.QtCore import pyqtSlot, Qt 4 | 5 | from pyqtgraph.parametertree import ParameterTree 6 | 7 | from .utils import splitter, layout 8 | 9 | 10 | class PreferencesTreeItem(QTreeWidgetItem): 11 | 12 | def __init__(self,name,widget,): 13 | 14 | super(PreferencesTreeItem,self).__init__(name) 15 | self.widget = widget 16 | 17 | class PreferencesWidget(QDialog): 18 | 19 | def __init__(self,parent,components): 20 | 21 | super(PreferencesWidget,self).__init__( 22 | parent, 23 | Qt.Window | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint, 24 | windowTitle='Preferences') 25 | 26 | self.stacked = QStackedWidget(self) 27 | self.preferences_tree = QTreeWidget(self, 28 | headerHidden=True, 29 | itemsExpandable=False, 30 | rootIsDecorated=False, 31 | columnCount=1) 32 | 33 | self.root = self.preferences_tree.invisibleRootItem() 34 | 35 | self.add('General', 36 | parent) 37 | 38 | for v in parent.components.values(): 39 | self.add(v.name,v) 40 | 41 | self.splitter = splitter((self.preferences_tree,self.stacked),(2,5)) 42 | layout(self,(self.splitter,),self) 43 | 44 | self.preferences_tree.currentItemChanged.connect(self.handleSelection) 45 | 46 | def add(self,name,component): 47 | 48 | if component.preferences: 49 | widget = ParameterTree() 50 | widget.setHeaderHidden(True) 51 | widget.setParameters(component.preferences,showTop=False) 52 | self.root.addChild(PreferencesTreeItem((name,), 53 | widget)) 54 | 55 | self.stacked.addWidget(widget) 56 | 57 | @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem) 58 | def handleSelection(self,item,*args): 59 | 60 | if item: 61 | self.stacked.setCurrentWidget(item.widget) 62 | 63 | -------------------------------------------------------------------------------- /cq_editor/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from pkg_resources import parse_version 4 | 5 | from PyQt5 import QtCore, QtWidgets 6 | from PyQt5.QtGui import QDesktopServices 7 | from PyQt5.QtCore import QUrl 8 | from PyQt5.QtWidgets import QFileDialog, QMessageBox 9 | 10 | DOCK_POSITIONS = {'right' : QtCore.Qt.RightDockWidgetArea, 11 | 'left' : QtCore.Qt.LeftDockWidgetArea, 12 | 'top' : QtCore.Qt.TopDockWidgetArea, 13 | 'bottom' : QtCore.Qt.BottomDockWidgetArea} 14 | 15 | def layout(parent,items, 16 | top_widget = None, 17 | layout_type = QtWidgets.QVBoxLayout, 18 | margin = 2, 19 | spacing = 0): 20 | 21 | if not top_widget: 22 | top_widget = QtWidgets.QWidget(parent) 23 | top_widget_was_none = True 24 | else: 25 | top_widget_was_none = False 26 | layout = layout_type(top_widget) 27 | top_widget.setLayout(layout) 28 | 29 | for item in items: layout.addWidget(item) 30 | 31 | layout.setSpacing(spacing) 32 | layout.setContentsMargins(margin,margin,margin,margin) 33 | 34 | if top_widget_was_none: 35 | return top_widget 36 | else: 37 | return layout 38 | 39 | def splitter(items, 40 | stretch_factors = None, 41 | orientation=QtCore.Qt.Horizontal): 42 | 43 | sp = QtWidgets.QSplitter(orientation) 44 | 45 | for item in items: sp.addWidget(item) 46 | 47 | if stretch_factors: 48 | for i,s in enumerate(stretch_factors): 49 | sp.setStretchFactor(i,s) 50 | 51 | 52 | return sp 53 | 54 | def dock(widget, 55 | title, 56 | parent, 57 | allowedAreas = QtCore.Qt.AllDockWidgetAreas, 58 | defaultArea = 'right', 59 | name=None, 60 | icon = None): 61 | 62 | dock = QtWidgets.QDockWidget(title,parent,objectName=title) 63 | 64 | if name: dock.setObjectName(name) 65 | if icon: dock.toggleViewAction().setIcon(icon) 66 | 67 | dock.setAllowedAreas(allowedAreas) 68 | dock.setWidget(widget) 69 | action = dock.toggleViewAction() 70 | action.setText(title) 71 | 72 | dock.setFeatures(QtWidgets.QDockWidget.DockWidgetFeatures(\ 73 | QtWidgets.QDockWidget.AllDockWidgetFeatures)) 74 | 75 | parent.addDockWidget(DOCK_POSITIONS[defaultArea], 76 | dock) 77 | 78 | return dock 79 | 80 | def add_actions(menu,actions): 81 | 82 | if len(actions) > 0: 83 | menu.addActions(actions) 84 | menu.addSeparator() 85 | 86 | def open_url(url): 87 | 88 | QDesktopServices.openUrl(QUrl(url)) 89 | 90 | def about_dialog(parent,title,text): 91 | 92 | QtWidgets.QMessageBox.about(parent,title,text) 93 | 94 | def get_save_filename(suffix): 95 | 96 | rv,_ = QFileDialog.getSaveFileName(filter='*.{}'.format(suffix)) 97 | if rv != '' and not rv.endswith(suffix): rv += '.'+suffix 98 | 99 | return rv 100 | 101 | def get_open_filename(suffix, curr_dir): 102 | 103 | rv,_ = QFileDialog.getOpenFileName(directory=curr_dir, filter='*.{}'.format(suffix)) 104 | if rv != '' and not rv.endswith(suffix): rv += '.'+suffix 105 | 106 | return rv 107 | 108 | def check_gtihub_for_updates(parent, 109 | mod, 110 | github_org='cadquery', 111 | github_proj='cadquery'): 112 | 113 | url = f'https://api.github.com/repos/{github_org}/{github_proj}/releases' 114 | resp = requests.get(url).json() 115 | 116 | newer = [el['tag_name'] for el in resp if not el['draft'] and \ 117 | parse_version(el['tag_name']) > parse_version(mod.__version__)] 118 | 119 | if newer: 120 | title='Updates available' 121 | text=f'There are newer versions of {github_proj} ' \ 122 | f'available on github:\n' + '\n'.join(newer) 123 | 124 | else: 125 | title='No updates available' 126 | text=f'You are already using the latest version of {github_proj}' 127 | 128 | QtWidgets.QMessageBox.about(parent,title,text) 129 | 130 | def confirm(parent,title,msg): 131 | 132 | rv = QMessageBox.question(parent, title, msg, QMessageBox.Yes, QMessageBox.No) 133 | 134 | return True if rv == QMessageBox.Yes else False 135 | -------------------------------------------------------------------------------- /cq_editor/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalovisko/LatticeQuery/7078d8cf11f658590c88e6e84e4c728ae2f8be9b/cq_editor/widgets/__init__.py -------------------------------------------------------------------------------- /cq_editor/widgets/console.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QApplication 2 | from PyQt5.QtCore import pyqtSlot 3 | 4 | from qtconsole.rich_jupyter_widget import RichJupyterWidget 5 | from qtconsole.inprocess import QtInProcessKernelManager 6 | 7 | from ..mixins import ComponentMixin 8 | 9 | class ConsoleWidget(RichJupyterWidget,ComponentMixin): 10 | 11 | name = 'Console' 12 | 13 | def __init__(self, customBanner=None, namespace=dict(), *args, **kwargs): 14 | super(ConsoleWidget, self).__init__(*args, **kwargs) 15 | 16 | # if not customBanner is None: 17 | # self.banner = customBanner 18 | 19 | self.font_size = 6 20 | self.kernel_manager = kernel_manager = QtInProcessKernelManager() 21 | kernel_manager.start_kernel(show_banner=False) 22 | kernel_manager.kernel.gui = 'qt' 23 | kernel_manager.kernel.shell.banner1 = "" 24 | 25 | self.kernel_client = kernel_client = self._kernel_manager.client() 26 | kernel_client.start_channels() 27 | 28 | def stop(): 29 | kernel_client.stop_channels() 30 | kernel_manager.shutdown_kernel() 31 | QApplication.instance().exit() 32 | 33 | self.exit_requested.connect(stop) 34 | 35 | self.clear() 36 | 37 | self.push_vars(namespace) 38 | 39 | @pyqtSlot(dict) 40 | def push_vars(self, variableDict): 41 | """ 42 | Given a dictionary containing name / value pairs, push those variables 43 | to the Jupyter console widget 44 | """ 45 | self.kernel_manager.kernel.shell.push(variableDict) 46 | 47 | def clear(self): 48 | """ 49 | Clears the terminal 50 | """ 51 | self._control.clear() 52 | 53 | 54 | def print_text(self, text): 55 | """ 56 | Prints some plain text to the console 57 | """ 58 | self._append_plain_text(text) 59 | 60 | def execute_command(self, command): 61 | """ 62 | Execute a command in the frame of the console widget 63 | """ 64 | self._execute(command, False) 65 | 66 | def _banner_default(self): 67 | 68 | return '' 69 | 70 | 71 | if __name__ == "__main__": 72 | 73 | 74 | import sys 75 | 76 | app = QApplication(sys.argv) 77 | 78 | console = ConsoleWidget(customBanner='IPython console test') 79 | console.show() 80 | 81 | sys.exit(app.exec_()) 82 | -------------------------------------------------------------------------------- /cq_editor/widgets/cq_object_inspector.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAction 2 | from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal 3 | 4 | from OCP.AIS import AIS_ColoredShape 5 | from OCP.gp import gp_Ax3 6 | 7 | from cadquery import Vector 8 | 9 | from ..mixins import ComponentMixin 10 | from ..icons import icon 11 | 12 | 13 | 14 | class CQChildItem(QTreeWidgetItem): 15 | 16 | def __init__(self,cq_item,**kwargs): 17 | 18 | super(CQChildItem,self).\ 19 | __init__([type(cq_item).__name__,str(cq_item)],**kwargs) 20 | 21 | self.cq_item = cq_item 22 | 23 | class CQStackItem(QTreeWidgetItem): 24 | 25 | def __init__(self,name,workplane=None,**kwargs): 26 | 27 | super(CQStackItem,self).__init__([name,''],**kwargs) 28 | 29 | self.workplane = workplane 30 | 31 | 32 | class CQObjectInspector(QTreeWidget,ComponentMixin): 33 | 34 | name = 'CQ Object Inspector' 35 | 36 | sigRemoveObjects = pyqtSignal(list) 37 | sigDisplayObjects = pyqtSignal(list,bool) 38 | sigShowPlane = pyqtSignal([bool],[bool,float]) 39 | sigChangePlane = pyqtSignal(gp_Ax3) 40 | 41 | def __init__(self,parent): 42 | 43 | super(CQObjectInspector,self).__init__(parent) 44 | self.setHeaderHidden(False) 45 | self.setRootIsDecorated(True) 46 | self.setContextMenuPolicy(Qt.ActionsContextMenu) 47 | self.setColumnCount(2) 48 | self.setHeaderLabels(['Type','Value']) 49 | 50 | self.root = self.invisibleRootItem() 51 | self.inspected_items = [] 52 | 53 | self._toolbar_actions = \ 54 | [QAction(icon('inspect'),'Inspect CQ object',self,\ 55 | toggled=self.inspect,checkable=True)] 56 | 57 | self.addActions(self._toolbar_actions) 58 | 59 | def menuActions(self): 60 | 61 | return {'Tools' : self._toolbar_actions} 62 | 63 | def toolbarActions(self): 64 | 65 | return self._toolbar_actions 66 | 67 | @pyqtSlot(bool) 68 | def inspect(self,value): 69 | 70 | if value: 71 | self.itemSelectionChanged.connect(self.handleSelection) 72 | self.itemSelectionChanged.emit() 73 | else: 74 | self.itemSelectionChanged.disconnect(self.handleSelection) 75 | self.sigRemoveObjects.emit(self.inspected_items) 76 | self.sigShowPlane.emit(False) 77 | 78 | @pyqtSlot() 79 | def handleSelection(self): 80 | 81 | inspected_items = self.inspected_items 82 | self.sigRemoveObjects.emit(inspected_items) 83 | inspected_items.clear() 84 | 85 | items = self.selectedItems() 86 | if len(items) == 0: 87 | return 88 | 89 | item = items[-1] 90 | if type(item) is CQStackItem: 91 | cq_plane = item.workplane.plane 92 | dim = item.workplane.largestDimension() 93 | plane = gp_Ax3(cq_plane.origin.toPnt(), 94 | cq_plane.zDir.toDir(), 95 | cq_plane.xDir.toDir()) 96 | self.sigChangePlane.emit(plane) 97 | self.sigShowPlane[bool,float].emit(True,dim) 98 | 99 | for child in (item.child(i) for i in range(item.childCount())): 100 | obj = child.cq_item 101 | if hasattr(obj,'wrapped') and type(obj) != Vector: 102 | ais = AIS_ColoredShape(obj.wrapped) 103 | inspected_items.append(ais) 104 | 105 | else: 106 | self.sigShowPlane.emit(False) 107 | obj = item.cq_item 108 | if hasattr(obj,'wrapped') and type(obj) != Vector: 109 | ais = AIS_ColoredShape(obj.wrapped) 110 | inspected_items.append(ais) 111 | 112 | self.sigDisplayObjects.emit(inspected_items,False) 113 | 114 | @pyqtSlot(object) 115 | def setObject(self,cq_obj): 116 | 117 | self.root.takeChildren() 118 | 119 | # iterate through parent objects if they exist 120 | while getattr(cq_obj, 'parent', None): 121 | current_frame = CQStackItem(str(cq_obj.plane.origin),workplane=cq_obj) 122 | self.root.addChild(current_frame) 123 | 124 | for obj in cq_obj.objects: 125 | current_frame.addChild(CQChildItem(obj)) 126 | 127 | cq_obj = cq_obj.parent 128 | 129 | -------------------------------------------------------------------------------- /cq_editor/widgets/debugger.py: -------------------------------------------------------------------------------- 1 | import sys, imp 2 | from enum import Enum, auto 3 | from imp import reload 4 | from types import SimpleNamespace 5 | 6 | from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction, 7 | QLabel, QTableView) 8 | from PyQt5.QtCore import Qt, QObject, pyqtSlot, pyqtSignal, QEventLoop, QAbstractTableModel 9 | from PyQt5 import QtCore 10 | 11 | from pyqtgraph.parametertree import Parameter, ParameterTree 12 | from logbook import info 13 | from spyder.utils.icon_manager import icon 14 | from path import Path 15 | from contextlib import ExitStack 16 | 17 | import cadquery as cq 18 | 19 | from ..mixins import ComponentMixin 20 | from ..utils import layout 21 | from ..cq_utils import find_cq_objects, reload_cq 22 | 23 | DUMMY_FILE = '' 24 | 25 | 26 | class DbgState(Enum): 27 | 28 | STEP = auto() 29 | CONT = auto() 30 | STEP_IN = auto() 31 | RETURN = auto() 32 | 33 | class DbgEevent(object): 34 | 35 | LINE = 'line' 36 | CALL = 'call' 37 | RETURN = 'return' 38 | 39 | class LocalsModel(QAbstractTableModel): 40 | 41 | HEADER = ('Name','Type', 'Value') 42 | 43 | def __init__(self,parent): 44 | 45 | super(LocalsModel,self).__init__(parent) 46 | self.frame = None 47 | 48 | def update_frame(self,frame): 49 | 50 | self.frame = \ 51 | [(k,type(v).__name__, str(v)) for k,v in frame.items() if not k.startswith('_')] 52 | 53 | 54 | def rowCount(self,parent=QtCore.QModelIndex()): 55 | 56 | if self.frame: 57 | return len(self.frame) 58 | else: 59 | return 0 60 | 61 | def columnCount(self,parent=QtCore.QModelIndex()): 62 | 63 | return 3 64 | 65 | def headerData(self, section, orientation, role=Qt.DisplayRole): 66 | if role == Qt.DisplayRole and orientation == Qt.Horizontal: 67 | return self.HEADER[section] 68 | return QAbstractTableModel.headerData(self, section, orientation, role) 69 | 70 | def data(self, index, role): 71 | if role == QtCore.Qt.DisplayRole: 72 | i = index.row() 73 | j = index.column() 74 | return self.frame[i][j] 75 | else: 76 | return QtCore.QVariant() 77 | 78 | 79 | class LocalsView(QTableView,ComponentMixin): 80 | 81 | name = 'Variables' 82 | 83 | def __init__(self,parent): 84 | 85 | super(LocalsView,self).__init__(parent) 86 | ComponentMixin.__init__(self) 87 | 88 | header = self.horizontalHeader() 89 | header.setStretchLastSection(True) 90 | 91 | vheader = self.verticalHeader() 92 | vheader.setVisible(False) 93 | 94 | @pyqtSlot(dict) 95 | def update_frame(self,frame): 96 | 97 | model = LocalsModel(self) 98 | model.update_frame(frame) 99 | 100 | self.setModel(model) 101 | 102 | class Debugger(QObject,ComponentMixin): 103 | 104 | name = 'Debugger' 105 | 106 | preferences = Parameter.create(name='Preferences',children=[ 107 | {'name': 'Reload CQ', 'type': 'bool', 'value': True}, 108 | {'name': 'Add script dir to path','type': 'bool', 'value': True}, 109 | {'name': 'Change working dir to script dir','type': 'bool', 'value': True}]) 110 | 111 | 112 | sigRendered = pyqtSignal(dict) 113 | sigLocals = pyqtSignal(dict) 114 | sigTraceback = pyqtSignal(object,str) 115 | 116 | sigFrameChanged = pyqtSignal(object) 117 | sigLineChanged = pyqtSignal(int) 118 | sigLocalsChanged = pyqtSignal(dict) 119 | sigCQChanged = pyqtSignal(dict,bool) 120 | sigDebugging = pyqtSignal(bool) 121 | 122 | 123 | def __init__(self,parent): 124 | 125 | super(Debugger,self).__init__(parent) 126 | ComponentMixin.__init__(self) 127 | 128 | self.inner_event_loop = QEventLoop(self) 129 | 130 | self._actions = \ 131 | {'Run' : [QAction(icon('run'), 132 | 'Render', 133 | self, 134 | shortcut='F5', 135 | triggered=self.render), 136 | QAction(icon('debug'), 137 | 'Debug', 138 | self, 139 | checkable=True, 140 | shortcut='ctrl+F5', 141 | triggered=self.debug), 142 | QAction(icon('arrow-step-over'), 143 | 'Step', 144 | self, 145 | shortcut='ctrl+F10', 146 | triggered=lambda: self.debug_cmd(DbgState.STEP)), 147 | QAction(icon('arrow-step-in'), 148 | 'Step in', 149 | self, 150 | shortcut='ctrl+F11', 151 | triggered=lambda: None), 152 | QAction(icon('arrow-continue'), 153 | 'Continue', 154 | self, 155 | shortcut='ctrl+F12', 156 | triggered=lambda: self.debug_cmd(DbgState.CONT)) 157 | ]} 158 | 159 | def get_current_script(self): 160 | 161 | return self.parent().components['editor'].get_text_with_eol() 162 | 163 | def get_breakpoints(self): 164 | 165 | return self.parent().components['editor'].debugger.get_breakpoints() 166 | 167 | def compile_code(self,cq_script): 168 | 169 | try: 170 | module = imp.new_module('temp') 171 | cq_code = compile(cq_script,'','exec') 172 | return cq_code,module 173 | except Exception: 174 | self.sigTraceback.emit(sys.exc_info(), 175 | cq_script) 176 | return None,None 177 | 178 | def _exec(self, code, locals_dict, globals_dict): 179 | 180 | with ExitStack() as stack: 181 | fname = self.parent().components['editor'].filename 182 | p = Path(fname if fname else '').abspath().dirname() 183 | 184 | if self.preferences['Add script dir to path'] and p.exists(): 185 | sys.path.insert(0,p) 186 | stack.callback(sys.path.remove, p) 187 | if self.preferences['Change working dir to script dir'] and p.exists(): 188 | stack.enter_context(p) 189 | 190 | exec(code, locals_dict, globals_dict) 191 | 192 | def _inject_locals(self,module): 193 | 194 | cq_objects = {} 195 | 196 | def _show_object(obj,name=None, options={}): 197 | 198 | if name: 199 | cq_objects.update({name : SimpleNamespace(shape=obj,options=options)}) 200 | else: 201 | cq_objects.update({str(id(obj)) : SimpleNamespace(shape=obj,options=options)}) 202 | 203 | def _debug(obj,name=None): 204 | 205 | _show_object(obj,name,options=dict(color='red',alpha=0.2)) 206 | 207 | module.__dict__['show_object'] = _show_object 208 | module.__dict__['debug'] = _debug 209 | module.__dict__['log'] = lambda x: info(str(x)) 210 | module.__dict__['cq'] = cq 211 | 212 | return cq_objects, set(module.__dict__)-{'cq'} 213 | 214 | def _cleanup_locals(self,module,injected_names): 215 | 216 | for name in injected_names: module.__dict__.pop(name) 217 | 218 | @pyqtSlot(bool) 219 | def render(self): 220 | 221 | if self.preferences['Reload CQ']: 222 | reload_cq() 223 | 224 | cq_script = self.get_current_script() 225 | cq_code,module = self.compile_code(cq_script) 226 | 227 | if cq_code is None: return 228 | 229 | cq_objects,injected_names = self._inject_locals(module) 230 | 231 | try: 232 | self._exec(cq_code, module.__dict__, module.__dict__) 233 | 234 | #remove the special methods 235 | self._cleanup_locals(module,injected_names) 236 | 237 | #collect all CQ objects if no explicit show_object was called 238 | if len(cq_objects) == 0: 239 | cq_objects = find_cq_objects(module.__dict__) 240 | self.sigRendered.emit(cq_objects) 241 | self.sigTraceback.emit(None, 242 | cq_script) 243 | self.sigLocals.emit(module.__dict__) 244 | except Exception: 245 | self.sigTraceback.emit(sys.exc_info(), 246 | cq_script) 247 | 248 | @pyqtSlot(bool) 249 | def debug(self,value): 250 | if value: 251 | self.sigDebugging.emit(True) 252 | self.state = DbgState.STEP 253 | 254 | self.script = self.get_current_script() 255 | code,module = self.compile_code(self.script) 256 | 257 | if code is None: 258 | self.sigDebugging.emit(False) 259 | self._actions['Run'][1].setChecked(False) 260 | return 261 | 262 | cq_objects,injected_names = self._inject_locals(module) 263 | 264 | self.breakpoints = [ el[0] for el in self.get_breakpoints()] 265 | 266 | #clear possible traceback 267 | self.sigTraceback.emit(None, 268 | self.script) 269 | try: 270 | sys.settrace(self.trace_callback) 271 | exec(code,module.__dict__,module.__dict__) 272 | except Exception: 273 | self.sigTraceback.emit(sys.exc_info(), 274 | self.script) 275 | finally: 276 | sys.settrace(None) 277 | self.sigDebugging.emit(False) 278 | self._actions['Run'][1].setChecked(False) 279 | 280 | if len(cq_objects) == 0: 281 | cq_objects = find_cq_objects(module.__dict__) 282 | self.sigRendered.emit(cq_objects) 283 | 284 | self._cleanup_locals(module,injected_names) 285 | self.sigLocals.emit(module.__dict__) 286 | else: 287 | sys.settrace(None) 288 | self.inner_event_loop.exit(0) 289 | 290 | 291 | 292 | def debug_cmd(self,state=DbgState.STEP): 293 | 294 | self.state = state 295 | self.inner_event_loop.exit(0) 296 | 297 | 298 | def trace_callback(self,frame,event,arg): 299 | 300 | filename = frame.f_code.co_filename 301 | 302 | if filename==DUMMY_FILE: 303 | self.trace_local(frame,event,arg) 304 | return self.trace_callback 305 | 306 | else: 307 | return None 308 | 309 | def trace_local(self,frame,event,arg): 310 | 311 | lineno = frame.f_lineno 312 | line = self.script.splitlines()[lineno-1] 313 | f_id = id(frame) 314 | 315 | if event in (DbgEevent.LINE,DbgEevent.RETURN): 316 | if (self.state in (DbgState.STEP, DbgState.STEP_IN)) \ 317 | or (lineno in self.breakpoints): 318 | self.sigLineChanged.emit(lineno) 319 | self.sigFrameChanged.emit(frame) 320 | self.sigLocalsChanged.emit(frame.f_locals) 321 | self.sigCQChanged.emit(find_cq_objects(frame.f_locals),True) 322 | 323 | self.inner_event_loop.exec_() 324 | 325 | elif event in (DbgEevent.RETURN): 326 | self.sigLocalsChanged.emit(frame.f_locals) 327 | 328 | elif event == DbgEevent.CALL: 329 | func_filename = frame.f_code.co_filename 330 | 331 | if self.state == DbgState.STEP_IN and func_filename == DUMMY_FILE: 332 | self.sigLineChanged.emit(lineno) 333 | self.sigFrameChanged.emit(frame) 334 | self.state = DbgState.STEP 335 | -------------------------------------------------------------------------------- /cq_editor/widgets/editor.py: -------------------------------------------------------------------------------- 1 | from spyder.plugins.editor.widgets.codeeditor import CodeEditor 2 | from PyQt5.QtCore import pyqtSignal, QFileSystemWatcher, QTimer 3 | from PyQt5.QtWidgets import QAction, QFileDialog 4 | from PyQt5.QtGui import QFontDatabase 5 | from path import Path 6 | 7 | import sys 8 | 9 | from pyqtgraph.parametertree import Parameter 10 | 11 | from ..mixins import ComponentMixin 12 | from ..utils import get_save_filename, get_open_filename, confirm 13 | 14 | from ..icons import icon 15 | 16 | class Editor(CodeEditor,ComponentMixin): 17 | 18 | name = 'Code Editor' 19 | 20 | # This signal is emitted whenever the currently-open file changes and 21 | # autoreload is enabled. 22 | triggerRerender = pyqtSignal(bool) 23 | sigFilenameChanged = pyqtSignal(str) 24 | 25 | preferences = Parameter.create(name='Preferences',children=[ 26 | {'name': 'Font size', 'type': 'int', 'value': 12}, 27 | {'name': 'Autoreload', 'type': 'bool', 'value': False}, 28 | {'name': 'Autoreload delay', 'type': 'int', 'value': 50}, 29 | {'name': 'Line wrap', 'type': 'bool', 'value': False}, 30 | {'name': 'Color scheme', 'type': 'list', 31 | 'values': ['Spyder','Monokai','Zenburn'], 'value': 'Spyder'}]) 32 | 33 | EXTENSIONS = 'py' 34 | 35 | def __init__(self,parent=None): 36 | 37 | self._watched_file = None 38 | 39 | super(Editor,self).__init__(parent) 40 | ComponentMixin.__init__(self) 41 | 42 | self.setup_editor(linenumbers=True, 43 | markers=True, 44 | edge_line=False, 45 | tab_mode=False, 46 | show_blanks=True, 47 | font=QFontDatabase.systemFont(QFontDatabase.FixedFont), 48 | language='Python', 49 | filename='') 50 | 51 | self._actions = \ 52 | {'File' : [QAction(icon('new'), 53 | 'New', 54 | self, 55 | shortcut='ctrl+N', 56 | triggered=self.new), 57 | QAction(icon('open'), 58 | 'Open', 59 | self, 60 | shortcut='ctrl+O', 61 | triggered=self.open), 62 | QAction(icon('save'), 63 | 'Save', 64 | self, 65 | shortcut='ctrl+S', 66 | triggered=self.save), 67 | QAction(icon('save_as'), 68 | 'Save as', 69 | self, 70 | shortcut='ctrl+shift+S', 71 | triggered=self.save_as), 72 | QAction(icon('autoreload'), 73 | 'Automatic reload and preview', 74 | self,triggered=self.autoreload, 75 | checkable=True, 76 | checked=False, 77 | objectName='autoreload'), 78 | ]} 79 | 80 | for a in self._actions.values(): 81 | self.addActions(a) 82 | 83 | 84 | self._fixContextMenu() 85 | 86 | # autoreload support 87 | self._file_watcher = QFileSystemWatcher(self) 88 | # we wait for 50ms after a file change for the file to be written completely 89 | self._file_watch_timer = QTimer(self) 90 | self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) 91 | self._file_watch_timer.setSingleShot(True) 92 | self._file_watcher.fileChanged.connect( 93 | lambda val: self._file_watch_timer.start()) 94 | self._file_watch_timer.timeout.connect(self._file_changed) 95 | 96 | self.updatePreferences() 97 | 98 | def _fixContextMenu(self): 99 | 100 | menu = self.menu 101 | 102 | menu.removeAction(self.run_cell_action) 103 | menu.removeAction(self.run_cell_and_advance_action) 104 | menu.removeAction(self.run_selection_action) 105 | menu.removeAction(self.re_run_last_cell_action) 106 | 107 | def updatePreferences(self,*args): 108 | 109 | self.set_color_scheme(self.preferences['Color scheme']) 110 | 111 | font = self.font() 112 | font.setPointSize(self.preferences['Font size']) 113 | self.set_font(font) 114 | 115 | self.findChild(QAction, 'autoreload') \ 116 | .setChecked(self.preferences['Autoreload']) 117 | 118 | self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) 119 | 120 | self.toggle_wrap_mode(self.preferences['Line wrap']) 121 | 122 | def confirm_discard(self): 123 | 124 | if self.modified: 125 | rv = confirm(self,'Please confirm','Current document is not saved - do you want to continue?') 126 | else: 127 | rv = True 128 | 129 | return rv 130 | 131 | def new(self): 132 | 133 | if not self.confirm_discard(): return 134 | 135 | self.set_text('') 136 | self.filename = '' 137 | self.reset_modified() 138 | 139 | def open(self): 140 | 141 | if not self.confirm_discard(): return 142 | 143 | curr_dir = Path(self.filename).abspath().dirname() 144 | fname = get_open_filename(self.EXTENSIONS, curr_dir) 145 | if fname != '': 146 | self.load_from_file(fname) 147 | 148 | def load_from_file(self,fname): 149 | 150 | self.set_text_from_file(fname) 151 | self.filename = fname 152 | self.reset_modified() 153 | 154 | def save(self): 155 | 156 | if self._filename != '': 157 | 158 | if self.preferences['Autoreload']: 159 | self._file_watcher.removePath(self.filename) 160 | self._file_watch_timer.stop() 161 | 162 | with open(self._filename,'w') as f: 163 | f.write(self.toPlainText()) 164 | 165 | if self.preferences['Autoreload']: 166 | self._file_watcher.addPath(self.filename) 167 | self.triggerRerender.emit(True) 168 | 169 | self.reset_modified() 170 | 171 | else: 172 | self.save_as() 173 | 174 | def save_as(self): 175 | 176 | fname = get_save_filename(self.EXTENSIONS) 177 | if fname != '': 178 | with open(fname,'w') as f: 179 | f.write(self.toPlainText()) 180 | self.filename = fname 181 | 182 | self.reset_modified() 183 | 184 | def _update_filewatcher(self): 185 | if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']): 186 | self._file_watcher.removePath(self._watched_file) 187 | self._watched_file = None 188 | if self.preferences['Autoreload'] and self.filename and self.filename != self._watched_file: 189 | self._watched_file = self._filename 190 | self._file_watcher.addPath(self.filename) 191 | 192 | @property 193 | def filename(self): 194 | return self._filename 195 | 196 | @filename.setter 197 | def filename(self, fname): 198 | self._filename = fname 199 | self._update_filewatcher() 200 | self.sigFilenameChanged.emit(fname) 201 | 202 | # callback triggered by QFileSystemWatcher 203 | def _file_changed(self): 204 | # neovim writes a file by removing it first 205 | # this causes QFileSystemWatcher to forget the file 206 | self._file_watcher.addPath(self._filename) 207 | self.set_text_from_file(self._filename) 208 | self.triggerRerender.emit(True) 209 | 210 | # Turn autoreload on/off. 211 | def autoreload(self, enabled): 212 | self.preferences['Autoreload'] = enabled 213 | self._update_filewatcher() 214 | 215 | def reset_modified(self): 216 | 217 | self.document().setModified(False) 218 | 219 | @property 220 | def modified(self): 221 | 222 | return self.document().isModified() 223 | 224 | def saveComponentState(self,store): 225 | 226 | if self.filename != '': 227 | store.setValue(self.name+'/state',self.filename) 228 | 229 | def restoreComponentState(self,store): 230 | 231 | filename = store.value(self.name+'/state',self.filename) 232 | 233 | if filename and filename != '': 234 | try: 235 | self.load_from_file(filename) 236 | except IOError: 237 | self._logger.warning(f'could not open {filename}') 238 | 239 | if __name__ == "__main__": 240 | 241 | from PyQt5.QtWidgets import QApplication 242 | 243 | app = QApplication(sys.argv) 244 | editor = Editor() 245 | editor.show() 246 | 247 | sys.exit(app.exec_()) 248 | -------------------------------------------------------------------------------- /cq_editor/widgets/log.py: -------------------------------------------------------------------------------- 1 | import logbook as logging 2 | 3 | from PyQt5.QtWidgets import QPlainTextEdit 4 | from PyQt5 import QtCore 5 | 6 | from ..mixins import ComponentMixin 7 | 8 | class QtLogHandler(logging.Handler,logging.StringFormatterHandlerMixin): 9 | 10 | def __init__(self, log_widget,*args,**kwargs): 11 | 12 | super(QtLogHandler,self).__init__(*args,**kwargs) 13 | logging.StringFormatterHandlerMixin.__init__(self,None) 14 | 15 | self.log_widget = log_widget 16 | 17 | def emit(self, record): 18 | 19 | msg = self.format(record) 20 | QtCore.QMetaObject\ 21 | .invokeMethod(self.log_widget, 22 | 'appendPlainText', 23 | QtCore.Qt.QueuedConnection, 24 | QtCore.Q_ARG(str, msg)) 25 | 26 | class LogViewer(QPlainTextEdit, ComponentMixin): 27 | 28 | name = 'Log viewer' 29 | 30 | def __init__(self,*args,**kwargs): 31 | 32 | super(LogViewer,self).__init__(*args,**kwargs) 33 | self._MAX_ROWS = 500 34 | 35 | self.setReadOnly(True) 36 | self.setMaximumBlockCount(self._MAX_ROWS) 37 | self.setLineWrapMode(QPlainTextEdit.NoWrap) 38 | 39 | self.handler = QtLogHandler(self) 40 | 41 | def append(self,msg): 42 | 43 | self.appendPlainText(msg) -------------------------------------------------------------------------------- /cq_editor/widgets/occt_widget.py: -------------------------------------------------------------------------------- 1 | from sys import platform 2 | 3 | 4 | from PyQt5.QtWidgets import QWidget, QApplication 5 | from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QEvent 6 | 7 | import OCP 8 | 9 | from OCP.Aspect import Aspect_DisplayConnection, Aspect_TypeOfTriedronPosition 10 | from OCP.OpenGl import OpenGl_GraphicDriver 11 | from OCP.V3d import V3d_Viewer 12 | from OCP.AIS import AIS_InteractiveContext, AIS_DisplayMode 13 | from OCP.Quantity import Quantity_Color 14 | 15 | 16 | ZOOM_STEP = 0.9 17 | 18 | 19 | class OCCTWidget(QWidget): 20 | 21 | sigObjectSelected = pyqtSignal(list) 22 | 23 | def __init__(self,parent=None): 24 | 25 | super(OCCTWidget,self).__init__(parent) 26 | 27 | self.setAttribute(Qt.WA_NativeWindow) 28 | self.setAttribute(Qt.WA_PaintOnScreen) 29 | self.setAttribute(Qt.WA_NoSystemBackground) 30 | 31 | self._initialized = False 32 | self._needs_update = False 33 | 34 | #OCCT secific things 35 | self.display_connection = Aspect_DisplayConnection() 36 | self.graphics_driver = OpenGl_GraphicDriver(self.display_connection) 37 | 38 | self.viewer = V3d_Viewer(self.graphics_driver) 39 | self.view = self.viewer.CreateView() 40 | self.context = AIS_InteractiveContext(self.viewer) 41 | 42 | #Trihedorn, lights, etc 43 | self.prepare_display() 44 | 45 | def prepare_display(self): 46 | 47 | view = self.view 48 | 49 | params = view.ChangeRenderingParams() 50 | params.NbMsaaSamples = 8 51 | params.IsAntialiasingEnabled = True 52 | 53 | view.TriedronDisplay( 54 | Aspect_TypeOfTriedronPosition.Aspect_TOTP_RIGHT_LOWER, 55 | Quantity_Color(), 0.1) 56 | 57 | viewer = self.viewer 58 | 59 | viewer.SetDefaultLights() 60 | viewer.SetLightOn() 61 | 62 | ctx = self.context 63 | 64 | ctx.SetDisplayMode(AIS_DisplayMode.AIS_Shaded, True) 65 | ctx.DefaultDrawer().SetFaceBoundaryDraw(True) 66 | 67 | def wheelEvent(self, event): 68 | 69 | delta = event.angleDelta().y() 70 | factor = ZOOM_STEP if delta<0 else 1/ZOOM_STEP 71 | 72 | self.view.SetZoom(factor) 73 | 74 | def mousePressEvent(self,event): 75 | 76 | pos = event.pos() 77 | 78 | if event.button() == Qt.LeftButton: 79 | self.view.StartRotation(pos.x(), pos.y()) 80 | elif event.button() == Qt.RightButton: 81 | self.view.StartZoomAtPoint(pos.x(), pos.y()) 82 | 83 | self.old_pos = pos 84 | 85 | def mouseMoveEvent(self,event): 86 | 87 | pos = event.pos() 88 | x,y = pos.x(),pos.y() 89 | 90 | if event.buttons() == Qt.LeftButton: 91 | self.view.Rotation(x,y) 92 | 93 | elif event.buttons() == Qt.MiddleButton: 94 | self.view.Pan(x - self.old_pos.x(), 95 | self.old_pos.y() - y, theToStart=True) 96 | 97 | elif event.buttons() == Qt.RightButton: 98 | self.view.ZoomAtPoint(self.old_pos.x(), y, 99 | x, self.old_pos.y()) 100 | 101 | self.old_pos = pos 102 | 103 | def mouseReleaseEvent(self,event): 104 | 105 | if event.button() == Qt.LeftButton: 106 | pos = event.pos() 107 | x,y = pos.x(),pos.y() 108 | 109 | self.context.MoveTo(x,y,self.view,True) 110 | 111 | self._handle_selection() 112 | 113 | def _handle_selection(self): 114 | 115 | self.context.Select(True) 116 | self.context.InitSelected() 117 | 118 | selected = [] 119 | if self.context.HasSelectedShape(): 120 | selected.append(self.context.SelectedShape()) 121 | 122 | self.sigObjectSelected.emit(selected) 123 | 124 | def paintEngine(self): 125 | 126 | return None 127 | 128 | def paintEvent(self, event): 129 | 130 | if not self._initialized: 131 | self._initialize() 132 | else: 133 | self.view.Redraw() 134 | 135 | def showEvent(self, event): 136 | 137 | super(OCCTWidget,self).showEvent(event) 138 | 139 | def resizeEvent(self, event): 140 | 141 | super(OCCTWidget,self).resizeEvent(event) 142 | 143 | self.view.MustBeResized() 144 | 145 | def _initialize(self): 146 | 147 | wins = { 148 | 'darwin' : self._get_window_osx, 149 | 'linux' : self._get_window_linux, 150 | 'win32': self._get_window_win 151 | } 152 | 153 | self.view.SetWindow(wins.get(platform,self._get_window_linux)(self.winId())) 154 | 155 | self._initialized = True 156 | 157 | def _get_window_win(self,wid): 158 | 159 | from OCP.WNT import WNT_Window 160 | 161 | return WNT_Window(wid.ascapsule()) 162 | 163 | def _get_window_linux(self,wid): 164 | 165 | from OCP.Xw import Xw_Window 166 | 167 | return Xw_Window(self.display_connection,int(wid)) 168 | 169 | def _get_window_osx(self,wid): 170 | 171 | from OCP.Cocoa import Cocoa_Window 172 | 173 | return Cocoa_Window(wid.ascapsule()) 174 | -------------------------------------------------------------------------------- /cq_editor/widgets/traceback_viewer.py: -------------------------------------------------------------------------------- 1 | from traceback import extract_tb, format_exception_only 2 | 3 | from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction, 4 | QLabel) 5 | from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal 6 | 7 | from ..mixins import ComponentMixin 8 | from ..utils import layout 9 | 10 | class TracebackTree(QTreeWidget): 11 | 12 | name = 'Traceback Viewer' 13 | 14 | def __init__(self,parent): 15 | 16 | super(TracebackTree,self).__init__(parent) 17 | self.setHeaderHidden(False) 18 | self.setItemsExpandable(False) 19 | self.setRootIsDecorated(False) 20 | self.setContextMenuPolicy(Qt.ActionsContextMenu) 21 | 22 | self.setColumnCount(3) 23 | self.setHeaderLabels(['File','Line','Code']) 24 | 25 | 26 | self.root = self.invisibleRootItem() 27 | 28 | class TracebackPane(QWidget,ComponentMixin): 29 | 30 | sigHighlightLine = pyqtSignal(int) 31 | 32 | def __init__(self,parent): 33 | 34 | super(TracebackPane,self).__init__(parent) 35 | 36 | self.tree = TracebackTree(self) 37 | self.current_exception = QLabel(self) 38 | self.current_exception.setStyleSheet(\ 39 | "QLabel {color : red; }"); 40 | 41 | layout(self, 42 | (self.current_exception, 43 | self.tree), 44 | self) 45 | 46 | self.tree.currentItemChanged.connect(self.handleSelection) 47 | 48 | @pyqtSlot(object,str) 49 | def addTraceback(self,exc_info,code): 50 | 51 | self.tree.clear() 52 | 53 | if exc_info: 54 | t,exc,tb = exc_info 55 | 56 | root = self.tree.root 57 | code = code.splitlines() 58 | tb = [t for t in extract_tb(tb) if '' in t.filename] #ignore highest frames (debug, exec) 59 | 60 | for el in tb: 61 | #workaround of the traceback module 62 | if el.line == '': 63 | line = code[el.lineno-1].strip() 64 | else: 65 | line = el.line 66 | 67 | root.addChild(QTreeWidgetItem([el.filename, 68 | str(el.lineno), 69 | line])) 70 | 71 | exc_name = t.__name__ 72 | exc_msg = str(exc) 73 | 74 | self.current_exception.\ 75 | setText('{}: {}'.format(exc_name,exc_msg)) 76 | 77 | # handle the special case of a SyntaxError 78 | if t is SyntaxError: 79 | root.addChild(QTreeWidgetItem([exc.filename, 80 | str(exc.lineno), 81 | exc.text.strip()])) 82 | else: 83 | self.current_exception.setText('') 84 | 85 | @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem) 86 | def handleSelection(self,item,*args): 87 | 88 | if item: 89 | f,line = item.data(0,0),int(item.data(1,0)) 90 | 91 | if '' in f: 92 | self.sigHighlightLine.emit(line) 93 | 94 | -------------------------------------------------------------------------------- /environment.yaml: -------------------------------------------------------------------------------- 1 | name: cqgui 2 | channels: 3 | - CadQuery 4 | - conda-forge 5 | - defaults 6 | dependencies: 7 | - _libgcc_mutex=0.1=conda_forge 8 | - _openmp_mutex=4.5=1_gnu 9 | - alabaster=0.7.12=py_0 10 | - alsa-lib=1.2.3=h516909a_0 11 | - aom=3.2.0=h9c3ff4c_2 12 | - argh=0.26.2=pyh9f0ad1d_1002 13 | - astroid=2.9.0=py38h578d9bd_0 14 | - async_generator=1.10=py_0 15 | - atomicwrites=1.4.0=pyh9f0ad1d_0 16 | - attrs=21.2.0=pyhd8ed1ab_0 17 | - autopep8=1.5.6=pyhd8ed1ab_0 18 | - babel=2.9.1=pyh44b312d_0 19 | - backcall=0.2.0=pyh9f0ad1d_0 20 | - backports=1.0=py_2 21 | - backports.functools_lru_cache=1.6.4=pyhd8ed1ab_0 22 | - black=21.11b1=pyhd8ed1ab_0 23 | - bleach=4.1.0=pyhd8ed1ab_0 24 | - brotlipy=0.7.0=py38h497a2fe_1003 25 | - bzip2=1.0.8=h7f98852_4 26 | - c-ares=1.18.1=h7f98852_0 27 | - ca-certificates=2022.4.26=h06a4308_0 28 | - certifi=2021.10.8=py38h06a4308_2 29 | - cffi=1.15.0=py38h3931269_0 30 | - chardet=4.0.0=py38h578d9bd_2 31 | - charset-normalizer=2.0.9=pyhd8ed1ab_0 32 | - click=8.0.3=py38h578d9bd_1 33 | - cloudpickle=2.0.0=pyhd8ed1ab_0 34 | - colorama=0.4.4=pyh9f0ad1d_0 35 | - cryptography=36.0.0=py38h3e25421_0 36 | - curl=7.80.0=h2574ce0_0 37 | - cytoolz=0.11.0=py38h7b6447c_0 38 | - dask-core=2022.2.1=pyhd3eb1b0_0 39 | - dataclasses=0.8=pyhc8e2a94_3 40 | - dbus=1.13.6=h48d8840_2 41 | - debugpy=1.5.1=py38h709712a_0 42 | - decorator=5.1.0=pyhd8ed1ab_0 43 | - defusedxml=0.7.1=pyhd8ed1ab_0 44 | - diff-match-patch=20200713=pyh9f0ad1d_0 45 | - docutils=0.17.1=py38h578d9bd_1 46 | - double-conversion=3.1.6=h9c3ff4c_0 47 | - eigen=3.4.0=h4bd325d_0 48 | - entrypoints=0.3=pyhd8ed1ab_1003 49 | - expat=2.4.1=h9c3ff4c_0 50 | - ezdxf=0.17=py38h1fd1430_1 51 | - ffmpeg=4.4.0=h6987444_5 52 | - flake8=3.8.4=py_0 53 | - font-ttf-dejavu-sans-mono=2.37=hab24e00_0 54 | - font-ttf-inconsolata=3.000=h77eed37_0 55 | - font-ttf-source-code-pro=2.038=h77eed37_0 56 | - font-ttf-ubuntu=0.83=hab24e00_0 57 | - fontconfig=2.13.1=hba837de_1005 58 | - fonts-conda-ecosystem=1=0 59 | - fonts-conda-forge=1=0 60 | - freeimage=3.18.0=h88c329d_7 61 | - freetype=2.10.4=h0708190_1 62 | - fsspec=2022.2.0=pyhd3eb1b0_0 63 | - future=0.18.2=py38h578d9bd_4 64 | - gettext=0.19.8.1=h73d1719_1008 65 | - giflib=5.2.1=h7b6447c_0 66 | - gl2ps=1.4.2=h0708190_0 67 | - glew=2.1.0=h9c3ff4c_2 68 | - glib=2.70.2=h780b84a_0 69 | - glib-tools=2.70.2=h780b84a_0 70 | - gmp=6.2.1=h58526e2_0 71 | - gnutls=3.6.13=h85f3911_1 72 | - gst-plugins-base=1.18.5=hf529b03_2 73 | - gstreamer=1.18.5=h9f60fe5_2 74 | - hdf4=4.2.15=h10796ff_3 75 | - hdf5=1.10.6=nompi_h6a2412b_1114 76 | - helpdev=0.7.1=pyhd8ed1ab_0 77 | - icu=68.2=h9c3ff4c_0 78 | - idna=3.1=pyhd3deb0d_0 79 | - ilmbase=2.5.5=h780b84a_0 80 | - imageio=2.9.0=pyhd3eb1b0_0 81 | - imagesize=1.3.0=pyhd8ed1ab_0 82 | - importlib-metadata=4.8.2=py38h578d9bd_0 83 | - importlib_metadata=4.8.2=hd8ed1ab_0 84 | - importlib_resources=5.4.0=pyhd8ed1ab_0 85 | - intervaltree=3.0.2=py_0 86 | - ipykernel=6.6.0=py38he5a9106_0 87 | - ipython=7.30.1=py38h578d9bd_0 88 | - ipython_genutils=0.2.0=py_1 89 | - isort=5.10.1=pyhd8ed1ab_0 90 | - jbig=2.1=h7f98852_2003 91 | - jedi=0.17.2=py38h578d9bd_2 92 | - jeepney=0.7.1=pyhd8ed1ab_0 93 | - jinja2=3.0.3=pyhd8ed1ab_0 94 | - jpeg=9d=h36c2ea0_0 95 | - jsoncpp=1.9.4=h4bd325d_3 96 | - jsonschema=4.2.1=pyhd8ed1ab_1 97 | - jupyter_client=7.1.0=pyhd8ed1ab_0 98 | - jupyter_core=4.9.1=py38h578d9bd_1 99 | - jupyterlab_pygments=0.1.2=pyh9f0ad1d_0 100 | - jxrlib=1.1=h7f98852_2 101 | - keyring=23.4.0=py38h578d9bd_0 102 | - krb5=1.19.2=hcc1bbae_3 103 | - lame=3.100=h7f98852_1001 104 | - lazy-object-proxy=1.6.0=py38h497a2fe_1 105 | - lcms2=2.12=hddcbb42_0 106 | - ld_impl_linux-64=2.36.1=hea4e1c9_2 107 | - lerc=3.0=h9c3ff4c_0 108 | - libblas=3.9.0=12_linux64_openblas 109 | - libcblas=3.9.0=12_linux64_openblas 110 | - libclang=11.1.0=default_ha53f305_1 111 | - libcurl=7.80.0=h2574ce0_0 112 | - libdeflate=1.8=h7f98852_0 113 | - libedit=3.1.20191231=he28a2e2_2 114 | - libev=4.33=h516909a_1 115 | - libevent=2.1.10=h9b69904_4 116 | - libffi=3.4.2=h7f98852_5 117 | - libgcc-ng=11.2.0=h1d223b6_11 118 | - libgfortran-ng=11.2.0=h69a702a_11 119 | - libgfortran5=11.2.0=h5c6108e_11 120 | - libglib=2.70.2=h174f98d_0 121 | - libglu=9.0.0=he1b5a44_1001 122 | - libgomp=11.2.0=h1d223b6_11 123 | - libiconv=1.16=h516909a_0 124 | - liblapack=3.9.0=12_linux64_openblas 125 | - libllvm11=11.1.0=hf817b99_2 126 | - libnetcdf=4.8.0=nompi_hcd642e3_103 127 | - libnghttp2=1.43.0=h812cca2_1 128 | - libnsl=2.0.0=h7f98852_0 129 | - libogg=1.3.4=h7f98852_1 130 | - libopenblas=0.3.18=pthreads_h8fe5266_0 131 | - libopus=1.3.1=h7f98852_1 132 | - libpng=1.6.37=h21135ba_2 133 | - libpq=13.5=hd57d9b9_1 134 | - libraw=0.20.2=h10796ff_1 135 | - libsodium=1.0.18=h36c2ea0_1 136 | - libspatialindex=1.9.3=h9c3ff4c_4 137 | - libssh2=1.10.0=ha56f1ee_2 138 | - libstdcxx-ng=11.2.0=he4da1e4_11 139 | - libtheora=1.1.1=h7f98852_1005 140 | - libtiff=4.3.0=h6f004c6_2 141 | - libuuid=2.32.1=h7f98852_1000 142 | - libvorbis=1.3.7=h9c3ff4c_0 143 | - libvpx=1.11.0=h9c3ff4c_3 144 | - libwebp=1.2.2=h55f646e_0 145 | - libwebp-base=1.2.2=h7f8727e_0 146 | - libxcb=1.13=h7f98852_1004 147 | - libxkbcommon=1.0.3=he3ba5ed_0 148 | - libxml2=2.9.12=h72842e0_0 149 | - libzip=1.8.0=h4de3113_1 150 | - libzlib=1.2.11=h36c2ea0_1013 151 | - locket=0.2.1=py38h06a4308_2 152 | - logbook=1.5.3=py38h497a2fe_5 153 | - loguru=0.5.3=py38h578d9bd_3 154 | - lz4-c=1.9.3=h9c3ff4c_1 155 | - markupsafe=2.0.1=py38h497a2fe_1 156 | - matplotlib-inline=0.1.3=pyhd8ed1ab_0 157 | - mccabe=0.6.1=py_1 158 | - mistune=0.8.4=py38h497a2fe_1005 159 | - multimethod=1.6=pyhd8ed1ab_0 160 | - mypy_extensions=0.4.3=py38h578d9bd_4 161 | - mysql-common=8.0.27=ha770c72_1 162 | - mysql-libs=8.0.27=hfa10184_1 163 | - nbclient=0.5.9=pyhd8ed1ab_0 164 | - nbconvert=6.3.0=py38h578d9bd_1 165 | - nbformat=5.1.3=pyhd8ed1ab_0 166 | - ncurses=6.2=h58526e2_4 167 | - nest-asyncio=1.5.4=pyhd8ed1ab_0 168 | - nettle=3.6=he412f7d_0 169 | - networkx=2.7.1=pyhd3eb1b0_0 170 | - nlopt=2.7.1=py38hd719023_0 171 | - nptyping=1.4.4=pyhd8ed1ab_0 172 | - nspr=4.32=h9c3ff4c_1 173 | - nss=3.73=hb5efdd6_0 174 | - numpy=1.21.4=py38he2449b9_0 175 | - numpydoc=1.1.0=py_1 176 | - occt=7.5.2=h7391655_2 177 | - ocp=7.5.2beta=1_py3.8 178 | - openexr=2.5.5=hf817b99_0 179 | - openh264=2.1.1=h780b84a_0 180 | - openjpeg=2.4.0=hb52868f_1 181 | - openssl=1.1.1o=h7f8727e_0 182 | - packaging=21.3=pyhd8ed1ab_0 183 | - pandoc=2.16.2=h7f98852_0 184 | - pandocfilters=1.5.0=pyhd8ed1ab_0 185 | - parso=0.7.0=pyh9f0ad1d_0 186 | - partd=1.2.0=pyhd3eb1b0_1 187 | - path=16.2.0=py38h578d9bd_1 188 | - path.py=12.5.0=0 189 | - pathspec=0.9.0=pyhd8ed1ab_0 190 | - pcre=8.45=h9c3ff4c_0 191 | - pexpect=4.8.0=pyh9f0ad1d_2 192 | - pickleshare=0.7.5=py_1003 193 | - pillow=9.0.1=py38h22f2fdc_0 194 | - pip=21.3.1=pyhd8ed1ab_0 195 | - platformdirs=2.3.0=pyhd8ed1ab_0 196 | - pluggy=1.0.0=py38h578d9bd_2 197 | - proj=7.2.0=h277dcde_2 198 | - prompt-toolkit=3.0.23=pyha770c72_0 199 | - psutil=5.8.0=py38h497a2fe_2 200 | - pthread-stubs=0.4=h36c2ea0_1001 201 | - ptyprocess=0.7.0=pyhd3deb0d_0 202 | - pugixml=1.11.4=h9c3ff4c_0 203 | - pycodestyle=2.6.0=pyh9f0ad1d_0 204 | - pycparser=2.21=pyhd8ed1ab_0 205 | - pydocstyle=6.1.1=pyhd8ed1ab_0 206 | - pyflakes=2.2.0=pyh9f0ad1d_0 207 | - pygments=2.10.0=pyhd8ed1ab_0 208 | - pylint=2.12.2=pyhd8ed1ab_0 209 | - pyls-black=0.4.6=pyh9f0ad1d_0 210 | - pyls-spyder=0.3.2=pyhd8ed1ab_0 211 | - pyopenssl=21.0.0=pyhd8ed1ab_0 212 | - pyparsing=3.0.6=pyhd8ed1ab_0 213 | - pyqt=5.12.3=py38h578d9bd_8 214 | - pyqt-impl=5.12.3=py38h0ffb2e6_8 215 | - pyqt5-sip=4.19.18=py38h709712a_8 216 | - pyqtchart=5.12=py38h7400c14_8 217 | - pyqtgraph=0.12.3=pyhd8ed1ab_0 218 | - pyqtwebengine=5.12.1=py38h7400c14_8 219 | - pyrsistent=0.18.0=py38h497a2fe_0 220 | - pysocks=1.7.1=py38h578d9bd_4 221 | - python=3.8.12=hb7a2778_2_cpython 222 | - python-dateutil=2.8.2=pyhd8ed1ab_0 223 | - python-jsonrpc-server=0.4.0=pyh9f0ad1d_0 224 | - python-language-server=0.36.2=pyhd8ed1ab_0 225 | - python_abi=3.8=2_cp38 226 | - pytz=2021.3=pyhd8ed1ab_0 227 | - pywavelets=1.3.0=py38h7f8727e_0 228 | - pyxdg=0.27=pyhd8ed1ab_0 229 | - pyyaml=6.0=py38h497a2fe_3 230 | - pyzmq=22.3.0=py38h2035c66_1 231 | - qdarkstyle=2.8.1=pyhd8ed1ab_2 232 | - qt=5.12.9=hda022c4_4 233 | - qtawesome=1.1.1=pyhd8ed1ab_0 234 | - qtconsole=5.2.1=pyhd8ed1ab_0 235 | - qtpy=1.11.3=pyhd8ed1ab_0 236 | - rapidjson=1.1.0=he1b5a44_1002 237 | - readline=8.1=h46c0cb4_0 238 | - regex=2021.11.10=py38h497a2fe_0 239 | - requests=2.26.0=pyhd8ed1ab_1 240 | - rope=0.22.0=pyhd8ed1ab_0 241 | - rtree=0.9.7=py38h02d302b_3 242 | - scikit-image=0.19.2=py38h51133e4_0 243 | - scipy=1.7.3=py38h56a6a73_0 244 | - secretstorage=3.3.1=py38h578d9bd_1 245 | - setuptools=59.4.0=py38h578d9bd_0 246 | - six=1.16.0=pyh6c4a22f_0 247 | - snowballstemmer=2.2.0=pyhd8ed1ab_0 248 | - sortedcontainers=2.4.0=pyhd8ed1ab_0 249 | - sphinx=4.3.1=pyh6c4a22f_0 250 | - sphinxcontrib-applehelp=1.0.2=py_0 251 | - sphinxcontrib-devhelp=1.0.2=py_0 252 | - sphinxcontrib-htmlhelp=2.0.0=pyhd8ed1ab_0 253 | - sphinxcontrib-jsmath=1.0.1=py_0 254 | - sphinxcontrib-qthelp=1.0.3=py_0 255 | - sphinxcontrib-serializinghtml=1.1.5=pyhd8ed1ab_1 256 | - spyder=4.2.5=py38h578d9bd_0 257 | - spyder-kernels=1.10.2=py38h578d9bd_0 258 | - sqlite=3.37.0=h9cd32fc_0 259 | - tbb=2020.2=h4bd325d_4 260 | - tbb-devel=2020.2=h4bd325d_4 261 | - testpath=0.5.0=pyhd8ed1ab_0 262 | - textdistance=4.2.2=pyhd8ed1ab_0 263 | - three-merge=0.1.1=pyh9f0ad1d_0 264 | - tifffile=2020.10.1=py38hdd07704_2 265 | - tk=8.6.11=h27826a3_1 266 | - toml=0.10.2=pyhd8ed1ab_0 267 | - tomli=1.2.2=pyhd8ed1ab_0 268 | - toolz=0.11.2=pyhd3eb1b0_0 269 | - tornado=6.1=py38h497a2fe_2 270 | - traitlets=5.1.1=pyhd8ed1ab_0 271 | - typed-ast=1.5.1=py38h497a2fe_0 272 | - typing-extensions=4.0.1=hd8ed1ab_0 273 | - typing_extensions=4.0.1=pyha770c72_0 274 | - typish=1.9.3=pyhd8ed1ab_0 275 | - ujson=4.2.0=py38h709712a_1 276 | - urllib3=1.26.7=pyhd8ed1ab_0 277 | - utfcpp=3.2.1=ha770c72_0 278 | - vtk=9.0.1=no_osmesa_py38h3850a3d_109 279 | - watchdog=1.0.2=py38h578d9bd_1 280 | - wcwidth=0.2.5=pyh9f0ad1d_2 281 | - webencodings=0.5.1=py_1 282 | - wheel=0.37.0=pyhd8ed1ab_1 283 | - wrapt=1.13.3=py38h497a2fe_1 284 | - wurlitzer=3.0.2=py38h578d9bd_1 285 | - x264=1!161.3030=h7f98852_1 286 | - x265=3.5=h4bd325d_1 287 | - xorg-kbproto=1.0.7=h7f98852_1002 288 | - xorg-libice=1.0.10=h7f98852_0 289 | - xorg-libsm=1.2.3=hd9c2040_1000 290 | - xorg-libx11=1.7.2=h7f98852_0 291 | - xorg-libxau=1.0.9=h7f98852_0 292 | - xorg-libxdmcp=1.1.3=h7f98852_0 293 | - xorg-libxext=1.3.4=h7f98852_1 294 | - xorg-libxt=1.2.1=h7f98852_2 295 | - xorg-xextproto=7.3.0=h7f98852_1002 296 | - xorg-xproto=7.0.31=h7f98852_1007 297 | - xz=5.2.5=h516909a_1 298 | - yaml=0.2.5=h516909a_0 299 | - yapf=0.31.0=pyhd8ed1ab_0 300 | - zeromq=4.3.4=h9c3ff4c_1 301 | - zipp=3.6.0=pyhd8ed1ab_0 302 | - zlib=1.2.11=h36c2ea0_1013 303 | - zstd=1.5.0=ha95c52a_0 304 | - pip: 305 | - cadquery==2.1 306 | - cqmore==0.1 307 | prefix: /home/jalovisko/anaconda3/envs/cqgui 308 | -------------------------------------------------------------------------------- /icons/cadquery_logo_dark.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalovisko/LatticeQuery/7078d8cf11f658590c88e6e84e4c728ae2f8be9b/icons/cadquery_logo_dark.ico -------------------------------------------------------------------------------- /icons/cadquery_logo_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 65 | 68 | 72 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /lattice_scripts/BCC_heterogeneous_lattice.py: -------------------------------------------------------------------------------- 1 | from lq.topologies.bcc import bcc_heterogeneous_lattice 2 | import time 3 | 4 | # USER INPUT 5 | 6 | unit_cell_size = 10 7 | min_strut_diameter = 0.8 8 | max_strut_diameter = 5. 9 | min_node_diameter = 0.88 10 | max_node_diameter = 5.5 11 | Nx = 4 12 | Ny = 4 13 | Nz = 20 14 | 15 | # END USER INPUT 16 | 17 | # Register our custom plugin before use. 18 | cq.Workplane.bcc_heterogeneous_lattice = bcc_heterogeneous_lattice 19 | 20 | #result = unit_cell(unit_cell_size, strut_radius) 21 | #timing performance 22 | start_time = time.time() 23 | result = bcc_heterogeneous_lattice(unit_cell_size, 24 | min_strut_diameter, 25 | max_strut_diameter, 26 | min_node_diameter, 27 | max_node_diameter, 28 | Nx, Ny, Nz, 29 | topology = 'bcc', 30 | rule = 'parabola' 31 | ) 32 | print('The excecution time is: %s seconds' % (time.time() - start_time)) 33 | -------------------------------------------------------------------------------- /lattice_scripts/FBCC_heterogeneous_lattice.py: -------------------------------------------------------------------------------- 1 | from lq.topologies.fbcc import fbcc_heterogeneous_lattice 2 | from lq.commons import eachpointAdaptive 3 | # USER INPUT 4 | 5 | unit_cell_size = 10 6 | min_strut_diameter = 1 7 | max_strut_diameter = 2 8 | min_node_diameter = 1.1 9 | max_node_diameter = 2.2 10 | Nx = 2 11 | Ny = 2 12 | Nz = 2 13 | 14 | # END USER INPUT 15 | 16 | # Register our custom plugin before use. 17 | cq.Workplane.fbcc_heterogeneous_lattice = fbcc_heterogeneous_lattice 18 | 19 | #result = unit_cell(unit_cell_size, strut_radius) 20 | result = fbcc_heterogeneous_lattice(unit_cell_size, 21 | min_strut_diameter, 22 | max_strut_diameter, 23 | min_node_diameter, 24 | max_node_diameter, 25 | Nx, Ny, Nz) -------------------------------------------------------------------------------- /lattice_scripts/FCC_heterogeneous_lattice.py: -------------------------------------------------------------------------------- 1 | from lq.topologies.fcc import fcc_heterogeneous_lattice 2 | 3 | # USER INPUT 4 | 5 | unit_cell_size = 10 6 | min_strut_diameter = 0.5 7 | max_strut_diameter = 2. 8 | min_node_diameter = 0.5 9 | max_node_diameter = 2. 10 | Nx = 5 11 | Ny = 1 12 | Nz = 5 13 | 14 | # END USER INPUT 15 | 16 | # Register our custom plugin before use. 17 | cq.Workplane.fcc_heterogeneous_lattice = fcc_heterogeneous_lattice 18 | 19 | #result = unit_cell(unit_cell_size, strut_radius) 20 | result = fcc_heterogeneous_lattice(unit_cell_size, 21 | min_strut_diameter, 22 | max_strut_diameter, 23 | min_node_diameter, 24 | max_node_diameter, 25 | Nx, Ny, Nz, 26 | type = 'fcc' 27 | #rule = 'sin' 28 | ) -------------------------------------------------------------------------------- /lattice_scripts/adaptive_Ls.py: -------------------------------------------------------------------------------- 1 | def eachpointAdaptive( 2 | self, 3 | callback, 4 | callback_extra_args = None, 5 | useLocalCoords = False 6 | ): 7 | """ 8 | Same as each(), except that (1) each item on the stack is converted into a point before it 9 | is passed into the callback function and (2) it allows to pass in additional arguments, one 10 | set for each object to process. 11 | 12 | Conversion of stack items into points means: the resulting stack has a point for each object 13 | on the original stack. Vertices and points remain a point. Faces, Wires, Solids, Edges, and 14 | Shells are converted to a point by using their center of mass. If the stack has zero length, a 15 | single point is returned, which is the center of the current workplane / coordinate system. 16 | 17 | This is adapted from here: 18 | https://github.com/CadQuery/cadquery/issues/628#issuecomment-807493984 19 | 20 | :param callback_extra_args: Array of dicts for keyword arguments that will be 21 | provided to the callback in addition to the obligatory location argument. The outer array 22 | level is indexed by the objects on the stack to iterate over, in the order they appear in 23 | the Workplane.objects attribute. The inner arrays are dicts of keyword arguments, each dict 24 | for one call of the callback function each. If a single dict is provided, then this set of 25 | keyword arguments is used for every call of the callback. 26 | :param useLocalCoords: Should points provided to the callback be in local or global coordinates. 27 | 28 | :return: CadQuery object which contains a list of vectors (points) on its stack. 29 | 30 | .. todo:: Implement that callback_extra_args can also be a single dict. 31 | .. todo:: Implement that empty dicts are used as arguments for calls to the callback if not 32 | enough sets are provided for all objects on the stack. 33 | """ 34 | 35 | # Convert the objects on the stack to a list of points. 36 | pnts = [] 37 | plane = self.plane 38 | loc = self.plane.location 39 | if len(self.objects) == 0: 40 | # When nothing is on the stack, use the workplane origin point. 41 | pnts.append(cq.Location()) 42 | else: 43 | for o in self.objects: 44 | if isinstance(o, (cq.Vector, cq.Shape)): 45 | pnts.append(loc.inverse * cq.Location(plane, o.Center())) 46 | else: 47 | pnts.append(o) 48 | 49 | # If no extra keyword arguments are provided to the callback, provide a list of empty dicts as 50 | # structure for the **() deferencing to work below without issues. 51 | if callback_extra_args is None: 52 | callback_extra_args = [{} for p in pnts] 53 | 54 | # Call the callback for each point and collect the objects it generates with each call. 55 | res = [] 56 | for i, p in enumerate(pnts): 57 | p = (p * loc) if useLocalCoords == False else p 58 | extra_args = callback_extra_args[i] 59 | p_res = callback(p, **extra_args) 60 | p_res = p_res.move(loc) if useLocalCoords == True else p_res 61 | res.append(p_res) 62 | 63 | # For result objects that are wires, make them pending if necessary. 64 | for r in res: 65 | if isinstance(r, cq.Wire) and not r.forConstruction: 66 | self._addPendingWire(r) 67 | 68 | return self.newObject(res) 69 | 70 | def create_l(location, diameter): 71 | result = ( 72 | cq.Workplane() 73 | 74 | # Vertical part. 75 | .circle(diameter / 2) 76 | .extrude(-500) 77 | 78 | # Horizontal part. 79 | .transformed(offset = cq.Vector(0,0,-500)) 80 | .transformed(rotate = cq.Vector(90,0,0)) 81 | .circle(diameter / 2) 82 | .extrude(-500) 83 | ) 84 | 85 | return result.val().located(location) 86 | 87 | # Register our custom plugin before use. 88 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 89 | 90 | model = ( 91 | cq.Workplane("XY") 92 | .pushPoints([(0, 0), (100, 0), (200, 0)]) 93 | .eachpointAdaptive( 94 | create_l, 95 | callback_extra_args = [{"diameter": 20}, {"diameter": 40}, {"diameter": 80}], 96 | useLocalCoords = True 97 | ) 98 | ) -------------------------------------------------------------------------------- /lattice_scripts/conform-surface.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | import numpy as np 3 | from math import sin, sqrt 4 | 5 | from lq.commons import cylinder_by_two_points, make_sphere 6 | 7 | # Set initial parameters 8 | dia = 2 9 | x = 0 10 | y = 0 11 | z = 0 12 | 13 | offset = 2 14 | 15 | # Create a numpy array with a range of 0 to 25 with a step of 0.25 16 | t = np.arange(0.0, 25.0, 0.25) 17 | 18 | # Calculate f(t) and df(t) using given formulas 19 | f = 5*np.sin(t)/t 20 | df = (t * np.cos(t) - np.sin(t))/(t*t) 21 | 22 | # Calculate slope using df(t) 23 | slope = - 1/df 24 | 25 | # Set diameters and unit cell parameters 26 | d_min = 0.125 27 | d_max = 0.75 28 | Nx = 10 # number of unit cells along X 29 | hz = 3 # height along Z 30 | 31 | # Initialize lists to store coordinates 32 | pts=[] 33 | g_pts1=[] 34 | g_pts2=[] 35 | 36 | # Calculate coordinates and store them in the lists 37 | for idx, x in enumerate(t): 38 | y = f[idx] 39 | if x == 0: 40 | # adds a coordinate to pts, g_pts1, and g_pts2 41 | y = 1.0 * 5 42 | pts += [(x, y, z)] 43 | g_pts1 += [(x, y + offset, z)] 44 | g_pts2 += [(x, y - offset, z)] 45 | else: 46 | # calculates the necessary values for xg1, yg1, xg2, and yg2 47 | # using some mathematical formulas 48 | y = f[idx] 49 | pts += [(x, y, z)] 50 | k = slope[idx] 51 | A = 1 + k*k 52 | B = x + k * k * x 53 | C = x * x + k * k * x * x - offset * offset 54 | xg1 = (2 * B + sqrt((4*B*B - 4*A*C))) / (2*A) 55 | xg2 = (2 * B - sqrt((4*B*B - 4*A*C))) / (2*A) 56 | pre_yg1 = k * (xg1 - x) + y 57 | pre_yg2 = k * (xg2 - x) + y 58 | if pre_yg1 >= pre_yg2: 59 | yg1 = pre_yg1 60 | yg2 = pre_yg2 61 | else: 62 | yg1 = pre_yg2 63 | yg2 = pre_yg1 64 | g_pts1 += [(xg1, yg1, z)] 65 | g_pts2 += [(xg2, yg2, z)] 66 | 67 | # Create a spline path with the coordinates in pts 68 | path = cq.Workplane("XY").spline(pts) 69 | 70 | # Create a sweep along the path with varying diameters 71 | sweep = (cq.Workplane("XY") 72 | .pushPoints([path.val().locationAt(0)]).circle(d_min) 73 | .pushPoints([path.val().locationAt(1)]).circle(d_max) 74 | .consolidateWires() 75 | .sweep(path,multisection=True) 76 | ) 77 | 78 | # Create a spline path with the coordinates in g_pts1 and 79 | # create a sweep along the path 80 | path_g1 = cq.Workplane("XY").spline(g_pts1) 81 | sweep_g1 = (cq.Workplane("XY") 82 | .pushPoints([path_g1.val().locationAt(0)]).circle(d_min) 83 | .pushPoints([path_g1.val().locationAt(1)]).circle(d_max) 84 | .consolidateWires() 85 | .sweep(path_g1,multisection=True) 86 | ) 87 | 88 | 89 | path_g2 = cq.Workplane("XY").spline(g_pts2) 90 | sweep_g2 = (cq.Workplane("XY") 91 | .pushPoints([path_g2.val().locationAt(0)]).circle(0.25) 92 | .pushPoints([path_g2.val().locationAt(1)]).circle(0.75) 93 | .consolidateWires() 94 | .sweep(path_g2,multisection=True) 95 | ) 96 | 97 | show_object(sweep) 98 | show_object(sweep_g1) 99 | show_object(sweep_g2) 100 | 101 | diameters = np.linspace(0.125,0.75,10) 102 | n_pts = int(len(pts) / Nx) 103 | for p, gp1, gp2, d in zip(pts[::n_pts], g_pts1[::n_pts], g_pts2[::n_pts], diameters): 104 | # Z-struts 105 | p_o = (p[0], p[1], hz) 106 | cyl = cylinder_by_two_points(p, p_o, d) 107 | show_object(cyl) 108 | gp1_o = (gp1[0], gp1[1], hz) 109 | cyl = cylinder_by_two_points(gp1, gp1_o, d) 110 | show_object(cyl) 111 | gp2_o = (gp2[0], gp2[1], hz) 112 | cyl = cylinder_by_two_points(gp2, gp2_o, d) 113 | show_object(cyl) 114 | # Y-struts 115 | cyl = cylinder_by_two_points(p, gp1, d) 116 | show_object(cyl) 117 | cyl = cylinder_by_two_points(p, gp2, d) 118 | show_object(cyl) -------------------------------------------------------------------------------- /lattice_scripts/cubic.py: -------------------------------------------------------------------------------- 1 | from lq.topologies.tcubic import tcubic_heterogeneous_lattice 2 | 3 | # USER INPUT 4 | 5 | unit_cell_size = 10 6 | min_strut_diameter = 1 7 | max_strut_diameter = 3 8 | min_node_diameter = 1.05 9 | max_node_diameter = 3.3 10 | Nx = 1 11 | Ny = 1 12 | Nz = 1 13 | min_truncation = 0.001 14 | max_truncation = 0.999 15 | # END USER INPUT 16 | 17 | # Register our custom plugin before use. 18 | cq.Workplane.tcubic_heterogeneous_lattice = tcubic_heterogeneous_lattice 19 | 20 | result = tcubic_heterogeneous_lattice(unit_cell_size, 21 | min_strut_diameter, 22 | max_strut_diameter, 23 | min_node_diameter, 24 | max_node_diameter, 25 | Nx, Ny, Nz, 26 | min_truncation, 27 | max_truncation, 28 | rule = 'linear_truncation') -------------------------------------------------------------------------------- /lattice_scripts/diamond.py: -------------------------------------------------------------------------------- 1 | from lq.topologies.diamond import diamond_heterogeneous_lattice 2 | 3 | # USER INPUT 4 | 5 | unit_cell_size = 10 6 | min_strut_diameter = 1 7 | max_strut_diameter = 1 8 | min_node_diameter = 1.1 9 | max_node_diameter = 1.1 10 | Nx = 2 11 | Ny = 2 12 | Nz = 2 13 | 14 | # END USER INPUT 15 | 16 | # Register our custom plugin before use. 17 | cq.Workplane.diamond_heterogeneous_lattice = diamond_heterogeneous_lattice 18 | 19 | #result = unit_cell(unit_cell_size, strut_radius) 20 | result = diamond_heterogeneous_lattice(unit_cell_size, 21 | min_strut_diameter, 22 | max_strut_diameter, 23 | min_node_diameter, 24 | max_node_diameter, 25 | Nx, Ny, Nz 26 | #rule = 'sin' 27 | ) -------------------------------------------------------------------------------- /lattice_scripts/gyroid.py: -------------------------------------------------------------------------------- 1 | # Python 2 | import cadquery as cq 3 | 4 | from lq.topologies.gyroid import gyroid_homogeneous_lattice 5 | cq.Workplane.gyroid_homogeneous_lattice = gyroid_homogeneous_lattice 6 | 7 | # BEGIN USER INPUT 8 | 9 | thickness = 0.1 10 | unit_cell_size = 10 11 | Nx = 2 12 | Ny = 2 13 | Nz = 2 14 | 15 | # END USER INPUT 16 | 17 | gyroid = gyroid_homogeneous_lattice(unit_cell_size, thickness, 18 | Nx, Ny, Nz) 19 | -------------------------------------------------------------------------------- /lattice_scripts/hetero-shwarz.py: -------------------------------------------------------------------------------- 1 | # Python 2 | import cadquery as cq 3 | import time 4 | from lq.topologies.schwartz import schwartz_p_heterogeneous_lattice 5 | cq.Workplane.schwartz_p_heterogeneous_lattice = schwartz_p_heterogeneous_lattice 6 | # import schwartz_d_heterogeneous_lattice instead for 7 | # the Schwarz D surface 8 | 9 | # BEGIN USER INPUT 10 | 11 | unit_cell_size = 3.98 12 | Nx = 1 13 | Ny = 1 14 | Nz = 1 15 | min_thickness = 0.9 16 | max_thickness = 2.1 17 | # END USER INPUT 18 | 19 | #timing performance 20 | start_time = time.time() 21 | schwartz = schwartz_p_heterogeneous_lattice(unit_cell_size,min_thickness, 22 | max_thickness, 23 | Nx, Ny, Nz 24 | #rule = 'sin' 25 | #rule = 'parabola' 26 | ) 27 | print('The excecution time is: %s seconds' % (time.time() - start_time)) 28 | -------------------------------------------------------------------------------- /lattice_scripts/heterogeneous_gyroid.py: -------------------------------------------------------------------------------- 1 | # Python 2 | import cadquery as cq 3 | 4 | from lq.topologies.gyroid import gyroid_heterogeneous_lattice 5 | cq.Workplane.gyroid_heterogeneous_lattice = gyroid_heterogeneous_lattice 6 | 7 | # BEGIN USER INPUT 8 | 9 | min_thickness = 1. 10 | max_thickness = 1. 11 | unit_cell_size = 20. 12 | Nx = 1 13 | Ny = 1 14 | Nz = 1 15 | 16 | # END USER INPUT 17 | 18 | gyroid = gyroid_heterogeneous_lattice(unit_cell_size, 19 | min_thickness, 20 | max_thickness, 21 | Nx, Ny, Nz, 22 | direction = 'z') 23 | -------------------------------------------------------------------------------- /lattice_scripts/heterogeneous_schwartz.py: -------------------------------------------------------------------------------- 1 | # Python 2 | import cadquery as cq 3 | import time 4 | from lq.topologies.schwartz import schwartz_p_heterogeneous_lattice 5 | cq.Workplane.schwartz_p_heterogeneous_lattice = schwartz_p_heterogeneous_lattice 6 | # import schwartz_d_heterogeneous_lattice instead for 7 | # the Schwarz D surface 8 | 9 | # BEGIN USER INPUT 10 | 11 | unit_cell_size = 10. 12 | Nx = 3 13 | Ny = 3 14 | Nz = 10 15 | min_thickness = 0.2 16 | max_thickness = 2.1 17 | # END USER INPUT 18 | 19 | #timing performance 20 | start_time = time.time() 21 | schwartz = schwartz_p_heterogeneous_lattice(unit_cell_size, min_thickness, max_thickness, 22 | Nx, Ny, Nz, 23 | #rule = 'sin' 24 | rule = 'parabola' 25 | ) 26 | print('The excecution time is: %s seconds' % (time.time() - start_time)) 27 | -------------------------------------------------------------------------------- /lattice_scripts/homogeneous_lattice.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | # User input begins here 4 | 5 | Ds = 1.0 # strut diameter 6 | UCsize = 10.0 # unit cell size 7 | Dn = 2.0 # node diameter 8 | Nx = 10 # N of cells in X direction 9 | Ny = 10 # N of cells in Y direction 10 | Nz = 10 # N of cells in Z direction 11 | 12 | # User input ends here 13 | 14 | def createUnitCells(self, 15 | strut_diameeter, 16 | unit_cell_size): 17 | strut_radius = strut_diameeter / 2.0 18 | half_unit_cell_size = unit_cell_size / 2.0 19 | # Defining the struts 20 | unit_cell = (cq.Workplane("front") 21 | # 1) 4 Z struts 22 | .rarray(unit_cell_size, unit_cell_size, 2, 2, True) 23 | .circle(strut_radius).extrude(unit_cell_size) # make a cylinder 24 | # 2) 4 X struts 25 | # We want to make a second cylinder perpendicular to the first, 26 | # but we have no face to base the workplane off 27 | .copyWorkplane( 28 | # create a temporary object with the required workplane 29 | cq.Workplane("right", 30 | origin = (-half_unit_cell_size, 0, half_unit_cell_size)) 31 | ) 32 | .rarray(unit_cell_size, unit_cell_size, 2, 2, True) 33 | .circle(strut_radius).extrude(unit_cell_size) 34 | # 3) 4 Y struts 35 | .copyWorkplane( 36 | # create a temporary object with the required workplane 37 | cq.Workplane("top", 38 | origin = (0, - half_unit_cell_size, half_unit_cell_size)) 39 | ) 40 | .rarray(unit_cell_size, unit_cell_size, 2, 2, True) 41 | .circle(strut_radius).extrude(unit_cell_size)) 42 | return self.eachpoint(lambda loc: unit_cell.val().located(loc), True) 43 | cq.Workplane.createUnitCells = createUnitCells 44 | 45 | def createNodes(self, 46 | node_diameter, 47 | unit_cell_size, 48 | delta = 0.01 # a small coefficient is needed because CQ thinks that it cuts through emptiness 49 | ): 50 | added_node_diameter = node_diameter + delta 51 | node_radius = node_diameter / 2.0 52 | bottom_nodes = (cq.Workplane("XY") 53 | .rarray(unit_cell_size, unit_cell_size, 2, 2, True) # bottom plane, 4 nodes 54 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 55 | .edges("|Z") 56 | .fillet(node_radius) 57 | .edges("|X") 58 | .fillet(node_radius)) 59 | return self.eachpoint(lambda loc: bottom_nodes.val().located(loc), True) 60 | cq.Workplane.createNodes = createNodes 61 | 62 | # Generating the positions for each unit cell 63 | pts = [] 64 | for i in range(Nx): 65 | for j in range(Ny): 66 | for k in range(Nz): 67 | pts.append((i * UCsize, j * UCsize, k * UCsize)) 68 | 69 | 70 | lattice = (cq.Workplane("XY") 71 | .pushPoints(pts) 72 | .createUnitCells(Ds, UCsize)) 73 | 74 | # This monstrosity is needed because createNodes creates 75 | # nodes only at the bottom of each unit cell 76 | # We simply add an 'empty' unit cell layer on top 77 | # and put nodes at the bottom of it. 78 | # Could it be done better? Yes. Too bad. 79 | k += 1 80 | for i in range(Nx): 81 | for j in range(Ny): 82 | pts.append((i * UCsize, j * UCsize, k * UCsize)) 83 | 84 | pts.append((i * UCsize, j * UCsize, k * UCsize)) 85 | nodes = (cq.Workplane("XY") 86 | .pushPoints(pts) 87 | .createNodes(Dn, UCsize)) 88 | -------------------------------------------------------------------------------- /lattice_scripts/lego_brick.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | lbumps = 4 # number of bumps long 4 | wbumps = 2 # number of bumps wide 5 | thin = True # True for thin, False for thick 6 | 7 | pitch = 8.0 8 | clearance = 0.1 9 | bumpDiam = 4.8 10 | bumpHeight = 1.8 11 | height = 3.2 if thin else 9.6 12 | 13 | t = (pitch - (2 * clearance) - bumpDiam) / 2.0 14 | postDiam = pitch - t # works out to 6.5 15 | total_length = lbumps * pitch - 2.0 * clearance 16 | total_width = wbumps * pitch - 2.0 * clearance 17 | 18 | s = cq.Workplane("XY").box(total_length, total_width, height) 19 | s = s.faces("Z").workplane(). \ 21 | rarray(pitch, pitch, lbumps, wbumps, True). \ 22 | circle(bumpDiam / 2.0). \ 23 | extrude(height - t) 24 | 25 | tmp = s.faces(" 1 and wbumps > 1: 28 | tmp = tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center = True). \ 29 | circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t) 30 | elif lbumps > 1: 31 | tmp = tmp.rarray(pitch, pitch, lbumps - 1, 1, center = True). \ 32 | circle(t).extrude(height - t) 33 | elif wbumps > 1: 34 | tmp = tmp.rarray(pitch, pitch, 1, wbumps - 1, center = True). \ 35 | circle(t).extrude(height - t) 36 | else: 37 | tmp = s 38 | 39 | # replay(tmp) 40 | #result = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125) -------------------------------------------------------------------------------- /lattice_scripts/rco.py: -------------------------------------------------------------------------------- 1 | from lq.topologies.rco import rco_heterogeneous_lattice 2 | cq.Workplane.rco_heterogeneous_lattice = rco_heterogeneous_lattice 3 | 4 | # USER INPUT 5 | 6 | unit_cell_size = 10 7 | min_strut_diameter = 5. 8 | max_strut_diameter = 1 9 | min_node_diameter = 5.5 10 | max_node_diameter = 1.05 11 | Nx = 1 12 | Ny = 10 13 | Nz = 10 14 | min_truncation = 0.001 15 | max_truncation = 0.999 16 | 17 | # END USER INPUT 18 | 19 | # Register our custom plugin before use. 20 | 21 | #result = unit_cell(unit_cell_size, strut_radius) 22 | result = rco_heterogeneous_lattice(unit_cell_size, 23 | min_strut_diameter, 24 | max_strut_diameter, 25 | min_node_diameter, 26 | max_node_diameter, 27 | Nx, Ny, Nz, 28 | min_truncation, 29 | max_truncation, 30 | 'linear_truncation') -------------------------------------------------------------------------------- /lattice_scripts/reverse_gyroid.py: -------------------------------------------------------------------------------- 1 | # Python 2 | import cadquery as cq 3 | 4 | from lq.topologies.gyroid import gyroid_heterogeneous_lattice 5 | from lq.commons import make_support_plate 6 | cq.Workplane.gyroid_heterogeneous_lattice = gyroid_heterogeneous_lattice 7 | 8 | # BEGIN USER INPUT 9 | 10 | min_thickness = 0.3104 * 10 11 | max_thickness = 0.3104 * 10 12 | unit_cell_size = 4 * 10 13 | Nx = 1 14 | Ny = 1 15 | Nz = 1 16 | 17 | # END USER INPUT 18 | 19 | gyroid = gyroid_heterogeneous_lattice(unit_cell_size, 20 | min_thickness, 21 | max_thickness, 22 | Nx, Ny, Nz, 23 | direction = 'x') 24 | 25 | box = cq.Workplane().transformed( 26 | offset = tuple([0.5*unit_cell_size]*3)).box( 27 | unit_cell_size*0.8, unit_cell_size*0.8, unit_cell_size*0.8) 28 | 29 | cfd = box -gyroid -------------------------------------------------------------------------------- /lattice_scripts/schwartz-d.py: -------------------------------------------------------------------------------- 1 | # Python 2 | import cadquery as cq 3 | from math import cos, sqrt 4 | import numpy as np 5 | import time 6 | from lq.topologies.schwartz import schwartz_d_heterogeneous_lattice 7 | cq.Workplane.schwartz_d_heterogeneous_lattice = schwartz_d_heterogeneous_lattice 8 | 9 | # BEGIN USER INPUT 10 | 11 | unit_cell_size = 20 12 | Nx = 1 13 | Ny = 1 14 | Nz = 20 15 | min_thickness = 0.2 16 | max_thickness = 3 17 | # END USER INPUT 18 | 19 | #timing performance 20 | start_time = time.time() 21 | 22 | result = schwartz_d_heterogeneous_lattice(unit_cell_size, min_thickness, max_thickness, 23 | Nx, Ny, Nz, 24 | rule = 'sin' 25 | ) 26 | 27 | print('The excecution time is: %s seconds' % (time.time() - start_time)) 28 | -------------------------------------------------------------------------------- /lattice_scripts/simple_cubic.py: -------------------------------------------------------------------------------- 1 | from lq.topologies.cubic import cubic_heterogeneous_lattice 2 | 3 | # USER INPUT 4 | 5 | unit_cell_size = 10 6 | min_strut_diameter = 1. 7 | max_strut_diameter = 1. 8 | min_node_diameter = 1. 9 | max_node_diameter = 1. 10 | Nx = 5 11 | Ny = 5 12 | Nz = 10 13 | min_truncation = 0.01 14 | max_truncation = 0.99 15 | # END USER INPUT 16 | 17 | # Register our custom plugin before use. 18 | cq.Workplane.cubic_heterogeneous_lattice = cubic_heterogeneous_lattice 19 | 20 | result = cubic_heterogeneous_lattice(unit_cell_size, 21 | min_strut_diameter, 22 | max_strut_diameter, 23 | min_node_diameter, 24 | max_node_diameter, 25 | Nx, Ny, Nz) 26 | #min_truncation, 27 | #max_truncation, -------------------------------------------------------------------------------- /lattice_scripts/support_plate.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | uc_size = 5 4 | Nx = 15 5 | Ny = 15 6 | thickness = 5 7 | allowance = 1 8 | 9 | x = Nx * uc_size + (allowance * 2) 10 | y = Ny * uc_size + (allowance * 2) 11 | 12 | support_plane = cq.Workplane().box(x, y, thickness) -------------------------------------------------------------------------------- /lattice_scripts/t_cubic.py: -------------------------------------------------------------------------------- 1 | from lq.topologies.tcubic import tcubic_heterogeneous_lattice 2 | 3 | # USER INPUT 4 | 5 | unit_cell_size = 10 6 | min_strut_diameter = 1 7 | max_strut_diameter = 1 8 | min_node_diameter = 1.05 9 | max_node_diameter = 1.05 10 | Nx = 10 11 | Ny = 1 12 | Nz = 10 13 | min_truncation = 0.001 14 | max_truncation = 0.999 15 | # END USER INPUT 16 | 17 | # Register our custom plugin before use. 18 | cq.Workplane.tcubic_heterogeneous_lattice = tcubic_heterogeneous_lattice 19 | 20 | result = tcubic_heterogeneous_lattice(unit_cell_size, 21 | min_strut_diameter, 22 | max_strut_diameter, 23 | min_node_diameter, 24 | max_node_diameter, 25 | Nx, Ny, Nz, 26 | min_truncation, 27 | max_truncation, 28 | rule = 'linear_truncation') -------------------------------------------------------------------------------- /lattice_scripts/tco.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | from lq.topologies.tco import tco_heterogeneous_lattice 4 | cq.Workplane.tco_heterogeneous_lattice = tco_heterogeneous_lattice 5 | 6 | # USER INPUT 7 | 8 | unit_cell_size = 100 9 | min_strut_diameter = 10 10 | max_strut_diameter = 10 11 | min_node_diameter = 10 12 | max_node_diameter = 10 13 | Nx = 1 14 | Ny = 1 15 | Nz = 1 16 | 17 | # END USER INPUT 18 | 19 | # Register our custom plugin before use. 20 | 21 | result = tco_heterogeneous_lattice(unit_cell_size, 22 | min_strut_diameter, 23 | max_strut_diameter, 24 | min_node_diameter, 25 | max_node_diameter, 26 | Nx, Ny, Nz) 27 | -------------------------------------------------------------------------------- /lattice_scripts/tetra_lattice.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | from math import pi, cos, acos, sin, sqrt, atan, tan 3 | import numpy as np 4 | 5 | 6 | # TETRAKAIDECAHEDRON 7 | 8 | def pmcross(self, r1=1, fn=6, forConstruction=False): 9 | def _makeCross(loc): 10 | pnts = [] 11 | t = 2*pi / fn 12 | R = r1/2 / sin(t) 13 | for i in range(fn+1): 14 | pts = [R*cos(i * t + pi/fn), R*sin(i * t + pi/fn)] 15 | pnts.append(cq.Vector(pts[0], pts[1], 0)) 16 | 17 | return cq.Wire.makePolygon(pnts, forConstruction).locate(loc) 18 | 19 | return self.eachpoint(_makeCross, True) 20 | cq.Workplane.pmcross = pmcross 21 | 22 | L = 20 23 | W = 20 24 | H = 20 25 | D = 12 # Cell diameter 26 | WT = 1 27 | FRO = 3 28 | FRI = 2 29 | NPS = 6 30 | 31 | A = 180 / NPS 32 | BS = acos(1/sqrt(3)) * 180/pi 33 | BH = acos(1/3) * 180/pi 34 | 35 | tc = 1. 36 | dc = (D -2*WT +2*tc) / (2 * cos((90-BS)*pi/180)) 37 | 38 | dcc = dc * tan(A*pi/180) 39 | 40 | path_h = cq.Workplane('XY').pmcross(dc, NPS) 41 | cell_h0 = cq.Workplane('XZ').workplane().center(dc/2,0).polygon(10,tc).sweep(path_h) 42 | 43 | lo = [cell_h0.val()] 44 | cell_s = cell_h0 45 | 46 | # ADD HEXAGONS 47 | cell_h1 = cell_h0.translate((-dc*sin(A*pi/180), dc*cos(A*pi/180), 0)) 48 | V1 = (-dcc*cos(A*pi/180), dcc*sin(A*pi/180), 0) 49 | V2 = (0, dcc, 0) 50 | cell_h1 = cell_h1.rotate(V1,V2,BH) 51 | cell_h1 = cell_h1.cut(cell_h0) 52 | cell_s = cell_s.union(cell_h1, glue=True) 53 | 54 | cell_h2 = cell_h0.translate((-dc*sin(A*pi/180), -dc*cos(A*pi/180), 0)) 55 | V1 = (-dcc*cos(A*pi/180), -dcc*sin(A*pi/180), 0) 56 | V2 = (0, -dcc, 0) 57 | cell_h2 = cell_h2.rotate(V1,V2,-BH) 58 | cell_h2 = cell_h2.cut(cell_h0) 59 | cell_s = cell_s.union(cell_h2, glue=True) 60 | 61 | cell_h3 = cell_h0.translate((dc, 0, 0)) 62 | V1 = (dc/2, -dcc/2, 0) 63 | V2 = (dc/2, dcc/2, 0) 64 | cell_h3 = cell_h3.rotate(V1,V2,-BH) 65 | cell_h3 = cell_h3.cut(cell_h0) 66 | cell_s = cell_s.union(cell_h3, glue=True) 67 | 68 | 69 | # ADD SYMMETRY 70 | dz = dc * cos((90-BH)*pi/180) + dcc * cos((90-BS)*pi/180) 71 | cell = cell_s.rotate((0,0,0),(1,0,0),180).rotate((0,0,0),(0,0,1),60).translate((0,0,dz)) 72 | cell = cell_s.union(cell).rotateAboutCenter((0,1,0),90-BS) 73 | 74 | # PATTERN 75 | dx = dc * sin((90-BS)*pi/180) + dcc/2 - tc/2 76 | dy = dz = dc * cos((90-BS)*pi/180) + dcc/2 + tc/2 77 | x_af0 = np.arange(0, H, dx)[:-2] 78 | y_af0 = np.arange(0, W, dy)[1:] 79 | z_af0 = np.arange(0, L, dz)#[:-1] 80 | 81 | x_af, y_af, z_af = np.meshgrid(x_af0, y_af0, z_af0) 82 | xyz_af = np.array([[x,y,z] for x,y,z in zip(x_af.flatten(), y_af.flatten(), z_af.flatten())]) 83 | 84 | xyz_af[::2, 0] += dx 85 | xyz_af[::2, 1] += dy 86 | xyz_af[::2, 2] += dz 87 | xyz_af = np.unique(xyz_af, axis = 0) 88 | xyz_af = [tuple(e) for e in xyz_af] 89 | 90 | def cell_pos(pos): 91 | return cell.translate(pos).val() 92 | cellS = cq.Workplane('XY').pushPoints(xyz_af).each(cell_pos).combine() 93 | cellS = cellS.translate((0, -W/2-dc/2+FRI, -L/2-dz/2)) -------------------------------------------------------------------------------- /lattice_scripts/tire.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | from lq.commons import eachpointAdaptive 4 | from lq.topologies.conformal import cylinder 5 | 6 | z_uc = 8 # no. of Z unit cell 7 | 8 | angle_uc_size = 15 # degrees 9 | z_uz_size = 100 # mm 10 | r_uz_size = 100 # mm 11 | 12 | min_thickness = 10 # mm 13 | max_thickness = 40 # mm 14 | 15 | inner_radius = 350 # mm 16 | outer_radius = 950 # mm 17 | 18 | arcs, radials, axials = cylinder(z_uc, 19 | angle_uc_size, 20 | z_uz_size, r_uz_size, 21 | min_thickness, 22 | max_thickness, 23 | inner_radius, 24 | outer_radius) 25 | -------------------------------------------------------------------------------- /lattice_scripts/tpms_test.py: -------------------------------------------------------------------------------- 1 | # Python 2 | import cadquery as cq 3 | 4 | from lq.topologies.gyroid import gyroid_homogeneous_lattice 5 | cq.Workplane.gyroid_homogeneous_lattice = gyroid_homogeneous_lattice 6 | 7 | # BEGIN USER INPUT 8 | 9 | thickness = 0.1 10 | unit_cell_size = 10 11 | Nx = 2 12 | Ny = 2 13 | Nz = 2 14 | 15 | # END USER INPUT 16 | 17 | gyroid = gyroid_homogeneous_lattice(unit_cell_size, thickness, 18 | Nx, Ny, Nz) 19 | -------------------------------------------------------------------------------- /lattice_scripts/tpms_transition.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | from lq.topologies.tpms_transition import transition_unit_cell#, gyroid_half_x, p_half, transition 4 | from lq.topologies.tpms_transition import transition_layer 5 | #cq.Workplane.gyroid_half_x = gyroid_half_x 6 | #cq.Workplane.p_half = p_half 7 | #cq.Workplane.transition = transition 8 | cq.Workplane.transition_unit_cell = transition_unit_cell 9 | 10 | from lq.commons import eachpointAdaptive 11 | 12 | # BEGIN USER INPUT 13 | 14 | min_thickness = 2. 15 | max_thickness = 2. 16 | unit_cell_size = 100. 17 | Nx = 1 18 | Ny = 1 19 | Nz = 1 20 | 21 | # END USER INPUT 22 | 23 | #lattice, tr = cq.Workplane().transition_unit_cell(thickness, unit_cell_size) 24 | 25 | g, p, tr = transition_layer( 26 | min_thickness, max_thickness, unit_cell_size, Ny, Nz, 'Z+') 27 | 28 | -------------------------------------------------------------------------------- /lattice_scripts/unit_cell.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | strut_diameeter = 1.0 4 | unit_cell_size = 10.0 5 | node_diameter = 2.0 6 | delta = 0.01 # a small coefficient is needed because CQ thinks that it cuts through emptiness 7 | strut_radius = strut_diameeter / 2.0 8 | half_unit_cell_size = unit_cell_size / 2.0 9 | 10 | # Defining the struts 11 | unit_cell = (cq.Workplane("front") 12 | # 1) 4 Z struts 13 | .rarray(unit_cell_size, unit_cell_size, 2, 2, True) 14 | .circle(strut_radius).extrude(unit_cell_size) # make a cylinder 15 | # 2) 4 X struts 16 | # We want to make a second cylinder perpendicular to the first, 17 | # but we have no face to base the workplane off 18 | .copyWorkplane( 19 | # create a temporary object with the required workplane 20 | cq.Workplane("right", 21 | origin = (-half_unit_cell_size, 0, half_unit_cell_size)) 22 | ) 23 | .rarray(unit_cell_size, unit_cell_size, 2, 2, True) 24 | .circle(strut_radius).extrude(unit_cell_size) 25 | # 3) 4 Y struts 26 | .copyWorkplane( 27 | # create a temporary object with the required workplane 28 | cq.Workplane("top", 29 | origin = (0, - half_unit_cell_size, half_unit_cell_size)) 30 | ) 31 | .rarray(unit_cell_size, unit_cell_size, 2, 2, True) 32 | .circle(strut_radius).extrude(unit_cell_size)) 33 | 34 | 35 | # Defining the nodes 36 | added_node_diameter = node_diameter + delta 37 | node_radius = node_diameter / 2.0 38 | bottom_nodes = (cq.Workplane("XY") 39 | .rarray(unit_cell_size, unit_cell_size, 2, 2, True) # bottom plane, 4 nodes 40 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 41 | .edges("|Z") 42 | .fillet(node_radius) 43 | .edges("|X") 44 | .fillet(node_radius)) 45 | top_nodes = (cq.Workplane("XY", 46 | origin = (0, 0, unit_cell_size)) 47 | .rarray(unit_cell_size, unit_cell_size, 2, 2, True) # top plane, 4 nodes 48 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 49 | .edges("|Z") 50 | .fillet(node_radius) 51 | .edges("|X") 52 | .fillet(node_radius)) 53 | 54 | """ 55 | sphere = (cq.Workplane("XY") 56 | .rarray(unit_cell_size, unit_cell_size, 2, 2, True) 57 | .threePointArc((1.0, 1.0), (0.0, 2.0)).close().revolve()) 58 | """ 59 | -------------------------------------------------------------------------------- /lattice_scripts/varying_Ls.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | from random import random 3 | 4 | def createLs(self, diam = 10): 5 | L = cq.Workplane().circle(diam / 2).extrude(- 500) 6 | L = (L.transformed(offset = cq.Vector(0, 0, - 500)) 7 | .transformed(rotate = cq.Vector(90, 0, 0)) 8 | .circle(diam / 2.0).extrude(- 500)) 9 | 10 | return self.union(self.eachpoint(lambda loc: L.val().located(loc), True)) 11 | 12 | cq.Workplane.createLs = createLs 13 | 14 | pnts = [] 15 | for i in range(10): 16 | pnts.append((i * 100, 0)) 17 | diams = [5 + 50 * random() for _ in range(len(pnts))] 18 | 19 | L2 = cq.Workplane().tag('base') 20 | 21 | for pnt, diam in zip(pnts, diams): 22 | L2 = L2.workplaneFromTagged('base').center(*pnt).createLs(diam) -------------------------------------------------------------------------------- /lattice_scripts/varying_truncation.py: -------------------------------------------------------------------------------- 1 | from lq.topologies.tcubic import tcubic_heterogeneous_lattice 2 | 3 | # USER INPUT 4 | 5 | unit_cell_size = 10 6 | min_strut_diameter = 4.5 7 | max_strut_diameter = 4 8 | min_node_diameter = 4.95 9 | max_node_diameter = 4.4 10 | Nx = 1 11 | Ny = 10 12 | Nz = 10 13 | min_truncation = 0.001 14 | max_truncation = 0.999 15 | # END USER INPUT 16 | 17 | # Register our custom plugin before use. 18 | cq.Workplane.tcubic_heterogeneous_lattice = tcubic_heterogeneous_lattice 19 | 20 | result = tcubic_heterogeneous_lattice(unit_cell_size, 21 | min_strut_diameter, 22 | max_strut_diameter, 23 | min_node_diameter, 24 | max_node_diameter, 25 | Nx, Ny, Nz, 26 | min_truncation, 27 | max_truncation, 28 | rule = 'linear', 29 | direction = 'X', 30 | truncation = 'linear') -------------------------------------------------------------------------------- /lattice_scripts/voronoi.py: -------------------------------------------------------------------------------- 1 | """ 2 | (L, H, W, t) = (100.0, 20.0, 20.0, 1.0) 3 | pts = [ 4 | (0, H/2.0, 0), 5 | (W/2.0, H/2.0, 0), 6 | (W/2.0, (H/2.0 - t), 0), 7 | (t/2.0, (H/2.0 - t), 0), 8 | (t/2.0, (t - H/2.0), 0), 9 | (W/2.0, (t - H/2.0), 0), 10 | (W/2.0, H/-2.0, 0), 11 | (0, H/-2.0, 0) 12 | ] 13 | result = cq.Workplane().polyline(pts) 14 | """ 15 | 16 | import numpy as np 17 | import scipy 18 | 19 | import cadquery as cq 20 | 21 | from lq.commons import cylinder_by_two_points, make_sphere 22 | 23 | <<<<<<< HEAD 24 | air_traffic_mess = np.random.random_sample((5000000, 3))*465 25 | ======= 26 | air_traffic_mess = np.random.random_sample((100000, 3)) * 9 27 | print(f'{len(air_traffic_mess)} seeds generated') 28 | >>>>>>> 74ebf6f0590a6abb5159c91dddb09e0b85c41647 29 | vor = scipy.spatial.Voronoi(air_traffic_mess) 30 | 31 | def fits(pt): 32 | for i in pt: 33 | if i < 0 or i > 1: 34 | return False 35 | return True 36 | 37 | <<<<<<< HEAD 38 | ======= 39 | print(f'{len(vor.ridge_vertices)} ridges detected') 40 | j = 0 41 | >>>>>>> 74ebf6f0590a6abb5159c91dddb09e0b85c41647 42 | for ridge_indices in vor.ridge_vertices: 43 | voronoi_ridge_coords = vor.vertices[ridge_indices] 44 | print(f'Ridge {j} of {len(vor.ridge_vertices)}...') 45 | j += 1 46 | for i in range(1, len(voronoi_ridge_coords[...,0])): 47 | startPoint = ( 48 | voronoi_ridge_coords[...,0][0], 49 | voronoi_ridge_coords[...,1][0], 50 | voronoi_ridge_coords[...,2][0] 51 | ) 52 | endPoint = ( 53 | voronoi_ridge_coords[...,0][i], 54 | voronoi_ridge_coords[...,1][i], 55 | voronoi_ridge_coords[...,2][i] 56 | ) 57 | if not fits(startPoint) or not fits(endPoint): 58 | continue 59 | print(startPoint, endPoint) 60 | #edge = cq.Edge.makeLine(startPoint, endPoint) 61 | #show_object(edge) 62 | radius = 0.01 63 | beam = cylinder_by_two_points(startPoint, endPoint, radius) 64 | show_object(beam) 65 | #sphere = make_sphere(cq.Vector(startPoint), radius) 66 | #show_object(sphere) 67 | #sphere = make_sphere(cq.Vector(endPoint), radius) 68 | #show_object(sphere) 69 | 70 | #result = cq.Workplane().polyline(pts) -------------------------------------------------------------------------------- /lattice_scripts/wavy_circle.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | R = 2.5 4 | A = 4*R/70 5 | N = 15 6 | 7 | phi = np.arange(0, 2 * np.pi, 0.01) 8 | rho = R + A * np.sin(N * phi) 9 | 10 | sPnts = [] 11 | for p, r in zip(phi, rho): 12 | x = 2 * r * np.cos(p) 13 | y = r * np.sin(p) 14 | sPnts += [(x, y, 0)] 15 | 16 | s = cq.Workplane("XY").moveTo(sPnts[0][0], sPnts[0][1]) 17 | r = s.spline(sPnts[1:], includeCurrent = True).close() 18 | result = r.workplane(offset = 10.0).ellipse(2.5, 1.25).loft(combine=True) -------------------------------------------------------------------------------- /lq/commons.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | def eachpointAdaptive( 4 | self, 5 | callback, 6 | callback_extra_args = None, 7 | useLocalCoords = False 8 | ): 9 | """ 10 | Same as each(), except that (1) each item on the stack is converted into a point before it 11 | is passed into the callback function and (2) it allows to pass in additional arguments, one 12 | set for each object to process. 13 | 14 | Conversion of stack items into points means: the resulting stack has a point for each object 15 | on the original stack. Vertices and points remain a point. Faces, Wires, Solids, Edges, and 16 | Shells are converted to a point by using their center of mass. If the stack has zero length, a 17 | single point is returned, which is the center of the current workplane / coordinate system. 18 | 19 | This is adapted from here: 20 | https://github.com/CadQuery/cadquery/issues/628#issuecomment-807493984 21 | 22 | :param callback_extra_args: Array of dicts for keyword arguments that will be 23 | provided to the callback in addition to the obligatory location argument. The outer array 24 | level is indexed by the objects on the stack to iterate over, in the order they appear in 25 | the Workplane.objects attribute. The inner arrays are dicts of keyword arguments, each dict 26 | for one call of the callback function each. If a single dict is provided, then this set of 27 | keyword arguments is used for every call of the callback. 28 | :param useLocalCoords: Should points provided to the callback be in local or global coordinates. 29 | 30 | :return: CadQuery object which contains a list of vectors (points) on its stack. 31 | 32 | .. todo:: Implement that callback_extra_args can also be a single dict. 33 | .. todo:: Implement that empty dicts are used as arguments for calls to the callback if not 34 | enough sets are provided for all objects on the stack. 35 | """ 36 | print('Building an element of an array...') 37 | # Convert the objects on the stack to a list of points. 38 | pnts = [] 39 | plane = self.plane 40 | loc = self.plane.location 41 | if len(self.objects) == 0: 42 | # When nothing is on the stack, use the workplane origin point. 43 | pnts.append(cq.Location()) 44 | else: 45 | for o in self.objects: 46 | if isinstance(o, (cq.Vector, cq.Shape)): 47 | pnts.append(loc.inverse * cq.Location(plane, o.Center())) 48 | else: 49 | pnts.append(o) 50 | 51 | # If no extra keyword arguments are provided to the callback, provide a list of empty dicts as 52 | # structure for the **() deferencing to work below without issues. 53 | if callback_extra_args is None: 54 | callback_extra_args = [{} for p in pnts] 55 | 56 | # Call the callback for each point and collect the objects it generates with each call. 57 | res = [] 58 | for i, p in enumerate(pnts): 59 | p = (p * loc) if useLocalCoords == False else p 60 | extra_args = callback_extra_args[i] 61 | p_res = callback(p, **extra_args) 62 | p_res = p_res.move(loc) if useLocalCoords == True else p_res 63 | res.append(p_res) 64 | 65 | # For result objects that are wires, make them pending if necessary. 66 | for r in res: 67 | if isinstance(r, cq.Wire) and not r.forConstruction: 68 | self._addPendingWire(r) 69 | print(f'Success!\n{"-"*20}') 70 | return self.newObject(res) 71 | # Register our custom plugin before use. 72 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 73 | 74 | def cylinder_tranformation(radius, height, 75 | rotation = cq.Vector(0, 0, 0), 76 | transformation = cq.Vector(0, 0, 0)): 77 | """ 78 | Create a cylinder with a 79 | circular cross section, and a height 80 | 81 | :param radius: The radius of the cylinder 82 | :param height: the height of the cylinder 83 | :param rotation: a vector that defines the rotation of the cylinder around its center 84 | :param transformation: a vector that represents the x, y, and z coordinates of the center of the 85 | cylinder 86 | :return: A cylinder with the specified parameters. 87 | """ 88 | return (cq.Workplane() 89 | .transformed(offset = transformation, 90 | rotate = rotation) 91 | .circle(radius) 92 | .extrude(height)) 93 | 94 | def cylinder_sequential_tranformation(radius, height, 95 | rotation = cq.Vector(0, 0, 0), 96 | transformation = cq.Vector(0, 0, 0)): 97 | """ 98 | Create a cylinder with a 99 | circular cross section, and a height 100 | in a position based on linear and angular transformation 101 | sequentially 102 | 103 | :param radius: The radius of the cylinder 104 | :param height: the height of the cylinder 105 | :param rotation: a vector that defines the rotation of the cylinder around its center 106 | :param transformation: a vector that represents the x, y, and z coordinates of the center of the 107 | cylinder 108 | :return: A cylinder with the specified parameters. 109 | """ 110 | return (cq.Workplane() 111 | .transformed(offset = transformation) 112 | .transformed(rotate = rotation) 113 | .circle(radius) 114 | .extrude(height)) 115 | 116 | def cuboid_tranformation(side, height, fillet, 117 | rotation = cq.Vector(0, 0, 0), 118 | transformation = cq.Vector(0, 0, 0)): 119 | """ 120 | Create a cylinder with a 121 | circular cross section, and a height 122 | 123 | :param radius: The radius of the cylinder 124 | :param height: the height of the cylinder 125 | :param rotation: a vector that defines the rotation of the cylinder around its center 126 | :param transformation: a vector that represents the x, y, and z coordinates of the center of the 127 | cylinder 128 | :return: A cylinder with the specified parameters. 129 | """ 130 | return (cq.Workplane("XY") 131 | .transformed(offset = transformation, 132 | rotate = rotation) 133 | .rect(side, side) 134 | .extrude(height) 135 | .edges().fillet(fillet) 136 | ) 137 | 138 | def cylinder_by_two_points(p1: tuple, 139 | p2: tuple, 140 | radius: float 141 | ) -> cq.cq.Workplane: 142 | """ 143 | Create a cylinder with a spline (which is in fact a line) 144 | path and two circles as end caps 145 | 146 | Args: 147 | p1 (tuple): tuple of the form (x, y, z) 148 | p2 (tuple): tuple of the form (x, y, z) 149 | radius (float): radius of the cylinder 150 | 151 | Returns: 152 | A CQ object. 153 | """ 154 | 155 | path = cq.Workplane().moveTo(p1[0], p1[1]).spline([p1, p2]) 156 | 157 | sweep = (cq.Workplane("XY") 158 | .pushPoints([path.val().locationAt(0)]).circle(radius) 159 | .pushPoints([path.val().locationAt(1)]).circle(radius) 160 | .consolidateWires() 161 | .sweep(path, multisection = True) 162 | ) 163 | return sweep 164 | 165 | def make_sphere(center: cq.Vector, radius: float) -> cq.cq.Workplane: 166 | """ 167 | It creates a sphere centered at `center` with radius `radius` 168 | 169 | Args: 170 | center (cq.Vector): The center of the sphere. 171 | radius (float): the radius of the sphere 172 | 173 | Returns: 174 | A cq.cq.Workplane object 175 | """ 176 | center = center - cq.Vector(0, radius, 0) 177 | wire = cq.Workplane('XY').transformed(offset = center) 178 | wire = wire.threePointArc((radius, radius), (0.0, 2.0 * radius)).close() 179 | sphere = wire.revolve() 180 | return sphere 181 | 182 | # The unit_cell class is a class that contains a unit cell size 183 | class unit_cell(): 184 | def __init__(self, unit_cell_size): 185 | self.unit_cell_size = unit_cell_size 186 | 187 | 188 | def make_support_plate( 189 | Nx: int, 190 | Ny: int, 191 | Nz: int, 192 | unit_cell_size: float, 193 | thickness: float, 194 | margin: float 195 | ) -> cq.cq.Workplane: 196 | side_x = Nx * unit_cell_size + 2 * margin 197 | side_y = Ny * unit_cell_size + 2 * margin 198 | result = cq.Workplane().transformed( 199 | offset = (0.5 * side_x - margin, 200 | 0.5 * side_y - margin, 201 | -0.5 * thickness)) 202 | result = result.box(side_x, side_y, thickness) 203 | return result 204 | -------------------------------------------------------------------------------- /lq/topologies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalovisko/LatticeQuery/7078d8cf11f658590c88e6e84e4c728ae2f8be9b/lq/topologies/__init__.py -------------------------------------------------------------------------------- /lq/topologies/bcc.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright (C) 2021, Advanced Design and Manufacturing Lab (ADML). 3 | # All rights reserved. 4 | # 5 | # This software and its documentation and related materials are owned by 6 | # ADML. The software may only be incorporated into application programs owned 7 | # by members of ADML. The structure and organization of this software are 8 | # the valuable trade secrets of ADML and its suppliers. The software is also 9 | # protected by copyright law and international treaty provisions. 10 | # 11 | # By use of this software, its documentation or related materials, you 12 | # acknowledge and accept the above terms. 13 | ############################################################################## 14 | 15 | from ..commons import eachpointAdaptive 16 | 17 | from math import hypot, acos, degrees 18 | import numpy as np 19 | 20 | import cadquery as cq 21 | 22 | # Register our custom plugins before use. 23 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 24 | 25 | def create_diagonal_strut( 26 | location: cq.occ_impl.geom.Location, 27 | unit_cell_size: np.float64, 28 | radius: np.float64, 29 | angle_x: np.float64, 30 | angle_y: np.float64) -> cq.occ_impl.shapes.Compound: 31 | """ 32 | Creates a solid model of a diagonal cylindrical strut. 33 | The angle is chosen with respect to the positive X direction 34 | 35 | Parameters 36 | ---------- 37 | location : cq.occ_impl.geom.Location 38 | point location of the strut 39 | unit_cell_size : np.float64 40 | unit cell size (in mm) 41 | angle_x : np.float64 42 | angle between the stut and the positibe direction of X 43 | in the local coordinate system 44 | angle_y : np.float64 45 | angle between the stut and the positibe direction of Y 46 | in the local coordinate system 47 | Returns 48 | ------- 49 | cq.occ_impl.shapes.Compound 50 | a solid model of the strut 51 | 52 | """ 53 | hypot2D = hypot(unit_cell_size, unit_cell_size) 54 | hypot3D = hypot(hypot2D, unit_cell_size) 55 | result = ( 56 | cq.Workplane() 57 | .transformed(rotate = cq.Vector(angle_x, angle_y, 0)) 58 | .circle(radius) 59 | .extrude(hypot3D) 60 | ) 61 | return result.val().located(location) 62 | 63 | def bcc_diagonals( 64 | unit_cell_size: np.float64, 65 | strut_radius: np.float64) -> cq.cq.Workplane: 66 | """ 67 | Creates a solid model of the diagonals in a BCC unit cell. 68 | 69 | Parameters 70 | ---------- 71 | location : cq.occ_impl.geom.Location 72 | point location of the strut 73 | unit_cell_size : np.float64 74 | unit cell size (in mm) 75 | strut_radius: np.float64 76 | strut radius (in mm) 77 | Returns 78 | ------- 79 | cq.occ_impl.shapes.Compound 80 | a solid model of the strut 81 | 82 | """ 83 | # In a cube ABCDA1B1C1D1 this is the angle C1AD 84 | angle_C1AD = 90 - degrees(acos(3**-.5)) 85 | corner_points = unit_cell_size * np.array( 86 | [(0, 0), 87 | (1, 0), 88 | (1, 1), 89 | (0, 1)] 90 | ) 91 | result = ( 92 | cq.Workplane("XY") 93 | .pushPoints(corner_points) 94 | .eachpointAdaptive( 95 | create_diagonal_strut, 96 | callback_extra_args = [ 97 | {"unit_cell_size": unit_cell_size, 98 | "radius": strut_radius, 99 | "angle_x": - 45, 100 | "angle_y": angle_C1AD}, 101 | {"unit_cell_size": unit_cell_size, 102 | "radius": strut_radius, 103 | "angle_x": - 45, 104 | "angle_y": - angle_C1AD}, 105 | {"unit_cell_size": unit_cell_size, 106 | "radius": strut_radius, 107 | "angle_x": 45, 108 | "angle_y": - angle_C1AD}, 109 | {"unit_cell_size": unit_cell_size, 110 | "radius": strut_radius, 111 | "angle_x": 45, 112 | "angle_y": angle_C1AD} 113 | ], 114 | useLocalCoords = True 115 | ) 116 | ) 117 | return result 118 | # Register our custom plugin before use. 119 | cq.Workplane.bcc_diagonals = bcc_diagonals 120 | 121 | def bcc_vertical_struts( 122 | unit_cell_size: np.float64, 123 | strut_radius: np.float64) -> cq.cq.Workplane: 124 | """ 125 | Creates vertical struts of a unit cell. 126 | 127 | Parameters 128 | ---------- 129 | unit_cell_size : np.float64 130 | unit cell size (in mm) 131 | strut_radius: np.float64 132 | strut radius (in mm) 133 | Returns 134 | ------- 135 | result: cq.cq.Workplane 136 | a solid model of the union of all vertical struts 137 | """ 138 | result = cq.Workplane("XY") 139 | corner_points = unit_cell_size * np.array( 140 | [(0, 0), 141 | (1, 0), 142 | (1, 1), 143 | (0, 1)] 144 | ) 145 | for point in corner_points: 146 | result = (result 147 | .union( 148 | cq.Workplane() 149 | .transformed(offset = cq.Vector(point[0], point[1])) 150 | .circle(strut_radius) 151 | .extrude(unit_cell_size) 152 | ) 153 | ) 154 | return result 155 | # Register our custom plugin before use. 156 | cq.Workplane.bcc_vertical_struts = bcc_vertical_struts 157 | 158 | def bcc_bottom_horizontal_struts(unit_cell_size, strut_radius): 159 | result = cq.Workplane("XY") 160 | angle = 90 161 | corner_points = unit_cell_size * np.array( 162 | [(0, 0), 163 | (1, 0), 164 | (1, 1), 165 | (0, 1)] 166 | ) 167 | for point in corner_points: 168 | result = (result 169 | .union( 170 | cq.Workplane() 171 | .transformed(offset = cq.Vector(point[0], point[1], 0), 172 | rotate = cq.Vector(90, angle, 0)) 173 | .circle(strut_radius) 174 | .extrude(unit_cell_size) 175 | ) 176 | ) 177 | angle += 90 178 | return result 179 | # Register our custom plugin before use. 180 | cq.Workplane.bcc_bottom_horizontal_struts = bcc_bottom_horizontal_struts 181 | 182 | def bcc_top_horizontal_struts(unit_cell_size, strut_radius): 183 | result = cq.Workplane("XY") 184 | angle = 90 185 | corner_points = unit_cell_size * np.array( 186 | [(0, 0), 187 | (1, 0), 188 | (1, 1), 189 | (0, 1)] 190 | ) 191 | for point in corner_points: 192 | result = (result 193 | .union( 194 | cq.Workplane() 195 | .transformed(offset = cq.Vector(point[0], point[1], unit_cell_size), 196 | rotate = cq.Vector(90, angle, 0)) 197 | .circle(strut_radius) 198 | .extrude(unit_cell_size) 199 | ) 200 | ) 201 | angle += 90 202 | return result 203 | # Register our custom plugin before use. 204 | cq.Workplane.bcc_top_horizontal_struts = bcc_top_horizontal_struts 205 | 206 | # Creates 4 nodes at the XY plane of each unit cell 207 | def create_nodes(node_diameter, 208 | unit_cell_size, 209 | delta = 0.01 # a small coefficient is needed because CQ thinks that it cuts through emptiness 210 | ): 211 | added_node_diameter = node_diameter + delta 212 | node_radius = node_diameter / 2.0 213 | result = cq.Workplane("XY") 214 | corner_points = unit_cell_size * np.array( 215 | [(0, 0), 216 | (1, 0), 217 | (1, 1), 218 | (0, 1)] 219 | ) 220 | for point in corner_points: 221 | result= (result 222 | .union( 223 | cq.Workplane() 224 | .transformed(offset = cq.Vector(point[0], point[1], 0)) 225 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 226 | .edges("|Z") 227 | .fillet(node_radius) 228 | .edges("|X") 229 | .fillet(node_radius) 230 | ) 231 | ) 232 | result= (result 233 | .union( 234 | cq.Workplane() 235 | .transformed(offset = cq.Vector(point[0], point[1], unit_cell_size)) 236 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 237 | .edges("|Z") 238 | .fillet(node_radius) 239 | .edges("|X") 240 | .fillet(node_radius) 241 | ) 242 | ) 243 | half_unit_cell_size = unit_cell_size / 2 244 | result= (result 245 | .union( 246 | cq.Workplane() 247 | .transformed(offset = cq.Vector(half_unit_cell_size, 248 | half_unit_cell_size, 249 | half_unit_cell_size)) 250 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 251 | .edges("|Z") 252 | .fillet(node_radius) 253 | .edges("|X") 254 | .fillet(node_radius) 255 | ) 256 | ) 257 | return result 258 | cq.Workplane.create_nodes = create_nodes 259 | 260 | def unit_cell(location, unit_cell_size, strut_radius, node_diameter, type): 261 | result = cq.Workplane("XY") 262 | result = result.union(bcc_diagonals(unit_cell_size, strut_radius)) 263 | if type == 'bccz': 264 | result = result.union(bcc_vertical_struts(unit_cell_size, strut_radius)) 265 | result = result.union(create_nodes(node_diameter, unit_cell_size)) 266 | return result.val().located(location) 267 | cq.Workplane.unit_cell = unit_cell 268 | 269 | def bcc_heterogeneous_lattice(unit_cell_size, 270 | min_strut_diameter, 271 | max_strut_diameter, 272 | min_node_diameter, 273 | max_node_diameter, 274 | Nx, Ny, Nz, 275 | topology = 'bcc', 276 | rule = 'linear', 277 | position = (0, 0, 0), 278 | rotation = (0, 0, 0)): 279 | if topology not in ['bcc', 'bccz', 'sbcc', 'sbccz']: 280 | raise TypeError(f'The type \'{topology}\' does not exist!') 281 | min_strut_radius = min_strut_diameter / 2.0 282 | max_strut_radius = max_strut_diameter / 2.0 283 | if rule == 'linear': 284 | strut_radii = np.linspace(min_strut_radius, 285 | max_strut_radius, 286 | Nz) 287 | node_diameters = np.linspace(min_node_diameter, 288 | max_node_diameter, 289 | Nz) 290 | if rule == 'sin': 291 | average = lambda num1, num2: (num1 + num2) / 2 292 | strut_radii = np.sin( 293 | np.linspace(min_strut_radius, max_strut_radius, Nz)*12) + 2*average(min_strut_radius, max_strut_radius) 294 | node_diameters = np.sin( 295 | np.linspace(min_node_diameter, max_node_diameter, Nz)*12) + 2*average(min_node_diameter, max_node_diameter) 296 | if rule == 'parabola': 297 | x = np.linspace(0, 1, num=Nz) 298 | frep = lambda d_min, d_max :-4*d_max*(x-0.5)*(x-0.5)+d_max+d_min 299 | strut_radii = frep(min_strut_radius, max_strut_radius) 300 | print(strut_radii) 301 | node_diameters = frep(min_node_diameter, max_node_diameter) 302 | UC_pnts = [] 303 | for i in range(Nx): 304 | for j in range(Ny): 305 | for k in range(Nz): 306 | UC_pnts.append((i * unit_cell_size, j * unit_cell_size, k * unit_cell_size)) 307 | print("Datapoints generated") 308 | result = cq.Workplane().tag('base').transformed( 309 | offset = cq.Vector(position[0], position[1], position[2]), 310 | rotate = cq.Vector(rotation[0], rotation[1], rotation[2])) 311 | result = result.pushPoints(UC_pnts) 312 | unit_cell_params = [] 313 | for i in range(Nx * Ny): 314 | for j in range(Nz): 315 | unit_cell_params.append({"unit_cell_size": unit_cell_size, 316 | "strut_radius": strut_radii[j], 317 | "node_diameter": node_diameters[j], 318 | "type": topology}) 319 | result = result.eachpointAdaptive(unit_cell, 320 | callback_extra_args = unit_cell_params, 321 | useLocalCoords = True) 322 | print("The lattice is generated") 323 | return result -------------------------------------------------------------------------------- /lq/topologies/bcc_old.py: -------------------------------------------------------------------------------- 1 | from ..commons import eachpointAdaptive, strut_based_unit_cell 2 | 3 | from math import hypot, acos, degrees 4 | import numpy as np 5 | 6 | import cadquery as cq 7 | 8 | # Register our custom plugins before use. 9 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 10 | 11 | class BCC(strut_based_unit_cell): 12 | def __init__(self, location, unit_cell_size, strut_radius, node_diameter): 13 | super().__init__(location, unit_cell_size, strut_radius, node_diameter) 14 | self.__corner_points = unit_cell_size * np.array( 15 | [(0, 0), 16 | (1, 0), 17 | (1, 1), 18 | (0, 1)] 19 | ) 20 | # In a cube ABCDA1B1C1D1 this is the angle C1AD 21 | self.__angle_C1AD = 90 - degrees(acos(3**-.5)) 22 | 23 | # The angle is chosen with respect to the positive X direction 24 | def __create_diagonal_strut(angle_x, angle_y): 25 | hypot2D = hypot(self.unit_cell_size, self.unit_cell_size) 26 | hypot3D = hypot(hypot2D, self.unit_cell_size) 27 | result = ( 28 | cq.Workplane() 29 | .transformed(rotate = cq.Vector(angle_x, angle_y, 0)) 30 | .circle(self.strut_radius) 31 | .extrude(hypot3D) 32 | ) 33 | return result.val().located(location) 34 | 35 | def __BCC_diagonals(): 36 | result = ( 37 | cq.Workplane("XY") 38 | .pushPoints(self.corner_points) 39 | .eachpointAdaptive( 40 | self.__create_diagonal_strut, 41 | callback_extra_args = [ 42 | {"angle_x": - 45, 43 | "angle_y": self.angle_C1AD}, 44 | {"angle_x": - 45, 45 | "angle_y": - self.angle_C1AD}, 46 | {"angle_x": 45, 47 | "angle_y": - self.angle_C1AD}, 48 | {"angle_x": 45, 49 | "angle_y": self.angle_C1AD} 50 | ], 51 | useLocalCoords = True 52 | ) 53 | ) 54 | return result 55 | 56 | def __BCC_vertical_struts(): 57 | result = cq.Workplane("XY") 58 | for point in self.corner_points: 59 | result = (result 60 | .union( 61 | cq.Workplane() 62 | .transformed(offset = cq.Vector(point[0], point[1])) 63 | .circle(self.strut_radius) 64 | .extrude(self.unit_cell_size) 65 | ) 66 | ) 67 | return result 68 | 69 | def __BCC_bottom_horizontal_struts(): 70 | result = cq.Workplane("XY") 71 | angle = 90 72 | for point in self.corner_points: 73 | result = (result 74 | .union( 75 | cq.Workplane() 76 | .transformed(offset = cq.Vector(point[0], point[1], 0), 77 | rotate = cq.Vector(90, angle, 0)) 78 | .circle(self.strut_radius) 79 | .extrude(self.unit_cell_size) 80 | ) 81 | ) 82 | angle += 90 83 | return result 84 | 85 | def __BCC_top_horizontal_struts(unit_cell_size, strut_radius): 86 | result = cq.Workplane("XY") 87 | angle = 90 88 | for point in self.corner_points: 89 | result = (result 90 | .union( 91 | cq.Workplane() 92 | .transformed(offset = cq.Vector(point[0], point[1], self.unit_cell_size), 93 | rotate = cq.Vector(90, angle, 0)) 94 | .circle(self.strut_radius) 95 | .extrude(self.unit_cell_size) 96 | ) 97 | ) 98 | angle += 90 99 | return result 100 | 101 | # Creates 4 nodes at the XY plane of each unit cell 102 | def createNodes(delta = 0.01 # a small coefficient is needed because CQ thinks that it cuts through emptiness 103 | ): 104 | added_node_diameter = self.node_diameter + delta 105 | node_radius = self.node_diameter / 2.0 106 | result = cq.Workplane("XY") 107 | for point in self.corner_points: 108 | result = (result 109 | .union( 110 | cq.Workplane() 111 | .transformed(offset = cq.Vector(point[0], point[1], 0)) 112 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 113 | .edges("|Z") 114 | .fillet(node_radius) 115 | .edges("|X") 116 | .fillet(node_radius) 117 | ) 118 | ) 119 | result = (result 120 | .union( 121 | cq.Workplane() 122 | .transformed(offset = cq.Vector(point[0], point[1], self.unit_cell_size)) 123 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 124 | .edges("|Z") 125 | .fillet(node_radius) 126 | .edges("|X") 127 | .fillet(node_radius) 128 | ) 129 | ) 130 | half_unit_cell_size = self.unit_cell_size / 2 131 | result = (result 132 | .union( 133 | cq.Workplane() 134 | .transformed(offset = cq.Vector(half_unit_cell_size, 135 | half_unit_cell_size, 136 | half_unit_cell_size)) 137 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 138 | .edges("|Z") 139 | .fillet(node_radius) 140 | .edges("|X") 141 | .fillet(node_radius) 142 | ) 143 | ) 144 | return result 145 | 146 | def get_model(): 147 | result = cq.Workplane("XY") 148 | result = (result 149 | .union(__BCC_diagonals(self.unit_cell_size, self.strut_radius)) 150 | .union(__BCC_vertical_struts(self.unit_cell_size, self.strut_radius)) 151 | .union(__BCC_bottom_horizontal_struts(self.unit_cell_size, self.strut_radius)) 152 | .union(__BCC_top_horizontal_struts(self.unit_cell_size, self.strut_radius)) 153 | .union(__createNodes(self.node_diameter, self.unit_cell_size)) 154 | ) 155 | return result.val().located(self.location) 156 | 157 | def BCC_heterogeneous_lattice(unit_cell_size, 158 | min_strut_diameter, 159 | max_strut_diameter, 160 | min_node_diameter, 161 | max_node_diameter, 162 | Nx, Ny, Nz): 163 | min_strut_radius = min_strut_diameter / 2.0 164 | max_strut_radius = max_strut_diameter / 2.0 165 | strut_radii = np.linspace(min_strut_radius, 166 | max_strut_radius, 167 | Nz) 168 | node_diameters = np.linspace(min_node_diameter, 169 | max_node_diameter, 170 | Nz) 171 | UC_pnts = [] 172 | for i in range(Nx): 173 | for j in range(Ny): 174 | for k in range(Nz): 175 | UC_pnts.append((i * unit_cell_size, j * unit_cell_size, k * unit_cell_size)) 176 | result = cq.Workplane().tag('base') 177 | result = result.pushPoints(UC_pnts) 178 | unit_cell_params = [] 179 | for i in range(Nx * Ny): 180 | for j in range(Nz): 181 | unit_cell_params.append({"unit_cell_size": unit_cell_size, 182 | "strut_radius": strut_radii[j], 183 | "node_diameter": node_diameters[j]}) 184 | result = result.eachpointAdaptive(unit_cell, 185 | callback_extra_args = unit_cell_params, 186 | useLocalCoords = True) 187 | #result = result.unit_cell(unit_cell_size, strut_radius, node_diameter) 188 | return result 189 | -------------------------------------------------------------------------------- /lq/topologies/conformal.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | def cylinder(z_uc, angle_uc_size, z_uz_size, r_uz_size, 4 | 5 | min_thickness, max_thickness, 6 | inner_radius, outer_radius): 7 | r_uc = (outer_radius - inner_radius) / r_uz_size 8 | delta_thickness = (max_thickness - min_thickness) / r_uc 9 | 10 | t = min_thickness 11 | arcs = cq.Workplane() 12 | 13 | for r in range(inner_radius, outer_radius + 1, r_uz_size): 14 | for z in range(z_uc + 1): 15 | arcs = arcs.union( 16 | cq.Workplane() 17 | .transformed(offset = cq.Vector(r, 18 | z * z_uz_size, 19 | 0)) 20 | .circle(t) 21 | .revolve(360, 22 | [-r, 0, 0], 23 | [-r, 1, 0]) 24 | ) 25 | 26 | t += delta_thickness 27 | 28 | t = min_thickness 29 | radials = cq.Workplane() 30 | for r in range(inner_radius, outer_radius, r_uz_size): 31 | for z in range(z_uc + 1): 32 | for phi in range(0, 360, angle_uc_size): 33 | radials = radials.union(cq.Workplane() 34 | .transformed(rotate = cq.Vector(0, 35 | phi, 36 | 0)) 37 | .transformed(offset = cq.Vector(0, 38 | z * z_uz_size, 39 | r)) 40 | .circle(t) 41 | .extrude(r_uz_size) 42 | ) 43 | t += delta_thickness 44 | 45 | 46 | t = min_thickness 47 | axials = cq.Workplane() 48 | for r in range(inner_radius, outer_radius + 1, r_uz_size): 49 | for phi in range(0, 360, angle_uc_size): 50 | axials = axials.union(cq.Workplane() 51 | .transformed(rotate = cq.Vector(0, 52 | phi, 53 | 0)) 54 | .transformed(offset = cq.Vector(0, 55 | 0, 56 | r)) 57 | .transformed(rotate = cq.Vector(- 90, 58 | 0, 59 | 0)) 60 | .circle(t) 61 | .extrude(z_uc * z_uz_size) 62 | ) 63 | t += delta_thickness 64 | return arcs, radials, axials -------------------------------------------------------------------------------- /lq/topologies/cubic.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright (C) 2021, Advanced Design and Manufacturing Lab (ADML). 3 | # All rights reserved. 4 | # 5 | # This software and its documentation and related materials are owned by 6 | # ADML. The software may only be incorporated into application programs owned 7 | # by members of ADML. The structure and organization of this software are 8 | # the valuable trade secrets of ADML and its suppliers. The software is also 9 | # protected by copyright law and international treaty provisions. 10 | # 11 | # By use of this software, its documentation or related materials, you 12 | # acknowledge and accept the above terms. 13 | ############################################################################## 14 | 15 | from ..commons import eachpointAdaptive 16 | 17 | from math import hypot, acos, degrees 18 | import numpy as np 19 | 20 | import cadquery as cq 21 | 22 | # Register our custom plugins before use. 23 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 24 | 25 | def z_struts( 26 | unit_cell_size: np.float64, 27 | strut_radius: np.float64) -> cq.cq.Workplane: 28 | """ 29 | Creates vertical struts of a unit cell. 30 | 31 | Parameters 32 | ---------- 33 | unit_cell_size : np.float64 34 | unit cell size (in mm) 35 | strut_radius: np.float64 36 | strut radius (in mm) 37 | Returns 38 | ------- 39 | result: cq.cq.Workplane 40 | a solid model of the union of all vertical struts 41 | """ 42 | result = cq.Workplane("XY") 43 | corner_points = unit_cell_size * np.array( 44 | [(0, 0), 45 | (1, 0), 46 | (1, 1), 47 | (0, 1)] 48 | ) 49 | for point in corner_points: 50 | result = (result 51 | .union( 52 | cq.Workplane() 53 | .transformed(offset = cq.Vector(point[0], point[1])) 54 | .circle(strut_radius) 55 | .extrude(unit_cell_size) 56 | ) 57 | ) 58 | return result 59 | # Register our custom plugin before use. 60 | cq.Workplane.z_struts = z_struts 61 | 62 | def bottom_xy_struts(unit_cell_size, strut_radius): 63 | result = cq.Workplane("XY") 64 | angle = 90 65 | corner_points = unit_cell_size * np.array( 66 | [(0, 0), 67 | (1, 0), 68 | (1, 1), 69 | (0, 1)] 70 | ) 71 | for point in corner_points: 72 | result = (result 73 | .union( 74 | cq.Workplane() 75 | .transformed(offset = cq.Vector(point[0], point[1], 0), 76 | rotate = cq.Vector(90, angle, 0)) 77 | .circle(strut_radius) 78 | .extrude(unit_cell_size) 79 | ) 80 | ) 81 | angle += 90 82 | return result 83 | # Register our custom plugin before use. 84 | cq.Workplane.bottom_xy_struts = bottom_xy_struts 85 | 86 | def top_xy_struts(unit_cell_size, strut_radius): 87 | result = cq.Workplane("XY") 88 | angle = 90 89 | corner_points = unit_cell_size * np.array( 90 | [(0, 0), 91 | (1, 0), 92 | (1, 1), 93 | (0, 1)] 94 | ) 95 | for point in corner_points: 96 | result = (result 97 | .union( 98 | cq.Workplane() 99 | .transformed(offset = cq.Vector(point[0], point[1], unit_cell_size), 100 | rotate = cq.Vector(90, angle, 0)) 101 | .circle(strut_radius) 102 | .extrude(unit_cell_size) 103 | ) 104 | ) 105 | angle += 90 106 | return result 107 | # Register our custom plugin before use. 108 | cq.Workplane.top_xy_struts = top_xy_struts 109 | 110 | # Creates 4 nodes at the XY plane of each unit cell 111 | def create_nodes(node_diameter, 112 | unit_cell_size, 113 | delta = 0.01 # a small coefficient is needed because CQ thinks that it cuts through emptiness 114 | ): 115 | added_node_diameter = node_diameter + delta 116 | node_radius = node_diameter / 2.0 117 | result = cq.Workplane("XY") 118 | corner_points = unit_cell_size * np.array( 119 | [(0, 0), 120 | (1, 0), 121 | (1, 1), 122 | (0, 1)] 123 | ) 124 | for point in corner_points: 125 | result= (result 126 | .union( 127 | cq.Workplane() 128 | .transformed(offset = cq.Vector(point[0], point[1], 0)) 129 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 130 | .edges("|Z") 131 | .fillet(node_radius) 132 | .edges("|X") 133 | .fillet(node_radius) 134 | ) 135 | ) 136 | result= (result 137 | .union( 138 | cq.Workplane() 139 | .transformed(offset = cq.Vector(point[0], point[1], unit_cell_size)) 140 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 141 | .edges("|Z") 142 | .fillet(node_radius) 143 | .edges("|X") 144 | .fillet(node_radius) 145 | ) 146 | ) 147 | return result 148 | cq.Workplane.create_nodes = create_nodes 149 | 150 | def unit_cell(location, unit_cell_size, strut_radius, node_diameter, type): 151 | result = cq.Workplane("XY") 152 | result = result.union(z_struts(unit_cell_size, strut_radius)) 153 | result = result.union(bottom_xy_struts(unit_cell_size, strut_radius)) 154 | result = result.union(top_xy_struts(unit_cell_size, strut_radius)) 155 | result = result.union(create_nodes(node_diameter, unit_cell_size)) 156 | return result.val().located(location) 157 | cq.Workplane.unit_cell = unit_cell 158 | 159 | def cubic_heterogeneous_lattice(unit_cell_size, 160 | min_strut_diameter, 161 | max_strut_diameter, 162 | min_node_diameter, 163 | max_node_diameter, 164 | Nx, Ny, Nz, 165 | type = 'cubic', 166 | rule = 'linear'): 167 | min_strut_radius = min_strut_diameter / 2.0 168 | max_strut_radius = max_strut_diameter / 2.0 169 | if rule == 'linear': 170 | strut_radii = np.linspace(min_strut_radius, 171 | max_strut_radius, 172 | Nz) 173 | node_diameters = np.linspace(min_node_diameter, 174 | max_node_diameter, 175 | Nz) 176 | if rule == 'sin': 177 | average = lambda num1, num2: (num1 + num2) / 2 178 | strut_radii = np.sin( 179 | np.linspace(min_strut_radius, max_strut_radius, Nz)*12) + 2*average(min_strut_radius, max_strut_radius) 180 | node_diameters = np.sin( 181 | np.linspace(min_node_diameter, max_node_diameter, Nz)*12) + 2*average(min_node_diameter, max_node_diameter) 182 | UC_pnts = [] 183 | for i in range(Nx): 184 | for j in range(Ny): 185 | for k in range(Nz): 186 | UC_pnts.append((i * unit_cell_size, j * unit_cell_size, k * unit_cell_size)) 187 | print("Datapoints generated") 188 | result = cq.Workplane().tag('base') 189 | result = result.pushPoints(UC_pnts) 190 | unit_cell_params = [] 191 | for i in range(Nx * Ny): 192 | for j in range(Nz): 193 | unit_cell_params.append({"unit_cell_size": unit_cell_size, 194 | "strut_radius": strut_radii[j], 195 | "node_diameter": node_diameters[j], 196 | "type": type}) 197 | result = result.eachpointAdaptive(unit_cell, 198 | callback_extra_args = unit_cell_params, 199 | useLocalCoords = True) 200 | print("The lattice is generated") 201 | return result -------------------------------------------------------------------------------- /lq/topologies/diamond.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright (C) 2021, Advanced Design and Manufacturing Lab (ADML). 3 | # All rights reserved. 4 | # 5 | # This software and its documentation and related materials are owned by 6 | # ADML. The software may only be incorporated into application programs owned 7 | # by members of ADML. The structure and organization of this software are 8 | # the valuable trade secrets of ADML and its suppliers. The software is also 9 | # protected by copyright law and international treaty provisions. 10 | # 11 | # By use of this software, its documentation or related materials, you 12 | # acknowledge and accept the above terms. 13 | ############################################################################## 14 | 15 | from typing import Tuple 16 | from numpy.lib.function_base import append 17 | from ..commons import eachpointAdaptive 18 | 19 | from math import hypot, acos, degrees 20 | import numpy as np 21 | 22 | import cadquery as cq 23 | 24 | # Register our custom plugins before use. 25 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 26 | 27 | PNT_LEVELS = [ 28 | [(1, 0), (0.5, 0.5), (0, 1)], 29 | [(0.75, 0.25), (0.25, 0.75)], 30 | [(0.5, 0), (1, 0.5), (0.5, 1), (0, 0.5)], 31 | [(0.25, 0.25), (0.75, 0.75)], 32 | [(0, 0), (0.5, 0.5), (1, 1)] 33 | ] 34 | 35 | def create_strut( 36 | unit_cell_size: np.float64, 37 | offset: Tuple, 38 | angle: Tuple, 39 | radius: np.float64) -> cq.occ_impl.shapes.Compound: 40 | """ 41 | Creates a solid model of a diagonal cylindrical strut. 42 | The angle is chosen with respect to the positive X direction 43 | 44 | Parameters 45 | ---------- 46 | location : cq.occ_impl.geom.Location 47 | point location of the strut 48 | unit_cell_size : np.float64 49 | unit cell size (in mm) 50 | angle_x : np.float64 51 | angle between the stut and the positibe direction of X 52 | in the local coordinate system 53 | angle_y : np.float64 54 | angle between the stut and the positibe direction of Y 55 | in the local coordinate system 56 | Returns 57 | ------- 58 | cq.occ_impl.shapes.Compound 59 | a solid model of the strut 60 | 61 | """ 62 | hypot2D = hypot(0.25 * unit_cell_size, 0.25 * unit_cell_size) 63 | strut_len = hypot(hypot2D, 0.25 * unit_cell_size) 64 | result = cq.Workplane() 65 | result = result.transformed(rotate = cq.Vector(angle), 66 | offset = cq.Vector(offset)) 67 | result = result.circle(radius).extrude(strut_len) 68 | return result 69 | # Register our custom plugin before use. 70 | cq.Workplane.create_strut = create_strut 71 | 72 | 73 | def create_diamond_struts( 74 | unit_cell_size: np.float64, 75 | radius: np.float64) -> cq.occ_impl.shapes.Compound: 76 | """ 77 | Creates a solid model of all cylindrical struts 78 | of the diamond topology. 79 | 80 | Parameters 81 | ---------- 82 | location : cq.occ_impl.geom.Location 83 | point location of the strut 84 | unit_cell_size : np.float64 85 | unit cell size (in mm) 86 | radius : np.float64 87 | strut radius (in mm) 88 | Returns 89 | ------- 90 | cq.occ_impl.shapes.Compound 91 | a solid model of the struts 92 | 93 | """ 94 | angle_x = 45 95 | # In a cube ABCDA1B1C1D1 this is the angle C1AD 96 | angle_c1ad = 90 - degrees(acos(3**-.5)) 97 | # 4 bottom struts 98 | result = create_strut(unit_cell_size, 99 | (unit_cell_size, 0, 0), 100 | (-45, -angle_c1ad, 0), 101 | radius) 102 | result = result.union(create_strut(unit_cell_size, 103 | (0.5*unit_cell_size, 0.5*unit_cell_size, 0), 104 | (-45, - angle_c1ad, 0), 105 | radius)) 106 | result = result.union(create_strut(unit_cell_size, 107 | (0.5*unit_cell_size, 0.5*unit_cell_size, 0), 108 | (45, angle_c1ad, 0), 109 | radius)) 110 | result = result.union(create_strut(unit_cell_size, 111 | (0, unit_cell_size, 0), 112 | (45, angle_c1ad, 0), 113 | radius)) 114 | # 4 2nd level struts 115 | result = result.union(create_strut(unit_cell_size, 116 | (0.75 * unit_cell_size, 0.25 * unit_cell_size, 0.25 * unit_cell_size), 117 | (-45, angle_c1ad, 0), 118 | radius)) 119 | result = result.union(create_strut(unit_cell_size, 120 | (0.75 * unit_cell_size, 0.25 * unit_cell_size, 0.25 * unit_cell_size), 121 | (45, - angle_c1ad, 0), 122 | radius)) 123 | result = result.union(create_strut(unit_cell_size, 124 | (0.25 * unit_cell_size, 0.75 * unit_cell_size, 0.25 * unit_cell_size), 125 | (-45, angle_c1ad, 0), 126 | radius)) 127 | result = result.union(create_strut(unit_cell_size, 128 | (0.25 * unit_cell_size, 0.75 * unit_cell_size, 0.25 * unit_cell_size), 129 | (45, - angle_c1ad, 0), 130 | radius)) 131 | # 4 3rd level struts 132 | result = result.union(create_strut(unit_cell_size, 133 | (0.5 * unit_cell_size, 0, 0.5 * unit_cell_size), 134 | (-45, - angle_c1ad, 0), 135 | radius)) 136 | result = result.union(create_strut(unit_cell_size, 137 | (unit_cell_size, 0.5 * unit_cell_size, 0.5 * unit_cell_size), 138 | (-45, - angle_c1ad, 0), 139 | radius)) 140 | result = result.union(create_strut(unit_cell_size, 141 | (0.5 * unit_cell_size, unit_cell_size, 0.5 * unit_cell_size), 142 | (45, angle_c1ad, 0), 143 | radius)) 144 | result = result.union(create_strut(unit_cell_size, 145 | (0, 0.5 * unit_cell_size, 0.5 * unit_cell_size), 146 | (45, angle_c1ad, 0), 147 | radius)) 148 | # 4 top struts 149 | result = result.union(create_strut(unit_cell_size, 150 | (0.25*unit_cell_size, 0.25*unit_cell_size, 0.75 * unit_cell_size), 151 | (45, - angle_c1ad, 0), 152 | radius)) 153 | result = result.union(create_strut(unit_cell_size, 154 | (0.25*unit_cell_size, 0.25*unit_cell_size, 0.75 * unit_cell_size), 155 | (- 45, angle_c1ad, 0), 156 | radius)) 157 | result = result.union(create_strut(unit_cell_size, 158 | (0.75*unit_cell_size, 0.75*unit_cell_size, 0.75 * unit_cell_size), 159 | (45, - angle_c1ad, 0), 160 | radius)) 161 | result = result.union(create_strut(unit_cell_size, 162 | (0.75*unit_cell_size, 0.75*unit_cell_size, 0.75 * unit_cell_size), 163 | (- 45, angle_c1ad, 0), 164 | radius)) 165 | 166 | return result 167 | # Register our custom plugin before use. 168 | cq.Workplane.create_diamond_struts = create_diamond_struts 169 | 170 | 171 | 172 | # Creates 4 nodes at the XY plane of each unit cell 173 | def create_nodes(node_diameter, 174 | unit_cell_size, 175 | delta = 0.01 # a small coefficient is needed because CQ thinks that it cuts through emptiness 176 | ): 177 | added_node_diameter = node_diameter + delta 178 | node_radius = node_diameter / 2.0 179 | 180 | z_level = 0 181 | result = cq.Workplane("XY") 182 | for pnt_level in PNT_LEVELS: 183 | for pnt in pnt_level: 184 | result = (result 185 | .union( 186 | cq.Workplane() 187 | .transformed(offset = cq.Vector(pnt[0] * unit_cell_size, 188 | pnt[1] * unit_cell_size, 189 | z_level)) 190 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 191 | .edges("|Z") 192 | .fillet(node_radius) 193 | .edges("|X") 194 | .fillet(node_radius) 195 | ) 196 | ) 197 | z_level += 0.25 * unit_cell_size 198 | return result 199 | cq.Workplane.create_nodes = create_nodes 200 | 201 | def unit_cell(location, unit_cell_size, strut_radius, node_diameter, type): 202 | result = cq.Workplane("XY") 203 | result = result.union(create_diamond_struts(unit_cell_size, strut_radius)) 204 | result = result.union(create_nodes(node_diameter, unit_cell_size)) 205 | return result.val().located(location) 206 | cq.Workplane.unit_cell = unit_cell 207 | 208 | def diamond_heterogeneous_lattice(unit_cell_size, 209 | min_strut_diameter, 210 | max_strut_diameter, 211 | min_node_diameter, 212 | max_node_diameter, 213 | Nx, Ny, Nz, 214 | rule = 'linear'): 215 | min_strut_radius = min_strut_diameter / 2.0 216 | max_strut_radius = max_strut_diameter / 2.0 217 | if rule == 'linear': 218 | strut_radii = np.linspace(min_strut_radius, 219 | max_strut_radius, 220 | Nz) 221 | node_diameters = np.linspace(min_node_diameter, 222 | max_node_diameter, 223 | Nz) 224 | if rule == 'sin': 225 | average = lambda num1, num2: (num1 + num2) / 2 226 | strut_radii = np.sin( 227 | np.linspace(min_strut_radius, max_strut_radius, Nz)*12) + 2*average(min_strut_radius, max_strut_radius) 228 | node_diameters = np.sin( 229 | np.linspace(min_node_diameter, max_node_diameter, Nz)*12) + 2*average(min_node_diameter, max_node_diameter) 230 | UC_pnts = [] 231 | for i in range(Nx): 232 | for j in range(Ny): 233 | for k in range(Nz): 234 | UC_pnts.append((i * unit_cell_size, j * unit_cell_size, k * unit_cell_size)) 235 | print("Datapoints generated") 236 | result = cq.Workplane().tag('base') 237 | result = result.pushPoints(UC_pnts) 238 | unit_cell_params = [] 239 | for i in range(Nx * Ny): 240 | for j in range(Nz): 241 | unit_cell_params.append({"unit_cell_size": unit_cell_size, 242 | "strut_radius": strut_radii[j], 243 | "node_diameter": node_diameters[j], 244 | "type": type}) 245 | result = result.eachpointAdaptive(unit_cell, 246 | callback_extra_args = unit_cell_params, 247 | useLocalCoords = True) 248 | print("The lattice is generated") 249 | return result -------------------------------------------------------------------------------- /lq/topologies/fbcc.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright (C) 2021, Advanced Design and Manufacturing Lab (ADML). 3 | # All rights reserved. 4 | # 5 | # This software and its documentation and related materials are owned by 6 | # ADML. The software may only be incorporated into application programs owned 7 | # by members of ADML. The structure and organization of this software are 8 | # the valuable trade secrets of ADML and its suppliers. The software is also 9 | # protected by copyright law and international treaty provisions. 10 | # 11 | # By use of this software, its documentation or related materials, you 12 | # acknowledge and accept the above terms. 13 | ############################################################################## 14 | 15 | from ..commons import eachpointAdaptive 16 | from .bcc import bcc_diagonals 17 | from .bcc import create_nodes as create_bcc_nodes 18 | from .fcc import create_diagonal_strut 19 | from .fcc import fcc_diagonals 20 | from .fcc import fcc_vertical_struts 21 | from .fcc import fcc_bottom_horizontal_struts 22 | from .fcc import fcc_horizontal_diagonal_struts 23 | from .fcc import fcc_top_horizontal_struts 24 | from .fcc import create_nodes 25 | 26 | from math import hypot, acos, degrees, hypot 27 | import numpy as np 28 | 29 | import cadquery as cq 30 | 31 | # Register our custom plugins before use. 32 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 33 | cq.Workplane.bcc_diagonals = bcc_diagonals 34 | cq.Workplane.create_bcc_nodes = create_bcc_nodes 35 | cq.Workplane.create_diagonal_strut = create_diagonal_strut 36 | cq.Workplane.fcc_diagonals = fcc_diagonals 37 | cq.Workplane.fcc_vertical_struts = fcc_vertical_struts 38 | cq.Workplane.fcc_bottom_horizontal_struts = fcc_bottom_horizontal_struts 39 | cq.Workplane.fcc_horizontal_diagonal_struts = fcc_horizontal_diagonal_struts 40 | cq.Workplane.fcc_top_horizontal_struts = fcc_top_horizontal_struts 41 | cq.Workplane.create_nodes = create_nodes 42 | 43 | def unit_cell(location, unit_cell_size, strut_radius, node_diameter, type): 44 | result = cq.Workplane("XY") 45 | result = result.union(bcc_diagonals(unit_cell_size, strut_radius)) 46 | result = result.union(fcc_diagonals(unit_cell_size, strut_radius)) 47 | if type in ['sfbcc', 'sfbccz']: 48 | result = result.union(create_nodes(node_diameter, unit_cell_size, type)) 49 | if type in ['fbccz', 'sfbccz']: 50 | result = result.union(fcc_vertical_struts(unit_cell_size, strut_radius)) 51 | if type == 'fbcc': 52 | result = result.union(fcc_horizontal_diagonal_struts(unit_cell_size, strut_radius)) 53 | result = result.union(create_nodes(node_diameter, unit_cell_size, type)) 54 | #result = result.union(create_bcc_nodes(node_diameter, unit_cell_size)) 55 | return result.val().located(location) 56 | cq.Workplane.unit_cell = unit_cell 57 | 58 | def fbcc_heterogeneous_lattice(unit_cell_size, 59 | min_strut_diameter, 60 | max_strut_diameter, 61 | min_node_diameter, 62 | max_node_diameter, 63 | Nx, Ny, Nz, 64 | type = 'fbcc', 65 | rule = 'linear'): 66 | if type not in ['fbcc', 'sfbcc', 'sfbccz']: 67 | raise TypeError(f'The type \'{type}\' does not exist!') 68 | min_strut_radius = min_strut_diameter / 2.0 69 | max_strut_radius = max_strut_diameter / 2.0 70 | if rule == 'linear': 71 | strut_radii = np.linspace(min_strut_radius, 72 | max_strut_radius, 73 | Nz) 74 | node_diameters = np.linspace(min_node_diameter, 75 | max_node_diameter, 76 | Nz) 77 | if rule == 'sin': 78 | average = lambda num1, num2: (num1 + num2) / 2 79 | strut_radii = np.sin( 80 | np.linspace(min_strut_radius, max_strut_radius, Nz)*12) + 2*average(min_strut_radius, max_strut_radius) 81 | node_diameters = np.sin( 82 | np.linspace(min_node_diameter, max_node_diameter, Nz)*12) + 2*average(min_node_diameter, max_node_diameter) 83 | UC_pnts = [] 84 | for i in range(Nx): 85 | for j in range(Ny): 86 | for k in range(Nz): 87 | UC_pnts.append((i * unit_cell_size, j * unit_cell_size, k * unit_cell_size)) 88 | print("Datapoints generated") 89 | result = cq.Workplane().tag('base') 90 | result = result.pushPoints(UC_pnts) 91 | unit_cell_params = [] 92 | for i in range(Nx * Ny): 93 | for j in range(Nz): 94 | unit_cell_params.append({"unit_cell_size": unit_cell_size, 95 | "strut_radius": strut_radii[j], 96 | "node_diameter": node_diameters[j], 97 | "type": type}) 98 | result = result.eachpointAdaptive(unit_cell, 99 | callback_extra_args = unit_cell_params, 100 | useLocalCoords = True) 101 | print("The lattice is generated") 102 | return result -------------------------------------------------------------------------------- /lq/topologies/gyroid.py: -------------------------------------------------------------------------------- 1 | from ..commons import eachpointAdaptive 2 | 3 | import numpy as np 4 | 5 | import cadquery as cq 6 | 7 | # Register our custom plugins before use. 8 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 9 | 10 | # Gyroïd, all edges are splines on different workplanes. 11 | def gyroid_000(self, thickness: float, unit_cell_size: float 12 | ) -> cq.cq.Workplane: 13 | """ 14 | Create a plate with a gyroid spline edge in the first 15 | (000) octant 16 | 17 | Args: 18 | thickness (float): the thickness of the plate 19 | unit_cell_size (float): The size of the unit cell. 20 | 21 | Returns: 22 | The result of the function. 23 | """ 24 | edge_points = [ 25 | [[0.5, 0.5], 26 | [0.25, 0.0], 27 | [0.5, - 0.5]], 28 | [[- 0.5, - 0.5], 29 | [0.0, - 0.25], 30 | [0.5, - 0.5]], 31 | [[- 0.5, - 0.5], 32 | [0.0, - 0.25], 33 | [0.5, - 0.5]], 34 | [[- 0.5, - 0.5], 35 | [- 0.25, 0.0], 36 | [- 0.5, 0.5]], 37 | [[0.5, 0.5], 38 | [0.0, 0.25], 39 | [- 0.5, 0.5]], 40 | [[0.5, 0.5], 41 | [0.0, 0.25], 42 | [- 0.5, 0.5]], 43 | ] 44 | # Multiplying the edge points by the unit cell size. 45 | edge_points = np.array(edge_points) * unit_cell_size 46 | 47 | plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"] 48 | offset_list = [- 1, 1, 1, 1, - 1, - 1] 49 | offset_list = np.array(offset_list) * unit_cell_size * 0.5 50 | edge_wire = ( 51 | cq.Workplane(plane_list[0]) 52 | .workplane(offset = - offset_list[0]) 53 | .spline(edge_points[0]) 54 | ) 55 | for i in range(len(edge_points) - 1): 56 | # Adding the spline to the wire. 57 | edge_wire = edge_wire.add( 58 | cq.Workplane(plane_list[i + 1]) 59 | .workplane(offset = - offset_list[i + 1]) 60 | .spline(edge_points[i + 1]) 61 | ) 62 | surface_points = [[0, 0, 0]] 63 | plate_4 = cq.Workplane("XY") 64 | # `interpPlate` is a function that interpolates a surface from a wire. 65 | plate_4 = plate_4.interpPlate(edge_wire, surface_points, thickness * 0.5) 66 | plate_4 = plate_4.union( 67 | cq.Workplane("XY").interpPlate(edge_wire, surface_points, - 0.5 * thickness) 68 | ) 69 | return self.union(self.eachpoint(lambda loc: plate_4.val().located(loc), True)) 70 | cq.Workplane.gyroid_000 = gyroid_000 71 | 72 | def unit_cell(location: cq.occ_impl.geom.Location, 73 | thickness: float, 74 | unit_cell_size: float, 75 | delta: float = 1e-8 # a small tolerance (1e-10 is too small) 76 | ) -> cq.cq.Workplane: 77 | """ 78 | Create a unit cell of gyroid, with a given thickness, and a given unit cell size 79 | in all 8 octants 80 | 81 | Args: 82 | location (cq.occ_impl.geom.Location): the location of the unit cell 83 | thickness (float): the thickness of the unit cell 84 | unit_cell_size (float): the size of the unit cell 85 | delta (float): a small tolerance (1e-10 is too small) 86 | 87 | Returns: 88 | A CQ object. 89 | """ 90 | 91 | half_unit_cell_size = unit_cell_size / 2.0 92 | # Octante 000 93 | pnts = [tuple(unit_cell_size / 2 for i in range(3))] 94 | cq.Workplane.gyroid_000 = gyroid_000 95 | g_000 = (cq.Workplane("XY") 96 | .pushPoints(pnts) 97 | .gyroid_000(thickness, unit_cell_size) 98 | ) 99 | result = g_000 100 | # Octante 100 101 | mirZY_pos = g_000.mirror(mirrorPlane = "ZY", 102 | basePointVector = (unit_cell_size, 0, 0)) 103 | g_100 = mirZY_pos.mirror(mirrorPlane = "XZ", 104 | basePointVector = (0, half_unit_cell_size, 0)) 105 | result = result.union(g_100) 106 | # Octante 110 107 | g_000_inverse = (cq.Workplane("XY") 108 | .pushPoints(pnts) 109 | .gyroid_000(- thickness, unit_cell_size)) 110 | mirXZ_pos = g_000_inverse.mirror(mirrorPlane = "XZ", 111 | basePointVector = (0, unit_cell_size, 0)) 112 | g_110 = mirXZ_pos.translate((unit_cell_size, 0, 0)) 113 | result = result.union(g_110) 114 | # Octante 010 115 | mirYZ_neg = g_110.mirror(mirrorPlane = "YZ", 116 | basePointVector = (unit_cell_size, 0, 0)) 117 | g_010 = mirYZ_neg.mirror(mirrorPlane = "XZ", 118 | basePointVector = (0, 1.5 * unit_cell_size, 0)) 119 | result = result.union(g_010) 120 | # Octante 001 121 | g_001 = g_110.translate((-unit_cell_size, 122 | -unit_cell_size, 123 | unit_cell_size)) 124 | result = result.union(g_001) 125 | # Octante 101 126 | g_101 = g_010.translate((unit_cell_size, 127 | -unit_cell_size, 128 | unit_cell_size)) 129 | result = result.union(g_101) 130 | # Octante 011 131 | g_011 = g_100.translate((- (1 + delta) * unit_cell_size, 132 | (1 + delta) * unit_cell_size, 133 | (1 + delta) * unit_cell_size)) 134 | result = result.union(g_011) 135 | # Octante 111 136 | g_111 = g_000.translate(((1 + delta) * unit_cell_size, 137 | (1 + delta) * unit_cell_size, 138 | (1 + delta) * unit_cell_size)) 139 | result = result.union(g_111) 140 | return result.val().located(location) 141 | cq.Workplane.unit_cell = unit_cell 142 | 143 | def gyroid_homogeneous_lattice(unit_cell_size: float, 144 | thickness: float, 145 | Nx: int, Ny: int, Nz: int 146 | ) -> cq.cq.Workplane: 147 | """ 148 | Create a unit cell of gyroid, and repeat it Nx, Ny, Nz times 149 | 150 | Args: 151 | unit_cell_size (float): The size of the unit cell. 152 | thickness (float): the thickness of the unit cell 153 | Nx (int): number of unit cells in x direction 154 | Ny (int): number of unit cells in the y direction 155 | Nz (int): Number of unit cells in the z direction 156 | 157 | Returns: 158 | A CQ object. 159 | """ 160 | 161 | # Register our custom plugins before use. 162 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 163 | UC_pnts = [] 164 | for i in range(Nx): 165 | for j in range(Ny): 166 | for k in range(Nz): 167 | UC_pnts.append((2 * i * unit_cell_size, 2 * j * unit_cell_size, 2 * k * unit_cell_size)) 168 | result = cq.Workplane().tag('base') 169 | result = result.pushPoints(UC_pnts) 170 | unit_cell_params = [] 171 | for i in range(Nx * Ny): 172 | for j in range(Nz): 173 | unit_cell_params.append({"thickness": thickness, 174 | "unit_cell_size": unit_cell_size}) 175 | result = result.eachpointAdaptive(unit_cell, 176 | callback_extra_args = unit_cell_params, 177 | useLocalCoords = True) 178 | return result 179 | 180 | def gyroid_heterogeneous_lattice(unit_cell_size: float, 181 | min_thickness: float, 182 | max_thickness: float, 183 | Nx: int, Ny: int, Nz: int, 184 | direction: str = 'z' 185 | ) -> cq.cq.Workplane: 186 | """ 187 | Create a linearly heterogeneous lattice of gyroid unit cells by creating a base workplane, 188 | then pushing a list of points to it. 189 | 190 | Args: 191 | unit_cell_size (float): The size of the unit cell in the x, y, and z directions. 192 | min_thickness (float): the minimum thickness of the unit cell 193 | max_thickness (float): The maximum thickness of the unit cell. 194 | Nx (int): Number of unit cells in the x direction 195 | Ny (int): Number of unit cells in the y direction 196 | Nz (int): Number of unit cells in the z direction 197 | direction (str): direction of thickness variation (x, y, z) 198 | Returns: 199 | A CQ object. 200 | """ 201 | coordinates_3d = ['x', 'y', 'z'] 202 | if direction not in coordinates_3d: 203 | raise ValueError(f'Direction {direction} does not exist. The acceptable directions are {coordinates_3d}') 204 | # Register the custrom plugin 205 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 206 | ns = {'x': Nx,'y': Ny, 'z': Nz} 207 | thicknesses = np.linspace(min_thickness, max_thickness, ns[direction]) 208 | UC_pnts = [] 209 | unit_cell_size = 0.5 * unit_cell_size # because unit cell is made of 8 mirrored features 210 | for i in range(Nx): 211 | for j in range(Ny): 212 | for k in range(Nz): 213 | UC_pnts.append((2 * i * unit_cell_size, 2 * j * unit_cell_size, 2 * k * unit_cell_size)) 214 | result = cq.Workplane().tag('base') 215 | result = result.pushPoints(UC_pnts) 216 | unit_cell_params = [] 217 | for i in range(Nx): 218 | for j in range(Ny): 219 | for k in range(Nz): 220 | n_coordinates = { 221 | 'x': i, 222 | 'y': j, 223 | 'z': k 224 | } 225 | unit_cell_params.append({"thickness": thicknesses[n_coordinates[direction]], 226 | "unit_cell_size": unit_cell_size}) 227 | result = result.eachpointAdaptive(unit_cell, 228 | callback_extra_args = unit_cell_params, 229 | useLocalCoords = True) 230 | return result 231 | 232 | -------------------------------------------------------------------------------- /lq/topologies/martensite.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright (C) 2021, Advanced Design and Manufacturing Lab (ADML). 3 | # All rights reserved. 4 | # 5 | # This software and its documentation and related materials are owned by 6 | # ADML. The software may only be incorporated into application programs owned 7 | # by members of ADML. The structure and organization of this software are 8 | # the valuable trade secrets of ADML and its suppliers. The software is also 9 | # protected by copyright law and international treaty provisions. 10 | # 11 | # By use of this software, its documentation or related materials, you 12 | # acknowledge and accept the above terms. 13 | ############################################################################## 14 | 15 | from ..commons import eachpointAdaptive 16 | from .fcc import unit_cell 17 | 18 | import numpy as np 19 | 20 | import cadquery as cq 21 | 22 | # Register our custom plugins before use. 23 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 24 | 25 | def fcc_martensite(unit_cell_size: float, 26 | strut_diameter: float, 27 | node_diameter: float, 28 | # Nx: int, 29 | Ny: int, 30 | Nz: int, 31 | uc_break: int): 32 | if uc_break < 1: 33 | raise ValueError('The value of the beginning of the break should larger than 1') 34 | UC_pnts = [] 35 | Nx = Nz + uc_break - 1 36 | for i in range(Nx): 37 | for j in range(Ny): 38 | for k in range(Nz): 39 | if k < i: 40 | UC_pnts.append( 41 | (i * unit_cell_size, 42 | j * unit_cell_size, 43 | k * unit_cell_size)) 44 | print("Datapoints generated") 45 | result = cq.Workplane().tag('base') 46 | result = result.pushPoints(UC_pnts) 47 | unit_cell_params = [] 48 | for i in range(Nx * Ny): 49 | for j in range(Nz): 50 | unit_cell_params.append({"unit_cell_size": unit_cell_size, 51 | "strut_radius": strut_diameter * 0.5, 52 | "node_diameter": node_diameter, 53 | "type": 'fcc'}) 54 | result = result.eachpointAdaptive(unit_cell, 55 | callback_extra_args = unit_cell_params, 56 | useLocalCoords = True) 57 | print("The lattice is generated") 58 | return result -------------------------------------------------------------------------------- /lq/topologies/schwartz.py: -------------------------------------------------------------------------------- 1 | from ..commons import eachpointAdaptive 2 | 3 | import numpy as np 4 | from math import cos, sqrt 5 | 6 | import cadquery as cq 7 | 8 | # Register our custom plugins before use. 9 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 10 | 11 | # Schwartz P surface, all edges are arcs on different workplanes. 12 | def schwartz_p_000(self, thickness, unit_cell_size): 13 | delta_radius = 0.5 - 0.5/sqrt(2) 14 | convex_pnts = [[0.0, 0.5], 15 | [delta_radius, delta_radius], 16 | [0.5, 0.0]] 17 | nonconvex_pnts = [[-0.5, 0.0], 18 | [-delta_radius,-delta_radius], 19 | [0.0, -0.5]] 20 | edge_points = [convex_pnts, 21 | nonconvex_pnts, 22 | convex_pnts, 23 | nonconvex_pnts, 24 | convex_pnts, 25 | nonconvex_pnts] 26 | edge_points = np.array(edge_points) * unit_cell_size 27 | 28 | plane_list = ["XZ", "XY", "YZ", "XZ", "XY", "YZ"] 29 | offset_list = [- 1, - 1, 1, 1, 1, - 1] 30 | offset_list = np.array(offset_list) * unit_cell_size * 0.5 31 | 32 | edge_wire = ( 33 | cq.Workplane(plane_list[0]) 34 | .workplane(offset = - offset_list[0]) 35 | .moveTo(edge_points[0][0][0], 36 | edge_points[0][0][1]) 37 | .threePointArc(tuple(edge_points[0][1]), 38 | tuple(edge_points[0][2])) 39 | ) 40 | 41 | for i in range(len(edge_points) - 1): 42 | edge_wire = edge_wire.add( 43 | cq.Workplane(plane_list[i + 1]) 44 | .workplane(offset = - offset_list[i + 1]) 45 | .moveTo(edge_points[i + 1][0][0], 46 | edge_points[i + 1][0][1]) 47 | .threePointArc(tuple(edge_points[i + 1][1]), 48 | tuple(edge_points[i + 1][2])) 49 | ) 50 | 51 | surface_points = [[0, 0, 0]] 52 | plate_4 = cq.Workplane("XY") 53 | print(edge_wire) 54 | print(surface_points) 55 | print(thickness) 56 | plate_4 = plate_4.interpPlate(edge_wire, surface_points, 0.5 * thickness) 57 | plate_4 = plate_4.union( 58 | cq.Workplane("XY").interpPlate(edge_wire, surface_points, - 0.5 * thickness) 59 | ) 60 | return self.union(self.eachpoint(lambda loc: plate_4.val().located(loc), True)) 61 | 62 | cq.Workplane.schwartz_p_000 = schwartz_p_000 63 | 64 | # Schwartz D surface, all edges are line segments on different workplanes. 65 | def schwartz_d_000(self, thickness, unit_cell_size): 66 | half_unit_cell = unit_cell_size * 0.5 67 | pts = [ 68 | (0, half_unit_cell, 0), 69 | (half_unit_cell, half_unit_cell, 0), 70 | (half_unit_cell, 0, 0), 71 | (half_unit_cell, 0, half_unit_cell), 72 | (0, 0, half_unit_cell), 73 | (0, half_unit_cell, half_unit_cell), 74 | (0, half_unit_cell, 0) 75 | ] 76 | edge_wire = cq.Workplane().polyline(pts) 77 | surface_points = [[half_unit_cell * 0.5, half_unit_cell * 0.5, half_unit_cell * 0.5]] 78 | plate = cq.Workplane("XY") 79 | plate = plate.interpPlate(edge_wire, surface_points, 0.5 * thickness) 80 | plate = plate.union( 81 | cq.Workplane("XY").interpPlate(edge_wire, surface_points, - 0.5 * thickness) 82 | ) 83 | return self.union(self.eachpoint(lambda loc: plate.val().located(loc), True)) 84 | 85 | cq.Workplane.schwartz_d_000 = schwartz_d_000 86 | 87 | def p_unit_cell(location, thickness, unit_cell_size, 88 | delta = 1e-8 # a small tolerance (1e-10 is too small) 89 | ): 90 | """ 91 | Create a unit cell of a Schwartian P surface by creating a unit cell of a 92 | Schwartz P, then mirroring it in three directions 93 | 94 | :param location: the location of the object 95 | :param thickness: the thickness of the material 96 | :param unit_cell_size: the size of the unit cell 97 | :param delta: a small tolerance (1e-10 is too small) 98 | :return: A CQ object. 99 | """ 100 | half_unit_cell_size = unit_cell_size * 0.5 101 | # Octante 000 102 | pnts = [tuple(unit_cell_size / 2 for i in range(3))] 103 | cq.Workplane.schwartz_p_000 = schwartz_p_000 104 | s_000 = (cq.Workplane("XY").pushPoints(pnts) 105 | .schwartz_p_000(thickness, unit_cell_size)) 106 | result = s_000 107 | # Octante 100 108 | s_100 = s_000.mirror(mirrorPlane = "ZY", 109 | basePointVector = (unit_cell_size, 0, 0)) 110 | result = result.union(s_100) 111 | # Octante 110 112 | #s_000_inverse = (cq.Workplane("XY").pushPoints(pnts) 113 | # .schwartz_p_000(- thickness, unit_cell_size)) 114 | s_110 = s_100.mirror(mirrorPlane = "XZ", 115 | basePointVector = (0, unit_cell_size, 0)) 116 | #s_110 = mirXZ_pos.translate((unit_cell_size, 0, 0)) 117 | result = result.union(s_110) 118 | # Octante 010 119 | s_010 = s_000.mirror(mirrorPlane = "XZ", 120 | basePointVector = (0, unit_cell_size, 0)) 121 | result = result.union(s_010) 122 | # The top side is just a mirror of the bottom one 123 | s_top = result.mirror(mirrorPlane = "XY", 124 | basePointVector = (0, 0, unit_cell_size)) 125 | result = result.union(s_top) 126 | return result.val().located(location) 127 | 128 | cq.Workplane.p_unit_cell = p_unit_cell 129 | 130 | def d_unit_cell(location, thickness, unit_cell_size 131 | ): 132 | """ 133 | Create a unit cell of a Schwartian D surface by creating a unit cell of a 134 | Schwartz polygon, then mirroring it in three directions 135 | 136 | :param location: the location of the object 137 | :param thickness: the thickness of the material 138 | :param unit_cell_size: the size of the unit cell 139 | :return: A CQ object. 140 | """ 141 | half_unit_cell = 0.5 * unit_cell_size 142 | # Octant 000 143 | result = cq.Workplane().schwartz_d_000(thickness, unit_cell_size) 144 | # Octant 110 145 | result = result.union(cq.Workplane().transformed( 146 | offset = cq.Vector(unit_cell_size, unit_cell_size, 0)).transformed( 147 | rotate = cq.Vector(0, 0, 180)) 148 | .schwartz_d_000(thickness, unit_cell_size)) 149 | # Octant 101 150 | result = result.union(cq.Workplane().transformed( 151 | offset = cq.Vector(half_unit_cell, half_unit_cell, half_unit_cell)).transformed( 152 | rotate = cq.Vector(0, 0, 270)) 153 | .schwartz_d_000(thickness, unit_cell_size)) 154 | # Octant 011 155 | result = result.union(cq.Workplane().transformed( 156 | offset = cq.Vector(half_unit_cell, half_unit_cell, half_unit_cell)).transformed( 157 | rotate = cq.Vector(0, 0, 90)) 158 | .schwartz_d_000(thickness, unit_cell_size)) 159 | return result.val().located(location) 160 | 161 | cq.Workplane.d_unit_cell = d_unit_cell 162 | 163 | def schwartz_p_heterogeneous_lattice(unit_cell_size, 164 | min_thickness, 165 | max_thickness, 166 | Nx, Ny, Nz, 167 | rule = 'linear'): 168 | # Register the custrom plugin 169 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 170 | if rule == 'linear': 171 | thicknesses = np.linspace(min_thickness, max_thickness, Nz) 172 | if rule == 'sin': 173 | average = lambda num1, num2: (num1 + num2) / 2 174 | x_data = np.linspace(0, Nz, num = Nz) 175 | print(x_data) 176 | thicknesses = 0.5 * np.sin(x_data) * (max_thickness - min_thickness) + average(min_thickness, max_thickness) 177 | print(thicknesses) 178 | if rule == 'parabola': 179 | x = np.linspace(0, 1, num=Nz) 180 | frep = lambda d_min, d_max :-4*d_max*(x-0.5)*(x-0.5)+d_max+d_min 181 | thicknesses = frep(min_thickness, max_thickness) 182 | UC_pnts = [] 183 | unit_cell_size = 0.5 * unit_cell_size # bacause it's made of 8 mirrored features 184 | for i in range(Nx): 185 | for j in range(Ny): 186 | for k in range(Nz): 187 | UC_pnts.append((2 * i * unit_cell_size, 2 * j * unit_cell_size, 2 * k * unit_cell_size)) 188 | result = cq.Workplane().tag('base') 189 | result = result.pushPoints(UC_pnts) 190 | unit_cell_params = [] 191 | for i in range(Nx * Ny): 192 | for j in range(Nz): 193 | unit_cell_params.append({"thickness": thicknesses[j], 194 | "unit_cell_size": unit_cell_size}) 195 | result = result.eachpointAdaptive(p_unit_cell, 196 | callback_extra_args = unit_cell_params, 197 | useLocalCoords = True) 198 | return result 199 | 200 | 201 | def schwartz_d_heterogeneous_lattice(unit_cell_size, 202 | min_thickness, 203 | max_thickness, 204 | Nx, Ny, Nz, 205 | rule = 'linear'): 206 | # Register the custrom plugin 207 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 208 | cq.Workplane.schwartz_d_000 = schwartz_d_000 209 | if rule == 'linear': 210 | thicknesses = np.linspace(min_thickness, max_thickness, Nz) 211 | if rule == 'sin': 212 | average = lambda num1, num2: (num1 + num2) / 2 213 | x_data = np.linspace(0, Nz, num = Nz) 214 | print(x_data) 215 | thicknesses = 0.5 * np.sin(x_data) * (max_thickness - min_thickness) + average(min_thickness, max_thickness) 216 | print(thicknesses) 217 | if rule == 'parabola': 218 | x = np.linspace(0, 1, num=Nz) 219 | frep = lambda d_min, d_max :-4*d_max*(x-0.5)*(x-0.5)+d_max+d_min 220 | thicknesses = frep(min_thickness, max_thickness) 221 | UC_pnts = [] 222 | for i in range(Nx): 223 | for j in range(Ny): 224 | for k in range(Nz): 225 | UC_pnts.append((i * unit_cell_size, j * unit_cell_size, k * unit_cell_size)) 226 | result = cq.Workplane().tag('base') 227 | result = result.pushPoints(UC_pnts) 228 | unit_cell_params = [] 229 | for i in range(Nx * Ny): 230 | for j in range(Nz): 231 | unit_cell_params.append({"thickness": thicknesses[j], 232 | "unit_cell_size": unit_cell_size}) 233 | result = result.eachpointAdaptive(d_unit_cell, 234 | callback_extra_args = unit_cell_params, 235 | useLocalCoords = True) 236 | return result 237 | 238 | -------------------------------------------------------------------------------- /lqgui_env.yml: -------------------------------------------------------------------------------- 1 | name: lq-occ-conda-test-py3 2 | channels: 3 | - CadQuery 4 | - conda-forge 5 | dependencies: 6 | - pyqt=5 7 | - pyqtgraph=0.12.3=pyhd8ed1ab_0 8 | - python=3.8 9 | - spyder=5 10 | - path=16.2.0=py38h578d9bd_1 11 | - logbook=1.5.3=py38h497a2fe_5 12 | - requests=2.26.0=pyhd8ed1ab_1 13 | - nptyping=1.4.2 14 | - cadquery=2.1 15 | - pip: 16 | - numba==0.57.0 17 | - pyparsing==2.2 18 | - scipy==1.10.1 19 | -------------------------------------------------------------------------------- /pyinstaller.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | import sys, site, os 4 | from path import Path 5 | 6 | block_cipher = None 7 | 8 | spyder_data = Path(site.getsitepackages()[-1]) / 'spyder' 9 | parso_grammar = (Path(site.getsitepackages()[-1]) / 'parso/python').glob('grammar*') 10 | 11 | if sys.platform == 'linux': 12 | occt_dir = os.path.join(Path(sys.prefix), 'share', 'opencascade') 13 | ocp_path = (os.path.join(HOMEPATH, 'OCP.cpython-38-x86_64-linux-gnu.so'), '.') 14 | elif sys.platform == 'darwin': 15 | occt_dir = os.path.join(Path(sys.prefix), 'share', 'opencascade') 16 | ocp_path = (os.path.join(HOMEPATH, 'OCP.cpython-38-darwin.so'), '.') 17 | elif sys.platform == 'win32': 18 | occt_dir = os.path.join(Path(sys.prefix), 'Library', 'share', 'opencascade') 19 | ocp_path = (os.path.join(HOMEPATH, 'OCP.cp38-win_amd64.pyd'), '.') 20 | 21 | a = Analysis(['run.py'], 22 | pathex=['.'], 23 | binaries=[ocp_path], 24 | datas=[(spyder_data, 'spyder'), 25 | (occt_dir, 'opencascade')] + 26 | [(p, 'parso/python') for p in parso_grammar], 27 | hiddenimports=['ipykernel.datapub'], 28 | hookspath=[], 29 | runtime_hooks=['pyinstaller/pyi_rth_occ.py', 30 | 'pyinstaller/pyi_rth_fontconfig.py'], 31 | excludes=['_tkinter',], 32 | win_no_prefer_redirects=False, 33 | win_private_assemblies=False, 34 | cipher=block_cipher, 35 | noarchive=False) 36 | 37 | pyz = PYZ(a.pure, a.zipped_data, 38 | cipher=block_cipher) 39 | exe = EXE(pyz, 40 | a.scripts, 41 | [], 42 | exclude_binaries=True, 43 | name='CQ-editor', 44 | debug=False, 45 | bootloader_ignore_signals=False, 46 | strip=False, 47 | upx=True, 48 | console=True, 49 | icon='icons/cadquery_logo_dark.ico') 50 | 51 | exclude = ('libGL','libEGL','libbsd') 52 | a.binaries = TOC([x for x in a.binaries if not x[0].startswith(exclude)]) 53 | 54 | coll = COLLECT(exe, 55 | a.binaries, 56 | a.zipfiles, 57 | a.datas, 58 | strip=False, 59 | upx=True, 60 | name='CQ-editor') 61 | -------------------------------------------------------------------------------- /pyinstaller/pyi_rth_fontconfig.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | if sys.platform.startswith('linux'): 5 | os.environ['FONTCONFIG_FILE'] = '/etc/fonts/fonts.conf' 6 | os.environ['FONTCONFIG_PATH'] = '/etc/fonts/' 7 | -------------------------------------------------------------------------------- /pyinstaller/pyi_rth_occ.py: -------------------------------------------------------------------------------- 1 | from os import environ as env 2 | 3 | env['CASROOT'] = 'opencascade' 4 | 5 | env['CSF_ShadersDirectory'] = 'opencascade/src/Shaders' 6 | env['CSF_UnitsLexicon'] = 'opencascade/src/UnitsAPI/Lexi_Expr.dat' 7 | env['CSF_UnitsDefinition'] = 'opencascade/src/UnitsAPI/Units.dat' 8 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | xvfb_args=-ac +extension GLX +render 3 | log_level=DEBUG -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os, sys, asyncio 2 | import faulthandler 3 | 4 | faulthandler.enable() 5 | 6 | if 'CASROOT' in os.environ: 7 | del os.environ['CASROOT'] 8 | 9 | if sys.platform == 'win32': 10 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 11 | 12 | from cq_editor.__main__ import main 13 | 14 | main() 15 | -------------------------------------------------------------------------------- /runtests_locally.sh: -------------------------------------------------------------------------------- 1 | python -m pytest --no-xvfb -s 2 | -------------------------------------------------------------------------------- /screenshots/hetero-schwartz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalovisko/LatticeQuery/7078d8cf11f658590c88e6e84e4c728ae2f8be9b/screenshots/hetero-schwartz.png -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalovisko/LatticeQuery/7078d8cf11f658590c88e6e84e4c728ae2f8be9b/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalovisko/LatticeQuery/7078d8cf11f658590c88e6e84e4c728ae2f8be9b/screenshots/screenshot2.png -------------------------------------------------------------------------------- /screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalovisko/LatticeQuery/7078d8cf11f658590c88e6e84e4c728ae2f8be9b/screenshots/screenshot3.png -------------------------------------------------------------------------------- /screenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jalovisko/LatticeQuery/7078d8cf11f658590c88e6e84e4c728ae2f8be9b/screenshots/screenshot4.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os.path 3 | 4 | from setuptools import setup, find_packages 5 | 6 | def read(rel_path): 7 | here = os.path.abspath(os.path.dirname(__file__)) 8 | with codecs.open(os.path.join(here, rel_path), 'r') as fp: 9 | return fp.read() 10 | 11 | def get_version(rel_path): 12 | for line in read(rel_path).splitlines(): 13 | if line.startswith('__version__'): 14 | delim = '"' if '"' in line else "'" 15 | return line.split(delim)[1] 16 | else: 17 | raise RuntimeError("Unable to find version string.") 18 | 19 | setup(name='CQ-editor', 20 | version=get_version('cq_editor/_version.py'), 21 | packages=find_packages(), 22 | entry_points={ 23 | 'gui_scripts': [ 24 | 'cq-editor = cq_editor.__main__:main', 25 | 'CQ-editor = cq_editor.__main__:main' 26 | ]} 27 | ) 28 | -------------------------------------------------------------------------------- /topologies/BCC/unit_cell.py: -------------------------------------------------------------------------------- 1 | from math import hypot, acos, degrees 2 | import numpy 3 | import cadquery as cq 4 | 5 | def eachpointAdaptive( 6 | self, 7 | callback, 8 | callback_extra_args = None, 9 | useLocalCoords = False 10 | ): 11 | """ 12 | Same as each(), except that (1) each item on the stack is converted into a point before it 13 | is passed into the callback function and (2) it allows to pass in additional arguments, one 14 | set for each object to process. 15 | 16 | Conversion of stack items into points means: the resulting stack has a point for each object 17 | on the original stack. Vertices and points remain a point. Faces, Wires, Solids, Edges, and 18 | Shells are converted to a point by using their center of mass. If the stack has zero length, a 19 | single point is returned, which is the center of the current workplane / coordinate system. 20 | 21 | This is adapted from here: 22 | https://github.com/CadQuery/cadquery/issues/628#issuecomment-807493984 23 | 24 | :param callback_extra_args: Array of dicts for keyword arguments that will be 25 | provided to the callback in addition to the obligatory location argument. The outer array 26 | level is indexed by the objects on the stack to iterate over, in the order they appear in 27 | the Workplane.objects attribute. The inner arrays are dicts of keyword arguments, each dict 28 | for one call of the callback function each. If a single dict is provided, then this set of 29 | keyword arguments is used for every call of the callback. 30 | :param useLocalCoords: Should points provided to the callback be in local or global coordinates. 31 | 32 | :return: CadQuery object which contains a list of vectors (points) on its stack. 33 | 34 | .. todo:: Implement that callback_extra_args can also be a single dict. 35 | .. todo:: Implement that empty dicts are used as arguments for calls to the callback if not 36 | enough sets are provided for all objects on the stack. 37 | """ 38 | 39 | # Convert the objects on the stack to a list of points. 40 | pnts = [] 41 | plane = self.plane 42 | loc = self.plane.location 43 | if len(self.objects) == 0: 44 | # When nothing is on the stack, use the workplane origin point. 45 | pnts.append(cq.Location()) 46 | else: 47 | for o in self.objects: 48 | if isinstance(o, (cq.Vector, cq.Shape)): 49 | pnts.append(loc.inverse * cq.Location(plane, o.Center())) 50 | else: 51 | pnts.append(o) 52 | 53 | # If no extra keyword arguments are provided to the callback, provide a list of empty dicts as 54 | # structure for the **() deferencing to work below without issues. 55 | if callback_extra_args is None: 56 | callback_extra_args = [{} for p in pnts] 57 | 58 | # Call the callback for each point and collect the objects it generates with each call. 59 | res = [] 60 | for i, p in enumerate(pnts): 61 | p = (p * loc) if useLocalCoords == False else p 62 | extra_args = callback_extra_args[i] 63 | p_res = callback(p, **extra_args) 64 | p_res = p_res.move(loc) if useLocalCoords == True else p_res 65 | res.append(p_res) 66 | 67 | # For result objects that are wires, make them pending if necessary. 68 | for r in res: 69 | if isinstance(r, cq.Wire) and not r.forConstruction: 70 | self._addPendingWire(r) 71 | 72 | return self.newObject(res) 73 | # Register our custom plugin before use. 74 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 75 | 76 | # The angle is chosen with respect to the positive X direction 77 | def create_diagonal_strut(location, unit_cell_size, radius, angle_x, angle_y): 78 | hypot2D = hypot(unit_cell_size, unit_cell_size) 79 | hypot3D = hypot(hypot2D, unit_cell_size) 80 | result = ( 81 | cq.Workplane() 82 | .transformed(rotate = cq.Vector(angle_x, angle_y, 0)) 83 | .circle(radius) 84 | .extrude(hypot3D) 85 | ) 86 | return result.val().located(location) 87 | 88 | # In a cube ABCDA1B1C1D1 this is the angle C1AD 89 | angle_C1AD = 90 - degrees(acos(3**-.5)) 90 | 91 | def BCC_diagonals(unit_cell_size, strut_radius): 92 | corner_points = unit_cell_size * numpy.array( 93 | [(0, 0), 94 | (1, 0), 95 | (1, 1), 96 | (0, 1)] 97 | ) 98 | result = ( 99 | cq.Workplane("XY") 100 | .pushPoints(corner_points) 101 | .eachpointAdaptive( 102 | create_diagonal_strut, 103 | callback_extra_args = [ 104 | {"unit_cell_size": unit_cell_size, 105 | "radius": strut_radius, 106 | "angle_x": - 45, 107 | "angle_y": angle_C1AD}, 108 | {"unit_cell_size": unit_cell_size, 109 | "radius": strut_radius, 110 | "angle_x": - 45, 111 | "angle_y": - angle_C1AD}, 112 | {"unit_cell_size": unit_cell_size, 113 | "radius": strut_radius, 114 | "angle_x": 45, 115 | "angle_y": - angle_C1AD}, 116 | {"unit_cell_size": unit_cell_size, 117 | "radius": strut_radius, 118 | "angle_x": 45, 119 | "angle_y": angle_C1AD} 120 | ], 121 | useLocalCoords = True 122 | ) 123 | ) 124 | return result 125 | # Register our custom plugin before use. 126 | cq.Workplane.BCC_diagonals = BCC_diagonals 127 | 128 | def BCC_vertical_struts(unit_cell_size, strut_radius): 129 | result = cq.Workplane("XY") 130 | corner_points = unit_cell_size * numpy.array( 131 | [(0, 0), 132 | (1, 0), 133 | (1, 1), 134 | (0, 1)] 135 | ) 136 | for point in corner_points: 137 | result = (result 138 | .union( 139 | cq.Workplane() 140 | .transformed(offset = cq.Vector(point[0], point[1])) 141 | .circle(strut_radius) 142 | .extrude(unit_cell_size) 143 | ) 144 | ) 145 | return result 146 | # Register our custom plugin before use. 147 | cq.Workplane.BCC_vertical_struts = BCC_vertical_struts 148 | 149 | def BCC_bottom_horizontal_struts(unit_cell_size, strut_radius): 150 | result = cq.Workplane("XY") 151 | angle = 90 152 | corner_points = unit_cell_size * numpy.array( 153 | [(0, 0), 154 | (1, 0), 155 | (1, 1), 156 | (0, 1)] 157 | ) 158 | for point in corner_points: 159 | result = (result 160 | .union( 161 | cq.Workplane() 162 | .transformed(offset = cq.Vector(point[0], point[1], 0), 163 | rotate = cq.Vector(90, angle, 0)) 164 | .circle(strut_radius) 165 | .extrude(unit_cell_size) 166 | ) 167 | ) 168 | angle += 90 169 | return result 170 | # Register our custom plugin before use. 171 | cq.Workplane.BCC_bottom_horizontal_struts = BCC_bottom_horizontal_struts 172 | 173 | def BCC_top_horizontal_struts(unit_cell_size, strut_radius): 174 | result = cq.Workplane("XY") 175 | angle = 90 176 | corner_points = unit_cell_size * numpy.array( 177 | [(0, 0), 178 | (1, 0), 179 | (1, 1), 180 | (0, 1)] 181 | ) 182 | for point in corner_points: 183 | result = (result 184 | .union( 185 | cq.Workplane() 186 | .transformed(offset = cq.Vector(point[0], point[1], unit_cell_size), 187 | rotate = cq.Vector(90, angle, 0)) 188 | .circle(strut_radius) 189 | .extrude(unit_cell_size) 190 | ) 191 | ) 192 | angle += 90 193 | return result 194 | # Register our custom plugin before use. 195 | cq.Workplane.BCC_top_horizontal_struts = BCC_top_horizontal_struts 196 | 197 | # Creates 4 nodes at the XY plane of each unit cell 198 | def createNodes(node_diameter, 199 | unit_cell_size, 200 | delta = 0.01 # a small coefficient is needed because CQ thinks that it cuts through emptiness 201 | ): 202 | added_node_diameter = node_diameter + delta 203 | node_radius = node_diameter / 2.0 204 | result = cq.Workplane("XY") 205 | corner_points = unit_cell_size * numpy.array( 206 | [(0, 0), 207 | (1, 0), 208 | (1, 1), 209 | (0, 1)] 210 | ) 211 | for point in corner_points: 212 | result= (result 213 | .union( 214 | cq.Workplane() 215 | .transformed(offset = cq.Vector(point[0], point[1], 0)) 216 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 217 | .edges("|Z") 218 | .fillet(node_radius) 219 | .edges("|X") 220 | .fillet(node_radius) 221 | ) 222 | ) 223 | result= (result 224 | .union( 225 | cq.Workplane() 226 | .transformed(offset = cq.Vector(point[0], point[1], unit_cell_size)) 227 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 228 | .edges("|Z") 229 | .fillet(node_radius) 230 | .edges("|X") 231 | .fillet(node_radius) 232 | ) 233 | ) 234 | half_unit_cell_size = unit_cell_size / 2 235 | result= (result 236 | .union( 237 | cq.Workplane() 238 | .transformed(offset = cq.Vector(half_unit_cell_size, 239 | half_unit_cell_size, 240 | half_unit_cell_size)) 241 | .box(added_node_diameter, added_node_diameter, added_node_diameter) 242 | .edges("|Z") 243 | .fillet(node_radius) 244 | .edges("|X") 245 | .fillet(node_radius) 246 | ) 247 | ) 248 | return result 249 | cq.Workplane.createNodes = createNodes 250 | 251 | def unit_cell(self, unit_cell_size, strut_radius, node_diameter): 252 | result = cq.Workplane("XY") 253 | result = (result 254 | .union(BCC_diagonals(unit_cell_size, strut_radius)) 255 | .union(BCC_vertical_struts(unit_cell_size, strut_radius)) 256 | .union(BCC_bottom_horizontal_struts(unit_cell_size, strut_radius)) 257 | .union(BCC_top_horizontal_struts(unit_cell_size, strut_radius)) 258 | .union(createNodes(node_diameter, unit_cell_size)) 259 | ) 260 | return self.union(self.eachpoint(lambda loc: result.val().located(loc), True)) 261 | cq.Workplane.unit_cell = unit_cell 262 | -------------------------------------------------------------------------------- /topologies/fblgen_helper.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | def eachpointAdaptive( 3 | self, 4 | callback, 5 | callback_extra_args = None, 6 | useLocalCoords = False 7 | ): 8 | """ 9 | Same as each(), except that (1) each item on the stack is converted into a point before it 10 | is passed into the callback function and (2) it allows to pass in additional arguments, one 11 | set for each object to process. 12 | 13 | Conversion of stack items into points means: the resulting stack has a point for each object 14 | on the original stack. Vertices and points remain a point. Faces, Wires, Solids, Edges, and 15 | Shells are converted to a point by using their center of mass. If the stack has zero length, a 16 | single point is returned, which is the center of the current workplane / coordinate system. 17 | 18 | This is adapted from here: 19 | https://github.com/CadQuery/cadquery/issues/628#issuecomment-807493984 20 | 21 | :param callback_extra_args: Array of dicts for keyword arguments that will be 22 | provided to the callback in addition to the obligatory location argument. The outer array 23 | level is indexed by the objects on the stack to iterate over, in the order they appear in 24 | the Workplane.objects attribute. The inner arrays are dicts of keyword arguments, each dict 25 | for one call of the callback function each. If a single dict is provided, then this set of 26 | keyword arguments is used for every call of the callback. 27 | :param useLocalCoords: Should points provided to the callback be in local or global coordinates. 28 | 29 | :return: CadQuery object which contains a list of vectors (points) on its stack. 30 | 31 | .. todo:: Implement that callback_extra_args can also be a single dict. 32 | .. todo:: Implement that empty dicts are used as arguments for calls to the callback if not 33 | enough sets are provided for all objects on the stack. 34 | """ 35 | 36 | # Convert the objects on the stack to a list of points. 37 | pnts = [] 38 | plane = self.plane 39 | loc = self.plane.location 40 | if len(self.objects) == 0: 41 | # When nothing is on the stack, use the workplane origin point. 42 | pnts.append(cq.Location()) 43 | else: 44 | for o in self.objects: 45 | if isinstance(o, (cq.Vector, cq.Shape)): 46 | pnts.append(loc.inverse * cq.Location(plane, o.Center())) 47 | else: 48 | pnts.append(o) 49 | 50 | # If no extra keyword arguments are provided to the callback, provide a list of empty dicts as 51 | # structure for the **() deferencing to work below without issues. 52 | if callback_extra_args is None: 53 | callback_extra_args = [{} for p in pnts] 54 | 55 | # Call the callback for each point and collect the objects it generates with each call. 56 | res = [] 57 | for i, p in enumerate(pnts): 58 | p = (p * loc) if useLocalCoords == False else p 59 | extra_args = callback_extra_args[i] 60 | p_res = callback(p, **extra_args) 61 | p_res = p_res.move(loc) if useLocalCoords == True else p_res 62 | res.append(p_res) 63 | 64 | # For result objects that are wires, make them pending if necessary. 65 | for r in res: 66 | if isinstance(r, cq.Wire) and not r.forConstruction: 67 | self._addPendingWire(r) 68 | 69 | return self.newObject(res) 70 | # Register our custom plugin before use. 71 | cq.Workplane.eachpointAdaptive = eachpointAdaptive 72 | 73 | --------------------------------------------------------------------------------