├── .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 | [](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 | 
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 |
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 |
--------------------------------------------------------------------------------