├── .gitignore ├── LICENSE ├── README.md ├── cqmore ├── __init__.py ├── _plugin.py ├── _solid.py ├── _typing.py ├── _util.py ├── _wire.py ├── curve.py ├── matrix.py ├── polygon.py └── polyhedron.py ├── docs ├── curve.md ├── images │ ├── curve_archimedeanSpiral.JPG │ ├── curve_circle.JPG │ ├── curve_egg.JPG │ ├── curve_helix.JPG │ ├── curve_lemniscateGerono.JPG │ ├── curve_logarithmicSpiral.JPG │ ├── curve_parametricEquation.JPG │ ├── curve_sphericalSpiral.JPG │ ├── curve_squircle.JPG │ ├── curve_superellipse.JPG │ ├── curve_superformula.JPG │ ├── curve_torusKnot.JPG │ ├── matrix_mirror.JPG │ ├── matrix_rotation.JPG │ ├── matrix_rotationX.JPG │ ├── matrix_rotationY.JPG │ ├── matrix_rotationZ.JPG │ ├── matrix_scaling.JPG │ ├── matrix_translation.JPG │ ├── polygon_hull2D.JPG │ ├── polygon_regularPolygon.JPG │ ├── polygon_regularPolygon2.JPG │ ├── polygon_star.JPG │ ├── polygon_taiwan.JPG │ ├── polyhedron_Polyhedron.JPG │ ├── polyhedron_dodecahedron.JPG │ ├── polyhedron_gridSurface.JPG │ ├── polyhedron_hexahedron.JPG │ ├── polyhedron_hull.JPG │ ├── polyhedron_icosahedron.JPG │ ├── polyhedron_octahedron.JPG │ ├── polyhedron_polarZonohedra.JPG │ ├── polyhedron_star.JPG │ ├── polyhedron_superellipsoid.JPG │ ├── polyhedron_sweep.JPG │ ├── polyhedron_sweep2.JPG │ ├── polyhedron_tetrahedron.JPG │ ├── polyhedron_uvSphere.JPG │ ├── workplane_cut2D.JPG │ ├── workplane_hull.JPG │ ├── workplane_hull2.JPG │ ├── workplane_hull2D.JPG │ ├── workplane_intersect2D.JPG │ ├── workplane_makePolygon.JPG │ ├── workplane_polyhedron.JPG │ ├── workplane_polylineJoin.JPG │ ├── workplane_polylineJoin2D.JPG │ ├── workplane_splineApproxSurface.JPG │ └── workplane_union2D.JPG ├── matrix.md ├── polygon.md ├── polyhedron.md └── workplane.md ├── examples ├── gyroid.py ├── gyroid_sphere.py ├── import_stl.py ├── mobius_strip.py ├── platonic_dice.py ├── platonic_solid_frame.py ├── reaction-diffusion.py ├── rounded_star.py ├── superellipsoids.py ├── torus_knot.py ├── turtle_tree.py ├── twisted_strip.py ├── voronoi_box.py ├── voronoi_cube.py └── voronoi_vase.py ├── images ├── ripple.JPG ├── superellipsoids.JPG └── twisted_stripes.JPG ├── setup.py └── tests ├── test_curve.py ├── test_matrix.py ├── test_plugin.py ├── test_polygon.py └── test_polyhedron.py /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cqMore 1.0 2 | 3 | cqMore aims to add more fundamental API to CadQuery. It's based on CadQuery 2.1 and Python 3.9. 4 | 5 | ![cqMore](images/superellipsoids.JPG) 6 | 7 | ## Installation 8 | 9 | Please use `conda` to install CadQuery and its dependencies (see [Getting started](https://github.com/CadQuery/cadquery#getting-started) of CadQuery). Then, use `conda` to install `git` if you don't have it: 10 | 11 | conda install git 12 | 13 | To install cqMore directly from GitHub, run the following `pip` command: 14 | 15 | pip install git+https://github.com/JustinSDK/cqMore 16 | 17 | ## Dependencies 18 | 19 | This plugin has no dependencies other than the cadquery library. The [examples](examples) list their own dependencies in the first comment, if any. 20 | 21 | ## Usage 22 | 23 | You may simply use `cqmore.Workplane` to replace `cadquery.Workplane`. For example: 24 | 25 | from cqmore import Workplane 26 | 27 | result = (Workplane() 28 | .rect(10, 10) 29 | .makePolygon(((-2, -2), (2, -2), (2, 2), (-2, 2))) 30 | .extrude(1) 31 | ) 32 | 33 | You may also attach methods of `cqmore.Workplane` to `cadquery.Workplane`, such as: 34 | 35 | from cadquery import Workplane 36 | import cqmore 37 | cqmore.extend(Workplane) 38 | 39 | result = (Workplane() 40 | .rect(10, 10) 41 | .makePolygon(((-2, -2), (2, -2), (2, 2), (-2, 2))) 42 | .extrude(1) 43 | ) 44 | 45 | ## API Reference 46 | 47 | - [`cqmore.Workplane`](docs/workplane.md) 48 | - [`cqmore.polygon`](docs/polygon.md) 49 | - [`cqmore.polyhedron`](docs/polyhedron.md) 50 | - [`cqmore.curve`](docs/curve.md) 51 | - [`cqmore.matrix`](docs/matrix.md) 52 | -------------------------------------------------------------------------------- /cqmore/__init__.py: -------------------------------------------------------------------------------- 1 | from ._plugin import Workplane 2 | from ._plugin import extend -------------------------------------------------------------------------------- /cqmore/_plugin.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Union, overload 2 | 3 | import cadquery 4 | from cadquery import Wire, Shape, Compound, Solid 5 | from cadquery.cq import T, VectorLike 6 | 7 | from ._typing import FaceIndices, MeshGrid 8 | from ._solid import makePolyhedron, polylineJoin, splineApproxSurface 9 | from ._wire import bool2D, makePolygon, polylineJoinWire 10 | from .polygon import hull2D 11 | from .polyhedron import hull 12 | 13 | 14 | class Workplane(cadquery.Workplane): 15 | """ 16 | Define plugins. You may simply use `cqmore.Workplane` to replace `cadquery.Workplane`. For example: 17 | 18 | from cqmore import Workplane 19 | 20 | result = (Workplane() 21 | .rect(10, 10) 22 | .makePolygon(((-2, -2), (2, -2), (2, 2), (-2, 2))) 23 | .extrude(1) 24 | ) 25 | 26 | You may also attach methods of `cqmore.Workplane` to `cadquery.Workplane`, such as: 27 | 28 | from cadquery import Workplane 29 | import cqmore 30 | cqmore.extend(Workplane) 31 | 32 | result = (Workplane() 33 | .rect(10, 10) 34 | .makePolygon(((-2, -2), (2, -2), (2, 2), (-2, 2))) 35 | .extrude(1) 36 | ) 37 | 38 | """ 39 | 40 | def makePolygon(self: T, points: Iterable[VectorLike], forConstruction: bool = False) -> T: 41 | """ 42 | Make a multiple sided wire through the provided points. 43 | 44 | ## Parameters 45 | 46 | - `points`: a list of x, y points comprise the polygon. 47 | - `forConstruction`: should the new wires be reference geometry only? 48 | 49 | ## Examples 50 | 51 | from cqmore import Workplane 52 | 53 | triangle = Workplane().makePolygon(((-2, -2), (2, -2), (0, 2))) 54 | 55 | """ 56 | 57 | p = makePolygon(points, forConstruction) 58 | return self.eachpoint(lambda loc: p.moved(loc), True) 59 | 60 | 61 | def intersect2D(self: T, toIntersect: Union[T, Wire]) -> T: 62 | """ 63 | Intersect the provided wire from the current wire. 64 | 65 | ## Parameters 66 | 67 | - `toIntersect`: a wire object, or a CQ object having a wire – object to intersect. 68 | 69 | ## Examples 70 | 71 | from cqmore import Workplane 72 | 73 | r1 = Workplane('YZ').rect(10, 10) 74 | r2 = Workplane('YZ').center(5, 5).rect(10, 10) 75 | intersected = r1.intersect2D(r2).extrude(1) 76 | 77 | """ 78 | 79 | return bool2D(self, toIntersect, 'intersect') 80 | 81 | 82 | def union2D(self: T, toUnion: Union[T, Wire]) -> T: 83 | """ 84 | Union the provided wire from the current wire. 85 | 86 | ## Parameters 87 | 88 | - `toUnion`: a wire object, or a CQ object having a wire – object to union. 89 | 90 | ## Examples 91 | 92 | from cqmore import Workplane 93 | 94 | r1 = Workplane('YZ').rect(10, 10) 95 | r2 = Workplane('YZ').center(5, 5).rect(10, 10) 96 | unioned = r1.union2D(r2).extrude(1) 97 | 98 | """ 99 | 100 | return bool2D(self, toUnion, 'union') 101 | 102 | 103 | def cut2D(self: T, toCut: Union[T, Wire]) -> T: 104 | """ 105 | Cut the provided wire from the current wire. 106 | 107 | ## Parameters 108 | 109 | - `toCut`: a wire object, or a CQ object having a wire – object to cut. 110 | 111 | ## Examples 112 | 113 | from cqmore import Workplane 114 | 115 | r1 = Workplane('YZ').rect(10, 10) 116 | r2 = Workplane('YZ').center(5, 5).rect(10, 10) 117 | cutted = r1.cut2D(r2).extrude(1) 118 | 119 | """ 120 | 121 | return bool2D(self, toCut, 'cut') 122 | 123 | @overload 124 | def hull2D(self: T) -> T: 125 | ... 126 | 127 | @overload 128 | def hull2D(self: T, points: Iterable[VectorLike] = ..., forConstruction: bool = ...) -> T: 129 | ... 130 | 131 | def hull2D(self: T, points = None, forConstruction = False) -> T: 132 | """ 133 | Create a convex hull through the provided points. 134 | 135 | ## Parameters 136 | 137 | - `points`: a list of x, y points. If it's `None`, use all pending wires 138 | in the parent chain to create a convex hull. 139 | - `forConstruction`: should the new wires be reference geometry only? 140 | 141 | ## Examples 142 | 143 | from random import random 144 | from cqmore import Workplane 145 | 146 | points = [(random(), random()) for i in range(20)] 147 | convex_hull = Workplane().hull2D(points) 148 | 149 | # an equivalent way 150 | # convex_hull = Workplane().polyline(points).close().hull2D() 151 | 152 | """ 153 | 154 | if points: 155 | p = makePolygon(hull2D(points), forConstruction) 156 | return self.eachpoint(lambda loc: p.moved(loc), True) 157 | 158 | wires = self.ctx.popPendingWires() 159 | 160 | p = makePolygon(hull2D(v.toTuple()[:-1] for wire in wires for v in wire.Vertices()), forConstruction) 161 | moved = self.eachpoint(lambda loc: p.moved(loc), True) 162 | return self.newObject([o for o in moved.objects if not (o in wires)]) 163 | 164 | 165 | def polylineJoin2D(self: T, points: Iterable[VectorLike], join: Union[T, Wire], forConstruction: bool = False) -> T: 166 | """ 167 | Place a join on each point. Hull each pair of joins and union all convex hulls. 168 | 169 | ## Parameters 170 | 171 | - `points`: a list of x, y points. 172 | - `join`: the wire as a join. 173 | - `forConstruction`: should the new wires be reference geometry only? 174 | 175 | ## Examples 176 | 177 | from cqmore import Workplane 178 | 179 | points = [(0, 0), (10, 10), (0, 15), (-10, 10), (-10, 0)] 180 | polyline = Workplane().polylineJoin2D(points, Workplane().polygon(6, 1)) 181 | 182 | """ 183 | 184 | polyline = polylineJoinWire(points, join, forConstruction) 185 | return self.eachpoint(lambda loc: polyline.moved(loc), True) 186 | 187 | 188 | def splineApproxSurface(self: T, points: MeshGrid, thickness: float = 0, combine: bool = True, clean: bool = True) -> T: 189 | """ 190 | Approximate a spline surface through the provided points. 191 | 192 | ## Parameters 193 | 194 | - `points`: a 2D list of Vectors that represent the control points. 195 | - `thickness`: the amount of being thick (return 2D surface if 0). 196 | - `combine`: should the results be combined with other solids on the stack (and each other)? 197 | - `clean`: call `clean()` afterwards to have a clean shape. 198 | 199 | ## Examples 200 | 201 | from cqmore import Workplane 202 | 203 | def paraboloid(x, y): 204 | return (x, y, ((y ** 2) - (x ** 2)) / 4) 205 | 206 | min_value = -30 207 | max_value = 30 208 | step = 5 209 | thickness = 0.5 210 | 211 | points = [[ 212 | paraboloid(x / 10, y / 10) 213 | for y in range(min_value, max_value + step, step) 214 | ] for x in range(min_value, max_value + step, step)] 215 | 216 | surface = Workplane().splineApproxSurface(points, thickness) 217 | 218 | """ 219 | 220 | return _solid_each_combine_clean(self, splineApproxSurface(points, thickness), combine, clean) 221 | 222 | 223 | def polyhedron(self: T, points: Iterable[VectorLike], faces: Iterable[FaceIndices], combine: bool = True, clean: bool = True) -> T: 224 | """ 225 | Create any polyhedron with 3D points(vertices) and faces that enclose the solid. 226 | Each face contains the indices (0 based) of 3 or more points from the `points`. 227 | 228 | ## Parameters 229 | 230 | - `points`: a list of 3D points(vertices). 231 | - `faces`: face indices to fully enclose the solid. When looking at any face from 232 | the outside, the face must list all points in a counter-clockwise order. 233 | - `combine`: should the results be combined with other solids on the stack (and each other)? 234 | - `clean`: call `clean()` afterwards to have a clean shape. 235 | 236 | ## Examples 237 | 238 | from cqmore import Workplane 239 | 240 | points = ((5, -5, -5), (-5, 5, -5), (5, 5, 5), (-5, -5, 5)) 241 | faces = ((0, 1, 2), (0, 3, 1), (1, 3, 2), (0, 2, 3)) 242 | tetrahedron = Workplane().polyhedron(points, faces) 243 | 244 | """ 245 | 246 | return _solid_each_combine_clean(self, makePolyhedron(points, faces), combine, clean) 247 | 248 | 249 | @overload 250 | def hull(self: T) -> T: 251 | ... 252 | 253 | @overload 254 | def hull(self: T, points: Iterable[VectorLike] = ..., combine: bool = ..., clean: bool = ...) -> T: 255 | ... 256 | 257 | def hull(self: T, points = None, combine = True, clean = True) -> T: 258 | """ 259 | Create a convex hull through the provided points. 260 | 261 | ## Parameters 262 | 263 | - `points`: a list of 3D points. If it's `None`, attempt to hull all of the items on the stack 264 | to create a convex hull. 265 | - `combine`: should the results be combined with other solids on the stack (and each other)? 266 | - `clean`: call `clean()` afterwards to have a clean shape. 267 | 268 | ## Examples 269 | 270 | # ex1 271 | 272 | from cqmore import Workplane 273 | 274 | points = ( 275 | (50, 50, 50), 276 | (50, 50, 0), 277 | (-50, 50, 0), 278 | (-50, -50, 0), 279 | (50, -50, 0), 280 | (0, 0, 50), 281 | (0, 0, -50) 282 | ) 283 | 284 | convex_hull = Workplane().hull(points) 285 | 286 | # ex2 287 | 288 | from cqmore import Workplane 289 | from cqmore.polyhedron import uvSphere 290 | 291 | convex_hull = (Workplane() 292 | .polyhedron(*uvSphere(10)) 293 | .box(20, 20, 5) 294 | .hull() 295 | ) 296 | 297 | """ 298 | 299 | if points: 300 | pts = points 301 | else: 302 | items: Iterable[Shape] = (o for o in self.objects if isinstance(o, Shape)) 303 | pts = (v.toTuple() 304 | for shape in items 305 | for v in shape.Vertices() 306 | ) 307 | 308 | return _solid_each_combine_clean(self, makePolyhedron(*hull(pts)), combine, clean) 309 | 310 | 311 | def polylineJoin(self: T, points: Iterable[VectorLike], join: Union[T, Solid, Compound], combine: bool = False, clean: bool = True) -> T: 312 | """ 313 | Place a join on each point. Hull each pair of joins and union all convex hulls. 314 | 315 | ## Parameters 316 | 317 | - `points`: a list of points. 318 | - `join`: the sold as a join. 319 | - `combine`: should the results be combined with other solids on the stack (and each other)? 320 | - `clean`: call `clean()` afterwards to have a clean shape. 321 | 322 | ## Examples 323 | 324 | from cqmore import Workplane 325 | 326 | polyline = (Workplane() 327 | .polylineJoin( 328 | [(0, 0, 0), (10, 0, 0), (10, 0, 10), (10, 10, 10)], 329 | Workplane().box(1, 1, 1) 330 | ) 331 | ) 332 | 333 | """ 334 | 335 | return _solid_each_combine_clean(self, polylineJoin(points, join), combine, clean) 336 | 337 | 338 | def _solid_each_combine_clean(workplane, solid, combine, clean): 339 | all = workplane.eachpoint(lambda loc: solid.moved(loc), True) 340 | if not combine: 341 | return all 342 | else: 343 | return workplane.union(all, clean=clean) 344 | 345 | 346 | def extend(workplaneClz): 347 | """ 348 | Extend `cadquery.Workplane`. 349 | 350 | ## Parameters 351 | 352 | - `workplaneClz`: `cadquery.Workplane`. 353 | 354 | ## Examples 355 | 356 | from cadquery import Workplane 357 | import cqmore 358 | cqmore.extend(Workplane) 359 | 360 | """ 361 | 362 | for attr in dir(Workplane): 363 | if not attr.startswith('__'): 364 | setattr(workplaneClz, attr, getattr(Workplane, attr)) 365 | return workplaneClz 366 | -------------------------------------------------------------------------------- /cqmore/_solid.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Union, cast 2 | 3 | from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin # type: ignore 4 | from OCP.GeomAbs import GeomAbs_Intersection # type: ignore 5 | 6 | from cadquery import Workplane, Shape, Edge, Face, Shell, Solid, Wire, Compound, Vector 7 | from cadquery.cq import T, VectorLike 8 | 9 | from ._typing import FaceIndices, MeshGrid 10 | from ._util import toTuples, toVectors 11 | 12 | from .polyhedron import hull 13 | 14 | import numpy 15 | 16 | 17 | def makePolyhedron(points: Iterable[VectorLike], faces: Iterable[FaceIndices]) -> Solid: 18 | vectors = numpy.array(toVectors(points)) 19 | 20 | return Solid.makeSolid( 21 | Shell.makeShell( 22 | Face.makeFromWires( 23 | Wire.assembleEdges( 24 | Edge.makeLine(*vts[[-1 + i, i]]) for i in range(vts.size) 25 | ) 26 | ) 27 | for vts in (vectors[list(face)] for face in faces) 28 | ) 29 | ) 30 | 31 | 32 | def polylineJoin(points: Iterable[VectorLike], join: Union[T, Solid, Compound]) -> Union[Solid, Compound]: 33 | if isinstance(join, Workplane): 34 | joinSolidCompound = join.val() 35 | elif isinstance(join, Solid) or isinstance(join, Compound): 36 | joinSolidCompound = join 37 | else: 38 | raise ValueError(f"Join type '{type(join)}' is not allowed") 39 | 40 | join_vts = tuple(v.toTuple() for v in cast(Shape, joinSolidCompound).Vertices()) 41 | joins = tuple( 42 | tuple( 43 | Vector(p) + Vector(vt) for vt in join_vts 44 | ) 45 | for p in toTuples(points) 46 | ) 47 | 48 | wp = Workplane() 49 | for i in range(len(joins) - 1): 50 | wp = wp.add(Workplane(makePolyhedron(*hull(joins[i] + joins[i + 1])))) 51 | 52 | return cast(Union[Solid, Compound], wp.combine().val()) 53 | 54 | 55 | def splineApproxSurface(points: MeshGrid, thickness: float) -> Union[Solid, Face]: 56 | if isinstance(points[0][0], Vector): 57 | face = Face.makeSplineApprox(cast(list[list[Vector]], points)) 58 | else: 59 | face = Face.makeSplineApprox([[Vector(*p) for p in col] for col in points]) 60 | 61 | # THICKEN SURFACE 62 | # abs() because negative values are allowed to set direction of thickening 63 | if abs(thickness) > 0: 64 | solid = BRepOffset_MakeOffset() 65 | solid.Initialize( 66 | face.wrapped, 67 | thickness, 68 | 1.0e-5, 69 | BRepOffset_Skin, 70 | False, 71 | False, 72 | GeomAbs_Intersection, 73 | True, 74 | ) # The last True is important to make solid 75 | solid.MakeOffsetShape() 76 | return Solid(solid.Shape()) 77 | else: 78 | return face -------------------------------------------------------------------------------- /cqmore/_typing.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Union 2 | 3 | from cadquery import Vector 4 | 5 | Point2D = tuple[float, float] 6 | Point3D = tuple[float, float, float] 7 | FaceIndices = tuple[int, ...] 8 | MeshGrid = Union[list[list[Point2D]], list[list[Point3D]], list[list[Vector]]] 9 | Polygon = Iterable[Point2D] -------------------------------------------------------------------------------- /cqmore/_util.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, cast 2 | 3 | from cadquery import Vector 4 | from cadquery.cq import VectorLike 5 | 6 | # Signum function 7 | def signum(n): 8 | return n and (1, -1)[n < 0] 9 | 10 | def toVectors(points: Iterable[VectorLike]) -> tuple[Vector]: 11 | if isinstance(next(iter(points)), Vector): 12 | return cast(tuple[Vector], list(points)) 13 | 14 | return cast(tuple[Vector], tuple(Vector(*p) for p in points)) 15 | 16 | 17 | def toTuples(points: Iterable[VectorLike]) -> tuple[tuple]: 18 | if isinstance(next(iter(points)), tuple): 19 | return cast(tuple[tuple], tuple(points)) 20 | 21 | r = tuple(v.toTuple() for v in cast(tuple[Vector], points)) 22 | return cast(tuple[tuple], r) 23 | -------------------------------------------------------------------------------- /cqmore/_wire.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Union, cast 2 | 3 | from cadquery import DirectionSelector, Wire, Workplane, Vector 4 | from cadquery.cq import T, VectorLike 5 | 6 | from ._util import toTuples, toVectors 7 | 8 | from .polygon import hull2D 9 | 10 | 11 | def bool2D(workplane: T, toBool: Union[T, Wire], boolMethod: str) -> T: 12 | if isinstance(toBool, Workplane): 13 | toExtruded = ( 14 | Workplane(workplane.plane) 15 | .add(toBool.ctx.pendingWires) 16 | .toPending() 17 | ) 18 | elif isinstance(toBool, Wire): 19 | toExtruded = ( 20 | Workplane(workplane.plane) 21 | .add(toBool) 22 | .toPending() 23 | ) 24 | else: 25 | raise ValueError(f"Cannot {boolMethod} type '{type(toBool)}'") 26 | 27 | booled = Workplane.__dict__[boolMethod](workplane.extrude(1), toExtruded.extrude(1)) 28 | planeZdir = DirectionSelector(-workplane.plane.zDir) 29 | return booled.faces(planeZdir).wires().toPending() 30 | 31 | def makePolygon(points: Iterable[VectorLike], forConstruction: bool = False) -> Wire: 32 | vts = toVectors(points) 33 | return Wire.makePolygon(vts + (vts[0], ), forConstruction) 34 | 35 | 36 | def polylineJoinWire(points: Iterable[VectorLike], join: Union[T, Wire], forConstruction: bool = False) -> Wire: 37 | if isinstance(join, Workplane): 38 | join_wire = cast(Wire, join.val()) 39 | elif isinstance(join, Wire): 40 | join_wire = join 41 | else: 42 | raise ValueError(f"Join type '{type(join)}' is not allowed") 43 | 44 | join_vts = tuple(v.toTuple() for v in join_wire.Vertices()) 45 | joins = tuple( 46 | tuple( 47 | Vector(p) + Vector(vt) for (*vt, _) in join_vts 48 | ) 49 | for p in toTuples(points) 50 | ) 51 | 52 | wp = Workplane() 53 | for i in range(len(joins) - 1): 54 | wp = wp.add( 55 | Workplane(makePolygon(hull2D(joins[i] + joins[i + 1]), forConstruction)) 56 | .toPending() 57 | .extrude(1) 58 | ) 59 | 60 | wire = cast(Wire, wp.combine().faces(' Point2D: 13 | ''' 14 | The parametric equation of a circle. 15 | 16 | ## Parameters 17 | 18 | - `t`: a parametric variable in the range 0 to 1. 19 | - `radius`: the circle radius. 20 | 21 | ## Examples 22 | 23 | from cqmore import Workplane 24 | from cqmore.curve import circle 25 | 26 | radius = 1 27 | 28 | c = (Workplane() 29 | .parametricCurve(lambda t: circle(t, radius)) 30 | .center(radius * 3, 0) 31 | .polyline([circle(t / 10, radius) for t in range(6)]).close() 32 | .extrude(1) 33 | ) 34 | 35 | ''' 36 | 37 | theta = t * tau 38 | return (radius * cos(theta), radius * sin(theta)) 39 | 40 | 41 | def logarithmicSpiral(t: float, a: float = 1, k: float = 0.306349) -> Point2D: 42 | ''' 43 | The parametric equation of a [logarithmic spiral](https://en.wikipedia.org/wiki/Logarithmic_spiral). 44 | Default to a golden spiral. 45 | 46 | ## Parameters 47 | 48 | - `t`: as it increases, the point traces a right-handed spiral about the z-axis, 49 | in a right-handed coordinate system. 50 | - `a`: the a parameter of the logarithmic spiral. 51 | - `k`: the k parameter of the logarithmic spiral. 52 | 53 | ## Examples 54 | 55 | from cqmore import Workplane 56 | from cqmore.curve import logarithmicSpiral 57 | 58 | spiral = (Workplane() 59 | .polyline([logarithmicSpiral(t / 360) for t in range(360 * 5)]) 60 | ) 61 | 62 | ''' 63 | 64 | theta = t * tau 65 | r = a * e ** (k * theta) 66 | return (r * cos(theta), r * sin(theta)) 67 | 68 | 69 | def archimedeanSpiral(t: float, a: float, b: float) -> Point2D: 70 | ''' 71 | The parametric equation of a [archimedean spiral](https://en.wikipedia.org/wiki/Archimedean_spiral). 72 | 73 | ## Parameters 74 | 75 | - `t`: as it increases, the point traces a right-handed spiral about the z-axis, 76 | in a right-handed coordinate system. 77 | - `a`: move the centerpoint of the spiral outward from the origin. 78 | - `b`: control the distance between loops. 79 | 80 | ## Examples 81 | 82 | from cqmore import Workplane 83 | from cqmore.curve import archimedeanSpiral 84 | 85 | spiral = (Workplane() 86 | .polyline([archimedeanSpiral(t / 360, 1, 1) for t in range(360 * 5)]) 87 | ) 88 | 89 | ''' 90 | 91 | theta = t * tau 92 | r = a + b * theta 93 | return (r * cos(theta), r * sin(theta)) 94 | 95 | 96 | def squircle(t: float, radius: float, s: float) -> Point2D: 97 | ''' 98 | The parametric equation of a [squircle](https://en.wikipedia.org/wiki/Squircle). 99 | 100 | ## Parameters 101 | 102 | - `t`: a parametric variable in the range 0 to 1. 103 | - `s`: the squareness parameter in the range 0 to 1. 104 | 105 | ## Examples 106 | 107 | from cqmore import Workplane 108 | from cqmore.curve import squircle 109 | 110 | r = 10 111 | r1 = Workplane() 112 | for i in range(0, 6): 113 | r1 = (r1.center(r * 3, 0) 114 | .parametricCurve(lambda t: squircle(t, r, i / 5)) 115 | .extrude(1) 116 | ) 117 | 118 | ''' 119 | 120 | def zeroIfNegative(v): 121 | return v if v >= 0 else 0 122 | 123 | theta = t * tau 124 | 125 | if s == 0: 126 | return (radius * cos(theta), radius * sin(theta)) 127 | 128 | rs = 0.5 * radius / s 129 | sscos2t = s ** 2 * cos(2 * theta) 130 | sq2cost = s * sqrt(2) * cos(theta) 131 | sq2sint = s * sqrt(2) * sin(theta) 132 | 133 | x = rs * (sqrt(zeroIfNegative(2 + 2 * sq2cost + sscos2t)) - sqrt(zeroIfNegative(2 - 2 * sq2cost + sscos2t))) 134 | y = rs * (sqrt(zeroIfNegative(2 + 2 * sq2sint - sscos2t)) - sqrt(zeroIfNegative(2 - 2 * sq2sint - sscos2t))) 135 | 136 | return (x, y) 137 | 138 | def egg(t: float, a: float, c: float = 2) -> Point2D: 139 | ''' 140 | The parametric equation of a [Egg Shaped Curve III](https://nyjp07.com/index_egg_by_SuudokuJuku_E.html). 141 | 142 | ## Parameters 143 | 144 | - `t`: a parametric variable in the range 0 to 1. 145 | - `a`: roughly the radius at the big end. 146 | - `c`: `a * c` is the length between ends. 147 | 148 | ## Examples 149 | 150 | from cqmore import Workplane 151 | from cqmore.curve import egg 152 | 153 | radius = 1 154 | 155 | c1 = (Workplane() 156 | .parametricCurve(lambda t: egg(t, radius, c = 2)) 157 | .center(radius * 2.5, 0) 158 | .parametricCurve(lambda t: egg(t, radius, c = 2.5)) 159 | .center(radius * 3, 0) 160 | .parametricCurve(lambda t: egg(t, radius, c = 3)) 161 | .extrude(1) 162 | ) 163 | 164 | ''' 165 | 166 | theta = t * tau 167 | 168 | cost = cos(theta) 169 | sint = sin(theta) 170 | 171 | x = a * ((c - 2) * cost + c + 2) * (cost + 1) / 4 172 | y = a * sint 173 | 174 | return (x, y) 175 | 176 | def helix(t: float, radius: float, slope: float) -> Point3D: 177 | ''' 178 | The parametric equation of a helix. 179 | 180 | ## Parameters 181 | 182 | - `t`: as it increases, the point traces a right-handed helix about the z-axis, 183 | in a right-handed coordinate system. 184 | - `radius`: the helix radius. 185 | - `slope `: the helix slope. 186 | 187 | ## Examples 188 | 189 | from cqmore import Workplane 190 | from cqmore.curve import helix 191 | 192 | radius = 1 193 | slope = 1 194 | 195 | c = (Workplane() 196 | .parametricCurve(lambda t: helix(t, radius, slope), stop = 3) 197 | ) 198 | 199 | ''' 200 | 201 | theta = t * tau 202 | return (radius * cos(theta), radius * sin(theta), radius * slope * t) 203 | 204 | 205 | def sphericalSpiral(t: float, radius: float, c: float = 2) -> Point3D: 206 | ''' 207 | The parametric equation of a [spherical spiral](https://en.wikipedia.org/wiki/Spiral#Spherical_spirals). 208 | 209 | ## Parameters 210 | 211 | - `t`: a parametric variable in the range 0 to 1. 212 | - `radius`: the sphere radius. 213 | - `c `: equal to twice the number of turns. 214 | 215 | ## Examples 216 | 217 | from cqmore import Workplane 218 | from cqmore.curve import sphericalSpiral 219 | 220 | radius = 10 221 | c = 10 222 | 223 | spiral = (Workplane() 224 | .parametricCurve(lambda t: sphericalSpiral(t, radius, c)) 225 | ) 226 | 227 | ''' 228 | 229 | theta = t * pi 230 | sinTheta = sin(theta) 231 | cosTheta = cos(theta) 232 | sinCTheta = sin(c * theta) 233 | cosCTheta = cos(c * theta) 234 | return ( 235 | radius * sinTheta * cosCTheta, 236 | radius * sinTheta * sinCTheta, 237 | radius * cosTheta 238 | ) 239 | 240 | 241 | def torusKnot(t: float, p: int, q: int) -> Point3D: 242 | ''' 243 | The parametric equation of a [torus knot](https://en.wikipedia.org/wiki/Torus_knot). 244 | 245 | ## Parameters 246 | 247 | - `t`: a parametric variable in the range 0 to 1. 248 | - `p`: the p parameter of the (p,q)-torus knot. 249 | - `q`: the q parameter of the (p,q)-torus knot. 250 | 251 | ## Examples 252 | 253 | from cqmore import Workplane 254 | from cqmore.curve import torusKnot 255 | 256 | p = 11 257 | q = 13 258 | 259 | c = (Workplane() 260 | .polyline([torusKnot(t / 360, p, q) for t in range(360)]) 261 | .close() 262 | ) 263 | 264 | ''' 265 | 266 | phi = t * tau 267 | q_phi = q * phi 268 | p_phi = p * phi 269 | r = cos(q_phi) + 2 270 | return (r * cos(p_phi), r * sin(p_phi), -sin(q_phi)) 271 | 272 | 273 | def lemniscateGerono(t: float, a: float = 1, b: float = 1, c: float = 0) -> Point3D: 274 | """ 275 | The parametric equation (a * sin(φ), b * sin(φ) * cos(φ) , c * cos(φ)) of a [Lemniscate of Gerono](https://en.wikipedia.org/wiki/Lemniscate_of_Gerono). 276 | 277 | ## Parameters 278 | 279 | - `t`: a parametric variable in the range 0 to 1. 280 | - `a`: the a parameter of the Lemniscate of Gerono. 281 | - `b`: the b parameter of the Lemniscate of Gerono. 282 | - `c`: the c parameter of the Lemniscate of Gerono. 283 | 284 | ## Examples 285 | 286 | from cqmore import Workplane 287 | from cqmore.curve import lemniscateGerono 288 | 289 | r = (Workplane() 290 | .parametricCurve(lambda t: lemniscateGerono(t))) 291 | 292 | """ 293 | 294 | phi = t * tau 295 | sinp = sin(phi) 296 | cosp = cos(phi) 297 | return (a * sinp, b * sinp * cosp , c * cosp) 298 | 299 | 300 | def superellipse(t: float, n: float, a: float = 1, b: float = 1) -> Point2D: 301 | """ 302 | The parametric equation of a [superellipse](https://en.wikipedia.org/wiki/Superellipse). 303 | 304 | ## Parameters 305 | 306 | - `t`: a parametric variable in the range 0 to 1. 307 | - `n`: the n parameter of the superellipse. 308 | - `a`: the a parameter of the superellipse. 309 | - `b`: the b parameter of the superellipse. 310 | 311 | ## Examples 312 | 313 | from cqmore import Workplane 314 | from cqmore.curve import superellipse 315 | 316 | r1 = Workplane() 317 | for i in range(3, 10): 318 | r1 = (r1.center(3, 0) 319 | .parametricCurve(lambda t: superellipse(t, i / 5), N = 20) 320 | .extrude(1) 321 | ) 322 | 323 | """ 324 | 325 | theta = tau * t 326 | cos_t = cos(theta) 327 | sin_t = sin(theta) 328 | two_n = 2 / n 329 | 330 | return ( 331 | (abs(cos_t) ** two_n) * a * signum(cos_t), 332 | (abs(sin_t) ** two_n) * b * signum(sin_t), 333 | ) 334 | 335 | 336 | def superformula(t: float, m: float, n1: float, n2: float, n3: float, a: float = 1, b: float = 1) -> Point2D: 337 | ''' 338 | The parametric equation of a [superformula](https://en.wikipedia.org/wiki/Superformula). 339 | 340 | ## Parameters 341 | 342 | - `t`: a parametric variable in the range 0 to 1. 343 | - `m`: the m parameter of the superformula. 344 | - `n1`: the n1 parameter of the superformula. 345 | - `n2`: the n2 parameter of the superformula. 346 | - `n3`: the n3 parameter of the superformula. 347 | - `a = 1`: the a parameter of the superformula. 348 | - `b = 1`: the b parameter of the superformula. 349 | 350 | 351 | ## Examples 352 | 353 | from cqmore import Workplane 354 | from cqmore.curve import superformula 355 | 356 | params = [ 357 | [3, 4.5, 10, 10], 358 | [4, 12, 15, 15], 359 | [7, 10, 6, 6], 360 | [5, 4, 4, 4], 361 | [5, 2, 7, 7] 362 | ] 363 | 364 | r1 = Workplane() 365 | for i in range(5): 366 | r1 = (r1.center(4, 0) 367 | .parametricCurve(lambda t: superformula(t, *params[i])) 368 | .extrude(1) 369 | ) 370 | 371 | ''' 372 | 373 | phi = t * tau 374 | r = (abs(cos(m * phi / 4) / a) ** n2 + abs(sin(m * phi / 4) / b) ** n3) ** (-1 / n1) 375 | 376 | return (r * cos(phi), r * sin(phi)) 377 | 378 | 379 | def parametricEquation(func: Callable[..., Union[Point2D, Point3D]], *args: Any, **kwargs: Any) -> Callable[[float], Union[Point2D, Point3D]]: 380 | ''' 381 | Convert `func` into a function f(t) used by `Workplane.parametricCurve`. 382 | 383 | ## Parameters 384 | 385 | - `func`: the parametric equation of a curve. 386 | - `*args`: the positional arguments. 387 | - `**kwargs`: the keyword arguments. 388 | 389 | ## Examples 390 | 391 | from cqmore import Workplane 392 | from cqmore.curve import torusKnot, parametricEquation 393 | 394 | p = 4 395 | q = 9 396 | 397 | c = (Workplane() 398 | .parametricCurve(parametricEquation(torusKnot, p = p, q = q)) 399 | ) 400 | 401 | ''' 402 | 403 | return lambda t: func(t, *args, **kwargs) 404 | -------------------------------------------------------------------------------- /cqmore/matrix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provide the `Matrix3D` class and functions for performing matrix and vector operations. 3 | Here's an example to build a translation matrix for translating a point. 4 | 5 | from cqmore.matrix import translation 6 | 7 | point = (10, 10, 10) 8 | m = translation((10, 0, 0)) # return a Matrix3D instance 9 | new_point = m.transform(point) # (20, 10, 10) 10 | 11 | `Matrix3D` supports matrix multiplication. You can combine multiple transformations in 12 | a single matrix. Say you have a point (10, 0, 0) and you want to translate it by (5, 0, 0) 13 | and then rotate it around the z-axis by 45 degrees. You can do it like: 14 | 15 | from cqmore.matrix import translation, rotationZ 16 | 17 | point = (10, 0, 0) 18 | m = rotationZ(45) @ translation((5, 0, 0)) 19 | new_point = m.transform(point) 20 | 21 | The right-most matrix is first multiplied with the point so you should read the 22 | multiplications from right to left. 23 | 24 | """ 25 | 26 | from typing import Iterable, Union, cast 27 | 28 | from cadquery import Vector 29 | 30 | from ._typing import Point3D 31 | 32 | from math import cos, sin, radians 33 | import numpy 34 | 35 | 36 | class Matrix3D: 37 | def __init__(self, m: Iterable[Iterable[float]]): 38 | ''' 39 | Create a matrix from an array-like object. 40 | 41 | ## Parameters 42 | 43 | - `m`: an array-like object. 44 | 45 | ## Examples 46 | 47 | from cqmore.matrix import Matrix3D 48 | 49 | v = (5, 5, 5) 50 | 51 | # Create a translation matrix 52 | translation = Matrix3D([ 53 | [1, 0, 0, v[0]], 54 | [0, 1, 0, v[1]], 55 | [0, 0, 1, v[2]], 56 | [0, 0, 0, 1] 57 | ]) 58 | 59 | ''' 60 | 61 | if isinstance(m, numpy.ndarray): 62 | self.wrapped = m 63 | else: 64 | self.wrapped = numpy.array(m) 65 | 66 | 67 | def __matmul__(self, that: 'Matrix3D') -> 'Matrix3D': 68 | return Matrix3D(self.wrapped @ that.wrapped) 69 | 70 | 71 | def transform(self, point: Union[Point3D, Vector]) -> Point3D: 72 | ''' 73 | Use the current matrix to transform a point. 74 | 75 | ## Parameters 76 | 77 | - `point`: the point to transform. 78 | 79 | ## Examples 80 | 81 | from cqmore.matrix import Matrix3D 82 | 83 | translation = Matrix3D([ 84 | [1, 0, 0, 5], 85 | [0, 1, 0, 5], 86 | [0, 0, 1, 5], 87 | [0, 0, 0, 1] 88 | ]) 89 | 90 | point = (10, 20, 30) 91 | translated = translation.transform(point) # (15, 25, 35) 92 | 93 | ''' 94 | 95 | vt = (point.x, point.y, point.z, 1) if isinstance(point, Vector) else point + (1,) 96 | return cast(Point3D, tuple((self.wrapped @ vt))[:-1]) 97 | 98 | 99 | def transformAll(self, points: Union[Iterable[Point3D], Iterable[Vector]]) -> tuple[Point3D]: 100 | ''' 101 | Use the current matrix to transform a list of points. 102 | 103 | ## Parameters 104 | 105 | - `points`: a list of points to transform. 106 | 107 | ## Examples 108 | 109 | from cqmore.matrix import Matrix3D 110 | 111 | translation = Matrix3D([ 112 | [1, 0, 0, 5], 113 | [0, 1, 0, 5], 114 | [0, 0, 1, 5], 115 | [0, 0, 0, 1] 116 | ]) 117 | 118 | points = [(10, 20, 30), (0, 0, 0), (-10, -20, -30)] 119 | 120 | # ((15, 25, 35), (5, 5, 5), (-5, -15, -25)) 121 | translated = translation.transformAll(points) 122 | 123 | ''' 124 | 125 | it = iter(points) 126 | if isinstance(next(it), Vector): 127 | r = (tuple((self.wrapped @ (v.x, v.y, v.z, 1)))[:-1] for v in cast(Iterable[Vector], points)) 128 | else: 129 | r = (tuple((self.wrapped @ (p + (1,))))[:-1] for p in cast(Iterable[Point3D], points)) 130 | 131 | return cast(tuple[Point3D], tuple(r)) 132 | 133 | 134 | _identity = [ 135 | [1, 0, 0, 0], 136 | [0, 1, 0, 0], 137 | [0, 0, 1, 0], 138 | [0, 0, 0, 1] 139 | ] 140 | 141 | def identity() -> Matrix3D: 142 | ''' 143 | Create an identity matrix. 144 | 145 | ## Examples 146 | 147 | from cqmore.matrix import identity 148 | 149 | m = identity() 150 | 151 | ''' 152 | 153 | return Matrix3D(numpy.array(_identity)) 154 | 155 | 156 | def scaling(v: Union[Point3D, Vector]) -> Matrix3D: 157 | ''' 158 | Create a scaling matrix. 159 | 160 | ## Parameters 161 | 162 | - `v`: scaling vector. 163 | 164 | ## Examples 165 | 166 | from cqmore.matrix import scaling 167 | from cqmore.polyhedron import uvSphere 168 | from cqmore import Workplane 169 | 170 | sphere = uvSphere(1, widthSegments = 12, heightSegments = 6) 171 | 172 | m = scaling((2, 1, 1)) 173 | scaled_points = m.transformAll(sphere.points) 174 | 175 | r = Workplane().polyhedron(scaled_points, sphere.faces) 176 | 177 | ''' 178 | 179 | return Matrix3D(_scaling(v)) 180 | 181 | 182 | def translation(v: Union[Point3D, Vector]) -> Matrix3D: 183 | ''' 184 | Create a translation matrix. 185 | 186 | ## Parameters 187 | 188 | - `v`: translation vector. 189 | 190 | ## Examples 191 | 192 | from cqmore.matrix import scaling, translation 193 | from cqmore.polyhedron import uvSphere 194 | from cqmore import Workplane 195 | 196 | sphere = uvSphere(1, widthSegments = 12, heightSegments = 6) 197 | 198 | r1 = Workplane().polyhedron(*sphere) 199 | s = scaling((2, 2, 2)) 200 | t = translation((3, 0, 0)) 201 | 202 | transformed_pts = (t @ s).transformAll(sphere.points) 203 | r2 = Workplane().polyhedron(transformed_pts, sphere.faces) 204 | 205 | ''' 206 | 207 | return Matrix3D(_translation(v)) 208 | 209 | 210 | def mirror(v: Union[Point3D, Vector]) -> Matrix3D: 211 | ''' 212 | Create a mirror matrix. 213 | 214 | ## Parameters 215 | 216 | - `v`: mirror vector. 217 | 218 | ## Examples 219 | 220 | from cqmore.matrix import mirror, translation 221 | from cqmore.polyhedron import tetrahedron 222 | from cqmore import Workplane 223 | 224 | t = tetrahedron(1) 225 | 226 | pts = translation((1, 0, 0)).transformAll(t.points) 227 | r1 = Workplane().polyhedron(pts, t.faces) 228 | 229 | mirrored_pts = mirror((1, 0, 0)).transformAll(pts) 230 | r2 = Workplane().polyhedron(mirrored_pts, t.faces) 231 | 232 | ''' 233 | 234 | return Matrix3D(_mirror(v)) 235 | 236 | 237 | def rotationX(angle: float) -> Matrix3D: 238 | ''' 239 | Create a rotation matrix around the x-axis. 240 | 241 | ## Parameters 242 | 243 | - `angle`: angle degrees. 244 | 245 | ## Examples 246 | 247 | from cqmore.matrix import translation, rotationX 248 | from cqmore.polyhedron import tetrahedron 249 | from cqmore import Workplane 250 | 251 | t = tetrahedron(1) 252 | pts = translation((0, 3, 0)).transformAll(t.points) 253 | 254 | for a in range(0, 360, 30): 255 | rotated_pts = rotationX(a).transformAll(pts) 256 | show_object(Workplane().polyhedron(rotated_pts, t.faces)) 257 | 258 | ''' 259 | 260 | return Matrix3D(_rotationX(angle)) 261 | 262 | 263 | def rotationY(angle: float) -> Matrix3D: 264 | ''' 265 | Create a rotation matrix around the y-axis. 266 | 267 | ## Parameters 268 | 269 | - `angle`: angle degrees. 270 | 271 | ## Examples 272 | 273 | from cqmore.matrix import translation, rotationY 274 | from cqmore.polyhedron import tetrahedron 275 | from cqmore import Workplane 276 | 277 | t = tetrahedron(1) 278 | 279 | for i in range(20): 280 | rotated_pts = (rotationY(i * 36) @ translation((3, i, 0))).transformAll(t.points) 281 | show_object(Workplane().polyhedron(rotated_pts, t.faces)) 282 | 283 | ''' 284 | 285 | return Matrix3D(_rotationY(angle)) 286 | 287 | 288 | def rotationZ(angle: float) -> Matrix3D: 289 | ''' 290 | Create a rotation matrix around the z-axis. 291 | 292 | ## Parameters 293 | 294 | - `angle`: angle degrees. 295 | 296 | ## Examples 297 | 298 | from cqmore import Workplane 299 | from cqmore.matrix import translation, rotationX, rotationZ 300 | from cqmore.polyhedron import sweep 301 | 302 | def mobius_strip(radius, frags): 303 | profile = [(10, -1, 0), (10, 1, 0), (-10, 1, 0), (-10, -1, 0)] 304 | 305 | translationX20 = translation((radius, 0, 0)) 306 | rotationX90 = rotationX(90) 307 | 308 | angle_step = 360 / frags 309 | profiles = [] 310 | for i in range(frags): 311 | m = rotationZ(i * angle_step) @ translationX20 @ rotationX90 @ rotationZ(i * angle_step / 2) 312 | profiles.append(m.transformAll(profile)) 313 | 314 | return Workplane().polyhedron(*sweep(profiles, closeIdx = 2)) 315 | 316 | radius = 20 317 | frags = 24 318 | 319 | strip = mobius_strip(radius, frags) 320 | 321 | ''' 322 | 323 | return Matrix3D(_rotationZ(angle)) 324 | 325 | 326 | def rotation(direction: Union[Point3D, Vector], angle: float) -> Matrix3D: 327 | ''' 328 | Create a rotation matrix around the given direction. 329 | 330 | ## Parameters 331 | 332 | - `direction`: axis of rotation. 333 | - `angle`: angle degrees. 334 | 335 | ## Examples 336 | 337 | from cqmore.matrix import rotation, translation, rotationY 338 | from cqmore.polyhedron import tetrahedron 339 | from cqmore import Workplane 340 | 341 | t = tetrahedron(1) 342 | pts = translation((6, 6, 3)).transformAll(t.points) 343 | direction = (10, 10, 10) 344 | 345 | for a in range(0, 360, 30): 346 | rotated_pts = rotation(direction, a).transformAll(pts) 347 | show_object(Workplane().polyhedron(rotated_pts, t.faces)) 348 | 349 | show_object( 350 | Workplane().polylineJoin( 351 | [(0, 0, 0), direction], 352 | Workplane().box(.1, .1, .1) 353 | ) 354 | ) 355 | 356 | ''' 357 | 358 | return Matrix3D(_rotation(direction, angle)) 359 | 360 | 361 | def _scaling(v: Union[Point3D, Vector]) -> numpy.ndarray: 362 | vt = (v.x, v.y, v.z) if isinstance(v, Vector) else v 363 | return numpy.array([ 364 | [vt[0], 0, 0, 0], 365 | [0, vt[1], 0, 0], 366 | [0, 0, vt[2], 0], 367 | [0, 0, 0, 1] 368 | ]) 369 | 370 | 371 | def _translation(v: Union[Point3D, Vector]) -> numpy.ndarray: 372 | vt = (v.x, v.y, v.z) if isinstance(v, Vector) else v 373 | return numpy.array([ 374 | [1, 0, 0, vt[0]], 375 | [0, 1, 0, vt[1]], 376 | [0, 0, 1, vt[2]], 377 | [0, 0, 0, 1] 378 | ]) 379 | 380 | 381 | def _mirror(v: Union[Point3D, Vector]) -> numpy.ndarray: 382 | vt = (v if isinstance(v, Vector) else Vector(*v)).normalized() 383 | txx = -2* vt.x * vt.x 384 | txy = -2* vt.x * vt.y 385 | txz = -2* vt.x * vt.z 386 | tyy = -2* vt.y * vt.y 387 | tyz = -2* vt.y * vt.z 388 | tzz = -2* vt.z * vt.z 389 | 390 | return numpy.array([ 391 | [1 + txx, txy, txz, 0], 392 | [txy, 1 + tyy, tyz, 0], 393 | [txz, tyz, 1 + tzz, 0], 394 | [0, 0, 0, 1] 395 | ]) 396 | 397 | 398 | def _rotationX(angle: float) -> numpy.ndarray: 399 | rad = radians(angle) 400 | c = cos(rad) 401 | s = sin(rad) 402 | return numpy.array([ 403 | [1, 0, 0, 0], 404 | [0, c, -s, 0], 405 | [0, s, c, 0], 406 | [0, 0, 0, 1] 407 | ]) 408 | 409 | 410 | def _rotationY(angle: float) -> numpy.ndarray: 411 | rad = radians(angle) 412 | c = cos(rad) 413 | s = sin(rad) 414 | return numpy.array([ 415 | [c, 0, s, 0], 416 | [0, 1, 0, 0], 417 | [-s, 0, c, 0], 418 | [0, 0, 0, 1] 419 | ]) 420 | 421 | 422 | def _rotationZ(angle: float) -> numpy.ndarray: 423 | rad = radians(angle) 424 | c = cos(rad) 425 | s = sin(rad) 426 | return numpy.array([ 427 | [c, -s, 0, 0], 428 | [s, c, 0, 0], 429 | [0, 0, 1, 0], 430 | [0, 0, 0, 1] 431 | ]) 432 | 433 | 434 | # Quaternions and spatial rotation 435 | def _rotation(direction: Union[Point3D, Vector], angle: float) -> numpy.ndarray: 436 | dir = direction if isinstance(direction, Vector) else Vector(*direction) 437 | axis = dir.normalized() 438 | half_a = radians(angle / 2) 439 | s = sin(half_a) 440 | x = s * axis.x 441 | y = s * axis.y 442 | z = s * axis.z 443 | w = cos(half_a) 444 | x2 = x + x 445 | y2 = y + y 446 | z2 = z + z 447 | xx = x * x2 448 | yx = y * x2 449 | yy = y * y2 450 | zx = z * x2 451 | zy = z * y2 452 | zz = z * z2 453 | wx = w * x2 454 | wy = w * y2 455 | wz = w * z2 456 | return numpy.array([ 457 | [1 - yy - zz, yx - wz, zx + wy, 0], 458 | [yx + wz, 1 - xx - zz, zy - wx, 0], 459 | [zx - wy, zy + wx, 1 - xx - yy, 0], 460 | [0, 0, 0, 1] 461 | ]) 462 | 463 | -------------------------------------------------------------------------------- /cqmore/polygon.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provide functions for creating simple polygons. 3 | 4 | """ 5 | 6 | from math import sin, cos, radians, tau 7 | from typing import Iterable, cast 8 | from cadquery import Vector 9 | from cadquery.cq import VectorLike 10 | 11 | from ._typing import Polygon 12 | from ._util import toTuples 13 | 14 | 15 | def taiwan(h: float, distance: float = 0) -> Polygon: 16 | """ 17 | Create a Taiwan profile. 18 | 19 | ## Parameters 20 | 21 | - `h`: The height of Taiwan. 22 | - `distance`: used for simplifying the shape. If the distance between a point and its 23 | previous points is not greater than distance, the point will be kept. 24 | 25 | ## Examples 26 | 27 | from cqmore import Workplane 28 | from cqmore.polygon import taiwan 29 | 30 | taiwan = (Workplane() 31 | .makePolygon(taiwan(20)) 32 | .extrude(1) 33 | ) 34 | 35 | """ 36 | 37 | profile = ((3.85724, 6.24078), (3.7902, 6.25779), (3.73596, 6.26288), (3.62807, 6.24587), (3.59581, 6.2602), (3.55429, 6.32386), (3.51698, 6.4571), (3.50768, 6.5361), (3.51188, 6.58284), (3.53917, 6.63708), (3.48981, 6.66934), (2.78286, 6.83905), (2.7218, 6.86112), (2.68441, 6.90272), (2.61063, 6.96209), (2.64288, 7.05549), (2.61062, 7.06568), (2.56396, 7.06058), (2.50281, 7.04109), (2.44849, 7.15647), (2.29319, 7.37123), (2.22438, 7.42555), (2.05644, 7.44248), (1.88674, 7.40509), (1.73901, 7.32879), (1.63795, 7.238), (1.58868, 7.14974), (1.54463, 7.0555), (1.5149, 7.0411), (1.48526, 7.01895), (1.53658, 6.9725), (1.58825, 6.92909), (1.63096, 6.86281), (1.61348, 6.80387), (1.57757, 6.83721), (1.53949, 6.89116), (1.48525, 6.87393), (1.43851, 6.87393), (1.40381, 6.86623), (1.37408, 6.85174), (1.28068, 6.77282), (1.20682, 6.7457), (0.723984, 6.65659), (0.586451, 6.60741), (0.5152, 6.57094), (0.458435, 6.52167), (0.393838, 6.47922), (0.108666, 6.38843), (-0.002506, 6.2925), (-0.110307, 6.16693), (-0.278327, 5.89785), (-0.312266, 5.80452), (-0.319942, 5.74076), (-0.35978, 5.72804), (-0.398773, 5.69411), (-0.431113, 5.64921), (-0.443073, 5.60508), (-0.45317, 5.54563), (-0.477762, 5.51429), (-0.504966, 5.48196), (-0.527025, 5.4379), (-0.511683, 5.32413), (-0.544022, 5.28008), (-0.630518, 5.10523), (-0.652506, 5.02118), (-0.721317, 4.96517), (-0.751047, 4.87851), (-0.807895, 4.81922), (-0.849502, 4.76313), (-0.835106, 4.67916), (-0.94535, 4.70123), (-1.03193, 4.67664), (-1.10318, 4.62476), (-1.16685, 4.55856), (-1.18724, 4.52125), (-1.20679, 4.47981), (-1.23643, 4.44755), (-1.33927, 4.41782), (-1.36462, 4.37621), (-1.38844, 4.28037), (-1.47747, 4.11993), (-1.51477, 4.03419), (-1.52674, 3.93321), (-1.55638, 3.87122), (-1.87389, 3.52162), (-1.92308, 3.43757), (-1.96721, 3.28479), (-2.11055, 3.03862), (-2.15224, 2.84946), (-2.18703, 2.75092), (-2.23874, 2.70864), (-2.2481, 2.65676), (-2.43313, 2.44284), (-2.47482, 2.40393), (-2.48998, 2.36148), (-2.49681, 2.20617), (-2.51727, 2.15951), (-2.59526, 2.09828), (-2.61312, 2.05583), (-2.62237, 2.00926), (-2.64705, 1.98728), (-2.68183, 1.97279), (-2.71915, 1.94559), (-2.76589, 1.90137), (-2.80741, 1.82768), (-2.89576, 1.58074), (-2.9601, 1.46267), (-3.00171, 1.34223), (-3.03396, 1.29978), (-3.14261, 1.19451), (-3.16223, 1.14945), (-3.18683, 1.04173), (-3.21142, 0.994987), (-3.30988, 0.906048), (-3.32504, 0.864443), (-3.42105, 0.738702), (-3.43797, 0.692212), (-3.47452, 0.558807), (-3.54409, 0.411083), (-3.62553, 0.283068), (-3.69434, 0.142925), (-3.72407, -0.047077), (-3.71634, -0.135425), (-3.67474, -0.317847), (-3.65948, -0.475761), (-3.62302, -0.539348), (-3.61029, -0.588533), (-3.62554, -0.628538), (-3.65703, -0.687068), (-3.61029, -0.73878), (-3.64002, -0.795629), (-3.57542, -0.918676), (-3.67902, -1.14784), (-3.62553, -1.18944), (-3.59589, -1.2046), (-3.60515, -1.24376), (-3.6476, -1.28781), (-3.68162, -1.37186), (-3.72138, -1.38205), (-3.74597, -1.40925), (-3.79785, -1.54426), (-3.80458, -1.58157), (-3.81731, -1.61121), (-3.84956, -1.65526), (-3.87677, -1.70958), (-3.8665, -1.75893), (-3.84714, -1.80812), (-3.83434, -1.86665), (-3.85397, -1.90666), (-3.89633, -1.91845), (-3.93532, -1.94052), (-3.94552, -2.0025), (-3.90315, -2.05169), (-3.83687, -2.10846), (-3.91452, -2.12525), (-3.92486, -2.16746), (-3.8021, -2.21718), (-3.78004, -2.24169), (-3.79537, -2.28599), (-3.84194, -2.30292), (-3.9014, -2.31052), (-3.94553, -2.3325), (-3.90902, -2.38984), (-3.81228, -2.42599), (-3.7487, -2.45311), (-3.7359, -2.49042), (-3.61032, -2.51249), (-3.51439, -2.52698), (-3.49737, -2.60598), (-3.53645, -2.69921), (-3.61031, -2.75101), (-3.55085, -2.813), (-3.52197, -2.86732), (-3.47455, -3.17194), (-3.46268, -3.20175), (-3.42108, -3.27805), (-3.36173, -3.36723), (-3.3667, -3.40353), (-3.39137, -3.45802), (-3.39137, -3.50468), (-3.37605, -3.57332), (-3.3048, -3.72879), (-3.22849, -3.84755), (-3.18689, -3.9332), (-3.16473, -4.02239), (-3.18175, -4.08606), (-3.21401, -4.15723), (-3.18942, -4.22848), (-3.1377, -4.29316), (-2.99756, -4.4237), (-2.91612, -4.51458), (-2.85675, -4.62575), (-2.84478, -4.76572), (-2.64282, -4.92119), (-2.55624, -4.90831), (-2.51219, -4.92877), (-2.47733, -4.95075), (-2.40355, -5.01441), (-2.33222, -5.06116), (-2.17691, -5.13485), (-2.10053, -5.19944), (-2.05126, -5.27328), (-2.00199, -5.30293), (-1.87641, -5.40147), (-1.84416, -5.44552), (-1.77804, -5.59577), (-1.69399, -5.70181), (-1.66687, -5.74611), (-1.35963, -6.53694), (-1.36899, -6.58351), (-1.4063, -6.638), (-1.41556, -6.68466), (-1.41556, -6.84915), (-1.42322, -6.93076), (-1.41556, -6.96992), (-1.33, -7.11073), (-1.34945, -7.21845), (-1.34692, -7.25315), (-1.28999, -7.29989), (-1.23921, -7.31522), (-1.20181, -7.27783), (-1.19675, -7.17685), (-1.07362, -7.20658), (-0.953101, -7.2609), (-0.849593, -7.33982), (-0.778259, -7.44248), (-0.775729, -7.35413), (-0.795186, -7.21416), (-0.797786, -7.08606), (-0.736641, -7.02913), (-0.685014, -6.99014), (-0.645009, -6.89851), (-0.620417, -6.78565), (-0.637343, -5.77062), (-0.612751, -5.52192), (-0.590692, -5.43534), (-0.443136, -5.1799), (-0.371802, -4.92362), (-0.341989, -4.73344), (-0.29036, -4.64283), (-0.16723, -4.48735), (-0.128151, -4.41105), (-0.046711, -4.20395), (-0.014539, -4.16235), (0.027066, -4.14297), (0.191803, -3.96039), (0.441265, -3.79565), (0.515127, -3.72608), (0.588904, -3.63285), (0.62032, -3.58105), (0.640788, -3.5317), (0.643228, -3.48757), (0.633124, -3.39173), (0.640794, -3.35189), (0.689979, -3.29336), (0.9039, -3.11338), (0.923353, -3.07093), (1.08363, -2.84522), (1.11605, -2.75342), (1.16262, -2.55407), (1.23396, -2.42344), (1.26116, -2.33518), (1.27809, -2.30031), (1.31035, -2.26637), (1.38421, -2.21213), (1.41638, -2.17987), (1.49024, -2.02701), (1.50464, -1.87935), (1.50212, -1.72405), (1.52671, -1.54937), (1.55897, -1.45589), (1.59897, -1.37942), (1.70669, -1.23616), (1.74147, -1.16044), (1.98243, 0.226167), (2.05637, 0.376416), (2.18195, 0.989965), (2.27788, 1.25316), (2.32959, 1.50186), (2.33469, 1.60107), (2.34741, 1.65447), (2.40173, 1.74366), (2.42118, 1.79798), (2.38391, 1.88363), (2.36176, 1.9185), (2.33473, 1.99994), (2.33213, 2.07136), (2.3542, 2.13747), (2.46798, 2.30473), (2.51203, 2.49221), (2.55625, 2.58553), (2.59356, 2.62284), (2.71147, 2.72138), (2.69968, 2.75364), (2.72175, 2.78497), (2.793, 2.8419), (2.89145, 2.97513), (2.98064, 3.06078), (2.97805, 3.26778), (2.98485, 3.3638), (2.99757, 3.41297), (3.01702, 3.44775), (3.03412, 3.48936), (3.02732, 3.59547), (3.03152, 3.64474), (3.14008, 3.76771), (3.23348, 3.82464), (3.27332, 3.8611), (3.26572, 3.90356), (3.23852, 3.94517), (3.22673, 3.98929), (3.23343, 4.02904), (3.32515, 4.08085), (3.33955, 4.16659), (3.31748, 4.26758), (3.2784, 4.34405), (3.3056, 4.3635), (3.37526, 4.38649), (3.39869, 4.42513), (3.32514, 4.42541), (3.22669, 4.4874), (3.17237, 4.58324), (3.16977, 4.70636), (3.19192, 4.7675), (3.19432, 4.8294), (3.14261, 5.06624), (3.14001, 5.17909), (3.14781, 5.29709), (3.1724, 5.41583), (3.20634, 5.51943), (3.25384, 5.59329), (3.54153, 5.94036), (3.62558, 6.01431), (3.94553, 6.17215), (3.91327, 6.21106)) 38 | tw = tuple((p[0] / 15 * h, p[1] / 15 * h) for p in profile) 39 | return tw if distance == 0 else tuple(tw[i] for i in range(len(tw) - 1) if (tw[i][0] - tw[i + 1][0]) ** 2 + (tw[i][1] - tw[i + 1][1]) ** 2 > distance ** 2) 40 | 41 | 42 | def regularPolygon(nSides: int, radius: float, thetaStart: float = 0, thetaEnd: float = 360) -> Polygon: 43 | """ 44 | Create a regular polygon. 45 | 46 | ## Parameters 47 | 48 | - `nSides`: number of sides. 49 | - `radius`: the size of the polygon. 50 | - `thetaStart`: start angle for the first segment. 51 | - `thetaEnd`: end angle for the last segment. 52 | 53 | ## Examples 54 | 55 | # ex1 56 | 57 | from cqmore import Workplane 58 | from cqmore.polygon import regularPolygon 59 | 60 | polygon = (Workplane() 61 | .makePolygon(regularPolygon(nSides = 6, radius = 10)) 62 | .extrude(1) 63 | ) 64 | 65 | # ex2 66 | 67 | from cqmore import Workplane 68 | from cqmore.polygon import regularPolygon 69 | 70 | polygon = (Workplane() 71 | .makePolygon( 72 | regularPolygon( 73 | nSides = 6, 74 | radius = 10, 75 | thetaStart = 45, 76 | thetaEnd = 270 77 | ) 78 | ) 79 | .extrude(1) 80 | ) 81 | 82 | """ 83 | 84 | def _polygon(a, end): 85 | return tuple( 86 | ( 87 | radius * cos(radians(thetaStart + i * a)), 88 | radius * sin(radians(thetaStart + i * a)) 89 | ) 90 | for i in range(end) 91 | ) 92 | 93 | da = thetaEnd - thetaStart 94 | if da > 360: 95 | raise ValueError('(thetaEnd - thetaStart) must be <= 360') 96 | 97 | a = da / nSides 98 | 99 | return _polygon(a, nSides) if da == 360 else ((0.0, 0.0), ) + _polygon(a, nSides + 1) 100 | 101 | 102 | def star(outerRadius: float = 1, innerRadius: float = 0.381966, n: int = 5) -> Polygon: 103 | """ 104 | Create a star. 105 | 106 | ## Parameters 107 | 108 | - `outerRadius`: the outer radius of the star. 109 | - `innerRadius`: The inner radius of the star. 110 | - `n`: the burst number. 111 | 112 | ## Examples 113 | 114 | from cqmore import Workplane 115 | from cqmore.polygon import star 116 | 117 | polygon = (Workplane() 118 | .makePolygon( 119 | star(outerRadius = 10, innerRadius = 5, n = 8) 120 | ) 121 | .extrude(1) 122 | ) 123 | 124 | """ 125 | 126 | right = tau / 4 127 | thetaStep = tau / n 128 | half_thetaStep = thetaStep / 2 129 | points = [] 130 | for i in range(n): 131 | a = thetaStep * i + right 132 | outerPoint = (outerRadius * cos(a), outerRadius * sin(a)) 133 | innerPoint = (innerRadius * cos(a + half_thetaStep), innerRadius * sin(a + half_thetaStep)) 134 | points.append(outerPoint) 135 | points.append(innerPoint) 136 | return points 137 | 138 | 139 | def hull2D(points: Iterable[VectorLike]) -> Polygon: 140 | """ 141 | Create a convex hull through the provided points. 142 | 143 | ## Parameters 144 | 145 | - `points`: a list of x, y points. 146 | 147 | ## Examples 148 | 149 | from random import random 150 | from cqmore import Workplane 151 | from cqmore.polygon import hull2D 152 | 153 | points = [(random(), random()) for i in range(20)] 154 | convex_hull = Workplane().makePolygon(hull2D(points)) 155 | 156 | """ 157 | 158 | def _in_convex(convex_hull, p): 159 | o = Vector(convex_hull[-2]) 160 | return (Vector(convex_hull[-1]) - o).cross(Vector(p) - o).z <= 0 161 | 162 | pts = sorted(toTuples(points)) 163 | 164 | convex_hull = pts[:2] 165 | # lower bound 166 | for p in pts[2:]: 167 | while len(convex_hull) >= 2 and _in_convex(convex_hull, p): 168 | convex_hull.pop() 169 | convex_hull.append(p) 170 | 171 | # upper bound 172 | upper_bound_start = len(convex_hull) + 1 173 | for p in pts[:-1][::-1]: 174 | while len(convex_hull) >= upper_bound_start and _in_convex(convex_hull, p): 175 | convex_hull.pop() 176 | convex_hull.append(p) 177 | convex_hull.pop() 178 | 179 | return cast(Polygon, convex_hull) 180 | -------------------------------------------------------------------------------- /cqmore/polyhedron.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provide the `Polyhedron` class and functions for creating `Polyhedron` instances. 3 | The `Polyhedron` class defines `points` and `faces` attributes. Here is a way 4 | to use them. 5 | 6 | from cqmore.polyhedron import uvSphere 7 | from cqmore import Workplane 8 | 9 | sphere = (Workplane() 10 | .polyhedron(*uvSphere(radius = 10, widthSegments = 10, heightSegments = 5)) 11 | ) 12 | 13 | """ 14 | 15 | from math import cos, radians, sin, pi, tau 16 | 17 | from typing import Iterable, NamedTuple, Union, cast 18 | 19 | from cadquery import Vector 20 | from cadquery.cq import VectorLike 21 | 22 | from ._util import toTuples, signum 23 | from ._typing import MeshGrid, Point3D, FaceIndices 24 | 25 | import numpy 26 | 27 | 28 | class Polyhedron(NamedTuple): 29 | ''' 30 | Define a polyhedron. 31 | 32 | ## Parameters 33 | 34 | - `points`: points of vertices. 35 | - `faces`: face indices. 36 | 37 | ## Examples 38 | 39 | from cqmore.polyhedron import Polyhedron 40 | from cqmore import Workplane 41 | 42 | points = ( 43 | (5, -5, -5), (-5, 5, -5), (5, 5, 5), (-5, -5, 5) 44 | ) 45 | 46 | faces = ( 47 | (0, 1, 2), (0, 3, 1), (1, 3, 2), (0, 2, 3) 48 | ) 49 | 50 | tetrahedron = Polyhedron(points, faces) 51 | tetrahedrons = (Workplane() 52 | .rect(15, 15, forConstruction = True) 53 | .vertices() 54 | .polyhedron(*tetrahedron) 55 | ) 56 | 57 | ''' 58 | 59 | points: Iterable[Point3D] 60 | faces: Iterable[FaceIndices] 61 | 62 | 63 | def uvSphere(radius: float, widthSegments: int = 3, heightSegments: int = 2) -> Polyhedron: 64 | ''' 65 | Create a UV sphere. 66 | 67 | ## Parameters 68 | 69 | - `radius`: sphere radius. 70 | - `widthSegments`: number of horizontal segments. 71 | - `heightSegments`: number of vertical segments. 72 | 73 | ## Examples 74 | 75 | from cqmore.polyhedron import uvSphere 76 | from cqmore import Workplane 77 | 78 | sphere = (Workplane() 79 | .polyhedron(*uvSphere(radius = 10, widthSegments = 10, heightSegments = 5)) 80 | ) 81 | 82 | ''' 83 | 84 | thetaStep = tau / widthSegments 85 | phiStep = pi / heightSegments 86 | points = [] 87 | for p in range(heightSegments - 1, 0, -1): 88 | for t in range(widthSegments): 89 | phi = p * phiStep 90 | theta = t * thetaStep 91 | sinPhi = sin(phi) 92 | x = radius * sinPhi * cos(theta) 93 | y = radius * sinPhi * sin(theta) 94 | z = radius * cos(phi) 95 | points.append((x, y, z)) 96 | points.extend(((0, 0, -radius), (0, 0, radius))) 97 | 98 | # ring 99 | faces = [] 100 | p_stop = heightSegments - 2 101 | t_stop = widthSegments - 1 102 | for p in range(p_stop): 103 | for t in range(t_stop): 104 | i0 = t + widthSegments * p 105 | i1 = (t + 1) + widthSegments * p 106 | i2 = (t + 1) + widthSegments * (p + 1) 107 | i3 = t + widthSegments * (p + 1) 108 | faces.extend(((i0, i1, i2), (i0, i2, i3))) 109 | i0 = t_stop + widthSegments * p 110 | i1 = widthSegments * p 111 | i2 = widthSegments * (p + 1) 112 | i3 = t_stop + widthSegments * (p + 1) 113 | faces.extend(((i0, i1, i2), (i0, i2, i3))) 114 | 115 | # bottom 116 | leng_points = len(points) 117 | bi = leng_points - 2 118 | for t in range(t_stop): 119 | faces.append((bi, t + 1, t)) 120 | faces.append((bi, 0, t_stop)) 121 | 122 | # top 123 | ti = leng_points - 1 124 | li = p_stop * widthSegments 125 | for t in range(t_stop): 126 | faces.append((ti, li + t, li + t + 1)) 127 | faces.append((ti, li + t_stop, li)) 128 | 129 | return Polyhedron(points, faces) 130 | 131 | 132 | def tetrahedron(radius: float, detail: int = 0) -> Polyhedron: 133 | ''' 134 | Create a tetrahedron. 135 | 136 | ## Parameters 137 | 138 | - `radius`: radius of the tetrahedron. 139 | - `detail`: setting this to a value greater than 0 adds vertices making it no longer a tetrahedron. 140 | 141 | ## Examples 142 | 143 | from cqmore.polyhedron import tetrahedron 144 | from cqmore import Workplane 145 | 146 | radius = 1 147 | polyhedra = Workplane() 148 | for detail in range(5): 149 | polyhedra.add( 150 | Workplane() 151 | .polyhedron(*tetrahedron(radius, detail)) 152 | .translate((2 * radius * detail, 0, 0)) 153 | ) 154 | 155 | ''' 156 | 157 | vectors = ( 158 | Vector(1, 1, 1), Vector(-1, -1, 1), Vector(-1, 1, -1), Vector(1, -1, -1) 159 | ) 160 | faces = ( 161 | (2, 1, 0), (0, 3, 2), (1, 3, 0), (2, 3, 1) 162 | ) 163 | return _divide_project(vectors, faces, radius, detail) 164 | 165 | 166 | def hexahedron(radius: float, detail: int = 0) -> Polyhedron: 167 | ''' 168 | Create a hexahedron. 169 | 170 | ## Parameters 171 | 172 | - `radius`: radius of the hexahedron. 173 | - `detail`: setting this to a value greater than 0 adds vertices making it no longer a hexahedron. 174 | 175 | ## Examples 176 | 177 | from cqmore.polyhedron import hexahedron 178 | from cqmore import Workplane 179 | 180 | radius = 1 181 | polyhedra = Workplane() 182 | for detail in range(5): 183 | polyhedra.add( 184 | Workplane() 185 | .polyhedron(*hexahedron(radius, detail)) 186 | .translate((2 * radius * detail, 0, 0)) 187 | ) 188 | 189 | ''' 190 | 191 | t = 1 / (3 ** 0.5) 192 | vectors = ( 193 | Vector(t, t, t), Vector(-t, t, t), Vector(-t, -t, t), Vector(t, -t, t), 194 | Vector(t, t, -t), Vector(-t, t, -t), Vector(-t, -t, -t), Vector(t, -t, -t) 195 | ) 196 | faces = ( 197 | (3, 7, 0), (7, 4, 0), 198 | (0, 4, 1), (4, 5, 1), 199 | (5, 6, 2), (1, 5, 2), 200 | (6, 7, 3), (2, 6, 3), 201 | (2, 3, 0), (1, 2, 0), 202 | (7, 6, 5), (4, 7, 5) 203 | ) 204 | return _divide_project(vectors, faces, radius, detail) 205 | 206 | 207 | def octahedron(radius: float, detail: int = 0) -> Polyhedron: 208 | ''' 209 | Create a octahedron. 210 | 211 | ## Parameters 212 | 213 | - `radius`: radius of the octahedron. 214 | - `detail`: setting this to a value greater than 0 adds vertices making it no longer a octahedron. 215 | 216 | ## Examples 217 | 218 | from cqmore.polyhedron import octahedron 219 | from cqmore import Workplane 220 | 221 | radius = 1 222 | polyhedra = Workplane() 223 | for detail in range(5): 224 | polyhedra.add( 225 | Workplane() 226 | .polyhedron(*octahedron(radius, detail)) 227 | .translate((2 * radius * detail, 0, 0)) 228 | ) 229 | 230 | ''' 231 | 232 | vectors = ( 233 | Vector(1, 0, 0), Vector(-1, 0, 0), Vector(0, 1, 0), 234 | Vector(0, -1, 0), Vector(0, 0, 1), Vector(0, 0, -1) 235 | ) 236 | faces = ( 237 | (0, 2, 4), (0, 4, 3), (0, 3, 5), 238 | (0, 5, 2), (1, 2, 5), (1, 5, 3), 239 | (1, 3, 4), (1, 4, 2) 240 | ) 241 | return _divide_project(vectors, faces, radius, detail) 242 | 243 | 244 | def dodecahedron(radius: float, detail: int = 0) -> Polyhedron: 245 | ''' 246 | Create a dodecahedron. 247 | 248 | ## Parameters 249 | 250 | - `radius`: radius of the dodecahedron. 251 | - `detail`: setting this to a value greater than 0 adds vertices making it no longer a dodecahedron. 252 | 253 | ## Examples 254 | 255 | from cqmore.polyhedron import dodecahedron 256 | from cqmore import Workplane 257 | 258 | radius = 1 259 | polyhedra = Workplane() 260 | for detail in range(5): 261 | polyhedra.add( 262 | Workplane() 263 | .polyhedron(*dodecahedron(radius, detail)) 264 | .translate((2 * radius * detail, 0, 0)) 265 | ) 266 | 267 | ''' 268 | 269 | t = (1 + 5 ** 0.5) / 2 270 | r = 1 / t 271 | vectors = ( 272 | # (±1, ±1, ±1) 273 | Vector(-1, -1, -1), Vector(-1, -1, 1), 274 | Vector(-1, 1, -1), Vector(-1, 1, 1), 275 | Vector(1, -1, -1), Vector(1, -1, 1), 276 | Vector(1, 1, -1), Vector(1, 1, 1), 277 | 278 | # (0, ±1/φ, ±φ) 279 | Vector(0, -r, -t), Vector(0, -r, t), 280 | Vector(0, r, -t), Vector(0, r, t), 281 | 282 | # (±1/φ, ±φ, 0) 283 | Vector(-r, -t, 0), Vector(-r, t, 0), 284 | Vector(r, -t, 0), Vector(r, t, 0), 285 | 286 | # (±φ, 0, ±1/φ) 287 | Vector(-t, 0, -r), Vector(t, 0, -r), 288 | Vector(-t, 0, r), Vector(t, 0, r) 289 | ) 290 | faces = ( 291 | (3, 11, 7), (3, 7, 15), (3, 15, 13), 292 | (7, 19, 17), (7, 17, 6), (7, 6, 15), 293 | (17, 4, 8), (17, 8, 10), (17, 10, 6), 294 | (8, 0, 16), (8, 16, 2), (8, 2, 10), 295 | (0, 12, 1), (0, 1, 18), (0, 18, 16), 296 | (6, 10, 2), (6, 2, 13), (6, 13, 15), 297 | (2, 16, 18), (2, 18, 3), (2, 3, 13), 298 | (18, 1, 9), (18, 9, 11), (18, 11, 3), 299 | (4, 14, 12), (4, 12, 0), (4, 0, 8), 300 | (11, 9, 5), (11, 5, 19), (11, 19, 7), 301 | (19, 5, 14), (19, 14, 4), (19, 4, 17), 302 | (1, 12, 14), (1, 14, 5), (1, 5, 9) 303 | ) 304 | return _divide_project(vectors, faces, radius, detail) 305 | 306 | 307 | def icosahedron(radius: float, detail: int = 0) -> Polyhedron: 308 | ''' 309 | Create a icosahedron. 310 | 311 | ## Parameters 312 | 313 | - `radius`: radius of the icosahedron. 314 | - `detail`: setting this to a value greater than 0 adds vertices making it no longer a icosahedron. 315 | 316 | ## Examples 317 | 318 | from cqmore.polyhedron import icosahedron 319 | from cqmore import Workplane 320 | 321 | radius = 1 322 | polyhedra = Workplane() 323 | for detail in range(5): 324 | polyhedra.add( 325 | Workplane() 326 | .polyhedron(*icosahedron(radius, detail)) 327 | .translate((2 * radius * detail, 0, 0)) 328 | ) 329 | 330 | ''' 331 | 332 | t = (1 + 5 ** 0.5) / 2 333 | vectors = ( 334 | Vector(-1, t, 0), Vector(1, t, 0), Vector(- 1, -t, 0), Vector(1, -t, 0), 335 | Vector(0, -1, t), Vector(0, 1, t), Vector(0, -1, -t), Vector(0, 1, -t), 336 | Vector(t, 0, -1), Vector(t, 0, 1), Vector(-t, 0, -1), Vector(-t, 0, 1) 337 | ) 338 | faces = ( 339 | (0, 11, 5), (0, 5, 1), (0, 1, 7), (0, 7, 10), (0, 10, 11), 340 | (1, 5, 9), (5, 11, 4), (11, 10, 2), (10, 7, 6), (7, 1, 8), 341 | (3, 9, 4), (3, 4, 2), (3, 2, 6), (3, 6, 8), (3, 8, 9), 342 | (4, 9, 5), (2, 4, 11), (6, 2, 10), (8, 6, 7), (9, 8, 1) 343 | ) 344 | return _divide_project(vectors, faces, radius, detail) 345 | 346 | 347 | def _divide_project(vectors, faces, radius, detail): 348 | def _idx(ci, ri, ri_base): 349 | return ci + ri_base[ri] 350 | 351 | def _divide(vectors, detail): 352 | rows = detail + 1 353 | vc = vectors[1] - vectors[0] 354 | vr = vectors[2] - vectors[0] 355 | dc = vc / rows 356 | dr = vr / rows 357 | vts = tuple( 358 | vectors[0] + ci * dc + ri * dr 359 | for ri in range(0, rows + 1) 360 | for ci in range(0, rows - ri + 1) 361 | ) 362 | 363 | acc = 0 364 | ri_base = [] 365 | for ri in range(rows + 1): 366 | ri_base.append(acc) 367 | acc = acc + rows - ri + 1 368 | 369 | faces = [] 370 | for ri in range(rows): 371 | cols = rows - ri - 1 372 | for ci in range(rows - ri): 373 | faces.append(( 374 | _idx(ci, ri, ri_base), 375 | _idx(ci + 1, ri, ri_base), 376 | _idx(ci, ri + 1, ri_base) 377 | )) 378 | if ci != cols: 379 | faces.append(( 380 | _idx(ci + 1, ri, ri_base), 381 | _idx(ci + 1, ri + 1, ri_base), 382 | _idx(ci, ri + 1, ri_base) 383 | )) 384 | 385 | return (vts, faces) 386 | 387 | if detail == 0: 388 | return Polyhedron( 389 | tuple((vt / vt.Length * radius).toTuple() for vt in vectors), 390 | faces 391 | ) 392 | 393 | subdivided_all = [] 394 | for face in faces: 395 | subdivided_all.append( 396 | _divide([vectors[i] for i in face], detail) 397 | ) 398 | 399 | flatten_points = tuple( 400 | (vt / vt.Length * radius).toTuple() 401 | for vts, _ in subdivided_all 402 | for vt in vts 403 | ) 404 | 405 | pts_number_per_tri = len(subdivided_all[0][0]) 406 | flatten_faces = tuple( 407 | (face[0] + i * pts_number_per_tri, face[1] + i * pts_number_per_tri, face[2] + i * pts_number_per_tri) 408 | for i in range(len(subdivided_all)) 409 | for face in subdivided_all[i][1] 410 | ) 411 | 412 | return Polyhedron(flatten_points, flatten_faces) 413 | 414 | 415 | def star(outerRadius: float = 1, innerRadius: float = 0.381966, height: float = 0.5, n: int = 5) -> Polyhedron: 416 | """ 417 | Create a star. 418 | 419 | ## Parameters 420 | 421 | - `outerRadius`: the outer radius of the star. 422 | - `innerRadius`: the inner radius of the star. 423 | - `height`: the star height. 424 | - `n`: the burst number. 425 | 426 | ## Examples 427 | 428 | from cqmore import Workplane 429 | from cqmore.polyhedron import star 430 | 431 | polyhedron = Workplane().polyhedron(*star()) 432 | 433 | """ 434 | 435 | right = tau / 4 436 | thetaStep = tau / n 437 | half_thetaStep = thetaStep / 2 438 | points = [] 439 | for i in range(n): 440 | a = thetaStep * i + right 441 | outerPoint = (outerRadius * cos(a), outerRadius * sin(a), 0) 442 | innerPoint = (innerRadius * cos(a + half_thetaStep), innerRadius * sin(a + half_thetaStep), 0) 443 | points.extend((outerPoint, innerPoint)) 444 | 445 | half_height = height / 2 446 | points.extend(((0, 0, half_height), (0, 0, -half_height))) 447 | 448 | leng_star = n * 2 449 | 450 | faces = [] 451 | for i in range(leng_star): 452 | j = (i + 1) % leng_star 453 | faces.extend(((i, j, leng_star), (leng_star + 1, j, i))) 454 | 455 | return Polyhedron(points, faces) 456 | 457 | 458 | def gridSurface(points: MeshGrid, thickness: float = 0) -> Polyhedron: 459 | """ 460 | Create a surface with a coordinate meshgrid. 461 | 462 | ## Parameters 463 | 464 | - `points`: a coordinate meshgrid. 465 | - `thickness`: the amount of being thick (return 2D surface if 0). 466 | 467 | ## Examples 468 | 469 | from math import sqrt, cos, radians 470 | from cqmore import Workplane 471 | from cqmore.polyhedron import gridSurface 472 | 473 | def ripple(x, y): 474 | n = radians(sqrt(x ** 2 + y ** 2)) 475 | return (x, y, 30 * (cos(n) + cos(3 * n))) 476 | 477 | min_value = -200 478 | max_value = 200 479 | step = 10 480 | thickness = 5 481 | 482 | points = [[ 483 | ripple(x, y) 484 | for y in range(min_value, max_value, step) 485 | ] for x in range(min_value, max_value, step)] 486 | 487 | sf = Workplane().polyhedron(*gridSurface(points, thickness)) 488 | 489 | """ 490 | 491 | # transpose and creater vectors 492 | if isinstance(points[0][0], Vector): 493 | vectors = cast(tuple[tuple[Vector]], 494 | tuple( 495 | tuple(points[ci][ri] for ci in range(len(points))) for ri in range(len(points[0])) 496 | ) 497 | ) 498 | else: 499 | vectors = cast(tuple[tuple[Vector]], 500 | tuple( 501 | tuple(Vector(*points[ci][ri]) for ci in range(len(points))) for ri in range(len(points[0])) 502 | ) 503 | ) 504 | 505 | leng_row = len(vectors) 506 | leng_col = len(vectors[0]) 507 | leng_pts = leng_col * leng_row 508 | 509 | def _append_face_normals(ci, ri, vt_normal_lt): 510 | v0 = vectors[ri][ci] 511 | v1 = vectors[ri][ci + 1] 512 | v2 = vectors[ri + 1][ci + 1] 513 | 514 | vt_normal_lt[ri][ci].append((v1 - v0).cross(v2 - v0)) 515 | vt_normal_lt[ri][ci + 1].append((v2 - v1).cross(v0 - v1)) 516 | vt_normal_lt[ri + 1][ci + 1].append((v0 - v2).cross(v1 - v2)) 517 | 518 | v0 = vectors[ri][ci] 519 | v1 = vectors[ri + 1][ci + 1] 520 | v2 = vectors[ri + 1][ci] 521 | 522 | vt_normal_lt[ri][ci].append((v1 - v0).cross(v2 - v0)) 523 | vt_normal_lt[ri + 1][ci + 1].append((v2 - v1).cross(v0 - v1)) 524 | vt_normal_lt[ri + 1][ci].append((v0 - v2).cross(v1 - v2)) 525 | 526 | def _all_pts(): 527 | if thickness == 0: 528 | return tuple((vt.x, vt.y, vt.z) for row in vectors for vt in row) 529 | 530 | vt_normal_lt = [[[] for _ in range(leng_col)] for _ in range(leng_row)] 531 | for ri in range(leng_row - 1): 532 | for ci in range(leng_col - 1): 533 | _append_face_normals(ci, ri, vt_normal_lt) 534 | 535 | half_thickness = thickness / 2 536 | front_thicken_pts = [] 537 | back_thicken_pts = [] 538 | 539 | for ri in range(leng_row): 540 | for ci in range(leng_col): 541 | # vertex normal 542 | n = sum(vt_normal_lt[ri][ci], Vector()).normalized() 543 | vt = vectors[ri][ci] 544 | v = vt + n.multiply(half_thickness) 545 | front_thicken_pts.append((v.x, v.y, v.z)) 546 | v = vt + n.multiply(-half_thickness) 547 | back_thicken_pts.append((v.x, v.y, v.z)) 548 | 549 | return front_thicken_pts + back_thicken_pts 550 | 551 | def _all_faces(): 552 | faces = [] 553 | for ci in range(leng_col - 1): 554 | i0 = ci 555 | i1 = (ci + 1) 556 | i2 = (ci + 1) + leng_col 557 | i3 = ci + leng_col 558 | faces.extend(((i0, i1, i2), (i0, i2, i3))) 559 | 560 | row0 = numpy.array(faces) 561 | for ri in range(1, leng_row - 1): 562 | faces.extend(map(tuple, (row0 + ri * leng_col))) 563 | 564 | if thickness == 0: 565 | return faces 566 | 567 | # fack faces 568 | faces.extend(tuple((f[2] + leng_pts, f[1] + leng_pts, f[0] + leng_pts) for f in faces)) 569 | 570 | # side faces #1 571 | for ci in range(leng_col - 1): 572 | i1 = ci + leng_pts 573 | i2 = ci + 1 574 | i3 = ci + leng_pts + 1 575 | faces.extend(((ci, i1, i2), (i1, i3, i2))) 576 | 577 | # side faces #2 #4 578 | rx = leng_col - 1 579 | for ri in range(leng_row - 1): 580 | i0 = rx + (ri + 1) * leng_col + leng_pts 581 | i1 = rx + (ri + 1) * leng_col 582 | i2 = rx + ri * leng_col 583 | i3 = rx + ri * leng_col + leng_pts 584 | faces.extend(((i0, i1, i2), (i3, i0, i2))) 585 | 586 | i0 = ri * leng_col 587 | i1 = (ri + 1) * leng_col 588 | i2 = (ri + 1) * leng_col + leng_pts 589 | i3 = ri * leng_col + leng_pts 590 | faces.extend(((i0, i1, i2), (i0, i2, i3))) 591 | 592 | # side faces #3 593 | for ci in range(leng_pts - leng_col, leng_pts - 1): 594 | i0 = ci + 1 595 | i1 = ci + leng_pts 596 | i2 = ci + leng_pts + 1 597 | faces.extend(((i0, i1, ci), (i0, i2, i1))) 598 | 599 | return faces 600 | 601 | return Polyhedron(_all_pts(), _all_faces()) 602 | 603 | 604 | def hull(points: Iterable[VectorLike]) -> Polyhedron: 605 | """ 606 | Create a convex hull through the provided points. 607 | 608 | ## Parameters 609 | 610 | - `points`: a list of 3D points. 611 | 612 | ## Examples 613 | 614 | from cqmore import Workplane 615 | from cqmore.polyhedron import hull 616 | 617 | points = ( 618 | (50, 50, 50), 619 | (50, 50, 0), 620 | (-50, 50, 0), 621 | (-50, -50, 0), 622 | (50, -50, 0), 623 | (0, 0, 50), 624 | (0, 0, -50) 625 | ) 626 | 627 | convex_hull = Workplane().polyhedron(*hull(points)) 628 | 629 | """ 630 | 631 | def _tv1(vectors, vtIndices): 632 | v0 = vtIndices[0] 633 | for v1 in range(1, len(vectors)): 634 | if (vectors[v1] - vectors[v0]).Length != 0: 635 | return v1 636 | raise ValueError('points are the same') 637 | 638 | def _tv2(vectors, vtIndices): 639 | v0, v1 = vtIndices 640 | for v2 in range(v1 + 1, len(vectors)): 641 | nL = (vectors[v1] - vectors[v0]).cross(vectors[v2] - vectors[v0]).Length 642 | if nL != 0: 643 | return v2 644 | raise ValueError('collinear points') 645 | 646 | def _tv3(vectors, vtIndices): 647 | v0, v1, v2 = vtIndices 648 | n = (vectors[v1] - vectors[v0]).cross(vectors[v2] - vectors[v0]) 649 | for v3 in range(v2 + 1, len(vectors)): 650 | e = vectors[v3] - vectors[v0] 651 | if n.dot(e) != 0: 652 | return v3 653 | 654 | raise ValueError('coplanar points') 655 | 656 | def _fstTetrahedron(vectors): 657 | vtIndices = [0] 658 | vtIndices.append(_tv1(vectors, vtIndices)) 659 | vtIndices.append(_tv2(vectors, vtIndices)) 660 | vtIndices.append(_tv3(vectors, vtIndices)) 661 | 662 | v0, v1, v2, v3 = vtIndices 663 | n = (vectors[v1] - vectors[v0]).cross(vectors[v2] - vectors[v0]) 664 | e = vectors[v3] - vectors[v0] 665 | 666 | return ( 667 | vtIndices, 668 | # faces 669 | ( 670 | (v1, v0, v2), 671 | (v0, v1, v3), 672 | (v1, v2, v3), 673 | (v2, v0, v3) 674 | ) 675 | if n.dot(e) > 0 else 676 | ( 677 | (v0, v1, v2), 678 | (v1, v0, v3), 679 | (v2, v1, v3), 680 | (v0, v2, v3) 681 | ) 682 | ) 683 | 684 | def _faceType(vectors, v, faces): 685 | vt0 = vectors[faces[0]] 686 | vt1 = vectors[faces[1]] 687 | vt2 = vectors[faces[2]] 688 | 689 | n = (vt1 - vt0).cross(vt2 - vt0) 690 | d = (vt0 - v).dot(n) 691 | 692 | return 1 if d > 0 else ( # convex 693 | -1 if d < 0 else # concav 694 | 0 # coplane 695 | ) 696 | 697 | def _nextFaces(i, currentFaces, types, edges): 698 | faces = [face for j, face in enumerate(currentFaces) if types[j] >= 0] 699 | 700 | for v0, v1, v2 in currentFaces: 701 | if edges[v0][v1] < 0 and edges[v0][v1] != edges[v1][v0]: 702 | faces.append((v0, v1, i)) 703 | 704 | if edges[v1][v2] < 0 and edges[v1][v2] != edges[v2][v1]: 705 | faces.append((v1, v2, i)) 706 | 707 | if edges[v2][v0] < 0 and edges[v2][v0] != edges[v0][v2]: 708 | faces.append((v2, v0, i)) 709 | 710 | return faces 711 | 712 | vectors = tuple(Vector(*p) for p in sorted(toTuples(points))) 713 | 714 | leng_vectors = len(vectors) 715 | edges = [[0] * leng_vectors for _ in range(leng_vectors)] 716 | 717 | vtIndices, faces = _fstTetrahedron(vectors) 718 | for i in range(leng_vectors): 719 | if not (i in vtIndices): 720 | types = tuple(_faceType(vectors, vectors[i], face) for face in faces) 721 | for j in range(len(faces)): 722 | edges[faces[j][0]][faces[j][1]] = types[j] 723 | edges[faces[j][1]][faces[j][2]] = types[j] 724 | edges[faces[j][2]][faces[j][0]] = types[j] 725 | faces = _nextFaces(i, faces, types, edges) 726 | 727 | pts = tuple(v.toTuple() for v in vectors) 728 | convex_vtIndices = {i for face in faces for i in face} 729 | convex_vertices = tuple(pts[i] for i in convex_vtIndices) 730 | 731 | v_i_lookup = {v: i for i, v in enumerate(convex_vertices)} 732 | convex_faces = tuple( 733 | tuple(v_i_lookup[pts[i]] for i in face) 734 | for face in faces 735 | ) 736 | 737 | return Polyhedron(convex_vertices, convex_faces) 738 | 739 | 740 | def superellipsoid(e: float, n: float, widthSegments: int = 3, heightSegments: int = 2) -> Polyhedron: 741 | """ 742 | Create a [superellipsoid](https://en.wikipedia.org/wiki/Superellipsoid). 743 | 744 | ## Parameters 745 | 746 | - `e`: the east-west parameter. 747 | - `n`: the north-south parameter. 748 | - `widthSegments`: number of horizontal segments. 749 | - `heightSegments`: number of vertical segments. 750 | 751 | ## Examples 752 | 753 | from cqmore import Workplane 754 | from cqmore.polyhedron import superellipsoid 755 | 756 | r = Workplane().polyhedron(*superellipsoid(2.5, .25, widthSegments = 24, heightSegments = 12)) 757 | 758 | """ 759 | 760 | def _c(w, m): 761 | cosw = cos(w) 762 | return signum(cosw) * pow(abs(cosw), m) # type: ignore 763 | 764 | def _s(w, m): 765 | sinw = sin(w) 766 | return signum(sinw) * pow(abs(sinw), m) # type: ignore 767 | 768 | a = 1 769 | b = 1 770 | c = 1 771 | 772 | real_nPhi = heightSegments + 2 773 | thetaStep = tau / widthSegments 774 | phiStep = pi / real_nPhi 775 | sections = [] 776 | for p in range(1, real_nPhi): 777 | phi = -pi / 2 + p * phiStep 778 | section = [] 779 | for t in range(widthSegments): 780 | theta = t * thetaStep 781 | x = a * _c(phi, n) * _c(theta, e) 782 | y = b * _c(phi, n) * _s(theta, e) 783 | z = c * _s(phi, n) 784 | section.append((x, y, z)) 785 | sections.append(section) 786 | 787 | return sweep(sections) 788 | 789 | 790 | def polarZonohedra(n: int, theta: float = 35.5) -> Polyhedron: 791 | """ 792 | Create a polar zonohedra. 793 | 794 | ## Parameters 795 | 796 | - `n`: n equal rhombs surrounding one vertex. (rotational symmetry). 797 | - `theta `: the pitch angle of the edges. 798 | 799 | ## Examples 800 | 801 | from cqmore.polyhedron import polarZonohedra 802 | from cqmore import Workplane 803 | 804 | pz = Workplane().polyhedron(*polarZonohedra(8, 45)) 805 | 806 | """ 807 | def _vertex(i, j, n, theta): 808 | if i > j: 809 | return (0, 0, 0) 810 | 811 | x = 0 812 | y = 0 813 | z = 0 814 | for k in range(i, j + 1): 815 | rtheta = radians(theta) 816 | cosa = cos(rtheta) 817 | a_i_n = radians(360 * k / n) 818 | x += cosa * cos(a_i_n) 819 | y += cosa * sin(a_i_n) 820 | z += sin(rtheta) 821 | return (x, y, z) 822 | 823 | def _rhombi(i, j, n, theta): 824 | return [ 825 | _vertex(i, -1 + j, n, theta), 826 | _vertex(i + 1, -1 + j, n, theta), 827 | _vertex(i + 1, j, n, theta), 828 | _vertex(i, j, n, theta) 829 | ] 830 | 831 | vt_faces = (_rhombi(i, j, n, theta) for i in range(n) for j in range(i + 1, n + i)) 832 | points = [vt for face_vts in vt_faces for vt in face_vts] 833 | faces = [tuple(i * 4 + j for j in range(4)) for i in range(int(len(points) / 4))] 834 | 835 | return Polyhedron(points, faces) 836 | 837 | 838 | def sweep(profiles: Union[list[list[Point3D]], list[list[Vector]]], closeIdx: int = -1) -> Polyhedron: 839 | """ 840 | Create a swept polyhedron. 841 | 842 | ## Parameters 843 | 844 | - `profiles`: list of profiles. 845 | - `closeIdx`: setting it to a value >= 0 creates faces between the first and last profile. 846 | The value decides which index of the last profile is connected to the first index 847 | of the first profile. 848 | 849 | ## Examples 850 | 851 | # ex1 852 | 853 | from cqmore import Workplane 854 | from cqmore.polyhedron import sweep 855 | 856 | profiles = [ 857 | [(10, 0, 0), (10, 0, 10), (20, 0, 10), (20, 0, 0)], 858 | [(0, 10, 0), (0, 10, 10), (0, 20, 10), (0, 20, 0)], 859 | [(-10, 0, 0), (-10, 0, 10), (-20, 0, 10), (-20, 0, 0)], 860 | [(0, -10, 0), (0, -10, 10), (0, -20, 10), (0, -20, 0)] 861 | ] 862 | 863 | r = Workplane().polyhedron(*sweep(profiles)) 864 | 865 | # ex2 866 | 867 | from cqmore import Workplane 868 | from cqmore.polyhedron import sweep 869 | 870 | profiles = [ 871 | [(10, 0, 0), (10, 0, 10), (20, 0, 10), (20, 0, 0)], 872 | [(0, 10, 0), (0, 10, 10), (0, 20, 10), (0, 20, 0)], 873 | [(-10, 0, 0), (-10, 0, 10), (-20, 0, 10), (-20, 0, 0)], 874 | [(0, -10, 0), (0, -10, 10), (0, -20, 10), (0, -20, 0)] 875 | ] 876 | 877 | r = Workplane().polyhedron(*sweep(profiles, closeIdx = 0)) 878 | 879 | """ 880 | 881 | def _revolving_faces0(leng_per_section): 882 | faces = [] 883 | for i in range(leng_per_section): 884 | rbi = (i + 1) % leng_per_section 885 | lti = leng_per_section + i 886 | rti = leng_per_section + rbi 887 | faces.extend(((i, rbi, lti), (rbi, rti, lti))) 888 | return faces 889 | 890 | leng_sections = len(profiles) 891 | leng_per_section = len(profiles[0]) 892 | 893 | faces0 = _revolving_faces0(leng_per_section) 894 | np_faces0 = numpy.array(faces0) 895 | 896 | faces = faces0 897 | for s in range(1, leng_sections - 1): 898 | faces.extend(map(tuple, (np_faces0 + (s * leng_per_section)))) 899 | 900 | if closeIdx == -1: 901 | faces.extend(( 902 | tuple(range(leng_per_section))[::-1], 903 | tuple(range(leng_per_section * (leng_sections - 1), leng_per_section * leng_sections)) 904 | )) 905 | else: 906 | idx_base = leng_per_section * (leng_sections - 1) 907 | for i in range(leng_per_section): 908 | li0 = idx_base + (closeIdx + i) % leng_per_section 909 | li1 = idx_base + (closeIdx + i + 1) % leng_per_section 910 | fi1 = (i + 1) % leng_per_section 911 | faces.extend(((li0, li1, i), (li1, fi1, i))) 912 | 913 | points = tuple(p for section in profiles for p in toTuples(section)) 914 | return Polyhedron(cast(Iterable[Point3D], points), faces) -------------------------------------------------------------------------------- /docs/curve.md: -------------------------------------------------------------------------------- 1 | # `cqmore.curve` 2 | 3 | Provide parametric equations of curves. 4 | 5 | ## 2D Functions 6 | 7 | Signature | Description 8 | --|-- 9 | [`circle(t,radius)`](curve.md#circle) | The parametric equation of a circle. 10 | [`logarithmicSpiral(t[,a,k])`](curve.md#logarithmicSpiral) | The parametric equation of a logarithmic spiral. 11 | [`archimedeanSpiral(t,a,b)`](curve.md#archimedeanSpiral) | The parametric equation of a archimedean spiral. 12 | [`squircle(t,radius,s)`](curve.md#squircle) | (Preview) The parametric equation of a squircle. 13 | [`egg(t,a[,c])`](curve.md#egg) | (Preview) The parametric equation of a Egg Shaped Curve III. 14 | [`superellipse(t,n[,a,b])`](curve.md#superellipse) | The parametric equation of a superellipse. 15 | [`superformula(t,m,n1,n2,n3[,a,b])`](curve.md#superformula) | The parametric equation of a superformula. 16 | 17 | ## 3D Functions 18 | 19 | Signature | Description 20 | --|-- 21 | [`helix(t,radius,slope)`](curve.md#helix) | The parametric equation of a helix. 22 | [`sphericalSpiral(t,radius[,c])`](curve.md#sphericalSpiral) | The parametric equation of a [spherical spiral](https://en.wikipedia.org/wiki/Spiral#Spherical_spirals). 23 | [`torusKnot(t,p,q)`](curve.md#torusKnot) | The parametric equation of a (p,q)-torus knot. 24 | [`lemniscateGerono(t[,a,b,c])`](curve.md#lemniscateGerono) | (Preview) The parametric equation of a [Lemniscate of Gerono](https://en.wikipedia.org/wiki/Lemniscate_of_Gerono). 25 | [`parametricEquation(func[,*args,**kwargs])`](curve.md#parametricEquation) | Convert `func` into a function f(t) used by `Workplane.parametricCurve`. 26 | 27 | ---- 28 | 29 | # `circle` 30 | 31 | The parametric equation of a circle. 32 | 33 | ## Parameters 34 | 35 | - `t` : a parametric variable in the range 0 to 1. 36 | - `radius` : circle radius. 37 | 38 | ## Examples 39 | 40 | from cqmore import Workplane 41 | from cqmore.curve import circle 42 | 43 | radius = 1 44 | 45 | c = (Workplane() 46 | .parametricCurve(lambda t: circle(t, radius)) 47 | .center(radius * 3, 0) 48 | .polyline([circle(t / 10, radius) for t in range(6)]).close() 49 | .extrude(1) 50 | ) 51 | 52 | ![circle](images/curve_circle.JPG) 53 | 54 | # `logarithmicSpiral` 55 | 56 | The parametric equation of a [logarithmic spiral](https://en.wikipedia.org/wiki/Logarithmic_spiral). 57 | Default to a golden spiral. 58 | 59 | ## Parameters 60 | 61 | - `t`: as it increases, the point traces a right-handed spiral about the z-axis, in a right-handed coordinate system. 62 | - `a = 1`: the a parameter of the logarithmic spiral. 63 | - `k = 0.306349`: the k parameter of the logarithmic spiral. 64 | 65 | ## Examples 66 | 67 | from cqmore import Workplane 68 | from cqmore.curve import logarithmicSpiral 69 | 70 | spiral = (Workplane() 71 | .polyline([logarithmicSpiral(t / 360) for t in range(360 * 5)]) 72 | ) 73 | 74 | ![logarithmicSpiral](images/curve_logarithmicSpiral.JPG) 75 | 76 | # `archimedeanSpiral` 77 | 78 | The parametric equation of a [archimedean spiral](https://en.wikipedia.org/wiki/Archimedean_spiral). 79 | 80 | ## Parameters 81 | 82 | - `t`: as it increases, the point traces a right-handed spiral about the z-axis, in a right-handed coordinate system. 83 | - `a`: move the centerpoint of the spiral outward from the origin. 84 | - `b`: control the distance between loops. 85 | 86 | ## Examples 87 | 88 | from cqmore import Workplane 89 | from cqmore.curve import archimedeanSpiral 90 | 91 | spiral = (Workplane() 92 | .polyline([archimedeanSpiral(t / 360, 1, 1) for t in range(360 * 5)]) 93 | ) 94 | 95 | ![archimedeanSpiral](images/curve_archimedeanSpiral.JPG) 96 | 97 | # `squircle` 98 | 99 | The parametric equation of a [squircle](https://en.wikipedia.org/wiki/Squircle). 100 | 101 | ## Parameters 102 | 103 | - `t`: a parametric variable in the range 0 to 1. 104 | - `s`: the squareness parameter in the range 0 to 1. 105 | 106 | ## Examples 107 | 108 | from cqmore import Workplane 109 | from cqmore.curve import squircle 110 | 111 | r = 10 112 | r1 = Workplane() 113 | for i in range(0, 6): 114 | r1 = (r1.center(r * 3, 0) 115 | .parametricCurve(lambda t: squircle(t, r, i / 5)) 116 | .extrude(1) 117 | ) 118 | 119 | ![squircle](images/curve_squircle.JPG) 120 | 121 | # `egg` 122 | 123 | The parametric equation of a [Egg Shaped Curve III](https://nyjp07.com/index_egg_by_SuudokuJuku_E.html). 124 | 125 | ## Parameters 126 | 127 | - `t`: a parametric variable in the range 0 to 1. 128 | - `a`: roughly the radius at the big end. 129 | - `c = 2`: `a * c` is the length between ends. 130 | 131 | ## Examples 132 | 133 | from cqmore import Workplane 134 | from cqmore.curve import egg 135 | 136 | radius = 1 137 | 138 | c1 = (Workplane() 139 | .parametricCurve(lambda t: egg(t, radius, c = 2)) 140 | .center(radius * 2.5, 0) 141 | .parametricCurve(lambda t: egg(t, radius, c = 2.5)) 142 | .center(radius * 3, 0) 143 | .parametricCurve(lambda t: egg(t, radius, c = 3)) 144 | .extrude(1) 145 | ) 146 | 147 | ![egg](images/curve_egg.JPG) 148 | 149 | # `superellipse` 150 | 151 | The parametric equation of a [superellipse](https://en.wikipedia.org/wiki/Superellipse). 152 | 153 | ## Parameters 154 | 155 | - `t`: a parametric variable in the range 0 to 1. 156 | - `n`: the n parameter of the superellipse. 157 | - `a = 1`: the a parameter of the superellipse. 158 | - `b = 1`: the b parameter of the superellipse. 159 | 160 | ## Examples 161 | 162 | from cqmore import Workplane 163 | from cqmore.curve import superellipse 164 | 165 | r1 = Workplane() 166 | for i in range(3, 10): 167 | r1 = (r1.center(3, 0) 168 | .parametricCurve(lambda t: superellipse(t, i / 5), N = 20) 169 | .extrude(1) 170 | ) 171 | 172 | ![superellipse](images/curve_superellipse.JPG) 173 | 174 | # `superformula` 175 | 176 | The parametric equation of a [superformula](https://en.wikipedia.org/wiki/Superformula). 177 | 178 | ## Parameters 179 | 180 | - `t`: a parametric variable in the range 0 to 1. 181 | - `m`: the m parameter of the superformula. 182 | - `n1`: the n1 parameter of the superformula. 183 | - `n2`: the n2 parameter of the superformula. 184 | - `n3`: the n3 parameter of the superformula. 185 | - `a = 1`: the a parameter of the superformula. 186 | - `b = 1`: the b parameter of the superformula. 187 | 188 | ## Examples 189 | 190 | from cqmore import Workplane 191 | from cqmore.curve import superformula 192 | 193 | params = [ 194 | [3, 4.5, 10, 10], 195 | [4, 12, 15, 15], 196 | [7, 10, 6, 6], 197 | [5, 4, 4, 4], 198 | [5, 2, 7, 7] 199 | ] 200 | 201 | r1 = Workplane() 202 | for i in range(5): 203 | r1 = (r1.center(4, 0) 204 | .parametricCurve(lambda t: superformula(t, *params[i])) 205 | .extrude(1) 206 | ) 207 | 208 | ![superformula](images/curve_superformula.JPG) 209 | 210 | # `helix` 211 | 212 | The parametric equation of a helix. 213 | 214 | ## Parameters 215 | 216 | - `t` : as it increases, the point traces a right-handed helix about the z-axis, in a right-handed coordinate system. 217 | - `radius`: the helix radius. 218 | - `slope `: the helix slope. 219 | 220 | ## Examples 221 | 222 | from cqmore import Workplane 223 | from cqmore.curve import helix 224 | 225 | radius = 1 226 | slope = 1 227 | 228 | c = (Workplane() 229 | .parametricCurve(lambda t: helix(t, radius, slope), stop = 3) 230 | ) 231 | 232 | ![helix](images/curve_helix.JPG) 233 | 234 | # `sphericalSpiral` 235 | 236 | The parametric equation of a [spherical spiral](https://en.wikipedia.org/wiki/Spiral#Spherical_spirals). 237 | 238 | ## Parameters 239 | 240 | - `t`: a parametric variable in the range 0 to 1. 241 | - `radius`: the sphere radius. 242 | - `c = 2`: equal to twice the number of turns. 243 | 244 | ## Examples 245 | 246 | from cqmore import Workplane 247 | from cqmore.curve import sphericalSpiral 248 | 249 | radius = 10 250 | c = 10 251 | 252 | spiral = (Workplane() 253 | .parametricCurve(lambda t: sphericalSpiral(t, radius, c)) 254 | ) 255 | 256 | ![sphericalSpiral](images/curve_sphericalSpiral.JPG) 257 | 258 | # `torusKnot` 259 | 260 | The parametric equation of a [torus knot](https://en.wikipedia.org/wiki/Torus_knot). 261 | 262 | ## Parameters 263 | 264 | - `t` : a parametric variable in the range 0 to 1. 265 | - `p`: the p parameter of the (p,q)-torus knot. 266 | - `q `: the q parameter of the (p,q)-torus knot. 267 | 268 | ## Examples 269 | 270 | from cqmore import Workplane 271 | from cqmore.curve import torusKnot 272 | 273 | p = 11 274 | q = 13 275 | 276 | c = (Workplane() 277 | .polyline([torusKnot(t / 360, p, q) for t in range(360)]) 278 | .close() 279 | ) 280 | 281 | ![torusKnot](images/curve_torusKnot.JPG) 282 | 283 | # `lemniscateGerono` 284 | 285 | The parametric equation (a * sin(φ), b * sin(φ) * cos(φ) , c * cos(φ)) of a [Lemniscate of Gerono](https://en.wikipedia.org/wiki/Lemniscate_of_Gerono). 286 | 287 | ## Parameters 288 | 289 | - `t`: a parametric variable in the range 0 to 1. 290 | - `a`: the a parameter of the Lemniscate of Gerono. 291 | - `b`: the b parameter of the Lemniscate of Gerono. 292 | - `c`: the c parameter of the Lemniscate of Gerono. 293 | 294 | ## Examples 295 | 296 | from cqmore import Workplane 297 | from cqmore.curve import lemniscateGerono 298 | 299 | r = (Workplane() 300 | .parametricCurve(lambda t: lemniscateGerono(t))) 301 | 302 | ![lemniscateGerono](images/curve_lemniscateGerono.JPG) 303 | 304 | # `parametricEquation` 305 | 306 | Convert `func` into a function f(t) used by `Workplane.parametricCurve`. 307 | 308 | ## Parameters 309 | 310 | - `func`: the parametric equation of a curve. 311 | - `*args`: the positional arguments. 312 | - `**kwargs`: the keyword arguments. 313 | 314 | ## Examples 315 | 316 | from cqmore import Workplane 317 | from cqmore.curve import torusKnot, parametricEquation 318 | 319 | p = 4 320 | q = 9 321 | 322 | c = (Workplane() 323 | .parametricCurve(parametricEquation(torusKnot, p = p, q = q)) 324 | ) 325 | 326 | ![parametricEquation](images/curve_parametricEquation.JPG) -------------------------------------------------------------------------------- /docs/images/curve_archimedeanSpiral.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_archimedeanSpiral.JPG -------------------------------------------------------------------------------- /docs/images/curve_circle.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_circle.JPG -------------------------------------------------------------------------------- /docs/images/curve_egg.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_egg.JPG -------------------------------------------------------------------------------- /docs/images/curve_helix.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_helix.JPG -------------------------------------------------------------------------------- /docs/images/curve_lemniscateGerono.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_lemniscateGerono.JPG -------------------------------------------------------------------------------- /docs/images/curve_logarithmicSpiral.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_logarithmicSpiral.JPG -------------------------------------------------------------------------------- /docs/images/curve_parametricEquation.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_parametricEquation.JPG -------------------------------------------------------------------------------- /docs/images/curve_sphericalSpiral.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_sphericalSpiral.JPG -------------------------------------------------------------------------------- /docs/images/curve_squircle.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_squircle.JPG -------------------------------------------------------------------------------- /docs/images/curve_superellipse.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_superellipse.JPG -------------------------------------------------------------------------------- /docs/images/curve_superformula.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_superformula.JPG -------------------------------------------------------------------------------- /docs/images/curve_torusKnot.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/curve_torusKnot.JPG -------------------------------------------------------------------------------- /docs/images/matrix_mirror.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/matrix_mirror.JPG -------------------------------------------------------------------------------- /docs/images/matrix_rotation.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/matrix_rotation.JPG -------------------------------------------------------------------------------- /docs/images/matrix_rotationX.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/matrix_rotationX.JPG -------------------------------------------------------------------------------- /docs/images/matrix_rotationY.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/matrix_rotationY.JPG -------------------------------------------------------------------------------- /docs/images/matrix_rotationZ.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/matrix_rotationZ.JPG -------------------------------------------------------------------------------- /docs/images/matrix_scaling.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/matrix_scaling.JPG -------------------------------------------------------------------------------- /docs/images/matrix_translation.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/matrix_translation.JPG -------------------------------------------------------------------------------- /docs/images/polygon_hull2D.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polygon_hull2D.JPG -------------------------------------------------------------------------------- /docs/images/polygon_regularPolygon.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polygon_regularPolygon.JPG -------------------------------------------------------------------------------- /docs/images/polygon_regularPolygon2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polygon_regularPolygon2.JPG -------------------------------------------------------------------------------- /docs/images/polygon_star.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polygon_star.JPG -------------------------------------------------------------------------------- /docs/images/polygon_taiwan.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polygon_taiwan.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_Polyhedron.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_Polyhedron.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_dodecahedron.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_dodecahedron.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_gridSurface.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_gridSurface.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_hexahedron.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_hexahedron.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_hull.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_hull.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_icosahedron.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_icosahedron.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_octahedron.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_octahedron.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_polarZonohedra.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_polarZonohedra.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_star.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_star.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_superellipsoid.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_superellipsoid.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_sweep.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_sweep.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_sweep2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_sweep2.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_tetrahedron.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_tetrahedron.JPG -------------------------------------------------------------------------------- /docs/images/polyhedron_uvSphere.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/polyhedron_uvSphere.JPG -------------------------------------------------------------------------------- /docs/images/workplane_cut2D.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_cut2D.JPG -------------------------------------------------------------------------------- /docs/images/workplane_hull.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_hull.JPG -------------------------------------------------------------------------------- /docs/images/workplane_hull2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_hull2.JPG -------------------------------------------------------------------------------- /docs/images/workplane_hull2D.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_hull2D.JPG -------------------------------------------------------------------------------- /docs/images/workplane_intersect2D.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_intersect2D.JPG -------------------------------------------------------------------------------- /docs/images/workplane_makePolygon.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_makePolygon.JPG -------------------------------------------------------------------------------- /docs/images/workplane_polyhedron.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_polyhedron.JPG -------------------------------------------------------------------------------- /docs/images/workplane_polylineJoin.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_polylineJoin.JPG -------------------------------------------------------------------------------- /docs/images/workplane_polylineJoin2D.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_polylineJoin2D.JPG -------------------------------------------------------------------------------- /docs/images/workplane_splineApproxSurface.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_splineApproxSurface.JPG -------------------------------------------------------------------------------- /docs/images/workplane_union2D.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/docs/images/workplane_union2D.JPG -------------------------------------------------------------------------------- /docs/matrix.md: -------------------------------------------------------------------------------- 1 | # `cqmore.matrix` 2 | 3 | Provide the `Matrix3D` class and functions for performing matrix and vector operations. Here's an example to build a translation matrix for translating a point. 4 | 5 | from cqmore.matrix import translation 6 | 7 | point = (10, 10, 10) 8 | m = translation((10, 0, 0)) # return a Matrix3D instance 9 | new_point = m.transform(point) # (20, 10, 10) 10 | 11 | `Matrix3D` supports matrix multiplication. You can combine multiple transformations in a single matrix. Say you have a point (10, 0, 0) and you want to translate it by (5, 0, 0) and then rotate it around the z-axis by 45 degrees. You can do it like: 12 | 13 | from cqmore.matrix import translation, rotationZ 14 | 15 | point = (10, 0, 0) 16 | m = rotationZ(45) @ translation((5, 0, 0)) 17 | new_point = m.transform(point) 18 | 19 | The right-most matrix is first multiplied with the point so you should read the multiplications from right to left. 20 | 21 | ## Classes 22 | 23 | Signature | Description 24 | --|-- 25 | [`Matrix3D(m)`](matrix.md#matrix3d) | Create a matrix from an array-like object. 26 | 27 | ## `Matrix3D` Operations 28 | 29 | Signature | Description 30 | --|-- 31 | [`transform(point)`](matrix.md#transform) | Use the current matrix to transform a point. 32 | [`transformAll(points)`](matrix.md#transformall) | Use the current matrix to transform a list of points. 33 | 34 | 35 | ## Functions 36 | 37 | Signature | Description 38 | --|-- 39 | [`identity()`](matrix.md#identity) | Create an identity matrix. 40 | [`scaling(v)`](matrix.md#scaling) | Create a scaling matrix. 41 | [`translation(v)`](matrix.md#translation) | Create a translation matrix. 42 | [`mirror(v)`](matrix.md#mirror) | Create a mirror matrix. 43 | [`rotationX(angle)`](matrix.md#rotationX) | Create a rotation matrix around the x-axis. 44 | [`rotationY(angle)`](matrix.md#rotationY) | Create a rotation matrix around the y-axis. 45 | [`rotationZ(angle)`](matrix.md#rotationZ) | Create a rotation matrix around the z-axis. 46 | [`rotation(direction, angle)`](matrix.md#rotation) | Create a rotation matrix around the given direction. 47 | 48 | ---- 49 | 50 | # `Matrix3D` 51 | 52 | Create a matrix from an array-like object. 53 | 54 | ## Parameters 55 | 56 | - `m`: an array-like object. 57 | 58 | ## Examples 59 | 60 | from cqmore.matrix import Matrix3D 61 | 62 | v = (5, 5, 5) 63 | 64 | # Create a translation matrix 65 | translation = Matrix3D([ 66 | [1, 0, 0, v[0]], 67 | [0, 1, 0, v[1]], 68 | [0, 0, 1, v[2]], 69 | [0, 0, 0, 1] 70 | ]) 71 | 72 | # `transform` 73 | 74 | Use the current matrix to transform a point. 75 | 76 | ## Parameters 77 | 78 | - `point`: the point to transform. 79 | 80 | ## Examples 81 | 82 | from cqmore.matrix import Matrix3D 83 | 84 | translation = Matrix3D([ 85 | [1, 0, 0, 5], 86 | [0, 1, 0, 5], 87 | [0, 0, 1, 5], 88 | [0, 0, 0, 1] 89 | ]) 90 | 91 | point = (10, 20, 30) 92 | translated = translation.transform(point) # (15, 25, 35) 93 | 94 | # `transformAll` 95 | 96 | Use the current matrix to transform a list of points. 97 | 98 | ## Parameters 99 | 100 | - `points`: a list of points to transform. 101 | 102 | ## Examples 103 | 104 | from cqmore.matrix import Matrix3D 105 | 106 | translation = Matrix3D([ 107 | [1, 0, 0, 5], 108 | [0, 1, 0, 5], 109 | [0, 0, 1, 5], 110 | [0, 0, 0, 1] 111 | ]) 112 | 113 | points = [(10, 20, 30), (0, 0, 0), (-10, -20, -30)] 114 | 115 | # ((15, 25, 35), (5, 5, 5), (-5, -15, -25)) 116 | translated = translation.transformAll(points) 117 | 118 | # `identity` 119 | 120 | Create an identity matrix. 121 | 122 | ## Examples 123 | 124 | from cqmore.matrix import identity 125 | 126 | m = identity() 127 | 128 | # `scaling` 129 | 130 | Create a scaling matrix. 131 | 132 | ## Parameters 133 | 134 | - `v`: scaling vector. 135 | 136 | ## Examples 137 | 138 | from cqmore.matrix import scaling 139 | from cqmore.polyhedron import uvSphere 140 | from cqmore import Workplane 141 | 142 | sphere = uvSphere(1, widthSegments = 12, heightSegments = 6) 143 | 144 | m = scaling((2, 1, 1)) 145 | scaled_points = m.transformAll(sphere.points) 146 | 147 | r = Workplane().polyhedron(scaled_points, sphere.faces) 148 | 149 | ![scaling](images/matrix_scaling.JPG) 150 | 151 | # `translation` 152 | 153 | Create a translation matrix. 154 | 155 | ## Parameters 156 | 157 | - `v`: translation vector. 158 | 159 | ## Examples 160 | 161 | from cqmore.matrix import scaling, translation 162 | from cqmore.polyhedron import uvSphere 163 | from cqmore import Workplane 164 | 165 | sphere = uvSphere(1, widthSegments = 12, heightSegments = 6) 166 | 167 | r1 = Workplane().polyhedron(*sphere) 168 | s = scaling((2, 2, 2)) 169 | t = translation((3, 0, 0)) 170 | 171 | transformed_pts = (t @ s).transformAll(sphere.points) 172 | r2 = Workplane().polyhedron(transformed_pts, sphere.faces) 173 | 174 | ![translation](images/matrix_translation.JPG) 175 | 176 | # `mirror` 177 | 178 | Create a mirror matrix. 179 | 180 | ## Parameters 181 | 182 | - `v`: mirror vector. 183 | 184 | ## Examples 185 | 186 | from cqmore.matrix import mirror, translation 187 | from cqmore.polyhedron import tetrahedron 188 | from cqmore import Workplane 189 | 190 | t = tetrahedron(1) 191 | 192 | pts = translation((1, 0, 0)).transformAll(t.points) 193 | r1 = Workplane().polyhedron(pts, t.faces) 194 | 195 | mirrored_pts = mirror((1, 0, 0)).transformAll(pts) 196 | r2 = Workplane().polyhedron(mirrored_pts, t.faces) 197 | 198 | ![mirror](images/matrix_mirror.JPG) 199 | 200 | # `rotationX` 201 | 202 | Create a rotation matrix around the x-axis. 203 | 204 | ## Parameters 205 | 206 | - `angle`: angle degrees. 207 | 208 | ## Examples 209 | 210 | from cqmore.matrix import translation, rotationX 211 | from cqmore.polyhedron import tetrahedron 212 | from cqmore import Workplane 213 | 214 | t = tetrahedron(1) 215 | pts = translation((0, 3, 0)).transformAll(t.points) 216 | 217 | for a in range(0, 360, 30): 218 | rotated_pts = rotationX(a).transformAll(pts) 219 | show_object(Workplane().polyhedron(rotated_pts, t.faces)) 220 | 221 | ![rotationX](images/matrix_rotationX.JPG) 222 | 223 | # `rotationY` 224 | 225 | Create a rotation matrix around the y-axis. 226 | 227 | ## Parameters 228 | 229 | - `angle`: angle degrees. 230 | 231 | ## Examples 232 | 233 | from cqmore.matrix import translation, rotationY 234 | from cqmore.polyhedron import tetrahedron 235 | from cqmore import Workplane 236 | 237 | t = tetrahedron(1) 238 | 239 | for i in range(20): 240 | rotated_pts = (rotationY(i * 36) @ translation((3, i, 0))).transformAll(t.points) 241 | show_object(Workplane().polyhedron(rotated_pts, t.faces)) 242 | 243 | ![rotationY](images/matrix_rotationY.JPG) 244 | 245 | # `rotationZ` 246 | 247 | Create a rotation matrix around the z-axis. 248 | 249 | ## Parameters 250 | 251 | - `angle`: angle degrees. 252 | 253 | ## Examples 254 | 255 | from cqmore import Workplane 256 | from cqmore.matrix import translation, rotationX, rotationZ 257 | from cqmore.polyhedron import sweep 258 | 259 | def mobius_strip(radius, frags): 260 | profile = [(10, -1, 0), (10, 1, 0), (-10, 1, 0), (-10, -1, 0)] 261 | 262 | translationX20 = translation((radius, 0, 0)) 263 | rotationX90 = rotationX(90) 264 | 265 | angle_step = 360 / frags 266 | profiles = [] 267 | for i in range(frags): 268 | m = rotationZ(i * angle_step) @ translationX20 @ rotationX90 @ rotationZ(i * angle_step / 2) 269 | profiles.append(m.transformAll(profile)) 270 | 271 | return Workplane().polyhedron(*sweep(profiles, closeIdx = 2)) 272 | 273 | radius = 20 274 | frags = 24 275 | 276 | strip = mobius_strip(radius, frags) 277 | 278 | ![rotationZ](images/matrix_rotationZ.JPG) 279 | 280 | # `rotation` 281 | 282 | Create a rotation matrix around the given direction. 283 | 284 | ## Parameters 285 | 286 | - `direction`: axis of rotation. 287 | - `angle`: angle degrees. 288 | 289 | ## Examples 290 | 291 | from cqmore.matrix import rotation, translation, rotationY 292 | from cqmore.polyhedron import tetrahedron 293 | from cqmore import Workplane 294 | 295 | t = tetrahedron(1) 296 | pts = translation((6, 6, 3)).transformAll(t.points) 297 | direction = (10, 10, 10) 298 | 299 | for a in range(0, 360, 30): 300 | rotated_pts = rotation(direction, a).transformAll(pts) 301 | show_object(Workplane().polyhedron(rotated_pts, t.faces)) 302 | 303 | show_object( 304 | Workplane().polylineJoin( 305 | [(0, 0, 0), direction], 306 | Workplane().box(.1, .1, .1) 307 | ) 308 | ) 309 | 310 | ![rotation](images/matrix_rotation.JPG) -------------------------------------------------------------------------------- /docs/polygon.md: -------------------------------------------------------------------------------- 1 | # `cqmore.polygon` 2 | 3 | Provide functions for creating simple polygons. 4 | 5 | ## Functions 6 | 7 | Signature | Description 8 | --|-- 9 | [`taiwan(h[,distance])`](polygon.md#taiwan) | Create a convex hull through the provided points. 10 | [`regularPolygon(nSides,radius[,thetaStart,thetaEnd])`](polygon.md#regularPolygon) | Create a regular polygon. 11 | [`star([outerRadius,innerRadius,n])`](polygon.md#star) | Create a star. 12 | [`hull2D(points)`](polygon.md#hull2D) | Create a convex hull through the provided points. 13 | 14 | ---- 15 | 16 | # `taiwan` 17 | 18 | Create a Taiwan profile. 19 | 20 | ## Parameters 21 | 22 | - `h` : The height of Taiwan. 23 | - `distance = 0` : used for simplifying the shape. If the distance between a point and its previous points is not greater than distance, the point will be kept. 24 | 25 | ## Examples 26 | 27 | from cqmore import Workplane 28 | from cqmore.polygon import taiwan 29 | 30 | taiwan = (Workplane() 31 | .makePolygon(taiwan(20)) 32 | .extrude(1) 33 | ) 34 | 35 | ![taiwan](images/polygon_taiwan.JPG) 36 | 37 | # `regularPolygon` 38 | 39 | Create a regular polygon. 40 | 41 | ## Parameters 42 | 43 | - `nSides`: number of sides. 44 | - `radius`: the size of the polygon. 45 | - `thetaStart = 0`: start angle for the first segment. 46 | - `thetaEnd = 360`: end angle for the last segment. 47 | 48 | ## Examples 49 | 50 | from cqmore import Workplane 51 | from cqmore.polygon import regularPolygon 52 | 53 | polygon = (Workplane() 54 | .makePolygon(regularPolygon(nSides = 6, radius = 10)) 55 | .extrude(1) 56 | ) 57 | 58 | ![regularPolygon](images/polygon_regularPolygon.JPG) 59 | 60 | 61 | from cqmore import Workplane 62 | from cqmore.polygon import regularPolygon 63 | 64 | polygon = (Workplane() 65 | .makePolygon( 66 | regularPolygon( 67 | nSides = 6, 68 | radius = 10, 69 | thetaStart = 45, 70 | thetaEnd = 270 71 | ) 72 | ) 73 | .extrude(1) 74 | ) 75 | 76 | ![regularPolygon](images/polygon_regularPolygon2.JPG) 77 | 78 | # `star` 79 | 80 | Create a star. Default to a pentagram. 81 | 82 | ## Parameters 83 | 84 | - `outerRadius = 1`: the outer radius of the star. 85 | - `innerRadius = 0.381966`: the inner radius of the star. 86 | - `n = 5`: the burst number. 87 | 88 | ## Examples 89 | 90 | from cqmore import Workplane 91 | from cqmore.polygon import star 92 | 93 | polygon = (Workplane() 94 | .makePolygon( 95 | star(outerRadius = 10, innerRadius = 5, n = 8) 96 | ) 97 | .extrude(1) 98 | ) 99 | 100 | ![star](images/polygon_star.JPG) 101 | 102 | # `hull2D` 103 | 104 | Create a convex hull through the provided points. 105 | 106 | ## Parameters 107 | 108 | - `points`: a list of x, y points. 109 | 110 | ## Examples 111 | 112 | from random import random 113 | from cqmore import Workplane 114 | from cqmore.polygon import hull2D 115 | 116 | points = [(random(), random()) for i in range(20)] 117 | convex_hull = Workplane().makePolygon(hull2D(points)) 118 | 119 | ![hull2D](images/polygon_hull2D.JPG) 120 | 121 | -------------------------------------------------------------------------------- /docs/polyhedron.md: -------------------------------------------------------------------------------- 1 | # `cqmore.polyhedron` 2 | 3 | Provide the `Polyhedron` class and functions for creating `Polyhedron` instances. The `Polyhedron` class defines `points` and `faces` attributes. Here is a way to use them. 4 | 5 | from cqmore.polyhedron import uvSphere 6 | from cqmore import Workplane 7 | 8 | sphere = (Workplane() 9 | .polyhedron(*uvSphere(radius = 10, widthSegments = 10, heightSegments = 5)) 10 | ) 11 | 12 | ## Classes 13 | 14 | Signature | Description 15 | --|-- 16 | [`Polyhedron(points,faces)`](polyhedron.md#polyhedron) | Define `points` and `faces` attributes. 17 | 18 | ## Functions 19 | 20 | Signature | Description 21 | --|-- 22 | [`uvSphere(radius,[heightSegments])`](polyhedron.md#uvsphere) | Create a UV sphere. 23 | [`tetrahedron(radius,[detail])`](polyhedron.md#tetrahedron) | Create a tetrahedron. 24 | [`hexahedron(radius,[detail])`](polyhedron.md#hexahedron) | Create a hexahedron. 25 | [`octahedron(radius,[detail])`](polyhedron.md#octahedron) | Create a octahedron. 26 | [`dodecahedron(radius,[detail])`](polyhedron.md#dodecahedron) | Create a dodecahedron. 27 | [`icosahedron(radius,[detail])`](polyhedron.md#icosahedron) | Create a icosahedron. 28 | [`star([outerRadius,innerRadius,height,n])`](polyhedron.md#star) | Create a star. 29 | [`gridSurface(points[,thickness])`](polyhedron.md#gridSurface) | Create a surface with a coordinate meshgrid. 30 | [`superellipsoid(e,n)`](polyhedron.md#superellipsoid) | Create a superellipsoid. 31 | [`polarZonohedra(n[,theta])`](polyhedron.md#polarZonohedra) | (Preview) Create a polar zonohedra. 32 | [`hull(points)`](polyhedron.md#hull) | Create a convex hull through the provided points. 33 | [`sweep(profiles)`](polyhedron.md#sweep) | Create a swept polyhedron. 34 | 35 | ---- 36 | 37 | # `Polyhedron` 38 | 39 | Define a polyhedron. 40 | 41 | ## Parameters 42 | 43 | - `points`: points of vertices. 44 | - `faces`: face indices. 45 | 46 | ## Examples 47 | 48 | from cqmore.polyhedron import Polyhedron 49 | from cqmore import Workplane 50 | 51 | points = ( 52 | (5, -5, -5), (-5, 5, -5), (5, 5, 5), (-5, -5, 5) 53 | ) 54 | 55 | faces = ( 56 | (0, 1, 2), (0, 3, 1), (1, 3, 2), (0, 2, 3) 57 | ) 58 | 59 | tetrahedron = Polyhedron(points, faces) 60 | tetrahedrons = (Workplane() 61 | .rect(15, 15, forConstruction = True) 62 | .vertices() 63 | .polyhedron(*tetrahedron) 64 | ) 65 | 66 | ![Polyhedron](images/polyhedron_Polyhedron.JPG) 67 | 68 | # `uvSphere` 69 | 70 | Create a UV sphere. 71 | 72 | ## Parameters 73 | 74 | - `radius`: sphere radius. 75 | - `widthSegments = 3`: number of horizontal segments. 76 | - `heightSegments = 2`: number of vertical segments. 77 | 78 | ## Examples 79 | 80 | from cqmore.polyhedron import uvSphere 81 | from cqmore import Workplane 82 | 83 | sphere = (Workplane() 84 | .polyhedron(*uvSphere(radius = 10, widthSegments = 10, heightSegments = 5)) 85 | ) 86 | 87 | ![uvSphere](images/polyhedron_uvSphere.JPG) 88 | 89 | # `tetrahedron` 90 | 91 | Create a tetrahedron. 92 | 93 | ## Parameters 94 | 95 | - `radius`: radius of the tetrahedron. 96 | - `detail = 0`: setting this to a value greater than 0 adds vertices making it no longer a tetrahedron. 97 | 98 | ## Examples 99 | 100 | from cqmore.polyhedron import tetrahedron 101 | from cqmore import Workplane 102 | 103 | radius = 1 104 | polyhedra = Workplane() 105 | for detail in range(5): 106 | polyhedra.add( 107 | Workplane() 108 | .polyhedron(*tetrahedron(radius, detail)) 109 | .translate((2 * radius * detail, 0, 0)) 110 | ) 111 | 112 | ![tetrahedron](images/polyhedron_tetrahedron.JPG) 113 | 114 | # `hexahedron` 115 | 116 | Create a hexahedron. 117 | 118 | ## Parameters 119 | 120 | - `radius`: radius of the hexahedron. 121 | - `detail = 0`: setting this to a value greater than 0 adds vertices making it no longer a hexahedron. 122 | 123 | ## Examples 124 | 125 | from cqmore.polyhedron import hexahedron 126 | from cqmore import Workplane 127 | 128 | radius = 1 129 | polyhedra = Workplane() 130 | for detail in range(5): 131 | polyhedra.add( 132 | Workplane() 133 | .polyhedron(*hexahedron(radius, detail)) 134 | .translate((2 * radius * detail, 0, 0)) 135 | ) 136 | 137 | ![hexahedron](images/polyhedron_hexahedron.JPG) 138 | 139 | # `octahedron` 140 | 141 | Create a octahedron. 142 | 143 | ## Parameters 144 | 145 | - `radius`: radius of the octahedron. 146 | - `detail = 0`: setting this to a value greater than 0 adds vertices making it no longer a octahedron. 147 | 148 | ## Examples 149 | 150 | from cqmore.polyhedron import octahedron 151 | from cqmore import Workplane 152 | 153 | radius = 1 154 | polyhedra = Workplane() 155 | for detail in range(5): 156 | polyhedra.add( 157 | Workplane() 158 | .polyhedron(*octahedron(radius, detail)) 159 | .translate((2 * radius * detail, 0, 0)) 160 | ) 161 | 162 | ![octahedron](images/polyhedron_octahedron.JPG) 163 | 164 | # `dodecahedron` 165 | 166 | Create a dodecahedron. 167 | 168 | ## Parameters 169 | 170 | - `radius`: radius of the dodecahedron. 171 | - `detail = 0`: setting this to a value greater than 0 adds vertices making it no longer a dodecahedron. 172 | 173 | ## Examples 174 | 175 | from cqmore.polyhedron import dodecahedron 176 | from cqmore import Workplane 177 | 178 | radius = 1 179 | polyhedra = Workplane() 180 | for detail in range(5): 181 | polyhedra.add( 182 | Workplane() 183 | .polyhedron(*dodecahedron(radius, detail)) 184 | .translate((2 * radius * detail, 0, 0)) 185 | ) 186 | 187 | ![dodecahedron](images/polyhedron_dodecahedron.JPG) 188 | 189 | # `icosahedron` 190 | 191 | Create a icosahedron. 192 | 193 | ## Parameters 194 | 195 | - `radius`: radius of the icosahedron. 196 | - `detail = 0`: setting this to a value greater than 0 adds vertices making it no longer a icosahedron. 197 | 198 | ## Examples 199 | 200 | from cqmore.polyhedron import icosahedron 201 | from cqmore import Workplane 202 | 203 | radius = 1 204 | polyhedra = Workplane() 205 | for detail in range(5): 206 | polyhedra.add( 207 | Workplane() 208 | .polyhedron(*icosahedron(radius, detail)) 209 | .translate((2 * radius * detail, 0, 0)) 210 | ) 211 | 212 | ![icosahedron](images/polyhedron_icosahedron.JPG) 213 | 214 | # `star` 215 | 216 | Create a star. Default to a pentagram. 217 | 218 | ## Parameters 219 | 220 | - `outerRadius = 1`: the outer radius of the star. 221 | - `innerRadius = 0.381966`: the inner radius of the star. 222 | - `height = 0.5`: the star height. 223 | - `n = 5`: the burst number. 224 | 225 | ## Examples 226 | 227 | from cqmore import Workplane 228 | from cqmore.polyhedron import star 229 | 230 | polyhedron = Workplane().polyhedron(*star()) 231 | 232 | ![star](images/polyhedron_star.JPG) 233 | 234 | # `gridSurface` 235 | 236 | Create a surface with a coordinate meshgrid. 237 | 238 | ## Parameters 239 | 240 | - `points`: a coordinate meshgrid. 241 | - `thickness = 0`: the amount of being thick (return 2D surface if 0). 242 | 243 | ## Examples 244 | 245 | from math import sqrt, cos, radians 246 | from cqmore import Workplane 247 | from cqmore.polyhedron import gridSurface 248 | 249 | def ripple(x, y): 250 | n = radians(sqrt(x ** 2 + y ** 2)) 251 | return (x, y, 30 * (cos(n) + cos(3 * n))) 252 | 253 | min_value = -200 254 | max_value = 200 255 | step = 10 256 | thickness = 5 257 | 258 | points = [[ 259 | ripple(x, y) 260 | for y in range(min_value, max_value, step) 261 | ] for x in range(min_value, max_value, step)] 262 | 263 | sf = Workplane().polyhedron(*gridSurface(points, thickness)) 264 | 265 | ![gridSurface](images/polyhedron_gridSurface.JPG) 266 | 267 | # `superellipsoid` 268 | 269 | Create a [superellipsoid](https://en.wikipedia.org/wiki/Superellipsoid). 270 | 271 | ## Parameters 272 | 273 | - `e`: the east-west parameter. 274 | - `n`: the north-south parameter. 275 | - `widthSegments = 3`: number of horizontal segments. 276 | - `heightSegments = 2`: number of vertical segments. 277 | 278 | ## Examples 279 | 280 | from cqmore import Workplane 281 | from cqmore.polyhedron import superellipsoid 282 | 283 | r = Workplane().polyhedron(*superellipsoid(2.5, .25, widthSegments = 24, heightSegments = 12)) 284 | 285 | ![superellipsoid](images/polyhedron_superellipsoid.JPG) 286 | 287 | # `polarZonohedra` 288 | 289 | Create a [polar zonohedra](https://mathworld.wolfram.com/PolarZonohedron.html). 290 | 291 | ## Parameters 292 | 293 | - `n`: n equal rhombs surrounding one vertex. (rotational symmetry). 294 | - `theta = 35.5`: the pitch angle of the edges. 295 | 296 | ## Examples 297 | 298 | from cqmore.polyhedron import polarZonohedra 299 | from cqmore import Workplane 300 | 301 | pz = Workplane().polyhedron(*polarZonohedra(8, 45)) 302 | 303 | ![polarZonohedra](images/polyhedron_polarZonohedra.JPG) 304 | 305 | # `hull` 306 | 307 | Create a convex hull through the provided points. 308 | 309 | ## Parameters 310 | 311 | - `points`: a list of 3D points. 312 | 313 | ## Examples 314 | 315 | from cqmore import Workplane 316 | from cqmore.polyhedron import hull 317 | 318 | points = ( 319 | (50, 50, 50), 320 | (50, 50, 0), 321 | (-50, 50, 0), 322 | (-50, -50, 0), 323 | (50, -50, 0), 324 | (0, 0, 50), 325 | (0, 0, -50) 326 | ) 327 | 328 | convex_hull = Workplane().polyhedron(*hull(points)) 329 | 330 | ![hull](images/polyhedron_hull.JPG) 331 | 332 | # `sweep` 333 | 334 | Create a swept polyhedron. 335 | 336 | ## Parameters 337 | 338 | - `profiles`: list of profiles. 339 | - `closeIdx`: setting it to a value >= 0 creates faces between the first and last profile. The value decides which index of the last profile is connected to the first index of the first profile. 340 | 341 | ## Examples 342 | 343 | from cqmore import Workplane 344 | from cqmore.polyhedron import sweep 345 | 346 | profiles = [ 347 | [(10, 0, 0), (10, 0, 10), (20, 0, 10), (20, 0, 0)], 348 | [(0, 10, 0), (0, 10, 10), (0, 20, 10), (0, 20, 0)], 349 | [(-10, 0, 0), (-10, 0, 10), (-20, 0, 10), (-20, 0, 0)], 350 | [(0, -10, 0), (0, -10, 10), (0, -20, 10), (0, -20, 0)] 351 | ] 352 | 353 | r = Workplane().polyhedron(*sweep(profiles)) 354 | 355 | ![sweep](images/polyhedron_sweep.JPG) 356 | 357 | from cqmore import Workplane 358 | from cqmore.polyhedron import sweep 359 | 360 | profiles = [ 361 | [(10, 0, 0), (10, 0, 10), (20, 0, 10), (20, 0, 0)], 362 | [(0, 10, 0), (0, 10, 10), (0, 20, 10), (0, 20, 0)], 363 | [(-10, 0, 0), (-10, 0, 10), (-20, 0, 10), (-20, 0, 0)], 364 | [(0, -10, 0), (0, -10, 10), (0, -20, 10), (0, -20, 0)] 365 | ] 366 | 367 | r = Workplane().polyhedron(*sweep(profiles, closeIdx = 0)) 368 | 369 | ![sweep](images/polyhedron_sweep2.JPG) -------------------------------------------------------------------------------- /docs/workplane.md: -------------------------------------------------------------------------------- 1 | # `cqmore.Workplane` 2 | 3 | Define plugins. You may simply use `cqmore.Workplane` to replace `cadquery.Workplane`. For example: 4 | 5 | from cqmore import Workplane 6 | 7 | result = (Workplane() 8 | .rect(10, 10) 9 | .makePolygon(((-2, -2), (2, -2), (2, 2), (-2, 2))) 10 | .extrude(1) 11 | ) 12 | 13 | You may also attach methods of `cqmore.Workplane` to `cadquery.Workplane`, such as: 14 | 15 | from cadquery import Workplane 16 | import cqmore 17 | cqmore.extend(Workplane) 18 | 19 | result = (Workplane() 20 | .rect(10, 10) 21 | .makePolygon(((-2, -2), (2, -2), (2, 2), (-2, 2))) 22 | .extrude(1) 23 | ) 24 | 25 | ## 2D Operations 26 | 27 | Signature | Description 28 | --|-- 29 | [`makePolygon(points[,forConstruction])`](workplane.md#makepolygon) | Make a multiple sided wire through the provided points. 30 | [`intersect2D(toIntersect)`](workplane.md#intersect2d) | Intersect the provided wire from the current wire. 31 | [`union2D(toUnion)`](workplane.md#union2d) | Union the provided wire from the current wire. 32 | [`cut2D(toCut)`](workplane.md#cut2d) | Cut the provided wire from the current wire. 33 | [`hull2D([points,forConstruction])`](workplane.md#hull2d) | Create a convex hull through the provided points. 34 | [`polylineJoin2D(points,join[,forConstruction])`](workplane.md#polylineJoin2D) | Place a join on each point. Hull each pair of joins and union all convex hulls. 35 | 36 | ## 3D Operations 37 | 38 | Signature | Description 39 | --|-- 40 | [`splineApproxSurface(points[,thickness,combine,clean])`](workplane.md#splineApproxSurface) | Approximate a spline surface through the provided points. 41 | [`polyhedron(points,faces[,combine,clean])`](workplane.md#polyhedron) | Create any polyhedron through 3D points(vertices) and faces that enclose the solid. 42 | [`hull([points,combine,clean])`](workplane.md#hull) | Create a convex hull through the provided points. 43 | [`polylineJoin(points,join[,combine,clean])`](workplane.md#polylineJoin) | Place a join on each point. Hull each pair of joins and union all convex hulls. 44 | 45 | ---- 46 | 47 | # `makePolygon` 48 | 49 | Make a multiple sided wire through the provided points. 50 | 51 | ## Parameters 52 | 53 | - `points`: a list of x,y points make up the polygon. 54 | - `forConstruction = False`: should the new wires be reference geometry only? 55 | 56 | ## Examples 57 | 58 | from cqmore import Workplane 59 | 60 | triangle = Workplane().makePolygon(((-2, -2), (2, -2), (0, 2))) 61 | 62 | ![makePolygon](images/workplane_makePolygon.JPG) 63 | 64 | # `intersect2D` 65 | 66 | Intersect the provided wire from the current wire. 67 | 68 | ## Parameters 69 | 70 | - `toIntersect`: a wire object, or a CQ object having a wire – object to intersect. 71 | 72 | ## Examples 73 | 74 | from cqmore import Workplane 75 | 76 | r1 = Workplane('YZ').rect(10, 10) 77 | r2 = Workplane('YZ').center(5, 5).rect(10, 10) 78 | intersected = r1.intersect2D(r2).extrude(1) 79 | 80 | ![intersect2D](images/workplane_intersect2D.JPG) 81 | 82 | # `union2D` 83 | 84 | Union the provided wire from the current wire. 85 | 86 | ## Parameters 87 | 88 | - `toUnion`: a wire object, or a CQ object having a wire – object to union. 89 | 90 | ## Examples 91 | 92 | from cqmore import Workplane 93 | 94 | r1 = Workplane('YZ').rect(10, 10) 95 | r2 = Workplane('YZ').center(5, 5).rect(10, 10) 96 | unioned = r1.union2D(r2).extrude(1) 97 | 98 | ![union2D](images/workplane_union2D.JPG) 99 | 100 | # `cut2D` 101 | 102 | Cut the provided wire from the current wire. 103 | 104 | ## Parameters 105 | 106 | - `toCut`: a wire object, or a CQ object having a wire – object to cut. 107 | 108 | ## Examples 109 | 110 | from cqmore import Workplane 111 | 112 | r1 = Workplane('YZ').rect(10, 10) 113 | r2 = Workplane('YZ').center(5, 5).rect(10, 10) 114 | cutted = r1.cut2D(r2).extrude(1) 115 | 116 | ![cut2D](images/workplane_cut2D.JPG) 117 | 118 | # `hull2D` 119 | 120 | Create a convex hull through the provided points. 121 | 122 | ## Parameters 123 | 124 | - `points = None`: a list of x, y points. If it's `None`, use all pending wires in the parent chain to create a convex hull. 125 | - `forConstruction = False`: should the new wires be reference geometry only? 126 | 127 | ## Examples 128 | 129 | from random import random 130 | from cqmore import Workplane 131 | 132 | points = [(random(), random()) for i in range(20)] 133 | convex_hull = Workplane().hull2D(points) 134 | 135 | # an equivalent way 136 | # convex_hull = Workplane().polyline(points).close().hull2D() 137 | 138 | ![hull2D](images/workplane_hull2D.JPG) 139 | 140 | # `polylineJoin2D` 141 | 142 | Place a join on each point. Hull each pair of joins and union all convex hulls. 143 | 144 | ## Parameters 145 | 146 | - `points`: a list of x, y points. 147 | - `join`: the wire as a join. 148 | - `forConstruction = False`: should the new wires be reference geometry only? 149 | 150 | ## Examples 151 | 152 | from cqmore import Workplane 153 | 154 | points = [(0, 0), (10, 10), (0, 15), (-10, 10), (-10, 0)] 155 | polyline = Workplane().polylineJoin2D(points, Workplane().polygon(6, 1)) 156 | 157 | ![polylineJoin2D](images/workplane_polylineJoin2D.JPG) 158 | 159 | # `splineApproxSurface` 160 | 161 | Approximate a spline surface through the provided points. 162 | 163 | ## Parameters 164 | 165 | - `points`: a 2D list of Vectors that represent the control points. 166 | - `thickness = 0`: the amount of being thick (return 2D surface if 0). 167 | - `combine = False`: should the results be combined with other solids on the stack (and each other)? 168 | - `clean = True`: call `clean()` afterwards to have a clean shape. 169 | 170 | ## Examples 171 | 172 | from cqmore import Workplane 173 | 174 | def paraboloid(x, y): 175 | return (x, y, ((y ** 2) - (x ** 2)) / 4) 176 | 177 | min_value = -30 178 | max_value = 30 179 | step = 5 180 | thickness = 0.5 181 | 182 | points = [[ 183 | paraboloid(x / 10, y / 10) 184 | for y in range(min_value, max_value + step, step) 185 | ] for x in range(min_value, max_value + step, step)] 186 | 187 | surface = Workplane().splineApproxSurface(points, thickness) 188 | 189 | ![splineApproxSurface](images/workplane_splineApproxSurface.JPG) 190 | 191 | # `polyhedron` 192 | 193 | Create any polyhedron with 3D points(vertices) and faces that enclose the solid. Each face contains the indices (0 based) of 3 or more points from the `points`. 194 | 195 | ## Parameters 196 | 197 | - `points`: a list of 3D points(vertices). 198 | - `faces`: face indices to fully enclose the solid. When looking at any face from the outside, the face must list all points in a counter-clockwise order. 199 | - `combine = True`: should the results be combined with other solids on the stack (and each other)? 200 | - `clean = True`: call `clean()` afterwards to have a clean shape. 201 | 202 | ## Examples 203 | 204 | from cqmore import Workplane 205 | 206 | points = ((5, -5, -5), (-5, 5, -5), (5, 5, 5), (-5, -5, 5)) 207 | faces = ((0, 1, 2), (0, 3, 1), (1, 3, 2), (0, 2, 3)) 208 | tetrahedron = Workplane().polyhedron(points, faces) 209 | 210 | ![polyhedron](images/workplane_polyhedron.JPG) 211 | 212 | # `hull` 213 | 214 | Create a convex hull through the provided points. 215 | 216 | ## Parameters 217 | 218 | - `points = None`: a list of 3D points. If it's `None`, attempt to hull all of the items on the stack to create a convex hull. 219 | - `combine = True`: should the results be combined with other solids on the stack (and each other)? 220 | - `clean = True`: call `clean()` afterwards to have a clean shape. 221 | 222 | ## Examples 223 | 224 | from cqmore import Workplane 225 | 226 | points = ( 227 | (50, 50, 50), 228 | (50, 50, 0), 229 | (-50, 50, 0), 230 | (-50, -50, 0), 231 | (50, -50, 0), 232 | (0, 0, 50), 233 | (0, 0, -50) 234 | ) 235 | 236 | convex_hull = Workplane().hull(points) 237 | 238 | ![hull](images/workplane_hull.JPG) 239 | 240 | 241 | from cqmore import Workplane 242 | from cqmore.polyhedron import uvSphere 243 | 244 | convex_hull = (Workplane() 245 | .polyhedron(*uvSphere(10)) 246 | .box(20, 20, 5) 247 | .hull() 248 | ) 249 | 250 | ![hull](images/workplane_hull2.JPG) 251 | 252 | # `polylineJoin` 253 | 254 | Place a join on each point. Hull each pair of joins and union all convex hulls. 255 | 256 | ## Parameters 257 | 258 | - `points`: a list of points. 259 | - `join`: the sold as a join. 260 | - `combine = True`: should the results be combined with other solids on the stack (and each other)? 261 | - `clean = True`: call `clean()` afterwards to have a clean shape. 262 | 263 | ## Examples 264 | 265 | from cqmore import Workplane 266 | 267 | polyline = (Workplane() 268 | .polylineJoin( 269 | [(0, 0, 0), (10, 0, 0), (10, 0, 10), (10, 10, 10)], 270 | Workplane().box(1, 1, 1) 271 | ) 272 | ) 273 | 274 | ![polylineJoin](images/workplane_polylineJoin.JPG) -------------------------------------------------------------------------------- /examples/gyroid.py: -------------------------------------------------------------------------------- 1 | # scikit-image 0.18 or later is required. 2 | 3 | import numpy as np 4 | from skimage import measure 5 | from math import sin, cos, radians 6 | from cqmore import Workplane 7 | 8 | def gyroid(length, width, height, thickness, step): 9 | def _gyroid(x, y, z, thickness, length, width, height): 10 | # is boundary? 11 | if x == 0 or y == 0 or z == 0 or x == length or y == width or z == height: 12 | return 0 13 | 14 | half_thickness = thickness / 2 15 | 16 | rx = radians(x) 17 | ry = radians(y) 18 | rz = radians(z) 19 | v = sin(rx) * cos(ry) + sin(ry) * cos(rz) + sin(rz) * cos(rx) 20 | return 1 if -half_thickness <= v <= half_thickness else 0 21 | vectorized_gyroid = np.frompyfunc(_gyroid, 7, 1) 22 | 23 | 24 | l_arange = np.arange(0, length + step, step) 25 | w_arange = np.arange(0, width + step, step) 26 | h_arange = np.arange(0, height + step, step) 27 | x, y, z = np.meshgrid(l_arange, w_arange, h_arange) 28 | 29 | points, faces, _, __ = measure.marching_cubes( # type: ignore 30 | vectorized_gyroid(x, y, z, thickness, length, width, height), 31 | 0, 32 | spacing = (radians(step),) * 3, 33 | allow_degenerate = False 34 | ) 35 | 36 | return Workplane().polyhedron(points, faces) 37 | 38 | 39 | if __name__ == '__main__': 40 | length = 360 41 | width = 360 42 | height = 360 43 | thickness = 0.4 44 | step = 10 45 | 46 | g = gyroid(length, width, height, thickness, step) 47 | 48 | # from cadquery import exporters 49 | # exporters.export(g, 'gyroid.stl') -------------------------------------------------------------------------------- /examples/gyroid_sphere.py: -------------------------------------------------------------------------------- 1 | # scikit-image 0.18 or later is required. 2 | 3 | from math import radians 4 | from gyroid import gyroid 5 | from cqmore import Workplane 6 | from cqmore.polyhedron import uvSphere 7 | 8 | def gyroid_sphere(thickness, period, step): 9 | width = 360 + step * 5 10 | 11 | g = gyroid(width, width, width, thickness, step) 12 | 13 | r = radians(180 * period) 14 | 15 | if period == 1: 16 | return Workplane().polyhedron(*uvSphere(r, 48, 24)).translate((r, r, r)).intersect(g) 17 | 18 | offset = radians(360) 19 | rg = range(period) 20 | 21 | row = Workplane() 22 | for i in rg: 23 | row.add(g.translate((offset * i, 0, 0))) 24 | row = row.combine() 25 | 26 | rect = Workplane() 27 | for i in rg: 28 | rect.add(row.translate((0, offset * i, 0))) 29 | rect = rect.combine() 30 | 31 | cube = Workplane() 32 | for i in rg: 33 | cube.add(rect.translate((0, 0, offset * i))) 34 | cube = cube.combine() 35 | 36 | return Workplane().polyhedron(*uvSphere(r, 48, 24)).translate((r, r, r)).intersect(cube) 37 | 38 | 39 | thickness = 0.4 40 | period = 2 41 | step = 10 42 | g = gyroid_sphere(thickness, period, step) 43 | 44 | # from cadquery import exporters 45 | # exporters.export(g, 'gyroid_sphere.stl') -------------------------------------------------------------------------------- /examples/import_stl.py: -------------------------------------------------------------------------------- 1 | # numpy-stl 2.16 or later is required. 2 | from stl.mesh import Mesh 3 | from cqmore import Workplane 4 | 5 | def import_stl(fileName): 6 | vectors = Mesh.from_file(fileName).vectors 7 | points = tuple(map(tuple, vectors.reshape((vectors.shape[0] * vectors.shape[1], 3)))) 8 | faces = [(i, i + 1, i + 2) for i in range(0, len(points), 3)] 9 | return Workplane().polyhedron(points, faces) 10 | 11 | stl = import_stl('yourfile.stl') -------------------------------------------------------------------------------- /examples/mobius_strip.py: -------------------------------------------------------------------------------- 1 | from cqmore import Workplane 2 | from cqmore.matrix import translation, rotationX, rotationZ 3 | from cqmore.polyhedron import sweep 4 | 5 | def mobius_strip(radius, frags): 6 | profile = [(10, -1, 0), (10, 1, 0), (-10, 1, 0), (-10, -1, 0)] 7 | 8 | translationX20 = translation((radius, 0, 0)) 9 | rotationX90 = rotationX(90) 10 | 11 | angle_step = 360 / frags 12 | profiles = [] 13 | for i in range(frags): 14 | m = rotationZ(i * angle_step) @ translationX20 @ rotationX90 @ rotationZ(i * angle_step / 2) 15 | profiles.append(m.transformAll(profile)) 16 | 17 | return Workplane().polyhedron(*sweep(profiles, closeIdx = 2)) 18 | 19 | radius = 20 20 | frags = 24 21 | 22 | strip = mobius_strip(radius, frags) -------------------------------------------------------------------------------- /examples/platonic_dice.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | 3 | from cadquery import Face 4 | from cqmore import Workplane 5 | from cqmore.polyhedron import tetrahedron, hexahedron, octahedron, dodecahedron, icosahedron 6 | 7 | number_of_faces = 12 # 4, 6, 8, 12 or 20 8 | radius = 10 9 | font_name = 'Arial Black' 10 | font_size = 5 11 | font_distance = 1 12 | detail = 0 13 | 14 | platonic_polyhedra = { 15 | 4: tetrahedron, 16 | 6: hexahedron, 17 | 8: octahedron, 18 | 12: dodecahedron, 19 | 20: icosahedron 20 | } 21 | 22 | dice = (Workplane() 23 | .polyhedron( 24 | *platonic_polyhedra[number_of_faces](radius, detail) 25 | ) 26 | ) 27 | 28 | faces = dice.faces().vals() 29 | nums = len(faces) 30 | texts = Workplane() 31 | for i in range(nums): 32 | texts.add( 33 | Workplane(faces[i]) 34 | .workplane(origin = cast(Face, faces[i]).Center()) 35 | .text( 36 | str(nums - i), 37 | font_size, 38 | -font_distance, 39 | font = font_name 40 | ) 41 | ) 42 | 43 | dice = dice.cut(texts) 44 | 45 | show_object(dice) # type: ignore -------------------------------------------------------------------------------- /examples/platonic_solid_frame.py: -------------------------------------------------------------------------------- 1 | from cqmore import Workplane 2 | from cqmore.polyhedron import ( 3 | tetrahedron, 4 | hexahedron, 5 | octahedron, 6 | dodecahedron, 7 | icosahedron 8 | ) 9 | 10 | radius = 15 11 | thickness = 1.25 12 | 13 | polyhedra = [ 14 | tetrahedron, 15 | hexahedron, 16 | octahedron, 17 | dodecahedron, 18 | icosahedron 19 | ] 20 | 21 | for i in range(5): 22 | polyhedron = (Workplane() 23 | .polyhedron(*(polyhedra[i](radius))) 24 | .faces() 25 | ) 26 | 27 | r = polyhedron.item(0).shell(-thickness) 28 | for j in range(1, polyhedron.size()): 29 | r = r.intersect(polyhedron.item(j).shell(-thickness)) 30 | 31 | show_object(r.translate((radius * i * 2, 0, 0))) # type: ignore -------------------------------------------------------------------------------- /examples/reaction-diffusion.py: -------------------------------------------------------------------------------- 1 | # scikit-image 0.18 or later is required. 2 | 3 | from logging.config import valid_ident 4 | import numpy as np 5 | from skimage import measure 6 | from cqmore import Workplane 7 | from cadquery import exporters 8 | from cqmore.polyhedron import gridSurface 9 | 10 | 11 | def gray_scott(feel, kill, generation, space_size = 200, init_size = 20, init_u = 0.5, init_v = 0.25, Du = 2e-5, Dv = 1e-5, dx = 0.01, dt = 1): 12 | def laplacian(u): 13 | return (np.roll(u, 1, axis=0) + np.roll(u, -1, axis=0) + 14 | np.roll(u, 1, axis=1) + np.roll(u, -1, axis=1) - 4 * u) / (dx * dx) 15 | 16 | u = np.ones((space_size, space_size)) 17 | v = np.zeros((space_size, space_size)) 18 | 19 | half_space_size = space_size // 2 20 | half_init_size = init_size // 2 21 | square_low = half_space_size - half_init_size 22 | square_high = half_space_size + half_init_size 23 | 24 | u[square_low:square_high, square_low:square_high] = init_u 25 | v[square_low:square_high, square_low:square_high] = init_v 26 | 27 | u += np.random.rand(space_size, space_size) * 0.1 28 | v += np.random.rand(space_size, space_size) * 0.1 29 | 30 | for _ in range(generation): 31 | reaction = u * v * v 32 | dudt = Du * laplacian(u) - reaction + feel * (1 - u) 33 | dvdt = Dv * laplacian(v) + reaction - (feel + kill) * v 34 | u += dt * dudt 35 | v += dt * dvdt 36 | 37 | return u 38 | 39 | 40 | def surface(u, amplitude = 1, thickness = 1): 41 | v = [[(float(x), float(y), float(d) * amplitude) for x, d in enumerate(r)] for y, r in enumerate(u)] 42 | return Workplane().polyhedron(*gridSurface(v, thickness)) 43 | 44 | 45 | def contours(u, space_size, density_threshold = .5, layer_h = 2, line_w = 2): 46 | all = Workplane() 47 | for contour in measure.find_contours(u, density_threshold): 48 | xs = contour[:, 1] 49 | ys = contour[:, 0] 50 | coords = [tuple(coord) for coord in np.dstack([xs, ys])[0]] 51 | scope = Workplane().polyline(coords).close() 52 | offset = Workplane().polyline(coords).close().offset2D(-line_w) 53 | all.add(scope.cut2D(offset).extrude(layer_h * 2)) 54 | 55 | all = all.combine() 56 | return (Workplane().rect(space_size, space_size).extrude(layer_h) 57 | .translate((space_size / 2, space_size / 2, -layer_h / 2)) 58 | .cut(all)) 59 | 60 | 61 | feel, kill = 0.04, 0.06 # amorphous 62 | # feel, kill = 0.035, 0.065 # spots 63 | # feel, kill = 0.012, 0.05 # wandering bubbles 64 | # feel, kill = 0.025, 0.05 # waves 65 | generation = 1000 66 | space_size = 150 67 | 68 | u = gray_scott(feel, kill, generation, space_size) 69 | r = contours(u, space_size) 70 | # r = surface(u, amplitude = 15) 71 | 72 | # exporters.export(r, 'reaction-diffusion.stl') -------------------------------------------------------------------------------- /examples/rounded_star.py: -------------------------------------------------------------------------------- 1 | from cqmore import Workplane 2 | from cqmore.polygon import star 3 | from cadquery import Edge, Vector, Wire 4 | 5 | def roundedStar(outerRadius: float = 1, innerRadius: float = 0.381966, n: int = 5, rounded: float = 0.5) -> Wire: 6 | vts = [Vector(*p) for p in star(outerRadius, innerRadius, n)] 7 | ts = rounded / outerRadius * 3 # tangent scale 8 | spokes = [1, innerRadius / outerRadius] 9 | tangents = [ 10 | Vector(-ts * vts[i].y / spokes[i % 2], ts * vts[i].x / spokes[i % 2], 0) 11 | for i in range(len(vts)) 12 | ] 13 | 14 | return Wire.assembleEdges( 15 | [Edge.makeSpline(listOfVector = vts, tangents = tangents, periodic = True, scale = False)] 16 | ) 17 | 18 | outerRadius = 1 19 | innerRadius = 0.381966 20 | n = 5 21 | rounded = 0.5 22 | 23 | w = (Workplane() 24 | .add(roundedStar(outerRadius, innerRadius, n, rounded)) 25 | ) -------------------------------------------------------------------------------- /examples/superellipsoids.py: -------------------------------------------------------------------------------- 1 | from cqmore import Workplane 2 | from cqmore.polyhedron import superellipsoid 3 | 4 | step = 0.5 5 | cols = 8 6 | rows = 6 7 | 8 | solids = Workplane() 9 | for ei in range(1, cols): 10 | e = ei * step 11 | for ni in range(1, rows): 12 | n = ni * step 13 | solids.add( 14 | Workplane() 15 | .polyhedron(*superellipsoid(e, n, widthSegments = 24, heightSegments = 12)) 16 | .translate((ei * 2.5, ni * 2.5, 0)) 17 | ) 18 | -------------------------------------------------------------------------------- /examples/torus_knot.py: -------------------------------------------------------------------------------- 1 | from cqmore import Workplane 2 | from cqmore.curve import torusKnot, parametricEquation 3 | from cqmore.polygon import star 4 | 5 | from cadquery import Plane, Vector 6 | 7 | def torus_knot(p, q): 8 | origin = torusKnot(0, p = p, q = q) 9 | v1 = Vector(*torusKnot(0.9, p = p, q = q)) 10 | v2 = Vector(*torusKnot(0.1, p = p, q = q)) 11 | 12 | return (Workplane(Plane(origin = origin, normal=(v2 - v1))) 13 | .makePolygon([(p[0] * 0.5, p[1] * 0.5) for p in star()]) 14 | .sweep( 15 | Workplane().parametricCurve( 16 | parametricEquation(torusKnot, p = p, q = q) 17 | ), 18 | auxSpine = Workplane().rect(1, 1) 19 | ) 20 | ) 21 | p = 3 22 | q = 2 23 | 24 | knot = torus_knot(p, q) -------------------------------------------------------------------------------- /examples/turtle_tree.py: -------------------------------------------------------------------------------- 1 | from cqmore.matrix import rotation, translation 2 | from cqmore import Workplane 3 | from cqmore.polyhedron import tetrahedron 4 | 5 | class Turtle: 6 | def __init__(self, pos = (0, 0, 0)): 7 | self.coordinateVt = pos 8 | self.xAxis = (1, 0, 0) 9 | self.yAxis = (0, 1, 0) 10 | self.zAxis = (0, 0, 1) 11 | 12 | 13 | def forward(self, leng): 14 | m = translation(tuple(elem * leng for elem in self.xAxis)) # type: ignore 15 | self.coordinateVt = m.transform(self.coordinateVt) 16 | return self 17 | 18 | 19 | def roll(self, angle): 20 | xr = rotation(self.xAxis, angle) 21 | self.yAxis = xr.transform(self.yAxis) 22 | self.zAxis = xr.transform(self.zAxis) 23 | return self 24 | 25 | 26 | def pitch(self, angle): 27 | yr = rotation(self.yAxis, -angle) 28 | self.xAxis = yr.transform(self.xAxis) 29 | self.zAxis = yr.transform(self.zAxis) 30 | return self 31 | 32 | 33 | def turn(self, angle): 34 | zr = rotation(self.zAxis, angle) 35 | self.xAxis = zr.transform(self.xAxis) 36 | self.yAxis = zr.transform(self.yAxis) 37 | return self 38 | 39 | 40 | def pos(self): 41 | return self.coordinateVt 42 | 43 | 44 | def copy(self): 45 | t = Turtle() 46 | t.coordinateVt = self.coordinateVt 47 | t.xAxis = self.xAxis 48 | t.yAxis = self.yAxis 49 | t.zAxis = self.zAxis 50 | return t 51 | 52 | def turtle_tree(leng, leng_scale1, leng_scale2, limit, turnAngle, rollAngle, line_diameter): 53 | _LINE_WORKPLANE = Workplane() 54 | _LINE_JOIN = _LINE_WORKPLANE.polyhedron(*tetrahedron(line_diameter / 2)) 55 | def line(p1, p2): 56 | return _LINE_WORKPLANE.polylineJoin([p1, p2], _LINE_JOIN) 57 | 58 | def _turtle_tree(workplane, turtle, leng, leng_scale1, leng_scale2, limit, turnAngle, rollAngle): 59 | if leng > limit: 60 | workplane = workplane.add( 61 | line(turtle.pos(), turtle.forward(leng).pos()) 62 | ) 63 | 64 | workplane = _turtle_tree( 65 | workplane, 66 | turtle.copy().turn(turnAngle), 67 | leng * leng_scale1, 68 | leng_scale1, 69 | leng_scale2, 70 | limit, 71 | turnAngle, 72 | rollAngle 73 | ) 74 | 75 | return _turtle_tree( 76 | workplane, 77 | turtle.copy().roll(rollAngle), 78 | leng * leng_scale2, 79 | leng_scale1, 80 | leng_scale2, 81 | limit, 82 | turnAngle, 83 | rollAngle 84 | ) 85 | 86 | return workplane 87 | 88 | return _turtle_tree( 89 | Workplane(), 90 | Turtle(), 91 | leng, 92 | leng_scale1, 93 | leng_scale2, 94 | limit, 95 | turnAngle, 96 | rollAngle 97 | ).combine() 98 | 99 | 100 | leng = 20 101 | limit = 1 102 | leng_scale1 = 0.4 103 | leng_scale2 = 0.9 104 | turnAngle = 60 105 | rollAngle = 135 106 | line_diameter = 4 107 | 108 | 109 | tree = turtle_tree( 110 | leng, 111 | leng_scale1, 112 | leng_scale2, 113 | limit, 114 | turnAngle, 115 | rollAngle, 116 | line_diameter 117 | ) -------------------------------------------------------------------------------- /examples/twisted_strip.py: -------------------------------------------------------------------------------- 1 | 2 | from math import sin, cos, radians 3 | from cqmore import Workplane 4 | 5 | u_step = 10 6 | v_step = 0.2 7 | thickness = 0.1 8 | 9 | def twisted_strip(u_step, v_step, thickness): 10 | points = [] 11 | for vi in range(5): 12 | col = [] 13 | for u in range(0, 720 + u_step, u_step): 14 | v = -1 + vi * v_step 15 | col.append(( 16 | (1 + v / 2 * cos(radians(u / 2))) * cos(radians(u)), 17 | (1 + v / 2 * cos(radians(u / 2))) * sin(radians(u)), 18 | v / 2 * sin(radians(u / 2)) 19 | )) 20 | 21 | points.append(col) 22 | 23 | return Workplane().splineApproxSurface(points, thickness) 24 | 25 | u_step = 10 26 | v_step = 0.2 27 | thickness = 0.1 28 | 29 | strip = twisted_strip(u_step, v_step, thickness) -------------------------------------------------------------------------------- /examples/voronoi_box.py: -------------------------------------------------------------------------------- 1 | # Scipy 1.6 or later is required. 2 | 3 | import numpy 4 | from scipy import spatial 5 | from cadquery import Vector 6 | from cqmore import Workplane 7 | from cqmore.matrix import translation, scaling 8 | 9 | def voronoi_box(n, length, width, height, thickness): 10 | def voronoiConvexs(n, length, width, height): 11 | points = numpy.random.rand(n, 3) * max(length, width, height) * 2 12 | voronoi = spatial.Voronoi(points) 13 | # round for avoiding floating-point error 14 | vertices = numpy.around(voronoi.vertices, decimals = 5) 15 | 16 | m_scaling = scaling((0.9, 0.9, 0.9)) 17 | 18 | convexs = Workplane() 19 | convex = Workplane() 20 | for region_i in voronoi.point_region: 21 | region = voronoi.regions[region_i] 22 | region_vts = [Vector(*vertices[i]) for i in region if i != -1] 23 | geom_center = sum(region_vts, Vector()) / len(region_vts) 24 | m = translation(geom_center.toTuple()) @ m_scaling @ translation((-geom_center).toTuple()) 25 | transformed = m.transformAll(v.toTuple() for v in region_vts) 26 | convexs.add(convex.hull(transformed)) 27 | return convexs 28 | 29 | convexs = voronoiConvexs(n, length, width, height) 30 | 31 | half_thickness = thickness / 2 32 | voronoi_frame = ( 33 | Workplane() 34 | .box(length - half_thickness, width - half_thickness, height - half_thickness) 35 | .faces('+Z') 36 | .shell(thickness) 37 | .translate((length, width, height)) 38 | .cut(convexs) 39 | .translate((-length, -width, -height + thickness / 8)) 40 | ) 41 | 42 | box = (Workplane() 43 | .box(length, width, height - thickness / 4) 44 | .faces('+Z') 45 | .shell(half_thickness) 46 | ) 47 | 48 | return voronoi_frame.union(box) 49 | 50 | n = 30 51 | length = 60 52 | width = 60 53 | height = 60 54 | thickness = 3 55 | 56 | box = voronoi_box(n, length, width, height, thickness) -------------------------------------------------------------------------------- /examples/voronoi_cube.py: -------------------------------------------------------------------------------- 1 | # Scipy 1.6 or later is required. 2 | 3 | from random import random 4 | import numpy 5 | from scipy import spatial 6 | from cadquery import Vector 7 | from cqmore import Workplane 8 | from cqmore.matrix import translation, scaling 9 | 10 | length = 60 11 | width = 60 12 | height = 120 13 | thickness = 2 14 | step = 30 15 | cube_frame = True 16 | 17 | def voronoi_cube(length, width, height, thickness, step): 18 | def random_points(length, width, height, step): 19 | random_scale = step / 2 20 | double_step = step * 2 21 | half_random_scale = random_scale / 2 22 | x_offset = -length / 2 - half_random_scale 23 | y_offset = -width / 2 - half_random_scale 24 | z_offset = -height / 2 - half_random_scale 25 | 26 | points = [] 27 | for x in range(-double_step, length + double_step, step): 28 | for y in range(-double_step, width + double_step, step): 29 | for z in range(-double_step, height + double_step, step): 30 | points.append([ 31 | x_offset + x + random() * random_scale, 32 | y_offset + y + random() * random_scale, 33 | z_offset + z + random() * random_scale] 34 | ) 35 | return points 36 | 37 | def voronoiConvexs(length, width, height, thickness, step): 38 | voronoi = spatial.Voronoi(random_points(length, width, height, step)) 39 | vertices = numpy.around(voronoi.vertices, decimals = 5) 40 | 41 | s = (step - thickness) / step 42 | m_scaling = scaling((s, s, s)) 43 | 44 | convexs = Workplane() 45 | convex = Workplane() 46 | for region_i in voronoi.point_region: 47 | region = voronoi.regions[region_i] 48 | region_vts = [Vector(*vertices[i]) for i in region if i != -1] 49 | geom_center = sum(region_vts, Vector()) / len(region_vts) 50 | m = translation(geom_center.toTuple()) @ m_scaling @ translation((-geom_center).toTuple()) 51 | transformed = m.transformAll(v.toTuple() for v in region_vts) 52 | convexs.add(convex.hull(transformed)) 53 | return convexs 54 | 55 | 56 | return (Workplane() 57 | .box(length, width, height) 58 | .cut(voronoiConvexs(length, width, height, thickness, step) 59 | ) 60 | ) 61 | 62 | 63 | def makeFrame(polyhedron): 64 | faces = polyhedron.faces() 65 | frame = faces.item(0).shell(-thickness) 66 | for j in range(1, faces.size()): 67 | frame = frame.intersect(faces.item(j).shell(-thickness)) 68 | return frame 69 | 70 | cube = voronoi_cube(length, width, height, thickness, step) 71 | 72 | if cube_frame: 73 | cube = makeFrame(Workplane().box(length, width, height)).union(cube) 74 | 75 | show_object(cube) # type: ignore -------------------------------------------------------------------------------- /examples/voronoi_vase.py: -------------------------------------------------------------------------------- 1 | # Scipy 1.6 or later is required. 2 | # not stable, float-error problems? 3 | # Keep on trying until you make it ... XD 4 | 5 | from random import random 6 | import numpy 7 | from scipy import spatial 8 | from cadquery import Vector 9 | from cqmore import Workplane 10 | from cqmore.matrix import translation, scaling 11 | from cqmore.polygon import regularPolygon 12 | from cqmore.polyhedron import sweep 13 | 14 | diameter = 90 15 | sides = 12 16 | height = 120 17 | thickness = 2 18 | step = 30 19 | 20 | def voronoi_vase(diameter, sides, height, thickness, step): 21 | def sided_vase(diameter, sides, height): 22 | r = diameter / 2 23 | 24 | half_height = height / 2 25 | radii = [r * 0.5, r * 0.8, r, r * 0.875, r * 0.725, r * 0.625, r * 0.6, r * 0.75] 26 | h_step = height / len(radii) 27 | sections = [] 28 | for i in range(len(radii)): 29 | polygon = regularPolygon(sides, radii[i]) 30 | sections.append([(p[0], p[1], -half_height + h_step * i) for p in polygon]) 31 | 32 | return Workplane().polyhedron(*sweep(sections)) 33 | 34 | def random_points(diameter, height, step): 35 | random_scale = step / 2 36 | double_step = step * 2 37 | half_random_scale = random_scale / 2 38 | x_offset = -diameter / 2 - half_random_scale 39 | y_offset = -diameter / 2 - half_random_scale 40 | z_offset = -height / 2 - half_random_scale 41 | 42 | points = [] 43 | for x in range(-double_step, diameter + double_step, step): 44 | for y in range(-double_step, diameter + double_step, step): 45 | for z in range(-double_step, height + double_step, step): 46 | points.append([ 47 | x_offset + x + random() * random_scale, 48 | y_offset + y + random() * random_scale, 49 | z_offset + z + random() * random_scale] 50 | ) 51 | return points 52 | 53 | def voronoiConvexs(diameter, height, thickness, step): 54 | voronoi = spatial.Voronoi(random_points(diameter, height, step)) 55 | vertices = numpy.around(voronoi.vertices, 5) 56 | 57 | s = (step - thickness) / step 58 | m_scaling = scaling((s, s, s)) 59 | 60 | convex = Workplane() 61 | convexs = Workplane() 62 | for region_i in voronoi.point_region: 63 | region = voronoi.regions[region_i] 64 | region_vts = [Vector(*vertices[i]) for i in region if i != -1] 65 | geom_center = sum(region_vts, Vector()) / len(region_vts) 66 | m = translation(geom_center.toTuple()) @ m_scaling @ translation((-geom_center).toTuple()) 67 | transformed = m.transformAll(v.toTuple() for v in region_vts) 68 | convexs.add(convex.hull(transformed)) 69 | 70 | return convexs 71 | 72 | vase = sided_vase(diameter, sides, height) 73 | 74 | outerShell = vase.faces('+Z').shell(thickness) 75 | innerShell = vase.faces('+Z').shell(-thickness) 76 | 77 | return (outerShell 78 | .intersect(voronoiConvexs(diameter, height, thickness, step)) 79 | .union(innerShell) 80 | ) 81 | 82 | vase = voronoi_vase(diameter, sides, height, thickness, step) 83 | -------------------------------------------------------------------------------- /images/ripple.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/images/ripple.JPG -------------------------------------------------------------------------------- /images/superellipsoids.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/images/superellipsoids.JPG -------------------------------------------------------------------------------- /images/twisted_stripes.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinSDK/cqMore/a490603a18e87e65d9a8c5d52ea0b0ce402d261c/images/twisted_stripes.JPG -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name = 'cqmore', 5 | version = '0.1', 6 | description = 'cadquery plugin (under construction)', 7 | author = 'Justin Lin', 8 | author_email = 'caterpillar@openhome.cc', 9 | license = 'Apache License 2.0', 10 | url = 'https://github.com/JustinSDK/cqMore', 11 | packages = find_packages() 12 | ) -------------------------------------------------------------------------------- /tests/test_curve.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from cqmore.curve import archimedeanSpiral, circle, helix, parametricEquation, sphericalSpiral, superellipse, superformula, torusKnot, logarithmicSpiral, squircle, egg, lemniscateGerono 3 | 4 | class TestCurve(unittest.TestCase): 5 | def test_circle(self): 6 | self.assertEqual( 7 | (-1.0, 1.2246467991473532e-16), 8 | circle(0.5, 1) 9 | ) 10 | 11 | 12 | def test_logarithmicSpiral(self): 13 | self.assertEqual( 14 | (-2.6180342969327595, 3.2061673217966954e-16), 15 | logarithmicSpiral(0.5) 16 | ) 17 | 18 | 19 | def test_archimedeanSpiral(self): 20 | self.assertEqual( 21 | (-4.141592653589793, 5.071988186590933e-16), 22 | archimedeanSpiral(0.5, 1, 1) 23 | ) 24 | 25 | 26 | def test_squircle(self): 27 | self.assertEqual( 28 | (-1.0, 2.220446049250313e-16), 29 | squircle(0.5, 1, 1) 30 | ) 31 | 32 | 33 | def test_egg(self): 34 | self.assertEqual( 35 | (0.0, 1.2246467991473532e-16), 36 | egg(0.5, 1, 2) 37 | ) 38 | 39 | 40 | def test_superellipse(self): 41 | self.assertEqual( 42 | (-1.0, 1.4997597826618576e-32), 43 | superellipse(0.5, 1) 44 | ) 45 | 46 | 47 | def test_superformula(self): 48 | self.assertEqual( 49 | (-1.851749424574581, 2.2677390056282136e-16), 50 | superformula(0.5, 3, 4.5, 10, 10) 51 | ) 52 | 53 | 54 | def test_helix(self): 55 | self.assertEqual( 56 | (1.0, -2.4492935982947064e-16, 1), 57 | helix(1, 1, 1) 58 | ) 59 | 60 | def test_sphericalSpiral(self): 61 | self.assertEqual( 62 | (-1.0, 6.123233995736766e-16, 6.123233995736766e-17), 63 | sphericalSpiral(.5, 1, 10) 64 | ) 65 | 66 | 67 | def test_torusKnot(self): 68 | self.assertEqual( 69 | (1.0, -2.4492935982947064e-16, -3.6739403974420594e-16), 70 | torusKnot(0.5, 2, 3) 71 | ) 72 | 73 | 74 | def test_lemniscateGerono(self): 75 | self.assertEqual( 76 | (1.2246467991473532e-16, -1.2246467991473532e-16, -1.0), 77 | lemniscateGerono(0.5, 1, 1, 1) 78 | ) 79 | 80 | 81 | def test_parametricEquation(self): 82 | self.assertEqual( 83 | (1.0, -2.4492935982947064e-16, -3.6739403974420594e-16), 84 | parametricEquation(torusKnot, p = 2, q = 3)(0.5) 85 | ) 86 | 87 | if __name__ == '__main__': 88 | unittest.main() 89 | -------------------------------------------------------------------------------- /tests/test_matrix.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from cqmore.matrix import Matrix3D, identity, mirror, rotation, rotationX, rotationY, rotationZ, scaling, translation 3 | from cqmore.polyhedron import tetrahedron, uvSphere 4 | 5 | class TestMatrix(unittest.TestCase): 6 | def test_Matrix3D(self): 7 | m = [ 8 | [1, 0, 0, 5], 9 | [0, 1, 0, 5], 10 | [0, 0, 1, 5], 11 | [0, 0, 0, 1] 12 | ] 13 | m3d = Matrix3D(m) 14 | 15 | self.assertListEqual(m, m3d.wrapped.tolist()) 16 | 17 | 18 | def test_transform(self): 19 | translation = Matrix3D([ 20 | [1, 0, 0, 5], 21 | [0, 1, 0, 5], 22 | [0, 0, 1, 5], 23 | [0, 0, 0, 1] 24 | ]) 25 | 26 | translated = translation.transform((10, 20, 30)) 27 | self.assertEqual((15, 25, 35), translated) 28 | 29 | 30 | def test_transformAll(self): 31 | translation = Matrix3D([ 32 | [1, 0, 0, 5], 33 | [0, 1, 0, 5], 34 | [0, 0, 1, 5], 35 | [0, 0, 0, 1] 36 | ]) 37 | 38 | points = [(10, 20, 30), (0, 0, 0), (-10, -20, -30)] 39 | translated = translation.transformAll(points) 40 | self.assertTupleEqual(((15, 25, 35), (5, 5, 5), (-5, -15, -25)), translated) 41 | 42 | 43 | def test_identity(self): 44 | m = identity() 45 | self.assertListEqual([ 46 | [1, 0, 0, 0], 47 | [0, 1, 0, 0], 48 | [0, 0, 1, 0], 49 | [0, 0, 0, 1] 50 | ], m.wrapped.tolist()) 51 | 52 | 53 | def test_scaling(self): 54 | sphere = uvSphere(1) 55 | m = scaling((2, 1, 1)) 56 | scaled_points = m.transformAll(sphere.points) 57 | 58 | expected = ((2.0, 0.0, 6.123233995736766e-17), (-0.9999999999999996, 0.8660254037844387, 6.123233995736766e-17), (-1.0000000000000009, -0.8660254037844385, 6.123233995736766e-17), (0, 0, -1), (0, 0, 1)) 59 | self.assertTupleEqual(expected, scaled_points) 60 | 61 | 62 | def test_translation(self): 63 | sphere = uvSphere(1, widthSegments = 12, heightSegments = 6) 64 | 65 | m = translation((3, 0, 0)) 66 | translated = m.transformAll(sphere.points) 67 | 68 | expected = ((3.5000000000000004, 0.0, -0.8660254037844385), (3.4330127018922196, 0.2500000000000001, -0.8660254037844385), (3.25, 0.4330127018922196, -0.8660254037844385), (3.0, 0.5000000000000003, -0.8660254037844385), (2.75, 0.43301270189221963, -0.8660254037844385), (2.5669872981077804, 0.25000000000000033, -0.8660254037844385), (2.4999999999999996, 6.12323399573677e-17, -0.8660254037844385), (2.5669872981077804, -0.25, -0.8660254037844385), (2.7499999999999996, -0.4330127018922195, -0.8660254037844385), (3.0, -0.5000000000000003, -0.8660254037844385), (3.25, -0.4330127018922198, -0.8660254037844385), (3.4330127018922196, -0.2500000000000004, -0.8660254037844385), (3.866025403784439, 0.0, -0.4999999999999998), (3.75, 0.4330127018922193, -0.4999999999999998), (3.4330127018922196, 0.75, -0.4999999999999998), (3.0, 0.8660254037844387, -0.4999999999999998), (2.566987298107781, 0.7500000000000001, -0.4999999999999998), (2.25, 0.43301270189221963, -0.4999999999999998), (2.133974596215561, 1.0605752387249069e-16, -0.4999999999999998), (2.25, -0.43301270189221913, -0.4999999999999998), (2.5669872981077804, -0.7499999999999999, -0.4999999999999998), (3.0, -0.8660254037844387, -0.4999999999999998), (3.4330127018922187, -0.7500000000000004, -0.4999999999999998), (3.75, -0.43301270189221974, -0.4999999999999998), (4.0, 0.0, 6.123233995736766e-17), (3.866025403784439, 0.49999999999999994, 6.123233995736766e-17), (3.5, 0.8660254037844386, 6.123233995736766e-17), (3.0, 1.0, 6.123233995736766e-17), (2.5, 0.8660254037844387, 6.123233995736766e-17), (2.1339745962155616, 0.5000000000000003, 6.123233995736766e-17), (2.0, 1.2246467991473532e-16, 6.123233995736766e-17), (2.133974596215561, -0.4999999999999997, 6.123233995736766e-17), (2.4999999999999996, -0.8660254037844385, 6.123233995736766e-17), (3.0, -1.0, 6.123233995736766e-17), (3.499999999999999, -0.866025403784439, 6.123233995736766e-17), (3.8660254037844384, -0.5000000000000004, 6.123233995736766e-17), (3.8660254037844384, 0.0, 0.5000000000000001), (3.75, 0.43301270189221924, 0.5000000000000001), (3.4330127018922196, 0.7499999999999999, 0.5000000000000001), (3.0, 0.8660254037844386, 0.5000000000000001), (2.566987298107781, 0.75, 0.5000000000000001), (2.25, 0.4330127018922196, 0.5000000000000001), (2.1339745962155616, 1.0605752387249068e-16, 0.5000000000000001), (2.25, -0.4330127018922191, 0.5000000000000001), (2.5669872981077804, -0.7499999999999998, 0.5000000000000001), (3.0, -0.8660254037844386, 0.5000000000000001), (3.4330127018922187, -0.7500000000000003, 0.5000000000000001), (3.7499999999999996, -0.4330127018922197, 0.5000000000000001), (3.5, 0.0, 0.8660254037844387), (3.433012701892219, 0.24999999999999994, 0.8660254037844387), (3.25, 0.43301270189221924, 0.8660254037844387), (3.0, 0.49999999999999994, 0.8660254037844387), (2.75, 0.4330127018922193, 0.8660254037844387), (2.566987298107781, 0.2500000000000001, 0.8660254037844387), (2.5, 6.123233995736765e-17, 0.8660254037844387), (2.566987298107781, -0.24999999999999983, 0.8660254037844387), (2.75, -0.4330127018922192, 0.8660254037844387), (3.0, -0.49999999999999994, 0.8660254037844387), (3.2499999999999996, -0.43301270189221946, 0.8660254037844387), (3.433012701892219, -0.25000000000000017, 0.8660254037844387), (3, 0, -1), (3, 0, 1)) 69 | self.assertTupleEqual(expected, translated) 70 | 71 | 72 | def test_mirror(self): 73 | t = tetrahedron(1) 74 | 75 | mirrored = mirror((1, 0, 0)).transformAll(t.points) 76 | 77 | expected = ((-0.5773502691896258, 0.5773502691896258, 0.5773502691896258), (0.5773502691896258, -0.5773502691896258, 0.5773502691896258), (0.5773502691896258, 0.5773502691896258, -0.5773502691896258), (-0.5773502691896258, -0.5773502691896258, -0.5773502691896258)) 78 | self.assertTupleEqual(expected, mirrored) 79 | 80 | 81 | def test_rotationX(self): 82 | t = tetrahedron(1) 83 | rotated = rotationX(90).transformAll(t.points) 84 | 85 | expected = ((0.5773502691896258, -0.5773502691896258, 0.5773502691896258), (-0.5773502691896258, -0.5773502691896258, -0.5773502691896258), (-0.5773502691896258, 0.5773502691896258, 0.5773502691896258), (0.5773502691896258, 0.5773502691896258, -0.5773502691896258)) 86 | self.assertTupleEqual(expected, rotated) 87 | 88 | 89 | def test_rotationY(self): 90 | t = tetrahedron(1) 91 | rotated = rotationY(90).transformAll(t.points) 92 | 93 | expected = ((0.5773502691896258, 0.5773502691896258, -0.5773502691896258), (0.5773502691896258, -0.5773502691896258, 0.5773502691896258), (-0.5773502691896258, 0.5773502691896258, 0.5773502691896258), (-0.5773502691896258, -0.5773502691896258, -0.5773502691896258)) 94 | self.assertTupleEqual(expected, rotated) 95 | 96 | 97 | def test_rotationZ(self): 98 | t = tetrahedron(1) 99 | rotated = rotationZ(90).transformAll(t.points) 100 | 101 | expected = ((-0.5773502691896258, 0.5773502691896258, 0.5773502691896258), (0.5773502691896258, -0.5773502691896258, 0.5773502691896258), (-0.5773502691896258, -0.5773502691896258, -0.5773502691896258), (0.5773502691896258, 0.5773502691896258, -0.5773502691896258)) 102 | self.assertTupleEqual(expected, rotated) 103 | 104 | 105 | def test_rotation(self): 106 | t = tetrahedron(1) 107 | rotated = rotation((10, 10, 10), 90).transformAll(t.points) 108 | 109 | expected = ((0.5773502691896258, 0.5773502691896258, 0.5773502691896258), (0.47421657693679153, -0.8591167563965422, -0.19245008972987518), (-0.8591167563965422, -0.1924500897298752, 0.47421657693679153), (-0.1924500897298752, 0.47421657693679153, -0.8591167563965422)) 110 | self.assertTupleEqual(expected, rotated) 111 | 112 | 113 | if __name__ == '__main__': 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import cast 3 | from cadquery import Vector, Vertex, Wire 4 | from cqmore import Workplane 5 | 6 | class TestWorkplane2D(unittest.TestCase): 7 | def test_makePolygon(self): 8 | points = ( 9 | (0, 0, 0), (10, 0, 0), (0, 10, 0), (-10, 0, 0) 10 | ) 11 | 12 | wire = Wire.makePolygon(( 13 | Vector(*p) for p in points + (points[0], ) 14 | ), 15 | False 16 | ) 17 | 18 | expected = cast(list[Wire], 19 | Workplane().rect(5, 5, forConstruction = True) 20 | .vertices() 21 | .eachpoint(lambda loc: wire.moved(loc)) 22 | .vals() 23 | ) 24 | actual = cast(list[Wire], 25 | Workplane().rect(5, 5, forConstruction = True) 26 | .vertices() 27 | .makePolygon(points) 28 | .vals() 29 | ) 30 | for i in range(len(expected)): 31 | self.assertWireEqual(expected[i], actual[i]) 32 | 33 | 34 | def test_intersect2D(self): 35 | r1 = Workplane().rect(10, 10) 36 | r2 = Workplane().center(5, 5).rect(10, 10) 37 | 38 | expected = cast(Wire, Workplane().center(2.5, 2.5).rect(5, 5).val()) 39 | actual = cast(Wire, r1.intersect2D(r2).val()) 40 | self.assertWireEqual(expected, actual) 41 | 42 | wire = cast(Wire, r2.val()) 43 | actual = cast(Wire, r1.intersect2D(wire).val()) 44 | self.assertWireEqual(expected, actual) 45 | 46 | 47 | def test_union2D(self): 48 | r1 = Workplane().rect(10, 10) 49 | r2 = Workplane().center(5, 5).rect(10, 10) 50 | 51 | expected = cast(Wire, Workplane().polyline( 52 | ((-5, -5), (5, -5), (5, 0), (10, 0), (10, 10), (0, 10), (0, 5), (-5, 5))).close().val() 53 | ) 54 | actual = cast(Wire, r1.union2D(r2).val()) 55 | self.assertWireEqual(expected, actual) 56 | 57 | wire = cast(Wire, r2.val()) 58 | actual = cast(Wire, r1.union2D(wire).val()) 59 | self.assertWireEqual(expected, actual) 60 | 61 | 62 | def test_cut2D(self): 63 | r1 = Workplane().rect(10, 10) 64 | r2 = Workplane().center(5, 5).rect(10, 10) 65 | 66 | expected = cast(Wire, Workplane().polyline( 67 | ((-5, -5), (5, -5), (5, 0), (0, 0), (0, 5), (-5, 5))).close().val() 68 | ) 69 | actual = cast(Wire, r1.cut2D(r2).val()) 70 | self.assertWireEqual(expected, actual) 71 | 72 | wire = cast(Wire, r2.val()) 73 | actual = cast(Wire, r1.cut2D(wire).val()) 74 | self.assertWireEqual(expected, actual) 75 | 76 | 77 | def test_hull2D(self): 78 | points = ( 79 | (0.855355763576732, 0.16864474737612778), 80 | (0.7653639827409133, 0.21243642222244463), 81 | (0.9956528164357294, 0.6119951986040002), 82 | (0.34997550432063895, 0.1878314178942282), 83 | (0.5811634308956635, 0.8646520280492559), 84 | (0.4556958318174945, 0.16723661322362438), 85 | (0.9398877210188867, 0.2413165664583884), 86 | (0.375900956434601, 0.09820967766941846), 87 | (0.8532633331060003, 0.5955415267257996) 88 | ) 89 | 90 | pts = Workplane().hull2D(points, forConstruction = True).vertices().vals() 91 | self.assertEqual(6, len(pts)) 92 | 93 | 94 | def test_polylineJoin2D(self): 95 | points = ((0, 0), (10, 10), (0, 15), (-10, 10), (-10, 0)) 96 | polyline = Workplane().polylineJoin2D(points, Workplane().polygon(6, 1)) 97 | self.assertEqual(18, len(polyline.vertices().vals())) 98 | 99 | 100 | def assertWireEqual(self, expected, actual): 101 | self.assertEqual(expected.geomType(), actual.geomType()) 102 | self.assertEqual(expected.Center(), actual.Center()) 103 | self.assertEqual(expected.Area(), actual.Area()) 104 | self.assertEqual(expected.CenterOfBoundBox(), actual.CenterOfBoundBox()) 105 | self.assertListEqual( 106 | sorted([v.toTuple() for v in expected.Vertices()]), 107 | sorted([v.toTuple() for v in actual.Vertices()]) 108 | ) 109 | 110 | class TestWorkplane3D(unittest.TestCase): 111 | def test_splineApproxSurface(self): 112 | def paraboloid(x, y): 113 | return (x, y, ((y ** 2) - (x ** 2)) / 4) 114 | 115 | min_value = -30 116 | max_value = 30 117 | step = 5 118 | thickness = 0.5 119 | 120 | points = [[ 121 | paraboloid(x / 10, y / 10) 122 | for y in range(min_value, max_value + step, step) 123 | ] for x in range(min_value, max_value + step, step)] 124 | 125 | surface = Workplane().splineApproxSurface(points, thickness) 126 | self.assertEqual(6, surface.faces().size()) 127 | 128 | 129 | def test_polyhedron(self): 130 | points = ( 131 | (5, -5, -5), (-5, 5, -5), (5, 5, 5), (-5, -5, 5) 132 | ) 133 | 134 | faces = ( 135 | (0, 1, 2), (0, 3, 1), (1, 3, 2), (0, 2, 3) 136 | ) 137 | 138 | tetrahedron = Workplane().polyhedron(points, faces) 139 | self.assertEqual(4, tetrahedron.faces().size()) 140 | 141 | vertices = tetrahedron.vertices() 142 | self.assertEqual(4, vertices.size()) 143 | 144 | actual = cast(list[Vertex], vertices.vals()) 145 | self.assertListEqual( 146 | sorted(points), 147 | sorted([v.toTuple() for v in actual]) 148 | ) 149 | 150 | 151 | def test_hull(self): 152 | points = ( 153 | (50, 50, 50), 154 | (50, 50, 0), 155 | (-50, 50, 0), 156 | (-50, -50, 0), 157 | (50, -50, 0), 158 | (0, 0, 50), 159 | (0, 0, -50) 160 | ) 161 | 162 | convex_hull = Workplane().hull(points) 163 | 164 | self.assertEqual(10, len(convex_hull.faces().vals())) 165 | self.assertEqual(7, len(convex_hull.vertices().vals())) 166 | 167 | 168 | def test_polylineJoin(self): 169 | polyline = (Workplane() 170 | .polylineJoin( 171 | [(0, 0, 0), (10, 0, 0), (10, 0, 10), (10, 10, 10)], 172 | Workplane().box(1, 1, 1) 173 | ) 174 | ) 175 | self.assertEqual(16, len(polyline.vertices().vals())) 176 | 177 | 178 | if __name__ == '__main__': 179 | unittest.main() 180 | -------------------------------------------------------------------------------- /tests/test_polygon.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from cqmore.polygon import regularPolygon, hull2D, star 3 | 4 | class TestPolygon(unittest.TestCase): 5 | def test_regularPolygon(self): 6 | polygon = regularPolygon(nSides = 6, radius = 10) 7 | self.assertEqual(6, len(tuple(polygon))) 8 | 9 | polygon = regularPolygon( 10 | nSides = 6, 11 | radius = 10, 12 | thetaStart = 45, 13 | thetaEnd = 270 14 | ) 15 | self.assertEqual(8, len(tuple(polygon))) 16 | 17 | 18 | def test_star(self): 19 | polygon = star(outerRadius = 10, innerRadius = 5, n = 8) 20 | self.assertEqual(16, len(tuple(polygon))) 21 | 22 | 23 | def test_hull2D(self): 24 | points = ( 25 | (0.855355763576732, 0.16864474737612778), 26 | (0.7653639827409133, 0.21243642222244463), 27 | (0.9956528164357294, 0.6119951986040002), 28 | (0.34997550432063895, 0.1878314178942282), 29 | (0.5811634308956635, 0.8646520280492559), 30 | (0.4556958318174945, 0.16723661322362438), 31 | (0.9398877210188867, 0.2413165664583884), 32 | (0.375900956434601, 0.09820967766941846), 33 | (0.8532633331060003, 0.5955415267257996) 34 | ) 35 | 36 | self.assertEqual(6, len(tuple(hull2D(points)))) 37 | 38 | 39 | if __name__ == '__main__': 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /tests/test_polyhedron.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from cqmore.polyhedron import gridSurface, star, superellipsoid, sweep, uvSphere, hull 3 | from cqmore.polyhedron import tetrahedron, hexahedron, octahedron, dodecahedron, icosahedron 4 | 5 | class TestPolyhedron(unittest.TestCase): 6 | def test_uvSphere(self): 7 | sphere = uvSphere(10, widthSegments = 6, heightSegments = 3) 8 | self.assertListEqual( 9 | [(0, 1, 7), (0, 7, 6), (1, 2, 8), (1, 8, 7), (2, 3, 9), (2, 9, 8), (3, 4, 10), (3, 10, 9), (4, 5, 11), (4, 11, 10), (5, 0, 6), (5, 6, 11), (12, 1, 0), (12, 2, 1), (12, 3, 2), (12, 4, 3), (12, 5, 4), (12, 0, 5), (13, 6, 7), (13, 7, 8), (13, 8, 9), (13, 9, 10), (13, 10, 11), (13, 11, 6)], 10 | list(sphere.faces) 11 | ) 12 | 13 | 14 | def test_tetrahedron(self): 15 | polyhedron = tetrahedron(1, 1) 16 | self.assertEqual(4 * 4, len(list(polyhedron.faces))) 17 | 18 | 19 | def test_hexahedron(self): 20 | polyhedron = hexahedron(1, 1) 21 | self.assertEqual(6 * 2 * 4, len(list(polyhedron.faces))) 22 | 23 | 24 | def test_octahedron(self): 25 | polyhedron = octahedron(1, 1) 26 | self.assertEqual(8 * 4, len(list(polyhedron.faces))) 27 | 28 | 29 | def test_dodecahedron(self): 30 | polyhedron = dodecahedron(1, 1) 31 | self.assertEqual(12 * 3 * 4, len(list(polyhedron.faces))) 32 | 33 | 34 | def test_icosahedron(self): 35 | polyhedron = icosahedron(1, 1) 36 | self.assertEqual(20 * 4, len(list(polyhedron.faces))) 37 | 38 | 39 | def test_star(self): 40 | polyhedron = star() 41 | 42 | self.assertEqual(12, len(list(polyhedron.points))) 43 | self.assertEqual(20, len(list(polyhedron.faces))) 44 | 45 | 46 | def test_gridSurface(self): 47 | points = [ 48 | [(0, 1, 0), (0, 10, 0), (0, 20, 0)], 49 | [(10, 0, 0), (10, 10, 1), (10, 21, 0)], 50 | [(20, 0, 0), (21, 10, 0), (20, 20, 0)] 51 | ] 52 | 53 | sf = gridSurface(points, 1) 54 | 55 | self.assertEqual(18, len(list(sf.points))) 56 | self.assertEqual(32, len(list(sf.faces))) 57 | 58 | 59 | def test_superellipsoid(self): 60 | p = superellipsoid(2.5, .25, widthSegments = 24, heightSegments = 12) 61 | 62 | self.assertEqual(312, len(list(p.points))) 63 | self.assertEqual(578, len(list(p.faces))) 64 | 65 | 66 | def test_hull(self): 67 | points = ( 68 | (50, 50, 50), 69 | (50, 50, 0), 70 | (-50, 50, 0), 71 | (-50, -50, 0), 72 | (50, -50, 0), 73 | (0, 0, 50), 74 | (0, 0, -50) 75 | ) 76 | 77 | convex_hull = hull(points) 78 | 79 | self.assertEqual(10, len(list(convex_hull.faces))) 80 | self.assertEqual(7, len(list(convex_hull.points))) 81 | 82 | 83 | def test_sweep(self): 84 | profiles = [ 85 | [(0, 0, 0), (10, 0, 0), (10, 10, 0), (0, 10, 0)], 86 | [(0, 0, 10), (20, 0, 10), (20, 20, 10), (0, 20, 10)], 87 | [(0, 0, 35), (5, 0, 35), (5, 5, 35), (0, 5, 35)], 88 | ] 89 | 90 | p = sweep(profiles) 91 | 92 | self.assertEqual(18, len(list(p.faces))) 93 | self.assertEqual(12, len(list(p.points))) 94 | 95 | if __name__ == '__main__': 96 | unittest.main() 97 | --------------------------------------------------------------------------------