diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 51ba9fad..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: 2 - -jobs: - runtests: - - macos: - xcode: "9.0" - - environment: - PYTEST_QT_API: pyqt5 - PYTHON_VERSION: 3.6 - - steps: - - checkout - - run: HOMEBREW_NO_AUTO_UPDATE=1 brew install wget - - run: cd && rm -rf ~/.pyenv && rm -rf ~/virtualenvs - - run: wget https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -O ~/miniconda.sh - - run: chmod +x ~/miniconda.sh && ~/miniconda.sh -b - - run: echo "export PATH=~/miniconda3/bin:$PATH" >> $BASH_ENV - - run: conda config --set always_yes yes - - run: conda create --quiet --name cqtest -c cadquery -c conda-forge -c dlr-sc pythonocc-core=0.18.2 oce=0.18.2 python=$PYTHON_VERSION pyparsing mock lldb - - run: | - source activate cqtest - pip install coverage - python setup.py install - conda env list - conda list - coverage run runtests.py - -workflows: - version: 2 - - workflow: - jobs: - - runtests \ No newline at end of file diff --git a/.gitignore b/.gitignore index e7df8cfc..8a19d8f0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/* .idea/* cadquery.egg-info target/* +.vscode diff --git a/.travis.yml b/.travis.yml index c6a7ba09..368c9f2d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ dist: trusty branches: only: - - adam-urbanczyk-OCC-version-update - master - "/\\d+\\.\\d+\\.?d*\\-*[a-z]*/" @@ -25,62 +24,51 @@ env: matrix: include: - - env: TRAVIS_PYTHON_VERSION=3.6 + - name: "Python 3.6 - osx" + env: PYTHON_VERSION=3.6 os: osx - - env: TRAVIS_PYTHON_VERSION=3.6 + - name: "Python 3.6 - linux" + env: PYTHON_VERSION=3.6 os: linux - - env: TRAVIS_PYTHON_VERSION=3.7 + - name: "Python 3.7 - osx" + env: PYTHON_VERSION=3.7 os: osx - - env: TRAVIS_PYTHON_VERSION=3.7 + - name: "Python 3.7 - linux" + env: PYTHON_VERSION=3.7 os: linux + - name: "Lint" + env: PYTHON_VERSION=3.7 + os: linux + script: + - black . --diff --check before_install: -- if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then - PY_MAJOR=2 ; - else - PY_MAJOR=3 ; - fi ; - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then +- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then OS=Linux ; else OS=MacOSX ; fi ; - wget https://repo.continuum.io/miniconda/Miniconda$PY_MAJOR-latest-$OS-x86_64.sh -O miniconda.sh + wget https://repo.anaconda.com/miniconda/Miniconda3-latest-$OS-x86_64.sh -O miniconda.sh - bash miniconda.sh -b -p $HOME/miniconda; - export PATH="$HOME/miniconda/bin:$HOME/miniconda/lib:$PATH"; -- hash -r; - conda config --set always_yes yes --set changeps1 no; -- conda create -y -q -n test_cq -c cadquery -c conda-forge pythonocc-core=0.18.2 oce=0.18.2 python=$TRAVIS_PYTHON_VERSION - pyparsing mock; -- source ~/miniconda/bin/activate test_cq; -- python -c 'import OCC.gp as gp; print(gp.gp_Vec())' -- pip install codecov +- conda env create -f environment.yml +- source ~/miniconda/bin/activate cadquery +- conda install -c conda-forge -c defaults -c cadquery python=$PYTHON_VERSION install: - python setup.py install before_script: -- ulimit -c unlimited -S +- ulimit -c unlimited -S - sudo rm -f /cores/core.* script: -- coverage run runtests.py +- pytest -v --cov after_success: - codecov after_failure: -- ls /cores/core.* +- ls /cores/core.* - lldb --core `ls /cores/core.*` --batch --one-line "bt" - - -before_deploy: -- conda install anaconda-client conda-build -- sh conda_build.sh - -deploy: -- provider: script - skip_cleanup: true - script: anaconda -v -t $ANACONDA_TOKEN upload --force --user cadquery /tmp/cbld/**/cadquery-*.tar.bz2 - on: - tags: true diff --git a/README.md b/README.md index e78e5203..2cd7aeb1 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,38 @@ You do not need to be a software developer to have a big impact on this project. It is asked that all contributions to this project be made in a respectful and considerate way. Please use the [Python Community Code of Conduct's](https://www.python.org/psf/codeofconduct/) guidelines as a reference. +### Contributing code + +If you are going to contribute code, make sure to follow this steps: + +- Consider opening an issue first to discuss what you have in mind +- Try to keep it as short and simple as possible (if you want to change several + things, start with just one!) +- Fork the CadQuery repository, clone your fork and create a new branch to + start working on your changes +- Start with the tests! How should CadQuery behave after your changes? Make + sure to add some tests to the test suite to ensure proper behavior +- Make sure your tests have assertions checking all the expected results +- Add a nice docstring to the test indicating what the test is doing; if there + is too much to explain, consider splitting the test in two! +- Go ahead and implement the changes +- Add a nice docstring to the functions/methods/classes you implement + describing what they do, what the expected parameters are and what it returns + (if anything) +- Update the documentation if there is any change to the public API +- Consider adding an example to the documentation showing your cool new + feature! +- Make sure nothing is broken (run the complete test suite with `pytest`) +- Run `black` to autoformat your code and make sure your code style complies + with CadQuery's +- Push the changes to your fork and open a pull-request upstream +- Keep an eye on the automated feedback you will receive from the CI pipelines; + if there is a test failing or some code is not properly formatted, you will + be notified without human intervention +- Be prepared for constructive feedback and criticism! +- Be patient and respectful, remember that those reviewing your code are also + working hard (sometimes reviewing changes is harder than implementing them!) + ### How to Report a Bug When filing a bug report [issue](https://github.com/CadQuery/cadquery/issues), please be sure to answer these questions: diff --git a/appveyor.yml b/appveyor.yml index 0f62ef51..2aac4e12 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,7 +9,7 @@ environment: MINICONDA_DIRNAME: C:\Miniconda36-x64 - PYTHON_VERSION: 3.7 MINICONDA_DIRNAME: C:\Miniconda37-x64 - + ANACONDA_TOKEN: secure: nxF/a2f3iS9KXGu7B/wKJYAk7Sm5wyAjoZoqJvPbRoVK4saaozVwOxDrjwJjJAYb @@ -17,34 +17,14 @@ install: - set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - conda config --set always_yes yes - conda update -q conda - - conda create --quiet --name cqtest -c cadquery -c conda-forge -c dlr-sc pythonocc-core=0.18.2 python=%PYTHON_VERSION% pyparsing mock coverage codecov - - activate cqtest - - pip install codecov - - python setup.py install + - conda env create -f environment.yml + - activate cadquery + - conda install -c conda-forge -c defaults -c cadquery python=%PYTHON_VERSION% build: false test_script: - - coverage run runtests.py - + - pytest -v --cov + on_success: - - codecov - -#deploy: -#- provider: Script -# on: -# APPVEYOR_REPO_TAG: true - -#before_deploy: -# - set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" -# - set "TRAVIS_TAG=%APPVEYOR_REPO_TAG_NAME%" -# - set "TRAVIS_COMMIT=%APPVEYOR_REPO_COMMIT%" -# - set "TRAVIS_PYTHON_VERSION=%PYTHON_VERSION%" -# - conda config --set always_yes yes -# - activate cqtest -# - conda install anaconda-client conda-build -# - conda info --envs -# - call conda_build.bat - -#deploy_script: -# - anaconda -v -t %ANACONDA_TOKEN% upload --force --user cadquery ./win-64/cadquery-*.tar.bz2 + - codecov \ No newline at end of file diff --git a/cadquery/__init__.py b/cadquery/__init__.py index 57e4148a..c789890a 100644 --- a/cadquery/__init__.py +++ b/cadquery/__init__.py @@ -1,25 +1,65 @@ # these items point to the OCC implementation from .occ_impl.geom import Plane, BoundBox, Vector, Matrix -from .occ_impl.shapes import (Shape, Vertex, Edge, Face, Wire, Solid, Shell, - Compound, sortWiresByBuildOrder) +from .occ_impl.shapes import ( + Shape, + Vertex, + Edge, + Face, + Wire, + Solid, + Shell, + Compound, + sortWiresByBuildOrder, +) from .occ_impl import exporters from .occ_impl import importers # these items are the common implementation # the order of these matter -from .selectors import (NearestToPointSelector, ParallelDirSelector, - DirectionSelector, PerpendicularDirSelector, TypeSelector, - DirectionMinMaxSelector, StringSyntaxSelector, Selector) +from .selectors import ( + NearestToPointSelector, + ParallelDirSelector, + DirectionSelector, + PerpendicularDirSelector, + TypeSelector, + DirectionMinMaxSelector, + StringSyntaxSelector, + Selector, +) from .cq import CQ, Workplane, selectors from . import plugins __all__ = [ - 'CQ', 'Workplane', 'plugins', 'selectors', 'Plane', 'BoundBox', 'Matrix', 'Vector', 'sortWiresByBuildOrder', - 'Shape', 'Vertex', 'Edge', 'Wire', 'Face', 'Solid', 'Shell', 'Compound', 'exporters', 'importers', - 'NearestToPointSelector', 'ParallelDirSelector', 'DirectionSelector', 'PerpendicularDirSelector', - 'TypeSelector', 'DirectionMinMaxSelector', 'StringSyntaxSelector', 'Selector', 'plugins' + "CQ", + "Workplane", + "plugins", + "selectors", + "Plane", + "BoundBox", + "Matrix", + "Vector", + "sortWiresByBuildOrder", + "Shape", + "Vertex", + "Edge", + "Wire", + "Face", + "Solid", + "Shell", + "Compound", + "exporters", + "importers", + "NearestToPointSelector", + "ParallelDirSelector", + "DirectionSelector", + "PerpendicularDirSelector", + "TypeSelector", + "DirectionMinMaxSelector", + "StringSyntaxSelector", + "Selector", + "plugins", ] -__version__ = "2.0.0dev" +__version__ = "2.0RC1" diff --git a/cadquery/cq.py b/cadquery/cq.py index bc289659..21249ded 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -18,8 +18,20 @@ """ import math -from . import Vector, Plane, Shape, Edge, Wire, Face, Solid, Compound, \ - sortWiresByBuildOrder, selectors, exporters +from copy import copy +from . import ( + Vector, + Plane, + Shape, + Edge, + Wire, + Face, + Solid, + Compound, + sortWiresByBuildOrder, + selectors, + exporters, +) class CQContext(object): @@ -31,7 +43,9 @@ class CQContext(object): """ def __init__(self): - self.pendingWires = [] # a list of wires that have been created and need to be extruded + self.pendingWires = ( + [] + ) # a list of wires that have been created and need to be extruded # a list of created pending edges that need to be joined into wires self.pendingEdges = [] # a reference to the first point for a set of edges. @@ -58,6 +72,7 @@ class CQ(object): self.objects = [] self.ctx = CQContext() self.parent = None + self._tag = None if obj: # guarded because sometimes None for internal use self.objects.append(obj) @@ -81,6 +96,17 @@ class CQ(object): r.objects = list(objlist) return r + def tag(self, name): + """ + Tags the current CQ object for later reference. + + :param name: the name to tag this object with + :type name: string + :returns: self, a cq object with tag applied + """ + self._tag = name + return self + def _collectProperty(self, propName): """ Collects all of the values for propName, @@ -99,8 +125,12 @@ class CQ(object): # tricky-- if an object is a compound of solids, # do not return all of the solids underneath-- typically # then we'll keep joining to ourself - if propName == 'Solids' and isinstance(o, Solid) and o.ShapeType() == 'Compound': - for i in getattr(o, 'Compounds')(): + if ( + propName == "Solids" + and isinstance(o, Solid) + and o.ShapeType() == "Compound" + ): + for i in getattr(o, "Compounds")(): all[i.hashCode()] = i else: if hasattr(o, propName): @@ -249,6 +279,22 @@ class CQ(object): """ return self.objects[0] + def _getTagged(self, name): + """ + Search the parent chain for a an object with tag == name. + + :param name: the tag to search for + :type name: string + :returns: the first CQ object in the parent chain with tag == name + :raises: ValueError if no object tagged name in the chain + """ + if self._tag == name: + return self + if self.parent is None: + raise ValueError("No CQ object named {} in chain".format(name)) + else: + return self.parent._getTagged(name) + def toOCC(self): """ Directly returns the wrapped FreeCAD object to cut down on the amount of boiler plate code @@ -259,8 +305,9 @@ class CQ(object): return self.objects[0].wrapped - def workplane(self, offset=0.0, invert=False, centerOption='CenterOfMass', - origin=None): + def workplane( + self, offset=0.0, invert=False, centerOption="CenterOfMass", origin=None + ): """ Creates a new 2-D workplane, located relative to the first face on the stack. @@ -310,6 +357,7 @@ class CQ(object): For now you can work around by creating a workplane and then offsetting the center afterwards. """ + def _isCoPlanar(f0, f1): """Test if two faces are on the same plane.""" p0 = f0.Center() @@ -318,9 +366,11 @@ class CQ(object): n1 = f1.normalAt() # test normals (direction of planes) - if not ((abs(n0.x - n1.x) < self.ctx.tolerance) or - (abs(n0.y - n1.y) < self.ctx.tolerance) or - (abs(n0.z - n1.z) < self.ctx.tolerance)): + if not ( + (abs(n0.x - n1.x) < self.ctx.tolerance) + or (abs(n0.y - n1.y) < self.ctx.tolerance) + or (abs(n0.z - n1.z) < self.ctx.tolerance) + ): return False # test if p1 is on the plane of f0 (offset of planes) @@ -339,22 +389,23 @@ class CQ(object): xd = Vector(1, 0, 0) return xd - if centerOption not in {'CenterOfMass', 'ProjectedOrigin', 'CenterOfBoundBox'}: - raise ValueError('Undefined centerOption value provided.') + if centerOption not in {"CenterOfMass", "ProjectedOrigin", "CenterOfBoundBox"}: + raise ValueError("Undefined centerOption value provided.") if len(self.objects) > 1: # are all objects 'PLANE'? - if not all(o.geomType() in ('PLANE', 'CIRCLE') for o in self.objects): + if not all(o.geomType() in ("PLANE", "CIRCLE") for o in self.objects): raise ValueError( - "If multiple objects selected, they all must be planar faces.") + "If multiple objects selected, they all must be planar faces." + ) # are all faces co-planar with each other? if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]): raise ValueError("Selected faces must be co-planar.") - if centerOption in {'CenterOfMass', 'ProjectedOrigin'}: + if centerOption in {"CenterOfMass", "ProjectedOrigin"}: center = Shape.CombinedCenter(self.objects) - elif centerOption == 'CenterOfBoundBox': + elif centerOption == "CenterOfBoundBox": center = Shape.CombinedCenterOfBoundBox(self.objects) normal = self.objects[0].normalAt() @@ -364,26 +415,27 @@ class CQ(object): obj = self.objects[0] if isinstance(obj, Face): - if centerOption in {'CenterOfMass', 'ProjectedOrigin'}: + if centerOption in {"CenterOfMass", "ProjectedOrigin"}: center = obj.Center() - elif centerOption == 'CenterOfBoundBox': + elif centerOption == "CenterOfBoundBox": center = obj.CenterOfBoundBox() normal = obj.normalAt(center) xDir = _computeXdir(normal) else: - if hasattr(obj, 'Center'): - if centerOption in {'CenterOfMass', 'ProjectedOrigin'}: + if hasattr(obj, "Center"): + if centerOption in {"CenterOfMass", "ProjectedOrigin"}: center = obj.Center() - elif centerOption == 'CenterOfBoundBox': + elif centerOption == "CenterOfBoundBox": center = obj.CenterOfBoundBox() normal = self.plane.zDir xDir = self.plane.xDir else: raise ValueError( - "Needs a face or a vertex or point on a work plane") + "Needs a face or a vertex or point on a work plane" + ) # update center to projected origin if desired - if centerOption == 'ProjectedOrigin': + if centerOption == "ProjectedOrigin": if origin is None: origin = self.plane.origin elif isinstance(origin, tuple): @@ -407,6 +459,31 @@ class CQ(object): # a new workplane has the center of the workplane on the stack return s + def copyWorkplane(self, obj): + """ + Copies the workplane from obj. + + :param obj: an object to copy the workplane from + :type obj: a CQ object + :returns: a CQ object with obj's workplane + """ + out = Workplane(obj.plane) + out.parent = self + out.ctx = self.ctx + return out + + def workplaneFromTagged(self, name): + """ + Copies the workplane from a tagged parent. + + :param name: tag to search for + :type name: string + :returns: a CQ object with name's workplane + """ + tagged = self._getTagged(name) + out = self.copyWorkplane(tagged) + return out + def first(self): """ Return the first item on the stack @@ -459,9 +536,7 @@ class CQ(object): return rv[0] if searchParents and self.parent is not None: - return self.parent._findType(types, - searchStack=True, - searchParents=True) + return self.parent._findType(types, searchStack=True, searchParents=True) return None @@ -501,20 +576,23 @@ class CQ(object): return self._findType(Face, searchStack, searchParents) - def _selectObjects(self, objType, selector=None): + def _selectObjects(self, objType, selector=None, tag=None): """ Filters objects of the selected type with the specified selector,and returns results :param objType: the type of object we are searching for :type objType: string: (Vertex|Edge|Wire|Solid|Shell|Compound|CompSolid) + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object with the selected objects on the stack. **Implementation Note**: This is the base implementation of the vertices,edges,faces, solids,shells, and other similar selector methods. It is a useful extension point for plugin developers to make other selector methods. """ + cq_obj = self._getTagged(tag) if tag else self # A single list of all faces from all objects on the stack - toReturn = self._collectProperty(objType) + toReturn = cq_obj._collectProperty(objType) if selector is not None: if isinstance(selector, str) or isinstance(selector, str): @@ -525,7 +603,7 @@ class CQ(object): return self.newObject(toReturn) - def vertices(self, selector=None): + def vertices(self, selector=None, tag=None): """ Select the vertices of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the vertices of all objects are collected and a list of @@ -533,6 +611,8 @@ class CQ(object): :param selector: :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains the *distinct* vertices of *all* objects on the current stack, after being filtered by the selector, if provided @@ -554,9 +634,9 @@ class CQ(object): :py:class:`StringSyntaxSelector` """ - return self._selectObjects('Vertices', selector) + return self._selectObjects("Vertices", selector, tag) - def faces(self, selector=None): + def faces(self, selector=None, tag=None): """ Select the faces of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the faces of all objects are collected and a list of all the @@ -564,6 +644,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* faces of *all* objects on the current stack, filtered by the provided selector. @@ -586,9 +668,9 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Faces', selector) + return self._selectObjects("Faces", selector, tag) - def edges(self, selector=None): + def edges(self, selector=None, tag=None): """ Select the edges of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the edges of all objects are collected and a list of all the @@ -596,6 +678,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* edges of *all* objects on the current stack, filtered by the provided selector. @@ -617,9 +701,9 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Edges', selector) + return self._selectObjects("Edges", selector, tag) - def wires(self, selector=None): + def wires(self, selector=None, tag=None): """ Select the wires of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the wires of all objects are collected and a list of all the @@ -627,6 +711,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* wires of *all* objects on the current stack, filtered by the provided selector. @@ -640,9 +726,9 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Wires', selector) + return self._selectObjects("Wires", selector, tag) - def solids(self, selector=None): + def solids(self, selector=None, tag=None): """ Select the solids of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the solids of all objects are collected and a list of all the @@ -650,6 +736,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on the current stack, filtered by the provided selector. @@ -666,9 +754,9 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Solids', selector) + return self._selectObjects("Solids", selector, tag) - def shells(self, selector=None): + def shells(self, selector=None, tag=None): """ Select the shells of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the shells of all objects are collected and a list of all the @@ -676,6 +764,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on the current stack, filtered by the provided selector. @@ -686,9 +776,9 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Shells', selector) + return self._selectObjects("Shells", selector, tag) - def compounds(self, selector=None): + def compounds(self, selector=None, tag=None): """ Select compounds on the stack, optionally filtering the selection. If there are multiple objects on the stack, they are collected and a list of all the distinct compounds @@ -696,6 +786,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on the current stack, filtered by the provided selector. @@ -704,7 +796,7 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Compounds', selector) + return self._selectObjects("Compounds", selector, tag) def toSvg(self, opts=None): """ @@ -777,8 +869,9 @@ class CQ(object): :type angleDegrees: float :returns: a CQ object """ - return self.newObject([o.rotate(axisStartPoint, axisEndPoint, angleDegrees) - for o in self.objects]) + return self.newObject( + [o.rotate(axisStartPoint, axisEndPoint, angleDegrees) for o in self.objects] + ) def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)): """ @@ -789,8 +882,7 @@ class CQ(object): :param basePointVector: the base point to mirror about :type basePointVector: tuple """ - newS = self.newObject( - [self.objects[0].mirror(mirrorPlane, basePointVector)]) + newS = self.newObject([self.objects[0].mirror(mirrorPlane, basePointVector)]) return newS.first() def translate(self, vec): @@ -943,7 +1035,7 @@ class Workplane(CQ): :py:meth:`CQ.workplane` """ - FOR_CONSTRUCTION = 'ForConstruction' + FOR_CONSTRUCTION = "ForConstruction" def __init__(self, inPlane, origin=(0, 0, 0), obj=None): """ @@ -967,7 +1059,7 @@ class Workplane(CQ): the *current point* is on the origin. """ - if inPlane.__class__.__name__ == 'Plane': + if inPlane.__class__.__name__ == "Plane": tmpPlane = inPlane elif isinstance(inPlane, str) or isinstance(inPlane, str): tmpPlane = Plane.named(inPlane, origin) @@ -976,15 +1068,16 @@ class Workplane(CQ): if tmpPlane is None: raise ValueError( - 'Provided value {} is not a valid work plane'.format(inPlane)) + "Provided value {} is not a valid work plane".format(inPlane) + ) self.obj = obj self.plane = tmpPlane - self.firstPoint = None # Changed so that workplane has the center as the first item on the stack self.objects = [self.plane.origin] self.parent = None self.ctx = CQContext() + self._tag = None def transformed(self, rotate=(0, 0, 0), offset=(0, 0, 0)): """ @@ -999,10 +1092,10 @@ class Workplane(CQ): """ # old api accepted a vector, so we'll check for that. - if rotate.__class__.__name__ == 'Vector': + if rotate.__class__.__name__ == "Vector": rotate = rotate.toTuple() - if offset.__class__.__name__ == 'Vector': + if offset.__class__.__name__ == "Vector": offset = offset.toTuple() p = self.plane.rotated(rotate) @@ -1026,7 +1119,7 @@ class Workplane(CQ): # copy the current state to the new object ns = Workplane("XY") - ns.plane = self.plane + ns.plane = copy(self.plane) ns.parent = self ns.objects = list(objlist) ns.ctx = self.ctx @@ -1060,14 +1153,35 @@ class Workplane(CQ): elif isinstance(obj, Vector): p = obj else: - raise RuntimeError( - "Cannot convert object type '%s' to vector " % type(obj)) + raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj)) if useLocalCoords: return self.plane.toLocalCoords(p) else: return p + def _findFromEdge(self, useLocalCoords=False): + """ + Finds the previous edge for an operation that needs it, similar to + method _findFromPoint. Examples include tangentArcPoint. + + :param useLocalCoords: selects whether the point is returned + in local coordinates or global coordinates. + :return: an Edge + """ + obj = self.objects[-1] + + if not isinstance(obj, Edge): + raise RuntimeError( + "Previous Edge requested, but the previous object was of " + + f"type {type(obj)}, not an Edge." + ) + + if useLocalCoords: + obj = self.plane.toLocalCoords(obj) + + return obj + def rarray(self, xSpacing, ySpacing, xCount, yCount, center=True): """ Creates an array of points and pushes them onto the stack. @@ -1187,15 +1301,16 @@ class Workplane(CQ): #this workplane is centered at x=0.5,y=0.5, the center of the upper face s = Workplane().box(1,1,1).faces(">Z").workplane() - s.center(-0.5,-0.5) # move the center to the corner + s = s.center(-0.5,-0.5) # move the center to the corner t = s.circle(0.25).extrude(0.2) assert ( t.faces().size() == 9 ) # a cube with a cylindrical nub at the top right corner The result is a cube with a round boss on the corner """ "Shift local coordinates to the specified location, according to current coordinates" - self.plane.setOrigin2d(x, y) - n = self.newObject([self.plane.origin]) + new_origin = self.plane.toWorldCoords((x, y)) + n = self.newObject([new_origin]) + n.plane.setOrigin2d(x, y) return n def lineTo(self, x, y, forConstruction=False): @@ -1367,28 +1482,35 @@ class Workplane(CQ): :return: A CQ object representing a slot """ - radius = diameter/2 + radius = diameter / 2 - p1 = pnt + Vector((-length/2) + radius, diameter/2) + p1 = pnt + Vector((-length / 2) + radius, diameter / 2) p2 = p1 + Vector(length - diameter, 0) p3 = p1 + Vector(length - diameter, -diameter) p4 = p1 + Vector(0, -diameter) arc1 = p2 + Vector(radius, -radius) arc2 = p4 + Vector(-radius, radius) - edges=[(Edge.makeLine(p1,p2))] + edges = [(Edge.makeLine(p1, p2))] edges.append(Edge.makeThreePointArc(p2, arc1, p3)) edges.append(Edge.makeLine(p3, p4)) edges.append(Edge.makeThreePointArc(p4, arc2, p1)) slot = Wire.assembleEdges(edges) - return slot.rotate(pnt, pnt + Vector(0,0,1), angle) + return slot.rotate(pnt, pnt + Vector(0, 0, 1), angle) return self.eachpoint(_makeslot, True) - def spline(self, listOfXYTuple, tangents=None, periodic=False, - forConstruction=False, includeCurrent=False, makeWire=False): + def spline( + self, + listOfXYTuple, + tangents=None, + periodic=False, + forConstruction=False, + includeCurrent=False, + makeWire=False, + ): """ Create a spline interpolated through the provided points. @@ -1435,8 +1557,10 @@ class Workplane(CQ): if tangents: t1, t2 = tangents - tangents = (self.plane.toWorldCoords(t1), - self.plane.toWorldCoords(t2)) + tangents = ( + self.plane.toWorldCoords(t1) - self.plane.origin, + self.plane.toWorldCoords(t2) - self.plane.origin, + ) e = Edge.makeSpline(allPoints, tangents=tangents, periodic=periodic) @@ -1464,9 +1588,68 @@ class Workplane(CQ): """ - allPoints = [func(start+stop*t/N) for t in range(N+1)] + allPoints = [func(start + stop * t / N) for t in range(N + 1)] - return self.spline(allPoints,includeCurrent=False,makeWire=True) + return self.spline(allPoints, includeCurrent=False, makeWire=True) + + def ellipseArc( + self, + x_radius, + y_radius, + angle1=360, + angle2=360, + rotation_angle=0.0, + sense=1, + forConstruction=False, + startAtCurrent=True, + makeWire=False, + ): + """Draw an elliptical arc with x and y radiuses either with start point at current point or + or current point being the center of the arc + + :param x_radius: x radius of the ellipse (along the x-axis of plane the ellipse should lie in) + :param y_radius: y radius of the ellipse (along the y-axis of plane the ellipse should lie in) + :param angle1: start angle of arc + :param angle2: end angle of arc (angle2 == angle1 return closed ellipse = default) + :param rotation_angle: angle to rotate the created ellipse / arc + :param sense: clockwise (-1) or counter clockwise (1) + :param startAtCurrent: True: start point of arc is moved to current point; False: center of + arc is on current point + :param makeWire: convert the resulting arc edge to a wire + """ + + # Start building the ellipse with the current point as center + center = self._findFromPoint(useLocalCoords=False) + e = Edge.makeEllipse( + x_radius, + y_radius, + center, + self.plane.zDir, + self.plane.xDir, + angle1, + angle2, + sense == 1, + ) + + # Rotate if necessary + if rotation_angle != 0.0: + e = e.rotate(center, center.add(self.plane.zDir), rotation_angle) + + # Move the start point of the ellipse onto the last current point + if startAtCurrent: + startPoint = e.startPoint() + e = e.translate(center.sub(startPoint)) + + if makeWire: + rv = Wire.assembleEdges([e]) + if not forConstruction: + self._addPendingWire(rv) + else: + rv = e + if not forConstruction: + self._addPendingEdge(e) + + return self.newObject([rv]) def threePointArc(self, point1, point2, forConstruction=False): """ @@ -1516,10 +1699,16 @@ class Workplane(CQ): midPoint = endPoint.add(startPoint).multiply(0.5) sagVector = endPoint.sub(startPoint).normalized().multiply(abs(sag)) - if(sag > 0): - sagVector.x, sagVector.y = -sagVector.y, sagVector.x # Rotate sagVector +90 deg + if sag > 0: + sagVector.x, sagVector.y = ( + -sagVector.y, + sagVector.x, + ) # Rotate sagVector +90 deg else: - sagVector.x, sagVector.y = sagVector.y, -sagVector.x # Rotate sagVector -90 deg + sagVector.x, sagVector.y = ( + sagVector.y, + -sagVector.x, + ) # Rotate sagVector -90 deg sagPoint = midPoint.add(sagVector) @@ -1545,7 +1734,7 @@ class Workplane(CQ): # Calculate the sagitta from the radius length = endPoint.sub(startPoint).Length / 2.0 try: - sag = abs(radius) - math.sqrt(radius**2 - length**2) + sag = abs(radius) - math.sqrt(radius ** 2 - length ** 2) except ValueError: raise ValueError("Arc radius is not large enough to reach the end point.") @@ -1555,6 +1744,37 @@ class Workplane(CQ): else: return self.sagittaArc(endPoint, -sag, forConstruction) + def tangentArcPoint(self, endpoint, forConstruction=False, relative=True): + """ + Draw an arc as a tangent from the end of the current edge to endpoint. + + :param endpoint: point for the arc to end at + :type endpoint: 2-tuple, 3-tuple or Vector + :param relative: True if endpoint is specified relative to the current point, False if endpoint is in workplane coordinates + :type relative: Bool + :return: a Workplane object with an arc on the stack + + Requires the the current first object on the stack is an Edge, as would + be the case after a lineTo operation or similar. + """ + + if not isinstance(endpoint, Vector): + endpoint = Vector(endpoint) + if relative: + endpoint = endpoint + self._findFromPoint(useLocalCoords=True) + endpoint = self.plane.toWorldCoords(endpoint) + + previousEdge = self._findFromEdge() + + arc = Edge.makeTangentArc( + previousEdge.endPoint(), previousEdge.tangentAt(1), endpoint + ) + + if not forConstruction: + self._addPendingEdge(arc) + + return self.newObject([arc]) + def rotateAndCopy(self, matrix): """ Makes a copy of all edges on the stack, rotates them according to the @@ -1580,8 +1800,7 @@ class Workplane(CQ): # attempt to consolidate wires together. consolidated = n.consolidateWires() - rotatedWires = self.plane.rotateShapes( - consolidated.wires().vals(), matrix) + rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(), matrix) for w in rotatedWires: consolidated.objects.append(w) @@ -1616,8 +1835,7 @@ class Workplane(CQ): # attempt to consolidate wires together. consolidated = n.consolidateWires() - mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), - 'Y') + mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), "Y") for w in mirroredWires: consolidated.objects.append(w) @@ -1646,8 +1864,7 @@ class Workplane(CQ): # attempt to consolidate wires together. consolidated = n.consolidateWires() - mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), - 'X') + mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), "X") for w in mirroredWires: consolidated.objects.append(w) @@ -1859,6 +2076,7 @@ class Workplane(CQ): better way to handle forConstruction project points not in the workplane plane onto the workplane plane """ + def makeRectangleWire(pnt): # Here pnt is in local coordinates due to useLocalCoords=True # (xc,yc,zc) = pnt.toTuple() @@ -1909,6 +2127,7 @@ class Workplane(CQ): project points not in the workplane plane onto the workplane plane """ + def makeCircleWire(obj): cir = Wire.makeCircle(radius, obj, Vector(0, 0, 1)) cir.forConstruction = forConstruction @@ -1916,6 +2135,42 @@ class Workplane(CQ): return self.eachpoint(makeCircleWire, useLocalCoordinates=True) + # ellipse from current point + def ellipse(self, x_radius, y_radius, rotation_angle=0.0, forConstruction=False): + """ + Make an ellipse for each item on the stack. + :param x_radius: x radius of the ellipse (x-axis of plane the ellipse should lie in) + :type x_radius: float > 0 + :param y_radius: y radius of the ellipse (y-axis of plane the ellipse should lie in) + :type y_radius: float > 0 + :param rotation_angle: angle to rotate the ellipse (0 = no rotation = default) + :type rotation_angle: float + :param forConstruction: should the new wires be reference geometry only? + :type forConstruction: true if the wires are for reference, false if they are creating + part geometry + :return: a new CQ object with the created wires on the stack + + *NOTE* Due to a bug in opencascade (https://tracker.dev.opencascade.org/view.php?id=31290) + the center of mass (equals center for next shape) is shifted. To create concentric ellipses + use Workplane("XY") + .center(10, 20).ellipse(100,10) + .center(0, 0).ellipse(50, 5) + """ + + def makeEllipseWire(obj): + elip = Wire.makeEllipse( + x_radius, + y_radius, + obj, + Vector(0, 0, 1), + Vector(1, 0, 0), + rotation_angle=rotation_angle, + ) + elip.forConstruction = forConstruction + return elip + + return self.eachpoint(makeEllipseWire, useLocalCoordinates=True) + def polygon(self, nSides, diameter, forConstruction=False): """ Creates a polygon inscribed in a circle of the specified diameter for each point on @@ -1927,19 +2182,25 @@ class Workplane(CQ): :param diameter: the size of the circle the polygon is inscribed into :return: a polygon wire """ + def _makePolygon(center): # pnt is a vector in local coordinates angle = 2.0 * math.pi / nSides pnts = [] for i in range(nSides + 1): - pnts.append(center + Vector((diameter / 2.0 * math.cos(angle * i)), - (diameter / 2.0 * math.sin(angle * i)), 0)) + pnts.append( + center + + Vector( + (diameter / 2.0 * math.cos(angle * i)), + (diameter / 2.0 * math.sin(angle * i)), + 0, + ) + ) return Wire.makePolygon(pnts, forConstruction) return self.eachpoint(_makePolygon, True) - def polyline(self, listOfXYTuple, forConstruction=False, - includeCurrent=False): + def polyline(self, listOfXYTuple, forConstruction=False, includeCurrent=False): """ Create a polyline from a list of points @@ -1984,11 +2245,13 @@ class Workplane(CQ): :return: a CQ object with a completed wire on the stack, if possible. - After 2-d drafting with lineTo,threePointArc, and polyline, it is necessary - to convert the edges produced by these into one or more wires. + After 2-d drafting with methods such as lineTo, threePointArc, + tangentArcPoint and polyline, it is necessary to convert the edges + produced by these into one or more wires. - When a set of edges is closed, cadQuery assumes it is safe to build the group of edges - into a wire. This example builds a simple triangular prism:: + When a set of edges is closed, cadQuery assumes it is safe to build + the group of edges into a wire. This example builds a simple triangular + prism:: s = Workplane().lineTo(1,0).lineTo(1,1).close().extrude(0.2) """ @@ -2089,11 +2352,11 @@ class Workplane(CQ): boreDir = Vector(0, 0, -1) # first make the hole hole = Solid.makeCylinder( - diameter / 2.0, depth, center, boreDir) # local coordianates! + diameter / 2.0, depth, center, boreDir + ) # local coordianates! # add the counter bore - cbore = Solid.makeCylinder( - cboreDiameter / 2.0, cboreDepth, center, boreDir) + cbore = Solid.makeCylinder(cboreDiameter / 2.0, cboreDepth, center, boreDir) r = hole.fuse(cbore) return r @@ -2142,7 +2405,8 @@ class Workplane(CQ): # first make the hole hole = Solid.makeCylinder( - diameter / 2.0, depth, center, boreDir) # local coords! + diameter / 2.0, depth, center, boreDir + ) # local coords! r = cskDiameter / 2.0 h = r / math.tan(math.radians(cskAngle / 2.0)) csk = Solid.makeCone(r, 0.0, h, center, boreDir) @@ -2191,7 +2455,8 @@ class Workplane(CQ): boreDir = Vector(0, 0, -1) # first make the hole hole = Solid.makeCylinder( - diameter / 2.0, depth, center, boreDir) # local coordinates! + diameter / 2.0, depth, center, boreDir + ) # local coordinates! return hole return self.cutEach(_makeHole, True, clean) @@ -2235,8 +2500,9 @@ class Workplane(CQ): # are multiple sets r = None for ws in wireSets: - thisObj = Solid.extrudeLinearWithRotation(ws[0], ws[1:], self.plane.origin, - eDir, angleDegrees) + thisObj = Solid.extrudeLinearWithRotation( + ws[0], ws[1:], self.plane.origin, eDir, angleDegrees + ) if r is None: r = thisObj else: @@ -2277,7 +2543,8 @@ class Workplane(CQ): selected may not be planar """ r = self._extrude( - distance, both=both, taper=taper) # returns a Solid (or a compound if there were multiple) + distance, both=both, taper=taper + ) # returns a Solid (or a compound if there were multiple) if combine: newS = self._combineWithBase(r) @@ -2287,7 +2554,9 @@ class Workplane(CQ): newS = newS.clean() return newS - def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True): + def revolve( + self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True + ): """ Use all un-revolved wires in the parent chain to create a solid. @@ -2344,8 +2613,17 @@ class Workplane(CQ): newS = newS.clean() return newS - def sweep(self, path, multisection=False, sweepAlongWires=None, makeSolid=True, isFrenet=False, - combine=True, clean=True, transition='right'): + def sweep( + self, + path, + multisection=False, + sweepAlongWires=None, + makeSolid=True, + isFrenet=False, + combine=True, + clean=True, + transition="right", + ): """ Use all un-extruded wires in the parent chain to create a swept solid. @@ -2360,22 +2638,27 @@ class Workplane(CQ): Possible values are {'transformed','round', 'right'} (default: 'right'). :return: a CQ object with the resulting solid selected. """ - - if not sweepAlongWires is None: - multisection=sweepAlongWires - - from warnings import warn - warn('sweepAlongWires keyword argument is is depracated and will '\ - 'be removed in the next version; use multisection instead', - DeprecationWarning) - r = self._sweep(path.wire(), multisection, makeSolid, isFrenet, - transition) # returns a Solid (or a compound if there were multiple) + if not sweepAlongWires is None: + multisection = sweepAlongWires + + from warnings import warn + + warn( + "sweepAlongWires keyword argument is is depracated and will " + "be removed in the next version; use multisection instead", + DeprecationWarning, + ) + + r = self._sweep( + path.wire(), multisection, makeSolid, isFrenet, transition + ) # returns a Solid (or a compound if there were multiple) if combine: newS = self._combineWithBase(r) else: newS = self.newObject([r]) - if clean: newS = newS.clean() + if clean: + newS = newS.clean() return newS def _combineWithBase(self, obj): @@ -2442,7 +2725,8 @@ class Workplane(CQ): solids = toUnion.solids().vals() if len(solids) < 1: raise ValueError( - "CQ object must have at least one solid on the stack to union!") + "CQ object must have at least one solid on the stack to union!" + ) newS = solids.pop(0) for s in solids: newS = newS.fuse(s) @@ -2492,7 +2776,7 @@ class Workplane(CQ): if clean: newS = newS.clean() - + return self.newObject([newS]) def intersect(self, toIntersect, clean=True): @@ -2522,8 +2806,9 @@ class Workplane(CQ): newS = solidRef.intersect(solidToIntersect) - if clean: newS = newS.clean() - + if clean: + newS = newS.clean() + return self.newObject([newS]) def cutBlind(self, distanceToCut, clean=True, taper=None): @@ -2580,19 +2865,18 @@ class Workplane(CQ): solidRef = self.findSolid() faceRef = self.findFace() - #if no faces on the stack take the nearest face parallel to the plane zDir + # if no faces on the stack take the nearest face parallel to the plane zDir if not faceRef: - #first select all with faces with good orietation + # first select all with faces with good orietation sel = selectors.PerpendicularDirSelector(self.plane.zDir) faces = sel.filter(solidRef.Faces()) - #then select the closest + # then select the closest sel = selectors.NearestToPointSelector(self.plane.origin.toTuple()) faceRef = sel.filter(faces)[0] rv = [] for solid in solidRef.Solids(): - s = solid.dprism(faceRef, wires, thruAll=True, additive=False, - taper=-taper) + s = solid.dprism(faceRef, wires, thruAll=True, additive=False, taper=-taper) if clean: s = s.clean() @@ -2635,9 +2919,8 @@ class Workplane(CQ): # group wires together into faces based on which ones are inside the others # result is a list of lists - - wireSets = sortWiresByBuildOrder( - list(self.ctx.pendingWires), []) + + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), []) # now all of the wires have been used to create an extrusion self.ctx.pendingWires = [] @@ -2655,18 +2938,17 @@ class Workplane(CQ): toFuse = [] if taper: - for ws in wireSets: - thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper) - toFuse.append(thisObj) + for ws in wireSets: + thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper) + toFuse.append(thisObj) else: - for ws in wireSets: - thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) - toFuse.append(thisObj) + for ws in wireSets: + thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) + toFuse.append(thisObj) - if both: - thisObj = Solid.extrudeLinear( - ws[0], ws[1:], eDir.multiply(-1.)) - toFuse.append(thisObj) + if both: + thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir.multiply(-1.0)) + toFuse.append(thisObj) return Compound.makeCompound(toFuse) @@ -2685,8 +2967,7 @@ class Workplane(CQ): This method is a utility method, primarily for plugin and internal use. """ # We have to gather the wires to be revolved - wireSets = sortWiresByBuildOrder( - list(self.ctx.pendingWires)) + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires)) # Mark that all of the wires have been used to create a revolution self.ctx.pendingWires = [] @@ -2694,14 +2975,19 @@ class Workplane(CQ): # Revolve the wires, make a compound out of them and then fuse them toFuse = [] for ws in wireSets: - thisObj = Solid.revolve( - ws[0], ws[1:], angleDegrees, axisStart, axisEnd) + thisObj = Solid.revolve(ws[0], ws[1:], angleDegrees, axisStart, axisEnd) toFuse.append(thisObj) return Compound.makeCompound(toFuse) - def _sweep(self, path, multisection=False, makeSolid=True, isFrenet=False, - transition='right'): + def _sweep( + self, + path, + multisection=False, + makeSolid=True, + isFrenet=False, + transition="right", + ): """ Makes a swept solid from an existing set of pending wires. @@ -2716,19 +3002,112 @@ class Workplane(CQ): if not multisection: wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires)) for ws in wireSets: - thisObj = Solid.sweep(ws[0], ws[1:], path.val(), makeSolid, - isFrenet, transition) + thisObj = Solid.sweep( + ws[0], ws[1:], path.val(), makeSolid, isFrenet, transition + ) toFuse.append(thisObj) else: sections = self.ctx.pendingWires thisObj = Solid.sweep_multi(sections, path.val(), makeSolid, isFrenet) toFuse.append(thisObj) - + self.ctx.pendingWires = [] return Compound.makeCompound(toFuse) - def box(self, length, width, height, centered=(True, True, True), combine=True, clean=True): + def interpPlate( + self, + surf_edges, + surf_pts=[], + thickness=0, + combine=False, + clean=True, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ): + """ + Returns a plate surface that is 'thickness' thick, enclosed by 'surf_edge_pts' points, and going through 'surf_pts' points. Using pushpoints directly with interpPlate and combine=True, can be very ressources intensive depending on the complexity of the shape. In this case set combine=False. + + :param surf_edges + :type 1 surf_edges: list of [x,y,z] float ordered coordinates + :type 2 surf_edges: list of ordered or unordered CadQuery wires + :param surf_pts = [] (uses only edges if []) + :type surf_pts: list of [x,y,z] float coordinates + :param thickness = 0 (returns 2D surface if 0) + :type thickness: float (may be negative or positive depending on thicknening direction) + :param combine: should the results be combined with other solids on the stack + (and each other)? + :type combine: true to combine shapes, false otherwise. + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :param Degree = 3 (OCCT default) + :type Degree: Integer >= 2 + :param NbPtsOnCur = 15 (OCCT default) + :type: NbPtsOnCur Integer >= 15 + :param NbIter = 2 (OCCT default) + :type: NbIterInteger >= 2 + :param Anisotropie = False (OCCT default) + :type Anisotropie: Boolean + :param: Tol2d = 0.00001 (OCCT default) + :type Tol2d: float > 0 + :param Tol3d = 0.0001 (OCCT default) + :type Tol3dReal: float > 0 + :param TolAng = 0.01 (OCCT default) + :type TolAngReal: float > 0 + :param TolCurv = 0.1 (OCCT default) + :type TolCurvReal: float > 0 + :param MaxDeg = 8 (OCCT default) + :type MaxDegInteger: Integer >= 2 (?) + :param MaxSegments = 9 (OCCT default) + :type MaxSegments: Integer >= 2 (?) + """ + + # If thickness is 0, only a 2D surface will be returned. + if thickness == 0: + combine = False + + # Creates interpolated plate + def _makeplate(pnt): + return Solid.interpPlate( + surf_edges, + surf_pts, + thickness, + degree, + nbPtsOnCur, + nbIter, + anisotropy, + tol2d, + tol3d, + tolAng, + tolCurv, + maxDeg, + maxSegments, + ).translate(pnt) + + plates = self.eachpoint(_makeplate, True) + + # if combination is not desired, just return the created boxes + if not combine: + return plates + else: + return self.union(plates, clean=clean) + + def box( + self, + length, + width, + height, + centered=(True, True, True), + combine=True, + clean=True, + ): """ Return a 3d box with specified dimensions for each object on the stack. @@ -2773,14 +3152,14 @@ class Workplane(CQ): def _makebox(pnt): - #(xp,yp,zp) = self.plane.toLocalCoords(pnt) + # (xp,yp,zp) = self.plane.toLocalCoords(pnt) (xp, yp, zp) = pnt.toTuple() if centered[0]: - xp -= (length / 2.0) + xp -= length / 2.0 if centered[1]: - yp -= (width / 2.0) + yp -= width / 2.0 if centered[2]: - zp -= (height / 2.0) + zp -= height / 2.0 return Solid.makeBox(length, width, height, Vector(xp, yp, zp)) @@ -2793,8 +3172,17 @@ class Workplane(CQ): # combine everything return self.union(boxes, clean=clean) - def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360, - centered=(True, True, True), combine=True, clean=True): + def sphere( + self, + radius, + direct=(0, 0, 1), + angle1=-90, + angle2=90, + angle3=360, + centered=(True, True, True), + combine=True, + clean=True, + ): """ Returns a 3D sphere with the specified radius for each point on the stack @@ -2851,7 +3239,9 @@ class Workplane(CQ): if not centered[2]: zp += radius - return Solid.makeSphere(radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3) + return Solid.makeSphere( + radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3 + ) # We want a sphere for each point on the workplane spheres = self.eachpoint(_makesphere, True) @@ -2862,6 +3252,79 @@ class Workplane(CQ): else: return self.union(spheres, clean=clean) + def wedge( + self, + dx, + dy, + dz, + xmin, + zmin, + xmax, + zmax, + pnt=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + centered=(True, True, True), + combine=True, + clean=True, + ): + """ + :param dx: Distance along the X axis + :param dy: Distance along the Y axis + :param dz: Distance along the Z axis + :param xmin: The minimum X location + :param zmin:The minimum Z location + :param xmax:The maximum X location + :param zmax: The maximum Z location + :param pnt: A vector (or tuple) for the origin of the direction for the wedge + :param dir: The direction vector (or tuple) for the major axis of the wedge + :param combine: Whether the results should be combined with other solids on the stack + (and each other) + :param clean: true to attempt to have the kernel clean up the geometry, false otherwise + :return: A wedge object for each point on the stack + + One wedge is created for each item on the current stack. If no items are on the stack, one + wedge using the current workplane center is created. + + If combine is true, the result will be a single object on the stack: + If a solid was found in the chain, the result is that solid with all wedges produced + fused onto it otherwise, the result is the combination of all the produced wedges + + If combine is false, the result will be a list of the wedges produced + """ + + # Convert the point tuple to a vector, if needed + if isinstance(pnt, tuple): + pnt = Vector(pnt) + + # Convert the direction tuple to a vector, if needed + if isinstance(dir, tuple): + dir = Vector(dir) + + def _makewedge(pnt): + (xp, yp, zp) = pnt.toTuple() + + if not centered[0]: + xp += dx / 2.0 + + if not centered[1]: + yp += dy / 2.0 + + if not centered[2]: + zp += dx / 2.0 + + return Solid.makeWedge( + dx, dy, dz, xmin, zmin, xmax, zmax, Vector(xp, yp, zp), dir + ) + + # We want a wedge for each point on the workplane + wedges = self.eachpoint(_makewedge) + + # If we don't need to combine everything, just return the created wedges + if not combine: + return wedges + else: + return self.union(wedges, clean=clean) + def clean(self): """ Cleans the current solid by removing unwanted edges from the @@ -2887,11 +3350,23 @@ class Workplane(CQ): cleanObjects = [obj.clean() for obj in self.objects] except AttributeError: raise AttributeError( - "%s object doesn't support `clean()` method!" % obj.ShapeType()) + "%s object doesn't support `clean()` method!" % obj.ShapeType() + ) return self.newObject(cleanObjects) - def text(self, txt, fontsize, distance, cut=True, combine=False, clean=True, - font="Arial", kind='regular',halign='center',valign='center'): + def text( + self, + txt, + fontsize, + distance, + cut=True, + combine=False, + clean=True, + font="Arial", + kind="regular", + halign="center", + valign="center", + ): """ Create a 3D text @@ -2918,8 +3393,16 @@ class Workplane(CQ): and the resulting solid becomes the new context solid. """ - r = Compound.makeText(txt,fontsize,distance,font=font,kind=kind, - halign=halign, valign=valign, position=self.plane) + r = Compound.makeText( + txt, + fontsize, + distance, + font=font, + kind=kind, + halign=halign, + valign=valign, + position=self.plane, + ) if cut: newS = self._cutFromBase(r) @@ -2937,7 +3420,6 @@ class Workplane(CQ): """ if type(self.objects[0]) is Vector: - return '< {} >'.format(self.__repr__()[1:-1]) + return "< {} >".format(self.__repr__()[1:-1]) else: return Compound.makeCompound(self.objects)._repr_html_() - diff --git a/cadquery/cq_directive.py b/cadquery/cq_directive.py index 11de3b5e..bfbfc0de 100644 --- a/cadquery/cq_directive.py +++ b/cadquery/cq_directive.py @@ -20,13 +20,22 @@ template = """ """ -template_content_indent = ' ' +template_content_indent = " " -def cq_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): +def cq_directive( + name, + arguments, + options, + content, + lineno, + content_offset, + block_text, + state, + state_machine, +): # only consider inline snippets - plot_code = '\n'.join(content) + plot_code = "\n".join(content) # Since we don't have a filename, use a hash based on the content # the script must define a variable called 'out', which is expected to @@ -52,22 +61,20 @@ def cq_directive(name, arguments, options, content, lineno, lines = [] # get rid of new lines - out_svg = out_svg.replace('\n', '') + out_svg = out_svg.replace("\n", "") txt_align = "left" if "align" in options: - txt_align = options['align'] + txt_align = options["align"] - lines.extend((template % locals()).split('\n')) + lines.extend((template % locals()).split("\n")) - lines.extend(['::', '']) - lines.extend([' %s' % row.rstrip() - for row in plot_code.split('\n')]) - lines.append('') + lines.extend(["::", ""]) + lines.extend([" %s" % row.rstrip() for row in plot_code.split("\n")]) + lines.append("") if len(lines): - state_machine.insert_input( - lines, state_machine.input_lines.source(0)) + state_machine.insert_input(lines, state_machine.input_lines.source(0)) return [] @@ -77,9 +84,10 @@ def setup(app): setup.config = app.config setup.confdir = app.confdir - options = {'height': directives.length_or_unitless, - 'width': directives.length_or_percentage_or_unitless, - 'align': directives.unchanged - } + options = { + "height": directives.length_or_unitless, + "width": directives.length_or_percentage_or_unitless, + "align": directives.unchanged, + } - app.add_directive('cq_plot', cq_directive, True, (0, 2, 0), **options) + app.add_directive("cq_plot", cq_directive, True, (0, 2, 0), **options) diff --git a/cadquery/cqgi.py b/cadquery/cqgi.py index e6611f46..4562fcef 100644 --- a/cadquery/cqgi.py +++ b/cadquery/cqgi.py @@ -9,6 +9,7 @@ import cadquery CQSCRIPT = "" + def parse(script_source): """ Parses the script as a model, and returns a model. @@ -34,6 +35,7 @@ class CQModel(object): the build method can be used to generate a 3d model """ + def __init__(self, script_source): """ Create an object by parsing the supplied python script. @@ -100,16 +102,20 @@ class CQModel(object): try: self.set_param_values(build_parameters) collector = ScriptCallback() - env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \ - .add_entry("__name__", "__cqgi__") \ - .add_entry("show_object", collector.show_object) \ - .add_entry("debug", collector.debug) \ - .add_entry("describe_parameter",collector.describe_parameter) \ + env = ( + EnvironmentBuilder() + .with_real_builtins() + .with_cadquery_objects() + .add_entry("__name__", "__cqgi__") + .add_entry("show_object", collector.show_object) + .add_entry("debug", collector.debug) + .add_entry("describe_parameter", collector.describe_parameter) .build() + ) - c = compile(self.ast_tree, CQSCRIPT, 'exec') - exec (c, env) - result.set_debug(collector.debugObjects ) + c = compile(self.ast_tree, CQSCRIPT, "exec") + exec(c, env) + result.set_debug(collector.debugObjects) result.set_success_result(collector.outputObjects) except Exception as ex: @@ -124,7 +130,9 @@ class CQModel(object): for k, v in params.items(): if k not in model_parameters: - raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k) + raise InvalidParameterError( + "Cannot set value '%s': not a parameter of the model." % k + ) p = model_parameters[k] p.set_value(v) @@ -134,10 +142,12 @@ class ShapeResult(object): """ An object created by a build, including the user parameters provided """ + def __init__(self): self.shape = None self.options = None + class BuildResult(object): """ The result of executing a CadQuery script. @@ -149,10 +159,11 @@ class BuildResult(object): If unsuccessful, the exception property contains a reference to the stack trace that occurred. """ + def __init__(self): self.buildTime = None - self.results = [] #list of ShapeResult - self.debugObjects = [] #list of ShapeResult + self.results = [] # list of ShapeResult + self.debugObjects = [] # list of ShapeResult self.first_result = None self.success = False self.exception = None @@ -176,13 +187,14 @@ class ScriptMetadata(object): Defines the metadata for a parsed CQ Script. the parameters property is a dict of InputParameter objects. """ + def __init__(self): self.parameters = {} def add_script_parameter(self, p): self.parameters[p.name] = p - def add_parameter_description(self,name,description): + def add_parameter_description(self, name, description): p = self.parameters[name] p.desc = description @@ -214,6 +226,7 @@ class InputParameter: provide additional metadata """ + def __init__(self): #: the default value for the variable. @@ -234,7 +247,9 @@ class InputParameter: self.ast_node = None @staticmethod - def create(ast_node, var_name, var_type, default_value, valid_values=None, desc=None): + def create( + ast_node, var_name, var_type, default_value, valid_values=None, desc=None + ): if valid_values is None: valid_values = [] @@ -251,8 +266,10 @@ class InputParameter: def set_value(self, new_value): if len(self.valid_values) > 0 and new_value not in self.valid_values: raise InvalidParameterError( - "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} " - .format(str(new_value), self.name, str(self.valid_values))) + "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} ".format( + str(new_value), self.name, str(self.valid_values) + ) + ) if self.varType == NumberParameterType: try: @@ -265,28 +282,33 @@ class InputParameter: self.ast_node.n = f except ValueError: raise InvalidParameterError( - "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric." - .format(str(new_value), self.name)) + "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric.".format( + str(new_value), self.name + ) + ) elif self.varType == StringParameterType: self.ast_node.s = str(new_value) elif self.varType == BooleanParameterType: if new_value: - if hasattr(ast, 'NameConstant'): + if hasattr(ast, "NameConstant"): self.ast_node.value = True else: - self.ast_node.id = 'True' + self.ast_node.id = "True" else: - if hasattr(ast, 'NameConstant'): + if hasattr(ast, "NameConstant"): self.ast_node.value = False else: - self.ast_node.id = 'False' + self.ast_node.id = "False" else: raise ValueError("Unknown Type of var: ", str(self.varType)) def __str__(self): return "InputParameter: {name=%s, type=%s, defaultValue=%s" % ( - self.name, str(self.varType), str(self.default_value)) + self.name, + str(self.varType), + str(self.default_value), + ) class ScriptCallback(object): @@ -295,22 +317,23 @@ class ScriptCallback(object): the show_object() method is exposed to CQ scripts, to allow them to return objects to the execution environment """ + def __init__(self): self.outputObjects = [] self.debugObjects = [] - def show_object(self, shape,options={}): + def show_object(self, shape, options={}): """ return an object to the executing environment, with options :param shape: a cadquery object :param options: a dictionary of options that will be made available to the executing environment """ o = ShapeResult() - o.options=options + o.options = options o.shape = shape self.outputObjects.append(o) - def debug(self,obj,args={}): + def debug(self, obj, args={}): """ Debug print/output an object, with optional arguments. """ @@ -319,7 +342,7 @@ class ScriptCallback(object): s.options = args self.debugObjects.append(s) - def describe_parameter(self,var_data ): + def describe_parameter(self, var_data): """ Do Nothing-- we parsed the ast ahead of execution to get what we need. """ @@ -335,12 +358,12 @@ class ScriptCallback(object): return len(self.outputObjects) > 0 - class InvalidParameterError(Exception): """ Raised when an attempt is made to provide a new parameter value that cannot be assigned to the model """ + pass @@ -349,6 +372,7 @@ class NoOutputError(Exception): Raised when the script does not execute the show_object() method to return a solid """ + pass @@ -386,6 +410,7 @@ class EnvironmentBuilder(object): The environment includes the builtins, as well as the other methods the script will need. """ + def __init__(self): self.env = {} @@ -393,12 +418,12 @@ class EnvironmentBuilder(object): return self.with_builtins(__builtins__) def with_builtins(self, env_dict): - self.env['__builtins__'] = env_dict + self.env["__builtins__"] = env_dict return self def with_cadquery_objects(self): - self.env['cadquery'] = cadquery - self.env['cq'] = cadquery + self.env["cadquery"] = cadquery + self.env["cq"] = cadquery return self def add_entry(self, name, value): @@ -408,30 +433,33 @@ class EnvironmentBuilder(object): def build(self): return self.env + class ParameterDescriptionFinder(ast.NodeTransformer): """ Visits a parse tree, looking for function calls to describe_parameter(var, description ) """ + def __init__(self, cq_model): self.cqModel = cq_model - def visit_Call(self,node): - """ + def visit_Call(self, node): + """ Called when we see a function call. Is it describe_parameter? """ - try: - if node.func.id == 'describe_parameter': + try: + if node.func.id == "describe_parameter": # looks like we have a call to our function. # first parameter is the variable, # second is the description varname = node.args[0].id desc = node.args[1].s - self.cqModel.add_parameter_description(varname,desc) + self.cqModel.add_parameter_description(varname, desc) - except: - #print "Unable to handle function call" + except: + # print "Unable to handle function call" pass - return node + return node + class ConstantAssignmentFinder(ast.NodeTransformer): """ @@ -446,24 +474,42 @@ class ConstantAssignmentFinder(ast.NodeTransformer): if type(value_node) == ast.Num: self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, NumberParameterType, value_node.n)) + InputParameter.create( + value_node, var_name, NumberParameterType, value_node.n + ) + ) elif type(value_node) == ast.Str: self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, StringParameterType, value_node.s)) + InputParameter.create( + value_node, var_name, StringParameterType, value_node.s + ) + ) elif type(value_node) == ast.Name: - if value_node.id == 'True': + if value_node.id == "True": self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, BooleanParameterType, True)) - elif value_node.id == 'False': + InputParameter.create( + value_node, var_name, BooleanParameterType, True + ) + ) + elif value_node.id == "False": self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, BooleanParameterType, False)) - elif hasattr(ast, 'NameConstant') and type(value_node) == ast.NameConstant: + InputParameter.create( + value_node, var_name, BooleanParameterType, False + ) + ) + elif hasattr(ast, "NameConstant") and type(value_node) == ast.NameConstant: if value_node.value == True: self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, BooleanParameterType, True)) + InputParameter.create( + value_node, var_name, BooleanParameterType, True + ) + ) else: self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, BooleanParameterType, False)) + InputParameter.create( + value_node, var_name, BooleanParameterType, False + ) + ) except: print("Unable to handle assignment for variable '%s'" % var_name) pass @@ -479,7 +525,7 @@ class ConstantAssignmentFinder(ast.NodeTransformer): # Handle the NamedConstant type that is only present in Python 3 astTypes = [ast.Num, ast.Str, ast.Name] - if hasattr(ast, 'NameConstant'): + if hasattr(ast, "NameConstant"): astTypes.append(ast.NameConstant) if type(node.value) in astTypes: diff --git a/cadquery/occ_impl/exporters.py b/cadquery/occ_impl/exporters.py index 8d023876..159a7754 100644 --- a/cadquery/occ_impl/exporters.py +++ b/cadquery/occ_impl/exporters.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals -#from OCP.Visualization import Tesselator + +# from OCP.Visualization import Tesselator import tempfile import os import sys + if sys.version_info.major == 2: import cStringIO as StringIO else: @@ -56,7 +58,7 @@ def exportShape(shape, exportType, fileLike, tolerance=0.1): The object should be already open and ready to write. The caller is responsible for closing the object """ - + from ..cq import CQ def tessellate(shape): @@ -72,7 +74,7 @@ def exportShape(shape, exportType, fileLike, tolerance=0.1): # add vertices for v in tess[0]: - mesher.addVertex(v.x,v.y,v.z) + mesher.addVertex(v.x, v.y, v.z) # add triangles for t in tess[1]: @@ -112,7 +114,7 @@ def readAndDeleteFile(fileName): return the contents as a string """ res = "" - with open(fileName, 'r') as f: + with open(fileName, "r") as f: res = "{}".format(f.read()) os.remove(fileName) @@ -148,32 +150,32 @@ class AmfWriter(object): self.tessellation = tessellation def writeAmf(self, outFile): - amf = ET.Element('amf', units=self.units) + amf = ET.Element("amf", units=self.units) # TODO: if result is a compound, we need to loop through them - object = ET.SubElement(amf, 'object', id="0") - mesh = ET.SubElement(object, 'mesh') - vertices = ET.SubElement(mesh, 'vertices') - volume = ET.SubElement(mesh, 'volume') + object = ET.SubElement(amf, "object", id="0") + mesh = ET.SubElement(object, "mesh") + vertices = ET.SubElement(mesh, "vertices") + volume = ET.SubElement(mesh, "volume") # add vertices for v in self.tessellation[0]: - vtx = ET.SubElement(vertices, 'vertex') - coord = ET.SubElement(vtx, 'coordinates') - x = ET.SubElement(coord, 'x') + vtx = ET.SubElement(vertices, "vertex") + coord = ET.SubElement(vtx, "coordinates") + x = ET.SubElement(coord, "x") x.text = str(v.x) - y = ET.SubElement(coord, 'y') + y = ET.SubElement(coord, "y") y.text = str(v.y) - z = ET.SubElement(coord, 'z') + z = ET.SubElement(coord, "z") z.text = str(v.z) # add triangles for t in self.tessellation[1]: - triangle = ET.SubElement(volume, 'triangle') - v1 = ET.SubElement(triangle, 'v1') + triangle = ET.SubElement(volume, "triangle") + v1 = ET.SubElement(triangle, "v1") v1.text = str(t[0]) - v2 = ET.SubElement(triangle, 'v2') + v2 = ET.SubElement(triangle, "v2") v2.text = str(t[1]) - v3 = ET.SubElement(triangle, 'v3') + v3 = ET.SubElement(triangle, "v3") v3.text = str(t[2]) amf = ET.ElementTree(amf).write(outFile, xml_declaration=True) @@ -211,11 +213,11 @@ class JsonMesh(object): def toJson(self): return JSON_TEMPLATE % { - 'vertices': str(self.vertices), - 'faces': str(self.faces), - 'nVertices': self.nVertices, - 'nFaces': self.nFaces - }; + "vertices": str(self.vertices), + "faces": str(self.faces), + "nVertices": self.nVertices, + "nFaces": self.nFaces, + } def makeSVGedge(e): @@ -229,20 +231,16 @@ def makeSVGedge(e): start = curve.FirstParameter() end = curve.LastParameter() - points = GCPnts_QuasiUniformDeflection(curve, - DISCRETIZATION_TOLERANCE, - start, - end) + points = GCPnts_QuasiUniformDeflection(curve, DISCRETIZATION_TOLERANCE, start, end) if points.IsDone(): - point_it = (points.Value(i + 1) for i in - range(points.NbPoints())) + point_it = (points.Value(i + 1) for i in range(points.NbPoints())) p = next(point_it) - cs.write('M{},{} '.format(p.X(), p.Y())) + cs.write("M{},{} ".format(p.X(), p.Y())) for p in point_it: - cs.write('L{},{} '.format(p.X(), p.Y())) + cs.write("L{},{} ".format(p.X(), p.Y())) return cs.getvalue() @@ -271,7 +269,7 @@ def getSVG(shape, opts=None): Export a shape to SVG """ - d = {'width': 800, 'height': 240, 'marginLeft': 200, 'marginTop': 20} + d = {"width": 800, "height": 240, "marginLeft": 200, "marginTop": 20} if opts: d.update(opts) @@ -279,17 +277,15 @@ def getSVG(shape, opts=None): # need to guess the scale and the coordinate center uom = guessUnitOfMeasure(shape) - width = float(d['width']) - height = float(d['height']) - marginLeft = float(d['marginLeft']) - marginTop = float(d['marginTop']) + width = float(d["width"]) + height = float(d["height"]) + marginLeft = float(d["marginLeft"]) + marginTop = float(d["marginTop"]) hlr = HLRBRep_Algo() hlr.Add(shape.wrapped) - projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), - DEFAULT_DIR) - ) + projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), DEFAULT_DIR)) hlr.Projector(projector) hlr.Update() @@ -330,8 +326,7 @@ def getSVG(shape, opts=None): # convert to native CQ objects visible = list(map(Shape, visible)) hidden = list(map(Shape, hidden)) - (hiddenPaths, visiblePaths) = getPaths(visible, - hidden) + (hiddenPaths, visiblePaths) = getPaths(visible, hidden) # get bounding box -- these are all in 2-d space bb = Compound.makeCompound(hidden + visible).BoundingBox() @@ -340,8 +335,10 @@ def getSVG(shape, opts=None): unitScale = min(width / bb.xlen * 0.75, height / bb.ylen * 0.75) # compute amount to translate-- move the top left into view - (xTranslate, yTranslate) = ((0 - bb.xmin) + marginLeft / - unitScale, (0 - bb.ymax) - marginTop / unitScale) + (xTranslate, yTranslate) = ( + (0 - bb.xmin) + marginLeft / unitScale, + (0 - bb.ymax) - marginTop / unitScale, + ) # compute paths ( again -- had to strip out freecad crap ) hiddenContent = "" @@ -356,19 +353,19 @@ def getSVG(shape, opts=None): { "unitScale": str(unitScale), "strokeWidth": str(1.0 / unitScale), - "hiddenContent": hiddenContent, + "hiddenContent": hiddenContent, "visibleContent": visibleContent, "xTranslate": str(xTranslate), "yTranslate": str(yTranslate), "width": str(width), "height": str(height), "textboxY": str(height - 30), - "uom": str(uom) + "uom": str(uom), } ) # svg = SVG_TEMPLATE % ( # {"content": projectedContent} - #) + # ) return svg @@ -380,7 +377,7 @@ def exportSVG(shape, fileName): """ svg = getSVG(shape.val()) - f = open(fileName, 'w') + f = open(fileName, "w") f.write(svg) f.close() @@ -465,4 +462,4 @@ SVG_TEMPLATE = """ """ -PATHTEMPLATE = "\t\t\t\n" +PATHTEMPLATE = '\t\t\t\n' diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index cdb9b4df..d42c7334 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -1,5 +1,5 @@ import math - + from OCP.gp import gp_Vec, gp_Ax1, gp_Ax3, gp_Pnt, gp_Dir, gp_Trsf, gp_GTrsf, gp, gp_XYZ from OCP.Bnd import Bnd_Box from OCP.BRepBndLib import BRepBndLib @@ -27,16 +27,16 @@ class Vector(object): if len(args) == 3: fV = gp_Vec(*args) elif len(args) == 2: - fV = gp_Vec(*args,0) + fV = gp_Vec(*args, 0) elif len(args) == 1: if isinstance(args[0], Vector): fV = gp_Vec(args[0].wrapped.XYZ()) elif isinstance(args[0], (tuple, list)): arg = args[0] - if len(arg)==3: + if len(arg) == 3: fV = gp_Vec(*arg) - elif len(arg)==2: - fV = gp_Vec(*arg,0) + elif len(arg) == 2: + fV = gp_Vec(*arg, 0) elif isinstance(args[0], (gp_Vec, gp_Pnt, gp_Dir)): fV = gp_Vec(args[0].XYZ()) elif isinstance(args[0], gp_XYZ): @@ -53,25 +53,25 @@ class Vector(object): @property def x(self): return self.wrapped.X() - + @x.setter - def x(self,value): + def x(self, value): self.wrapped.SetX(value) @property def y(self): return self.wrapped.Y() - + @y.setter - def y(self,value): + def y(self, value): self.wrapped.SetY(value) @property def z(self): return self.wrapped.Z() - + @z.setter - def z(self,value): + def z(self, value): self.wrapped.SetZ(value) @property @@ -132,16 +132,13 @@ class Vector(object): return self.wrapped.Angle(v.wrapped) def distanceToLine(self): - raise NotImplementedError( - "Have not needed this yet, but FreeCAD supports it!") + raise NotImplementedError("Have not needed this yet, but OCCT supports it!") def projectToLine(self): - raise NotImplementedError( - "Have not needed this yet, but FreeCAD supports it!") + raise NotImplementedError("Have not needed this yet, but OCCT supports it!") def distanceToPlane(self): - raise NotImplementedError( - "Have not needed this yet, but FreeCAD supports it!") + raise NotImplementedError("Have not needed this yet, but OCCT supports it!") def projectToPlane(self, plane): """ @@ -154,7 +151,7 @@ class Vector(object): base = plane.origin normal = plane.zDir - return self-normal*(((self-base).dot(normal))/normal.Length**2) + return self - normal * (((self - base).dot(normal)) / normal.Length ** 2) def __neg__(self): return self * -1 @@ -163,18 +160,19 @@ class Vector(object): return self.Length def __repr__(self): - return 'Vector: ' + str((self.x, self.y, self.z)) + return "Vector: " + str((self.x, self.y, self.z)) def __str__(self): - return 'Vector: ' + str((self.x, self.y, self.z)) + return "Vector: " + str((self.x, self.y, self.z)) def __eq__(self, other): return self.wrapped.IsEqual(other.wrapped, 0.00001, 0.00001) - ''' + + """ is not implemented in OCC def __ne__(self, other): return self.wrapped.__ne__(other) - ''' + """ def toPnt(self): @@ -222,44 +220,48 @@ class Matrix: elif isinstance(matrix, (list, tuple)): # Validate matrix size & 4x4 last row value valid_sizes = all( - (isinstance(row, (list, tuple)) and (len(row) == 4)) - for row in matrix + (isinstance(row, (list, tuple)) and (len(row) == 4)) for row in matrix ) and len(matrix) in (3, 4) if not valid_sizes: - raise TypeError("Matrix constructor requires 2d list of 4x3 or 4x4, but got: {!r}".format(matrix)) - elif (len(matrix) == 4) and (tuple(matrix[3]) != (0,0,0,1)): - raise ValueError("Expected the last row to be [0,0,0,1], but got: {!r}".format(matrix[3])) + raise TypeError( + "Matrix constructor requires 2d list of 4x3 or 4x4, but got: {!r}".format( + matrix + ) + ) + elif (len(matrix) == 4) and (tuple(matrix[3]) != (0, 0, 0, 1)): + raise ValueError( + "Expected the last row to be [0,0,0,1], but got: {!r}".format( + matrix[3] + ) + ) # Assign values to matrix self.wrapped = gp_GTrsf() - [self.wrapped.SetValue(i+1,j+1,e) - for i,row in enumerate(matrix[:3]) - for j,e in enumerate(row)] - + [ + self.wrapped.SetValue(i + 1, j + 1, e) + for i, row in enumerate(matrix[:3]) + for j, e in enumerate(row) + ] + else: - raise TypeError( - "Invalid param to matrix constructor: {}".format(matrix)) + raise TypeError("Invalid param to matrix constructor: {}".format(matrix)) def rotateX(self, angle): - self._rotate(gp.OX_s(), - angle) + self._rotate(gp.OX_s(), angle) def rotateY(self, angle): - self._rotate(gp.OY_s(), - angle) + self._rotate(gp.OY_s(), angle) def rotateZ(self, angle): - self._rotate(gp.OZ_s(), - angle) + self._rotate(gp.OZ_s(), angle) def _rotate(self, direction, angle): new = gp_Trsf() - new.SetRotation(direction, - angle) + new.SetRotation(direction, angle) self.wrapped = self.wrapped * gp_GTrsf(new) @@ -277,11 +279,12 @@ class Matrix: def transposed_list(self): """Needed by the cqparts gltf exporter """ - + trsf = self.wrapped - data = [[trsf.Value(i,j) for j in range(1,5)] for i in range(1,4)] + \ - [[0.,0.,0.,1.]] - + data = [[trsf.Value(i, j) for j in range(1, 5)] for i in range(1, 4)] + [ + [0.0, 0.0, 0.0, 1.0] + ] + return [data[j][i] for i in range(4) for j in range(4)] def __getitem__(self, rc): @@ -298,7 +301,7 @@ class Matrix: else: # gp_GTrsf doesn't provide access to the 4th row because it has # an implied value as below: - return [0., 0., 0., 1.][c] + return [0.0, 0.0, 0.0, 1.0][c] else: raise IndexError("Out of bounds access into 4x4 matrix: {!r}".format(rc)) @@ -352,95 +355,94 @@ class Plane(object): namedPlanes = { # origin, xDir, normal - 'XY': Plane(origin, (1, 0, 0), (0, 0, 1)), - 'YZ': Plane(origin, (0, 1, 0), (1, 0, 0)), - 'ZX': Plane(origin, (0, 0, 1), (0, 1, 0)), - 'XZ': Plane(origin, (1, 0, 0), (0, -1, 0)), - 'YX': Plane(origin, (0, 1, 0), (0, 0, -1)), - 'ZY': Plane(origin, (0, 0, 1), (-1, 0, 0)), - 'front': Plane(origin, (1, 0, 0), (0, 0, 1)), - 'back': Plane(origin, (-1, 0, 0), (0, 0, -1)), - 'left': Plane(origin, (0, 0, 1), (-1, 0, 0)), - 'right': Plane(origin, (0, 0, -1), (1, 0, 0)), - 'top': Plane(origin, (1, 0, 0), (0, 1, 0)), - 'bottom': Plane(origin, (1, 0, 0), (0, -1, 0)) + "XY": Plane(origin, (1, 0, 0), (0, 0, 1)), + "YZ": Plane(origin, (0, 1, 0), (1, 0, 0)), + "ZX": Plane(origin, (0, 0, 1), (0, 1, 0)), + "XZ": Plane(origin, (1, 0, 0), (0, -1, 0)), + "YX": Plane(origin, (0, 1, 0), (0, 0, -1)), + "ZY": Plane(origin, (0, 0, 1), (-1, 0, 0)), + "front": Plane(origin, (1, 0, 0), (0, 0, 1)), + "back": Plane(origin, (-1, 0, 0), (0, 0, -1)), + "left": Plane(origin, (0, 0, 1), (-1, 0, 0)), + "right": Plane(origin, (0, 0, -1), (1, 0, 0)), + "top": Plane(origin, (1, 0, 0), (0, 1, 0)), + "bottom": Plane(origin, (1, 0, 0), (0, -1, 0)), } try: return namedPlanes[stdName] except KeyError: - raise ValueError('Supported names are {}'.format( - list(namedPlanes.keys()))) + raise ValueError("Supported names are {}".format(list(namedPlanes.keys()))) @classmethod def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): - plane = Plane.named('XY', origin) + plane = Plane.named("XY", origin) plane._setPlaneDir(xDir) return plane @classmethod def YZ(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)): - plane = Plane.named('YZ', origin) + plane = Plane.named("YZ", origin) plane._setPlaneDir(xDir) return plane @classmethod def ZX(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): - plane = Plane.named('ZX', origin) + plane = Plane.named("ZX", origin) plane._setPlaneDir(xDir) return plane @classmethod def XZ(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): - plane = Plane.named('XZ', origin) + plane = Plane.named("XZ", origin) plane._setPlaneDir(xDir) return plane @classmethod def YX(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)): - plane = Plane.named('YX', origin) + plane = Plane.named("YX", origin) plane._setPlaneDir(xDir) return plane @classmethod def ZY(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): - plane = Plane.named('ZY', origin) + plane = Plane.named("ZY", origin) plane._setPlaneDir(xDir) return plane @classmethod def front(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): - plane = Plane.named('front', origin) + plane = Plane.named("front", origin) plane._setPlaneDir(xDir) return plane @classmethod def back(cls, origin=(0, 0, 0), xDir=Vector(-1, 0, 0)): - plane = Plane.named('back', origin) + plane = Plane.named("back", origin) plane._setPlaneDir(xDir) return plane @classmethod def left(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): - plane = Plane.named('left', origin) + plane = Plane.named("left", origin) plane._setPlaneDir(xDir) return plane @classmethod def right(cls, origin=(0, 0, 0), xDir=Vector(0, 0, -1)): - plane = Plane.named('right', origin) + plane = Plane.named("right", origin) plane._setPlaneDir(xDir) return plane @classmethod def top(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): - plane = Plane.named('top', origin) + plane = Plane.named("top", origin) plane._setPlaneDir(xDir) return plane @classmethod def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): - plane = Plane.named('bottom', origin) + plane = Plane.named("bottom", origin) plane._setPlaneDir(xDir) return plane @@ -458,12 +460,12 @@ class Plane(object): :return: a plane in the global space, with the xDirection of the plane in the specified direction. """ zDir = Vector(normal) - if (zDir.Length == 0.0): - raise ValueError('normal should be non null') + if zDir.Length == 0.0: + raise ValueError("normal should be non null") xDir = Vector(xDir) - if (xDir.Length == 0.0): - raise ValueError('xDir should be non null') + if xDir.Length == 0.0: + raise ValueError("xDir should be non null") self.zDir = zDir.normalized() self._setPlaneDir(xDir) @@ -489,7 +491,8 @@ class Plane(object): @property def origin(self): return self._origin -# TODO is this property rly needed -- why not handle this in the constructor + + # TODO is this property rly needed -- why not handle this in the constructor @origin.setter def origin(self, value): @@ -545,7 +548,7 @@ class Plane(object): pass - ''' + """ # TODO: also use a set of points along the wire to test as well. # TODO: would it be more efficient to create objects in the local # coordinate system, and then transform to global @@ -562,7 +565,7 @@ class Plane(object): # findOutsideBox actually inspects both ways, here we only want to # know if one is inside the other return bb == BoundBox.findOutsideBox2D(bb, tb) - ''' + """ def toLocalCoords(self, obj): """Project the provided coordinates onto this plane @@ -580,7 +583,7 @@ class Plane(object): """ from .shapes import Shape - + if isinstance(obj, Vector): return obj.transform(self.fG) elif isinstance(obj, Shape): @@ -588,7 +591,9 @@ class Plane(object): else: raise ValueError( "Don't know how to convert type {} to local coordinates".format( - type(obj))) + type(obj) + ) + ) def toWorldCoords(self, tuplePoint): """Convert a point in local coordinates to global coordinates @@ -619,19 +624,29 @@ class Plane(object): :param rotate: Vector [xDegrees, yDegrees, zDegrees] :return: a copy of this plane rotated as requested. """ + # NB: this is not a geometric Vector rotate = Vector(rotate) # Convert to radians. rotate = rotate.multiply(math.pi / 180.0) # Compute rotation matrix. - m = Matrix() - m.rotateX(rotate.x) - m.rotateY(rotate.y) - m.rotateZ(rotate.z) + T1 = gp_Trsf() + T1.SetRotation( + gp_Ax1(gp_Pnt(*(0, 0, 0)), gp_Dir(*self.xDir.toTuple())), rotate.x + ) + T2 = gp_Trsf() + T2.SetRotation( + gp_Ax1(gp_Pnt(*(0, 0, 0)), gp_Dir(*self.yDir.toTuple())), rotate.y + ) + T3 = gp_Trsf() + T3.SetRotation( + gp_Ax1(gp_Pnt(*(0, 0, 0)), gp_Dir(*self.zDir.toTuple())), rotate.z + ) + T = Matrix(gp_GTrsf(T1 * T2 * T3)) # Compute the new plane. - newXdir = self.xDir.transform(m) - newZdir = self.zDir.transform(m) + newXdir = self.xDir.transform(T) + newZdir = self.zDir.transform(T) return Plane(self.origin, newXdir, newZdir) @@ -655,7 +670,7 @@ class Plane(object): raise NotImplementedError - ''' + """ resultWires = [] for w in listOfShapes: mirrored = w.transformGeometry(rotationMatrix.wrapped) @@ -681,21 +696,19 @@ class Plane(object): resultWires.append(cadquery.Shape.cast(mirroredWire)) - return resultWires''' + return resultWires""" - def mirrorInPlane(self, listOfShapes, axis='X'): + def mirrorInPlane(self, listOfShapes, axis="X"): - local_coord_system = gp_Ax3(self.origin.toPnt(), - self.zDir.toDir(), - self.xDir.toDir()) + local_coord_system = gp_Ax3( + self.origin.toPnt(), self.zDir.toDir(), self.xDir.toDir() + ) T = gp_Trsf() - if axis == 'X': - T.SetMirror(gp_Ax1(self.origin.toPnt(), - local_coord_system.XDirection())) - elif axis == 'Y': - T.SetMirror(gp_Ax1(self.origin.toPnt(), - local_coord_system.YDirection())) + if axis == "X": + T.SetMirror(gp_Ax1(self.origin.toPnt(), local_coord_system.XDirection())) + elif axis == "Y": + T.SetMirror(gp_Ax1(self.origin.toPnt(), local_coord_system.YDirection())) else: raise NotImplementedError @@ -726,22 +739,21 @@ class Plane(object): # the double-inverting is strange, and I don't understand it. forward = Matrix() inverse = Matrix() - + forwardT = gp_Trsf() inverseT = gp_Trsf() global_coord_system = gp_Ax3() - local_coord_system = gp_Ax3(gp_Pnt(*self.origin.toTuple()), - gp_Dir(*self.zDir.toTuple()), - gp_Dir(*self.xDir.toTuple()) - ) + local_coord_system = gp_Ax3( + gp_Pnt(*self.origin.toTuple()), + gp_Dir(*self.zDir.toTuple()), + gp_Dir(*self.xDir.toTuple()), + ) - forwardT.SetTransformation(global_coord_system, - local_coord_system) + forwardT.SetTransformation(global_coord_system, local_coord_system) forward.wrapped = gp_GTrsf(forwardT) - - inverseT.SetTransformation(local_coord_system, - global_coord_system) + + inverseT.SetTransformation(local_coord_system, global_coord_system) inverse.wrapped = gp_GTrsf(inverseT) # TODO verify if this is OK @@ -751,7 +763,7 @@ class Plane(object): class BoundBox(object): - """A BoundingBox for an object or set of objects. Wraps the OCP.one""" + """A BoundingBox for an object or set of objects. Wraps the OCP one""" def __init__(self, bb): self.wrapped = bb @@ -767,11 +779,9 @@ class BoundBox(object): self.zmax = ZMax self.zlen = ZMax - ZMin - self.center = Vector((XMax + XMin) / 2, - (YMax + YMin) / 2, - (ZMax + ZMin) / 2) + self.center = Vector((XMax + XMin) / 2, (YMax + YMin) / 2, (ZMax + ZMin) / 2) - self.DiagonalLength = self.wrapped.SquareExtent()**0.5 + self.DiagonalLength = self.wrapped.SquareExtent() ** 0.5 def add(self, obj, tol=1e-8): """Returns a modified (expanded) bounding box @@ -810,30 +820,36 @@ class BoundBox(object): the built-in implementation i do not understand. """ - if (bb1.XMin < bb2.XMin and - bb1.XMax > bb2.XMax and - bb1.YMin < bb2.YMin and - bb1.YMax > bb2.YMax): + if ( + bb1.XMin < bb2.XMin + and bb1.XMax > bb2.XMax + and bb1.YMin < bb2.YMin + and bb1.YMax > bb2.YMax + ): return bb1 - if (bb2.XMin < bb1.XMin and - bb2.XMax > bb1.XMax and - bb2.YMin < bb1.YMin and - bb2.YMax > bb1.YMax): + if ( + bb2.XMin < bb1.XMin + and bb2.XMax > bb1.XMax + and bb2.YMin < bb1.YMin + and bb2.YMax > bb1.YMax + ): return bb2 return None @classmethod def _fromTopoDS(cls, shape, tol=None, optimal=True): - ''' + """ Constructs a bounding box from a TopoDS_Shape - ''' + """ tol = TOL if tol is None else tol # tol = TOL (by default) bbox = Bnd_Box() - + if optimal: - BRepBndLib.AddOptimal_s(shape, bbox) #this is 'exact' but expensive - not yet wrapped by PythonOCC + BRepBndLib.AddOptimal_s( + shape, bbox + ) # this is 'exact' but expensive - not yet wrapped by PythonOCC else: mesh = BRepMesh_IncrementalMesh(shape, tol, True) mesh.Perform() @@ -844,12 +860,14 @@ class BoundBox(object): def isInside(self, b2): """Is the provided bounding box inside this one?""" - if (b2.xmin > self.xmin and - b2.ymin > self.ymin and - b2.zmin > self.zmin and - b2.xmax < self.xmax and - b2.ymax < self.ymax and - b2.zmax < self.zmax): + if ( + b2.xmin > self.xmin + and b2.ymin > self.ymin + and b2.zmin > self.zmin + and b2.xmax < self.xmax + and b2.ymax < self.ymax + and b2.zmax < self.zmax + ): return True else: return False diff --git a/cadquery/occ_impl/importers.py b/cadquery/occ_impl/importers.py index a351e6b5..f5f27378 100644 --- a/cadquery/occ_impl/importers.py +++ b/cadquery/occ_impl/importers.py @@ -34,14 +34,14 @@ def importStep(fileName): Accepts a file name and loads the STEP file into a cadquery shape :param fileName: The path and name of the STEP file to be imported """ - + # Now read and return the shape reader = STEPControl_Reader() readStatus = reader.ReadFile(fileName) if readStatus != OCP.IFSelect.IFSelect_RetDone: raise ValueError("STEP File could not be loaded") for i in range(reader.NbRootsForTransfer()): - reader.TransferRoot(i+1) + reader.TransferRoot(i + 1) occ_shapes = [] for i in range(reader.NbShapes()): diff --git a/cadquery/occ_impl/jupyter_tools.py b/cadquery/occ_impl/jupyter_tools.py index 0c4f64b8..04e988c0 100644 --- a/cadquery/occ_impl/jupyter_tools.py +++ b/cadquery/occ_impl/jupyter_tools.py @@ -1,103 +1,8 @@ -from OCC.Display.WebGl.x3dom_renderer import X3DExporter -from OCC.Core.gp import gp_Quaternion, gp_Vec -from uuid import uuid4 -from math import tan -from xml.etree import ElementTree +from IPython.display import SVG -from .geom import BoundBox +from .exporters import toString, ExportTypes -BOILERPLATE = \ -''' - -
- - - - {src} - - -
- -''' +def display(shape): -#https://stackoverflow.com/questions/950087/how-do-i-include-a-javascript-file-in-another-javascript-file -#better if else - -ROT = (0.77,0.3,0.55,1.28) -ROT = (0.,0,0,1.) -FOV = 0.2 - -def add_x3d_boilerplate(src, height=400, center=(0,0,0), d=(0,0,15), fov=FOV, rot='{} {} {} {} '.format(*ROT)): - - return BOILERPLATE.format(src=src, - id=uuid4(), - height=height, - x=d[0], - y=d[1], - z=d[2], - x0=center[0], - y0=center[1], - z0=center[2], - fov=fov, - rot=rot) - -def x3d_display(shape, - vertex_shader=None, - fragment_shader=None, - export_edges=True, - color=(1,1,0), - specular_color=(1,1,1), - shininess=0.4, - transparency=0.4, - line_color=(0,0,0), - line_width=2., - mesh_quality=.3): - - # Export to XML tag - exporter = X3DExporter(shape, - vertex_shader, - fragment_shader, - export_edges, - color, - specular_color, - shininess, - transparency, - line_color, - line_width, - mesh_quality) - - exporter.compute() - x3d_str = exporter.to_x3dfile_string(shape_id=0) - xml_et = ElementTree.fromstring(x3d_str) - scene_tag = xml_et.find('./Scene') - - # Viewport Parameters - bb = BoundBox._fromTopoDS(shape) - d = max(bb.xlen,bb.ylen,bb.zlen) - c = bb.center - - vec = gp_Vec(0,0,d/1.5/tan(FOV/2)) - quat = gp_Quaternion(*ROT) - vec = quat*(vec) + c.wrapped - - # return boilerplate + Scene - return add_x3d_boilerplate(ElementTree.tostring(scene_tag).decode('utf-8'), - d=(vec.X(),vec.Y(),vec.Z()), - center=(c.x,c.y,c.z)) + return SVG(toString(shape, ExportTypes.SVG)) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index f7c24ff4..776b94c2 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -3,38 +3,56 @@ from .geom import Vector, BoundBox, Plane import OCP.TopAbs as ta # Tolopolgy type enum import OCP.GeomAbs as ga # Geometry type enum -from OCP.gp import (gp_Vec, gp_Pnt, gp_Ax1, gp_Ax2, gp_Ax3, gp_Dir, gp_Circ, - gp_Trsf, gp_Pln, gp_GTrsf, gp_Pnt2d, gp_Dir2d) +from OCP.gp import ( + gp_Vec, + gp_Pnt, + gp_Ax1, + gp_Ax2, + gp_Ax3, + gp_Dir, + gp_Circ, + gp_Trsf, + gp_Pln, + gp_GTrsf, + gp_Pnt2d, + gp_Dir2d, + gp_Elips, +) # collection of pints (used for spline construction) from OCP.TColgp import TColgp_Array1OfPnt, TColgp_HArray1OfPnt from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface -from OCP.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex, - BRepBuilderAPI_MakeEdge, - BRepBuilderAPI_MakeFace, - BRepBuilderAPI_MakePolygon, - BRepBuilderAPI_MakeWire, - BRepBuilderAPI_Sewing, - BRepBuilderAPI_MakeSolid, - BRepBuilderAPI_Copy, - BRepBuilderAPI_GTransform, - BRepBuilderAPI_Transform, - BRepBuilderAPI_Transformed, - BRepBuilderAPI_RightCorner, - BRepBuilderAPI_RoundCorner) +from OCP.BRepBuilderAPI import ( + BRepBuilderAPI_MakeVertex, + BRepBuilderAPI_MakeEdge, + BRepBuilderAPI_MakeFace, + BRepBuilderAPI_MakePolygon, + BRepBuilderAPI_MakeWire, + BRepBuilderAPI_Sewing, + BRepBuilderAPI_MakeSolid, + BRepBuilderAPI_Copy, + BRepBuilderAPI_GTransform, + BRepBuilderAPI_Transform, + BRepBuilderAPI_Transformed, + BRepBuilderAPI_RightCorner, + BRepBuilderAPI_RoundCorner, +) + # properties used to store mass calculation result from OCP.GProp import GProp_GProps -from OCP.BRepGProp import BRepGProp_Face, BRepGProp # used for mass calculation +from OCP.BRepGProp import BRepGProp_Face, BRepGProp # used for mass calculation from OCP.BRepLProp import BRepLProp_CLProps # local curve properties -from OCP.BRepPrimAPI import (BRepPrimAPI_MakeBox, - BRepPrimAPI_MakeCone, - BRepPrimAPI_MakeCylinder, - BRepPrimAPI_MakeTorus, - BRepPrimAPI_MakeWedge, - BRepPrimAPI_MakePrism, - BRepPrimAPI_MakeRevol, - BRepPrimAPI_MakeSphere) +from OCP.BRepPrimAPI import ( + BRepPrimAPI_MakeBox, + BRepPrimAPI_MakeCone, + BRepPrimAPI_MakeCylinder, + BRepPrimAPI_MakeTorus, + BRepPrimAPI_MakeWedge, + BRepPrimAPI_MakePrism, + BRepPrimAPI_MakeRevol, + BRepPrimAPI_MakeSphere, +) from OCP.TopExp import TopExp_Explorer # Toplogy explorer @@ -43,31 +61,28 @@ from OCP.BRep import BRep_Tool from OCP.TopoDS import TopoDS, TopoDS_Builder, TopoDS_Compound -from OCP.GC import GC_MakeArcOfCircle # geometry construction +from OCP.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse # geometry construction from OCP.GCE2d import GCE2d_MakeSegment -from OCP.GeomAPI import (GeomAPI_Interpolate, - GeomAPI_ProjectPointOnSurf) +from OCP.GeomAPI import GeomAPI_Interpolate, GeomAPI_ProjectPointOnSurf from OCP.BRepFill import BRepFill -from OCP.BRepAlgoAPI import (BRepAlgoAPI_Common, - BRepAlgoAPI_Fuse, - BRepAlgoAPI_Cut) +from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Fuse, BRepAlgoAPI_Cut from OCP.Geom import Geom_ConicalSurface, Geom_CylindricalSurface from OCP.Geom2d import Geom2d_Line -from OCP.BRepLib import BRepLib +from OCP.BRepLib import BRepLib, BRepLib_FuseEdges -from OCP.BRepOffsetAPI import (BRepOffsetAPI_ThruSections, - BRepOffsetAPI_MakePipeShell, - BRepOffsetAPI_MakeThickSolid) +from OCP.BRepOffsetAPI import ( + BRepOffsetAPI_ThruSections, + BRepOffsetAPI_MakePipeShell, + BRepOffsetAPI_MakeThickSolid, +) -from OCP.BRepFilletAPI import (BRepFilletAPI_MakeChamfer, - BRepFilletAPI_MakeFillet) +from OCP.BRepFilletAPI import BRepFilletAPI_MakeChamfer, BRepFilletAPI_MakeFillet -from OCP.TopTools import (TopTools_IndexedDataMapOfShapeListOfShape, - TopTools_ListOfShape) +from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape from OCP.TopExp import TopExp @@ -86,11 +101,13 @@ from OCP.LocOpe import LocOpe_DPrism from OCP.BRepCheck import BRepCheck_Analyzer -from OCP.Font import (Font_FontMgr, - Font_BRepTextBuilder, - Font_FA_Regular, - Font_FA_Italic, - Font_FA_Bold) +from OCP.Font import ( + Font_FontMgr, + Font_BRepTextBuilder, + Font_FA_Regular, + Font_FA_Italic, + Font_FA_Bold, +) from OCP.BRepFeat import BRepFeat_MakeDPrism @@ -100,79 +117,92 @@ from OCP.TCollection import TCollection_AsciiString from OCP.TopLoc import TopLoc_Location +from OCP.GeomAbs import GeomAbs_C0 +from OCP.GeomAbs import GeomAbs_Intersection +from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeFilling +from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin +from OCP.ShapeFix import ShapeFix_Wire + from math import pi, sqrt from functools import reduce +import warnings TOLERANCE = 1e-6 -DEG2RAD = 2 * pi / 360. -HASH_CODE_MAX = 2147483647 # max 32bit signed int, required by OCP.HashCode +DEG2RAD = 2 * pi / 360.0 +HASH_CODE_MAX = 2147483647 # max 32bit signed int, required by OCC.Core.HashCode -shape_LUT = \ - {ta.TopAbs_VERTEX: 'Vertex', - ta.TopAbs_EDGE: 'Edge', - ta.TopAbs_WIRE: 'Wire', - ta.TopAbs_FACE: 'Face', - ta.TopAbs_SHELL: 'Shell', - ta.TopAbs_SOLID: 'Solid', - ta.TopAbs_COMPOUND: 'Compound'} +shape_LUT = { + ta.TopAbs_VERTEX: "Vertex", + ta.TopAbs_EDGE: "Edge", + ta.TopAbs_WIRE: "Wire", + ta.TopAbs_FACE: "Face", + ta.TopAbs_SHELL: "Shell", + ta.TopAbs_SOLID: "Solid", + ta.TopAbs_COMPOUND: "Compound", +} -shape_properties_LUT = \ - {ta.TopAbs_VERTEX: None, - ta.TopAbs_EDGE: BRepGProp.LinearProperties_s, - ta.TopAbs_WIRE: BRepGProp.LinearProperties_s, - ta.TopAbs_FACE: BRepGProp.SurfaceProperties_s, - ta.TopAbs_SHELL: BRepGProp.SurfaceProperties_s, - ta.TopAbs_SOLID: BRepGProp.VolumeProperties_s, - ta.TopAbs_COMPOUND: BRepGProp.VolumeProperties_s} +shape_properties_LUT = { + ta.TopAbs_VERTEX: None, + ta.TopAbs_EDGE: BRepGProp.LinearProperties_s, + ta.TopAbs_WIRE: BRepGProp.LinearProperties_s, + ta.TopAbs_FACE: BRepGProp.SurfaceProperties_s, + ta.TopAbs_SHELL: BRepGProp.SurfaceProperties_s, + ta.TopAbs_SOLID: BRepGProp.VolumeProperties_s, + ta.TopAbs_COMPOUND: BRepGProp.VolumeProperties_s, +} inverse_shape_LUT = {v: k for k, v in shape_LUT.items()} -downcast_LUT = \ - {ta.TopAbs_VERTEX: TopoDS.Vertex_s, - ta.TopAbs_EDGE: TopoDS.Edge_s, - ta.TopAbs_WIRE: TopoDS.Wire_s, - ta.TopAbs_FACE: TopoDS.Face_s, - ta.TopAbs_SHELL: TopoDS.Shell_s, - ta.TopAbs_SOLID: TopoDS.Solid_s, - ta.TopAbs_COMPOUND: TopoDS.Compound_s} +downcast_LUT = { + ta.TopAbs_VERTEX: TopoDS.Vertex_s, + ta.TopAbs_EDGE: TopoDS.Edge_s, + ta.TopAbs_WIRE: TopoDS.Wire_s, + ta.TopAbs_FACE: TopoDS.Face_s, + ta.TopAbs_SHELL: TopoDS.Shell_s, + ta.TopAbs_SOLID: TopoDS.Solid_s, + ta.TopAbs_COMPOUND: TopoDS.Compound_s, +} -geom_LUT = \ - {ta.TopAbs_VERTEX: 'Vertex', - ta.TopAbs_EDGE: BRepAdaptor_Curve, - ta.TopAbs_WIRE: 'Wire', - ta.TopAbs_FACE: BRepAdaptor_Surface, - ta.TopAbs_SHELL: 'Shell', - ta.TopAbs_SOLID: 'Solid', - ta.TopAbs_COMPOUND: 'Compound'} +geom_LUT = { + ta.TopAbs_VERTEX: "Vertex", + ta.TopAbs_EDGE: BRepAdaptor_Curve, + ta.TopAbs_WIRE: "Wire", + ta.TopAbs_FACE: BRepAdaptor_Surface, + ta.TopAbs_SHELL: "Shell", + ta.TopAbs_SOLID: "Solid", + ta.TopAbs_COMPOUND: "Compound", +} -geom_LUT_FACE = \ - {ga.GeomAbs_Plane : 'PLANE', - ga.GeomAbs_Cylinder : 'CYLINDER', - ga.GeomAbs_Cone : 'CONE', - ga.GeomAbs_Sphere : 'SPHERE', - ga.GeomAbs_Torus : 'TORUS', - ga.GeomAbs_BezierSurface : 'BEZIER', - ga.GeomAbs_BSplineSurface : 'BSPLINE', - ga.GeomAbs_SurfaceOfRevolution : 'REVOLUTION', - ga.GeomAbs_SurfaceOfExtrusion : 'EXTRUSION', - ga.GeomAbs_OffsetSurface : 'OFFSET', - ga.GeomAbs_OtherSurface : 'OTHER'} +geom_LUT_FACE = { + ga.GeomAbs_Plane: "PLANE", + ga.GeomAbs_Cylinder: "CYLINDER", + ga.GeomAbs_Cone: "CONE", + ga.GeomAbs_Sphere: "SPHERE", + ga.GeomAbs_Torus: "TORUS", + ga.GeomAbs_BezierSurface: "BEZIER", + ga.GeomAbs_BSplineSurface: "BSPLINE", + ga.GeomAbs_SurfaceOfRevolution: "REVOLUTION", + ga.GeomAbs_SurfaceOfExtrusion: "EXTRUSION", + ga.GeomAbs_OffsetSurface: "OFFSET", + ga.GeomAbs_OtherSurface: "OTHER", +} -geom_LUT_EDGE = \ - {ga.GeomAbs_Line : 'LINE', - ga.GeomAbs_Circle : 'CIRCLE', - ga.GeomAbs_Ellipse : 'ELLIPSE', - ga.GeomAbs_Hyperbola : 'HYPERBOLA', - ga.GeomAbs_Parabola : 'PARABOLA', - ga.GeomAbs_BezierCurve : 'BEZIER', - ga.GeomAbs_BSplineCurve : 'BSPLINE', - ga.GeomAbs_OtherCurve : 'OTHER'} +geom_LUT_EDGE = { + ga.GeomAbs_Line: "LINE", + ga.GeomAbs_Circle: "CIRCLE", + ga.GeomAbs_Ellipse: "ELLIPSE", + ga.GeomAbs_Hyperbola: "HYPERBOLA", + ga.GeomAbs_Parabola: "PARABOLA", + ga.GeomAbs_BezierCurve: "BEZIER", + ga.GeomAbs_BSplineCurve: "BSPLINE", + ga.GeomAbs_OtherCurve: "OTHER", +} def downcast(topods_obj): - ''' + """ Downcasts a TopoDS object to suitable specialized type - ''' + """ return downcast_LUT[topods_obj.ShapeType()](topods_obj) @@ -193,47 +223,47 @@ class Shape(object): def clean(self): """Experimental clean using ShapeUpgrade""" - upgrader = ShapeUpgrade_UnifySameDomain( - self.wrapped, True, True, True) - upgrader.AllowInternalEdges(True) + upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True) + upgrader.AllowInternalEdges(False) upgrader.Build() return self.cast(upgrader.Shape()) - + def fix(self): """Try to fix shape if not valid""" if not BRepCheck_Analyzer(self.wrapped).IsValid(): sf = ShapeFix_Shape(self.wrapped) sf.Perform() fixed = downcast(sf.Shape()) - + return self.cast(fixed) - + return self @classmethod def cast(cls, obj, forConstruction=False): "Returns the right type of wrapper, given a FreeCAD object" - + tr = None # define the shape lookup table for casting - constructor_LUT = {ta.TopAbs_VERTEX: Vertex, - ta.TopAbs_EDGE: Edge, - ta.TopAbs_WIRE: Wire, - ta.TopAbs_FACE: Face, - ta.TopAbs_SHELL: Shell, - ta.TopAbs_SOLID: Solid, - ta.TopAbs_COMPOUND: Compound} + constructor_LUT = { + ta.TopAbs_VERTEX: Vertex, + ta.TopAbs_EDGE: Edge, + ta.TopAbs_WIRE: Wire, + ta.TopAbs_FACE: Face, + ta.TopAbs_SHELL: Shell, + ta.TopAbs_SOLID: Solid, + ta.TopAbs_COMPOUND: Compound, + } t = obj.ShapeType() # NB downcast is nedded to handly TopoDS_Shape types tr = constructor_LUT[t](downcast(obj)) tr.forConstruction = forConstruction - return tr - + def exportStl(self, fileName, precision=1e-5): mesh = BRepMesh_IncrementalMesh(self.wrapped, precision, True) @@ -256,7 +286,7 @@ class Shape(object): """ return BRepTools.Write_s(self.wrapped, fileName) - + def geomType(self): """ Gets the underlying geometry type @@ -290,7 +320,7 @@ class Shape(object): return geom_LUT_EDGE[tr(self.wrapped).GetType()] else: return geom_LUT_FACE[tr(self.wrapped).GetType()] - + def hashCode(self): return self.wrapped.HashCode(HASH_CODE_MAX) @@ -322,8 +352,7 @@ class Shape(object): basePointVector = Vector(basePointVector) T = gp_Trsf() - T.SetMirror(gp_Ax2(gp_Pnt(*basePointVector.toTuple()), - mirrorPlaneNormalVector)) + T.SetMirror(gp_Ax2(gp_Pnt(*basePointVector.toTuple()), mirrorPlaneNormalVector)) return self._apply_transform(T) @@ -331,15 +360,14 @@ class Shape(object): def _center_of_mass(shape): Properties = GProp_GProps() - BRepGProp.VolumeProperties_s(shape, - Properties) + BRepGProp.VolumeProperties_s(shape, Properties) return Vector(Properties.CentreOfMass()) def Center(self): - ''' + """ Center of mass - ''' + """ return Shape.centerOfMass(self) @@ -354,14 +382,15 @@ class Shape(object): :param objects: a list of objects with mass """ total_mass = sum(Shape.computeMass(o) for o in objects) - weighted_centers = [Shape.centerOfMass(o).multiply( - Shape.computeMass(o)) for o in objects] + weighted_centers = [ + Shape.centerOfMass(o).multiply(Shape.computeMass(o)) for o in objects + ] sum_wc = weighted_centers[0] for wc in weighted_centers[1:]: sum_wc = sum_wc.add(wc) - return Vector(sum_wc.multiply(1. / total_mass)) + return Vector(sum_wc.multiply(1.0 / total_mass)) @staticmethod def computeMass(obj): @@ -409,7 +438,7 @@ class Shape(object): for wc in weighted_centers[1:]: sum_wc = sum_wc.add(wc) - return Vector(sum_wc.multiply(1. / total_mass)) + return Vector(sum_wc.multiply(1.0 / total_mass)) def Closed(self): return self.wrapped.Closed() @@ -432,30 +461,33 @@ class Shape(object): def Vertices(self): - return [Vertex(i) for i in self._entities('Vertex')] + return [Vertex(i) for i in self._entities("Vertex")] def Edges(self): - return [Edge(i) for i in self._entities('Edge') if not BRep_Tool.Degenerated_s(TopoDS.Edge_s(i))] + return [ + Edge(i) + for i in self._entities("Edge") + if not BRep_Tool.Degenerated_s(TopoDS.Edge_s(i)) + ] def Compounds(self): - return [Compound(i) for i in self._entities('Compound')] + return [Compound(i) for i in self._entities("Compound")] def Wires(self): - return [Wire(i) for i in self._entities('Wire')] + return [Wire(i) for i in self._entities("Wire")] def Faces(self): - return [Face(i) for i in self._entities('Face')] + return [Face(i) for i in self._entities("Face")] def Shells(self): - return [Shell(i) for i in self._entities('Shell')] + return [Shell(i) for i in self._entities("Shell")] def Solids(self): - return [Solid(i) for i in self._entities('Solid')] + return [Solid(i) for i in self._entities("Solid")] def Area(self): Properties = GProp_GProps() - BRepGProp.SurfaceProperties_s(self.wrapped, - Properties) + BRepGProp.SurfaceProperties_s(self.wrapped, Properties) return Properties.Mass() @@ -465,9 +497,7 @@ class Shape(object): def _apply_transform(self, T): - return Shape.cast(BRepBuilderAPI_Transform(self.wrapped, - T, - True).Shape()) + return Shape.cast(BRepBuilderAPI_Transform(self.wrapped, T, True).Shape()) def rotate(self, startVector, endVector, angleDegrees): """ @@ -484,9 +514,10 @@ class Shape(object): endVector = Vector(endVector) T = gp_Trsf() - T.SetRotation(gp_Ax1(startVector.toPnt(), - (endVector - startVector).toDir()), - angleDegrees * DEG2RAD) + T.SetRotation( + gp_Ax1(startVector.toPnt(), (endVector - startVector).toDir()), + angleDegrees * DEG2RAD, + ) return self._apply_transform(T) @@ -503,8 +534,7 @@ class Shape(object): def scale(self, factor): T = gp_Trsf() - T.SetScale(gp_Pnt(), - factor) + T.SetScale(gp_Pnt(), factor) return self._apply_transform(T) @@ -519,8 +549,9 @@ class Shape(object): with all objects keeping their type """ - r = Shape.cast(BRepBuilderAPI_Transform(self.wrapped, - tMatrix.wrapped.Trsf()).Shape()) + r = Shape.cast( + BRepBuilderAPI_Transform(self.wrapped, tMatrix.wrapped.Trsf()).Shape() + ) r.forConstruction = self.forConstruction return r @@ -538,9 +569,9 @@ class Shape(object): If your transformation is only translation and rotation, it is safer to use transformShape, which doesnt change the underlying type of the geometry, but cannot handle skew transformations """ - r = Shape.cast(BRepBuilderAPI_GTransform(self.wrapped, - tMatrix.wrapped, - True).Shape()) + r = Shape.cast( + BRepBuilderAPI_GTransform(self.wrapped, tMatrix.wrapped, True).Shape() + ) r.forConstruction = self.forConstruction return r @@ -552,8 +583,7 @@ class Shape(object): """ Remove a shape from another one """ - return Shape.cast(BRepAlgoAPI_Cut(self.wrapped, - toCut.wrapped).Shape()) + return Shape.cast(BRepAlgoAPI_Cut(self.wrapped, toCut.wrapped).Shape()) def fuse(self, toFuse): """ @@ -569,16 +599,16 @@ class Shape(object): """ Construct shape intersection """ - return Shape.cast(BRepAlgoAPI_Common(self.wrapped, - toIntersect.wrapped).Shape()) + return Shape.cast(BRepAlgoAPI_Common(self.wrapped, toIntersect.wrapped).Shape()) def _repr_html_(self): """ Jupyter 3D representation support """ - from .jupyter_tools import x3d_display - return x3d_display(self.wrapped, export_edges=True) + from .jupyter_tools import display + + return display(self) class Vertex(Shape): @@ -598,9 +628,7 @@ class Vertex(Shape): def toTuple(self): geom_point = BRep_Tool.Pnt_s(self.wrapped) - return (geom_point.X(), - geom_point.Y(), - geom_point.Z()) + return (geom_point.X(), geom_point.Y(), geom_point.Z()) def Center(self): """ @@ -611,12 +639,10 @@ class Vertex(Shape): @classmethod def makeVertex(cls, x, y, z): - return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z) - ).Vertex()) + return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z)).Vertex()) class Mixin1D(object): - def Length(self): Properties = GProp_GProps() @@ -677,8 +703,8 @@ class Edge(Shape, Mixin1D): curve = self._geomAdaptor() umin, umax = curve.FirstParameter(), curve.LastParameter() - umid = (1-locationParam)*umin + locationParam*umax - + umid = (1 - locationParam) * umin + locationParam * umax + curve_props = BRepLProp_CLProps(curve, 2, curve.Tolerance()) curve_props.SetParameter(umid) @@ -691,35 +717,88 @@ class Edge(Shape, Mixin1D): def Center(self): Properties = GProp_GProps() - BRepGProp.LinearProperties_s(self.wrapped, - Properties) + BRepGProp.LinearProperties_s(self.wrapped, Properties) return Vector(Properties.CentreOfMass()) @classmethod - def makeCircle(cls, radius, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angle1=360.0, angle2=360): + def makeCircle( + cls, radius, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angle1=360.0, angle2=360 + ): """ """ pnt = Vector(pnt) dir = Vector(dir) - circle_gp = gp_Circ(gp_Ax2(pnt.toPnt(), - dir.toDir()), - radius) + circle_gp = gp_Circ(gp_Ax2(pnt.toPnt(), dir.toDir()), radius) if angle1 == angle2: # full circle case return cls(BRepBuilderAPI_MakeEdge(circle_gp).Edge()) else: # arc case - circle_geom = GC_MakeArcOfCircle(circle_gp, - angle1 * DEG2RAD, - angle2 * DEG2RAD, - True).Value() + circle_geom = GC_MakeArcOfCircle( + circle_gp, angle1 * DEG2RAD, angle2 * DEG2RAD, True + ).Value() return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) @classmethod - def makeSpline(cls, listOfVector, tangents=None, periodic=False, - tol = 1e-6): + def makeEllipse( + cls, + x_radius, + y_radius, + pnt=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + xdir=Vector(1, 0, 0), + angle1=360.0, + angle2=360.0, + sense=1, + ): + """ + Makes an Ellipse centered at the provided point, having normal in the provided direction + :param cls: + :param x_radius: x radius of the ellipse (along the x-axis of plane the ellipse should lie in) + :param y_radius: y radius of the ellipse (along the y-axis of plane the ellipse should lie in) + :param pnt: vector representing the center of the ellipse + :param dir: vector representing the direction of the plane the ellipse should lie in + :param angle1: start angle of arc + :param angle2: end angle of arc (angle2 == angle1 return closed ellipse = default) + :param sense: clockwise (-1) or counter clockwise (1) + :return: an Edge + """ + + pnt = Vector(pnt).toPnt() + dir = Vector(dir).toDir() + xdir = Vector(xdir).toDir() + + ax1 = gp_Ax1(pnt, dir) + ax2 = gp_Ax2(pnt, dir, xdir) + + if y_radius > x_radius: + # swap x and y radius and rotate by 90° afterwards to create an ellipse with x_radius < y_radius + correction_angle = 90.0 * DEG2RAD + ellipse_gp = gp_Elips(ax2, y_radius, x_radius).Rotated( + ax1, correction_angle + ) + else: + correction_angle = 0.0 + ellipse_gp = gp_Elips(ax2, x_radius, y_radius) + + if angle1 == angle2: # full ellipse case + ellipse = cls(BRepBuilderAPI_MakeEdge(ellipse_gp).Edge()) + else: # arc case + # take correction_angle into account + ellipse_geom = GC_MakeArcOfEllipse( + ellipse_gp, + angle1 * DEG2RAD - correction_angle, + angle2 * DEG2RAD - correction_angle, + sense == 1, + ).Value() + ellipse = cls(BRepBuilderAPI_MakeEdge(ellipse_geom).Edge()) + + return ellipse + + @classmethod + def makeSpline(cls, listOfVector, tangents=None, periodic=False, tol=1e-6): """ Interpolate a spline through the provided points. :param cls: @@ -731,12 +810,12 @@ class Edge(Shape, Mixin1D): """ pnts = TColgp_HArray1OfPnt(1, len(listOfVector)) for ix, v in enumerate(listOfVector): - pnts.SetValue(ix+1, v.toPnt()) + pnts.SetValue(ix + 1, v.toPnt()) spline_builder = GeomAPI_Interpolate(pnts, periodic, tol) if tangents: - v1,v2 = tangents - spline_builder.Load(v1.wrapped,v2.wrapped) + v1, v2 = tangents + spline_builder.Load(v1.wrapped, v2.wrapped) spline_builder.Perform() spline_geom = spline_builder.Curve() @@ -753,9 +832,22 @@ class Edge(Shape, Mixin1D): :param v3: end vector :return: an edge object through the three points """ - circle_geom = GC_MakeArcOfCircle(v1.toPnt(), - v2.toPnt(), - v3.toPnt()).Value() + circle_geom = GC_MakeArcOfCircle(v1.toPnt(), v2.toPnt(), v3.toPnt()).Value() + + return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) + + @classmethod + def makeTangentArc(cls, v1, v2, v3): + """ + Makes a tangent arc from point v1, in the direction of v2 and ends at + v3. + :param cls: + :param v1: start vector + :param v2: tangent vector + :param v3: end vector + :return: an edge + """ + circle_geom = GC_MakeArcOfCircle(v1.toPnt(), v2.wrapped, v3.toPnt()).Value() return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) @@ -767,8 +859,7 @@ class Edge(Shape, Mixin1D): :param v2: Vector that represents the second point :return: A linear edge between the two provided points """ - return cls(BRepBuilderAPI_MakeEdge(v1.toPnt(), - v2.toPnt()).Edge()) + return cls(BRepBuilderAPI_MakeEdge(v1.toPnt(), v2.toPnt()).Edge()) class Wire(Shape, Mixin1D): @@ -797,12 +888,27 @@ class Wire(Shape, Mixin1D): """ Attempts to build a wire that consists of the edges in the provided list :param cls: - :param listOfEdges: a list of Edge objects + :param listOfEdges: a list of Edge objects. The edges are not to be consecutive. :return: a wire with the edges assembled + :BRepBuilderAPI_MakeWire::Error() values + :BRepBuilderAPI_WireDone = 0 + :BRepBuilderAPI_EmptyWire = 1 + :BRepBuilderAPI_DisconnectedWire = 2 + :BRepBuilderAPI_NonManifoldWire = 3 """ wire_builder = BRepBuilderAPI_MakeWire() - for edge in listOfEdges: - wire_builder.Add(edge.wrapped) + + for e in listOfEdges: + wire_builder.Add(e.wrapped) + + wire_builder.Build() + + if not wire_builder.IsDone(): + w = ( + "BRepBuilderAPI_MakeWire::Error(): returns the construction status. BRepBuilderAPI_WireDone if the wire is built, or another value of the BRepBuilderAPI_WireError enumeration indicating why the construction failed = " + + str(wire_builder.Error()) + ) + warnings.warn(w) return cls(wire_builder.Wire()) @@ -820,6 +926,46 @@ class Wire(Shape, Mixin1D): w = cls.assembleEdges([circle_edge]) return w + @classmethod + def makeEllipse( + cls, + x_radius, + y_radius, + center, + normal, + xDir, + angle1=360.0, + angle2=360.0, + rotation_angle=0.0, + closed=True, + ): + """ + Makes an Ellipse centered at the provided point, having normal in the provided direction + :param x_radius: floating point major radius of the ellipse (x-axis), must be > 0 + :param y_radius: floating point minor radius of the ellipse (y-axis), must be > 0 + :param center: vector representing the center of the circle + :param normal: vector representing the direction of the plane the circle should lie in + :param angle1: start angle of arc + :param angle2: end angle of arc + :param rotation_angle: angle to rotate the created ellipse / arc + :return: Wire + """ + + ellipse_edge = Edge.makeEllipse( + x_radius, y_radius, center, normal, xDir, angle1, angle2 + ) + + if angle1 != angle2 and closed: + line = Edge.makeLine(ellipse_edge.endPoint(), ellipse_edge.startPoint()) + w = cls.assembleEdges([ellipse_edge, line]) + else: + w = cls.assembleEdges([ellipse_edge]) + + if rotation_angle != 0.0: + w = w.rotate(center, center + normal, rotation_angle) + + return w + @classmethod def makePolygon(cls, listOfVertices, forConstruction=False): # convert list of tuples into Vectors. @@ -834,8 +980,16 @@ class Wire(Shape, Mixin1D): return w @classmethod - def makeHelix(cls, pitch, height, radius, center=Vector(0, 0, 0), - dir=Vector(0, 0, 1), angle=360.0, lefthand=False): + def makeHelix( + cls, + pitch, + height, + radius, + center=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + angle=360.0, + lefthand=False, + ): """ Make a helix with a given pitch, height and radius By default a cylindrical surface is used to create the helix. If @@ -843,13 +997,14 @@ class Wire(Shape, Mixin1D): """ # 1. build underlying cylindrical/conical surface - if angle == 360.: - geom_surf = Geom_CylindricalSurface(gp_Ax3(center.toPnt(), dir.toDir()), - radius) + if angle == 360.0: + geom_surf = Geom_CylindricalSurface( + gp_Ax3(center.toPnt(), dir.toDir()), radius + ) else: - geom_surf = Geom_ConicalSurface(gp_Ax3(center.toPnt(), dir.toDir()), - angle * DEG2RAD, - radius) + geom_surf = Geom_ConicalSurface( + gp_Ax3(center.toPnt(), dir.toDir()), angle * DEG2RAD, radius + ) # 2. construct an semgent in the u,v domain if lefthand: @@ -859,8 +1014,8 @@ class Wire(Shape, Mixin1D): # 3. put it together into a wire n_turns = height / pitch - u_start = geom_line.Value(0.) - u_stop = geom_line.Value(sqrt(n_turns * ((2 * pi)**2 + pitch**2))) + u_start = geom_line.Value(0.0) + u_stop = geom_line.Value(sqrt(n_turns * ((2 * pi) ** 2 + pitch ** 2))) geom_seg = GCE2d_MakeSegment(u_start, u_stop).Value() e = BRepBuilderAPI_MakeEdge(geom_seg, geom_surf).Edge() @@ -914,8 +1069,7 @@ class Face(Shape): v = 0.5 * (v0 + v1) else: # project point on surface - projector = GeomAPI_ProjectPointOnSurf(locationVector.toPnt(), - surface) + projector = GeomAPI_ProjectPointOnSurf(locationVector.toPnt(), surface) u, v = projector.LowerDistanceParameters() @@ -928,21 +1082,87 @@ class Face(Shape): def Center(self): Properties = GProp_GProps() - BRepGProp.SurfaceProperties_s(self.wrapped, - Properties) + BRepGProp.SurfaceProperties_s(self.wrapped, Properties) return Vector(Properties.CentreOfMass()) - + def outerWire(self): - + return self.cast(BRepTools.OuterWire_s(self.wrapped)) - + def innerWires(self): - + outer = self.outerWire() - + return [w for w in self.Wires() if not w.isSame(outer)] + @classmethod + def makeNSidedSurface( + cls, + edges, + points, + continuity=GeomAbs_C0, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ): + """ + Returns a surface enclosed by a closed polygon defined by 'edges' and going through 'points'. + :param points + :type points: list of gp_Pnt + :param edges + :type edges: list of Edge + :param continuity=GeomAbs_C0 + :type continuity: OCC.Core.GeomAbs continuity condition + :param Degree = 3 (OCCT default) + :type Degree: Integer >= 2 + :param NbPtsOnCur = 15 (OCCT default) + :type: NbPtsOnCur Integer >= 15 + :param NbIter = 2 (OCCT default) + :type: NbIterInteger >= 2 + :param Anisotropie = False (OCCT default) + :type Anisotropie: Boolean + :param: Tol2d = 0.00001 (OCCT default) + :type Tol2d: float > 0 + :param Tol3d = 0.0001 (OCCT default) + :type Tol3dReal: float > 0 + :param TolAng = 0.01 (OCCT default) + :type TolAngReal: float > 0 + :param TolCurv = 0.1 (OCCT default) + :type TolCurvReal: float > 0 + :param MaxDeg = 8 (OCCT default) + :type MaxDegInteger: Integer >= 2 (?) + :param MaxSegments = 9 (OCCT default) + :type MaxSegments: Integer >= 2 (?) + """ + + n_sided = BRepOffsetAPI_MakeFilling( + degree, + nbPtsOnCur, + nbIter, + anisotropy, + tol2d, + tol3d, + tolAng, + tolCurv, + maxDeg, + maxSegments, + ) + for edge in edges: + n_sided.Add(edge.wrapped, continuity) + for pt in points: + n_sided.Add(pt) + n_sided.Build() + face = n_sided.Shape() + return cls.cast(face).fix() + @classmethod def makePlane(cls, length, width, basePnt=(0, 0, 0), dir=(0, 0, 1)): basePnt = Vector(basePnt) @@ -950,11 +1170,11 @@ class Face(Shape): pln_geom = gp_Pln(basePnt.toPnt(), dir.toDir()) - return cls(BRepBuilderAPI_MakeFace(pln_geom, - -width * 0.5, - width * 0.5, - -length * 0.5, - length * 0.5).Face()) + return cls( + BRepBuilderAPI_MakeFace( + pln_geom, -width * 0.5, width * 0.5, -length * 0.5, length * 0.5 + ).Face() + ) @classmethod def makeRuledSurface(cls, edgeOrWire1, edgeOrWire2, dist=None): @@ -965,26 +1185,24 @@ class Face(Shape): """ if isinstance(edgeOrWire1, Wire): - return cls.cast(BRepFill.Shell_s(edgeOrWire1.wrapped, - edgeOrWire1.wrapped)) + return cls.cast(BRepFill.Shell_s(edgeOrWire1.wrapped, edgeOrWire1.wrapped)) else: - return cls.cast(BRepFill.Face_s(edgeOrWire1.wrapped, - edgeOrWire1.wrapped)) + return cls.cast(BRepFill.Face_s(edgeOrWire1.wrapped, edgeOrWire1.wrapped)) @classmethod def makeFromWires(cls, outerWire, innerWires=[]): - ''' + """ Makes a planar face from one or more wires - ''' - - face_builder = BRepBuilderAPI_MakeFace(outerWire.wrapped,True) + """ + + face_builder = BRepBuilderAPI_MakeFace(outerWire.wrapped, True) for w in innerWires: face_builder.Add(w.wrapped) - + face_builder.Build() face = face_builder.Shape() - + return cls.cast(face).fix() @@ -1002,37 +1220,42 @@ class Shell(Shape): shell_builder.Add(face.wrapped) shell_builder.Perform() - - return cls.cast(shell_builder.SewedShape()) + s = shell_builder.SewedShape() + + return cls.cast(s) class Mixin3D(object): - def tessellate(self, tolerance): - - import faulthandler - faulthandler.enable() - - if not BRepTools.Triangulation_s(self.wrapped,tolerance): - BRepMesh_IncrementalMesh(self.wrapped,tolerance,True) + import faulthandler + + faulthandler.enable() + + if not BRepTools.Triangulation_s(self.wrapped, tolerance): + BRepMesh_IncrementalMesh(self.wrapped, tolerance, True) vertices = [] - triangles = [] + triangles = [] offset = 0 - + for f in self.Faces(): - + loc = TopLoc_Location() - poly = BRep_Tool.Triangulation_s(f.wrapped,loc) + poly = BRep_Tool.Triangulation_s(f.wrapped, loc) Trsf = loc.Transformation() - + # add vertices - vertices += [Vector(v.X(),v.Y(),v.Z()) for v in (v.Transformed(Trsf) for v in poly.Nodes())] - + vertices += [ + Vector(v.X(), v.Y(), v.Z()) + for v in (v.Transformed(Trsf) for v in poly.Nodes()) + ] + # add triangles - triangles += [tuple(el+offset for el in t.Get()) for t in poly.Triangles()] - + triangles += [ + tuple(el + offset for el in t.Get()) for t in poly.Triangles() + ] + offset += poly.NbNodes() return vertices, triangles @@ -1065,10 +1288,9 @@ class Mixin3D(object): # make a edge --> faces mapping edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape() - TopExp.MapShapesAndAncestors_s(self.wrapped, - ta.TopAbs_EDGE, - ta.TopAbs_FACE, - edge_face_map) + TopExp.MapShapesAndAncestors_s( + self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map + ) # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped) @@ -1082,10 +1304,9 @@ class Mixin3D(object): for e in nativeEdges: face = edge_face_map.FindFromKey(e).First() - chamfer_builder.Add(d1, - d2, - e, - TopoDS.Face_s(face)) # NB: edge_face_map return a generic TopoDS_Shape + chamfer_builder.Add( + d1, d2, e, TopoDS.Face_s(face) + ) # NB: edge_face_map return a generic TopoDS_Shape return self.__class__(chamfer_builder.Shape()) def shell(self, faceList, thickness, tolerance=0.0001): @@ -1102,10 +1323,9 @@ class Mixin3D(object): for f in faceList: occ_faces_list.Append(f.wrapped) - shell_builder = BRepOffsetAPI_MakeThickSolid(self.wrapped, - occ_faces_list, - thickness, - tolerance) + shell_builder = BRepOffsetAPI_MakeThickSolid( + self.wrapped, occ_faces_list, thickness, tolerance + ) shell_builder.Build() @@ -1126,29 +1346,137 @@ class Mixin3D(object): solid_classifier = BRepClass3d_SolidClassifier(self.wrapped) solid_classifier.Perform(gp_Pnt(*point), tolerance) - return (solid_classifier.State() == ta.TopAbs_IN or - solid_classifier.IsOnAFace()) + return solid_classifier.State() == ta.TopAbs_IN or solid_classifier.IsOnAFace() class Solid(Shape, Mixin3D): """ a single solid """ - + + @classmethod + def interpPlate( + cls, + surf_edges, + surf_pts, + thickness, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ): + """ + Returns a plate surface that is 'thickness' thick, enclosed by 'surf_edge_pts' points, and going through 'surf_pts' points. + + :param surf_edges + :type 1 surf_edges: list of [x,y,z] float ordered coordinates + :type 2 surf_edges: list of ordered or unordered CadQuery wires + :param surf_pts = [] (uses only edges if []) + :type surf_pts: list of [x,y,z] float coordinates + :param thickness = 0 (returns 2D surface if 0) + :type thickness: float (may be negative or positive depending on thicknening direction) + :param Degree = 3 (OCCT default) + :type Degree: Integer >= 2 + :param NbPtsOnCur = 15 (OCCT default) + :type: NbPtsOnCur Integer >= 15 + :param NbIter = 2 (OCCT default) + :type: NbIterInteger >= 2 + :param Anisotropie = False (OCCT default) + :type Anisotropie: Boolean + :param: Tol2d = 0.00001 (OCCT default) + :type Tol2d: float > 0 + :param Tol3d = 0.0001 (OCCT default) + :type Tol3dReal: float > 0 + :param TolAng = 0.01 (OCCT default) + :type TolAngReal: float > 0 + :param TolCurv = 0.1 (OCCT default) + :type TolCurvReal: float > 0 + :param MaxDeg = 8 (OCCT default) + :type MaxDegInteger: Integer >= 2 (?) + :param MaxSegments = 9 (OCCT default) + :type MaxSegments: Integer >= 2 (?) + """ + + # POINTS CONSTRAINTS: list of (x,y,z) points, optional. + pts_array = [gp_Pnt(*pt) for pt in surf_pts] + + # EDGE CONSTRAINTS + # If a list of wires is provided, make a closed wire + if not isinstance(surf_edges, list): + surf_edges = [o.vals()[0] for o in surf_edges.all()] + surf_edges = Wire.assembleEdges(surf_edges) + w = surf_edges.wrapped + + # If a list of (x,y,z) points provided, build closed polygon + if isinstance(surf_edges, list): + e_array = [Vector(*e) for e in surf_edges] + wire_builder = BRepBuilderAPI_MakePolygon() + for e in e_array: # Create polygon from edges + wire_builder.Add(e.toPnt()) + wire_builder.Close() + w = wire_builder.Wire() + + edges = [i for i in Shape(w).Edges()] + + # MAKE SURFACE + continuity = GeomAbs_C0 # Fixed, changing to anything else crashes. + face = Face.makeNSidedSurface( + edges, + pts_array, + continuity, + degree, + nbPtsOnCur, + nbIter, + anisotropy, + tol2d, + tol3d, + tolAng, + tolCurv, + maxDeg, + maxSegments, + ) + + # THICKEN SURFACE + if ( + abs(thickness) > 0 + ): # abs() because negative values are allowed to set direction of thickening + solid = BRepOffset_MakeOffset() + solid.Initialize( + face.wrapped, + thickness, + 1.0e-5, + BRepOffset_Skin, + False, + False, + GeomAbs_Intersection, + True, + ) # The last True is important to make solid + solid.MakeOffsetShape() + return cls(solid.Shape()) + else: # Return 2D surface only + return face + @classmethod def isSolid(cls, obj): """ Returns true if the object is a FreeCAD solid, false otherwise """ - if hasattr(obj, 'ShapeType'): - if obj.ShapeType == 'Solid' or \ - (obj.ShapeType == 'Compound' and len(obj.Solids) > 0): + if hasattr(obj, "ShapeType"): + if obj.ShapeType == "Solid" or ( + obj.ShapeType == "Compound" and len(obj.Solids) > 0 + ): return True return False - + @classmethod def makeSolid(cls, shell): - + return cls(BRepBuilderAPI_MakeSolid(shell.wrapped).Solid()) @classmethod @@ -1157,53 +1485,77 @@ class Solid(Shape, Mixin3D): makeBox(length,width,height,[pnt,dir]) -- Make a box located in pnt with the dimensions (length,width,height) By default pnt=Vector(0,0,0) and dir=Vector(0,0,1)' """ - return cls(BRepPrimAPI_MakeBox(gp_Ax2(pnt.toPnt(), - dir.toDir()), - length, - width, - height).Shape()) + return cls( + BRepPrimAPI_MakeBox( + gp_Ax2(pnt.toPnt(), dir.toDir()), length, width, height + ).Shape() + ) @classmethod - def makeCone(cls, radius1, radius2, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360): + def makeCone( + cls, + radius1, + radius2, + height, + pnt=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + angleDegrees=360, + ): """ Make a cone with given radii and height By default pnt=Vector(0,0,0), dir=Vector(0,0,1) and angle=360' """ - return cls(BRepPrimAPI_MakeCone(gp_Ax2(pnt.toPnt(), - dir.toDir()), - radius1, - radius2, - height, - angleDegrees * DEG2RAD).Shape()) + return cls( + BRepPrimAPI_MakeCone( + gp_Ax2(pnt.toPnt(), dir.toDir()), + radius1, + radius2, + height, + angleDegrees * DEG2RAD, + ).Shape() + ) @classmethod - def makeCylinder(cls, radius, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360): + def makeCylinder( + cls, radius, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360 + ): """ makeCylinder(radius,height,[pnt,dir,angle]) -- Make a cylinder with a given radius and height By default pnt=Vector(0,0,0),dir=Vector(0,0,1) and angle=360' """ - return cls(BRepPrimAPI_MakeCylinder(gp_Ax2(pnt.toPnt(), - dir.toDir()), - radius, - height, - angleDegrees * DEG2RAD).Shape()) + return cls( + BRepPrimAPI_MakeCylinder( + gp_Ax2(pnt.toPnt(), dir.toDir()), radius, height, angleDegrees * DEG2RAD + ).Shape() + ) @classmethod - def makeTorus(cls, radius1, radius2, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None): + def makeTorus( + cls, + radius1, + radius2, + pnt=None, + dir=None, + angleDegrees1=None, + angleDegrees2=None, + ): """ makeTorus(radius1,radius2,[pnt,dir,angle1,angle2,angle]) -- Make a torus with agiven radii and angles By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0 ,angle1=360 and angle=360' """ - return cls(BRepPrimAPI_MakeTorus(gp_Ax2(pnt.toPnt(), - dir.toDir()), - radius1, - radius2, - angleDegrees1 * DEG2RAD, - angleDegrees2 * DEG2RAD).Shape()) + return cls( + BRepPrimAPI_MakeTorus( + gp_Ax2(pnt.toPnt(), dir.toDir()), + radius1, + radius2, + angleDegrees1 * DEG2RAD, + angleDegrees2 * DEG2RAD, + ).Shape() + ) @classmethod def makeLoft(cls, listOfWire, ruled=False): @@ -1225,36 +1577,52 @@ class Solid(Shape, Mixin3D): return cls(loft_builder.Shape()) @classmethod - def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)): + def makeWedge( + cls, + dx, + dy, + dz, + xmin, + zmin, + xmax, + zmax, + pnt=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + ): """ Make a wedge located in pnt By default pnt=Vector(0,0,0) and dir=Vector(0,0,1) """ - return cls(BRepPrimAPI_MakeWedge(gp_Ax2(pnt.toPnt(), - dir.toDir()), - xmin, - ymin, - zmin, - z2min, - x2min, - xmax, - ymax, - zmax, - z2max, - x2max).Solid()) + + return cls( + BRepPrimAPI_MakeWedge( + gp_Ax2(pnt.toPnt(), dir.toDir()), dx, dy, dz, xmin, zmin, xmax, zmax + ).Solid() + ) @classmethod - def makeSphere(cls, radius, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees1=0, angleDegrees2=90, angleDegrees3=360): + def makeSphere( + cls, + radius, + pnt=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + angleDegrees1=0, + angleDegrees2=90, + angleDegrees3=360, + ): """ Make a sphere with a given radius By default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360 """ - return cls(BRepPrimAPI_MakeSphere(gp_Ax2(pnt.toPnt(), - dir.toDir()), - radius, - angleDegrees1 * DEG2RAD, - angleDegrees2 * DEG2RAD, - angleDegrees3 * DEG2RAD).Shape()) + return cls( + BRepPrimAPI_MakeSphere( + gp_Ax2(pnt.toPnt(), dir.toDir()), + radius, + angleDegrees1 * DEG2RAD, + angleDegrees2 * DEG2RAD, + angleDegrees3 * DEG2RAD, + ).Shape() + ) @classmethod def _extrudeAuxSpine(cls, wire, spine, auxSpine): @@ -1269,7 +1637,9 @@ class Solid(Shape, Mixin3D): return extrude_builder.Shape() @classmethod - def extrudeLinearWithRotation(cls, outerWire, innerWires, vecCenter, vecNormal, angleDegrees): + def extrudeLinearWithRotation( + cls, outerWire, innerWires, vecCenter, vecNormal, angleDegrees + ): """ Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector. @@ -1292,26 +1662,25 @@ class Solid(Shape, Mixin3D): """ # make straight spine straight_spine_e = Edge.makeLine(vecCenter, vecCenter.add(vecNormal)) - straight_spine_w = Wire.combine([straight_spine_e, ]).wrapped + straight_spine_w = Wire.combine([straight_spine_e,]).wrapped # make an auxliliary spine - pitch = 360. / angleDegrees * vecNormal.Length + pitch = 360.0 / angleDegrees * vecNormal.Length radius = 1 - aux_spine_w = Wire.makeHelix(pitch, - vecNormal.Length, - radius, - center=vecCenter, - dir=vecNormal).wrapped + aux_spine_w = Wire.makeHelix( + pitch, vecNormal.Length, radius, center=vecCenter, dir=vecNormal + ).wrapped # extrude the outer wire - outer_solid = cls._extrudeAuxSpine(outerWire.wrapped, - straight_spine_w, - aux_spine_w) + outer_solid = cls._extrudeAuxSpine( + outerWire.wrapped, straight_spine_w, aux_spine_w + ) # extrude inner wires - inner_solids = [cls._extrudeAuxSpine(w.wrapped, - straight_spine_w. - aux_spine_w) for w in innerWires] + inner_solids = [ + cls._extrudeAuxSpine(w.wrapped, straight_spine_w.aux_spine_w) + for w in innerWires + ] # combine the inner solids into compund inner_comp = Compound._makeCompound(inner_solids) @@ -1344,21 +1713,19 @@ class Solid(Shape, Mixin3D): reliable. """ - if taper==0: + if taper == 0: face = Face.makeFromWires(outerWire, innerWires) - prism_builder = BRepPrimAPI_MakePrism( - face.wrapped, vecNormal.wrapped, True) + prism_builder = BRepPrimAPI_MakePrism(face.wrapped, vecNormal.wrapped, True) else: face = Face.makeFromWires(outerWire) faceNormal = face.normalAt() - d = 1 if vecNormal.getAngle(faceNormal)<90 * DEG2RAD else -1 - prism_builder = LocOpe_DPrism(face.wrapped, - d * vecNormal.Length, - d * taper * DEG2RAD) + d = 1 if vecNormal.getAngle(faceNormal) < 90 * DEG2RAD else -1 + prism_builder = LocOpe_DPrism( + face.wrapped, d * vecNormal.Length, d * taper * DEG2RAD + ) return cls(prism_builder.Shape()) - @classmethod def revolve(cls, outerWire, innerWires, angleDegrees, axisStart, axisEnd): """ @@ -1390,20 +1757,28 @@ class Solid(Shape, Mixin3D): v1 = Vector(axisStart) v2 = Vector(axisEnd) v2 = v2 - v1 - revol_builder = BRepPrimAPI_MakeRevol(face.wrapped, - gp_Ax1(v1.toPnt(), v2.toDir()), - angleDegrees * DEG2RAD, - True) + revol_builder = BRepPrimAPI_MakeRevol( + face.wrapped, gp_Ax1(v1.toPnt(), v2.toDir()), angleDegrees * DEG2RAD, True + ) return cls(revol_builder.Shape()) - _transModeDict = {'transformed' : BRepBuilderAPI_Transformed, - 'round' : BRepBuilderAPI_RoundCorner, - 'right' : BRepBuilderAPI_RightCorner} + _transModeDict = { + "transformed": BRepBuilderAPI_Transformed, + "round": BRepBuilderAPI_RoundCorner, + "right": BRepBuilderAPI_RightCorner, + } @classmethod - def sweep(cls, outerWire, innerWires, path, makeSolid=True, isFrenet=False, - transitionMode='transformed'): + def sweep( + cls, + outerWire, + innerWires, + path, + makeSolid=True, + isFrenet=False, + transitionMode="transformed", + ): """ Attempt to sweep the list of wires into a prismatic solid along the provided path @@ -1417,11 +1792,11 @@ class Solid(Shape, Mixin3D): Possible values are {'transformed','round', 'right'} (default: 'right'). :return: a Solid object """ - if path.ShapeType() == 'Edge': - path = Wire.assembleEdges([path, ]) + if path.ShapeType() == "Edge": + path = Wire.assembleEdges([path,]) shapes = [] - for w in [outerWire]+innerWires: + for w in [outerWire] + innerWires: builder = BRepOffsetAPI_MakePipeShell(path.wrapped) builder.SetMode(isFrenet) builder.SetTransitionMode(cls._transModeDict[transitionMode]) @@ -1433,10 +1808,10 @@ class Solid(Shape, Mixin3D): shapes.append(cls(builder.Shape())) - rv,inner_shapes = shapes[0],shapes[1:] + rv, inner_shapes = shapes[0], shapes[1:] if inner_shapes: - inner_shapes = reduce(lambda a,b: a.fuse(b),inner_shapes) + inner_shapes = reduce(lambda a, b: a.fuse(b), inner_shapes) rv = rv.cut(inner_shapes) return rv @@ -1450,8 +1825,8 @@ class Solid(Shape, Mixin3D): :param path: The wire to sweep the face resulting from the wires over :return: a Solid object """ - if path.ShapeType() == 'Edge': - path = Wire.assembleEdges([path, ]) + if path.ShapeType() == "Edge": + path = Wire.assembleEdges([path,]) builder = BRepOffsetAPI_MakePipeShell(path.wrapped) @@ -1466,8 +1841,7 @@ class Solid(Shape, Mixin3D): return cls(builder.Shape()) - def dprism(self, basis, profiles, depth=None, taper=0, thruAll=True, - additive=True): + def dprism(self, basis, profiles, depth=None, taper=0, thruAll=True, additive=True): """ Make a prismatic feature (additive or subtractive) @@ -1482,13 +1856,10 @@ class Solid(Shape, Mixin3D): shape = self.wrapped basis = basis.wrapped for p in sorted_profiles: - face = Face.makeFromWires(p[0],p[1:]) - feat = BRepFeat_MakeDPrism(shape, - face.wrapped, - basis, - taper*DEG2RAD, - additive, - False) + face = Face.makeFromWires(p[0], p[1:]) + feat = BRepFeat_MakeDPrism( + shape, face.wrapped, basis, taper * DEG2RAD, additive, False + ) if thruAll: feat.PerformThruAll() @@ -1499,21 +1870,22 @@ class Solid(Shape, Mixin3D): return self.__class__(shape) + class Compound(Shape, Mixin3D): """ a collection of disconnected solids """ - + @staticmethod def _makeCompound(listOfShapes): - + comp = TopoDS_Compound() comp_builder = TopoDS_Builder() comp_builder.MakeCompound(comp) for s in listOfShapes: comp_builder.Add(comp, s) - + return comp @classmethod @@ -1525,48 +1897,58 @@ class Compound(Shape, Mixin3D): return cls(cls._makeCompound((s.wrapped for s in listOfShapes))) @classmethod - def makeText(cls, text, size, height, font="Arial", kind='regular', - halign='center', valign='center',position=Plane.XY()): + def makeText( + cls, + text, + size, + height, + font="Arial", + kind="regular", + halign="center", + valign="center", + position=Plane.XY(), + ): """ Create a 3D text """ - font_kind = {'regular' : Font_FA_Regular, - 'bold' : Font_FA_Bold, - 'italic' : Font_FA_Italic}[kind] - + font_kind = { + "regular": Font_FA_Regular, + "bold": Font_FA_Bold, + "italic": Font_FA_Italic, + }[kind] + mgr = Font_FontMgr.GetInstance_s() - font = mgr.FindFont(TCollection_AsciiString(font), - font_kind) + font = mgr.FindFont(TCollection_AsciiString(font), font_kind) builder = Font_BRepTextBuilder() - text_flat = Shape(builder.Perform(font.FontPath(font_kind).ToCString(), - size, - font_kind, - text)) - + text_flat = Shape( + builder.Perform(font.FontPath(font_kind).ToCString(), size, font_kind, text) + ) + bb = text_flat.BoundingBox() - + t = Vector() - - if halign == 'center': - t.x = -bb.xlen/2 - elif halign == 'right': + + if halign == "center": + t.x = -bb.xlen / 2 + elif halign == "right": t.x = -bb.xlen - - if valign == 'center': - t.y = -bb.ylen/2 - elif valign == 'top': + + if valign == "center": + t.y = -bb.ylen / 2 + elif valign == "top": t.y = -bb.ylen - + text_flat = text_flat.translate(t) - - vecNormal = text_flat.Faces()[0].normalAt()*height + + vecNormal = text_flat.Faces()[0].normalAt() * height text_3d = BRepPrimAPI_MakePrism(text_flat.wrapped, vecNormal.wrapped) return cls(text_3d.Shape()).transformShape(position.rG) + def sortWiresByBuildOrder(wireList, result={}): """Tries to determine how wires should be combined into faces. @@ -1586,12 +1968,14 @@ def sortWiresByBuildOrder(wireList, result={}): # check if we have something to sort at all if len(wireList) < 2: - return [wireList, ] + return [ + wireList, + ] # make a Face, NB: this might return a compound of faces faces = Face.makeFromWires(wireList[0], wireList[1:]) - - rv = [] + + rv = [] for face in faces.Faces(): rv.append([face.outerWire(),] + face.innerWires()) diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 8ead7737..f4eb361d 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -21,9 +21,22 @@ import re import math from cadquery import Vector, Edge, Vertex, Face, Solid, Shell, Compound from collections import defaultdict -from pyparsing import Literal, Word, nums, Optional, Combine, oneOf, upcaseTokens,\ - CaselessLiteral, Group, infixNotation, opAssoc, Forward,\ - ZeroOrMore, Keyword +from pyparsing import ( + Literal, + Word, + nums, + Optional, + Combine, + oneOf, + upcaseTokens, + CaselessLiteral, + Group, + infixNotation, + opAssoc, + Forward, + ZeroOrMore, + Keyword, +) from functools import reduce @@ -81,7 +94,6 @@ class NearestToPointSelector(Selector): self.pnt = pnt def filter(self, objectList): - def dist(tShape): return tShape.Center().sub(Vector(*self.pnt)).Length # if tShape.ShapeType == 'Vertex': @@ -121,15 +133,18 @@ class BoxSelector(Selector): def isInsideBox(p): # using XOR for checking if x/y/z is in between regardless # of order of x/y/z0 and x/y/z1 - return ((p.x < x0) ^ (p.x < x1)) and \ - ((p.y < y0) ^ (p.y < y1)) and \ - ((p.z < z0) ^ (p.z < z1)) + return ( + ((p.x < x0) ^ (p.x < x1)) + and ((p.y < y0) ^ (p.y < y1)) + and ((p.z < z0) ^ (p.z < z1)) + ) for o in objectList: if self.test_boundingbox: bb = o.BoundingBox() - if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and \ - isInsideBox(Vector(bb.xmax, bb.ymax, bb.zmax)): + if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and isInsideBox( + Vector(bb.xmax, bb.ymax, bb.zmax) + ): result.append(o) else: if isInsideBox(o.Center()): @@ -168,7 +183,9 @@ class BaseDirSelector(Selector): if self.test(normal): r.append(o) - elif type(o) == Edge and (o.geomType() == 'LINE' or o.geomType() == 'PLANE'): + elif type(o) == Edge and ( + o.geomType() == "LINE" or o.geomType() == "PLANE" + ): # an edge is parallel to a direction if its underlying geometry is plane or line tangent = o.tangentAt() if self.test(tangent): @@ -247,8 +264,7 @@ class PerpendicularDirSelector(BaseDirSelector): def test(self, vec): angle = self.direction.getAngle(vec) - r = (abs(angle) < self.TOLERANCE) or ( - abs(angle - math.pi) < self.TOLERANCE) + r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE) return not r @@ -314,17 +330,16 @@ class DirectionMinMaxSelector(Selector): self.TOLERANCE = tolerance def filter(self, objectList): - def distance(tShape): return tShape.Center().dot(self.vector) # import OrderedDict from collections import OrderedDict + # make and distance to object dict objectDict = {distance(el): el for el in objectList} # transform it into an ordered dict - objectDict = OrderedDict(sorted(list(objectDict.items()), - key=lambda x: x[0])) + objectDict = OrderedDict(sorted(list(objectDict.items()), key=lambda x: x[0])) # find out the max/min distance if self.directionMax: @@ -370,8 +385,9 @@ class DirectionNthSelector(ParallelDirSelector): objectDict[round(distance(el), digits)].append(el) # choose the Nth unique rounded distance - nth_distance = sorted(list(objectDict.keys()), - reverse=not self.directionMax)[self.N] + nth_distance = sorted(list(objectDict.keys()), reverse=not self.directionMax)[ + self.N + ] # map back to original objects and return return objectDict[nth_distance] @@ -388,8 +404,9 @@ class BinarySelector(Selector): self.right = right def filter(self, objectList): - return self.filterResults(self.left.filter(objectList), - self.right.filter(objectList)) + return self.filterResults( + self.left.filter(objectList), self.right.filter(objectList) + ) def filterResults(self, r_left, r_right): raise NotImplementedError @@ -445,52 +462,56 @@ def _makeGrammar(): """ # float definition - point = Literal('.') - plusmin = Literal('+') | Literal('-') + point = Literal(".") + plusmin = Literal("+") | Literal("-") number = Word(nums) integer = Combine(Optional(plusmin) + number) floatn = Combine(integer + Optional(point + Optional(number))) # vector definition - lbracket = Literal('(') - rbracket = Literal(')') - comma = Literal(',') - vector = Combine(lbracket + floatn('x') + comma + - floatn('y') + comma + floatn('z') + rbracket) + lbracket = Literal("(") + rbracket = Literal(")") + comma = Literal(",") + vector = Combine( + lbracket + floatn("x") + comma + floatn("y") + comma + floatn("z") + rbracket + ) # direction definition - simple_dir = oneOf(['X', 'Y', 'Z', 'XY', 'XZ', 'YZ']) - direction = simple_dir('simple_dir') | vector('vector_dir') + simple_dir = oneOf(["X", "Y", "Z", "XY", "XZ", "YZ"]) + direction = simple_dir("simple_dir") | vector("vector_dir") # CQ type definition - cqtype = oneOf(['Plane', 'Cylinder', 'Sphere', 'Cone', 'Line', 'Circle', 'Arc'], - caseless=True) + cqtype = oneOf( + ["Plane", "Cylinder", "Sphere", "Cone", "Line", "Circle", "Arc"], caseless=True + ) cqtype = cqtype.setParseAction(upcaseTokens) # type operator - type_op = Literal('%') + type_op = Literal("%") # direction operator - direction_op = oneOf(['>', '<']) + direction_op = oneOf([">", "<"]) # index definition - ix_number = Group(Optional('-') + Word(nums)) - lsqbracket = Literal('[').suppress() - rsqbracket = Literal(']').suppress() + ix_number = Group(Optional("-") + Word(nums)) + lsqbracket = Literal("[").suppress() + rsqbracket = Literal("]").suppress() - index = lsqbracket + ix_number('index') + rsqbracket + index = lsqbracket + ix_number("index") + rsqbracket # other operators - other_op = oneOf(['|', '#', '+', '-']) + other_op = oneOf(["|", "#", "+", "-"]) # named view - named_view = oneOf(['front', 'back', 'left', 'right', 'top', 'bottom']) + named_view = oneOf(["front", "back", "left", "right", "top", "bottom"]) - return direction('only_dir') | \ - (type_op('type_op') + cqtype('cq_type')) | \ - (direction_op('dir_op') + direction('dir') + Optional(index)) | \ - (other_op('other_op') + direction('dir')) | \ - named_view('named_view') + return ( + direction("only_dir") + | (type_op("type_op") + cqtype("cq_type")) + | (direction_op("dir_op") + direction("dir") + Optional(index)) + | (other_op("other_op") + direction("dir")) + | named_view("named_view") + ) _grammar = _makeGrammar() # make a grammar instance @@ -506,33 +527,34 @@ class _SimpleStringSyntaxSelector(Selector): # define all token to object mappings self.axes = { - 'X': Vector(1, 0, 0), - 'Y': Vector(0, 1, 0), - 'Z': Vector(0, 0, 1), - 'XY': Vector(1, 1, 0), - 'YZ': Vector(0, 1, 1), - 'XZ': Vector(1, 0, 1) + "X": Vector(1, 0, 0), + "Y": Vector(0, 1, 0), + "Z": Vector(0, 0, 1), + "XY": Vector(1, 1, 0), + "YZ": Vector(0, 1, 1), + "XZ": Vector(1, 0, 1), } self.namedViews = { - 'front': (Vector(0, 0, 1), True), - 'back': (Vector(0, 0, 1), False), - 'left': (Vector(1, 0, 0), False), - 'right': (Vector(1, 0, 0), True), - 'top': (Vector(0, 1, 0), True), - 'bottom': (Vector(0, 1, 0), False) + "front": (Vector(0, 0, 1), True), + "back": (Vector(0, 0, 1), False), + "left": (Vector(1, 0, 0), False), + "right": (Vector(1, 0, 0), True), + "top": (Vector(0, 1, 0), True), + "bottom": (Vector(0, 1, 0), False), } self.operatorMinMax = { - '>': True, - '<': False, + ">": True, + "<": False, } self.operator = { - '+': DirectionSelector, - '-': lambda v: DirectionSelector(-v), - '#': PerpendicularDirSelector, - '|': ParallelDirSelector} + "+": DirectionSelector, + "-": lambda v: DirectionSelector(-v), + "#": PerpendicularDirSelector, + "|": ParallelDirSelector, + } self.parseResults = parseResults self.mySelector = self._chooseSelector(parseResults) @@ -541,23 +563,25 @@ class _SimpleStringSyntaxSelector(Selector): """ Sets up the underlying filters accordingly """ - if 'only_dir' in pr: + if "only_dir" in pr: vec = self._getVector(pr) return DirectionSelector(vec) - elif 'type_op' in pr: + elif "type_op" in pr: return TypeSelector(pr.cq_type) - elif 'dir_op' in pr: + elif "dir_op" in pr: vec = self._getVector(pr) minmax = self.operatorMinMax[pr.dir_op] - if 'index' in pr: - return DirectionNthSelector(vec, int(''.join(pr.index.asList())), minmax) + if "index" in pr: + return DirectionNthSelector( + vec, int("".join(pr.index.asList())), minmax + ) else: return DirectionMinMaxSelector(vec, minmax) - elif 'other_op' in pr: + elif "other_op" in pr: vec = self._getVector(pr) return self.operator[pr.other_op](vec) @@ -569,7 +593,7 @@ class _SimpleStringSyntaxSelector(Selector): """ Translate parsed vector string into a CQ Vector """ - if 'vector_dir' in pr: + if "vector_dir" in pr: vec = pr.vector_dir return Vector(float(vec.x), float(vec.y), float(vec.z)) else: @@ -590,10 +614,10 @@ def _makeExpressionGrammar(atom): """ # define operators - and_op = Literal('and') - or_op = Literal('or') - delta_op = oneOf(['exc', 'except']) - not_op = Literal('not') + and_op = Literal("and") + or_op = Literal("or") + delta_op = oneOf(["exc", "except"]) + not_op = Literal("not") def atom_callback(res): return _SimpleStringSyntaxSelector(res) @@ -622,11 +646,15 @@ def _makeExpressionGrammar(atom): return InverseSelector(right) # construct the final grammar and set all the callbacks - expr = infixNotation(atom, - [(and_op, 2, opAssoc.LEFT, and_callback), - (or_op, 2, opAssoc.LEFT, or_callback), - (delta_op, 2, opAssoc.LEFT, exc_callback), - (not_op, 1, opAssoc.RIGHT, not_callback)]) + expr = infixNotation( + atom, + [ + (and_op, 2, opAssoc.LEFT, and_callback), + (or_op, 2, opAssoc.LEFT, or_callback), + (delta_op, 2, opAssoc.LEFT, exc_callback), + (not_op, 1, opAssoc.RIGHT, not_callback), + ], + ) return expr @@ -690,8 +718,7 @@ class StringSyntaxSelector(Selector): Feed the input string through the parser and construct an relevant complex selector object """ self.selectorString = selectorString - parse_result = _expression_grammar.parseString(selectorString, - parseAll=True) + parse_result = _expression_grammar.parseString(selectorString, parseAll=True) self.mySelector = parse_result.asList()[0] def filter(self, objectList): diff --git a/conda/meta.yaml b/conda/meta.yaml index 2fe8c171..ce663fda 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -20,11 +20,12 @@ requirements: - pyparsing 2.* test: + requires: + - pytest source_files: - - runtests.py - tests/ commands: - - python runtests.py + - pytest -v about: summary: CadQuery fork based on PythonOCC diff --git a/doc/apireference.rst b/doc/apireference.rst index e83e7b93..7cdd2928 100644 --- a/doc/apireference.rst +++ b/doc/apireference.rst @@ -54,6 +54,7 @@ All 2-d operations require a **Workplane** object to be created. Workplane.threePointArc Workplane.sagittaArc Workplane.radiusArc + Workplane.tangentArcPoint Workplane.rotateAndCopy Workplane.mirrorY Workplane.mirrorX diff --git a/doc/conf.py b/doc/conf.py index fb76407e..4e8517aa 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -13,71 +13,77 @@ import sys, os import os.path -#print "working path is %s" % os.getcwd() -#sys.path.append("../cadquery") + +# print "working path is %s" % os.getcwd() +# sys.path.append("../cadquery") import cadquery -#settings._target = None +# settings._target = None # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.autosummary','cadquery.cq_directive'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + "cadquery.cq_directive", +] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'CadQuery' -copyright = u'Parametric Products Intellectual Holdings LLC, All Rights Reserved' +project = u"CadQuery" +copyright = u"Parametric Products Intellectual Holdings LLC, All Rights Reserved" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.0' +version = "1.0" # The full version, including alpha/beta/rc tags. -release = '1.0.0' +release = "1.0.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). @@ -85,27 +91,27 @@ add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = 'timlinux-linfiniti-sphinx' -html_theme = 'sphinx_rtd_theme' +# html_theme = 'timlinux-linfiniti-sphinx' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = { +# html_theme_options = { # "headerfont": "'Open Sans',Arial,sans-serif", # #"bodyfont:": "'Open Sans',Arial,sans-serif", # #"headerbg" : "{image: url('/img/bg/body.jpg');color:#000000;}", @@ -115,9 +121,9 @@ html_theme = 'sphinx_rtd_theme' ## "headercolor1": "#13171A;", # "headercolor2": "#444;", # "headerlinkcolor" : "#13171A;", -#} +# } -#agogo options +# agogo options """ bodyfont (CSS font family): Font for normal text. headerfont (CSS font family): Font for headings. @@ -133,14 +139,14 @@ html_theme = 'sphinx_rtd_theme' textalign (CSS text-align value): Text alignment for the body, default is justify. """ # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = "CadQuery Documentation" # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. @@ -149,36 +155,36 @@ html_logo = "_static/cqlogo.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False @@ -187,72 +193,66 @@ html_show_sourcelink = False html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'CadQuerydoc' +htmlhelp_basename = "CadQuerydoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'CadQuery.tex', u'CadQuery Documentation', - u'David Cowden', 'manual'), + ("index", "CadQuery.tex", u"CadQuery Documentation", u"David Cowden", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'cadquery', u'CadQuery Documentation', - [u'David Cowden'], 1) -] +man_pages = [("index", "cadquery", u"CadQuery Documentation", [u"David Cowden"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -261,16 +261,22 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'CadQuery', u'CadQuery Documentation', - u'David Cowden', 'CadQuery', 'A Fluent CAD api', - 'Miscellaneous'), + ( + "index", + "CadQuery", + u"CadQuery Documentation", + u"David Cowden", + "CadQuery", + "A Fluent CAD api", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/doc/examples.rst b/doc/examples.rst index 391af5a0..cee51b36 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -71,8 +71,8 @@ of a working plane is at the center of the face. The default hole depth is thro center_hole_dia = 22.0 # Create a box based on the dimensions above and add a 22mm center hole - result = cq.Workplane("XY").box(length, height, thickness) \ - .faces(">Z").workplane().hole(center_hole_dia) + result = (cq.Workplane("XY").box(length, height, thickness) + .faces(">Z").workplane().hole(center_hole_dia)) show_object(result) @@ -121,8 +121,8 @@ closed curve. .. cq_plot:: - result = cq.Workplane("front").lineTo(2.0, 0).lineTo(2.0, 1.0).threePointArc((1.0, 1.5),(0.0, 1.0))\ - .close().extrude(0.25) + result = (cq.Workplane("front").lineTo(2.0, 0).lineTo(2.0, 1.0).threePointArc((1.0, 1.5),(0.0, 1.0)) + .close().extrude(0.25)) show_object(result) @@ -152,7 +152,7 @@ A new work plane center can be established at any point. result = result.center(1.5, 0.0).rect(0.5, 0.5) # new work center is (1.5, 0.0) result = result.center(-1.5, 1.5).circle(0.25) # new work center is ( 0.0, 1.5). - #the new center is specified relative to the previous center, not global coordinates! + # The new center is specified relative to the previous center, not global coordinates! result = result.extrude(0.25) show_object(result) @@ -204,8 +204,8 @@ correct for small hole sizes. .. cq_plot:: - result = cq.Workplane("front").box(3.0, 4.0, 0.25).pushPoints ( [ ( 0,0.75 ),(0, -0.75) ]) \ - .polygon(6, 1.0).cutThruAll() + result = (cq.Workplane("front").box(3.0, 4.0, 0.25).pushPoints ( [ ( 0,0.75 ),(0, -0.75) ]) + .polygon(6, 1.0).cutThruAll()) show_object(result) .. topic:: Api References @@ -460,6 +460,32 @@ This example uses an offset workplane to make a compound object, which is perfec * :py:meth:`Workplane.box` * :py:meth:`Workplane` +Copying Workplanes +-------------------------- + +An existing CQ object can copy a workplane from another CQ object. + +.. cq_plot:: + + result = (cq.Workplane("front").circle(1).extrude(10) # make a cylinder + # We want to make a second cylinder perpendicular to the first, + # but we have no face to base the workplane off + .copyWorkplane( + # create a temporary object with the required workplane + cq.Workplane("right", origin=(-5, 0, 0)) + ).circle(1).extrude(10)) + show_object(result) + +.. topic:: API References + + .. hlist: + :columns: 2 + + * :py:meth:`CQ.copyWorkplane` **!** + * :py:meth:`Workplane.circle` + * :py:meth:`Workplane.extrude` + * :py:meth:`Workplane` + Rotated Workplanes -------------------------- @@ -467,9 +493,9 @@ You can create a rotated work plane by specifying angles of rotation relative to .. cq_plot:: - result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").workplane() \ - .transformed(offset=cq.Vector(0, -1.5, 1.0),rotate=cq.Vector(60, 0, 0)) \ - .rect(1.5,1.5,forConstruction=True).vertices().hole(0.25) + result = (cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").workplane() + .transformed(offset=cq.Vector(0, -1.5, 1.0),rotate=cq.Vector(60, 0, 0)) + .rect(1.5,1.5,forConstruction=True).vertices().hole(0.25)) show_object(result) .. topic:: Api References @@ -492,8 +518,8 @@ In the example below, a rectangle is drawn, and its vertices are used to locate .. cq_plot:: - result = cq.Workplane("front").box(2, 2, 0.5).faces(">Z").workplane() \ - .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.125 ) + result = (cq.Workplane("front").box(2, 2, 0.5).faces(">Z").workplane() + .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.125 )) show_object(result) .. topic:: Api References @@ -538,8 +564,8 @@ and a circular section. .. cq_plot:: - result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").circle(1.5) \ - .workplane(offset=3.0).rect(0.75, 0.5).loft(combine=True) + result = (cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").circle(1.5) + .workplane(offset=3.0).rect(0.75, 0.5).loft(combine=True)) show_object(result) @@ -563,8 +589,8 @@ Similar to :py:meth:`Workplane.hole` , these functions operate on a list of poin .. cq_plot:: - result = cq.Workplane(cq.Plane.XY()).box(4,2, 0.5).faces(">Z").workplane().rect(3.5, 1.5, forConstruction=True)\ - .vertices().cboreHole(0.125, 0.25, 0.125, depth=None) + result = (cq.Workplane(cq.Plane.XY()).box(4,2, 0.5).faces(">Z").workplane().rect(3.5, 1.5, forConstruction=True) + .vertices().cboreHole(0.125, 0.25, 0.125, depth=None)) show_object(result) @@ -604,6 +630,55 @@ Here we fillet all of the edges of a simple plate. * :py:meth:`Workplane.edges` * :py:meth:`Workplane` +Tagging objects +---------------- + +The :py:meth:`CQ.tag` method can be used to tag a particular object in the chain with a string, so that it can be refered to later in the chain. + +The :py:meth:`CQ.workplaneFromTagged` method applies :py:meth:`CQ.copyWorkplane` to a tagged object. For example, when extruding two different solids from a surface, after the first solid is extruded it can become difficult to reselect the original surface with CadQuery's other selectors. + +.. cq_plot:: + + result = (cq.Workplane("XY") + # create and tag the base workplane + .box(10, 10, 10).faces(">Z").workplane().tag("baseplane") + # extrude a cylinder + .center(-3, 0).circle(1).extrude(3) + # to reselect the base workplane, simply + .workplaneFromTagged("baseplane") + # extrude a second cylinder + .center(3, 0).circle(1).extrude(2)) + show_object(result) + + +Tags can also be used with most selectors, including :py:meth:`CQ.vertices`, :py:meth:`CQ.faces`, :py:meth:`CQ.edges`, :py:meth:`CQ.wires`, :py:meth:`CQ.shells`, :py:meth:`CQ.solids` and :py:meth:`CQ.compounds`. + +.. cq_plot:: + + result = (cq.Workplane("XY") + # create a triangular prism and tag it + .polygon(3, 5).extrude(4).tag("prism") + # create a sphere that obscures the prism + .sphere(10) + # create features based on the prism's faces + .faces("X", tag="prism").faces(">Y").workplane().circle(1).cutThruAll()) + show_object(result) + +.. topic:: Api References + + .. hlist:: + :columns: 2 + + * :py:meth:`CQ.tag` **!** + * :py:meth:`CQ.getTagged` **!** + * :py:meth:`CQ.workplaneFromTagged` **!** + * :py:meth:`Workplane.extrude` + * :py:meth:`Workplane.cutThruAll` + * :py:meth:`Workplane.circle` + * :py:meth:`Workplane.faces` + * :py:meth:`Workplane` + A Parametric Bearing Pillow Block ------------------------------------ @@ -614,10 +689,10 @@ with just a few lines of code. (length,height,bearing_diam, thickness,padding) = ( 30.0, 40.0, 22.0, 10.0, 8.0) - result = cq.Workplane("XY").box(length,height,thickness).faces(">Z").workplane().hole(bearing_diam) \ - .faces(">Z").workplane() \ - .rect(length-padding,height-padding,forConstruction=True) \ - .vertices().cboreHole(2.4, 4.4, 2.1) + result = (cq.Workplane("XY").box(length,height,thickness).faces(">Z").workplane().hole(bearing_diam) + .faces(">Z").workplane() + .rect(length-padding,height-padding,forConstruction=True) + .vertices().cboreHole(2.4, 4.4, 2.1)) show_object(result) @@ -665,10 +740,10 @@ ones at 13 lines, but that's very short compared to the pythonOCC version, which (L,w,t) = (20.0, 6.0, 3.0) s = cq.Workplane("XY") - #draw half the profile of the bottle and extrude it - p = s.center(-L/2.0, 0).vLine(w/2.0) \ - .threePointArc((L/2.0, w/2.0 + t),(L, w/2.0)).vLine(-w/2.0) \ - .mirrorX().extrude(30.0,True) + # Draw half the profile of the bottle and extrude it + p = (s.center(-L/2.0, 0).vLine(w/2.0) + .threePointArc((L/2.0, w/2.0 + t),(L, w/2.0)).vLine(-w/2.0) + .mirrorX().extrude(30.0,True)) #make the neck p = p.faces(">Z").workplane().circle(3.0).extrude(2.0,True) @@ -729,9 +804,10 @@ A Parametric Enclosure oshell = oshell.edges("|Z").fillet(p_sideRadius) #inner shell - ishell = oshell.faces("Z").workplane(-p_thickness)\ - .rect(POSTWIDTH,POSTLENGTH,forConstruction=True)\ - .vertices().circle(p_screwpostOD/2.0).circle(p_screwpostID/2.0)\ - .extrude((-1.0)*(p_outerHeight + p_lipHeight -p_thickness ),True) + box = (box.faces(">Z").workplane(-p_thickness) + .rect(POSTWIDTH,POSTLENGTH,forConstruction=True) + .vertices().circle(p_screwpostOD/2.0).circle(p_screwpostID/2.0) + .extrude((-1.0)*(p_outerHeight + p_lipHeight -p_thickness ),True)) #split lid into top and bottom parts (lid,bottom) = box.faces(">Z").workplane(-p_thickness -p_lipHeight ).split(keepTop=True,keepBottom=True).all() #splits into two solids @@ -835,23 +911,23 @@ regarding the underside of the brick. s = s.faces("Z").workplane(). \ - rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) \ - .extrude(bumpHeight) + s = (s.faces(">Z").workplane(). + rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) + .extrude(bumpHeight)) # add posts on the bottom. posts are different diameter depending on geometry # solid studs for 1 bump, tubes for multiple, none for 1x1 tmp = s.faces(" 1 and wbumps > 1: - tmp = tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). \ - circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t) + tmp = (tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). + circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t)) elif lbumps > 1: - tmp = tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). \ - circle(t).extrude(height - t) + tmp = (tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). + circle(t).extrude(height - t)) elif wbumps > 1: - tmp = tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). \ - circle(t).extrude(height - t) + tmp = (tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). + circle(t).extrude(height - t)) else: tmp = s @@ -1018,12 +1094,12 @@ Braille Example line_start_pos += Point(0, -cell_geometry.interline) r = get_cylinder_radius(cell_geometry) - base = base.faces('>Z').vertices('Z').vertices('Z').edges() \ - .fillet(r - 0.001) + base = (base.faces('>Z').edges() + .fillet(r - 0.001)) hidding_box = cq.Workplane('XY').box( base_width, base_height, base_thickness, centered=(False, False, False)) result = hidding_box.union(base) @@ -1119,7 +1195,7 @@ This specific examples generates a helical cycloidal gear. return hypocycloid(t,r1,r2) # create the gear profile and extrude it - result = cq.Workplane('XY').parametricCurve(lambda t: gear(t*2*pi,6,1))\ - .twistExtrude(15,90).faces('>Z').workplane().circle(2).cutThruAll() + result = (cq.Workplane('XY').parametricCurve(lambda t: gear(t*2*pi,6,1)) + .twistExtrude(15,90).faces('>Z').workplane().circle(2).cutThruAll()) show_object(result) diff --git a/doc/primer.rst b/doc/primer.rst index f036de3f..88d2c7f5 100644 --- a/doc/primer.rst +++ b/doc/primer.rst @@ -111,12 +111,19 @@ backwards in the stack to get the face as well:: You can browse stack access methods here: :ref:`stackMethods`. +.. _chaining: + Chaining --------------------------- All CadQuery methods return another CadQuery object, so that you can chain the methods together fluently. Use the core CQ methods to get at the objects that were created. +Each time a new CadQuery object is produced during these chained calls, it has a ``parent`` attribute that points +to the CadQuery object that created it. Several CadQuery methods search this parent chain, for example when searching +for the context solid. You can also give a CadQuery object a tag, and further down your chain of CadQuery calls you +can refer back to this particular object using it's tag. + The Context Solid --------------------------- diff --git a/environment.yml b/environment.yml index dfc98e17..e9310ae1 100644 --- a/environment.yml +++ b/environment.yml @@ -1,14 +1,18 @@ name: cadquery channels: - - conda-forge - cadquery + - conda-forge + - defaults dependencies: - - python=3.6 - - pythonocc-core=0.18.2 - - oce=0.18.2 + - python>=3.6 + - cadquery::pythonocc-core - pyparsing + - sphinx + - sphinx_rtd_theme + - black + - codecov + - pytest + - pytest-cov + - pip - pip: - "--editable=." - # Documentation - - sphinx - - sphinx_rtd_theme diff --git a/examples/Ex001_Simple_Block.py b/examples/Ex001_Simple_Block.py index f72445f4..8496d103 100644 --- a/examples/Ex001_Simple_Block.py +++ b/examples/Ex001_Simple_Block.py @@ -1,9 +1,9 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -length = 80.0 # Length of the block -height = 60.0 # Height of the block -thickness = 10.0 # Thickness of the block +length = 80.0 # Length of the block +height = 60.0 # Height of the block +thickness = 10.0 # Thickness of the block # Create a 3D block based on the dimension variables above. # 1. Establishes a workplane that an object can be built on. diff --git a/examples/Ex002_Block_With_Bored_Center_Hole.py b/examples/Ex002_Block_With_Bored_Center_Hole.py index a825f98e..87ba3926 100644 --- a/examples/Ex002_Block_With_Bored_Center_Hole.py +++ b/examples/Ex002_Block_With_Bored_Center_Hole.py @@ -1,10 +1,10 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -length = 80.0 # Length of the block -height = 60.0 # Height of the block -thickness = 10.0 # Thickness of the block -center_hole_dia = 22.0 # Diameter of center hole in block +length = 80.0 # Length of the block +height = 60.0 # Height of the block +thickness = 10.0 # Thickness of the block +center_hole_dia = 22.0 # Diameter of center hole in block # Create a block based on the dimensions above and add a 22mm center hole. # 1. Establishes a workplane that an object can be built on. @@ -13,8 +13,13 @@ center_hole_dia = 22.0 # Diameter of center hole in block # 2. The highest (max) Z face is selected and a new workplane is created on it. # 3. The new workplane is used to drill a hole through the block. # 3a. The hole is automatically centered in the workplane. -result = cq.Workplane("XY").box(length, height, thickness) \ - .faces(">Z").workplane().hole(center_hole_dia) +result = ( + cq.Workplane("XY") + .box(length, height, thickness) + .faces(">Z") + .workplane() + .hole(center_hole_dia) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py b/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py index 1b2be255..373478c5 100644 --- a/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py +++ b/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py @@ -1,15 +1,15 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -length = 80.0 # Length of the block -width = 60.0 # Width of the block -height = 100.0 # Height of the block -thickness = 10.0 # Thickness of the block -center_hole_dia = 22.0 # Diameter of center hole in block -cbore_hole_diameter = 2.4 # Bolt shank/threads clearance hole diameter -cbore_inset = 12.0 # How far from the edge the cbored holes are set -cbore_diameter = 4.4 # Bolt head pocket hole diameter -cbore_depth = 2.1 # Bolt head pocket hole depth +length = 80.0 # Length of the block +width = 60.0 # Width of the block +height = 100.0 # Height of the block +thickness = 10.0 # Thickness of the block +center_hole_dia = 22.0 # Diameter of center hole in block +cbore_hole_diameter = 2.4 # Bolt shank/threads clearance hole diameter +cbore_inset = 12.0 # How far from the edge the cbored holes are set +cbore_diameter = 4.4 # Bolt head pocket hole diameter +cbore_depth = 2.1 # Bolt head pocket hole depth # Create a 3D block based on the dimensions above and add a 22mm center hold # and 4 counterbored holes for bolts @@ -26,12 +26,20 @@ cbore_depth = 2.1 # Bolt head pocket hole depth # do not show up in the final displayed geometry. # 6. The vertices of the rectangle (corners) are selected, and a counter-bored # hole is placed at each of the vertices (all 4 of them at once). -result = cq.Workplane("XY").box(length, height, thickness) \ - .faces(">Z").workplane().hole(center_hole_dia) \ - .faces(">Z").workplane() \ - .rect(length - cbore_inset, height - cbore_inset, forConstruction=True) \ - .vertices().cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth) \ - .edges("|Z").fillet(2.0) +result = ( + cq.Workplane("XY") + .box(length, height, thickness) + .faces(">Z") + .workplane() + .hole(center_hole_dia) + .faces(">Z") + .workplane() + .rect(length - cbore_inset, height - cbore_inset, forConstruction=True) + .vertices() + .cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth) + .edges("|Z") + .fillet(2.0) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex004_Extruded_Cylindrical_Plate.py b/examples/Ex004_Extruded_Cylindrical_Plate.py index 6ccb2eca..d287887c 100644 --- a/examples/Ex004_Extruded_Cylindrical_Plate.py +++ b/examples/Ex004_Extruded_Cylindrical_Plate.py @@ -1,10 +1,10 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -circle_radius = 50.0 # Radius of the plate -thickness = 13.0 # Thickness of the plate -rectangle_width = 13.0 # Width of rectangular hole in cylindrical plate -rectangle_length = 19.0 # Length of rectangular hole in cylindrical plate +circle_radius = 50.0 # Radius of the plate +thickness = 13.0 # Thickness of the plate +rectangle_width = 13.0 # Width of rectangular hole in cylindrical plate +rectangle_length = 19.0 # Length of rectangular hole in cylindrical plate # Extrude a cylindrical plate with a rectangular hole in the middle of it. # 1. Establishes a workplane that an object can be built on. @@ -21,9 +21,12 @@ rectangle_length = 19.0 # Length of rectangular hole in cylindrical plate # plate with a rectangular hole in the center. # 3a. circle() and rect() could be changed to any other shape to completely # change the resulting plate and/or the hole in it. -result = cq.Workplane("front").circle(circle_radius) \ - .rect(rectangle_width, rectangle_length) \ - .extrude(thickness) +result = ( + cq.Workplane("front") + .circle(circle_radius) + .rect(rectangle_width, rectangle_length) + .extrude(thickness) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex005_Extruded_Lines_and_Arcs.py b/examples/Ex005_Extruded_Lines_and_Arcs.py index fa93a893..c9fda2af 100644 --- a/examples/Ex005_Extruded_Lines_and_Arcs.py +++ b/examples/Ex005_Extruded_Lines_and_Arcs.py @@ -1,8 +1,8 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -width = 2.0 # Overall width of the plate -thickness = 0.25 # Thickness of the plate +width = 2.0 # Overall width of the plate +thickness = 0.25 # Thickness of the plate # Extrude a plate outline made of lines and an arc # 1. Establishes a workplane that an object can be built on. @@ -34,12 +34,16 @@ thickness = 0.25 # Thickness of the plate # 7a. Without the close(), the 2D sketch will be left open and the extrude # operation will provide unpredictable results. # 8. The 2D sketch is extruded into a solid object of the specified thickness. -result = cq.Workplane("front").lineTo(width, 0) \ - .lineTo(width, 1.0) \ - .threePointArc((1.0, 1.5), (0.0, 1.0)) \ - .sagittaArc((-0.5, 1.0), 0.2) \ - .radiusArc((-0.7, -0.2), -1.5) \ - .close().extrude(thickness) +result = ( + cq.Workplane("front") + .lineTo(width, 0) + .lineTo(width, 1.0) + .threePointArc((1.0, 1.5), (0.0, 1.0)) + .sagittaArc((-0.5, 1.0), 0.2) + .radiusArc((-0.7, -0.2), -1.5) + .close() + .extrude(thickness) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex006_Moving_the_Current_Working_Point.py b/examples/Ex006_Moving_the_Current_Working_Point.py index 3c26121a..b1337538 100644 --- a/examples/Ex006_Moving_the_Current_Working_Point.py +++ b/examples/Ex006_Moving_the_Current_Working_Point.py @@ -1,8 +1,8 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -circle_radius = 3.0 # The outside radius of the plate -thickness = 0.25 # The thickness of the plate +circle_radius = 3.0 # The outside radius of the plate +thickness = 0.25 # The thickness of the plate # Make a plate with two cutouts in it by moving the workplane center point # 1. Establishes a workplane that an object can be built on. diff --git a/examples/Ex007_Using_Point_Lists.py b/examples/Ex007_Using_Point_Lists.py index d824c750..ad577fcd 100644 --- a/examples/Ex007_Using_Point_Lists.py +++ b/examples/Ex007_Using_Point_Lists.py @@ -1,9 +1,9 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -plate_radius = 2.0 # The radius of the plate that will be extruded +plate_radius = 2.0 # The radius of the plate that will be extruded hole_pattern_radius = 0.25 # Radius of circle where the holes will be placed -thickness = 0.125 # The thickness of the plate that will be extruded +thickness = 0.125 # The thickness of the plate that will be extruded # Make a plate with 4 holes in it at various points in a polar arrangement from # the center of the workplane. diff --git a/examples/Ex008_Polygon_Creation.py b/examples/Ex008_Polygon_Creation.py index 2fdecfc5..43f8eae9 100644 --- a/examples/Ex008_Polygon_Creation.py +++ b/examples/Ex008_Polygon_Creation.py @@ -1,11 +1,11 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -width = 3.0 # The width of the plate -height = 4.0 # The height of the plate -thickness = 0.25 # The thickness of the plate -polygon_sides = 6 # The number of sides that the polygonal holes should have -polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points +width = 3.0 # The width of the plate +height = 4.0 # The height of the plate +thickness = 0.25 # The thickness of the plate +polygon_sides = 6 # The number of sides that the polygonal holes should have +polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points # Create a plate with two polygons cut through it # 1. Establishes a workplane that an object can be built on. @@ -30,10 +30,13 @@ polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points # like cutBlind() assume a positive cut direction, but cutThruAll() assumes # instead that the cut is made from a max direction and cuts downward from # that max through all objects. -result = cq.Workplane("front").box(width, height, thickness) \ - .pushPoints([(0, 0.75), (0, -0.75)]) \ - .polygon(polygon_sides, polygon_dia) \ - .cutThruAll() +result = ( + cq.Workplane("front") + .box(width, height, thickness) + .pushPoints([(0, 0.75), (0, -0.75)]) + .polygon(polygon_sides, polygon_dia) + .cutThruAll() +) # Displays the result of this script show_object(result) diff --git a/examples/Ex009_Polylines.py b/examples/Ex009_Polylines.py index 85a7d6ae..70f83e87 100644 --- a/examples/Ex009_Polylines.py +++ b/examples/Ex009_Polylines.py @@ -6,13 +6,13 @@ import cadquery as cq # Define the points that the polyline will be drawn to/thru pts = [ - (W/2.0, H/2.0), - (W/2.0, (H/2.0 - t)), - (t/2.0, (H/2.0-t)), - (t/2.0, (t - H/2.0)), - (W/2.0, (t - H/2.0)), - (W/2.0, H/-2.0), - (0, H/-2.0) + (W / 2.0, H / 2.0), + (W / 2.0, (H / 2.0 - t)), + (t / 2.0, (H / 2.0 - t)), + (t / 2.0, (t - H / 2.0)), + (W / 2.0, (t - H / 2.0)), + (W / 2.0, H / -2.0), + (0, H / -2.0), ] # We generate half of the I-beam outline and then mirror it to create the full @@ -30,10 +30,7 @@ pts = [ # 3. Only half of the I-beam profile has been drawn so far. That half is # mirrored around the Y-axis to create the complete I-beam profile. # 4. The I-beam profile is extruded to the final length of the beam. -result = cq.Workplane("front").moveTo(0, H/2.0) \ - .polyline(pts) \ - .mirrorY() \ - .extrude(L) +result = cq.Workplane("front").moveTo(0, H / 2.0).polyline(pts).mirrorY().extrude(L) # Displays the result of this script show_object(result) diff --git a/examples/Ex010_Defining_an_Edge_with_a_Spline.py b/examples/Ex010_Defining_an_Edge_with_a_Spline.py index 8b4c67cb..57b06328 100644 --- a/examples/Ex010_Defining_an_Edge_with_a_Spline.py +++ b/examples/Ex010_Defining_an_Edge_with_a_Spline.py @@ -13,7 +13,7 @@ sPnts = [ (1.5, 1.0), (1.0, 1.25), (0.5, 1.0), - (0, 1.0) + (0, 1.0), ] # 2. Generate our plate with the spline feature and make sure it is a diff --git a/examples/Ex015_Rotated_Workplanes.py b/examples/Ex015_Rotated_Workplanes.py index a964e015..602b4cb0 100644 --- a/examples/Ex015_Rotated_Workplanes.py +++ b/examples/Ex015_Rotated_Workplanes.py @@ -13,10 +13,16 @@ import cadquery as cq # 6. Selects the vertices of the for-construction rectangle. # 7. Places holes at the center of each selected vertex. # 7a. Since the workplane is rotated, this results in angled holes in the face. -result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z") \ - .workplane() \ - .transformed(offset=(0, -1.5, 1.0), rotate=(60, 0, 0)) \ - .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.25) +result = ( + cq.Workplane("front") + .box(4.0, 4.0, 0.25) + .faces(">Z") + .workplane() + .transformed(offset=(0, -1.5, 1.0), rotate=(60, 0, 0)) + .rect(1.5, 1.5, forConstruction=True) + .vertices() + .hole(0.25) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex016_Using_Construction_Geometry.py b/examples/Ex016_Using_Construction_Geometry.py index 48a4f870..37e653f8 100644 --- a/examples/Ex016_Using_Construction_Geometry.py +++ b/examples/Ex016_Using_Construction_Geometry.py @@ -12,10 +12,15 @@ import cadquery as cq # other geometry. # 6. Selects the vertices of the for-construction rectangle. # 7. Places holes at the center of each selected vertex. -result = cq.Workplane("front").box(2, 2, 0.5)\ - .faces(">Z").workplane() \ - .rect(1.5, 1.5, forConstruction=True).vertices() \ - .hole(0.125) +result = ( + cq.Workplane("front") + .box(2, 2, 0.5) + .faces(">Z") + .workplane() + .rect(1.5, 1.5, forConstruction=True) + .vertices() + .hole(0.125) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex018_Making_Lofts.py b/examples/Ex018_Making_Lofts.py index 6e9ad1e2..2106d99f 100644 --- a/examples/Ex018_Making_Lofts.py +++ b/examples/Ex018_Making_Lofts.py @@ -11,10 +11,15 @@ import cadquery as cq # 5. Creates a workplane 3 mm above the face the circle was drawn on. # 6. Draws a 2D circle on the new, offset workplane. # 7. Creates a loft between the circle and the rectangle. -result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z") \ - .circle(1.5).workplane(offset=3.0) \ - .rect(0.75, 0.5) \ - .loft(combine=True) +result = ( + cq.Workplane("front") + .box(4.0, 4.0, 0.25) + .faces(">Z") + .circle(1.5) + .workplane(offset=3.0) + .rect(0.75, 0.5) + .loft(combine=True) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex019_Counter_Sunk_Holes.py b/examples/Ex019_Counter_Sunk_Holes.py index e75039a4..e6a8d84c 100644 --- a/examples/Ex019_Counter_Sunk_Holes.py +++ b/examples/Ex019_Counter_Sunk_Holes.py @@ -11,9 +11,15 @@ import cadquery as cq # function. # 5a. When the depth of the counter-sink hole is set to None, the hole will be # cut through. -result = cq.Workplane(cq.Plane.XY()).box(4, 2, 0.5).faces(">Z") \ - .workplane().rect(3.5, 1.5, forConstruction=True) \ - .vertices().cskHole(0.125, 0.25, 82.0, depth=None) +result = ( + cq.Workplane(cq.Plane.XY()) + .box(4, 2, 0.5) + .faces(">Z") + .workplane() + .rect(3.5, 1.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82.0, depth=None) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex021_Splitting_an_Object.py b/examples/Ex021_Splitting_an_Object.py index e903a13d..bdd217b4 100644 --- a/examples/Ex021_Splitting_an_Object.py +++ b/examples/Ex021_Splitting_an_Object.py @@ -9,8 +9,7 @@ import cadquery as cq # that new geometry can be built on. # 4. Draws a 2D circle on the new workplane and then uses it to cut a hole # all the way through the box. -c = cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane() \ - .circle(0.25).cutThruAll() +c = cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane().circle(0.25).cutThruAll() # 5. Selects the face furthest away from the origin in the +Y axis direction. # 6. Creates an offset workplane that is set in the center of the object. diff --git a/examples/Ex022_Revolution.py b/examples/Ex022_Revolution.py index c5f31070..eb4debb8 100644 --- a/examples/Ex022_Revolution.py +++ b/examples/Ex022_Revolution.py @@ -9,13 +9,13 @@ angle_degrees = 360.0 # Revolve a cylinder from a rectangle # Switch comments around in this section to try the revolve operation with different parameters result = cq.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve() -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve(angle_degrees) -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5)) -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5, -5),(-5, 5)) -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5),(-5,5), False) +# result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve(angle_degrees) +# result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5)) +# result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5, -5),(-5, 5)) +# result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5),(-5,5), False) # Revolve a donut with square walls -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, True).revolve(angle_degrees, (20, 0), (20, 10)) +# result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, True).revolve(angle_degrees, (20, 0), (20, 10)) # Displays the result of this script show_object(result) diff --git a/examples/Ex023_Sweep.py b/examples/Ex023_Sweep.py index c2ba5017..c5b01607 100644 --- a/examples/Ex023_Sweep.py +++ b/examples/Ex023_Sweep.py @@ -1,11 +1,7 @@ import cadquery as cq # Points we will use to create spline and polyline paths to sweep over -pts = [ - (0, 1), - (1, 2), - (2, 4) -] +pts = [(0, 1), (1, 2), (2, 4)] # Spline path generated from our list of points (tuples) path = cq.Workplane("XZ").spline(pts) @@ -37,4 +33,4 @@ show_object(defaultSweep) show_object(frenetShell.translate((5, 0, 0))) show_object(defaultRect.translate((10, 0, 0))) show_object(plineSweep.translate((15, 0, 0))) -show_object(arcSweep.translate((20, 0, 0))) \ No newline at end of file +show_object(arcSweep.translate((20, 0, 0))) diff --git a/examples/Ex024_Sweep_With_Multiple_Sections.py b/examples/Ex024_Sweep_With_Multiple_Sections.py index 6659e61e..5c4cd005 100644 --- a/examples/Ex024_Sweep_With_Multiple_Sections.py +++ b/examples/Ex024_Sweep_With_Multiple_Sections.py @@ -4,37 +4,80 @@ import cadquery as cq path = cq.Workplane("XZ").moveTo(-10, 0).lineTo(10, 0) # Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0 -defaultSweep = cq.Workplane("YZ").workplane(offset=-10.0).circle(2.0). \ - workplane(offset=10.0).circle(1.0). \ - workplane(offset=10.0).circle(2.0).sweep(path, multisection=True) +defaultSweep = ( + cq.Workplane("YZ") + .workplane(offset=-10.0) + .circle(2.0) + .workplane(offset=10.0) + .circle(1.0) + .workplane(offset=10.0) + .circle(2.0) + .sweep(path, multisection=True) +) # We can sweep thrue different shapes -recttocircleSweep = cq.Workplane("YZ").workplane(offset=-10.0).rect(2.0, 2.0). \ - workplane(offset=8.0).circle(1.0).workplane(offset=4.0).circle(1.0). \ - workplane(offset=8.0).rect(2.0, 2.0).sweep(path, multisection=True) +recttocircleSweep = ( + cq.Workplane("YZ") + .workplane(offset=-10.0) + .rect(2.0, 2.0) + .workplane(offset=8.0) + .circle(1.0) + .workplane(offset=4.0) + .circle(1.0) + .workplane(offset=8.0) + .rect(2.0, 2.0) + .sweep(path, multisection=True) +) -circletorectSweep = cq.Workplane("YZ").workplane(offset=-10.0).circle(1.0). \ - workplane(offset=7.0).rect(2.0, 2.0).workplane(offset=6.0).rect(2.0, 2.0). \ - workplane(offset=7.0).circle(1.0).sweep(path, multisection=True) +circletorectSweep = ( + cq.Workplane("YZ") + .workplane(offset=-10.0) + .circle(1.0) + .workplane(offset=7.0) + .rect(2.0, 2.0) + .workplane(offset=6.0) + .rect(2.0, 2.0) + .workplane(offset=7.0) + .circle(1.0) + .sweep(path, multisection=True) +) # Placement of the Shape is important otherwise could produce unexpected shape -specialSweep = cq.Workplane("YZ").circle(1.0).workplane(offset=10.0).rect(2.0, 2.0). \ - sweep(path, multisection=True) +specialSweep = ( + cq.Workplane("YZ") + .circle(1.0) + .workplane(offset=10.0) + .rect(2.0, 2.0) + .sweep(path, multisection=True) +) # Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0 -path = cq.Workplane("XZ").moveTo(-5, 4).lineTo(0, 4). \ - threePointArc((4, 0), (0, -4)).lineTo(-5, -4) +path = ( + cq.Workplane("XZ") + .moveTo(-5, 4) + .lineTo(0, 4) + .threePointArc((4, 0), (0, -4)) + .lineTo(-5, -4) +) # Placement of different shapes should follow the path # cylinder r=1.5 along first line # then sweep allong arc from r=1.5 to r=1.0 # then cylinder r=1.0 along last line -arcSweep = cq.Workplane("YZ").workplane(offset=-5).moveTo(0, 4).circle(1.5). \ - workplane(offset=5).circle(1.5). \ - moveTo(0, -8).circle(1.0). \ - workplane(offset=-5).circle(1.0). \ - sweep(path, multisection=True) +arcSweep = ( + cq.Workplane("YZ") + .workplane(offset=-5) + .moveTo(0, 4) + .circle(1.5) + .workplane(offset=5) + .circle(1.5) + .moveTo(0, -8) + .circle(1.0) + .workplane(offset=-5) + .circle(1.0) + .sweep(path, multisection=True) +) # Translate the resulting solids so that they do not overlap and display them left to right @@ -43,5 +86,3 @@ show_object(circletorectSweep.translate((0, 5, 0))) show_object(recttocircleSweep.translate((0, 10, 0))) show_object(specialSweep.translate((0, 15, 0))) show_object(arcSweep.translate((0, -5, 0))) - - diff --git a/examples/Ex100_Lego_Brick.py b/examples/Ex100_Lego_Brick.py index 6a6381f7..ca2fae61 100644 --- a/examples/Ex100_Lego_Brick.py +++ b/examples/Ex100_Lego_Brick.py @@ -4,8 +4,8 @@ import cadquery as cq ##### # Inputs ###### -lbumps = 1 # number of bumps long -wbumps = 1 # number of bumps wide +lbumps = 1 # number of bumps long +wbumps = 1 # number of bumps wide thin = True # True for thin, False for thick # @@ -22,8 +22,8 @@ else: t = (pitch - (2 * clearance) - bumpDiam) / 2.0 postDiam = pitch - t # works out to 6.5 -total_length = lbumps*pitch - 2.0*clearance -total_width = wbumps*pitch - 2.0*clearance +total_length = lbumps * pitch - 2.0 * clearance +total_width = wbumps * pitch - 2.0 * clearance # make the base s = cq.Workplane("XY").box(total_length, total_width, height) @@ -32,23 +32,37 @@ s = cq.Workplane("XY").box(total_length, total_width, height) s = s.faces("Z").workplane(). \ - rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) \ +s = ( + s.faces(">Z") + .workplane() + .rarray(pitch, pitch, lbumps, wbumps, True) + .circle(bumpDiam / 2.0) .extrude(bumpHeight) +) # add posts on the bottom. posts are different diameter depending on geometry # solid studs for 1 bump, tubes for multiple, none for 1x1 tmp = s.faces(" 1 and wbumps > 1: - tmp = tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). \ - circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t) + tmp = ( + tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True) + .circle(postDiam / 2.0) + .circle(bumpDiam / 2.0) + .extrude(height - t) + ) elif lbumps > 1: - tmp = tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). \ - circle(t).extrude(height - t) + tmp = ( + tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True) + .circle(t) + .extrude(height - t) + ) elif wbumps > 1: - tmp = tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). \ - circle(t).extrude(height - t) + tmp = ( + tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True) + .circle(t) + .extrude(height - t) + ) else: tmp = s diff --git a/examples/Ex101_InterpPlate.py b/examples/Ex101_InterpPlate.py new file mode 100644 index 00000000..aa7db4cd --- /dev/null +++ b/examples/Ex101_InterpPlate.py @@ -0,0 +1,175 @@ +from math import sin, cos, pi, sqrt +import cadquery as cq + +# TEST_1 +# example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface. +thickness = 0 +edge_points = [[0.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 10.0, 10.0], [0.0, 0.0, 10.0]] +surface_points = [[5.0, 5.0, 5.0]] +plate_0 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness) +print("plate_0.val().Volume() = ", plate_0.val().Volume()) +plate_0 = plate_0.translate((0, 6 * 12, 0)) +show_object(plate_0) + +# EXAMPLE 1 +# Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides +thickness = 0.1 +edge_points = [ + [-7.0, -7.0, 0.0], + [-3.0, -10.0, 3.0], + [7.0, -7.0, 0.0], + [7.0, 7.0, 0.0], + [-7.0, 7.0, 0.0], +] +edge_wire = cq.Workplane("XY").polyline( + [(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)] +) +# edge_wire = edge_wire.add(cq.Workplane("YZ").workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)).polyline([(-7.,0.), (3,-3), (7.,0.)])) +# In CadQuery Sept-2019 it worked with rotate=cq.Vector(0, 45, 0). In CadQuery Dec-2019 rotate=cq.Vector(45, 0, 0) only closes the wire. +edge_wire = edge_wire.add( + cq.Workplane("YZ") + .workplane() + .transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)) + .spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)]) +) +surface_points = [[-3.0, -3.0, -3.0], [3.0, 3.0, 3.0]] +plate_1 = cq.Workplane("XY").interpPlate(edge_wire, surface_points, thickness) +# plate_1 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness) # list of (x,y,z) points instead of wires for edges +print("plate_1.val().Volume() = ", plate_1.val().Volume()) +show_object(plate_1) + +# EXAMPLE 2 +# Embossed star, need to change optional parameters to obtain nice looking result. +r1 = 3.0 +r2 = 10.0 +fn = 6 +thickness = 0.1 +edge_points = [ + [r1 * cos(i * pi / fn), r1 * sin(i * pi / fn)] + if i % 2 == 0 + else [r2 * cos(i * pi / fn), r2 * sin(i * pi / fn)] + for i in range(2 * fn + 1) +] +edge_wire = cq.Workplane("XY").polyline(edge_points) +r2 = 4.5 +surface_points = [ + [r2 * cos(i * pi / fn), r2 * sin(i * pi / fn), 1.0] for i in range(2 * fn) +] + [[0.0, 0.0, -2.0]] +plate_2 = cq.Workplane("XY").interpPlate( + edge_wire, + surface_points, + thickness, + combine=True, + clean=True, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=49, +) +# plate_2 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness, combine=True, clean=True, Degree=3, NbPtsOnCur=15, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=49) # list of (x,y,z) points instead of wires for edges +print("plate_2.val().Volume() = ", plate_2.val().Volume()) +plate_2 = plate_2.translate((0, 2 * 12, 0)) +show_object(plate_2) + +# EXAMPLE 3 +# Points on hexagonal pattern coordinates, use of pushpoints. +r1 = 1.0 +N = 3 +ca = cos(30.0 * pi / 180.0) +sa = sin(30.0 * pi / 180.0) +# EVEN ROWS +pts = [ + (-3.0, -3.0), + (-1.267949, -3.0), + (0.464102, -3.0), + (2.196152, -3.0), + (-3.0, 0.0), + (-1.267949, 0.0), + (0.464102, 0.0), + (2.196152, 0.0), + (-2.133974, -1.5), + (-0.401923, -1.5), + (1.330127, -1.5), + (3.062178, -1.5), + (-2.133975, 1.5), + (-0.401924, 1.5), + (1.330127, 1.5), + (3.062178, 1.5), +] +# Spike surface +thickness = 0.1 +fn = 6 +edge_points = [ + [ + r1 * cos(i * 2 * pi / fn + 30 * pi / 180), + r1 * sin(i * 2 * pi / fn + 30 * pi / 180), + ] + for i in range(fn + 1) +] +surface_points = [ + [ + r1 / 4 * cos(i * 2 * pi / fn + 30 * pi / 180), + r1 / 4 * sin(i * 2 * pi / fn + 30 * pi / 180), + 0.75, + ] + for i in range(fn + 1) +] + [[0, 0, 2]] +edge_wire = cq.Workplane("XY").polyline(edge_points) +plate_3 = ( + cq.Workplane("XY") + .pushPoints(pts) + .interpPlate( + edge_wire, + surface_points, + thickness, + combine=False, + clean=False, + degree=2, + nbPtsOnCur=20, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ) +) +print("plate_3.val().Volume() = ", plate_3.val().Volume()) +plate_3 = plate_3.translate((0, 4 * 11, 0)) +show_object(plate_3) + +# EXAMPLE 4 +# Gyroïd, all edges are splines on different workplanes. +thickness = 0.1 +edge_points = [ + [[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], +] +plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"] +offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] +edge_wire = ( + cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) +) +for i in range(len(edge_points) - 1): + edge_wire = edge_wire.add( + cq.Workplane(plane_list[i + 1]) + .workplane(offset=-offset_list[i + 1]) + .spline(edge_points[i + 1]) + ) +surface_points = [[0, 0, 0]] +plate_4 = cq.Workplane("XY").interpPlate(edge_wire, surface_points, thickness) +print("plate_4.val().Volume() = ", plate_4.val().Volume()) +plate_4 = plate_4.translate((0, 5 * 12, 0)) +show_object(plate_4) diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 872a66d2..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx-rtd-theme==0.1.9 -travis-sphinx==1.1.0 -Sphinx==1.3.1 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/runtests.py b/runtests.py deleted file mode 100644 index 00720f52..00000000 --- a/runtests.py +++ /dev/null @@ -1,22 +0,0 @@ -import sys -from tests import * -import cadquery -import unittest - -#if you are on python 2.7, you can use -m uniitest discover. -#but this is required for python 2.6.6 on windows. FreeCAD0.12 will not load -#on py 2.7.x on win -suite = unittest.TestSuite() - -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadObjects.TestCadObjects)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadQuery.TestCadQuery)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQGI.TestCQGI)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestCQSelectors)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestExporters.TestExporters)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestImporters.TestImporters)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestJupyter.TestJupyter)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes)) - -if __name__ == '__main__': - result = unittest.TextTestRunner(verbosity=2).run(suite) - sys.exit(not result.wasSuccessful()) diff --git a/setup.py b/setup.py index 24d2a8e1..48f712e4 100644 --- a/setup.py +++ b/setup.py @@ -15,44 +15,48 @@ import os from setuptools import setup -#if we are building in travis, use the build number as the sub-minor version -version = '0.5-SNAPSHOT' -if 'TRAVIS_TAG' in os.environ.keys(): - version= os.environ['TRAVIS_TAG'] +# if we are building in travis, use the build number as the sub-minor version +version = "0.5-SNAPSHOT" +if "TRAVIS_TAG" in os.environ.keys(): + version = os.environ["TRAVIS_TAG"] setup( - name='cadquery', + name="cadquery", version=version, - url='https://github.com/dcowden/cadquery', - license='Apache Public License 2.0', - author='David Cowden', - author_email='dave.cowden@gmail.com', - description='CadQuery is a parametric scripting language for creating and traversing CAD models', - long_description=open('README.md').read(), - packages=['cadquery','cadquery.contrib','cadquery.occ_impl','cadquery.plugins','tests'], - install_requires=['pyparsing'], + url="https://github.com/dcowden/cadquery", + license="Apache Public License 2.0", + author="David Cowden", + author_email="dave.cowden@gmail.com", + description="CadQuery is a parametric scripting language for creating and traversing CAD models", + long_description=open("README.md").read(), + packages=[ + "cadquery", + "cadquery.contrib", + "cadquery.occ_impl", + "cadquery.plugins", + "tests", + ], include_package_data=True, zip_safe=False, - platforms='any', - test_suite='tests', - + platforms="any", + test_suite="tests", classifiers=[ - 'Development Status :: 5 - Production/Stable', + "Development Status :: 5 - Production/Stable", #'Development Status :: 6 - Mature', #'Development Status :: 7 - Inactive', - 'Intended Audience :: Developers', - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: Information Technology', - 'Intended Audience :: Science/Research', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX', - 'Operating System :: MacOS', - 'Operating System :: Unix', - 'Programming Language :: Python', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Internet', - 'Topic :: Scientific/Engineering' - ] + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: Unix", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Internet", + "Topic :: Scientific/Engineering", + ], ) diff --git a/tests/TestCadObjects.py b/tests/TestCadObjects.py deleted file mode 100644 index 4362834f..00000000 --- a/tests/TestCadObjects.py +++ /dev/null @@ -1,284 +0,0 @@ -# system modules -import sys -import unittest -from tests import BaseTest -from OCP.gp import gp, gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_XYZ -from OCP.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex, - BRepBuilderAPI_MakeEdge, - BRepBuilderAPI_MakeFace) - -from OCP.GC import GC_MakeCircle - -from cadquery import * - - -class TestCadObjects(BaseTest): - - def _make_circle(self): - - circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3), gp.DZ_s()), - 2.) - return Shape.cast(BRepBuilderAPI_MakeEdge(circle).Edge()) - - def testVectorConstructors(self): - v1 = Vector(1, 2, 3) - v2 = Vector((1, 2, 3)) - v3 = Vector(gp_Vec(1, 2, 3)) - v4 = Vector([1,2,3]) - v5 = Vector(gp_XYZ(1,2,3)) - - for v in [v1, v2, v3, v4, v5]: - self.assertTupleAlmostEquals((1, 2, 3), v.toTuple(), 4) - - v6 = Vector((1,2)) - v7 = Vector([1,2]) - v8 = Vector(1,2) - - for v in [v6, v7, v8]: - self.assertTupleAlmostEquals((1, 2, 0), v.toTuple(), 4) - - v9 = Vector() - self.assertTupleAlmostEquals((0, 0, 0), v9.toTuple(), 4) - - v9.x = 1. - v9.y = 2. - v9.z = 3. - self.assertTupleAlmostEquals((1, 2, 3), (v9.x, v9.y, v9.z), 4) - - def testVertex(self): - """ - Tests basic vertex functions - """ - v = Vertex.makeVertex(1, 1, 1) - self.assertEqual(1, v.X) - self.assertEqual(Vector, type(v.Center())) - - def testBasicBoundingBox(self): - v = Vertex.makeVertex(1, 1, 1) - v2 = Vertex.makeVertex(2, 2, 2) - self.assertEqual(BoundBox, type(v.BoundingBox())) - self.assertEqual(BoundBox, type(v2.BoundingBox())) - - bb1 = v.BoundingBox().add(v2.BoundingBox()) - - # OCC uses some approximations - self.assertAlmostEqual(bb1.xlen, 1.0, 1) - - def testEdgeWrapperCenter(self): - e = self._make_circle() - - self.assertTupleAlmostEquals((1.0, 2.0, 3.0), e.Center().toTuple(), 3) - - def testEdgeWrapperMakeCircle(self): - halfCircleEdge = Edge.makeCircle(radius=10, pnt=( - 0, 0, 0), dir=(0, 0, 1), angle1=0, angle2=180) - - #self.assertTupleAlmostEquals((0.0, 5.0, 0.0), halfCircleEdge.CenterOfBoundBox(0.0001).toTuple(),3) - self.assertTupleAlmostEquals( - (10.0, 0.0, 0.0), halfCircleEdge.startPoint().toTuple(), 3) - self.assertTupleAlmostEquals( - (-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3) - - def testFaceWrapperMakePlane(self): - mplane = Face.makePlane(10, 10) - - self.assertTupleAlmostEquals( - (0.0, 0.0, 1.0), mplane.normalAt().toTuple(), 3) - - def testCenterOfBoundBox(self): - pass - - def testCombinedCenterOfBoundBox(self): - pass - - def testCompoundCenter(self): - """ - Tests whether or not a proper weighted center can be found for a compound - """ - - def cylinders(self, radius, height): - def _cyl(pnt): - # Inner function to build a cylinder - return Solid.makeCylinder(radius, height, pnt) - - # Combine all the cylinders into a single compound - r = self.eachpoint(_cyl, True).combineSolids() - - return r - - Workplane.cyl = cylinders - - # Now test. here we want weird workplane to see if the objects are transformed right - s = Workplane("XY").rect( - 2.0, 3.0, forConstruction=True).vertices().cyl(0.25, 0.5) - - self.assertEqual(4, len(s.val().Solids())) - self.assertTupleAlmostEquals( - (0.0, 0.0, 0.25), s.val().Center().toTuple(), 3) - - def testDot(self): - v1 = Vector(2, 2, 2) - v2 = Vector(1, -1, 1) - self.assertEqual(2.0, v1.dot(v2)) - - def testVectorAdd(self): - result = Vector(1, 2, 0) + Vector(0, 0, 3) - self.assertTupleAlmostEquals((1.0, 2.0, 3.0), result.toTuple(), 3) - - def testVectorOperators(self): - result = Vector(1, 1, 1) + Vector(2, 2, 2) - self.assertEqual(Vector(3, 3, 3), result) - - result = Vector(1, 2, 3) - Vector(3, 2, 1) - self.assertEqual(Vector(-2, 0, 2), result) - - result = Vector(1, 2, 3) * 2 - self.assertEqual(Vector(2, 4, 6), result) - - result = Vector(2, 4, 6) / 2 - self.assertEqual(Vector(1, 2, 3), result) - - self.assertEqual(Vector(-1, -1, -1), -Vector(1, 1, 1)) - - self.assertEqual(0, abs(Vector(0, 0, 0))) - self.assertEqual(1, abs(Vector(1, 0, 0))) - self.assertEqual((1+4+9)**0.5, abs(Vector(1, 2, 3))) - - def testVectorEquals(self): - a = Vector(1, 2, 3) - b = Vector(1, 2, 3) - c = Vector(1, 2, 3.000001) - self.assertEqual(a, b) - self.assertEqual(a, c) - - def testVectorProject(self): - """ - Test method to project vector to plane. - """ - decimal_places = 9 - - normal = Vector(1, 2, 3) - base = Vector(5, 7, 9) - x_dir = Vector(1, 0, 0) - - # test passing Plane object - point = Vector(10, 11, 12).projectToPlane(Plane(base, x_dir, normal)) - self.assertTupleAlmostEquals(point.toTuple(), (59/7, 55/7, 51/7), - decimal_places) - - def testMatrixCreationAndAccess(self): - def matrix_vals(m): - return [[m[r,c] for c in range(4)] for r in range(4)] - # default constructor creates a 4x4 identity matrix - m = Matrix() - identity = [[1., 0., 0., 0.], - [0., 1., 0., 0.], - [0., 0., 1., 0.], - [0., 0., 0., 1.]] - self.assertEqual(identity, matrix_vals(m)) - - vals4x4 = [[1., 0., 0., 1.], - [0., 1., 0., 2.], - [0., 0., 1., 3.], - [0., 0., 0., 1.]] - vals4x4_tuple = tuple(tuple(r) for r in vals4x4) - - # test constructor with 16-value input - m = Matrix(vals4x4) - self.assertEqual(vals4x4, matrix_vals(m)) - m = Matrix(vals4x4_tuple) - self.assertEqual(vals4x4, matrix_vals(m)) - - # test constructor with 12-value input (the last 4 are an implied - # [0,0,0,1]) - m = Matrix(vals4x4[:3]) - self.assertEqual(vals4x4, matrix_vals(m)) - m = Matrix(vals4x4_tuple[:3]) - self.assertEqual(vals4x4, matrix_vals(m)) - - # Test 16-value input with invalid values for the last 4 - invalid = [[1., 0., 0., 1.], - [0., 1., 0., 2.], - [0., 0., 1., 3.], - [1., 2., 3., 4.]] - with self.assertRaises(ValueError): - Matrix(invalid) - - # Test input with invalid size / nested types - with self.assertRaises(TypeError): - Matrix([[1, 2, 3, 4], [1, 2, 3], [1, 2, 3, 4]]) - with self.assertRaises(TypeError): - Matrix([1,2,3]) - - # Invalid sub-type - with self.assertRaises(TypeError): - Matrix([[1, 2, 3, 4], 'abc', [1, 2, 3, 4]]) - - # test out-of-bounds access - m = Matrix() - with self.assertRaises(IndexError): - m[0, 4] - with self.assertRaises(IndexError): - m[4, 0] - with self.assertRaises(IndexError): - m['ab'] - - - def testTranslate(self): - e = Edge.makeCircle(2, (1, 2, 3)) - e2 = e.translate(Vector(0, 0, 1)) - - self.assertTupleAlmostEquals((1.0, 2.0, 4.0), e2.Center().toTuple(), 3) - - def testVertices(self): - e = Shape.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0), - gp_Pnt(1, 1, 0)).Edge()) - self.assertEqual(2, len(e.Vertices())) - - def testPlaneEqual(self): - # default orientation - self.assertEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)) - ) - # moved origin - self.assertEqual( - Plane(origin=(2,1,-1), xDir=(1,0,0), normal=(0,0,1)), - Plane(origin=(2,1,-1), xDir=(1,0,0), normal=(0,0,1)) - ) - # moved x-axis - self.assertEqual( - Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1)), - Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1)) - ) - # moved z-axis - self.assertEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1)), - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1)) - ) - - def testPlaneNotEqual(self): - # type difference - for value in [None, 0, 1, 'abc']: - self.assertNotEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), - value - ) - # origin difference - self.assertNotEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), - Plane(origin=(0,0,1), xDir=(1,0,0), normal=(0,0,1)) - ) - # x-axis difference - self.assertNotEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), - Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1)) - ) - # z-axis difference - self.assertNotEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1)) - ) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py deleted file mode 100644 index 88a68711..00000000 --- a/tests/TestCadQuery.py +++ /dev/null @@ -1,2082 +0,0 @@ -""" - This module tests cadquery creation and manipulation functions - -""" -# system modules -import math,os.path,time,tempfile - -# my modules -from cadquery import * -from cadquery import exporters -from tests import BaseTest, writeStringToFile, makeUnitCube, readFileAsString, makeUnitSquareWire, makeCube - -# where unit test output will be saved -OUTDIR = tempfile.gettempdir() -SUMMARY_FILE = os.path.join(OUTDIR, "testSummary.html") - -SUMMARY_TEMPLATE = """ - - - - - - -""" - -TEST_RESULT_TEMPLATE = """ -

%(name)s

- %(svg)s -
- -""" - -# clean up any summary file that is in the output directory. -# i know, this sux, but there is no other way to do this in 2.6, as we cannot do class fixutres till 2.7 -writeStringToFile(SUMMARY_TEMPLATE, SUMMARY_FILE) - - -class TestCadQuery(BaseTest): - - def tearDown(self): - """ - Update summary with data from this test. - This is a really hackey way of doing it-- we get a startup event from module load, - but there is no way in unittest to get a single shutdown event-- except for stuff in 2.7 and above - - So what we do here is to read the existing file, stick in more content, and leave it - """ - svgFile = os.path.join(OUTDIR, self._testMethodName + ".svg") - - # all tests do not produce output - if os.path.exists(svgFile): - existingSummary = readFileAsString(SUMMARY_FILE) - svgText = readFileAsString(svgFile) - svgText = svgText.replace( - '', "") - - # now write data into the file - # the content we are replacing it with also includes the marker, so it can be replaced again - existingSummary = existingSummary.replace("", TEST_RESULT_TEMPLATE % ( - dict(svg=svgText, name=self._testMethodName))) - - writeStringToFile(existingSummary, SUMMARY_FILE) - - def saveModel(self, shape): - """ - shape must be a CQ object - Save models in SVG and STEP format - """ - shape.exportSvg(os.path.join(OUTDIR, self._testMethodName + ".svg")) - shape.val().exportStep(os.path.join(OUTDIR, self._testMethodName + ".step")) - - def testToOCC(self): - """ - Tests to make sure that a CadQuery object is converted correctly to a OCC object. - """ - r = Workplane('XY').rect(5, 5).extrude(5) - - r = r.toOCC() - - import OCP as OCC - self.assertEqual(type(r), OCC.TopoDS.TopoDS_Compound) - - def testToSVG(self): - """ - Tests to make sure that a CadQuery object is converted correctly to SVG - """ - r = Workplane('XY').rect(5, 5).extrude(5) - - r_str = r.toSvg() - - # Make sure that a couple of sections from the SVG output make sense - self.assertTrue(r_str.index('path d="M') > 0) - self.assertTrue(r_str.index( - 'line x1="30" y1="-30" x2="58" y2="-15" stroke-width="3"') > 0) - - def testCubePlugin(self): - """ - Tests a plugin that combines cubes together with a base - :return: - """ - # make the plugin method - - def makeCubes(self, length): - # self refers to the CQ or Workplane object - - # inner method that creates a cube - def _singleCube(pnt): - # pnt is a location in local coordinates - # since we're using eachpoint with useLocalCoordinates=True - return Solid.makeBox(length, length, length, pnt) - - # use CQ utility method to iterate over the stack, call our - # method, and convert to/from local coordinates. - return self.eachpoint(_singleCube, True) - - # link the plugin in - Workplane.makeCubes = makeCubes - - # call it - result = Workplane("XY").box(6.0, 8.0, 0.5).faces( - ">Z").rect(4.0, 4.0, forConstruction=True).vertices() - result = result.makeCubes(1.0) - result = result.combineSolids() - self.saveModel(result) - self.assertEqual(1, result.solids().size()) - - def testCylinderPlugin(self): - """ - Tests a cylinder plugin. - The plugin creates cylinders of the specified radius and height for each item on the stack - - This is a very short plugin that illustrates just about the simplest possible - plugin - """ - - def cylinders(self, radius, height): - - def _cyl(pnt): - # inner function to build a cylinder - return Solid.makeCylinder(radius, height, pnt) - - # combine all the cylinders into a single compound - r = self.eachpoint(_cyl, True).combineSolids() - return r - Workplane.cyl = cylinders - - # now test. here we want weird workplane to see if the objects are transformed right - s = Workplane(Plane(Vector((0, 0, 0)), Vector((1, -1, 0)), Vector((1, 1, 0)))).rect(2.0, 3.0, forConstruction=True).vertices() \ - .cyl(0.25, 0.5) - self.assertEqual(4, s.solids().size()) - self.saveModel(s) - - def testPolygonPlugin(self): - """ - Tests a plugin to make regular polygons around points on the stack - - Demonstratings using eachpoint to allow working in local coordinates - to create geometry - """ - - def rPoly(self, nSides, diameter): - - def _makePolygon(center): - # pnt is a vector in local coordinates - angle = 2.0 * math.pi / nSides - pnts = [] - for i in range(nSides + 1): - pnts.append(center + Vector((diameter / 2.0 * math.cos(angle * i)), - (diameter / 2.0 * math.sin(angle * i)), 0)) - return Wire.makePolygon(pnts) - - return self.eachpoint(_makePolygon, True) - - Workplane.rPoly = rPoly - - s = Workplane("XY").box(4.0, 4.0, 0.25).faces(">Z").workplane().rect(2.0, 2.0, forConstruction=True).vertices()\ - .rPoly(5, 0.5).cutThruAll() - - # 6 base sides, 4 pentagons, 5 sides each = 26 - self.assertEqual(26, s.faces().size()) - self.saveModel(s) - - def testPointList(self): - """ - Tests adding points and using them - """ - c = CQ(makeUnitCube()) - - s = c.faces(">Z").workplane().pushPoints( - [(-0.3, 0.3), (0.3, 0.3), (0, 0)]) - self.assertEqual(3, s.size()) - # TODO: is the ability to iterate over points with circle really worth it? - # maybe we should just require using all() and a loop for this. the semantics and - # possible combinations got too hard ( ie, .circle().circle() ) was really odd - body = s.circle(0.05).cutThruAll() - self.saveModel(body) - self.assertEqual(9, body.faces().size()) - - # Test the case when using eachpoint with only a blank workplane - def callback_fn(pnt): - self.assertEqual((0.0, 0.0), (pnt.x, pnt.y)) - - r = Workplane('XY') - r.objects = [] - r.eachpoint(callback_fn) - - def testWorkplaneFromFace(self): - # make a workplane on the top face - s = CQ(makeUnitCube()).faces(">Z").workplane() - r = s.circle(0.125).cutBlind(-2.0) - self.saveModel(r) - # the result should have 7 faces - self.assertEqual(7, r.faces().size()) - self.assertEqual(type(r.val()), Compound) - self.assertEqual(type(r.first().val()), Compound) - - def testFrontReference(self): - # make a workplane on the top face - s = CQ(makeUnitCube()).faces("front").workplane() - r = s.circle(0.125).cutBlind(-2.0) - self.saveModel(r) - # the result should have 7 faces - self.assertEqual(7, r.faces().size()) - self.assertEqual(type(r.val()), Compound) - self.assertEqual(type(r.first().val()), Compound) - - def testRotate(self): - """Test solid rotation at the CQ object level.""" - box = Workplane("XY").box(1, 1, 5) - box.rotate((0, 0, 0), (1, 0, 0), 90) - startPoint = box.faces("Z").circle(1.5)\ - .workplane(offset=3.0).rect(0.75, 0.5).loft(combine=True) - self.saveModel(s) - #self.assertEqual(1,s.solids().size() ) - #self.assertEqual(8,s.faces().size() ) - - def testRevolveCylinder(self): - """ - Test creating a solid using the revolve operation. - :return: - """ - # The dimensions of the model. These can be modified rather than changing the - # shape's code directly. - rectangle_width = 10.0 - rectangle_length = 10.0 - angle_degrees = 360.0 - - # Test revolve without any options for making a cylinder - result = Workplane("XY").rect( - rectangle_width, rectangle_length, False).revolve() - self.assertEqual(3, result.faces().size()) - self.assertEqual(2, result.vertices().size()) - self.assertEqual(3, result.edges().size()) - - # Test revolve when only setting the angle to revolve through - result = Workplane("XY").rect( - rectangle_width, rectangle_length, False).revolve(angle_degrees) - self.assertEqual(3, result.faces().size()) - self.assertEqual(2, result.vertices().size()) - self.assertEqual(3, result.edges().size()) - result = Workplane("XY").rect( - rectangle_width, rectangle_length, False).revolve(270.0) - self.assertEqual(5, result.faces().size()) - self.assertEqual(6, result.vertices().size()) - self.assertEqual(9, result.edges().size()) - - # Test when passing revolve the angle and the axis of revolution's start point - result = Workplane("XY").rect( - rectangle_width, rectangle_length).revolve(angle_degrees, (-5, -5)) - self.assertEqual(3, result.faces().size()) - self.assertEqual(2, result.vertices().size()) - self.assertEqual(3, result.edges().size()) - result = Workplane("XY").rect( - rectangle_width, rectangle_length).revolve(270.0, (-5, -5)) - self.assertEqual(5, result.faces().size()) - self.assertEqual(6, result.vertices().size()) - self.assertEqual(9, result.edges().size()) - - # Test when passing revolve the angle and both the start and ends of the axis of revolution - result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve( - angle_degrees, (-5, -5), (-5, 5)) - self.assertEqual(3, result.faces().size()) - self.assertEqual(2, result.vertices().size()) - self.assertEqual(3, result.edges().size()) - result = Workplane("XY").rect( - rectangle_width, rectangle_length).revolve(270.0, (-5, -5), (-5, 5)) - self.assertEqual(5, result.faces().size()) - self.assertEqual(6, result.vertices().size()) - self.assertEqual(9, result.edges().size()) - - # Testing all of the above without combine - result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve( - angle_degrees, (-5, -5), (-5, 5), False) - self.assertEqual(3, result.faces().size()) - self.assertEqual(2, result.vertices().size()) - self.assertEqual(3, result.edges().size()) - result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve( - 270.0, (-5, -5), (-5, 5), False) - self.assertEqual(5, result.faces().size()) - self.assertEqual(6, result.vertices().size()) - self.assertEqual(9, result.edges().size()) - - def testRevolveDonut(self): - """ - Test creating a solid donut shape with square walls - :return: - """ - # The dimensions of the model. These can be modified rather than changing the - # shape's code directly. - rectangle_width = 10.0 - rectangle_length = 10.0 - angle_degrees = 360.0 - - result = Workplane("XY").rect(rectangle_width, rectangle_length, True)\ - .revolve(angle_degrees, (20, 0), (20, 10)) - self.assertEqual(4, result.faces().size()) - self.assertEqual(4, result.vertices().size()) - self.assertEqual(6, result.edges().size()) - - def testRevolveCone(self): - """ - Test creating a solid from a revolved triangle - :return: - """ - result = Workplane("XY").lineTo(0, 10).lineTo(5, 0).close().revolve() - self.assertEqual(2, result.faces().size()) - self.assertEqual(2, result.vertices().size()) - self.assertEqual(2, result.edges().size()) - - def testSpline(self): - """ - Tests construction of splines - """ - pts = [ - (0, 0), - (0, 1), - (1, 2), - (2, 4) - ] - - # Spline path - just a smoke test - path = Workplane("XZ").spline(pts).val() - - # Closed spline - path_closed = Workplane("XZ").spline(pts,periodic=True).val() - self.assertTrue(path_closed.IsClosed()) - - # attempt to build a valid face - w = Wire.assembleEdges([path_closed,]) - f = Face.makeFromWires(w) - self.assertTrue(f.isValid()) - - # attempt to build an invalid face - w = Wire.assembleEdges([path,]) - f = Face.makeFromWires(w) - self.assertFalse(f.isValid()) - - # Spline with explicit tangents - path_const = Workplane("XZ").spline(pts,tangents=((0,1),(1,0))).val() - self.assertFalse(path.tangentAt(0) == path_const.tangentAt(0)) - self.assertFalse(path.tangentAt(1) == path_const.tangentAt(1)) - - # test include current - path1 = Workplane("XZ").spline(pts[1:],includeCurrent=True).val() - self.assertAlmostEqual(path.Length(),path1.Length()) - - def testSweep(self): - """ - Tests the operation of sweeping a wire(s) along a path - """ - pts = [ - (0, 0), - (0, 1), - (1, 2), - (2, 4) - ] - - # Spline path - path = Workplane("XZ").spline(pts) - - # Test defaults - result = Workplane("XY").circle(1.0).sweep(path) - self.assertEqual(3, result.faces().size()) - self.assertEqual(3, result.edges().size()) - - # Test with makeSolid False - result = Workplane("XY").circle(1.0).sweep(path, makeSolid=False) - self.assertEqual(1, result.faces().size()) - self.assertEqual(3, result.edges().size()) - - # Test with isFrenet True - result = Workplane("XY").circle(1.0).sweep(path, isFrenet=True) - self.assertEqual(3, result.faces().size()) - self.assertEqual(3, result.edges().size()) - - # Test with makeSolid False and isFrenet True - result = Workplane("XY").circle(1.0).sweep( - path, makeSolid=False, isFrenet=True) - self.assertEqual(1, result.faces().size()) - self.assertEqual(3, result.edges().size()) - - # Test rectangle with defaults - result = Workplane("XY").rect(1.0, 1.0).sweep(path) - self.assertEqual(6, result.faces().size()) - self.assertEqual(12, result.edges().size()) - - # Polyline path - path = Workplane("XZ").polyline(pts) - - # Test defaults - result = Workplane("XY").circle(0.1).sweep(path,transition='transformed') - self.assertEqual(5, result.faces().size()) - self.assertEqual(7, result.edges().size()) - - # Polyline path and one inner profiles - path = Workplane("XZ").polyline(pts) - - # Test defaults - result = Workplane("XY").circle(0.2).circle(0.1).sweep(path,transition='transformed') - self.assertEqual(8, result.faces().size()) - self.assertEqual(14, result.edges().size()) - - # Polyline path and different transition settings - for t in ('transformed','right','round'): - path = Workplane("XZ").polyline(pts) - - result = Workplane("XY").circle(0.2).rect(0.2,0.1).rect(0.1,0.2)\ - .sweep(path,transition=t) - self.assertTrue(result.solids().val().isValid()) - - # Polyline path and multiple inner profiles - path = Workplane("XZ").polyline(pts) - - # Test defaults - result = Workplane("XY").circle(0.2).rect(0.2,0.1).rect(0.1,0.2)\ - .circle(0.1).sweep(path) - self.assertTrue(result.solids().val().isValid()) - - # Arc path - path = Workplane("XZ").threePointArc((1.0, 1.5), (0.0, 1.0)) - - # Test defaults - result = Workplane("XY").circle(0.1).sweep(path) - self.assertEqual(3, result.faces().size()) - self.assertEqual(3, result.edges().size()) - - def testMultisectionSweep(self): - """ - Tests the operation of sweeping along a list of wire(s) along a path - """ - - # X axis line length 20.0 - path = Workplane("XZ").moveTo(-10, 0).lineTo(10, 0) - - # Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0 - defaultSweep = Workplane("YZ").workplane(offset=-10.0).circle(2.0). \ - workplane(offset=10.0).circle(1.0). \ - workplane(offset=10.0).circle(2.0).sweep(path, multisection=True) - - # We can sweep thrue different shapes - recttocircleSweep = Workplane("YZ").workplane(offset=-10.0).rect(2.0, 2.0). \ - workplane(offset=8.0).circle(1.0).workplane(offset=4.0).circle(1.0). \ - workplane(offset=8.0).rect(2.0, 2.0).sweep(path, multisection=True) - - circletorectSweep = Workplane("YZ").workplane(offset=-10.0).circle(1.0). \ - workplane(offset=7.0).rect(2.0, 2.0).workplane(offset=6.0).rect(2.0, 2.0). \ - workplane(offset=7.0).circle(1.0).sweep(path, multisection=True) - - # Placement of the Shape is important otherwise could produce unexpected shape - specialSweep = Workplane("YZ").circle(1.0).workplane(offset=10.0).rect(2.0, 2.0). \ - sweep(path, multisection=True) - - # Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0 - path = Workplane("XZ").moveTo(-5, 4).lineTo(0, 4). \ - threePointArc((4, 0), (0, -4)).lineTo(-5, -4) - - # Placement of different shapes should follow the path - # cylinder r=1.5 along first line - # then sweep allong arc from r=1.5 to r=1.0 - # then cylinder r=1.0 along last line - arcSweep = Workplane("YZ").workplane(offset=-5).moveTo(0, 4).circle(1.5). \ - workplane(offset=5).circle(1.5). \ - moveTo(0, -8).circle(1.0). \ - workplane(offset=-5).circle(1.0). \ - sweep(path, multisection=True) - - # Test and saveModel - self.assertEqual(1, defaultSweep.solids().size()) - self.assertEqual(1, circletorectSweep.solids().size()) - self.assertEqual(1, recttocircleSweep.solids().size()) - self.assertEqual(1, specialSweep.solids().size()) - self.assertEqual(1, arcSweep.solids().size()) - self.saveModel(defaultSweep) - - def testTwistExtrude(self): - """ - Tests extrusion while twisting through an angle. - """ - profile = Workplane('XY').rect(10, 10) - r = profile.twistExtrude(10, 45, False) - - self.assertEqual(6, r.faces().size()) - - def testTwistExtrudeCombine(self): - """ - Tests extrusion while twisting through an angle, combining with other solids. - """ - profile = Workplane('XY').rect(10, 10) - r = profile.twistExtrude(10, 45) - - self.assertEqual(6, r.faces().size()) - - def testRectArray(self): - NUMX = 3 - NUMY = 3 - s = Workplane("XY").box(40, 40, 5, centered=(True, True, True)).faces( - ">Z").workplane().rarray(8.0, 8.0, NUMX, NUMY, True).circle(2.0).extrude(2.0) - #s = Workplane("XY").box(40,40,5,centered=(True,True,True)).faces(">Z").workplane().circle(2.0).extrude(2.0) - self.saveModel(s) - # 6 faces for the box, 2 faces for each cylinder - self.assertEqual(6 + NUMX * NUMY * 2, s.faces().size()) - - def testPolarArray(self): - radius = 10 - - # Test for proper number of elements - s = Workplane("XY").polarArray(radius, 0, 180, 1) - self.assertEqual(1, s.size()) - s = Workplane("XY").polarArray(radius, 0, 180, 6) - self.assertEqual(6, s.size()) - - # Test for proper placement when fill == True - s = Workplane("XY").polarArray(radius, 0, 180, 3) - self.assertAlmostEqual(0, s.objects[1].x) - self.assertAlmostEqual(radius, s.objects[1].y) - - # Test for proper placement when angle to fill is multiple of 360 deg - s = Workplane("XY").polarArray(radius, 0, 360, 4) - self.assertAlmostEqual(0, s.objects[1].x) - self.assertAlmostEqual(radius, s.objects[1].y) - - # Test for proper placement when fill == False - s = Workplane("XY").polarArray(radius, 0, 90, 3, fill=False) - self.assertAlmostEqual(0, s.objects[1].x) - self.assertAlmostEqual(radius, s.objects[1].y) - - # Test for proper operation of startAngle - s = Workplane("XY").polarArray(radius, 90, 180, 3) - self.assertAlmostEqual(0, s.objects[0].x) - self.assertAlmostEqual(radius, s.objects[0].y) - - def testNestedCircle(self): - s = Workplane("XY").box(40, 40, 5).pushPoints( - [(10, 0), (0, 10)]).circle(4).circle(2).extrude(4) - self.saveModel(s) - self.assertEqual(14, s.faces().size()) - - def testLegoBrick(self): - # test making a simple lego brick - # which of the below - - # inputs - lbumps = 8 - wbumps = 2 - - # lego brick constants - P = 8.0 # nominal pitch - c = 0.1 # clearance on each brick side - H = 1.2 * P # nominal height of a brick - bumpDiam = 4.8 # the standard bump diameter - # the nominal thickness of the walls, normally 1.5 - t = (P - (2 * c) - bumpDiam) / 2.0 - - postDiam = P - t # works out to 6.5 - total_length = lbumps * P - 2.0 * c - total_width = wbumps * P - 2.0 * c - - # build the brick - s = Workplane("XY").box(total_length, total_width, H) # make the base - s = s.faces("Z").workplane().rarray(P, P, lbumps, wbumps, True).circle( - bumpDiam / 2.0).extrude(1.8) # make the bumps on the top - - # add posts on the bottom. posts are different diameter depending on geometry - # solid studs for 1 bump, tubes for multiple, none for 1x1 - # this is cheating a little-- how to select the inner face from the shell? - tmp = s.faces(" 1 and wbumps > 1: - tmp = tmp.rarray(P, P, lbumps - 1, wbumps - 1, center=True).circle( - postDiam / 2.0).circle(bumpDiam / 2.0).extrude(H - t) - elif lbumps > 1: - tmp = tmp.rarray(P, P, lbumps - 1, 1, - center=True).circle(t).extrude(H - t) - elif wbumps > 1: - tmp = tmp.rarray(P, P, 1, wbumps - 1, - center=True).circle(t).extrude(H - t) - - self.saveModel(s) - - def testAngledHoles(self): - s = Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").workplane().transformed(offset=Vector(0, -1.5, 1.0), rotate=Vector(60, 0, 0))\ - .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.25) - self.saveModel(s) - self.assertEqual(10, s.faces().size()) - - def testTranslateSolid(self): - c = CQ(makeUnitCube()) - self.assertAlmostEqual(0.0, c.faces( - "Z').workplane().circle(0.125).extrude( - 0.5, True) # make a boss, not updating the original - self.assertEqual(8, r.faces().size()) # just the boss faces - self.assertEqual(6, c.faces().size()) # original is not modified - - def testSolidReferencesCombineTrue(self): - s = Workplane(Plane.XY()) - r = s.rect(2.0, 2.0).extrude(0.5) - # the result of course has 6 faces - self.assertEqual(6, r.faces().size()) - # the original workplane does not, because it did not have a solid initially - self.assertEqual(0, s.faces().size()) - - t = r.faces(">Z").workplane().rect(0.25, 0.25).extrude(0.5, True) - # of course the result has 11 faces - self.assertEqual(11, t.faces().size()) - # r (being the parent) remains unmodified - self.assertEqual(6, r.faces().size()) - self.saveModel(r) - - def testSolidReferenceCombineFalse(self): - s = Workplane(Plane.XY()) - r = s.rect(2.0, 2.0).extrude(0.5) - # the result of course has 6 faces - self.assertEqual(6, r.faces().size()) - # the original workplane does not, because it did not have a solid initially - self.assertEqual(0, s.faces().size()) - - t = r.faces(">Z").workplane().rect(0.25, 0.25).extrude(0.5, False) - # result has 6 faces, becuase it was not combined with the original - self.assertEqual(6, t.faces().size()) - self.assertEqual(6, r.faces().size()) # original is unmodified as well - # subseuent opertions use that context solid afterwards - - def testSimpleWorkplane(self): - """ - A simple square part with a hole in it - """ - s = Workplane(Plane.XY()) - r = s.rect(2.0, 2.0).extrude(0.5)\ - .faces(">Z").workplane()\ - .circle(0.25).cutBlind(-1.0) - - self.saveModel(r) - self.assertEqual(7, r.faces().size()) - - def testMultiFaceWorkplane(self): - """ - Test Creation of workplane from multiple co-planar face - selection. - """ - s = Workplane('XY').box(1, 1, 1).faces( - '>Z').rect(1, 0.5).cutBlind(-0.2) - - w = s.faces('>Z').workplane() - o = w.objects[0] # origin of the workplane - self.assertAlmostEqual(o.x, 0., 3) - self.assertAlmostEqual(o.y, 0., 3) - self.assertAlmostEqual(o.z, 0.5, 3) - - def testTriangularPrism(self): - s = Workplane("XY").lineTo(1, 0).lineTo(1, 1).close().extrude(0.2) - self.saveModel(s) - - def testMultiWireWorkplane(self): - """ - A simple square part with a hole in it-- but this time done as a single extrusion - with two wires, as opposed to s cut - """ - s = Workplane(Plane.XY()) - r = s.rect(2.0, 2.0).circle(0.25).extrude(0.5) - - self.saveModel(r) - self.assertEqual(7, r.faces().size()) - - def testConstructionWire(self): - """ - Tests a wire with several holes, that are based on the vertices of a square - also tests using a workplane plane other than XY - """ - s = Workplane(Plane.YZ()) - r = s.rect(2.0, 2.0).rect( - 1.3, 1.3, forConstruction=True).vertices().circle(0.125).extrude(0.5) - self.saveModel(r) - # 10 faces-- 6 plus 4 holes, the vertices of the second rect. - self.assertEqual(10, r.faces().size()) - - def testTwoWorkplanes(self): - """ - Tests a model that uses more than one workplane - """ - # base block - s = Workplane(Plane.XY()) - - # TODO: this syntax is nice, but the iteration might not be worth - # the complexity. - # the simpler and slightly longer version would be: - # r = s.rect(2.0,2.0).rect(1.3,1.3,forConstruction=True).vertices() - # for c in r.all(): - # c.circle(0.125).extrude(0.5,True) - r = s.rect(2.0, 2.0).rect( - 1.3, 1.3, forConstruction=True).vertices().circle(0.125).extrude(0.5) - - # side hole, blind deep 1.9 - t = r.faces(">Y").workplane().circle(0.125).cutBlind(-1.9) - self.saveModel(t) - self.assertEqual(12, t.faces().size()) - - def testCut(self): - """ - Tests the cut function by itself to catch the case where a Solid object is passed. - """ - s = Workplane(Plane.XY()) - currentS = s.rect(2.0, 2.0).extrude(0.5) - toCut = s.rect(1.0, 1.0).extrude(0.5) - - resS = currentS.cut(toCut.val()) - - self.assertEqual(10, resS.faces().size()) - - def testIntersect(self): - """ - Tests the intersect function. - """ - s = Workplane(Plane.XY()) - currentS = s.rect(2.0, 2.0).extrude(0.5) - toIntersect = s.rect(1.0, 1.0).extrude(1) - - resS = currentS.intersect(toIntersect.val()) - - self.assertEqual(6, resS.faces().size()) - self.assertAlmostEqual(resS.val().Volume(),0.5) - - resS = currentS.intersect(toIntersect) - - self.assertEqual(6, resS.faces().size()) - self.assertAlmostEqual(resS.val().Volume(),0.5) - - def testBoundingBox(self): - """ - Tests the boudingbox center of a model - """ - result0 = (Workplane("XY") - .moveTo(10, 0) - .lineTo(5, 0) - .threePointArc((3.9393, 0.4393), (3.5, 1.5)) - .threePointArc((3.0607, 2.5607), (2, 3)) - .lineTo(1.5, 3) - .threePointArc((0.4393, 3.4393), (0, 4.5)) - .lineTo(0, 13.5) - .threePointArc((0.4393, 14.5607), (1.5, 15)) - .lineTo(28, 15) - .lineTo(28, 13.5) - .lineTo(24, 13.5) - .lineTo(24, 11.5) - .lineTo(27, 11.5) - .lineTo(27, 10) - .lineTo(22, 10) - .lineTo(22, 13.2) - .lineTo(14.5, 13.2) - .lineTo(14.5, 10) - .lineTo(12.5, 10) - .lineTo(12.5, 13.2) - .lineTo(5.5, 13.2) - .lineTo(5.5, 2) - .threePointArc((5.793, 1.293), (6.5, 1)) - .lineTo(10, 1) - .close()) - result = result0.extrude(100) - bb_center = result.val().BoundingBox().center - self.saveModel(result) - self.assertAlmostEqual(14.0, bb_center.x, 3) - self.assertAlmostEqual(7.5, bb_center.y, 3) - self.assertAlmostEqual(50.0, bb_center.z, 3) - - # The following will raise with the default tolerance of TOL 1e-2 - bb = result.val().BoundingBox(tolerance=1e-3) - self.assertAlmostEqual(0.0, bb.xmin, 2) - self.assertAlmostEqual(28, bb.xmax, 2) - self.assertAlmostEqual(0.0, bb.ymin, 2) - self.assertAlmostEqual(15.0, bb.ymax, 2) - self.assertAlmostEqual(0.0, bb.zmin, 2) - self.assertAlmostEqual(100.0, bb.zmax, 2) - - def testCutThroughAll(self): - """ - Tests a model that uses more than one workplane - """ - # base block - s = Workplane(Plane.XY()) - r = s.rect(2.0, 2.0).rect( - 1.3, 1.3, forConstruction=True).vertices().circle(0.125).extrude(0.5) - - - # thru all without explicit face selection - t = r.circle(0.5).cutThruAll() - self.assertEqual(11, t.faces().size()) - - # side hole, thru all - t = t.faces(">Y").workplane().circle(0.125).cutThruAll() - self.saveModel(t) - self.assertEqual(13, t.faces().size()) - - def testCutToFaceOffsetNOTIMPLEMENTEDYET(self): - """ - Tests cutting up to a given face, or an offset from a face - """ - # base block - s = Workplane(Plane.XY()) - r = s.rect(2.0, 2.0).rect( - 1.3, 1.3, forConstruction=True).vertices().circle(0.125).extrude(0.5) - - # side hole, up to 0.1 from the last face - try: - t = r.faces(">Y").workplane().circle( - 0.125).cutToOffsetFromFace(r.faces().mminDist(Dir.Y), 0.1) - # should end up being a blind hole - self.assertEqual(10, t.faces().size()) - t.first().val().exportStep('c:/temp/testCutToFace.STEP') - except: - pass - # Not Implemented Yet - - def testWorkplaneOnExistingSolid(self): - "Tests extruding on an existing solid" - c = CQ(makeUnitCube()).faces(">Z").workplane().circle( - 0.25).circle(0.125).extrude(0.25) - self.saveModel(c) - self.assertEqual(10, c.faces().size()) - - def testWorkplaneCenterMove(self): - # this workplane is centered at x=0.5,y=0.5, the center of the upper face - s = Workplane("XY").box(1, 1, 1).faces(">Z").workplane( - ).center(-0.5, -0.5) # move the center to the corner - - t = s.circle(0.25).extrude(0.2) # make a boss - self.assertEqual(9, t.faces().size()) - self.saveModel(t) - - def testBasicLines(self): - "Make a triangluar boss" - global OUTDIR - s = Workplane(Plane.XY()) - - # TODO: extrude() should imply wire() if not done already - # most users dont understand what a wire is, they are just drawing - - r = s.lineTo(1.0, 0).lineTo(0, 1.0).close().wire().extrude(0.25) - r.val().exportStep(os.path.join(OUTDIR, 'testBasicLinesStep1.STEP')) - - # no faces on the original workplane - self.assertEqual(0, s.faces().size()) - # 5 faces on newly created object - self.assertEqual(5, r.faces().size()) - - # now add a circle through a side face - r1 = r.faces("+XY").workplane().circle(0.08).cutThruAll() - self.assertEqual(6, r1.faces().size()) - r1.val().exportStep(os.path.join(OUTDIR, 'testBasicLinesXY.STEP')) - - # now add a circle through a top - r2 = r1.faces("+Z").workplane().circle(0.08).cutThruAll() - self.assertEqual(9, r2.faces().size()) - r2.val().exportStep(os.path.join(OUTDIR, 'testBasicLinesZ.STEP')) - - self.saveModel(r2) - - def test2DDrawing(self): - """ - Draw things like 2D lines and arcs, should be expanded later to include all 2D constructs - """ - s = Workplane(Plane.XY()) - r = s.lineTo(1.0, 0.0) \ - .lineTo(1.0, 1.0) \ - .threePointArc((1.0, 1.5), (0.0, 1.0)) \ - .lineTo(0.0, 0.0) \ - .moveTo(1.0, 0.0) \ - .lineTo(2.0, 0.0) \ - .lineTo(2.0, 2.0) \ - .threePointArc((2.0, 2.5), (0.0, 2.0)) \ - .lineTo(-2.0, 2.0) \ - .lineTo(-2.0, 0.0) \ - .close() - - self.assertEqual(1, r.wires().size()) - - # Test the *LineTo functions - s = Workplane(Plane.XY()) - r = s.hLineTo(1.0).vLineTo(1.0).hLineTo(0.0).close() - - self.assertEqual(1, r.wire().size()) - self.assertEqual(4, r.edges().size()) - - # Test the *Line functions - s = Workplane(Plane.XY()) - r = s.hLine(1.0).vLine(1.0).hLine(-1.0).close() - - self.assertEqual(1, r.wire().size()) - self.assertEqual(4, r.edges().size()) - - # Test the move function - s = Workplane(Plane.XY()) - r = s.move(1.0, 1.0).hLine(1.0).vLine(1.0).hLine(-1.0).close() - - self.assertEqual(1, r.wire().size()) - self.assertEqual(4, r.edges().size()) - self.assertEqual((1.0, 1.0), - (r.vertices(selectors.NearestToPointSelector((0.0, 0.0, 0.0))) - .first().val().X, - r.vertices( - selectors.NearestToPointSelector((0.0, 0.0, 0.0))) - .first().val().Y)) - - # Test the sagittaArc and radiusArc functions - a1 = Workplane(Plane.YZ()).threePointArc((5, 1), (10, 0)) - a2 = Workplane(Plane.YZ()).sagittaArc((10, 0), -1) - a3 = Workplane(Plane.YZ()).threePointArc((6, 2), (12, 0)) - a4 = Workplane(Plane.YZ()).radiusArc((12, 0), -10) - - assert(a1.edges().first().val().geomType() == "CIRCLE") - assert(a2.edges().first().val().geomType() == "CIRCLE") - assert(a3.edges().first().val().geomType() == "CIRCLE") - assert(a4.edges().first().val().geomType() == "CIRCLE") - - assert(a1.edges().first().val().Length() == a2.edges().first().val().Length()) - assert(a3.edges().first().val().Length() == a4.edges().first().val().Length()) - - def testPolarLines(self): - """ - Draw some polar lines and check expected results - """ - - # Test the PolarLine* functions - s = Workplane(Plane.XY()) - r = s.polarLine(10, 45) \ - .polarLineTo(10, -45) \ - .polarLine(10, -180) \ - .polarLine(-10, -90) \ - .close() - - # a single wire, 5 edges - self.assertEqual(1, r.wires().size()) - self.assertEqual(5, r.wires().edges().size()) - - def testLargestDimension(self): - """ - Tests the largestDimension function when no solids are on the stack and when there are - """ - r = Workplane('XY').box(1, 1, 1) - dim = r.largestDimension() - - self.assertAlmostEqual(8.7, dim, 1) - - r = Workplane('XY') - dim = r.largestDimension() - - self.assertEqual(-1, dim) - - def testOccBottle(self): - """ - Make the OCC bottle example. - """ - - L = 20.0 - w = 6.0 - t = 3.0 - - s = Workplane(Plane.XY()) - # draw half the profile of the bottle - p = s.center(-L / 2.0, 0).vLine(w / 2.0).threePointArc((L / 2.0, w / 2.0 + t), (L, w / 2.0)).vLine(-w / 2.0).mirrorX()\ - .extrude(30.0, True) - - # make the neck - p.faces(">Z").workplane().circle(3.0).extrude( - 2.0, True) # .edges().fillet(0.05) - - # make a shell - p.faces(">Z").shell(0.3) - self.saveModel(p) - - def testSplineShape(self): - """ - Tests making a shape with an edge that is a spline - """ - s = Workplane(Plane.XY()) - sPnts = [ - (2.75, 1.5), - (2.5, 1.75), - (2.0, 1.5), - (1.5, 1.0), - (1.0, 1.25), - (0.5, 1.0), - (0, 1.0) - ] - r = s.lineTo(3.0, 0).lineTo(3.0, 1.0).spline(sPnts).close() - r = r.extrude(0.5) - self.saveModel(r) - - def testSimpleMirror(self): - """ - Tests a simple mirroring operation - """ - s = Workplane("XY").lineTo(2, 2).threePointArc((3, 1), (2, 0)) \ - .mirrorX().extrude(0.25) - self.assertEqual(6, s.faces().size()) - self.saveModel(s) - - def testUnorderedMirror(self): - """ - Tests whether or not a wire can be mirrored if its mirror won't connect to it - """ - r = 20 - s = 7 - t = 1.5 - - points = [ - (0, 0), - (0, t / 2), - (r / 2 - 1.5 * t, r / 2 - t), - (s / 2, r / 2 - t), - (s / 2, r / 2), - (r / 2, r / 2), - (r / 2, s / 2), - (r / 2 - t, s / 2), - (r / 2 - t, r / 2 - 1.5 * t), - (t / 2, 0) - ] - - r = Workplane("XY").polyline(points).mirrorX() - - self.assertEqual(1, r.wires().size()) - self.assertEqual(18, r.edges().size()) - - # try the same with includeCurrent=True - r = Workplane("XY").polyline(points[1:],includeCurrent=True).mirrorX() - - self.assertEqual(1, r.wires().size()) - self.assertEqual(18, r.edges().size()) - - - def testChainedMirror(self): - """ - Tests whether or not calling mirrorX().mirrorY() works correctly - """ - r = 20 - s = 7 - t = 1.5 - - points = [ - (0, 0), - (0, t/2), - (r/2-1.5*t, r/2-t), - (s/2, r/2-t), - (s/2, r/2), - (r/2, r/2), - (r/2, s/2), - (r/2-t, s/2), - (r/2-t, r/2-1.5*t), - (t/2, 0) - ] - - r = Workplane("XY").polyline(points).mirrorX().mirrorY() \ - .extrude(1).faces('>Z') - - self.assertEquals(1, r.wires().size()) - self.assertEquals(32, r.edges().size()) - - # TODO: Re-work testIbeam test below now that chaining works - # TODO: Add toLocalCoords and toWorldCoords tests - - def testIbeam(self): - """ - Make an ibeam. demonstrates fancy mirroring - """ - s = Workplane(Plane.XY()) - L = 100.0 - H = 20.0 - W = 20.0 - - t = 1.0 - # TODO: for some reason doing 1/4 of the profile and mirroring twice ( .mirrorX().mirrorY() ) - # did not work, due to a bug in freecad-- it was losing edges when creating a composite wire. - # i just side-stepped it for now - - pts = [ - (0, 0), - (0, H / 2.0), - (W / 2.0, H / 2.0), - (W / 2.0, (H / 2.0 - t)), - (t / 2.0, (H / 2.0 - t)), - (t / 2.0, (t - H / 2.0)), - (W / 2.0, (t - H / 2.0)), - (W / 2.0, H / -2.0), - (0, H / -2.0) - ] - r = s.polyline(pts).mirrorY() # these other forms also work - res = r.extrude(L) - self.saveModel(res) - - def testCone(self): - """ - Tests that a simple cone works - """ - s = Solid.makeCone(0, 1.0, 2.0) - t = CQ(s) - self.saveModel(t) - self.assertEqual(2, t.faces().size()) - - def testFillet(self): - """ - Tests filleting edges on a solid - """ - c = CQ(makeUnitCube()).faces(">Z").workplane().circle( - 0.25).extrude(0.25, True).edges("|Z").fillet(0.2) - self.saveModel(c) - self.assertEqual(12, c.faces().size()) - - def testChamfer(self): - """ - Test chamfer API with a box shape - """ - cube = CQ(makeUnitCube()).faces(">Z").chamfer(0.1) - self.saveModel(cube) - self.assertEqual(10, cube.faces().size()) - - def testChamferAsymmetrical(self): - """ - Test chamfer API with a box shape for asymmetrical lengths - """ - cube = CQ(makeUnitCube()).faces(">Z").chamfer(0.1, 0.2) - self.saveModel(cube) - self.assertEqual(10, cube.faces().size()) - - # test if edge lengths are different - edge = cube.edges(">Z").vals()[0] - self.assertAlmostEqual(0.6, edge.Length(), 3) - edge = cube.edges("|Z").vals()[0] - self.assertAlmostEqual(0.9, edge.Length(), 3) - - def testChamferCylinder(self): - """ - Test chamfer API with a cylinder shape - """ - cylinder = Workplane("XY").circle( - 1).extrude(1).faces(">Z").chamfer(0.1) - self.saveModel(cylinder) - self.assertEqual(4, cylinder.faces().size()) - - def testCounterBores(self): - """ - Tests making a set of counterbored holes in a face - """ - c = CQ(makeCube(3.0)) - pnts = [ - (-1.0, -1.0), (0.0, 0.0), (1.0, 1.0) - ] - c = c.faces(">Z").workplane().pushPoints( - pnts).cboreHole(0.1, 0.25, 0.25, 0.75) - self.assertEqual(18, c.faces().size()) - self.saveModel(c) - - # Tests the case where the depth of the cboreHole is not specified - c2 = CQ(makeCube(3.0)) - pnts = [ - (-1.0, -1.0), (0.0, 0.0), (1.0, 1.0) - ] - c2 = c2.faces(">Z").workplane().pushPoints( - pnts).cboreHole(0.1, 0.25, 0.25) - self.assertEqual(15, c2.faces().size()) - - def testCounterSinks(self): - """ - Tests countersinks - """ - s = Workplane(Plane.XY()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - def testSplitKeepingHalf(self): - """ - Tests splitting a solid - """ - - # drill a hole in the side - c = CQ(makeUnitCube()).faces( - ">Z").workplane().circle(0.25).cutThruAll() - - self.assertEqual(7, c.faces().size()) - - # now cut it in half sideways - result = c.faces(">Y").workplane(-0.5).split(keepTop=True) - self.saveModel(result) - self.assertEqual(8, result.faces().size()) - - def testSplitKeepingBoth(self): - """ - Tests splitting a solid - """ - - # drill a hole in the side - c = CQ(makeUnitCube()).faces( - ">Z").workplane().circle(0.25).cutThruAll() - self.assertEqual(7, c.faces().size()) - - # now cut it in half sideways - result = c.faces( - ">Y").workplane(-0.5).split(keepTop=True, keepBottom=True) - - # stack will have both halves, original will be unchanged - # two solids are on the stack, eac - self.assertEqual(2, result.solids().size()) - self.assertEqual(8, result.solids().item(0).faces().size()) - self.assertEqual(8, result.solids().item(1).faces().size()) - - def testSplitKeepingBottom(self): - """ - Tests splitting a solid improperly - """ - # Drill a hole in the side - c = CQ(makeUnitCube()).faces( - ">Z").workplane().circle(0.25).cutThruAll() - self.assertEqual(7, c.faces().size()) - - # Now cut it in half sideways - result = c.faces( - ">Y").workplane(-0.5).split(keepTop=False, keepBottom=True) - - # stack will have both halves, original will be unchanged - # one solid is on the stack - self.assertEqual(1, result.solids().size()) - self.assertEqual(8, result.solids().item(0).faces().size()) - - def testBoxDefaults(self): - """ - Tests creating a single box - """ - s = Workplane("XY").box(2, 3, 4) - self.assertEqual(1, s.solids().size()) - self.saveModel(s) - - def testSimpleShell(self): - """ - Create s simple box - """ - s = Workplane("XY").box(2, 2, 2).faces("+Z").shell(0.05) - self.saveModel(s) - self.assertEqual(23, s.faces().size()) - - def testOpenCornerShell(self): - s = Workplane("XY").box(1, 1, 1) - s1 = s.faces("+Z") - s1.add(s.faces("+Y")).add(s.faces("+X")) - self.saveModel(s1.shell(0.2)) - - # Tests the list option variation of add - s1 = s.faces("+Z") - s1.add(s.faces("+Y")).add([s.faces("+X")]) - - # Tests the raw object option variation of add - s1 = s.faces("+Z") - s1.add(s.faces("+Y")).add(s.faces("+X").val().wrapped) - - def testTopFaceFillet(self): - s = Workplane("XY").box(1, 1, 1).faces("+Z").edges().fillet(0.1) - self.assertEqual(s.faces().size(), 10) - self.saveModel(s) - - def testBoxPointList(self): - """ - Tests creating an array of boxes - """ - s = Workplane("XY").rect(4.0, 4.0, forConstruction=True).vertices().box( - 0.25, 0.25, 0.25, combine=True) - # 1 object, 4 solids because the object is a compound - self.assertEqual(4, s.solids().size()) - self.assertEqual(1, s.size()) - self.saveModel(s) - - s = Workplane("XY").rect(4.0, 4.0, forConstruction=True).vertices().box( - 0.25, 0.25, 0.25, combine=False) - # 4 objects, 4 solids, because each is a separate solid - self.assertEqual(4, s.size()) - self.assertEqual(4, s.solids().size()) - - def testBoxCombine(self): - s = Workplane("XY").box(4, 4, 0.5).faces(">Z").workplane().rect( - 3, 3, forConstruction=True).vertices().box(0.25, 0.25, 0.25, combine=True) - - self.saveModel(s) - self.assertEqual(1, s.solids().size()) # we should have one big solid - # should have 26 faces. 6 for the box, and 4x5 for the smaller cubes - self.assertEqual(26, s.faces().size()) - - def testSphereDefaults(self): - s = Workplane("XY").sphere(10) - # self.saveModel(s) # Until FreeCAD fixes their sphere operation - self.assertEqual(1, s.solids().size()) - self.assertEqual(1, s.faces().size()) - - def testSphereCustom(self): - s = Workplane("XY").sphere(10, angle1=0, angle2=90, - angle3=360, centered=(False, False, False)) - self.saveModel(s) - self.assertEqual(1, s.solids().size()) - self.assertEqual(2, s.faces().size()) - - def testSpherePointList(self): - s = Workplane("XY").rect( - 4.0, 4.0, forConstruction=True).vertices().sphere(0.25, combine=False) - # self.saveModel(s) # Until FreeCAD fixes their sphere operation - self.assertEqual(4, s.solids().size()) - self.assertEqual(4, s.faces().size()) - - def testSphereCombine(self): - s = Workplane("XY").rect( - 4.0, 4.0, forConstruction=True).vertices().sphere(2.25, combine=True) - # self.saveModel(s) # Until FreeCAD fixes their sphere operation - self.assertEqual(1, s.solids().size()) - self.assertEqual(4, s.faces().size()) - - def testQuickStartXY(self): - s = Workplane(Plane.XY()).box(2, 4, 0.5).faces(">Z").workplane().rect(1.5, 3.5, forConstruction=True)\ - .vertices().cskHole(0.125, 0.25, 82, depth=None) - self.assertEqual(1, s.solids().size()) - self.assertEqual(14, s.faces().size()) - self.saveModel(s) - - def testQuickStartYZ(self): - s = Workplane(Plane.YZ()).box(2, 4, 0.5).faces(">X").workplane().rect(1.5, 3.5, forConstruction=True)\ - .vertices().cskHole(0.125, 0.25, 82, depth=None) - self.assertEqual(1, s.solids().size()) - self.assertEqual(14, s.faces().size()) - self.saveModel(s) - - def testQuickStartXZ(self): - s = Workplane(Plane.XZ()).box(2, 4, 0.5).faces(">Y").workplane().rect(1.5, 3.5, forConstruction=True)\ - .vertices().cskHole(0.125, 0.25, 82, depth=None) - self.assertEqual(1, s.solids().size()) - self.assertEqual(14, s.faces().size()) - self.saveModel(s) - - def testDoubleTwistedLoft(self): - s = Workplane("XY").polygon(8, 20.0).workplane(offset=4.0).transformed( - rotate=Vector(0, 0, 15.0)).polygon(8, 20).loft() - s2 = Workplane("XY").polygon(8, 20.0).workplane( - offset=-4.0).transformed(rotate=Vector(0, 0, 15.0)).polygon(8, 20).loft() - # self.assertEquals(10,s.faces().size()) - # self.assertEquals(1,s.solids().size()) - s3 = s.combineSolids(s2) - self.saveModel(s3) - - def testTwistedLoft(self): - s = Workplane("XY").polygon(8, 20.0).workplane(offset=4.0).transformed( - rotate=Vector(0, 0, 15.0)).polygon(8, 20).loft() - self.assertEqual(10, s.faces().size()) - self.assertEqual(1, s.solids().size()) - self.saveModel(s) - - def testUnions(self): - # duplicates a memory problem of some kind reported when combining lots of objects - s = Workplane("XY").rect(0.5, 0.5).extrude(5.0) - o = [] - beginTime = time.time() - for i in range(15): - t = Workplane("XY").center(10.0 * i, 0).rect(0.5, 0.5).extrude(5.0) - o.append(t) - - # union stuff - for oo in o: - s = s.union(oo) - print("Total time %0.3f" % (time.time() - beginTime)) - - # Test unioning a Solid object - s = Workplane(Plane.XY()) - currentS = s.rect(2.0, 2.0).extrude(0.5) - toUnion = s.rect(1.0, 1.0).extrude(1.0) - - resS = currentS.union(toUnion) - - self.assertEqual(11,resS.faces().size()) - - def testCombine(self): - s = Workplane(Plane.XY()) - objects1 = s.rect(2.0, 2.0).extrude(0.5).faces( - '>Z').rect(1.0, 1.0).extrude(0.5) - - objects1.combine() - - self.assertEqual(11, objects1.faces().size()) - - def testCombineSolidsInLoop(self): - # duplicates a memory problem of some kind reported when combining lots of objects - s = Workplane("XY").rect(0.5, 0.5).extrude(5.0) - o = [] - beginTime = time.time() - for i in range(15): - t = Workplane("XY").center(10.0 * i, 0).rect(0.5, 0.5).extrude(5.0) - o.append(t) - - # append the 'good way' - for oo in o: - s.add(oo) - s = s.combineSolids() - - print("Total time %0.3f" % (time.time() - beginTime)) - - self.saveModel(s) - - def testClean(self): - """ - Tests the `clean()` method which is called automatically. - """ - - # make a cube with a splitter edge on one of the faces - # autosimplify should remove the splitter - s = Workplane("XY").moveTo(0, 0).line(5, 0).line(5, 0).line(0, 10).\ - line(-10, 0).close().extrude(10) - - self.assertEqual(6, s.faces().size()) - - # test removal of splitter caused by union operation - s = Workplane("XY").box(10, 10, 10).union( - Workplane("XY").box(20, 10, 10)) - - self.assertEqual(6, s.faces().size()) - - # test removal of splitter caused by extrude+combine operation - s = Workplane("XY").box(10, 10, 10).faces(">Y").\ - workplane().rect(5, 10, 5).extrude(20) - - self.assertEqual(10, s.faces().size()) - - # test removal of splitter caused by double hole operation - s = Workplane("XY").box(10, 10, 10).faces(">Z").workplane().\ - hole(3, 5).faces(">Z").workplane().hole(3, 10) - - self.assertEqual(7, s.faces().size()) - - # test removal of splitter caused by cutThruAll - s = Workplane("XY").box(10, 10, 10).faces(">Y").workplane().\ - rect(10, 5).cutBlind(-5).faces(">Z").workplane().\ - center(0, 2.5).rect(5, 5).cutThruAll() - - self.assertEqual(18, s.faces().size()) - - # test removal of splitter with box - s = Workplane("XY").box(5, 5, 5).box(10, 5, 2) - - self.assertEqual(14, s.faces().size()) - - def testNoClean(self): - """ - Test the case when clean is disabled. - """ - # test disabling autoSimplify - s = Workplane("XY").moveTo(0, 0).line(5, 0).line(5, 0).line(0, 10).\ - line(-10, 0).close().extrude(10, clean=False) - self.assertEqual(7, s.faces().size()) - - s = Workplane("XY").box(10, 10, 10).\ - union(Workplane("XY").box(20, 10, 10), clean=False) - self.assertEqual(14, s.faces().size()) - - s = Workplane("XY").box(10, 10, 10).faces(">Y").\ - workplane().rect(5, 10, 5).extrude(20, clean=False) - - self.assertEqual(12, s.faces().size()) - - def testExplicitClean(self): - """ - Test running of `clean()` method explicitly. - """ - s = Workplane("XY").moveTo(0, 0).line(5, 0).line(5, 0).line(0, 10).\ - line(-10, 0).close().extrude(10, clean=False).clean() - self.assertEqual(6, s.faces().size()) - - def testPlanes(self): - """ - Test other planes other than the normal ones (XY, YZ) - """ - # ZX plane - s = Workplane(Plane.ZX()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - # YX plane - s = Workplane(Plane.YX()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - # YX plane - s = Workplane(Plane.YX()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - # ZY plane - s = Workplane(Plane.ZY()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - # front plane - s = Workplane(Plane.front()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - # back plane - s = Workplane(Plane.back()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - # left plane - s = Workplane(Plane.left()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - # right plane - s = Workplane(Plane.right()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - # top plane - s = Workplane(Plane.top()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - # bottom plane - s = Workplane(Plane.bottom()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) - self.saveModel(result) - - def testIsInside(self): - """ - Testing if one box is inside of another. - """ - box1 = Workplane(Plane.XY()).box(10, 10, 10) - box2 = Workplane(Plane.XY()).box(5, 5, 5) - - self.assertFalse(box2.val().BoundingBox().isInside(box1.val().BoundingBox())) - self.assertTrue(box1.val().BoundingBox().isInside(box2.val().BoundingBox())) - - def testCup(self): - """ - UOM = "mm" - - # - # PARAMETERS and PRESETS - # These parameters can be manipulated by end users - # - bottomDiameter = FloatParam(min=10.0,presets={'default':50.0,'tumbler':50.0,'shot':35.0,'tea':50.0,'saucer':100.0},group="Basics", desc="Bottom diameter") - topDiameter = FloatParam(min=10.0,presets={'default':85.0,'tumbler':85.0,'shot':50.0,'tea':51.0,'saucer':400.0 },group="Basics", desc="Top diameter") - thickness = FloatParam(min=0.1,presets={'default':2.0,'tumbler':2.0,'shot':2.66,'tea':2.0,'saucer':2.0},group="Basics", desc="Thickness") - height = FloatParam(min=1.0,presets={'default':80.0,'tumbler':80.0,'shot':59.0,'tea':125.0,'saucer':40.0},group="Basics", desc="Overall height") - lipradius = FloatParam(min=1.0,presets={'default':1.0,'tumbler':1.0,'shot':0.8,'tea':1.0,'saucer':1.0},group="Basics", desc="Lip Radius") - bottomThickness = FloatParam(min=1.0,presets={'default':5.0,'tumbler':5.0,'shot':10.0,'tea':10.0,'saucer':5.0},group="Basics", desc="BottomThickness") - - # - # Your build method. It must return a solid object - # - def build(): - br = bottomDiameter.value / 2.0 - tr = topDiameter.value / 2.0 - t = thickness.value - s1 = Workplane("XY").circle(br).workplane(offset=height.value).circle(tr).loft() - s2 = Workplane("XY").workplane(offset=bottomThickness.value).circle(br - t ).workplane(offset=height.value - t ).circle(tr - t).loft() - - cup = s1.cut(s2) - cup.faces(">Z").edges().fillet(lipradius.value) - return cup - """ - - # for some reason shell doesnt work on this simple shape. how disappointing! - td = 50.0 - bd = 20.0 - h = 10.0 - t = 1.0 - s1 = Workplane("XY").circle(bd).workplane(offset=h).circle(td).loft() - s2 = Workplane("XY").workplane(offset=t).circle( - bd - (2.0 * t)).workplane(offset=(h - t)).circle(td - (2.0 * t)).loft() - s3 = s1.cut(s2) - self.saveModel(s3) - - def testEnclosure(self): - """ - Builds an electronics enclosure - Original FreeCAD script: 81 source statements ,not including variables - This script: 34 - """ - - # parameter definitions - p_outerWidth = 100.0 # Outer width of box enclosure - p_outerLength = 150.0 # Outer length of box enclosure - p_outerHeight = 50.0 # Outer height of box enclosure - - p_thickness = 3.0 # Thickness of the box walls - p_sideRadius = 10.0 # Radius for the curves around the sides of the bo - # Radius for the curves on the top and bottom edges of the box - p_topAndBottomRadius = 2.0 - - # How far in from the edges the screwposts should be place. - p_screwpostInset = 12.0 - # nner Diameter of the screwpost holes, should be roughly screw diameter not including threads - p_screwpostID = 4.0 - # Outer Diameter of the screwposts.\nDetermines overall thickness of the posts - p_screwpostOD = 10.0 - - p_boreDiameter = 8.0 # Diameter of the counterbore hole, if any - p_boreDepth = 1.0 # Depth of the counterbore hole, if - # Outer diameter of countersink. Should roughly match the outer diameter of the screw head - p_countersinkDiameter = 0.0 - # Countersink angle (complete angle between opposite sides, not from center to one side) - p_countersinkAngle = 90.0 - # Whether to place the lid with the top facing down or not. - p_flipLid = True - # Height of lip on the underside of the lid.\nSits inside the box body for a snug fit. - p_lipHeight = 1.0 - - # outer shell - oshell = Workplane("XY").rect(p_outerWidth, p_outerLength).extrude( - p_outerHeight + p_lipHeight) - - # weird geometry happens if we make the fillets in the wrong order - if p_sideRadius > p_topAndBottomRadius: - oshell = oshell.edges("|Z").fillet(p_sideRadius)\ - .edges("#Z").fillet(p_topAndBottomRadius) - else: - oshell = oshell.edges("#Z").fillet(p_topAndBottomRadius)\ - .edges("|Z").fillet(p_sideRadius) - - # inner shell - ishell = oshell.faces("Z").workplane(-p_thickness)\ - .rect(POSTWIDTH, POSTLENGTH, forConstruction=True)\ - .vertices()\ - .circle(p_screwpostOD / 2.0)\ - .circle(p_screwpostID / 2.0)\ - .extrude((-1.0) * (p_outerHeight + p_lipHeight - p_thickness), True) - - # split lid into top and bottom parts - (lid, bottom) = box.faces(">Z").workplane(-p_thickness - - p_lipHeight).split(keepTop=True, keepBottom=True).all() # splits into two solids - - # translate the lid, and subtract the bottom from it to produce the lid inset - lowerLid = lid.translate((0, 0, -p_lipHeight)) - cutlip = lowerLid.cut(bottom).translate( - (p_outerWidth + p_thickness, 0, p_thickness - p_outerHeight + p_lipHeight)) - - # compute centers for counterbore/countersink or counterbore - topOfLidCenters = cutlip.faces(">Z").workplane().rect( - POSTWIDTH, POSTLENGTH, forConstruction=True).vertices() - - # add holes of the desired type - if p_boreDiameter > 0 and p_boreDepth > 0: - topOfLid = topOfLidCenters.cboreHole( - p_screwpostID, p_boreDiameter, p_boreDepth, (2.0) * p_thickness) - elif p_countersinkDiameter > 0 and p_countersinkAngle > 0: - topOfLid = topOfLidCenters.cskHole( - p_screwpostID, p_countersinkDiameter, p_countersinkAngle, (2.0) * p_thickness) - else: - topOfLid = topOfLidCenters.hole(p_screwpostID, (2.0) * p_thickness) - - # flip lid upside down if desired - if p_flipLid: - topOfLid.rotateAboutCenter((1, 0, 0), 180) - - # return the combined result - result = topOfLid.union(bottom) - - self.saveModel(result) - - def testExtrude(self): - """ - Test extrude - """ - r = 1. - h = 1. - decimal_places = 9. - - # extrude in one direction - s = Workplane("XY").circle(r).extrude(h, both=False) - - top_face = s.faces(">Z") - bottom_face = s.faces("Z") - bottom_face = s.faces("Z") - bottom_face = s.faces("Z") - bottom_face = s.faces(" 0) - - # cut a tapered hole - s = Workplane("XY").rect(2*r,2*r).extrude(2*h).faces('>Z').workplane()\ - .rect(r,r).cutBlind(-h, taper=t) - - middle_face = s.faces('>Z[-2]') - - self.assertTrue(middle_face.val().Area() < 1) - - def testClose(self): - # Close without endPoint and startPoint coincide. - # Create a half-circle - a = Workplane(Plane.XY()).sagittaArc((10, 0), 2).close().extrude(2) - - # Close when endPoint and startPoint coincide. - # Create a double half-circle - b = Workplane(Plane.XY()).sagittaArc((10, 0), 2).sagittaArc((0, 0), 2).close().extrude(2) - - # The b shape shall have twice the volume of the a shape. - self.assertAlmostEqual(a.val().Volume() * 2.0, b.val().Volume()) - - # Testcase 3 from issue #238 - thickness = 3.0 - length = 10.0 - width = 5.0 - - obj1 = Workplane('XY', origin=(0, 0, -thickness / 2)) \ - .moveTo(length / 2, 0).threePointArc((0, width / 2), (-length / 2, 0)) \ - .threePointArc((0, -width / 2), (length / 2, 0)) \ - .close().extrude(thickness) - - os_x = 8.0 # Offset in X - os_y = -19.5 # Offset in Y - - obj2 = Workplane('YZ', origin=(os_x, os_y, -thickness / 2)) \ - .moveTo(os_x + length / 2, os_y).sagittaArc((os_x -length / 2, os_y), width / 2) \ - .sagittaArc((os_x + length / 2, os_y), width / 2) \ - .close().extrude(thickness) - - # The obj1 shape shall have the same volume as the obj2 shape. - self.assertAlmostEqual(obj1.val().Volume(), obj2.val().Volume()) - - def testText(self): - - box = Workplane("XY" ).box(4, 4, 0.5) - - obj1 = box.faces('>Z').workplane()\ - .text('CQ 2.0',0.5,-.05,cut=True,halign='left',valign='bottom', font='Sans') - - #combined object should have smaller volume - self.assertGreater(box.val().Volume(),obj1.val().Volume()) - - obj2 = box.faces('>Z').workplane()\ - .text('CQ 2.0',0.5,.05,cut=False,combine=True, font='Sans') - - #combined object should have bigger volume - self.assertLess(box.val().Volume(),obj2.val().Volume()) - - #verify that the number of top faces is correct (NB: this is font specific) - self.assertEqual(len(obj2.faces('>Z').vals()),5) - - obj3 = box.faces('>Z').workplane()\ - .text('CQ 2.0',0.5,.05,cut=False,combine=False,halign='right',valign='top', font='Sans') - - #verify that the number of solids is correct - self.assertEqual(len(obj3.solids().vals()),5) - - def testParametricCurve(self): - - from math import sin, cos, pi - - k = 4 - r = 1 - - func = lambda t: ( r*(k+1)*cos(t) - r* cos((k+1)*t), - r*(k+1)*sin(t) - r* sin((k+1)*t)) - - res_open = Workplane('XY').parametricCurve(func).extrude(3) - - #open profile generates an invalid solid - self.assertFalse(res_open.solids().val().isValid()) - - res_closed = Workplane('XY').parametricCurve(func,start=0,stop=2*pi)\ - .extrude(3) - - #closed profile will generate a valid solid with 3 faces - self.assertTrue(res_closed.solids().val().isValid()) - self.assertEqual(len(res_closed.faces().vals()),3) - - def testMakeShellSolid(self): - - c0 = math.sqrt(2)/4 - vertices = [[c0, -c0, c0], [c0, c0, -c0], [-c0, c0, c0], [-c0, -c0, -c0]] - faces_ixs = [[0, 1, 2, 0], [1, 0, 3, 1], [2, 3, 0, 2], [3, 2, 1, 3]] - - faces = [] - for ixs in faces_ixs: - lines = [] - for v1,v2 in zip(ixs,ixs[1:]): - lines.append(Edge.makeLine(Vector(*vertices[v1]), - Vector(*vertices[v2]))) - wire = Wire.combine(lines) - faces.append(Face.makeFromWires(wire)) - - shell = Shell.makeShell(faces) - solid = Solid.makeSolid(shell) - - self.assertTrue(shell.isValid()) - self.assertTrue(solid.isValid()) - - self.assertEqual(len(solid.Vertices()),4) - self.assertEqual(len(solid.Faces()),4) - - def testIsInsideSolid(self): - # test solid - model = Workplane('XY').box(10,10,10) - solid = model.val() # get first object on stack - - self.assertTrue(solid.isInside((0,0,0))) - self.assertFalse(solid.isInside((10,10,10))) - self.assertTrue(solid.isInside((Vector(3,3,3)))) - self.assertFalse(solid.isInside((Vector(30.0,30.0,30.0)))) - - self.assertTrue(solid.isInside((0,0,4.99), tolerance=0.1)) - self.assertTrue(solid.isInside((0,0,5))) # check point on surface - self.assertTrue(solid.isInside((0,0,5.01), tolerance=0.1)) - self.assertFalse(solid.isInside((0,0,5.1), tolerance=0.1)) - - # test compound solid - model = Workplane('XY').box(10,10,10) - model = model.moveTo(50,50).box(10,10,10) - solid = model.val() - - self.assertTrue(solid.isInside((0,0,0))) - self.assertTrue(solid.isInside((50,50,0))) - self.assertFalse(solid.isInside((50,56,0))) - - # make sure raises on non solid - model = Workplane('XY').rect(10,10) - solid = model.val() - with self.assertRaises(AttributeError): - solid.isInside((0,0,0)) - - # test solid with an internal void - void = Workplane('XY').box(10,10,10) - model = Workplane('XY').box(100,100,100).cut(void) - solid = model.val() - - self.assertFalse(solid.isInside((0,0,0))) - self.assertTrue(solid.isInside((40,40,40))) - self.assertFalse(solid.isInside((55,55,55))) - - def testWorkplaneCenterOptions(self): - """ - Test options for specifiying origin of workplane - """ - decimal_places = 9 - - pts = [(0,0),(90,0),(90,30),(30,30),(30,60),(0.0,60)] - - r = Workplane("XY").polyline(pts).close().extrude(10.0) - - origin = r.faces(">Z").workplane(centerOption='ProjectedOrigin') \ - .plane.origin.toTuple() - self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places) - - origin = r.faces(">Z").workplane(centerOption='CenterOfMass') \ - .plane.origin.toTuple() - self.assertTupleAlmostEquals(origin, (37.5, 22.5, 10.0), decimal_places) - - origin = r.faces(">Z").workplane(centerOption='CenterOfBoundBox') \ - .plane.origin.toTuple() - self.assertTupleAlmostEquals(origin, (45.0, 30.0, 10.0), decimal_places) - - origin = r.faces(">Z").workplane(centerOption='ProjectedOrigin',origin=(30,10,20)) \ - .plane.origin.toTuple() - self.assertTupleAlmostEquals(origin, (30.0, 10.0, 10.0), decimal_places) - - origin = r.faces(">Z").workplane(centerOption='ProjectedOrigin',origin=Vector(30,10,20)) \ - .plane.origin.toTuple() - self.assertTupleAlmostEquals(origin, (30.0, 10.0, 10.0), decimal_places) - - with self.assertRaises(ValueError): - origin = r.faces(">Z").workplane(centerOption='undefined') - - # test case where plane origin is shifted with center call - r = r.faces(">Z").workplane(centerOption='ProjectedOrigin').center(30,0) \ - .hole(90) - - origin = r.faces(">Z").workplane(centerOption='ProjectedOrigin') \ - .plane.origin.toTuple() - self.assertTupleAlmostEquals(origin, (30.0, 0.0, 10.0), decimal_places) - - origin = r.faces(">Z").workplane(centerOption='ProjectedOrigin', origin=(0,0,0)) \ - .plane.origin.toTuple() - self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places) - - # make sure projection works in all directions - r = Workplane("YZ").polyline(pts).close().extrude(10.0) - - origin = r.faces(">X").workplane(centerOption='ProjectedOrigin') \ - .plane.origin.toTuple() - self.assertTupleAlmostEquals(origin, (10.0, 0.0, 0.0), decimal_places) - - origin = r.faces(">X").workplane(centerOption='CenterOfMass') \ - .plane.origin.toTuple() - self.assertTupleAlmostEquals(origin, (10.0, 37.5, 22.5), decimal_places) - - origin = r.faces(">X").workplane(centerOption='CenterOfBoundBox') \ - .plane.origin.toTuple() - self.assertTupleAlmostEquals(origin, (10.0, 45.0, 30.0), decimal_places) - - r = Workplane("XZ").polyline(pts).close().extrude(10.0) - - origin = r.faces("Z").workplane().slot2D(4,1,0).cutThruAll() - self.assertAlmostEqual(result.val().Volume(), 21.214601837, decimal_places) - result = box.faces(">Z").workplane().slot2D(4,1,0).cutBlind(-0.5) - self.assertAlmostEqual(result.val().Volume(), 23.107300918, decimal_places) - - # Test to see if slot is rotated correctly - result = Workplane("XY").slot2D(4,1,45).extrude(1) - point = result.faces(">Z").edges(">X").first().val().startPoint().toTuple() - self.assertTupleAlmostEquals(point, (0.707106781, 1.414213562, 1.0), decimal_places) - diff --git a/tests/__init__.py b/tests/__init__.py index 87035cbd..4d900158 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,21 +6,23 @@ import os def readFileAsString(fileName): - f = open(fileName, 'r') + f = open(fileName, "r") s = f.read() f.close() return s def writeStringToFile(strToWrite, fileName): - f = open(fileName, 'w') + f = open(fileName, "w") f.write(strToWrite) f.close() def makeUnitSquareWire(): V = Vector - return Wire.makePolygon([V(0, 0, 0), V(1, 0, 0), V(1, 1, 0), V(0, 1, 0), V(0, 0, 0)]) + return Wire.makePolygon( + [V(0, 0, 0), V(1, 0, 0), V(1, 1, 0), V(0, 1, 0), V(0, 0, 0)] + ) def makeUnitCube(): @@ -38,25 +40,23 @@ def toTuple(v): elif type(v) == Vector: return v.toTuple() else: - raise RuntimeError( - "dont know how to convert type %s to tuple" % str(type(v))) + raise RuntimeError("dont know how to convert type %s to tuple" % str(type(v))) class BaseTest(unittest.TestCase): - def assertTupleAlmostEquals(self, expected, actual, places): for i, j in zip(actual, expected): self.assertAlmostEqual(i, j, places) __all__ = [ - 'TestCadObjects', - 'TestCadQuery', - 'TestCQGI', - 'TestCQSelectors', - 'TestCQSelectors', - 'TestExporters', - 'TestImporters', - 'TestJupyter', - 'TestWorkplanes', + "TestCadObjects", + "TestCadQuery", + "TestCQGI", + "TestCQSelectors", + "TestCQSelectors", + "TestExporters", + "TestImporters", + "TestJupyter", + "TestWorkplanes", ] diff --git a/tests/test_cad_objects.py b/tests/test_cad_objects.py new file mode 100644 index 00000000..fa66db35 --- /dev/null +++ b/tests/test_cad_objects.py @@ -0,0 +1,406 @@ +# system modules +import math +import sys +import unittest +from tests import BaseTest +from OCP.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_Elips, gp, gp_XYZ +from OCP.BRepBuilderAPI import ( + BRepBuilderAPI_MakeVertex, + BRepBuilderAPI_MakeEdge, + BRepBuilderAPI_MakeFace, +) + +from OCP.GC import GC_MakeCircle + +from cadquery import * + +DEG2RAD = 2 * math.pi / 360 + + +class TestCadObjects(BaseTest): + def _make_circle(self): + + circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3), gp.DZ_s()), 2.0) + return Shape.cast(BRepBuilderAPI_MakeEdge(circle).Edge()) + + def _make_ellipse(self): + + ellipse = gp_Elips(gp_Ax2(gp_Pnt(1, 2, 3), gp.DZ_s()), 4.0, 2.0) + return Shape.cast(BRepBuilderAPI_MakeEdge(ellipse).Edge()) + + def testVectorConstructors(self): + v1 = Vector(1, 2, 3) + v2 = Vector((1, 2, 3)) + v3 = Vector(gp_Vec(1, 2, 3)) + v4 = Vector([1, 2, 3]) + v5 = Vector(gp_XYZ(1, 2, 3)) + + for v in [v1, v2, v3, v4, v5]: + self.assertTupleAlmostEquals((1, 2, 3), v.toTuple(), 4) + + v6 = Vector((1, 2)) + v7 = Vector([1, 2]) + v8 = Vector(1, 2) + + for v in [v6, v7, v8]: + self.assertTupleAlmostEquals((1, 2, 0), v.toTuple(), 4) + + v9 = Vector() + self.assertTupleAlmostEquals((0, 0, 0), v9.toTuple(), 4) + + v9.x = 1.0 + v9.y = 2.0 + v9.z = 3.0 + self.assertTupleAlmostEquals((1, 2, 3), (v9.x, v9.y, v9.z), 4) + + def testVertex(self): + """ + Tests basic vertex functions + """ + v = Vertex.makeVertex(1, 1, 1) + self.assertEqual(1, v.X) + self.assertEqual(Vector, type(v.Center())) + + def testBasicBoundingBox(self): + v = Vertex.makeVertex(1, 1, 1) + v2 = Vertex.makeVertex(2, 2, 2) + self.assertEqual(BoundBox, type(v.BoundingBox())) + self.assertEqual(BoundBox, type(v2.BoundingBox())) + + bb1 = v.BoundingBox().add(v2.BoundingBox()) + + # OCC uses some approximations + self.assertAlmostEqual(bb1.xlen, 1.0, 1) + + def testEdgeWrapperCenter(self): + e = self._make_circle() + + self.assertTupleAlmostEquals((1.0, 2.0, 3.0), e.Center().toTuple(), 3) + + def testEdgeWrapperEllipseCenter(self): + e = self._make_ellipse() + w = Wire.assembleEdges([e]) + self.assertTupleAlmostEquals( + (1.0, 2.0, 3.0), Face.makeFromWires(w).Center().toTuple(), 3 + ) + + def testEdgeWrapperMakeCircle(self): + halfCircleEdge = Edge.makeCircle( + radius=10, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=0, angle2=180 + ) + + # self.assertTupleAlmostEquals((0.0, 5.0, 0.0), halfCircleEdge.CenterOfBoundBox(0.0001).toTuple(),3) + self.assertTupleAlmostEquals( + (10.0, 0.0, 0.0), halfCircleEdge.startPoint().toTuple(), 3 + ) + self.assertTupleAlmostEquals( + (-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3 + ) + + def testEdgeWrapperMakeTangentArc(self): + tangent_arc = Edge.makeTangentArc( + Vector(1, 1), # starts at 1, 1 + Vector(0, 1), # tangent at start of arc is in the +y direction + Vector(2, 1), # arc curves 180 degrees and ends at 2, 1 + ) + self.assertTupleAlmostEquals((1, 1, 0), tangent_arc.startPoint().toTuple(), 3) + self.assertTupleAlmostEquals((2, 1, 0), tangent_arc.endPoint().toTuple(), 3) + self.assertTupleAlmostEquals( + (0, 1, 0), tangent_arc.tangentAt(locationParam=0).toTuple(), 3 + ) + self.assertTupleAlmostEquals( + (1, 0, 0), tangent_arc.tangentAt(locationParam=0.5).toTuple(), 3 + ) + self.assertTupleAlmostEquals( + (0, -1, 0), tangent_arc.tangentAt(locationParam=1).toTuple(), 3 + ) + + def testEdgeWrapperMakeEllipse1(self): + # Check x_radius > y_radius + x_radius, y_radius = 20, 10 + angle1, angle2 = -75.0, 90.0 + arcEllipseEdge = Edge.makeEllipse( + x_radius=x_radius, + y_radius=y_radius, + pnt=(0, 0, 0), + dir=(0, 0, 1), + angle1=angle1, + angle2=angle2, + ) + + start = ( + x_radius * math.cos(angle1 * DEG2RAD), + y_radius * math.sin(angle1 * DEG2RAD), + 0.0, + ) + end = ( + x_radius * math.cos(angle2 * DEG2RAD), + y_radius * math.sin(angle2 * DEG2RAD), + 0.0, + ) + self.assertTupleAlmostEquals(start, arcEllipseEdge.startPoint().toTuple(), 3) + self.assertTupleAlmostEquals(end, arcEllipseEdge.endPoint().toTuple(), 3) + + def testEdgeWrapperMakeEllipse2(self): + # Check x_radius < y_radius + x_radius, y_radius = 10, 20 + angle1, angle2 = 0.0, 45.0 + arcEllipseEdge = Edge.makeEllipse( + x_radius=x_radius, + y_radius=y_radius, + pnt=(0, 0, 0), + dir=(0, 0, 1), + angle1=angle1, + angle2=angle2, + ) + + start = ( + x_radius * math.cos(angle1 * DEG2RAD), + y_radius * math.sin(angle1 * DEG2RAD), + 0.0, + ) + end = ( + x_radius * math.cos(angle2 * DEG2RAD), + y_radius * math.sin(angle2 * DEG2RAD), + 0.0, + ) + self.assertTupleAlmostEquals(start, arcEllipseEdge.startPoint().toTuple(), 3) + self.assertTupleAlmostEquals(end, arcEllipseEdge.endPoint().toTuple(), 3) + + def testEdgeWrapperMakeCircleWithEllipse(self): + # Check x_radius == y_radius + x_radius, y_radius = 20, 20 + angle1, angle2 = 15.0, 60.0 + arcEllipseEdge = Edge.makeEllipse( + x_radius=x_radius, + y_radius=y_radius, + pnt=(0, 0, 0), + dir=(0, 0, 1), + angle1=angle1, + angle2=angle2, + ) + + start = ( + x_radius * math.cos(angle1 * DEG2RAD), + y_radius * math.sin(angle1 * DEG2RAD), + 0.0, + ) + end = ( + x_radius * math.cos(angle2 * DEG2RAD), + y_radius * math.sin(angle2 * DEG2RAD), + 0.0, + ) + self.assertTupleAlmostEquals(start, arcEllipseEdge.startPoint().toTuple(), 3) + self.assertTupleAlmostEquals(end, arcEllipseEdge.endPoint().toTuple(), 3) + + def testFaceWrapperMakePlane(self): + mplane = Face.makePlane(10, 10) + + self.assertTupleAlmostEquals((0.0, 0.0, 1.0), mplane.normalAt().toTuple(), 3) + + def testCenterOfBoundBox(self): + pass + + def testCombinedCenterOfBoundBox(self): + pass + + def testCompoundCenter(self): + """ + Tests whether or not a proper weighted center can be found for a compound + """ + + def cylinders(self, radius, height): + def _cyl(pnt): + # Inner function to build a cylinder + return Solid.makeCylinder(radius, height, pnt) + + # Combine all the cylinders into a single compound + r = self.eachpoint(_cyl, True).combineSolids() + + return r + + Workplane.cyl = cylinders + + # Now test. here we want weird workplane to see if the objects are transformed right + s = ( + Workplane("XY") + .rect(2.0, 3.0, forConstruction=True) + .vertices() + .cyl(0.25, 0.5) + ) + + self.assertEqual(4, len(s.val().Solids())) + self.assertTupleAlmostEquals((0.0, 0.0, 0.25), s.val().Center().toTuple(), 3) + + def testDot(self): + v1 = Vector(2, 2, 2) + v2 = Vector(1, -1, 1) + self.assertEqual(2.0, v1.dot(v2)) + + def testVectorAdd(self): + result = Vector(1, 2, 0) + Vector(0, 0, 3) + self.assertTupleAlmostEquals((1.0, 2.0, 3.0), result.toTuple(), 3) + + def testVectorOperators(self): + result = Vector(1, 1, 1) + Vector(2, 2, 2) + self.assertEqual(Vector(3, 3, 3), result) + + result = Vector(1, 2, 3) - Vector(3, 2, 1) + self.assertEqual(Vector(-2, 0, 2), result) + + result = Vector(1, 2, 3) * 2 + self.assertEqual(Vector(2, 4, 6), result) + + result = Vector(2, 4, 6) / 2 + self.assertEqual(Vector(1, 2, 3), result) + + self.assertEqual(Vector(-1, -1, -1), -Vector(1, 1, 1)) + + self.assertEqual(0, abs(Vector(0, 0, 0))) + self.assertEqual(1, abs(Vector(1, 0, 0))) + self.assertEqual((1 + 4 + 9) ** 0.5, abs(Vector(1, 2, 3))) + + def testVectorEquals(self): + a = Vector(1, 2, 3) + b = Vector(1, 2, 3) + c = Vector(1, 2, 3.000001) + self.assertEqual(a, b) + self.assertEqual(a, c) + + def testVectorProject(self): + """ + Test method to project vector to plane. + """ + decimal_places = 9 + + normal = Vector(1, 2, 3) + base = Vector(5, 7, 9) + x_dir = Vector(1, 0, 0) + + # test passing Plane object + point = Vector(10, 11, 12).projectToPlane(Plane(base, x_dir, normal)) + self.assertTupleAlmostEquals( + point.toTuple(), (59 / 7, 55 / 7, 51 / 7), decimal_places + ) + + def testMatrixCreationAndAccess(self): + def matrix_vals(m): + return [[m[r, c] for c in range(4)] for r in range(4)] + + # default constructor creates a 4x4 identity matrix + m = Matrix() + identity = [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + self.assertEqual(identity, matrix_vals(m)) + + vals4x4 = [ + [1.0, 0.0, 0.0, 1.0], + [0.0, 1.0, 0.0, 2.0], + [0.0, 0.0, 1.0, 3.0], + [0.0, 0.0, 0.0, 1.0], + ] + vals4x4_tuple = tuple(tuple(r) for r in vals4x4) + + # test constructor with 16-value input + m = Matrix(vals4x4) + self.assertEqual(vals4x4, matrix_vals(m)) + m = Matrix(vals4x4_tuple) + self.assertEqual(vals4x4, matrix_vals(m)) + + # test constructor with 12-value input (the last 4 are an implied + # [0,0,0,1]) + m = Matrix(vals4x4[:3]) + self.assertEqual(vals4x4, matrix_vals(m)) + m = Matrix(vals4x4_tuple[:3]) + self.assertEqual(vals4x4, matrix_vals(m)) + + # Test 16-value input with invalid values for the last 4 + invalid = [ + [1.0, 0.0, 0.0, 1.0], + [0.0, 1.0, 0.0, 2.0], + [0.0, 0.0, 1.0, 3.0], + [1.0, 2.0, 3.0, 4.0], + ] + with self.assertRaises(ValueError): + Matrix(invalid) + + # Test input with invalid size / nested types + with self.assertRaises(TypeError): + Matrix([[1, 2, 3, 4], [1, 2, 3], [1, 2, 3, 4]]) + with self.assertRaises(TypeError): + Matrix([1, 2, 3]) + + # Invalid sub-type + with self.assertRaises(TypeError): + Matrix([[1, 2, 3, 4], "abc", [1, 2, 3, 4]]) + + # test out-of-bounds access + m = Matrix() + with self.assertRaises(IndexError): + m[0, 4] + with self.assertRaises(IndexError): + m[4, 0] + with self.assertRaises(IndexError): + m["ab"] + + def testTranslate(self): + e = Edge.makeCircle(2, (1, 2, 3)) + e2 = e.translate(Vector(0, 0, 1)) + + self.assertTupleAlmostEquals((1.0, 2.0, 4.0), e2.Center().toTuple(), 3) + + def testVertices(self): + e = Shape.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0), gp_Pnt(1, 1, 0)).Edge()) + self.assertEqual(2, len(e.Vertices())) + + def testPlaneEqual(self): + # default orientation + self.assertEqual( + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), + ) + # moved origin + self.assertEqual( + Plane(origin=(2, 1, -1), xDir=(1, 0, 0), normal=(0, 0, 1)), + Plane(origin=(2, 1, -1), xDir=(1, 0, 0), normal=(0, 0, 1)), + ) + # moved x-axis + self.assertEqual( + Plane(origin=(0, 0, 0), xDir=(1, 1, 0), normal=(0, 0, 1)), + Plane(origin=(0, 0, 0), xDir=(1, 1, 0), normal=(0, 0, 1)), + ) + # moved z-axis + self.assertEqual( + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 1)), + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 1)), + ) + + def testPlaneNotEqual(self): + # type difference + for value in [None, 0, 1, "abc"]: + self.assertNotEqual( + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), value + ) + # origin difference + self.assertNotEqual( + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), + Plane(origin=(0, 0, 1), xDir=(1, 0, 0), normal=(0, 0, 1)), + ) + # x-axis difference + self.assertNotEqual( + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), + Plane(origin=(0, 0, 0), xDir=(1, 1, 0), normal=(0, 0, 1)), + ) + # z-axis difference + self.assertNotEqual( + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 1)), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py new file mode 100644 index 00000000..b25bf69a --- /dev/null +++ b/tests/test_cadquery.py @@ -0,0 +1,3350 @@ +""" + This module tests cadquery creation and manipulation functions + +""" +# system modules +import math, os.path, time, tempfile +from random import choice +from random import random +from random import randrange + +from pytest import approx + +# my modules +from cadquery import * +from cadquery import exporters +from tests import ( + BaseTest, + writeStringToFile, + makeUnitCube, + readFileAsString, + makeUnitSquareWire, + makeCube, +) + +# where unit test output will be saved +OUTDIR = tempfile.gettempdir() +SUMMARY_FILE = os.path.join(OUTDIR, "testSummary.html") + +SUMMARY_TEMPLATE = """ + + + + + + +""" + +TEST_RESULT_TEMPLATE = """ +

%(name)s

+ %(svg)s +
+ +""" + +# clean up any summary file that is in the output directory. +# i know, this sux, but there is no other way to do this in 2.6, as we cannot do class fixutres till 2.7 +writeStringToFile(SUMMARY_TEMPLATE, SUMMARY_FILE) + + +class TestCadQuery(BaseTest): + def tearDown(self): + """ + Update summary with data from this test. + This is a really hackey way of doing it-- we get a startup event from module load, + but there is no way in unittest to get a single shutdown event-- except for stuff in 2.7 and above + + So what we do here is to read the existing file, stick in more content, and leave it + """ + svgFile = os.path.join(OUTDIR, self._testMethodName + ".svg") + + # all tests do not produce output + if os.path.exists(svgFile): + existingSummary = readFileAsString(SUMMARY_FILE) + svgText = readFileAsString(svgFile) + svgText = svgText.replace( + '', "" + ) + + # now write data into the file + # the content we are replacing it with also includes the marker, so it can be replaced again + existingSummary = existingSummary.replace( + "", + TEST_RESULT_TEMPLATE % (dict(svg=svgText, name=self._testMethodName)), + ) + + writeStringToFile(existingSummary, SUMMARY_FILE) + + def saveModel(self, shape): + """ + shape must be a CQ object + Save models in SVG and STEP format + """ + shape.exportSvg(os.path.join(OUTDIR, self._testMethodName + ".svg")) + shape.val().exportStep(os.path.join(OUTDIR, self._testMethodName + ".step")) + + def testToOCC(self): + """ + Tests to make sure that a CadQuery object is converted correctly to a OCC object. + """ + r = Workplane("XY").rect(5, 5).extrude(5) + + r = r.toOCC() + + import OCP + + self.assertEqual(type(r), OCP.TopoDS.TopoDS_Compound) + + def testToSVG(self): + """ + Tests to make sure that a CadQuery object is converted correctly to SVG + """ + r = Workplane("XY").rect(5, 5).extrude(5) + + r_str = r.toSvg() + + # Make sure that a couple of sections from the SVG output make sense + self.assertTrue(r_str.index('path d="M') > 0) + self.assertTrue( + r_str.index('line x1="30" y1="-30" x2="58" y2="-15" stroke-width="3"') > 0 + ) + + def testCubePlugin(self): + """ + Tests a plugin that combines cubes together with a base + :return: + """ + # make the plugin method + + def makeCubes(self, length): + # self refers to the CQ or Workplane object + + # inner method that creates a cube + def _singleCube(pnt): + # pnt is a location in local coordinates + # since we're using eachpoint with useLocalCoordinates=True + return Solid.makeBox(length, length, length, pnt) + + # use CQ utility method to iterate over the stack, call our + # method, and convert to/from local coordinates. + return self.eachpoint(_singleCube, True) + + # link the plugin in + Workplane.makeCubes = makeCubes + + # call it + result = ( + Workplane("XY") + .box(6.0, 8.0, 0.5) + .faces(">Z") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + ) + result = result.makeCubes(1.0) + result = result.combineSolids() + self.saveModel(result) + self.assertEqual(1, result.solids().size()) + + def testCylinderPlugin(self): + """ + Tests a cylinder plugin. + The plugin creates cylinders of the specified radius and height for each item on the stack + + This is a very short plugin that illustrates just about the simplest possible + plugin + """ + + def cylinders(self, radius, height): + def _cyl(pnt): + # inner function to build a cylinder + return Solid.makeCylinder(radius, height, pnt) + + # combine all the cylinders into a single compound + r = self.eachpoint(_cyl, True).combineSolids() + return r + + Workplane.cyl = cylinders + + # now test. here we want weird workplane to see if the objects are transformed right + s = ( + Workplane(Plane(Vector((0, 0, 0)), Vector((1, -1, 0)), Vector((1, 1, 0)))) + .rect(2.0, 3.0, forConstruction=True) + .vertices() + .cyl(0.25, 0.5) + ) + self.assertEqual(4, s.solids().size()) + self.saveModel(s) + + def testPolygonPlugin(self): + """ + Tests a plugin to make regular polygons around points on the stack + + Demonstratings using eachpoint to allow working in local coordinates + to create geometry + """ + + def rPoly(self, nSides, diameter): + def _makePolygon(center): + # pnt is a vector in local coordinates + angle = 2.0 * math.pi / nSides + pnts = [] + for i in range(nSides + 1): + pnts.append( + center + + Vector( + (diameter / 2.0 * math.cos(angle * i)), + (diameter / 2.0 * math.sin(angle * i)), + 0, + ) + ) + return Wire.makePolygon(pnts) + + return self.eachpoint(_makePolygon, True) + + Workplane.rPoly = rPoly + + s = ( + Workplane("XY") + .box(4.0, 4.0, 0.25) + .faces(">Z") + .workplane() + .rect(2.0, 2.0, forConstruction=True) + .vertices() + .rPoly(5, 0.5) + .cutThruAll() + ) + + # 6 base sides, 4 pentagons, 5 sides each = 26 + self.assertEqual(26, s.faces().size()) + self.saveModel(s) + + def testPointList(self): + """ + Tests adding points and using them + """ + c = CQ(makeUnitCube()) + + s = c.faces(">Z").workplane().pushPoints([(-0.3, 0.3), (0.3, 0.3), (0, 0)]) + self.assertEqual(3, s.size()) + # TODO: is the ability to iterate over points with circle really worth it? + # maybe we should just require using all() and a loop for this. the semantics and + # possible combinations got too hard ( ie, .circle().circle() ) was really odd + body = s.circle(0.05).cutThruAll() + self.saveModel(body) + self.assertEqual(9, body.faces().size()) + + # Test the case when using eachpoint with only a blank workplane + def callback_fn(pnt): + self.assertEqual((0.0, 0.0), (pnt.x, pnt.y)) + + r = Workplane("XY") + r.objects = [] + r.eachpoint(callback_fn) + + def testWorkplaneFromFace(self): + # make a workplane on the top face + s = CQ(makeUnitCube()).faces(">Z").workplane() + r = s.circle(0.125).cutBlind(-2.0) + self.saveModel(r) + # the result should have 7 faces + self.assertEqual(7, r.faces().size()) + self.assertEqual(type(r.val()), Compound) + self.assertEqual(type(r.first().val()), Compound) + + def testFrontReference(self): + # make a workplane on the top face + s = CQ(makeUnitCube()).faces("front").workplane() + r = s.circle(0.125).cutBlind(-2.0) + self.saveModel(r) + # the result should have 7 faces + self.assertEqual(7, r.faces().size()) + self.assertEqual(type(r.val()), Compound) + self.assertEqual(type(r.first().val()), Compound) + + def testRotate(self): + """Test solid rotation at the CQ object level.""" + box = Workplane("XY").box(1, 1, 5) + box.rotate((0, 0, 0), (1, 0, 0), 90) + startPoint = box.faces("Z") + .circle(1.5) + .workplane(offset=3.0) + .rect(0.75, 0.5) + .loft(combine=True) + ) + self.saveModel(s) + # self.assertEqual(1,s.solids().size() ) + # self.assertEqual(8,s.faces().size() ) + + def testRevolveCylinder(self): + """ + Test creating a solid using the revolve operation. + :return: + """ + # The dimensions of the model. These can be modified rather than changing the + # shape's code directly. + rectangle_width = 10.0 + rectangle_length = 10.0 + angle_degrees = 360.0 + + # Test revolve without any options for making a cylinder + result = ( + Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve() + ) + self.assertEqual(3, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + + # Test revolve when only setting the angle to revolve through + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length, False) + .revolve(angle_degrees) + ) + self.assertEqual(3, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length, False) + .revolve(270.0) + ) + self.assertEqual(5, result.faces().size()) + self.assertEqual(6, result.vertices().size()) + self.assertEqual(9, result.edges().size()) + + # Test when passing revolve the angle and the axis of revolution's start point + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(angle_degrees, (-5, -5)) + ) + self.assertEqual(3, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(270.0, (-5, -5)) + ) + self.assertEqual(5, result.faces().size()) + self.assertEqual(6, result.vertices().size()) + self.assertEqual(9, result.edges().size()) + + # Test when passing revolve the angle and both the start and ends of the axis of revolution + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(angle_degrees, (-5, -5), (-5, 5)) + ) + self.assertEqual(3, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(270.0, (-5, -5), (-5, 5)) + ) + self.assertEqual(5, result.faces().size()) + self.assertEqual(6, result.vertices().size()) + self.assertEqual(9, result.edges().size()) + + # Testing all of the above without combine + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(angle_degrees, (-5, -5), (-5, 5), False) + ) + self.assertEqual(3, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(270.0, (-5, -5), (-5, 5), False) + ) + self.assertEqual(5, result.faces().size()) + self.assertEqual(6, result.vertices().size()) + self.assertEqual(9, result.edges().size()) + + def testRevolveDonut(self): + """ + Test creating a solid donut shape with square walls + :return: + """ + # The dimensions of the model. These can be modified rather than changing the + # shape's code directly. + rectangle_width = 10.0 + rectangle_length = 10.0 + angle_degrees = 360.0 + + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length, True) + .revolve(angle_degrees, (20, 0), (20, 10)) + ) + self.assertEqual(4, result.faces().size()) + self.assertEqual(4, result.vertices().size()) + self.assertEqual(6, result.edges().size()) + + def testRevolveCone(self): + """ + Test creating a solid from a revolved triangle + :return: + """ + result = Workplane("XY").lineTo(0, 10).lineTo(5, 0).close().revolve() + self.assertEqual(2, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(2, result.edges().size()) + + def testSpline(self): + """ + Tests construction of splines + """ + pts = [(0, 0), (0, 1), (1, 2), (2, 4)] + + # Spline path - just a smoke test + path = Workplane("XZ").spline(pts).val() + + # Closed spline + path_closed = Workplane("XZ").spline(pts, periodic=True).val() + self.assertTrue(path_closed.IsClosed()) + + # attempt to build a valid face + w = Wire.assembleEdges([path_closed,]) + f = Face.makeFromWires(w) + self.assertTrue(f.isValid()) + + # attempt to build an invalid face + w = Wire.assembleEdges([path,]) + f = Face.makeFromWires(w) + self.assertFalse(f.isValid()) + + # Spline with explicit tangents + path_const = Workplane("XZ").spline(pts, tangents=((0, 1), (1, 0))).val() + self.assertFalse(path.tangentAt(0) == path_const.tangentAt(0)) + self.assertFalse(path.tangentAt(1) == path_const.tangentAt(1)) + + # test include current + path1 = Workplane("XZ").spline(pts[1:], includeCurrent=True).val() + self.assertAlmostEqual(path.Length(), path1.Length()) + + # test tangents and offset plane + pts = [(0, 0), (-1, 1), (-2, 0), (-1, 0)] + tangents = [(0, 1), (1, 0)] + + path2 = Workplane("XY", (0, 0, 10)).spline(pts, tangents=tangents) + self.assertAlmostEqual(path2.val().tangentAt(0).z, 0) + + def testRotatedEllipse(self): + def rotatePoint(x, y, alpha): + # rotation matrix + a = alpha * DEG2RAD + r = ((math.cos(a), math.sin(a)), (-math.sin(a), math.cos(a))) + return ((x * r[0][0] + y * r[1][0]), (x * r[0][1] + y * r[1][1])) + + def ellipsePoints(r1, r2, a): + return (r1 * math.cos(a * DEG2RAD), r2 * math.sin(a * DEG2RAD)) + + DEG2RAD = math.pi / 180.0 + p0 = (10, 20) + a1, a2 = 30, -60 + r1, r2 = 20, 10 + ra = 25 + + sx_rot, sy_rot = rotatePoint(*ellipsePoints(r1, r2, a1), ra) + ex_rot, ey_rot = rotatePoint(*ellipsePoints(r1, r2, a2), ra) + + # startAtCurrent=False, sense = 1 + ellipseArc1 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, r2, startAtCurrent=False, angle1=a1, angle2=a2, rotation_angle=ra + ) + ) + start = ellipseArc1.vertices().objects[0] + end = ellipseArc1.vertices().objects[1] + + self.assertTupleAlmostEquals( + (start.X, start.Y), (p0[0] + sx_rot, p0[1] + sy_rot), 3 + ) + self.assertTupleAlmostEquals( + (end.X, end.Y), (p0[0] + ex_rot, p0[1] + ey_rot), 3 + ) + + # startAtCurrent=True, sense = 1 + ellipseArc2 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, r2, startAtCurrent=True, angle1=a1, angle2=a2, rotation_angle=ra + ) + ) + start = ellipseArc2.vertices().objects[0] + end = ellipseArc2.vertices().objects[1] + + self.assertTupleAlmostEquals( + (start.X, start.Y), (p0[0] + sx_rot - sx_rot, p0[1] + sy_rot - sy_rot), 3 + ) + self.assertTupleAlmostEquals( + (end.X, end.Y), (p0[0] + ex_rot - sx_rot, p0[1] + ey_rot - sy_rot), 3 + ) + + # startAtCurrent=False, sense = -1 + ellipseArc3 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, + r2, + startAtCurrent=False, + angle1=a1, + angle2=a2, + rotation_angle=ra, + sense=-1, + ) + ) + start = ellipseArc3.vertices().objects[0] + end = ellipseArc3.vertices().objects[1] + + # swap start and end points for coparison due to different sense + self.assertTupleAlmostEquals( + (start.X, start.Y), (p0[0] + ex_rot, p0[1] + ey_rot), 3 + ) + self.assertTupleAlmostEquals( + (end.X, end.Y), (p0[0] + sx_rot, p0[1] + sy_rot), 3 + ) + + # startAtCurrent=True, sense = -1 + ellipseArc4 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, + r2, + startAtCurrent=True, + angle1=a1, + angle2=a2, + rotation_angle=ra, + sense=-1, + makeWire=True, + ) + ) + + self.assertEqual(len(ellipseArc4.ctx.pendingWires), 1) + + start = ellipseArc4.vertices().objects[0] + end = ellipseArc4.vertices().objects[1] + + # swap start and end points for coparison due to different sense + self.assertTupleAlmostEquals( + (start.X, start.Y), (p0[0] + ex_rot - ex_rot, p0[1] + ey_rot - ey_rot), 3 + ) + self.assertTupleAlmostEquals( + (end.X, end.Y), (p0[0] + sx_rot - ex_rot, p0[1] + sy_rot - ey_rot), 3 + ) + + def testEllipseArcsClockwise(self): + ellipseArc = ( + Workplane("XY") + .moveTo(10, 15) + .ellipseArc(5, 4, -10, 190, 45, sense=-1, startAtCurrent=False) + ) + sp = ellipseArc.val().startPoint() + ep = ellipseArc.val().endPoint() + self.assertTupleAlmostEquals( + (sp.x, sp.y), (7.009330014275797, 11.027027582524015), 3 + ) + self.assertTupleAlmostEquals( + (ep.x, ep.y), (13.972972417475985, 17.990669985724203), 3 + ) + + ellipseArc = ( + ellipseArc.ellipseArc(5, 4, -10, 190, 315, sense=-1) + .ellipseArc(5, 4, -10, 190, 225, sense=-1) + .ellipseArc(5, 4, -10, 190, 135, sense=-1) + ) + ep = ellipseArc.val().endPoint() + self.assertTupleAlmostEquals((sp.x, sp.y), (ep.x, ep.y), 3) + + def testEllipseArcsCounterClockwise(self): + ellipseArc = ( + Workplane("XY") + .moveTo(10, 15) + .ellipseArc(5, 4, -10, 190, 45, startAtCurrent=False) + ) + sp = ellipseArc.val().startPoint() + ep = ellipseArc.val().endPoint() + self.assertTupleAlmostEquals( + (sp.x, sp.y), (13.972972417475985, 17.990669985724203), 3 + ) + self.assertTupleAlmostEquals( + (ep.x, ep.y), (7.009330014275797, 11.027027582524015), 3 + ) + + ellipseArc = ( + ellipseArc.ellipseArc(5, 4, -10, 190, 135) + .ellipseArc(5, 4, -10, 190, 225) + .ellipseArc(5, 4, -10, 190, 315) + ) + ep = ellipseArc.val().endPoint() + self.assertTupleAlmostEquals((sp.x, sp.y), (ep.x, ep.y), 3) + + def testEllipseCenterAndMoveTo(self): + # Whether we start from a center() call or a moveTo call, it should be the same ellipse Arc + p0 = (10, 20) + a1, a2 = 30, -60 + r1, r2 = 20, 10 + ra = 25 + + ellipseArc1 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, r2, startAtCurrent=False, angle1=a1, angle2=a2, rotation_angle=ra + ) + ) + sp1 = ellipseArc1.val().startPoint() + ep1 = ellipseArc1.val().endPoint() + + ellipseArc2 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, r2, startAtCurrent=False, angle1=a1, angle2=a2, rotation_angle=ra + ) + ) + sp2 = ellipseArc2.val().startPoint() + ep2 = ellipseArc2.val().endPoint() + + self.assertTupleAlmostEquals(sp1.toTuple(), sp2.toTuple(), 3) + self.assertTupleAlmostEquals(ep1.toTuple(), ep2.toTuple(), 3) + + def testMakeEllipse(self): + el = Wire.makeEllipse( + 1, 2, Vector(0, 0, 0), Vector(0, 0, 1), Vector(1, 0, 0), 0, 90, 45, True, + ) + + self.assertTrue(el.IsClosed()) + self.assertTrue(el.isValid()) + + def testSweep(self): + """ + Tests the operation of sweeping a wire(s) along a path + """ + pts = [(0, 0), (0, 1), (1, 2), (2, 4)] + + # Spline path + path = Workplane("XZ").spline(pts) + + # Test defaults + result = Workplane("XY").circle(1.0).sweep(path) + self.assertEqual(3, result.faces().size()) + self.assertEqual(3, result.edges().size()) + + # Test with makeSolid False + result = Workplane("XY").circle(1.0).sweep(path, makeSolid=False) + self.assertEqual(1, result.faces().size()) + self.assertEqual(3, result.edges().size()) + + # Test with isFrenet True + result = Workplane("XY").circle(1.0).sweep(path, isFrenet=True) + self.assertEqual(3, result.faces().size()) + self.assertEqual(3, result.edges().size()) + + # Test with makeSolid False and isFrenet True + result = Workplane("XY").circle(1.0).sweep(path, makeSolid=False, isFrenet=True) + self.assertEqual(1, result.faces().size()) + self.assertEqual(3, result.edges().size()) + + # Test rectangle with defaults + result = Workplane("XY").rect(1.0, 1.0).sweep(path) + self.assertEqual(6, result.faces().size()) + self.assertEqual(12, result.edges().size()) + + # Polyline path + path = Workplane("XZ").polyline(pts) + + # Test defaults + result = Workplane("XY").circle(0.1).sweep(path, transition="transformed") + self.assertEqual(5, result.faces().size()) + self.assertEqual(7, result.edges().size()) + + # Polyline path and one inner profiles + path = Workplane("XZ").polyline(pts) + + # Test defaults + result = ( + Workplane("XY") + .circle(0.2) + .circle(0.1) + .sweep(path, transition="transformed") + ) + self.assertEqual(8, result.faces().size()) + self.assertEqual(14, result.edges().size()) + + # Polyline path and different transition settings + for t in ("transformed", "right", "round"): + path = Workplane("XZ").polyline(pts) + + result = ( + Workplane("XY") + .circle(0.2) + .rect(0.2, 0.1) + .rect(0.1, 0.2) + .sweep(path, transition=t) + ) + self.assertTrue(result.solids().val().isValid()) + + # Polyline path and multiple inner profiles + path = Workplane("XZ").polyline(pts) + + # Test defaults + result = ( + Workplane("XY") + .circle(0.2) + .rect(0.2, 0.1) + .rect(0.1, 0.2) + .circle(0.1) + .sweep(path) + ) + self.assertTrue(result.solids().val().isValid()) + + # Arc path + path = Workplane("XZ").threePointArc((1.0, 1.5), (0.0, 1.0)) + + # Test defaults + result = Workplane("XY").circle(0.1).sweep(path) + self.assertEqual(3, result.faces().size()) + self.assertEqual(3, result.edges().size()) + + def testMultisectionSweep(self): + """ + Tests the operation of sweeping along a list of wire(s) along a path + """ + + # X axis line length 20.0 + path = Workplane("XZ").moveTo(-10, 0).lineTo(10, 0) + + # Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0 + defaultSweep = ( + Workplane("YZ") + .workplane(offset=-10.0) + .circle(2.0) + .workplane(offset=10.0) + .circle(1.0) + .workplane(offset=10.0) + .circle(2.0) + .sweep(path, multisection=True) + ) + + # We can sweep thrue different shapes + recttocircleSweep = ( + Workplane("YZ") + .workplane(offset=-10.0) + .rect(2.0, 2.0) + .workplane(offset=8.0) + .circle(1.0) + .workplane(offset=4.0) + .circle(1.0) + .workplane(offset=8.0) + .rect(2.0, 2.0) + .sweep(path, multisection=True) + ) + + circletorectSweep = ( + Workplane("YZ") + .workplane(offset=-10.0) + .circle(1.0) + .workplane(offset=7.0) + .rect(2.0, 2.0) + .workplane(offset=6.0) + .rect(2.0, 2.0) + .workplane(offset=7.0) + .circle(1.0) + .sweep(path, multisection=True) + ) + + # Placement of the Shape is important otherwise could produce unexpected shape + specialSweep = ( + Workplane("YZ") + .circle(1.0) + .workplane(offset=10.0) + .rect(2.0, 2.0) + .sweep(path, multisection=True) + ) + + # Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0 + path = ( + Workplane("XZ") + .moveTo(-5, 4) + .lineTo(0, 4) + .threePointArc((4, 0), (0, -4)) + .lineTo(-5, -4) + ) + + # Placement of different shapes should follow the path + # cylinder r=1.5 along first line + # then sweep allong arc from r=1.5 to r=1.0 + # then cylinder r=1.0 along last line + arcSweep = ( + Workplane("YZ") + .workplane(offset=-5) + .moveTo(0, 4) + .circle(1.5) + .workplane(offset=5) + .circle(1.5) + .moveTo(0, -8) + .circle(1.0) + .workplane(offset=-5) + .circle(1.0) + .sweep(path, multisection=True) + ) + + # Test and saveModel + self.assertEqual(1, defaultSweep.solids().size()) + self.assertEqual(1, circletorectSweep.solids().size()) + self.assertEqual(1, recttocircleSweep.solids().size()) + self.assertEqual(1, specialSweep.solids().size()) + self.assertEqual(1, arcSweep.solids().size()) + self.saveModel(defaultSweep) + + def testTwistExtrude(self): + """ + Tests extrusion while twisting through an angle. + """ + profile = Workplane("XY").rect(10, 10) + r = profile.twistExtrude(10, 45, False) + + self.assertEqual(6, r.faces().size()) + + def testTwistExtrudeCombine(self): + """ + Tests extrusion while twisting through an angle, combining with other solids. + """ + profile = Workplane("XY").rect(10, 10) + r = profile.twistExtrude(10, 45) + + self.assertEqual(6, r.faces().size()) + + def testRectArray(self): + NUMX = 3 + NUMY = 3 + s = ( + Workplane("XY") + .box(40, 40, 5, centered=(True, True, True)) + .faces(">Z") + .workplane() + .rarray(8.0, 8.0, NUMX, NUMY, True) + .circle(2.0) + .extrude(2.0) + ) + # s = Workplane("XY").box(40,40,5,centered=(True,True,True)).faces(">Z").workplane().circle(2.0).extrude(2.0) + self.saveModel(s) + # 6 faces for the box, 2 faces for each cylinder + self.assertEqual(6 + NUMX * NUMY * 2, s.faces().size()) + + def testPolarArray(self): + radius = 10 + + # Test for proper number of elements + s = Workplane("XY").polarArray(radius, 0, 180, 1) + self.assertEqual(1, s.size()) + s = Workplane("XY").polarArray(radius, 0, 180, 6) + self.assertEqual(6, s.size()) + + # Test for proper placement when fill == True + s = Workplane("XY").polarArray(radius, 0, 180, 3) + self.assertAlmostEqual(0, s.objects[1].x) + self.assertAlmostEqual(radius, s.objects[1].y) + + # Test for proper placement when angle to fill is multiple of 360 deg + s = Workplane("XY").polarArray(radius, 0, 360, 4) + self.assertAlmostEqual(0, s.objects[1].x) + self.assertAlmostEqual(radius, s.objects[1].y) + + # Test for proper placement when fill == False + s = Workplane("XY").polarArray(radius, 0, 90, 3, fill=False) + self.assertAlmostEqual(0, s.objects[1].x) + self.assertAlmostEqual(radius, s.objects[1].y) + + # Test for proper operation of startAngle + s = Workplane("XY").polarArray(radius, 90, 180, 3) + self.assertAlmostEqual(0, s.objects[0].x) + self.assertAlmostEqual(radius, s.objects[0].y) + + def testNestedCircle(self): + s = ( + Workplane("XY") + .box(40, 40, 5) + .pushPoints([(10, 0), (0, 10)]) + .circle(4) + .circle(2) + .extrude(4) + ) + self.saveModel(s) + self.assertEqual(14, s.faces().size()) + + def testConcentricEllipses(self): + concentricEllipses = ( + Workplane("XY").center(10, 20).ellipse(100, 10).center(0, 0).ellipse(50, 5) + ) + v = concentricEllipses.vertices().objects[0] + self.assertTupleAlmostEquals((v.X, v.Y), (10 + 50, 20), 3) + + def testLegoBrick(self): + # test making a simple lego brick + # which of the below + + # inputs + lbumps = 8 + wbumps = 2 + + # lego brick constants + P = 8.0 # nominal pitch + c = 0.1 # clearance on each brick side + H = 1.2 * P # nominal height of a brick + bumpDiam = 4.8 # the standard bump diameter + # the nominal thickness of the walls, normally 1.5 + t = (P - (2 * c) - bumpDiam) / 2.0 + + postDiam = P - t # works out to 6.5 + total_length = lbumps * P - 2.0 * c + total_width = wbumps * P - 2.0 * c + + # build the brick + s = Workplane("XY").box(total_length, total_width, H) # make the base + s = s.faces("Z") + .workplane() + .rarray(P, P, lbumps, wbumps, True) + .circle(bumpDiam / 2.0) + .extrude(1.8) + ) # make the bumps on the top + + # add posts on the bottom. posts are different diameter depending on geometry + # solid studs for 1 bump, tubes for multiple, none for 1x1 + # this is cheating a little-- how to select the inner face from the shell? + tmp = s.faces(" 1 and wbumps > 1: + tmp = ( + tmp.rarray(P, P, lbumps - 1, wbumps - 1, center=True) + .circle(postDiam / 2.0) + .circle(bumpDiam / 2.0) + .extrude(H - t) + ) + elif lbumps > 1: + tmp = tmp.rarray(P, P, lbumps - 1, 1, center=True).circle(t).extrude(H - t) + elif wbumps > 1: + tmp = tmp.rarray(P, P, 1, wbumps - 1, center=True).circle(t).extrude(H - t) + + self.saveModel(s) + + def testAngledHoles(self): + s = ( + Workplane("front") + .box(4.0, 4.0, 0.25) + .faces(">Z") + .workplane() + .transformed(offset=Vector(0, -1.5, 1.0), rotate=Vector(60, 0, 0)) + .rect(1.5, 1.5, forConstruction=True) + .vertices() + .hole(0.25) + ) + self.saveModel(s) + self.assertEqual(10, s.faces().size()) + + def testTranslateSolid(self): + c = CQ(makeUnitCube()) + self.assertAlmostEqual(0.0, c.faces("Z").workplane().circle(0.125).extrude(0.5, True) + ) # make a boss, not updating the original + self.assertEqual(8, r.faces().size()) # just the boss faces + self.assertEqual(6, c.faces().size()) # original is not modified + + def testSolidReferencesCombineTrue(self): + s = Workplane(Plane.XY()) + r = s.rect(2.0, 2.0).extrude(0.5) + # the result of course has 6 faces + self.assertEqual(6, r.faces().size()) + # the original workplane does not, because it did not have a solid initially + self.assertEqual(0, s.faces().size()) + + t = r.faces(">Z").workplane().rect(0.25, 0.25).extrude(0.5, True) + # of course the result has 11 faces + self.assertEqual(11, t.faces().size()) + # r (being the parent) remains unmodified + self.assertEqual(6, r.faces().size()) + self.saveModel(r) + + def testSolidReferenceCombineFalse(self): + s = Workplane(Plane.XY()) + r = s.rect(2.0, 2.0).extrude(0.5) + # the result of course has 6 faces + self.assertEqual(6, r.faces().size()) + # the original workplane does not, because it did not have a solid initially + self.assertEqual(0, s.faces().size()) + + t = r.faces(">Z").workplane().rect(0.25, 0.25).extrude(0.5, False) + # result has 6 faces, becuase it was not combined with the original + self.assertEqual(6, t.faces().size()) + self.assertEqual(6, r.faces().size()) # original is unmodified as well + # subseuent opertions use that context solid afterwards + + def testSimpleWorkplane(self): + """ + A simple square part with a hole in it + """ + s = Workplane(Plane.XY()) + r = ( + s.rect(2.0, 2.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .circle(0.25) + .cutBlind(-1.0) + ) + + self.saveModel(r) + self.assertEqual(7, r.faces().size()) + + def testMultiFaceWorkplane(self): + """ + Test Creation of workplane from multiple co-planar face + selection. + """ + s = Workplane("XY").box(1, 1, 1).faces(">Z").rect(1, 0.5).cutBlind(-0.2) + + w = s.faces(">Z").workplane() + o = w.objects[0] # origin of the workplane + self.assertAlmostEqual(o.x, 0.0, 3) + self.assertAlmostEqual(o.y, 0.0, 3) + self.assertAlmostEqual(o.z, 0.5, 3) + + def testTriangularPrism(self): + s = Workplane("XY").lineTo(1, 0).lineTo(1, 1).close().extrude(0.2) + self.saveModel(s) + + def testMultiWireWorkplane(self): + """ + A simple square part with a hole in it-- but this time done as a single extrusion + with two wires, as opposed to s cut + """ + s = Workplane(Plane.XY()) + r = s.rect(2.0, 2.0).circle(0.25).extrude(0.5) + + self.saveModel(r) + self.assertEqual(7, r.faces().size()) + + def testConstructionWire(self): + """ + Tests a wire with several holes, that are based on the vertices of a square + also tests using a workplane plane other than XY + """ + s = Workplane(Plane.YZ()) + r = ( + s.rect(2.0, 2.0) + .rect(1.3, 1.3, forConstruction=True) + .vertices() + .circle(0.125) + .extrude(0.5) + ) + self.saveModel(r) + # 10 faces-- 6 plus 4 holes, the vertices of the second rect. + self.assertEqual(10, r.faces().size()) + + def testTwoWorkplanes(self): + """ + Tests a model that uses more than one workplane + """ + # base block + s = Workplane(Plane.XY()) + + # TODO: this syntax is nice, but the iteration might not be worth + # the complexity. + # the simpler and slightly longer version would be: + # r = s.rect(2.0,2.0).rect(1.3,1.3,forConstruction=True).vertices() + # for c in r.all(): + # c.circle(0.125).extrude(0.5,True) + r = ( + s.rect(2.0, 2.0) + .rect(1.3, 1.3, forConstruction=True) + .vertices() + .circle(0.125) + .extrude(0.5) + ) + + # side hole, blind deep 1.9 + t = r.faces(">Y").workplane().circle(0.125).cutBlind(-1.9) + self.saveModel(t) + self.assertEqual(12, t.faces().size()) + + def testCut(self): + """ + Tests the cut function by itself to catch the case where a Solid object is passed. + """ + s = Workplane(Plane.XY()) + currentS = s.rect(2.0, 2.0).extrude(0.5) + toCut = s.rect(1.0, 1.0).extrude(0.5) + + resS = currentS.cut(toCut.val()) + + self.assertEqual(10, resS.faces().size()) + + def testIntersect(self): + """ + Tests the intersect function. + """ + s = Workplane(Plane.XY()) + currentS = s.rect(2.0, 2.0).extrude(0.5) + toIntersect = s.rect(1.0, 1.0).extrude(1) + + resS = currentS.intersect(toIntersect.val()) + + self.assertEqual(6, resS.faces().size()) + self.assertAlmostEqual(resS.val().Volume(), 0.5) + + resS = currentS.intersect(toIntersect) + + self.assertEqual(6, resS.faces().size()) + self.assertAlmostEqual(resS.val().Volume(), 0.5) + + def testBoundingBox(self): + """ + Tests the boudingbox center of a model + """ + result0 = ( + Workplane("XY") + .moveTo(10, 0) + .lineTo(5, 0) + .threePointArc((3.9393, 0.4393), (3.5, 1.5)) + .threePointArc((3.0607, 2.5607), (2, 3)) + .lineTo(1.5, 3) + .threePointArc((0.4393, 3.4393), (0, 4.5)) + .lineTo(0, 13.5) + .threePointArc((0.4393, 14.5607), (1.5, 15)) + .lineTo(28, 15) + .lineTo(28, 13.5) + .lineTo(24, 13.5) + .lineTo(24, 11.5) + .lineTo(27, 11.5) + .lineTo(27, 10) + .lineTo(22, 10) + .lineTo(22, 13.2) + .lineTo(14.5, 13.2) + .lineTo(14.5, 10) + .lineTo(12.5, 10) + .lineTo(12.5, 13.2) + .lineTo(5.5, 13.2) + .lineTo(5.5, 2) + .threePointArc((5.793, 1.293), (6.5, 1)) + .lineTo(10, 1) + .close() + ) + result = result0.extrude(100) + bb_center = result.val().BoundingBox().center + self.saveModel(result) + self.assertAlmostEqual(14.0, bb_center.x, 3) + self.assertAlmostEqual(7.5, bb_center.y, 3) + self.assertAlmostEqual(50.0, bb_center.z, 3) + + # The following will raise with the default tolerance of TOL 1e-2 + bb = result.val().BoundingBox(tolerance=1e-3) + self.assertAlmostEqual(0.0, bb.xmin, 2) + self.assertAlmostEqual(28, bb.xmax, 2) + self.assertAlmostEqual(0.0, bb.ymin, 2) + self.assertAlmostEqual(15.0, bb.ymax, 2) + self.assertAlmostEqual(0.0, bb.zmin, 2) + self.assertAlmostEqual(100.0, bb.zmax, 2) + + def testCutThroughAll(self): + """ + Tests a model that uses more than one workplane + """ + # base block + s = Workplane(Plane.XY()) + r = ( + s.rect(2.0, 2.0) + .rect(1.3, 1.3, forConstruction=True) + .vertices() + .circle(0.125) + .extrude(0.5) + ) + + # thru all without explicit face selection + t = r.circle(0.5).cutThruAll() + self.assertEqual(11, t.faces().size()) + + # side hole, thru all + t = t.faces(">Y").workplane().circle(0.125).cutThruAll() + self.saveModel(t) + self.assertEqual(13, t.faces().size()) + + def testCutToFaceOffsetNOTIMPLEMENTEDYET(self): + """ + Tests cutting up to a given face, or an offset from a face + """ + # base block + s = Workplane(Plane.XY()) + r = ( + s.rect(2.0, 2.0) + .rect(1.3, 1.3, forConstruction=True) + .vertices() + .circle(0.125) + .extrude(0.5) + ) + + # side hole, up to 0.1 from the last face + try: + t = ( + r.faces(">Y") + .workplane() + .circle(0.125) + .cutToOffsetFromFace(r.faces().mminDist(Dir.Y), 0.1) + ) + # should end up being a blind hole + self.assertEqual(10, t.faces().size()) + t.first().val().exportStep("c:/temp/testCutToFace.STEP") + except: + pass + # Not Implemented Yet + + def testWorkplaneOnExistingSolid(self): + "Tests extruding on an existing solid" + c = ( + CQ(makeUnitCube()) + .faces(">Z") + .workplane() + .circle(0.25) + .circle(0.125) + .extrude(0.25) + ) + self.saveModel(c) + self.assertEqual(10, c.faces().size()) + + def testWorkplaneCenterMove(self): + # this workplane is centered at x=0.5,y=0.5, the center of the upper face + s = ( + Workplane("XY").box(1, 1, 1).faces(">Z").workplane().center(-0.5, -0.5) + ) # move the center to the corner + + t = s.circle(0.25).extrude(0.2) # make a boss + self.assertEqual(9, t.faces().size()) + self.saveModel(t) + + def testBasicLines(self): + "Make a triangluar boss" + global OUTDIR + s = Workplane(Plane.XY()) + + # TODO: extrude() should imply wire() if not done already + # most users dont understand what a wire is, they are just drawing + + r = s.lineTo(1.0, 0).lineTo(0, 1.0).close().wire().extrude(0.25) + r.val().exportStep(os.path.join(OUTDIR, "testBasicLinesStep1.STEP")) + + # no faces on the original workplane + self.assertEqual(0, s.faces().size()) + # 5 faces on newly created object + self.assertEqual(5, r.faces().size()) + + # now add a circle through a side face + r1 = r.faces("+XY").workplane().circle(0.08).cutThruAll() + self.assertEqual(6, r1.faces().size()) + r1.val().exportStep(os.path.join(OUTDIR, "testBasicLinesXY.STEP")) + + # now add a circle through a top + r2 = r1.faces("+Z").workplane().circle(0.08).cutThruAll() + self.assertEqual(9, r2.faces().size()) + r2.val().exportStep(os.path.join(OUTDIR, "testBasicLinesZ.STEP")) + + self.saveModel(r2) + + def test2DDrawing(self): + """ + Draw things like 2D lines and arcs, should be expanded later to include all 2D constructs + """ + s = Workplane(Plane.XY()) + r = ( + s.lineTo(1.0, 0.0) + .lineTo(1.0, 1.0) + .threePointArc((1.0, 1.5), (0.0, 1.0)) + .lineTo(0.0, 0.0) + .moveTo(1.0, 0.0) + .lineTo(2.0, 0.0) + .lineTo(2.0, 2.0) + .threePointArc((2.0, 2.5), (0.0, 2.0)) + .lineTo(-2.0, 2.0) + .lineTo(-2.0, 0.0) + .close() + ) + + self.assertEqual(1, r.wires().size()) + + # Test the *LineTo functions + s = Workplane(Plane.XY()) + r = s.hLineTo(1.0).vLineTo(1.0).hLineTo(0.0).close() + + self.assertEqual(1, r.wire().size()) + self.assertEqual(4, r.edges().size()) + + # Test the *Line functions + s = Workplane(Plane.XY()) + r = s.hLine(1.0).vLine(1.0).hLine(-1.0).close() + + self.assertEqual(1, r.wire().size()) + self.assertEqual(4, r.edges().size()) + + # Test the move function + s = Workplane(Plane.XY()) + r = s.move(1.0, 1.0).hLine(1.0).vLine(1.0).hLine(-1.0).close() + + self.assertEqual(1, r.wire().size()) + self.assertEqual(4, r.edges().size()) + self.assertEqual( + (1.0, 1.0), + ( + r.vertices(selectors.NearestToPointSelector((0.0, 0.0, 0.0))) + .first() + .val() + .X, + r.vertices(selectors.NearestToPointSelector((0.0, 0.0, 0.0))) + .first() + .val() + .Y, + ), + ) + + # Test the sagittaArc and radiusArc functions + a1 = Workplane(Plane.YZ()).threePointArc((5, 1), (10, 0)) + a2 = Workplane(Plane.YZ()).sagittaArc((10, 0), -1) + a3 = Workplane(Plane.YZ()).threePointArc((6, 2), (12, 0)) + a4 = Workplane(Plane.YZ()).radiusArc((12, 0), -10) + + assert a1.edges().first().val().geomType() == "CIRCLE" + assert a2.edges().first().val().geomType() == "CIRCLE" + assert a3.edges().first().val().geomType() == "CIRCLE" + assert a4.edges().first().val().geomType() == "CIRCLE" + + assert a1.edges().first().val().Length() == a2.edges().first().val().Length() + assert a3.edges().first().val().Length() == a4.edges().first().val().Length() + + def testPolarLines(self): + """ + Draw some polar lines and check expected results + """ + + # Test the PolarLine* functions + s = Workplane(Plane.XY()) + r = ( + s.polarLine(10, 45) + .polarLineTo(10, -45) + .polarLine(10, -180) + .polarLine(-10, -90) + .close() + ) + + # a single wire, 5 edges + self.assertEqual(1, r.wires().size()) + self.assertEqual(5, r.wires().edges().size()) + + def testLargestDimension(self): + """ + Tests the largestDimension function when no solids are on the stack and when there are + """ + r = Workplane("XY").box(1, 1, 1) + dim = r.largestDimension() + + self.assertAlmostEqual(8.7, dim, 1) + + r = Workplane("XY") + dim = r.largestDimension() + + self.assertEqual(-1, dim) + + def testOccBottle(self): + """ + Make the OCC bottle example. + """ + + L = 20.0 + w = 6.0 + t = 3.0 + + s = Workplane(Plane.XY()) + # draw half the profile of the bottle + p = ( + s.center(-L / 2.0, 0) + .vLine(w / 2.0) + .threePointArc((L / 2.0, w / 2.0 + t), (L, w / 2.0)) + .vLine(-w / 2.0) + .mirrorX() + .extrude(30.0, True) + ) + + # make the neck + p.faces(">Z").workplane().circle(3.0).extrude( + 2.0, True + ) # .edges().fillet(0.05) + + # make a shell + p.faces(">Z").shell(0.3) + self.saveModel(p) + + def testSplineShape(self): + """ + Tests making a shape with an edge that is a spline + """ + s = Workplane(Plane.XY()) + sPnts = [ + (2.75, 1.5), + (2.5, 1.75), + (2.0, 1.5), + (1.5, 1.0), + (1.0, 1.25), + (0.5, 1.0), + (0, 1.0), + ] + r = s.lineTo(3.0, 0).lineTo(3.0, 1.0).spline(sPnts).close() + r = r.extrude(0.5) + self.saveModel(r) + + def testSimpleMirror(self): + """ + Tests a simple mirroring operation + """ + s = ( + Workplane("XY") + .lineTo(2, 2) + .threePointArc((3, 1), (2, 0)) + .mirrorX() + .extrude(0.25) + ) + self.assertEqual(6, s.faces().size()) + self.saveModel(s) + + def testUnorderedMirror(self): + """ + Tests whether or not a wire can be mirrored if its mirror won't connect to it + """ + r = 20 + s = 7 + t = 1.5 + + points = [ + (0, 0), + (0, t / 2), + (r / 2 - 1.5 * t, r / 2 - t), + (s / 2, r / 2 - t), + (s / 2, r / 2), + (r / 2, r / 2), + (r / 2, s / 2), + (r / 2 - t, s / 2), + (r / 2 - t, r / 2 - 1.5 * t), + (t / 2, 0), + ] + + r = Workplane("XY").polyline(points).mirrorX() + + self.assertEqual(1, r.wires().size()) + self.assertEqual(18, r.edges().size()) + + # try the same with includeCurrent=True + r = Workplane("XY").polyline(points[1:], includeCurrent=True).mirrorX() + + self.assertEqual(1, r.wires().size()) + self.assertEqual(18, r.edges().size()) + + def testChainedMirror(self): + """ + Tests whether or not calling mirrorX().mirrorY() works correctly + """ + r = 20 + s = 7 + t = 1.5 + + points = [ + (0, 0), + (0, t / 2), + (r / 2 - 1.5 * t, r / 2 - t), + (s / 2, r / 2 - t), + (s / 2, r / 2), + (r / 2, r / 2), + (r / 2, s / 2), + (r / 2 - t, s / 2), + (r / 2 - t, r / 2 - 1.5 * t), + (t / 2, 0), + ] + + r = Workplane("XY").polyline(points).mirrorX().mirrorY().extrude(1).faces(">Z") + + self.assertEqual(1, r.wires().size()) + self.assertEqual(32, r.edges().size()) + + # TODO: Re-work testIbeam test below now that chaining works + # TODO: Add toLocalCoords and toWorldCoords tests + + def testIbeam(self): + """ + Make an ibeam. demonstrates fancy mirroring + """ + s = Workplane(Plane.XY()) + L = 100.0 + H = 20.0 + W = 20.0 + + t = 1.0 + # TODO: for some reason doing 1/4 of the profile and mirroring twice ( .mirrorX().mirrorY() ) + # did not work, due to a bug in freecad-- it was losing edges when creating a composite wire. + # i just side-stepped it for now + + pts = [ + (0, 0), + (0, H / 2.0), + (W / 2.0, H / 2.0), + (W / 2.0, (H / 2.0 - t)), + (t / 2.0, (H / 2.0 - t)), + (t / 2.0, (t - H / 2.0)), + (W / 2.0, (t - H / 2.0)), + (W / 2.0, H / -2.0), + (0, H / -2.0), + ] + r = s.polyline(pts).mirrorY() # these other forms also work + res = r.extrude(L) + self.saveModel(res) + + def testCone(self): + """ + Tests that a simple cone works + """ + s = Solid.makeCone(0, 1.0, 2.0) + t = CQ(s) + self.saveModel(t) + self.assertEqual(2, t.faces().size()) + + def testFillet(self): + """ + Tests filleting edges on a solid + """ + c = ( + CQ(makeUnitCube()) + .faces(">Z") + .workplane() + .circle(0.25) + .extrude(0.25, True) + .edges("|Z") + .fillet(0.2) + ) + self.saveModel(c) + self.assertEqual(12, c.faces().size()) + + def testChamfer(self): + """ + Test chamfer API with a box shape + """ + cube = CQ(makeUnitCube()).faces(">Z").chamfer(0.1) + self.saveModel(cube) + self.assertEqual(10, cube.faces().size()) + + def testChamferAsymmetrical(self): + """ + Test chamfer API with a box shape for asymmetrical lengths + """ + cube = CQ(makeUnitCube()).faces(">Z").chamfer(0.1, 0.2) + self.saveModel(cube) + self.assertEqual(10, cube.faces().size()) + + # test if edge lengths are different + edge = cube.edges(">Z").vals()[0] + self.assertAlmostEqual(0.6, edge.Length(), 3) + edge = cube.edges("|Z").vals()[0] + self.assertAlmostEqual(0.9, edge.Length(), 3) + + def testChamferCylinder(self): + """ + Test chamfer API with a cylinder shape + """ + cylinder = Workplane("XY").circle(1).extrude(1).faces(">Z").chamfer(0.1) + self.saveModel(cylinder) + self.assertEqual(4, cylinder.faces().size()) + + def testCounterBores(self): + """ + Tests making a set of counterbored holes in a face + """ + c = CQ(makeCube(3.0)) + pnts = [(-1.0, -1.0), (0.0, 0.0), (1.0, 1.0)] + c = c.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1, 0.25, 0.25, 0.75) + self.assertEqual(18, c.faces().size()) + self.saveModel(c) + + # Tests the case where the depth of the cboreHole is not specified + c2 = CQ(makeCube(3.0)) + pnts = [(-1.0, -1.0), (0.0, 0.0), (1.0, 1.0)] + c2 = c2.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1, 0.25, 0.25) + self.assertEqual(15, c2.faces().size()) + + def testCounterSinks(self): + """ + Tests countersinks + """ + s = Workplane(Plane.XY()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + def testSplitKeepingHalf(self): + """ + Tests splitting a solid + """ + + # drill a hole in the side + c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() + + self.assertEqual(7, c.faces().size()) + + # now cut it in half sideways + result = c.faces(">Y").workplane(-0.5).split(keepTop=True) + self.saveModel(result) + self.assertEqual(8, result.faces().size()) + + def testSplitKeepingBoth(self): + """ + Tests splitting a solid + """ + + # drill a hole in the side + c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() + self.assertEqual(7, c.faces().size()) + + # now cut it in half sideways + result = c.faces(">Y").workplane(-0.5).split(keepTop=True, keepBottom=True) + + # stack will have both halves, original will be unchanged + # two solids are on the stack, eac + self.assertEqual(2, result.solids().size()) + self.assertEqual(8, result.solids().item(0).faces().size()) + self.assertEqual(8, result.solids().item(1).faces().size()) + + def testSplitKeepingBottom(self): + """ + Tests splitting a solid improperly + """ + # Drill a hole in the side + c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() + self.assertEqual(7, c.faces().size()) + + # Now cut it in half sideways + result = c.faces(">Y").workplane(-0.5).split(keepTop=False, keepBottom=True) + + # stack will have both halves, original will be unchanged + # one solid is on the stack + self.assertEqual(1, result.solids().size()) + self.assertEqual(8, result.solids().item(0).faces().size()) + + def testBoxDefaults(self): + """ + Tests creating a single box + """ + s = Workplane("XY").box(2, 3, 4) + self.assertEqual(1, s.solids().size()) + self.saveModel(s) + + def testSimpleShell(self): + """ + Create s simple box + """ + s = Workplane("XY").box(2, 2, 2).faces("+Z").shell(0.05) + self.saveModel(s) + self.assertEqual(23, s.faces().size()) + + def testOpenCornerShell(self): + s = Workplane("XY").box(1, 1, 1) + s1 = s.faces("+Z") + s1.add(s.faces("+Y")).add(s.faces("+X")) + self.saveModel(s1.shell(0.2)) + + # Tests the list option variation of add + s1 = s.faces("+Z") + s1.add(s.faces("+Y")).add([s.faces("+X")]) + + # Tests the raw object option variation of add + s1 = s.faces("+Z") + s1.add(s.faces("+Y")).add(s.faces("+X").val().wrapped) + + def testTopFaceFillet(self): + s = Workplane("XY").box(1, 1, 1).faces("+Z").edges().fillet(0.1) + self.assertEqual(s.faces().size(), 10) + self.saveModel(s) + + def testBoxPointList(self): + """ + Tests creating an array of boxes + """ + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .box(0.25, 0.25, 0.25, combine=True) + ) + # 1 object, 4 solids because the object is a compound + self.assertEqual(4, s.solids().size()) + self.assertEqual(1, s.size()) + self.saveModel(s) + + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .box(0.25, 0.25, 0.25, combine=False) + ) + # 4 objects, 4 solids, because each is a separate solid + self.assertEqual(4, s.size()) + self.assertEqual(4, s.solids().size()) + + def testBoxCombine(self): + s = ( + Workplane("XY") + .box(4, 4, 0.5) + .faces(">Z") + .workplane() + .rect(3, 3, forConstruction=True) + .vertices() + .box(0.25, 0.25, 0.25, combine=True) + ) + + self.saveModel(s) + self.assertEqual(1, s.solids().size()) # we should have one big solid + # should have 26 faces. 6 for the box, and 4x5 for the smaller cubes + self.assertEqual(26, s.faces().size()) + + def testSphereDefaults(self): + s = Workplane("XY").sphere(10) + self.saveModel(s) # Until FreeCAD fixes their sphere operation + self.assertEqual(1, s.solids().size()) + self.assertEqual(1, s.faces().size()) + + def testSphereCustom(self): + s = Workplane("XY").sphere( + 10, angle1=0, angle2=90, angle3=360, centered=(False, False, False) + ) + self.saveModel(s) + self.assertEqual(1, s.solids().size()) + self.assertEqual(2, s.faces().size()) + + def testSpherePointList(self): + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .sphere(0.25, combine=False) + ) + # self.saveModel(s) # Until FreeCAD fixes their sphere operation + self.assertEqual(4, s.solids().size()) + self.assertEqual(4, s.faces().size()) + + def testSphereCombine(self): + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .sphere(2.25, combine=True) + ) + # self.saveModel(s) # Until FreeCAD fixes their sphere operation + self.assertEqual(1, s.solids().size()) + self.assertEqual(4, s.faces().size()) + + def testWedgeDefaults(self): + s = Workplane("XY").wedge(10, 10, 10, 5, 5, 5, 5) + self.saveModel(s) + self.assertEqual(1, s.solids().size()) + self.assertEqual(5, s.faces().size()) + self.assertEqual(5, s.vertices().size()) + + def testWedgeCentering(self): + s = Workplane("XY").wedge( + 10, 10, 10, 5, 5, 5, 5, centered=(False, False, False) + ) + # self.saveModel(s) + self.assertEqual(1, s.solids().size()) + self.assertEqual(5, s.faces().size()) + self.assertEqual(5, s.vertices().size()) + + def testWedgePointList(self): + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .wedge(10, 10, 10, 5, 5, 5, 5, combine=False) + ) + # self.saveModel(s) + self.assertEqual(4, s.solids().size()) + self.assertEqual(20, s.faces().size()) + self.assertEqual(20, s.vertices().size()) + + def testWedgeCombined(self): + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .wedge(10, 10, 10, 5, 5, 5, 5, combine=True) + ) + # self.saveModel(s) + self.assertEqual(1, s.solids().size()) + self.assertEqual(12, s.faces().size()) + self.assertEqual(16, s.vertices().size()) + + def testQuickStartXY(self): + s = ( + Workplane(Plane.XY()) + .box(2, 4, 0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.assertEqual(1, s.solids().size()) + self.assertEqual(14, s.faces().size()) + self.saveModel(s) + + def testQuickStartYZ(self): + s = ( + Workplane(Plane.YZ()) + .box(2, 4, 0.5) + .faces(">X") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.assertEqual(1, s.solids().size()) + self.assertEqual(14, s.faces().size()) + self.saveModel(s) + + def testQuickStartXZ(self): + s = ( + Workplane(Plane.XZ()) + .box(2, 4, 0.5) + .faces(">Y") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.assertEqual(1, s.solids().size()) + self.assertEqual(14, s.faces().size()) + self.saveModel(s) + + def testDoubleTwistedLoft(self): + s = ( + Workplane("XY") + .polygon(8, 20.0) + .workplane(offset=4.0) + .transformed(rotate=Vector(0, 0, 15.0)) + .polygon(8, 20) + .loft() + ) + s2 = ( + Workplane("XY") + .polygon(8, 20.0) + .workplane(offset=-4.0) + .transformed(rotate=Vector(0, 0, 15.0)) + .polygon(8, 20) + .loft() + ) + # self.assertEquals(10,s.faces().size()) + # self.assertEquals(1,s.solids().size()) + s3 = s.combineSolids(s2) + self.saveModel(s3) + + def testTwistedLoft(self): + s = ( + Workplane("XY") + .polygon(8, 20.0) + .workplane(offset=4.0) + .transformed(rotate=Vector(0, 0, 15.0)) + .polygon(8, 20) + .loft() + ) + self.assertEqual(10, s.faces().size()) + self.assertEqual(1, s.solids().size()) + self.saveModel(s) + + def testUnions(self): + # duplicates a memory problem of some kind reported when combining lots of objects + s = Workplane("XY").rect(0.5, 0.5).extrude(5.0) + o = [] + beginTime = time.time() + for i in range(15): + t = Workplane("XY").center(10.0 * i, 0).rect(0.5, 0.5).extrude(5.0) + o.append(t) + + # union stuff + for oo in o: + s = s.union(oo) + print("Total time %0.3f" % (time.time() - beginTime)) + + # Test unioning a Solid object + s = Workplane(Plane.XY()) + currentS = s.rect(2.0, 2.0).extrude(0.5) + toUnion = s.rect(1.0, 1.0).extrude(1.0) + + resS = currentS.union(toUnion) + + self.assertEqual(11, resS.faces().size()) + + def testCombine(self): + s = Workplane(Plane.XY()) + objects1 = s.rect(2.0, 2.0).extrude(0.5).faces(">Z").rect(1.0, 1.0).extrude(0.5) + + objects1.combine() + + self.assertEqual(11, objects1.faces().size()) + + def testCombineSolidsInLoop(self): + # duplicates a memory problem of some kind reported when combining lots of objects + s = Workplane("XY").rect(0.5, 0.5).extrude(5.0) + o = [] + beginTime = time.time() + for i in range(15): + t = Workplane("XY").center(10.0 * i, 0).rect(0.5, 0.5).extrude(5.0) + o.append(t) + + # append the 'good way' + for oo in o: + s.add(oo) + s = s.combineSolids() + + print("Total time %0.3f" % (time.time() - beginTime)) + + self.saveModel(s) + + def testClean(self): + """ + Tests the `clean()` method which is called automatically. + """ + + # make a cube with a splitter edge on one of the faces + # autosimplify should remove the splitter + s = ( + Workplane("XY") + .moveTo(0, 0) + .line(5, 0) + .line(5, 0) + .line(0, 10) + .line(-10, 0) + .close() + .extrude(10) + ) + + self.assertEqual(6, s.faces().size()) + + # test removal of splitter caused by union operation + s = Workplane("XY").box(10, 10, 10).union(Workplane("XY").box(20, 10, 10)) + + self.assertEqual(6, s.faces().size()) + + # test removal of splitter caused by extrude+combine operation + s = ( + Workplane("XY") + .box(10, 10, 10) + .faces(">Y") + .workplane() + .rect(5, 10, 5) + .extrude(20) + ) + + self.assertEqual(10, s.faces().size()) + + # test removal of splitter caused by double hole operation + s = ( + Workplane("XY") + .box(10, 10, 10) + .faces(">Z") + .workplane() + .hole(3, 5) + .faces(">Z") + .workplane() + .hole(3, 10) + ) + + self.assertEqual(7, s.faces().size()) + + # test removal of splitter caused by cutThruAll + s = ( + Workplane("XY") + .box(10, 10, 10) + .faces(">Y") + .workplane() + .rect(10, 5) + .cutBlind(-5) + .faces(">Z") + .workplane() + .center(0, 2.5) + .rect(5, 5) + .cutThruAll() + ) + + self.assertEqual(18, s.faces().size()) + + # test removal of splitter with box + s = Workplane("XY").box(5, 5, 5).box(10, 5, 2) + + self.assertEqual(14, s.faces().size()) + + def testNoClean(self): + """ + Test the case when clean is disabled. + """ + # test disabling autoSimplify + s = ( + Workplane("XY") + .moveTo(0, 0) + .line(5, 0) + .line(5, 0) + .line(0, 10) + .line(-10, 0) + .close() + .extrude(10, clean=False) + ) + self.assertEqual(7, s.faces().size()) + + s = ( + Workplane("XY") + .box(10, 10, 10) + .union(Workplane("XY").box(20, 10, 10), clean=False) + ) + self.assertEqual(14, s.faces().size()) + + s = ( + Workplane("XY") + .box(10, 10, 10) + .faces(">Y") + .workplane() + .rect(5, 10, 5) + .extrude(20, clean=False) + ) + + self.assertEqual(12, s.faces().size()) + + def testExplicitClean(self): + """ + Test running of `clean()` method explicitly. + """ + s = ( + Workplane("XY") + .moveTo(0, 0) + .line(5, 0) + .line(5, 0) + .line(0, 10) + .line(-10, 0) + .close() + .extrude(10, clean=False) + .clean() + ) + self.assertEqual(6, s.faces().size()) + + def testPlanes(self): + """ + Test other planes other than the normal ones (XY, YZ) + """ + # ZX plane + s = Workplane(Plane.ZX()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + # YX plane + s = Workplane(Plane.YX()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + # YX plane + s = Workplane(Plane.YX()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + # ZY plane + s = Workplane(Plane.ZY()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + # front plane + s = Workplane(Plane.front()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + # back plane + s = Workplane(Plane.back()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + # left plane + s = Workplane(Plane.left()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + # right plane + s = Workplane(Plane.right()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + # top plane + s = Workplane(Plane.top()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + # bottom plane + s = Workplane(Plane.bottom()) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) + self.saveModel(result) + + def testIsInside(self): + """ + Testing if one box is inside of another. + """ + box1 = Workplane(Plane.XY()).box(10, 10, 10) + box2 = Workplane(Plane.XY()).box(5, 5, 5) + + self.assertFalse(box2.val().BoundingBox().isInside(box1.val().BoundingBox())) + self.assertTrue(box1.val().BoundingBox().isInside(box2.val().BoundingBox())) + + def testCup(self): + """ + UOM = "mm" + + # + # PARAMETERS and PRESETS + # These parameters can be manipulated by end users + # + bottomDiameter = FloatParam(min=10.0,presets={'default':50.0,'tumbler':50.0,'shot':35.0,'tea':50.0,'saucer':100.0},group="Basics", desc="Bottom diameter") + topDiameter = FloatParam(min=10.0,presets={'default':85.0,'tumbler':85.0,'shot':50.0,'tea':51.0,'saucer':400.0 },group="Basics", desc="Top diameter") + thickness = FloatParam(min=0.1,presets={'default':2.0,'tumbler':2.0,'shot':2.66,'tea':2.0,'saucer':2.0},group="Basics", desc="Thickness") + height = FloatParam(min=1.0,presets={'default':80.0,'tumbler':80.0,'shot':59.0,'tea':125.0,'saucer':40.0},group="Basics", desc="Overall height") + lipradius = FloatParam(min=1.0,presets={'default':1.0,'tumbler':1.0,'shot':0.8,'tea':1.0,'saucer':1.0},group="Basics", desc="Lip Radius") + bottomThickness = FloatParam(min=1.0,presets={'default':5.0,'tumbler':5.0,'shot':10.0,'tea':10.0,'saucer':5.0},group="Basics", desc="BottomThickness") + + # + # Your build method. It must return a solid object + # + def build(): + br = bottomDiameter.value / 2.0 + tr = topDiameter.value / 2.0 + t = thickness.value + s1 = Workplane("XY").circle(br).workplane(offset=height.value).circle(tr).loft() + s2 = Workplane("XY").workplane(offset=bottomThickness.value).circle(br - t ).workplane(offset=height.value - t ).circle(tr - t).loft() + + cup = s1.cut(s2) + cup.faces(">Z").edges().fillet(lipradius.value) + return cup + """ + + # for some reason shell doesnt work on this simple shape. how disappointing! + td = 50.0 + bd = 20.0 + h = 10.0 + t = 1.0 + s1 = Workplane("XY").circle(bd).workplane(offset=h).circle(td).loft() + s2 = ( + Workplane("XY") + .workplane(offset=t) + .circle(bd - (2.0 * t)) + .workplane(offset=(h - t)) + .circle(td - (2.0 * t)) + .loft() + ) + s3 = s1.cut(s2) + self.saveModel(s3) + + def testEnclosure(self): + """ + Builds an electronics enclosure + Original FreeCAD script: 81 source statements ,not including variables + This script: 34 + """ + + # parameter definitions + p_outerWidth = 100.0 # Outer width of box enclosure + p_outerLength = 150.0 # Outer length of box enclosure + p_outerHeight = 50.0 # Outer height of box enclosure + + p_thickness = 3.0 # Thickness of the box walls + p_sideRadius = 10.0 # Radius for the curves around the sides of the bo + # Radius for the curves on the top and bottom edges of the box + p_topAndBottomRadius = 2.0 + + # How far in from the edges the screwposts should be place. + p_screwpostInset = 12.0 + # nner Diameter of the screwpost holes, should be roughly screw diameter not including threads + p_screwpostID = 4.0 + # Outer Diameter of the screwposts.\nDetermines overall thickness of the posts + p_screwpostOD = 10.0 + + p_boreDiameter = 8.0 # Diameter of the counterbore hole, if any + p_boreDepth = 1.0 # Depth of the counterbore hole, if + # Outer diameter of countersink. Should roughly match the outer diameter of the screw head + p_countersinkDiameter = 0.0 + # Countersink angle (complete angle between opposite sides, not from center to one side) + p_countersinkAngle = 90.0 + # Whether to place the lid with the top facing down or not. + p_flipLid = True + # Height of lip on the underside of the lid.\nSits inside the box body for a snug fit. + p_lipHeight = 1.0 + + # outer shell + oshell = ( + Workplane("XY") + .rect(p_outerWidth, p_outerLength) + .extrude(p_outerHeight + p_lipHeight) + ) + + # weird geometry happens if we make the fillets in the wrong order + if p_sideRadius > p_topAndBottomRadius: + oshell = ( + oshell.edges("|Z") + .fillet(p_sideRadius) + .edges("#Z") + .fillet(p_topAndBottomRadius) + ) + else: + oshell = ( + oshell.edges("#Z") + .fillet(p_topAndBottomRadius) + .edges("|Z") + .fillet(p_sideRadius) + ) + + # inner shell + ishell = ( + oshell.faces("Z") + .workplane(-p_thickness) + .rect(POSTWIDTH, POSTLENGTH, forConstruction=True) + .vertices() + .circle(p_screwpostOD / 2.0) + .circle(p_screwpostID / 2.0) + .extrude((-1.0) * (p_outerHeight + p_lipHeight - p_thickness), True) + ) + + # split lid into top and bottom parts + (lid, bottom) = ( + box.faces(">Z") + .workplane(-p_thickness - p_lipHeight) + .split(keepTop=True, keepBottom=True) + .all() + ) # splits into two solids + + # translate the lid, and subtract the bottom from it to produce the lid inset + lowerLid = lid.translate((0, 0, -p_lipHeight)) + cutlip = lowerLid.cut(bottom).translate( + (p_outerWidth + p_thickness, 0, p_thickness - p_outerHeight + p_lipHeight) + ) + + # compute centers for counterbore/countersink or counterbore + topOfLidCenters = ( + cutlip.faces(">Z") + .workplane() + .rect(POSTWIDTH, POSTLENGTH, forConstruction=True) + .vertices() + ) + + # add holes of the desired type + if p_boreDiameter > 0 and p_boreDepth > 0: + topOfLid = topOfLidCenters.cboreHole( + p_screwpostID, p_boreDiameter, p_boreDepth, (2.0) * p_thickness + ) + elif p_countersinkDiameter > 0 and p_countersinkAngle > 0: + topOfLid = topOfLidCenters.cskHole( + p_screwpostID, + p_countersinkDiameter, + p_countersinkAngle, + (2.0) * p_thickness, + ) + else: + topOfLid = topOfLidCenters.hole(p_screwpostID, (2.0) * p_thickness) + + # flip lid upside down if desired + if p_flipLid: + topOfLid.rotateAboutCenter((1, 0, 0), 180) + + # return the combined result + result = topOfLid.union(bottom) + + self.saveModel(result) + + def testExtrude(self): + """ + Test extrude + """ + r = 1.0 + h = 1.0 + decimal_places = 9.0 + + # extrude in one direction + s = Workplane("XY").circle(r).extrude(h, both=False) + + top_face = s.faces(">Z") + bottom_face = s.faces("Z") + bottom_face = s.faces("Z") + bottom_face = s.faces("Z") + bottom_face = s.faces(" 0) + + # cut a tapered hole + s = ( + Workplane("XY") + .rect(2 * r, 2 * r) + .extrude(2 * h) + .faces(">Z") + .workplane() + .rect(r, r) + .cutBlind(-h, taper=t) + ) + + middle_face = s.faces(">Z[-2]") + + self.assertTrue(middle_face.val().Area() < 1) + + def testClose(self): + # Close without endPoint and startPoint coincide. + # Create a half-circle + a = Workplane(Plane.XY()).sagittaArc((10, 0), 2).close().extrude(2) + + # Close when endPoint and startPoint coincide. + # Create a double half-circle + b = ( + Workplane(Plane.XY()) + .sagittaArc((10, 0), 2) + .sagittaArc((0, 0), 2) + .close() + .extrude(2) + ) + + # The b shape shall have twice the volume of the a shape. + self.assertAlmostEqual(a.val().Volume() * 2.0, b.val().Volume()) + + # Testcase 3 from issue #238 + thickness = 3.0 + length = 10.0 + width = 5.0 + + obj1 = ( + Workplane("XY", origin=(0, 0, -thickness / 2)) + .moveTo(length / 2, 0) + .threePointArc((0, width / 2), (-length / 2, 0)) + .threePointArc((0, -width / 2), (length / 2, 0)) + .close() + .extrude(thickness) + ) + + os_x = 8.0 # Offset in X + os_y = -19.5 # Offset in Y + + obj2 = ( + Workplane("YZ", origin=(os_x, os_y, -thickness / 2)) + .moveTo(os_x + length / 2, os_y) + .sagittaArc((os_x - length / 2, os_y), width / 2) + .sagittaArc((os_x + length / 2, os_y), width / 2) + .close() + .extrude(thickness) + ) + + # The obj1 shape shall have the same volume as the obj2 shape. + self.assertAlmostEqual(obj1.val().Volume(), obj2.val().Volume()) + + def testText(self): + + box = Workplane("XY").box(4, 4, 0.5) + + obj1 = ( + box.faces(">Z") + .workplane() + .text( + "CQ 2.0", + 0.5, + -0.05, + cut=True, + halign="left", + valign="bottom", + font="Sans", + ) + ) + + # combined object should have smaller volume + self.assertGreater(box.val().Volume(), obj1.val().Volume()) + + obj2 = ( + box.faces(">Z") + .workplane() + .text("CQ 2.0", 0.5, 0.05, cut=False, combine=True, font="Sans") + ) + + # combined object should have bigger volume + self.assertLess(box.val().Volume(), obj2.val().Volume()) + + # verify that the number of top faces is correct (NB: this is font specific) + self.assertEqual(len(obj2.faces(">Z").vals()), 5) + + obj3 = ( + box.faces(">Z") + .workplane() + .text( + "CQ 2.0", + 0.5, + 0.05, + cut=False, + combine=False, + halign="right", + valign="top", + font="Sans", + ) + ) + + # verify that the number of solids is correct + self.assertEqual(len(obj3.solids().vals()), 5) + + def testParametricCurve(self): + + from math import sin, cos, pi + + k = 4 + r = 1 + + func = lambda t: ( + r * (k + 1) * cos(t) - r * cos((k + 1) * t), + r * (k + 1) * sin(t) - r * sin((k + 1) * t), + ) + + res_open = Workplane("XY").parametricCurve(func).extrude(3) + + # open profile generates an invalid solid + self.assertFalse(res_open.solids().val().isValid()) + + res_closed = ( + Workplane("XY").parametricCurve(func, start=0, stop=2 * pi).extrude(3) + ) + + # closed profile will generate a valid solid with 3 faces + self.assertTrue(res_closed.solids().val().isValid()) + self.assertEqual(len(res_closed.faces().vals()), 3) + + def testMakeShellSolid(self): + + c0 = math.sqrt(2) / 4 + vertices = [[c0, -c0, c0], [c0, c0, -c0], [-c0, c0, c0], [-c0, -c0, -c0]] + faces_ixs = [[0, 1, 2, 0], [1, 0, 3, 1], [2, 3, 0, 2], [3, 2, 1, 3]] + + faces = [] + for ixs in faces_ixs: + lines = [] + for v1, v2 in zip(ixs, ixs[1:]): + lines.append( + Edge.makeLine(Vector(*vertices[v1]), Vector(*vertices[v2])) + ) + wire = Wire.combine(lines) + faces.append(Face.makeFromWires(wire)) + + shell = Shell.makeShell(faces) + solid = Solid.makeSolid(shell) + + self.assertTrue(shell.isValid()) + self.assertTrue(solid.isValid()) + + self.assertEqual(len(solid.Vertices()), 4) + self.assertEqual(len(solid.Faces()), 4) + + def testIsInsideSolid(self): + # test solid + model = Workplane("XY").box(10, 10, 10) + solid = model.val() # get first object on stack + + self.assertTrue(solid.isInside((0, 0, 0))) + self.assertFalse(solid.isInside((10, 10, 10))) + self.assertTrue(solid.isInside((Vector(3, 3, 3)))) + self.assertFalse(solid.isInside((Vector(30.0, 30.0, 30.0)))) + + self.assertTrue(solid.isInside((0, 0, 4.99), tolerance=0.1)) + self.assertTrue(solid.isInside((0, 0, 5))) # check point on surface + self.assertTrue(solid.isInside((0, 0, 5.01), tolerance=0.1)) + self.assertFalse(solid.isInside((0, 0, 5.1), tolerance=0.1)) + + # test compound solid + model = Workplane("XY").box(10, 10, 10) + model = model.moveTo(50, 50).box(10, 10, 10) + solid = model.val() + + self.assertTrue(solid.isInside((0, 0, 0))) + self.assertTrue(solid.isInside((50, 50, 0))) + self.assertFalse(solid.isInside((50, 56, 0))) + + # make sure raises on non solid + model = Workplane("XY").rect(10, 10) + solid = model.val() + with self.assertRaises(AttributeError): + solid.isInside((0, 0, 0)) + + # test solid with an internal void + void = Workplane("XY").box(10, 10, 10) + model = Workplane("XY").box(100, 100, 100).cut(void) + solid = model.val() + + self.assertFalse(solid.isInside((0, 0, 0))) + self.assertTrue(solid.isInside((40, 40, 40))) + self.assertFalse(solid.isInside((55, 55, 55))) + + def testWorkplaneCenterOptions(self): + """ + Test options for specifiying origin of workplane + """ + decimal_places = 9 + + pts = [(0, 0), (90, 0), (90, 30), (30, 30), (30, 60), (0.0, 60)] + + r = Workplane("XY").polyline(pts).close().extrude(10.0) + + origin = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin") + .plane.origin.toTuple() + ) + self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places) + + origin = ( + r.faces(">Z").workplane(centerOption="CenterOfMass").plane.origin.toTuple() + ) + self.assertTupleAlmostEquals(origin, (37.5, 22.5, 10.0), decimal_places) + + origin = ( + r.faces(">Z") + .workplane(centerOption="CenterOfBoundBox") + .plane.origin.toTuple() + ) + self.assertTupleAlmostEquals(origin, (45.0, 30.0, 10.0), decimal_places) + + origin = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin", origin=(30, 10, 20)) + .plane.origin.toTuple() + ) + self.assertTupleAlmostEquals(origin, (30.0, 10.0, 10.0), decimal_places) + + origin = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin", origin=Vector(30, 10, 20)) + .plane.origin.toTuple() + ) + self.assertTupleAlmostEquals(origin, (30.0, 10.0, 10.0), decimal_places) + + with self.assertRaises(ValueError): + origin = r.faces(">Z").workplane(centerOption="undefined") + + # test case where plane origin is shifted with center call + r = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin") + .center(30, 0) + .hole(90) + ) + + origin = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin") + .plane.origin.toTuple() + ) + self.assertTupleAlmostEquals(origin, (30.0, 0.0, 10.0), decimal_places) + + origin = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin", origin=(0, 0, 0)) + .plane.origin.toTuple() + ) + self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places) + + # make sure projection works in all directions + r = Workplane("YZ").polyline(pts).close().extrude(10.0) + + origin = ( + r.faces(">X") + .workplane(centerOption="ProjectedOrigin") + .plane.origin.toTuple() + ) + self.assertTupleAlmostEquals(origin, (10.0, 0.0, 0.0), decimal_places) + + origin = ( + r.faces(">X").workplane(centerOption="CenterOfMass").plane.origin.toTuple() + ) + self.assertTupleAlmostEquals(origin, (10.0, 37.5, 22.5), decimal_places) + + origin = ( + r.faces(">X") + .workplane(centerOption="CenterOfBoundBox") + .plane.origin.toTuple() + ) + self.assertTupleAlmostEquals(origin, (10.0, 45.0, 30.0), decimal_places) + + r = Workplane("XZ").polyline(pts).close().extrude(10.0) + + origin = ( + r.faces("Z").workplane().slot2D(4, 1, 0).cutThruAll() + self.assertAlmostEqual(result.val().Volume(), 21.214601837, decimal_places) + result = box.faces(">Z").workplane().slot2D(4, 1, 0).cutBlind(-0.5) + self.assertAlmostEqual(result.val().Volume(), 23.107300918, decimal_places) + + # Test to see if slot is rotated correctly + result = Workplane("XY").slot2D(4, 1, 45).extrude(1) + point = result.faces(">Z").edges(">X").first().val().startPoint().toTuple() + self.assertTupleAlmostEquals( + point, (0.707106781, 1.414213562, 1.0), decimal_places + ) + + def test_assembleEdges(self): + + # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides + # Passes an open wire to assembleEdges so that IsDone is true but Error returns 2 to test the warning functionality. + edge_points = [ + [-7.0, -7.0, 0.0], + [-3.0, -10.0, 3.0], + [7.0, -7.0, 0.0], + [7.0, 7.0, 0.0], + [-7.0, 7.0, 0.0], + ] + edge_wire = Workplane("XY").polyline( + [(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)] + ) + edge_wire = edge_wire.add( + Workplane("YZ") + .workplane() + .transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0)) + .spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)]) + ) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = Wire.assembleEdges(edge_wire) + + # Embossed star, need to change optional parameters to obtain nice looking result. + r1 = 3.0 + r2 = 10.0 + fn = 6 + edge_points = [ + [r1 * math.cos(i * math.pi / fn), r1 * math.sin(i * math.pi / fn)] + if i % 2 == 0 + else [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn)] + for i in range(2 * fn + 1) + ] + edge_wire = Workplane("XY").polyline(edge_points) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = Wire.assembleEdges(edge_wire) + + # Points on hexagonal pattern coordinates, use of pushpoints. + r1 = 1.0 + fn = 6 + edge_points = [ + [r1 * math.cos(i * 2 * math.pi / fn), r1 * math.sin(i * 2 * math.pi / fn)] + for i in range(fn + 1) + ] + surface_points = [ + [0.25, 0, 0.75], + [-0.25, 0, 0.75], + [0, 0.25, 0.75], + [0, -0.25, 0.75], + [0, 0, 2], + ] + edge_wire = Workplane("XY").polyline(edge_points) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = Wire.assembleEdges(edge_wire) + + # Gyroïd, all edges are splines on different workplanes. + edge_points = [ + [[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + ] + plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"] + offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] + edge_wire = ( + Workplane(plane_list[0]) + .workplane(offset=-offset_list[0]) + .spline(edge_points[0]) + ) + for i in range(len(edge_points) - 1): + edge_wire = edge_wire.add( + Workplane(plane_list[i + 1]) + .workplane(offset=-offset_list[i + 1]) + .spline(edge_points[i + 1]) + ) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = Wire.assembleEdges(edge_wire) + + def testTag(self): + + # test tagging + result = ( + Workplane("XY") + .pushPoints([(-2, 0), (2, 0)]) + .box(1, 1, 1, combine=False) + .tag("2 solids") + .union(Workplane("XY").box(6, 1, 1)) + ) + self.assertEqual(len(result.objects), 1) + result = result._getTagged("2 solids") + self.assertEqual(len(result.objects), 2) + + def testCopyWorkplane(self): + + obj0 = Workplane("XY").box(1, 1, 10).faces(">Z").workplane() + obj1 = Workplane("XY").copyWorkplane(obj0).box(1, 1, 1) + self.assertTupleAlmostEquals((0, 0, 5), obj1.val().Center().toTuple(), 9) + + def testWorkplaneFromTagged(self): + + # create a flat, wide base. Extrude one object 4 units high, another + # object ontop of it 6 units high. Go back to base plane. Extrude an + # object 11 units high. Assert that top face is 11 units high. + result = ( + Workplane("XY") + .box(10, 10, 1, centered=(True, True, False)) + .faces(">Z") + .workplane() + .tag("base") + .center(3, 0) + .rect(2, 2) + .extrude(4) + .faces(">Z") + .workplane() + .circle(1) + .extrude(6) + .workplaneFromTagged("base") + .center(-3, 0) + .circle(1) + .extrude(11) + ) + self.assertTupleAlmostEquals( + result.faces(">Z").val().Center().toTuple(), (-3, 0, 12), 9 + ) + + def testTagSelectors(self): + + result0 = Workplane("XY").box(1, 1, 1).tag("box").sphere(1) + # result is currently a sphere + self.assertEqual(1, result0.faces().size()) + # a box has 8 vertices + self.assertEqual(8, result0.vertices(tag="box").size()) + # 6 faces + self.assertEqual(6, result0.faces(tag="box").size()) + # 12 edges + self.assertEqual(12, result0.edges(tag="box").size()) + # 6 wires + self.assertEqual(6, result0.wires(tag="box").size()) + + # create two solids, tag them, join to one solid + result1 = ( + Workplane("XY") + .pushPoints([(1, 0), (-1, 0)]) + .box(1, 1, 1) + .tag("boxes") + .sphere(1) + ) + self.assertEqual(1, result1.solids().size()) + self.assertEqual(2, result1.solids(tag="boxes").size()) + self.assertEqual(1, result1.shells().size()) + self.assertEqual(2, result1.shells(tag="boxes").size()) + + # create 4 individual objects, tag it, then combine to one compound + result2 = ( + Workplane("XY") + .rect(4, 4) + .vertices() + .box(1, 1, 1, combine=False) + .tag("4 objs") + ) + result2 = result2.newObject([Compound.makeCompound(result2.objects)]) + self.assertEqual(1, result2.compounds().size()) + self.assertEqual(0, result2.compounds(tag="4 objs").size()) + + def test_interpPlate(self): + """ + Tests the interpPlate() functionnalites + Numerical values of Areas and Volumes were obtained with the Area() and Volume() functions on a Linux machine under Debian 10 with python 3.7. + """ + + # example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface. + thickness = 0 + edge_points = [ + [0.0, 0.0, 0.0], + [0.0, 10.0, 0.0], + [0.0, 10.0, 10.0], + [0.0, 0.0, 10.0], + ] + surface_points = [[5.0, 5.0, 5.0]] + plate_0 = Workplane("XY").interpPlate(edge_points, surface_points, thickness) + self.assertTrue(plate_0.val().isValid()) + self.assertAlmostEqual(plate_0.val().Area(), 141.218823892, 1) + + # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides + thickness = 0.1 + edge_points = [ + [-7.0, -7.0, 0.0], + [-3.0, -10.0, 3.0], + [7.0, -7.0, 0.0], + [7.0, 7.0, 0.0], + [-7.0, 7.0, 0.0], + ] + edge_wire = Workplane("XY").polyline( + [(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)] + ) + # edge_wire = edge_wire.add(Workplane('YZ').workplane().transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0)).polyline([(-7.,0.), (3,-3), (7.,0.)])) + # In CadQuery Sept-2019 it worked with rotate=Vector(0, 45, 0). In CadQuery Dec-2019 rotate=Vector(45, 0, 0) only closes the wire. + edge_wire = edge_wire.add( + Workplane("YZ") + .workplane() + .transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0)) + .spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)]) + ) + surface_points = [[-3.0, -3.0, -3.0], [3.0, 3.0, 3.0]] + plate_1 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness) + self.assertTrue(plate_1.val().isValid()) + self.assertAlmostEqual(plate_1.val().Volume(), 26.124970206, 3) + + # Embossed star, need to change optional parameters to obtain nice looking result. + r1 = 3.0 + r2 = 10.0 + fn = 6 + thickness = 0.1 + edge_points = [ + [r1 * math.cos(i * math.pi / fn), r1 * math.sin(i * math.pi / fn)] + if i % 2 == 0 + else [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn)] + for i in range(2 * fn + 1) + ] + edge_wire = Workplane("XY").polyline(edge_points) + r2 = 4.5 + surface_points = [ + [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn), 1.0] + for i in range(2 * fn) + ] + [[0.0, 0.0, -2.0]] + plate_2 = Workplane("XY").interpPlate( + edge_wire, + surface_points, + thickness, + combine=True, + clean=True, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=49, + ) + self.assertTrue(plate_2.val().isValid()) + self.assertAlmostEqual(plate_2.val().Volume(), 10.956054314, 0) + + # Points on hexagonal pattern coordinates, use of pushpoints. + r1 = 1.0 + N = 3 + ca = math.cos(30.0 * math.pi / 180.0) + sa = math.sin(30.0 * math.pi / 180.0) + # EVEN ROWS + pts = [ + (-3.0, -3.0), + (-1.267949, -3.0), + (0.464102, -3.0), + (2.196152, -3.0), + (-3.0, 0.0), + (-1.267949, 0.0), + (0.464102, 0.0), + (2.196152, 0.0), + (-2.133974, -1.5), + (-0.401923, -1.5), + (1.330127, -1.5), + (3.062178, -1.5), + (-2.133975, 1.5), + (-0.401924, 1.5), + (1.330127, 1.5), + (3.062178, 1.5), + ] + # Spike surface + thickness = 0.1 + fn = 6 + edge_points = [ + [ + r1 * math.cos(i * 2 * math.pi / fn + 30 * math.pi / 180), + r1 * math.sin(i * 2 * math.pi / fn + 30 * math.pi / 180), + ] + for i in range(fn + 1) + ] + surface_points = [ + [ + r1 / 4 * math.cos(i * 2 * math.pi / fn + 30 * math.pi / 180), + r1 / 4 * math.sin(i * 2 * math.pi / fn + 30 * math.pi / 180), + 0.75, + ] + for i in range(fn + 1) + ] + [[0, 0, 2]] + edge_wire = Workplane("XY").polyline(edge_points) + plate_3 = ( + Workplane("XY") + .pushPoints(pts) + .interpPlate( + edge_wire, + surface_points, + thickness, + combine=False, + clean=False, + degree=2, + nbPtsOnCur=20, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ) + ) + self.assertTrue(plate_3.val().isValid()) + self.assertAlmostEqual(plate_3.val().Volume(), 0.45893954685189414, 1) + + # Gyroïd, all edges are splines on different workplanes. + thickness = 0.1 + edge_points = [ + [[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + ] + plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"] + offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] + edge_wire = ( + Workplane(plane_list[0]) + .workplane(offset=-offset_list[0]) + .spline(edge_points[0]) + ) + for i in range(len(edge_points) - 1): + edge_wire = edge_wire.add( + Workplane(plane_list[i + 1]) + .workplane(offset=-offset_list[i + 1]) + .spline(edge_points[i + 1]) + ) + surface_points = [[0, 0, 0]] + plate_4 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness) + self.assertTrue(plate_4.val().isValid()) + self.assertAlmostEqual(plate_4.val().Volume(), 7.760559490, 3) + + def testTangentArcToPoint(self): + + # create a simple shape with tangents of straight edges and see if it has the correct area + s0 = ( + Workplane("XY") + .hLine(1) + .tangentArcPoint((1, 1), relative=False) + .hLineTo(0) + .tangentArcPoint((0, 0), relative=False) + .close() + .extrude(1) + ) + area0 = s0.faces(">Z").val().Area() + self.assertAlmostEqual(area0, (1 + math.pi * 0.5 ** 2), 4) + + # test relative coords + s1 = ( + Workplane("XY") + .hLine(1) + .tangentArcPoint((0, 1), relative=True) + .hLineTo(0) + .tangentArcPoint((0, -1), relative=True) + .close() + .extrude(1) + ) + self.assertTupleAlmostEquals( + s1.val().Center().toTuple(), s0.val().Center().toTuple(), 4 + ) + self.assertAlmostEqual(s1.val().Volume(), s0.val().Volume(), 4) + + # consecutive tangent arcs + s1 = ( + Workplane("XY") + .vLine(2) + .tangentArcPoint((1, 0)) + .tangentArcPoint((1, 0)) + .tangentArcPoint((1, 0)) + .vLine(-2) + .close() + .extrude(1) + ) + self.assertAlmostEqual( + s1.faces(">Z").val().Area(), 2 * 3 + 0.5 * math.pi * 0.5 ** 2, 4 + ) + + # tangentArc on the end of a spline + # spline will be a simple arc of a circle, then finished off with a + # tangentArcPoint + angles = [idx * 1.5 * math.pi / 10 for idx in range(10)] + pts = [(math.sin(a), math.cos(a)) for a in angles] + s2 = ( + Workplane("XY") + .spline(pts) + .tangentArcPoint((0, 1), relative=False) + .close() + .extrude(1) + ) + # volume should almost be pi, but not accurately because we need to + # start with a spline + self.assertAlmostEqual(s2.val().Volume(), math.pi, 1) + # assert local coords are mapped to global correctly + arc0 = Workplane("XZ", origin=(1, 1, 1)).hLine(1).tangentArcPoint((1, 1)).val() + self.assertTupleAlmostEquals(arc0.endPoint().toTuple(), (3, 1, 2), 4) + + # tangentArcPoint with 3-tuple argument + w0 = Workplane("XY").lineTo(1, 1).tangentArcPoint((1, 1, 1)).wire() + zmax = w0.val().BoundingBox().zmax + self.assertAlmostEqual(zmax, 1, 1) + + def test_findFromEdge(self): + part = Workplane("XY", origin=(1, 1, 1)).hLine(1) + found_edge = part._findFromEdge(useLocalCoords=False) + self.assertTupleAlmostEquals(found_edge.startPoint().toTuple(), (1, 1, 1), 3) + self.assertTupleAlmostEquals(found_edge.Center().toTuple(), (1.5, 1, 1), 3) + self.assertTupleAlmostEquals(found_edge.endPoint().toTuple(), (2, 1, 1), 3) + found_edge = part._findFromEdge(useLocalCoords=True) + self.assertTupleAlmostEquals(found_edge.endPoint().toTuple(), (1, 0, 0), 3) + # check _findFromEdge can find a spline + pts = [(0, 0), (0, 1), (1, 2), (2, 4)] + spline0 = Workplane("XZ").spline(pts)._findFromEdge() + self.assertTupleAlmostEquals((2, 0, 4), spline0.endPoint().toTuple(), 3) + # check method fails if no edge is present + part2 = Workplane("XY").box(1, 1, 1) + with self.assertRaises(RuntimeError): + part2._findFromEdge() + with self.assertRaises(RuntimeError): + part2._findFromEdge(useLocalCoords=True) diff --git a/tests/TestCQGI.py b/tests/test_cqgi.py similarity index 84% rename from tests/TestCQGI.py rename to tests/test_cqgi.py index a7218ae6..43d38d77 100644 --- a/tests/TestCQGI.py +++ b/tests/test_cqgi.py @@ -42,8 +42,9 @@ class TestCQGI(BaseTest): model = cqgi.CQModel(TESTSCRIPT) metadata = model.metadata - self.assertEqual(set(metadata.parameters.keys()), { - 'height', 'width', 'a', 'b', 'foo'}) + self.assertEqual( + set(metadata.parameters.keys()), {"height", "width", "a", "b", "foo"} + ) def test_build_with_debug(self): model = cqgi.CQModel(TEST_DEBUG_SCRIPT) @@ -51,7 +52,7 @@ class TestCQGI(BaseTest): debugItems = result.debugObjects self.assertTrue(len(debugItems) == 2) self.assertTrue(debugItems[0].shape == "bar") - self.assertTrue(debugItems[0].options == {"color": 'yellow'}) + self.assertTrue(debugItems[0].options == {"color": "yellow"}) self.assertTrue(debugItems[1].shape == 2.0) self.assertTrue(debugItems[1].options == {}) @@ -65,7 +66,7 @@ class TestCQGI(BaseTest): def test_build_with_different_params(self): model = cqgi.CQModel(TESTSCRIPT) - result = model.build({'height': 3.0}) + result = model.build({"height": 3.0}) self.assertTrue(result.results[0].shape == "3.0|3.0|bar|1.0") def test_describe_parameters(self): @@ -76,9 +77,9 @@ class TestCQGI(BaseTest): """ ) model = cqgi.CQModel(script) - a_param = model.metadata.parameters['a'] + a_param = model.metadata.parameters["a"] self.assertTrue(a_param.default_value == 2.0) - self.assertTrue(a_param.desc == 'FirstLetter') + self.assertTrue(a_param.desc == "FirstLetter") self.assertTrue(a_param.varType == cqgi.NumberParameterType) def test_describe_parameter_invalid_doesnt_fail_script(self): @@ -89,8 +90,8 @@ class TestCQGI(BaseTest): """ ) model = cqgi.CQModel(script) - a_param = model.metadata.parameters['a'] - self.assertTrue(a_param.name == 'a') + a_param = model.metadata.parameters["a"] + self.assertTrue(a_param.name == "a") def test_build_with_exception(self): badscript = textwrap.dedent( @@ -115,7 +116,7 @@ class TestCQGI(BaseTest): with self.assertRaises(Exception) as context: model = cqgi.CQModel(badscript) - self.assertTrue('invalid syntax' in context.exception.args) + self.assertTrue("invalid syntax" in context.exception.args) def test_that_two_results_are_returned(self): script = textwrap.dedent( @@ -140,7 +141,7 @@ class TestCQGI(BaseTest): show_object(h) """ ) - result = cqgi.parse(script).build({'h': 33.33}) + result = cqgi.parse(script).build({"h": 33.33}) self.assertEqual(result.results[0].shape, "33.33") def test_that_assigning_string_to_number_fails(self): @@ -150,9 +151,8 @@ class TestCQGI(BaseTest): show_object(h) """ ) - result = cqgi.parse(script).build({'h': "a string"}) - self.assertTrue(isinstance(result.exception, - cqgi.InvalidParameterError)) + result = cqgi.parse(script).build({"h": "a string"}) + self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError)) def test_that_assigning_unknown_var_fails(self): script = textwrap.dedent( @@ -162,9 +162,8 @@ class TestCQGI(BaseTest): """ ) - result = cqgi.parse(script).build({'w': "var is not there"}) - self.assertTrue(isinstance(result.exception, - cqgi.InvalidParameterError)) + result = cqgi.parse(script).build({"w": "var is not there"}) + self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError)) def test_that_cq_objects_are_visible(self): script = textwrap.dedent( @@ -198,10 +197,10 @@ class TestCQGI(BaseTest): """ ) - result = cqgi.parse(script).build({'h': False}) + result = cqgi.parse(script).build({"h": False}) self.assertTrue(result.success) - self.assertEqual(result.first_result.shape, '*False*') + self.assertEqual(result.first_result.shape, "*False*") def test_that_only_top_level_vars_are_detected(self): script = textwrap.dedent( diff --git a/tests/TestExporters.py b/tests/test_exporters.py similarity index 62% rename from tests/TestExporters.py rename to tests/test_exporters.py index db631bea..a91eab98 100644 --- a/tests/TestExporters.py +++ b/tests/test_exporters.py @@ -12,7 +12,6 @@ from tests import BaseTest class TestExporters(BaseTest): - def _exportBox(self, eType, stringsToFind): """ Exports a test object, and then looks for @@ -21,31 +20,32 @@ class TestExporters(BaseTest): """ p = Workplane("XY").box(1, 2, 3) - if eType == exporters.ExportTypes.AMF: + if eType == exporters.ExportTypes.AMF: s = io.BytesIO() else: s = io.StringIO() - + exporters.exportShape(p, eType, s, 0.1) - result = '{}'.format(s.getvalue()) + result = "{}".format(s.getvalue()) for q in stringsToFind: self.assertTrue(result.find(q) > -1) return result def testSTL(self): - self._exportBox(exporters.ExportTypes.STL, ['facet normal']) + self._exportBox(exporters.ExportTypes.STL, ["facet normal"]) def testSVG(self): - self._exportBox(exporters.ExportTypes.SVG, ['']) + self._exportBox(exporters.ExportTypes.AMF, [""]) def testSTEP(self): - self._exportBox(exporters.ExportTypes.STEP, ['FILE_SCHEMA']) + self._exportBox(exporters.ExportTypes.STEP, ["FILE_SCHEMA"]) def testTJS(self): - self._exportBox(exporters.ExportTypes.TJS, [ - 'vertices', 'formatVersion', 'faces']) + self._exportBox( + exporters.ExportTypes.TJS, ["vertices", "formatVersion", "faces"] + ) diff --git a/tests/TestImporters.py b/tests/test_importers.py similarity index 78% rename from tests/TestImporters.py rename to tests/test_importers.py index 16be51dc..018fcd9f 100644 --- a/tests/TestImporters.py +++ b/tests/test_importers.py @@ -36,12 +36,18 @@ class TestImporters(BaseTest): self.assertTrue(importedShape.val().ShapeType() == "Solid") # Check the number of faces and vertices per face to make sure we have a box shape - self.assertTrue(importedShape.faces("+X").size() == - 1 and importedShape.faces("+X").vertices().size() == 4) - self.assertTrue(importedShape.faces("+Y").size() == - 1 and importedShape.faces("+Y").vertices().size() == 4) - self.assertTrue(importedShape.faces("+Z").size() == - 1 and importedShape.faces("+Z").vertices().size() == 4) + self.assertTrue( + importedShape.faces("+X").size() == 1 + and importedShape.faces("+X").vertices().size() == 4 + ) + self.assertTrue( + importedShape.faces("+Y").size() == 1 + and importedShape.faces("+Y").vertices().size() == 4 + ) + self.assertTrue( + importedShape.faces("+Z").size() == 1 + and importedShape.faces("+Z").vertices().size() == 4 + ) def testSTEP(self): """ @@ -55,7 +61,8 @@ class TestImporters(BaseTest): not segfault. """ tmpfile = OUTDIR + "/badSTEP.step" - with open(tmpfile, 'w') as f: f.write("invalid STEP file") + with open(tmpfile, "w") as f: + f.write("invalid STEP file") with self.assertRaises(ValueError): importers.importShape(importers.ImportTypes.STEP, tmpfile) @@ -69,6 +76,8 @@ class TestImporters(BaseTest): objs = importers.importShape(importers.ImportTypes.STEP, filename) self.assertEqual(2, len(objs.all())) -if __name__ == '__main__': + +if __name__ == "__main__": import unittest + unittest.main() diff --git a/tests/TestJupyter.py b/tests/test_jupyter.py similarity index 86% rename from tests/TestJupyter.py rename to tests/test_jupyter.py index 7f4b92c2..ad7db4db 100644 --- a/tests/TestJupyter.py +++ b/tests/test_jupyter.py @@ -2,9 +2,10 @@ from tests import BaseTest import cadquery + class TestJupyter(BaseTest): def test_repr_html(self): - cube = cadquery.Workplane('XY').box(1, 1, 1) + cube = cadquery.Workplane("XY").box(1, 1, 1) shape = cube.val() self.assertIsInstance(shape, cadquery.occ_impl.shapes.Solid) diff --git a/tests/TestCQSelectors.py b/tests/test_selectors.py similarity index 75% rename from tests/TestCQSelectors.py rename to tests/test_selectors.py index d557375e..4bddb8ba 100644 --- a/tests/TestCQSelectors.py +++ b/tests/test_selectors.py @@ -1,4 +1,4 @@ -__author__ = 'dcowden' +__author__ = "dcowden" """ Tests for CadQuery Selectors @@ -20,22 +20,19 @@ from cadquery import selectors class TestCQSelectors(BaseTest): - def testWorkplaneCenter(self): "Test Moving workplane center" s = Workplane(Plane.XY()) # current point and world point should be equal - self.assertTupleAlmostEquals( - (0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3) + self.assertTupleAlmostEquals((0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3) # move origin and confirm center moves - s.center(-2.0, -2.0) + s = s.center(-2.0, -2.0) # current point should be 0,0, but - self.assertTupleAlmostEquals( - (-2.0, -2.0, 0.0), s.plane.origin.toTuple(), 3) + self.assertTupleAlmostEquals((-2.0, -2.0, 0.0), s.plane.origin.toTuple(), 3) def testVertices(self): t = makeUnitSquareWire() # square box @@ -43,8 +40,7 @@ class TestCQSelectors(BaseTest): self.assertEqual(4, c.vertices().size()) self.assertEqual(4, c.edges().size()) - self.assertEqual(0, c.vertices().edges().size() - ) # no edges on any vertices + self.assertEqual(0, c.vertices().edges().size()) # no edges on any vertices # but selecting all edges still yields all vertices self.assertEqual(4, c.edges().vertices().size()) self.assertEqual(1, c.wires().size()) # just one wire @@ -71,8 +67,7 @@ class TestCQSelectors(BaseTest): def testFirst(self): c = CQ(makeUnitCube()) self.assertEqual(type(c.vertices().first().val()), Vertex) - self.assertEqual( - type(c.vertices().first().first().first().val()), Vertex) + self.assertEqual(type(c.vertices().first().first().first().val()), Vertex) def testCompounds(self): c = CQ(makeUnitSquareWire()) @@ -99,11 +94,11 @@ class TestCQSelectors(BaseTest): def testFaceTypesFilter(self): "Filters by face type" c = CQ(makeUnitCube()) - self.assertEqual(c.faces().size(), c.faces('%PLANE').size()) - self.assertEqual(c.faces().size(), c.faces('%plane').size()) - self.assertEqual(0, c.faces('%sphere').size()) - self.assertEqual(0, c.faces('%cone').size()) - self.assertEqual(0, c.faces('%SPHERE').size()) + self.assertEqual(c.faces().size(), c.faces("%PLANE").size()) + self.assertEqual(c.faces().size(), c.faces("%plane").size()) + self.assertEqual(0, c.faces("%sphere").size()) + self.assertEqual(0, c.faces("%cone").size()) + self.assertEqual(0, c.faces("%SPHERE").size()) def testPerpendicularDirFilter(self): c = CQ(makeUnitCube()) @@ -131,10 +126,12 @@ class TestCQSelectors(BaseTest): # faces parallel to Z axis self.assertEqual(2, c.faces("|Z").size()) # TODO: provide short names for ParallelDirSelector - self.assertEqual(2, c.faces(selectors.ParallelDirSelector( - Vector((0, 0, 1)))).size()) # same thing as above - self.assertEqual(2, c.faces(selectors.ParallelDirSelector( - Vector((0, 0, -1)))).size()) # same thing as above + self.assertEqual( + 2, c.faces(selectors.ParallelDirSelector(Vector((0, 0, 1)))).size() + ) # same thing as above + self.assertEqual( + 2, c.faces(selectors.ParallelDirSelector(Vector((0, 0, -1)))).size() + ) # same thing as above # just for fun, vertices on faces parallel to z self.assertEqual(8, c.faces("|Z").vertices().size()) @@ -178,97 +175,96 @@ class TestCQSelectors(BaseTest): self.assertEqual(4, len(el)) def testNthDistance(self): - c = Workplane('XY').pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1) + c = Workplane("XY").pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1) # 2nd face val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), 1)).val() self.assertAlmostEqual(val.Center().x, -1.5) # 2nd face with inversed selection vector - val = c.faces(selectors.DirectionNthSelector( - Vector(-1, 0, 0), 1)).val() + val = c.faces(selectors.DirectionNthSelector(Vector(-1, 0, 0), 1)).val() self.assertAlmostEqual(val.Center().x, 1.5) # 2nd last face - val = c.faces(selectors.DirectionNthSelector( - Vector(1, 0, 0), -2)).val() + val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), -2)).val() self.assertAlmostEqual(val.Center().x, 1.5) # Last face - val = c.faces(selectors.DirectionNthSelector( - Vector(1, 0, 0), -1)).val() + val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), -1)).val() self.assertAlmostEqual(val.Center().x, 2.5) # check if the selected face if normal to the specified Vector - self.assertAlmostEqual( - val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0) + self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0) # repeat the test using string based selector # 2nd face - val = c.faces('>(1,0,0)[1]').val() + val = c.faces(">(1,0,0)[1]").val() self.assertAlmostEqual(val.Center().x, -1.5) - val = c.faces('>X[1]').val() + val = c.faces(">X[1]").val() self.assertAlmostEqual(val.Center().x, -1.5) # 2nd face with inversed selection vector - val = c.faces('>(-1,0,0)[1]').val() + val = c.faces(">(-1,0,0)[1]").val() self.assertAlmostEqual(val.Center().x, 1.5) - val = c.faces('X[-2]').val() + val = c.faces(">X[-2]").val() self.assertAlmostEqual(val.Center().x, 1.5) # Last face - val = c.faces('>X[-1]').val() + val = c.faces(">X[-1]").val() self.assertAlmostEqual(val.Center().x, 2.5) # check if the selected face if normal to the specified Vector - self.assertAlmostEqual( - val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0) + self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0) # test selection of multiple faces with the same distance - c = Workplane('XY')\ - .box(1, 4, 1, centered=(False, True, False)).faces('Z')\ + c = ( + Workplane("XY") + .box(1, 4, 1, centered=(False, True, False)) + .faces("Z") .box(1, 1, 1, centered=(True, True, False)) + ) # select 2nd from the bottom (NB python indexing is 0-based) - vals = c.faces('>Z[1]').vals() + vals = c.faces(">Z[1]").vals() self.assertEqual(len(vals), 2) - val = c.faces('>Z[1]').val() + val = c.faces(">Z[1]").val() self.assertAlmostEqual(val.Center().z, 1) # do the same but by selecting 3rd from the top - vals = c.faces('Z[-1] is equivalent to >Z - val1 = c.faces('>Z[-1]').val() - val2 = c.faces('>Z').val() - self.assertTupleAlmostEquals(val1.Center().toTuple(), - val2.Center().toTuple(), - 3) + val1 = c.faces(">Z[-1]").val() + val2 = c.faces(">Z").val() + self.assertTupleAlmostEquals( + val1.Center().toTuple(), val2.Center().toTuple(), 3 + ) def testNearestTo(self): c = CQ(makeUnitCube()) @@ -302,7 +298,7 @@ class TestCQSelectors(BaseTest): ((0.9, -0.1, -0.1), (1.1, 0.1, 0.1), (1.0, 0.0, 0.0)), ((0.9, 0.9, -0.1), (1.1, 1.1, 0.1), (1.0, 1.0, 0.0)), ((-0.1, 0.9, -0.1), (0.1, 1.1, 0.1), (0.0, 1.0, 0.0)), - ((0.9, -0.1, 0.9), (1.1, 0.1, 1.1), (1.0, 0.0, 1.0)) + ((0.9, -0.1, 0.9), (1.1, 0.1, 1.1), (1.0, 0.0, 1.0)), ] for d in test_data_vertices: @@ -318,11 +314,13 @@ class TestCQSelectors(BaseTest): self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3) # test multiple vertices selection - vl = c.vertices(selectors.BoxSelector( - (-0.1, -0.1, 0.9), (0.1, 1.1, 1.1))).vals() + vl = c.vertices( + selectors.BoxSelector((-0.1, -0.1, 0.9), (0.1, 1.1, 1.1)) + ).vals() self.assertEqual(2, len(vl)) - vl = c.vertices(selectors.BoxSelector( - (-0.1, -0.1, -0.1), (0.1, 1.1, 1.1))).vals() + vl = c.vertices( + selectors.BoxSelector((-0.1, -0.1, -0.1), (0.1, 1.1, 1.1)) + ).vals() self.assertEqual(4, len(vl)) # test edge selection @@ -331,7 +329,7 @@ class TestCQSelectors(BaseTest): ((0.4, -0.1, -0.1), (0.6, 0.1, 0.1), (0.5, 0.0, 0.0)), ((-0.1, -0.1, 0.4), (0.1, 0.1, 0.6), (0.0, 0.0, 0.5)), ((0.9, 0.9, 0.4), (1.1, 1.1, 0.6), (1.0, 1.0, 0.5)), - ((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0)) + ((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0)), ] for d in test_data_edges: @@ -347,11 +345,9 @@ class TestCQSelectors(BaseTest): self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3) # test multiple edge selection - el = c.edges(selectors.BoxSelector( - (-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals() + el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals() self.assertEqual(2, len(el)) - el = c.edges(selectors.BoxSelector( - (-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals() + el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals() self.assertEqual(3, len(el)) # test face selection @@ -360,7 +356,7 @@ class TestCQSelectors(BaseTest): ((0.4, -0.1, 0.4), (0.6, 0.1, 0.6), (0.5, 0.0, 0.5)), ((0.9, 0.4, 0.4), (1.1, 0.6, 0.6), (1.0, 0.5, 0.5)), ((0.4, 0.4, 0.9), (0.6, 0.6, 1.1), (0.5, 0.5, 1.0)), - ((0.4, 0.4, -0.1), (0.6, 0.6, 0.1), (0.5, 0.5, 0.0)) + ((0.4, 0.4, -0.1), (0.6, 0.6, 0.1), (0.5, 0.5, 0.0)), ] for d in test_data_faces: @@ -376,22 +372,23 @@ class TestCQSelectors(BaseTest): self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3) # test multiple face selection - fl = c.faces(selectors.BoxSelector( - (0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals() + fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals() self.assertEqual(2, len(fl)) - fl = c.faces(selectors.BoxSelector( - (0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals() + fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals() self.assertEqual(3, len(fl)) # test boundingbox option - el = c.edges(selectors.BoxSelector( - (-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True)).vals() + el = c.edges( + selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True) + ).vals() self.assertEqual(1, len(el)) - fl = c.faces(selectors.BoxSelector( - (0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True)).vals() + fl = c.faces( + selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True) + ).vals() self.assertEqual(0, len(fl)) - fl = c.faces(selectors.BoxSelector( - (-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True)).vals() + fl = c.faces( + selectors.BoxSelector((-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True) + ).vals() self.assertEqual(1, len(fl)) def testAndSelector(self): @@ -400,12 +397,13 @@ class TestCQSelectors(BaseTest): S = selectors.StringSyntaxSelector BS = selectors.BoxSelector - el = c.edges(selectors.AndSelector( - S('|X'), BS((-2, -2, 0.1), (2, 2, 2)))).vals() + el = c.edges( + selectors.AndSelector(S("|X"), BS((-2, -2, 0.1), (2, 2, 2))) + ).vals() self.assertEqual(2, len(el)) # test 'and' (intersection) operator - el = c.edges(S('|X') & BS((-2, -2, 0.1), (2, 2, 2))).vals() + el = c.edges(S("|X") & BS((-2, -2, 0.1), (2, 2, 2))).vals() self.assertEqual(2, len(el)) # test using extended string syntax @@ -455,27 +453,27 @@ class TestCQSelectors(BaseTest): S = selectors.StringSyntaxSelector - fl = c.faces(selectors.InverseSelector(S('>Z'))).vals() + fl = c.faces(selectors.InverseSelector(S(">Z"))).vals() self.assertEqual(5, len(fl)) - el = c.faces('>Z').edges(selectors.InverseSelector(S('>X'))).vals() + el = c.faces(">Z").edges(selectors.InverseSelector(S(">X"))).vals() self.assertEqual(3, len(el)) # test invert operator - fl = c.faces(-S('>Z')).vals() + fl = c.faces(-S(">Z")).vals() self.assertEqual(5, len(fl)) - el = c.faces('>Z').edges(-S('>X')).vals() + el = c.faces(">Z").edges(-S(">X")).vals() self.assertEqual(3, len(el)) # test using extended string syntax - fl = c.faces('not >Z').vals() + fl = c.faces("not >Z").vals() self.assertEqual(5, len(fl)) - el = c.faces('>Z').edges('not >X').vals() + el = c.faces(">Z").edges("not >X").vals() self.assertEqual(3, len(el)) def testComplexStringSelector(self): c = CQ(makeUnitCube()) - v = c.vertices('(>X and >Y) or (X and >Y) or (XZ', - '(1,4,55.)[20]', - '|XY', - '(0,0,1) or XY except >(1,1,1)[-1]', - '(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)', - 'not ( X or Y )'] + expressions = [ + "+X ", + "-Y", + "|(1,0,0)", + "#(1.,1.4114,-0.532)", + "%Plane", + ">XZ", + "(1,4,55.)[20]", + "|XY", + "(0,0,1) or XY except >(1,1,1)[-1]", + "(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)", + "not ( X or Y )", + ] for e in expressions: gram.parseString(e, parseAll=True) diff --git a/tests/TestWorkplanes.py b/tests/test_workplanes.py similarity index 76% rename from tests/TestWorkplanes.py rename to tests/test_workplanes.py index 414a16f1..9548e7ed 100644 --- a/tests/TestWorkplanes.py +++ b/tests/test_workplanes.py @@ -16,65 +16,70 @@ zInvAxis_ = Vector(0, 0, -1) class TestWorkplanes(BaseTest): - def testYZPlaneOrigins(self): # xy plane-- with origin at x=0.25 base = Vector(0.25, 0, 0) p = Plane(base, Vector(0, 1, 0), Vector(1, 0, 0)) # origin is always (0,0,0) in local coordinates - self.assertTupleAlmostEquals( - (0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) + self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) - #(0,0,0) is always the original base in global coordinates + # (0,0,0) is always the original base in global coordinates self.assertTupleAlmostEquals( - base.toTuple(), p.toWorldCoords((0, 0)).toTuple(), 2) + base.toTuple(), p.toWorldCoords((0, 0)).toTuple(), 2 + ) def testXYPlaneOrigins(self): base = Vector(0, 0, 0.25) p = Plane(base, Vector(1, 0, 0), Vector(0, 0, 1)) # origin is always (0,0,0) in local coordinates - self.assertTupleAlmostEquals( - (0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) + self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) - #(0,0,0) is always the original base in global coordinates + # (0,0,0) is always the original base in global coordinates self.assertTupleAlmostEquals( - toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2) + toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2 + ) def testXZPlaneOrigins(self): base = Vector(0, 0.25, 0) p = Plane(base, Vector(0, 0, 1), Vector(0, 1, 0)) - #(0,0,0) is always the original base in global coordinates + # (0,0,0) is always the original base in global coordinates self.assertTupleAlmostEquals( - toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2) + toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2 + ) # origin is always (0,0,0) in local coordinates - self.assertTupleAlmostEquals( - (0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) + self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) def testPlaneBasics(self): p = Plane.XY() # local to world self.assertTupleAlmostEquals( - (1.0, 1.0, 0), p.toWorldCoords((1, 1)).toTuple(), 2) + (1.0, 1.0, 0), p.toWorldCoords((1, 1)).toTuple(), 2 + ) self.assertTupleAlmostEquals( - (-1.0, -1.0, 0), p.toWorldCoords((-1, -1)).toTuple(), 2) + (-1.0, -1.0, 0), p.toWorldCoords((-1, -1)).toTuple(), 2 + ) # world to local self.assertTupleAlmostEquals( - (-1.0, -1.0), p.toLocalCoords(Vector(-1, -1, 0)).toTuple(), 2) + (-1.0, -1.0), p.toLocalCoords(Vector(-1, -1, 0)).toTuple(), 2 + ) self.assertTupleAlmostEquals( - (1.0, 1.0), p.toLocalCoords(Vector(1, 1, 0)).toTuple(), 2) + (1.0, 1.0), p.toLocalCoords(Vector(1, 1, 0)).toTuple(), 2 + ) p = Plane.YZ() self.assertTupleAlmostEquals( - (0, 1.0, 1.0), p.toWorldCoords((1, 1)).toTuple(), 2) + (0, 1.0, 1.0), p.toWorldCoords((1, 1)).toTuple(), 2 + ) # world to local self.assertTupleAlmostEquals( - (1.0, 1.0), p.toLocalCoords(Vector(0, 1, 1)).toTuple(), 2) + (1.0, 1.0), p.toLocalCoords(Vector(0, 1, 1)).toTuple(), 2 + ) p = Plane.XZ() r = p.toWorldCoords((1, 1)).toTuple() @@ -82,62 +87,68 @@ class TestWorkplanes(BaseTest): # world to local self.assertTupleAlmostEquals( - (1.0, 1.0), p.toLocalCoords(Vector(1, 0, 1)).toTuple(), 2) + (1.0, 1.0), p.toLocalCoords(Vector(1, 0, 1)).toTuple(), 2 + ) def testOffsetPlanes(self): "Tests that a plane offset from the origin works ok too" p = Plane.XY(origin=(10.0, 10.0, 0)) self.assertTupleAlmostEquals( - (11.0, 11.0, 0.0), p.toWorldCoords((1.0, 1.0)).toTuple(), 2) - self.assertTupleAlmostEquals((2.0, 2.0), p.toLocalCoords( - Vector(12.0, 12.0, 0)).toTuple(), 2) + (11.0, 11.0, 0.0), p.toWorldCoords((1.0, 1.0)).toTuple(), 2 + ) + self.assertTupleAlmostEquals( + (2.0, 2.0), p.toLocalCoords(Vector(12.0, 12.0, 0)).toTuple(), 2 + ) # TODO test these offsets in the other dimensions too p = Plane.YZ(origin=(0, 2, 2)) self.assertTupleAlmostEquals( - (0.0, 5.0, 5.0), p.toWorldCoords((3.0, 3.0)).toTuple(), 2) - self.assertTupleAlmostEquals((10, 10.0, 0.0), p.toLocalCoords( - Vector(0.0, 12.0, 12.0)).toTuple(), 2) + (0.0, 5.0, 5.0), p.toWorldCoords((3.0, 3.0)).toTuple(), 2 + ) + self.assertTupleAlmostEquals( + (10, 10.0, 0.0), p.toLocalCoords(Vector(0.0, 12.0, 12.0)).toTuple(), 2 + ) p = Plane.XZ(origin=(2, 0, 2)) r = p.toWorldCoords((1.0, 1.0)).toTuple() self.assertTupleAlmostEquals((3.0, 0.0, 3.0), r, 2) - self.assertTupleAlmostEquals((10.0, 10.0), p.toLocalCoords( - Vector(12.0, 0.0, 12.0)).toTuple(), 2) + self.assertTupleAlmostEquals( + (10.0, 10.0), p.toLocalCoords(Vector(12.0, 0.0, 12.0)).toTuple(), 2 + ) def testXYPlaneBasics(self): - p = Plane.named('XY') + p = Plane.named("XY") self.assertTupleAlmostEquals(p.zDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4) def testYZPlaneBasics(self): - p = Plane.named('YZ') + p = Plane.named("YZ") self.assertTupleAlmostEquals(p.zDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4) def testZXPlaneBasics(self): - p = Plane.named('ZX') + p = Plane.named("ZX") self.assertTupleAlmostEquals(p.zDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4) def testXZPlaneBasics(self): - p = Plane.named('XZ') + p = Plane.named("XZ") self.assertTupleAlmostEquals(p.zDir.toTuple(), yInvAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4) def testYXPlaneBasics(self): - p = Plane.named('YX') + p = Plane.named("YX") self.assertTupleAlmostEquals(p.zDir.toTuple(), zInvAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4) def testZYPlaneBasics(self): - p = Plane.named('ZY') + p = Plane.named("ZY") self.assertTupleAlmostEquals(p.zDir.toTuple(), xInvAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4)