Merge branch 'master' into OCP

This commit is contained in:
adam-urbanczyk
2020-03-22 17:53:01 +01:00
56 changed files with 6554 additions and 3930 deletions

View File

@ -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

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ dist/*
.idea/* .idea/*
cadquery.egg-info cadquery.egg-info
target/* target/*
.vscode

View File

@ -5,7 +5,6 @@ dist: trusty
branches: branches:
only: only:
- adam-urbanczyk-OCC-version-update
- master - master
- "/\\d+\\.\\d+\\.?d*\\-*[a-z]*/" - "/\\d+\\.\\d+\\.?d*\\-*[a-z]*/"
@ -25,62 +24,51 @@ env:
matrix: matrix:
include: include:
- env: TRAVIS_PYTHON_VERSION=3.6 - name: "Python 3.6 - osx"
env: PYTHON_VERSION=3.6
os: osx os: osx
- env: TRAVIS_PYTHON_VERSION=3.6 - name: "Python 3.6 - linux"
env: PYTHON_VERSION=3.6
os: linux os: linux
- env: TRAVIS_PYTHON_VERSION=3.7 - name: "Python 3.7 - osx"
env: PYTHON_VERSION=3.7
os: osx os: osx
- env: TRAVIS_PYTHON_VERSION=3.7 - name: "Python 3.7 - linux"
env: PYTHON_VERSION=3.7
os: linux os: linux
- name: "Lint"
env: PYTHON_VERSION=3.7
os: linux
script:
- black . --diff --check
before_install: before_install:
- if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
PY_MAJOR=2 ;
else
PY_MAJOR=3 ;
fi ;
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
OS=Linux ; OS=Linux ;
else else
OS=MacOSX ; OS=MacOSX ;
fi ; 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; - bash miniconda.sh -b -p $HOME/miniconda;
- export PATH="$HOME/miniconda/bin:$HOME/miniconda/lib:$PATH"; - export PATH="$HOME/miniconda/bin:$HOME/miniconda/lib:$PATH";
- hash -r;
- conda config --set always_yes yes --set changeps1 no; - 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 - conda env create -f environment.yml
pyparsing mock; - source ~/miniconda/bin/activate cadquery
- source ~/miniconda/bin/activate test_cq; - conda install -c conda-forge -c defaults -c cadquery python=$PYTHON_VERSION
- python -c 'import OCC.gp as gp; print(gp.gp_Vec())'
- pip install codecov
install: install:
- python setup.py install - python setup.py install
before_script: before_script:
- ulimit -c unlimited -S - ulimit -c unlimited -S
- sudo rm -f /cores/core.* - sudo rm -f /cores/core.*
script: script:
- coverage run runtests.py - pytest -v --cov
after_success: after_success:
- codecov - codecov
after_failure: after_failure:
- ls /cores/core.* - ls /cores/core.*
- lldb --core `ls /cores/core.*` --batch --one-line "bt" - 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

View File

@ -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. 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 ### How to Report a Bug
When filing a bug report [issue](https://github.com/CadQuery/cadquery/issues), please be sure to answer these questions: When filing a bug report [issue](https://github.com/CadQuery/cadquery/issues), please be sure to answer these questions:

View File

@ -9,7 +9,7 @@ environment:
MINICONDA_DIRNAME: C:\Miniconda36-x64 MINICONDA_DIRNAME: C:\Miniconda36-x64
- PYTHON_VERSION: 3.7 - PYTHON_VERSION: 3.7
MINICONDA_DIRNAME: C:\Miniconda37-x64 MINICONDA_DIRNAME: C:\Miniconda37-x64
ANACONDA_TOKEN: ANACONDA_TOKEN:
secure: nxF/a2f3iS9KXGu7B/wKJYAk7Sm5wyAjoZoqJvPbRoVK4saaozVwOxDrjwJjJAYb secure: nxF/a2f3iS9KXGu7B/wKJYAk7Sm5wyAjoZoqJvPbRoVK4saaozVwOxDrjwJjJAYb
@ -17,34 +17,14 @@ install:
- set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%"
- conda config --set always_yes yes - conda config --set always_yes yes
- conda update -q conda - 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 - conda env create -f environment.yml
- activate cqtest - activate cadquery
- pip install codecov - conda install -c conda-forge -c defaults -c cadquery python=%PYTHON_VERSION%
- python setup.py install
build: false build: false
test_script: test_script:
- coverage run runtests.py - pytest -v --cov
on_success: on_success:
- codecov - 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

View File

@ -1,25 +1,65 @@
# these items point to the OCC implementation # these items point to the OCC implementation
from .occ_impl.geom import Plane, BoundBox, Vector, Matrix from .occ_impl.geom import Plane, BoundBox, Vector, Matrix
from .occ_impl.shapes import (Shape, Vertex, Edge, Face, Wire, Solid, Shell, from .occ_impl.shapes import (
Compound, sortWiresByBuildOrder) Shape,
Vertex,
Edge,
Face,
Wire,
Solid,
Shell,
Compound,
sortWiresByBuildOrder,
)
from .occ_impl import exporters from .occ_impl import exporters
from .occ_impl import importers from .occ_impl import importers
# these items are the common implementation # these items are the common implementation
# the order of these matter # the order of these matter
from .selectors import (NearestToPointSelector, ParallelDirSelector, from .selectors import (
DirectionSelector, PerpendicularDirSelector, TypeSelector, NearestToPointSelector,
DirectionMinMaxSelector, StringSyntaxSelector, Selector) ParallelDirSelector,
DirectionSelector,
PerpendicularDirSelector,
TypeSelector,
DirectionMinMaxSelector,
StringSyntaxSelector,
Selector,
)
from .cq import CQ, Workplane, selectors from .cq import CQ, Workplane, selectors
from . import plugins from . import plugins
__all__ = [ __all__ = [
'CQ', 'Workplane', 'plugins', 'selectors', 'Plane', 'BoundBox', 'Matrix', 'Vector', 'sortWiresByBuildOrder', "CQ",
'Shape', 'Vertex', 'Edge', 'Wire', 'Face', 'Solid', 'Shell', 'Compound', 'exporters', 'importers', "Workplane",
'NearestToPointSelector', 'ParallelDirSelector', 'DirectionSelector', 'PerpendicularDirSelector', "plugins",
'TypeSelector', 'DirectionMinMaxSelector', 'StringSyntaxSelector', 'Selector', '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"

File diff suppressed because it is too large Load Diff

View File

@ -20,13 +20,22 @@ template = """
</div> </div>
""" """
template_content_indent = ' ' template_content_indent = " "
def cq_directive(name, arguments, options, content, lineno, def cq_directive(
content_offset, block_text, state, state_machine): name,
arguments,
options,
content,
lineno,
content_offset,
block_text,
state,
state_machine,
):
# only consider inline snippets # 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 # 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 # 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 = [] lines = []
# get rid of new lines # get rid of new lines
out_svg = out_svg.replace('\n', '') out_svg = out_svg.replace("\n", "")
txt_align = "left" txt_align = "left"
if "align" in options: 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(["::", ""])
lines.extend([' %s' % row.rstrip() lines.extend([" %s" % row.rstrip() for row in plot_code.split("\n")])
for row in plot_code.split('\n')]) lines.append("")
lines.append('')
if len(lines): if len(lines):
state_machine.insert_input( state_machine.insert_input(lines, state_machine.input_lines.source(0))
lines, state_machine.input_lines.source(0))
return [] return []
@ -77,9 +84,10 @@ def setup(app):
setup.config = app.config setup.config = app.config
setup.confdir = app.confdir setup.confdir = app.confdir
options = {'height': directives.length_or_unitless, options = {
'width': directives.length_or_percentage_or_unitless, "height": directives.length_or_unitless,
'align': directives.unchanged "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)

View File

@ -9,6 +9,7 @@ import cadquery
CQSCRIPT = "<cqscript>" CQSCRIPT = "<cqscript>"
def parse(script_source): def parse(script_source):
""" """
Parses the script as a model, and returns a model. 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 the build method can be used to generate a 3d model
""" """
def __init__(self, script_source): def __init__(self, script_source):
""" """
Create an object by parsing the supplied python script. Create an object by parsing the supplied python script.
@ -100,16 +102,20 @@ class CQModel(object):
try: try:
self.set_param_values(build_parameters) self.set_param_values(build_parameters)
collector = ScriptCallback() collector = ScriptCallback()
env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \ env = (
.add_entry("__name__", "__cqgi__") \ EnvironmentBuilder()
.add_entry("show_object", collector.show_object) \ .with_real_builtins()
.add_entry("debug", collector.debug) \ .with_cadquery_objects()
.add_entry("describe_parameter",collector.describe_parameter) \ .add_entry("__name__", "__cqgi__")
.add_entry("show_object", collector.show_object)
.add_entry("debug", collector.debug)
.add_entry("describe_parameter", collector.describe_parameter)
.build() .build()
)
c = compile(self.ast_tree, CQSCRIPT, 'exec') c = compile(self.ast_tree, CQSCRIPT, "exec")
exec (c, env) exec(c, env)
result.set_debug(collector.debugObjects ) result.set_debug(collector.debugObjects)
result.set_success_result(collector.outputObjects) result.set_success_result(collector.outputObjects)
except Exception as ex: except Exception as ex:
@ -124,7 +130,9 @@ class CQModel(object):
for k, v in params.items(): for k, v in params.items():
if k not in model_parameters: 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 = model_parameters[k]
p.set_value(v) p.set_value(v)
@ -134,10 +142,12 @@ class ShapeResult(object):
""" """
An object created by a build, including the user parameters provided An object created by a build, including the user parameters provided
""" """
def __init__(self): def __init__(self):
self.shape = None self.shape = None
self.options = None self.options = None
class BuildResult(object): class BuildResult(object):
""" """
The result of executing a CadQuery script. The result of executing a CadQuery script.
@ -149,10 +159,11 @@ class BuildResult(object):
If unsuccessful, the exception property contains a reference to If unsuccessful, the exception property contains a reference to
the stack trace that occurred. the stack trace that occurred.
""" """
def __init__(self): def __init__(self):
self.buildTime = None self.buildTime = None
self.results = [] #list of ShapeResult self.results = [] # list of ShapeResult
self.debugObjects = [] #list of ShapeResult self.debugObjects = [] # list of ShapeResult
self.first_result = None self.first_result = None
self.success = False self.success = False
self.exception = None self.exception = None
@ -176,13 +187,14 @@ class ScriptMetadata(object):
Defines the metadata for a parsed CQ Script. Defines the metadata for a parsed CQ Script.
the parameters property is a dict of InputParameter objects. the parameters property is a dict of InputParameter objects.
""" """
def __init__(self): def __init__(self):
self.parameters = {} self.parameters = {}
def add_script_parameter(self, p): def add_script_parameter(self, p):
self.parameters[p.name] = 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 = self.parameters[name]
p.desc = description p.desc = description
@ -214,6 +226,7 @@ class InputParameter:
provide additional metadata provide additional metadata
""" """
def __init__(self): def __init__(self):
#: the default value for the variable. #: the default value for the variable.
@ -234,7 +247,9 @@ class InputParameter:
self.ast_node = None self.ast_node = None
@staticmethod @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: if valid_values is None:
valid_values = [] valid_values = []
@ -251,8 +266,10 @@ class InputParameter:
def set_value(self, new_value): def set_value(self, new_value):
if len(self.valid_values) > 0 and new_value not in self.valid_values: if len(self.valid_values) > 0 and new_value not in self.valid_values:
raise InvalidParameterError( raise InvalidParameterError(
"Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} " "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} ".format(
.format(str(new_value), self.name, str(self.valid_values))) str(new_value), self.name, str(self.valid_values)
)
)
if self.varType == NumberParameterType: if self.varType == NumberParameterType:
try: try:
@ -265,28 +282,33 @@ class InputParameter:
self.ast_node.n = f self.ast_node.n = f
except ValueError: except ValueError:
raise InvalidParameterError( raise InvalidParameterError(
"Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric." "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric.".format(
.format(str(new_value), self.name)) str(new_value), self.name
)
)
elif self.varType == StringParameterType: elif self.varType == StringParameterType:
self.ast_node.s = str(new_value) self.ast_node.s = str(new_value)
elif self.varType == BooleanParameterType: elif self.varType == BooleanParameterType:
if new_value: if new_value:
if hasattr(ast, 'NameConstant'): if hasattr(ast, "NameConstant"):
self.ast_node.value = True self.ast_node.value = True
else: else:
self.ast_node.id = 'True' self.ast_node.id = "True"
else: else:
if hasattr(ast, 'NameConstant'): if hasattr(ast, "NameConstant"):
self.ast_node.value = False self.ast_node.value = False
else: else:
self.ast_node.id = 'False' self.ast_node.id = "False"
else: else:
raise ValueError("Unknown Type of var: ", str(self.varType)) raise ValueError("Unknown Type of var: ", str(self.varType))
def __str__(self): def __str__(self):
return "InputParameter: {name=%s, type=%s, defaultValue=%s" % ( 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): class ScriptCallback(object):
@ -295,22 +317,23 @@ class ScriptCallback(object):
the show_object() method is exposed to CQ scripts, to allow them the show_object() method is exposed to CQ scripts, to allow them
to return objects to the execution environment to return objects to the execution environment
""" """
def __init__(self): def __init__(self):
self.outputObjects = [] self.outputObjects = []
self.debugObjects = [] self.debugObjects = []
def show_object(self, shape,options={}): def show_object(self, shape, options={}):
""" """
return an object to the executing environment, with options return an object to the executing environment, with options
:param shape: a cadquery object :param shape: a cadquery object
:param options: a dictionary of options that will be made available to the executing environment :param options: a dictionary of options that will be made available to the executing environment
""" """
o = ShapeResult() o = ShapeResult()
o.options=options o.options = options
o.shape = shape o.shape = shape
self.outputObjects.append(o) self.outputObjects.append(o)
def debug(self,obj,args={}): def debug(self, obj, args={}):
""" """
Debug print/output an object, with optional arguments. Debug print/output an object, with optional arguments.
""" """
@ -319,7 +342,7 @@ class ScriptCallback(object):
s.options = args s.options = args
self.debugObjects.append(s) 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. 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 return len(self.outputObjects) > 0
class InvalidParameterError(Exception): class InvalidParameterError(Exception):
""" """
Raised when an attempt is made to provide a new parameter value Raised when an attempt is made to provide a new parameter value
that cannot be assigned to the model that cannot be assigned to the model
""" """
pass pass
@ -349,6 +372,7 @@ class NoOutputError(Exception):
Raised when the script does not execute the show_object() method to Raised when the script does not execute the show_object() method to
return a solid return a solid
""" """
pass pass
@ -386,6 +410,7 @@ class EnvironmentBuilder(object):
The environment includes the builtins, as well as The environment includes the builtins, as well as
the other methods the script will need. the other methods the script will need.
""" """
def __init__(self): def __init__(self):
self.env = {} self.env = {}
@ -393,12 +418,12 @@ class EnvironmentBuilder(object):
return self.with_builtins(__builtins__) return self.with_builtins(__builtins__)
def with_builtins(self, env_dict): def with_builtins(self, env_dict):
self.env['__builtins__'] = env_dict self.env["__builtins__"] = env_dict
return self return self
def with_cadquery_objects(self): def with_cadquery_objects(self):
self.env['cadquery'] = cadquery self.env["cadquery"] = cadquery
self.env['cq'] = cadquery self.env["cq"] = cadquery
return self return self
def add_entry(self, name, value): def add_entry(self, name, value):
@ -408,30 +433,33 @@ class EnvironmentBuilder(object):
def build(self): def build(self):
return self.env return self.env
class ParameterDescriptionFinder(ast.NodeTransformer): class ParameterDescriptionFinder(ast.NodeTransformer):
""" """
Visits a parse tree, looking for function calls to describe_parameter(var, description ) Visits a parse tree, looking for function calls to describe_parameter(var, description )
""" """
def __init__(self, cq_model): def __init__(self, cq_model):
self.cqModel = 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? Called when we see a function call. Is it describe_parameter?
""" """
try: try:
if node.func.id == 'describe_parameter': if node.func.id == "describe_parameter":
# looks like we have a call to our function. # looks like we have a call to our function.
# first parameter is the variable, # first parameter is the variable,
# second is the description # second is the description
varname = node.args[0].id varname = node.args[0].id
desc = node.args[1].s desc = node.args[1].s
self.cqModel.add_parameter_description(varname,desc) self.cqModel.add_parameter_description(varname, desc)
except: except:
#print "Unable to handle function call" # print "Unable to handle function call"
pass pass
return node return node
class ConstantAssignmentFinder(ast.NodeTransformer): class ConstantAssignmentFinder(ast.NodeTransformer):
""" """
@ -446,24 +474,42 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
if type(value_node) == ast.Num: if type(value_node) == ast.Num:
self.cqModel.add_script_parameter( 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: elif type(value_node) == ast.Str:
self.cqModel.add_script_parameter( 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: elif type(value_node) == ast.Name:
if value_node.id == 'True': if value_node.id == "True":
self.cqModel.add_script_parameter( self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, True)) InputParameter.create(
elif value_node.id == 'False': value_node, var_name, BooleanParameterType, True
)
)
elif value_node.id == "False":
self.cqModel.add_script_parameter( self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, False)) InputParameter.create(
elif hasattr(ast, 'NameConstant') and type(value_node) == ast.NameConstant: value_node, var_name, BooleanParameterType, False
)
)
elif hasattr(ast, "NameConstant") and type(value_node) == ast.NameConstant:
if value_node.value == True: if value_node.value == True:
self.cqModel.add_script_parameter( self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, True)) InputParameter.create(
value_node, var_name, BooleanParameterType, True
)
)
else: else:
self.cqModel.add_script_parameter( self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, False)) InputParameter.create(
value_node, var_name, BooleanParameterType, False
)
)
except: except:
print("Unable to handle assignment for variable '%s'" % var_name) print("Unable to handle assignment for variable '%s'" % var_name)
pass pass
@ -479,7 +525,7 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
# Handle the NamedConstant type that is only present in Python 3 # Handle the NamedConstant type that is only present in Python 3
astTypes = [ast.Num, ast.Str, ast.Name] astTypes = [ast.Num, ast.Str, ast.Name]
if hasattr(ast, 'NameConstant'): if hasattr(ast, "NameConstant"):
astTypes.append(ast.NameConstant) astTypes.append(ast.NameConstant)
if type(node.value) in astTypes: if type(node.value) in astTypes:

View File

@ -1,9 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
#from OCP.Visualization import Tesselator
# from OCP.Visualization import Tesselator
import tempfile import tempfile
import os import os
import sys import sys
if sys.version_info.major == 2: if sys.version_info.major == 2:
import cStringIO as StringIO import cStringIO as StringIO
else: 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 The object should be already open and ready to write. The caller is responsible
for closing the object for closing the object
""" """
from ..cq import CQ from ..cq import CQ
def tessellate(shape): def tessellate(shape):
@ -72,7 +74,7 @@ def exportShape(shape, exportType, fileLike, tolerance=0.1):
# add vertices # add vertices
for v in tess[0]: for v in tess[0]:
mesher.addVertex(v.x,v.y,v.z) mesher.addVertex(v.x, v.y, v.z)
# add triangles # add triangles
for t in tess[1]: for t in tess[1]:
@ -112,7 +114,7 @@ def readAndDeleteFile(fileName):
return the contents as a string return the contents as a string
""" """
res = "" res = ""
with open(fileName, 'r') as f: with open(fileName, "r") as f:
res = "{}".format(f.read()) res = "{}".format(f.read())
os.remove(fileName) os.remove(fileName)
@ -148,32 +150,32 @@ class AmfWriter(object):
self.tessellation = tessellation self.tessellation = tessellation
def writeAmf(self, outFile): 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 # TODO: if result is a compound, we need to loop through them
object = ET.SubElement(amf, 'object', id="0") object = ET.SubElement(amf, "object", id="0")
mesh = ET.SubElement(object, 'mesh') mesh = ET.SubElement(object, "mesh")
vertices = ET.SubElement(mesh, 'vertices') vertices = ET.SubElement(mesh, "vertices")
volume = ET.SubElement(mesh, 'volume') volume = ET.SubElement(mesh, "volume")
# add vertices # add vertices
for v in self.tessellation[0]: for v in self.tessellation[0]:
vtx = ET.SubElement(vertices, 'vertex') vtx = ET.SubElement(vertices, "vertex")
coord = ET.SubElement(vtx, 'coordinates') coord = ET.SubElement(vtx, "coordinates")
x = ET.SubElement(coord, 'x') x = ET.SubElement(coord, "x")
x.text = str(v.x) x.text = str(v.x)
y = ET.SubElement(coord, 'y') y = ET.SubElement(coord, "y")
y.text = str(v.y) y.text = str(v.y)
z = ET.SubElement(coord, 'z') z = ET.SubElement(coord, "z")
z.text = str(v.z) z.text = str(v.z)
# add triangles # add triangles
for t in self.tessellation[1]: for t in self.tessellation[1]:
triangle = ET.SubElement(volume, 'triangle') triangle = ET.SubElement(volume, "triangle")
v1 = ET.SubElement(triangle, 'v1') v1 = ET.SubElement(triangle, "v1")
v1.text = str(t[0]) v1.text = str(t[0])
v2 = ET.SubElement(triangle, 'v2') v2 = ET.SubElement(triangle, "v2")
v2.text = str(t[1]) v2.text = str(t[1])
v3 = ET.SubElement(triangle, 'v3') v3 = ET.SubElement(triangle, "v3")
v3.text = str(t[2]) v3.text = str(t[2])
amf = ET.ElementTree(amf).write(outFile, xml_declaration=True) amf = ET.ElementTree(amf).write(outFile, xml_declaration=True)
@ -211,11 +213,11 @@ class JsonMesh(object):
def toJson(self): def toJson(self):
return JSON_TEMPLATE % { return JSON_TEMPLATE % {
'vertices': str(self.vertices), "vertices": str(self.vertices),
'faces': str(self.faces), "faces": str(self.faces),
'nVertices': self.nVertices, "nVertices": self.nVertices,
'nFaces': self.nFaces "nFaces": self.nFaces,
}; }
def makeSVGedge(e): def makeSVGedge(e):
@ -229,20 +231,16 @@ def makeSVGedge(e):
start = curve.FirstParameter() start = curve.FirstParameter()
end = curve.LastParameter() end = curve.LastParameter()
points = GCPnts_QuasiUniformDeflection(curve, points = GCPnts_QuasiUniformDeflection(curve, DISCRETIZATION_TOLERANCE, start, end)
DISCRETIZATION_TOLERANCE,
start,
end)
if points.IsDone(): if points.IsDone():
point_it = (points.Value(i + 1) for i in point_it = (points.Value(i + 1) for i in range(points.NbPoints()))
range(points.NbPoints()))
p = next(point_it) 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: for p in point_it:
cs.write('L{},{} '.format(p.X(), p.Y())) cs.write("L{},{} ".format(p.X(), p.Y()))
return cs.getvalue() return cs.getvalue()
@ -271,7 +269,7 @@ def getSVG(shape, opts=None):
Export a shape to SVG 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: if opts:
d.update(opts) d.update(opts)
@ -279,17 +277,15 @@ def getSVG(shape, opts=None):
# need to guess the scale and the coordinate center # need to guess the scale and the coordinate center
uom = guessUnitOfMeasure(shape) uom = guessUnitOfMeasure(shape)
width = float(d['width']) width = float(d["width"])
height = float(d['height']) height = float(d["height"])
marginLeft = float(d['marginLeft']) marginLeft = float(d["marginLeft"])
marginTop = float(d['marginTop']) marginTop = float(d["marginTop"])
hlr = HLRBRep_Algo() hlr = HLRBRep_Algo()
hlr.Add(shape.wrapped) hlr.Add(shape.wrapped)
projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), DEFAULT_DIR))
DEFAULT_DIR)
)
hlr.Projector(projector) hlr.Projector(projector)
hlr.Update() hlr.Update()
@ -330,8 +326,7 @@ def getSVG(shape, opts=None):
# convert to native CQ objects # convert to native CQ objects
visible = list(map(Shape, visible)) visible = list(map(Shape, visible))
hidden = list(map(Shape, hidden)) hidden = list(map(Shape, hidden))
(hiddenPaths, visiblePaths) = getPaths(visible, (hiddenPaths, visiblePaths) = getPaths(visible, hidden)
hidden)
# get bounding box -- these are all in 2-d space # get bounding box -- these are all in 2-d space
bb = Compound.makeCompound(hidden + visible).BoundingBox() 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) unitScale = min(width / bb.xlen * 0.75, height / bb.ylen * 0.75)
# compute amount to translate-- move the top left into view # compute amount to translate-- move the top left into view
(xTranslate, yTranslate) = ((0 - bb.xmin) + marginLeft / (xTranslate, yTranslate) = (
unitScale, (0 - bb.ymax) - marginTop / unitScale) (0 - bb.xmin) + marginLeft / unitScale,
(0 - bb.ymax) - marginTop / unitScale,
)
# compute paths ( again -- had to strip out freecad crap ) # compute paths ( again -- had to strip out freecad crap )
hiddenContent = "" hiddenContent = ""
@ -356,19 +353,19 @@ def getSVG(shape, opts=None):
{ {
"unitScale": str(unitScale), "unitScale": str(unitScale),
"strokeWidth": str(1.0 / unitScale), "strokeWidth": str(1.0 / unitScale),
"hiddenContent": hiddenContent, "hiddenContent": hiddenContent,
"visibleContent": visibleContent, "visibleContent": visibleContent,
"xTranslate": str(xTranslate), "xTranslate": str(xTranslate),
"yTranslate": str(yTranslate), "yTranslate": str(yTranslate),
"width": str(width), "width": str(width),
"height": str(height), "height": str(height),
"textboxY": str(height - 30), "textboxY": str(height - 30),
"uom": str(uom) "uom": str(uom),
} }
) )
# svg = SVG_TEMPLATE % ( # svg = SVG_TEMPLATE % (
# {"content": projectedContent} # {"content": projectedContent}
#) # )
return svg return svg
@ -380,7 +377,7 @@ def exportSVG(shape, fileName):
""" """
svg = getSVG(shape.val()) svg = getSVG(shape.val())
f = open(fileName, 'w') f = open(fileName, "w")
f.write(svg) f.write(svg)
f.close() f.close()
@ -465,4 +462,4 @@ SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
</svg> </svg>
""" """
PATHTEMPLATE = "\t\t\t<path d=\"%s\" />\n" PATHTEMPLATE = '\t\t\t<path d="%s" />\n'

View File

@ -1,5 +1,5 @@
import math 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.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.Bnd import Bnd_Box
from OCP.BRepBndLib import BRepBndLib from OCP.BRepBndLib import BRepBndLib
@ -27,16 +27,16 @@ class Vector(object):
if len(args) == 3: if len(args) == 3:
fV = gp_Vec(*args) fV = gp_Vec(*args)
elif len(args) == 2: elif len(args) == 2:
fV = gp_Vec(*args,0) fV = gp_Vec(*args, 0)
elif len(args) == 1: elif len(args) == 1:
if isinstance(args[0], Vector): if isinstance(args[0], Vector):
fV = gp_Vec(args[0].wrapped.XYZ()) fV = gp_Vec(args[0].wrapped.XYZ())
elif isinstance(args[0], (tuple, list)): elif isinstance(args[0], (tuple, list)):
arg = args[0] arg = args[0]
if len(arg)==3: if len(arg) == 3:
fV = gp_Vec(*arg) fV = gp_Vec(*arg)
elif len(arg)==2: elif len(arg) == 2:
fV = gp_Vec(*arg,0) fV = gp_Vec(*arg, 0)
elif isinstance(args[0], (gp_Vec, gp_Pnt, gp_Dir)): elif isinstance(args[0], (gp_Vec, gp_Pnt, gp_Dir)):
fV = gp_Vec(args[0].XYZ()) fV = gp_Vec(args[0].XYZ())
elif isinstance(args[0], gp_XYZ): elif isinstance(args[0], gp_XYZ):
@ -53,25 +53,25 @@ class Vector(object):
@property @property
def x(self): def x(self):
return self.wrapped.X() return self.wrapped.X()
@x.setter @x.setter
def x(self,value): def x(self, value):
self.wrapped.SetX(value) self.wrapped.SetX(value)
@property @property
def y(self): def y(self):
return self.wrapped.Y() return self.wrapped.Y()
@y.setter @y.setter
def y(self,value): def y(self, value):
self.wrapped.SetY(value) self.wrapped.SetY(value)
@property @property
def z(self): def z(self):
return self.wrapped.Z() return self.wrapped.Z()
@z.setter @z.setter
def z(self,value): def z(self, value):
self.wrapped.SetZ(value) self.wrapped.SetZ(value)
@property @property
@ -132,16 +132,13 @@ class Vector(object):
return self.wrapped.Angle(v.wrapped) return self.wrapped.Angle(v.wrapped)
def distanceToLine(self): def distanceToLine(self):
raise NotImplementedError( raise NotImplementedError("Have not needed this yet, but OCCT supports it!")
"Have not needed this yet, but FreeCAD supports it!")
def projectToLine(self): def projectToLine(self):
raise NotImplementedError( raise NotImplementedError("Have not needed this yet, but OCCT supports it!")
"Have not needed this yet, but FreeCAD supports it!")
def distanceToPlane(self): def distanceToPlane(self):
raise NotImplementedError( raise NotImplementedError("Have not needed this yet, but OCCT supports it!")
"Have not needed this yet, but FreeCAD supports it!")
def projectToPlane(self, plane): def projectToPlane(self, plane):
""" """
@ -154,7 +151,7 @@ class Vector(object):
base = plane.origin base = plane.origin
normal = plane.zDir 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): def __neg__(self):
return self * -1 return self * -1
@ -163,18 +160,19 @@ class Vector(object):
return self.Length return self.Length
def __repr__(self): def __repr__(self):
return 'Vector: ' + str((self.x, self.y, self.z)) return "Vector: " + str((self.x, self.y, self.z))
def __str__(self): 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): def __eq__(self, other):
return self.wrapped.IsEqual(other.wrapped, 0.00001, 0.00001) return self.wrapped.IsEqual(other.wrapped, 0.00001, 0.00001)
'''
"""
is not implemented in OCC is not implemented in OCC
def __ne__(self, other): def __ne__(self, other):
return self.wrapped.__ne__(other) return self.wrapped.__ne__(other)
''' """
def toPnt(self): def toPnt(self):
@ -222,44 +220,48 @@ class Matrix:
elif isinstance(matrix, (list, tuple)): elif isinstance(matrix, (list, tuple)):
# Validate matrix size & 4x4 last row value # Validate matrix size & 4x4 last row value
valid_sizes = all( valid_sizes = all(
(isinstance(row, (list, tuple)) and (len(row) == 4)) (isinstance(row, (list, tuple)) and (len(row) == 4)) for row in matrix
for row in matrix
) and len(matrix) in (3, 4) ) and len(matrix) in (3, 4)
if not valid_sizes: if not valid_sizes:
raise TypeError("Matrix constructor requires 2d list of 4x3 or 4x4, but got: {!r}".format(matrix)) raise TypeError(
elif (len(matrix) == 4) and (tuple(matrix[3]) != (0,0,0,1)): "Matrix constructor requires 2d list of 4x3 or 4x4, but got: {!r}".format(
raise ValueError("Expected the last row to be [0,0,0,1], but got: {!r}".format(matrix[3])) 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 # Assign values to matrix
self.wrapped = gp_GTrsf() self.wrapped = gp_GTrsf()
[self.wrapped.SetValue(i+1,j+1,e) [
for i,row in enumerate(matrix[:3]) self.wrapped.SetValue(i + 1, j + 1, e)
for j,e in enumerate(row)] for i, row in enumerate(matrix[:3])
for j, e in enumerate(row)
]
else: else:
raise TypeError( raise TypeError("Invalid param to matrix constructor: {}".format(matrix))
"Invalid param to matrix constructor: {}".format(matrix))
def rotateX(self, angle): def rotateX(self, angle):
self._rotate(gp.OX_s(), self._rotate(gp.OX_s(), angle)
angle)
def rotateY(self, angle): def rotateY(self, angle):
self._rotate(gp.OY_s(), self._rotate(gp.OY_s(), angle)
angle)
def rotateZ(self, angle): def rotateZ(self, angle):
self._rotate(gp.OZ_s(), self._rotate(gp.OZ_s(), angle)
angle)
def _rotate(self, direction, angle): def _rotate(self, direction, angle):
new = gp_Trsf() new = gp_Trsf()
new.SetRotation(direction, new.SetRotation(direction, angle)
angle)
self.wrapped = self.wrapped * gp_GTrsf(new) self.wrapped = self.wrapped * gp_GTrsf(new)
@ -277,11 +279,12 @@ class Matrix:
def transposed_list(self): def transposed_list(self):
"""Needed by the cqparts gltf exporter """Needed by the cqparts gltf exporter
""" """
trsf = self.wrapped trsf = self.wrapped
data = [[trsf.Value(i,j) for j in range(1,5)] for i in range(1,4)] + \ data = [[trsf.Value(i, j) for j in range(1, 5)] for i in range(1, 4)] + [
[[0.,0.,0.,1.]] [0.0, 0.0, 0.0, 1.0]
]
return [data[j][i] for i in range(4) for j in range(4)] return [data[j][i] for i in range(4) for j in range(4)]
def __getitem__(self, rc): def __getitem__(self, rc):
@ -298,7 +301,7 @@ class Matrix:
else: else:
# gp_GTrsf doesn't provide access to the 4th row because it has # gp_GTrsf doesn't provide access to the 4th row because it has
# an implied value as below: # an implied value as below:
return [0., 0., 0., 1.][c] return [0.0, 0.0, 0.0, 1.0][c]
else: else:
raise IndexError("Out of bounds access into 4x4 matrix: {!r}".format(rc)) raise IndexError("Out of bounds access into 4x4 matrix: {!r}".format(rc))
@ -352,95 +355,94 @@ class Plane(object):
namedPlanes = { namedPlanes = {
# origin, xDir, normal # origin, xDir, normal
'XY': Plane(origin, (1, 0, 0), (0, 0, 1)), "XY": Plane(origin, (1, 0, 0), (0, 0, 1)),
'YZ': Plane(origin, (0, 1, 0), (1, 0, 0)), "YZ": Plane(origin, (0, 1, 0), (1, 0, 0)),
'ZX': Plane(origin, (0, 0, 1), (0, 1, 0)), "ZX": Plane(origin, (0, 0, 1), (0, 1, 0)),
'XZ': Plane(origin, (1, 0, 0), (0, -1, 0)), "XZ": Plane(origin, (1, 0, 0), (0, -1, 0)),
'YX': Plane(origin, (0, 1, 0), (0, 0, -1)), "YX": Plane(origin, (0, 1, 0), (0, 0, -1)),
'ZY': Plane(origin, (0, 0, 1), (-1, 0, 0)), "ZY": Plane(origin, (0, 0, 1), (-1, 0, 0)),
'front': Plane(origin, (1, 0, 0), (0, 0, 1)), "front": Plane(origin, (1, 0, 0), (0, 0, 1)),
'back': 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)), "left": Plane(origin, (0, 0, 1), (-1, 0, 0)),
'right': 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)), "top": Plane(origin, (1, 0, 0), (0, 1, 0)),
'bottom': Plane(origin, (1, 0, 0), (0, -1, 0)) "bottom": Plane(origin, (1, 0, 0), (0, -1, 0)),
} }
try: try:
return namedPlanes[stdName] return namedPlanes[stdName]
except KeyError: except KeyError:
raise ValueError('Supported names are {}'.format( raise ValueError("Supported names are {}".format(list(namedPlanes.keys())))
list(namedPlanes.keys())))
@classmethod @classmethod
def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def YZ(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def ZX(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def XZ(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def YX(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def ZY(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def front(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def back(cls, origin=(0, 0, 0), xDir=Vector(-1, 0, 0)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def left(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def right(cls, origin=(0, 0, 0), xDir=Vector(0, 0, -1)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def top(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): 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) plane._setPlaneDir(xDir)
return plane return plane
@classmethod @classmethod
def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): 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) plane._setPlaneDir(xDir)
return plane 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. :return: a plane in the global space, with the xDirection of the plane in the specified direction.
""" """
zDir = Vector(normal) zDir = Vector(normal)
if (zDir.Length == 0.0): if zDir.Length == 0.0:
raise ValueError('normal should be non null') raise ValueError("normal should be non null")
xDir = Vector(xDir) xDir = Vector(xDir)
if (xDir.Length == 0.0): if xDir.Length == 0.0:
raise ValueError('xDir should be non null') raise ValueError("xDir should be non null")
self.zDir = zDir.normalized() self.zDir = zDir.normalized()
self._setPlaneDir(xDir) self._setPlaneDir(xDir)
@ -489,7 +491,8 @@ class Plane(object):
@property @property
def origin(self): def origin(self):
return self._origin 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 @origin.setter
def origin(self, value): def origin(self, value):
@ -545,7 +548,7 @@ class Plane(object):
pass pass
''' """
# TODO: also use a set of points along the wire to test as well. # 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 # TODO: would it be more efficient to create objects in the local
# coordinate system, and then transform to global # coordinate system, and then transform to global
@ -562,7 +565,7 @@ class Plane(object):
# findOutsideBox actually inspects both ways, here we only want to # findOutsideBox actually inspects both ways, here we only want to
# know if one is inside the other # know if one is inside the other
return bb == BoundBox.findOutsideBox2D(bb, tb) return bb == BoundBox.findOutsideBox2D(bb, tb)
''' """
def toLocalCoords(self, obj): def toLocalCoords(self, obj):
"""Project the provided coordinates onto this plane """Project the provided coordinates onto this plane
@ -580,7 +583,7 @@ class Plane(object):
""" """
from .shapes import Shape from .shapes import Shape
if isinstance(obj, Vector): if isinstance(obj, Vector):
return obj.transform(self.fG) return obj.transform(self.fG)
elif isinstance(obj, Shape): elif isinstance(obj, Shape):
@ -588,7 +591,9 @@ class Plane(object):
else: else:
raise ValueError( raise ValueError(
"Don't know how to convert type {} to local coordinates".format( "Don't know how to convert type {} to local coordinates".format(
type(obj))) type(obj)
)
)
def toWorldCoords(self, tuplePoint): def toWorldCoords(self, tuplePoint):
"""Convert a point in local coordinates to global coordinates """Convert a point in local coordinates to global coordinates
@ -619,19 +624,29 @@ class Plane(object):
:param rotate: Vector [xDegrees, yDegrees, zDegrees] :param rotate: Vector [xDegrees, yDegrees, zDegrees]
:return: a copy of this plane rotated as requested. :return: a copy of this plane rotated as requested.
""" """
# NB: this is not a geometric Vector
rotate = Vector(rotate) rotate = Vector(rotate)
# Convert to radians. # Convert to radians.
rotate = rotate.multiply(math.pi / 180.0) rotate = rotate.multiply(math.pi / 180.0)
# Compute rotation matrix. # Compute rotation matrix.
m = Matrix() T1 = gp_Trsf()
m.rotateX(rotate.x) T1.SetRotation(
m.rotateY(rotate.y) gp_Ax1(gp_Pnt(*(0, 0, 0)), gp_Dir(*self.xDir.toTuple())), rotate.x
m.rotateZ(rotate.z) )
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. # Compute the new plane.
newXdir = self.xDir.transform(m) newXdir = self.xDir.transform(T)
newZdir = self.zDir.transform(m) newZdir = self.zDir.transform(T)
return Plane(self.origin, newXdir, newZdir) return Plane(self.origin, newXdir, newZdir)
@ -655,7 +670,7 @@ class Plane(object):
raise NotImplementedError raise NotImplementedError
''' """
resultWires = [] resultWires = []
for w in listOfShapes: for w in listOfShapes:
mirrored = w.transformGeometry(rotationMatrix.wrapped) mirrored = w.transformGeometry(rotationMatrix.wrapped)
@ -681,21 +696,19 @@ class Plane(object):
resultWires.append(cadquery.Shape.cast(mirroredWire)) 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(), local_coord_system = gp_Ax3(
self.zDir.toDir(), self.origin.toPnt(), self.zDir.toDir(), self.xDir.toDir()
self.xDir.toDir()) )
T = gp_Trsf() T = gp_Trsf()
if axis == 'X': if axis == "X":
T.SetMirror(gp_Ax1(self.origin.toPnt(), T.SetMirror(gp_Ax1(self.origin.toPnt(), local_coord_system.XDirection()))
local_coord_system.XDirection())) elif axis == "Y":
elif axis == 'Y': T.SetMirror(gp_Ax1(self.origin.toPnt(), local_coord_system.YDirection()))
T.SetMirror(gp_Ax1(self.origin.toPnt(),
local_coord_system.YDirection()))
else: else:
raise NotImplementedError raise NotImplementedError
@ -726,22 +739,21 @@ class Plane(object):
# the double-inverting is strange, and I don't understand it. # the double-inverting is strange, and I don't understand it.
forward = Matrix() forward = Matrix()
inverse = Matrix() inverse = Matrix()
forwardT = gp_Trsf() forwardT = gp_Trsf()
inverseT = gp_Trsf() inverseT = gp_Trsf()
global_coord_system = gp_Ax3() global_coord_system = gp_Ax3()
local_coord_system = gp_Ax3(gp_Pnt(*self.origin.toTuple()), local_coord_system = gp_Ax3(
gp_Dir(*self.zDir.toTuple()), gp_Pnt(*self.origin.toTuple()),
gp_Dir(*self.xDir.toTuple()) gp_Dir(*self.zDir.toTuple()),
) gp_Dir(*self.xDir.toTuple()),
)
forwardT.SetTransformation(global_coord_system, forwardT.SetTransformation(global_coord_system, local_coord_system)
local_coord_system)
forward.wrapped = gp_GTrsf(forwardT) forward.wrapped = gp_GTrsf(forwardT)
inverseT.SetTransformation(local_coord_system, inverseT.SetTransformation(local_coord_system, global_coord_system)
global_coord_system)
inverse.wrapped = gp_GTrsf(inverseT) inverse.wrapped = gp_GTrsf(inverseT)
# TODO verify if this is OK # TODO verify if this is OK
@ -751,7 +763,7 @@ class Plane(object):
class BoundBox(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): def __init__(self, bb):
self.wrapped = bb self.wrapped = bb
@ -767,11 +779,9 @@ class BoundBox(object):
self.zmax = ZMax self.zmax = ZMax
self.zlen = ZMax - ZMin self.zlen = ZMax - ZMin
self.center = Vector((XMax + XMin) / 2, self.center = Vector((XMax + XMin) / 2, (YMax + YMin) / 2, (ZMax + ZMin) / 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): def add(self, obj, tol=1e-8):
"""Returns a modified (expanded) bounding box """Returns a modified (expanded) bounding box
@ -810,30 +820,36 @@ class BoundBox(object):
the built-in implementation i do not understand. the built-in implementation i do not understand.
""" """
if (bb1.XMin < bb2.XMin and if (
bb1.XMax > bb2.XMax and bb1.XMin < bb2.XMin
bb1.YMin < bb2.YMin and and bb1.XMax > bb2.XMax
bb1.YMax > bb2.YMax): and bb1.YMin < bb2.YMin
and bb1.YMax > bb2.YMax
):
return bb1 return bb1
if (bb2.XMin < bb1.XMin and if (
bb2.XMax > bb1.XMax and bb2.XMin < bb1.XMin
bb2.YMin < bb1.YMin and and bb2.XMax > bb1.XMax
bb2.YMax > bb1.YMax): and bb2.YMin < bb1.YMin
and bb2.YMax > bb1.YMax
):
return bb2 return bb2
return None return None
@classmethod @classmethod
def _fromTopoDS(cls, shape, tol=None, optimal=True): def _fromTopoDS(cls, shape, tol=None, optimal=True):
''' """
Constructs a bounding box from a TopoDS_Shape Constructs a bounding box from a TopoDS_Shape
''' """
tol = TOL if tol is None else tol # tol = TOL (by default) tol = TOL if tol is None else tol # tol = TOL (by default)
bbox = Bnd_Box() bbox = Bnd_Box()
if optimal: 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: else:
mesh = BRepMesh_IncrementalMesh(shape, tol, True) mesh = BRepMesh_IncrementalMesh(shape, tol, True)
mesh.Perform() mesh.Perform()
@ -844,12 +860,14 @@ class BoundBox(object):
def isInside(self, b2): def isInside(self, b2):
"""Is the provided bounding box inside this one?""" """Is the provided bounding box inside this one?"""
if (b2.xmin > self.xmin and if (
b2.ymin > self.ymin and b2.xmin > self.xmin
b2.zmin > self.zmin and and b2.ymin > self.ymin
b2.xmax < self.xmax and and b2.zmin > self.zmin
b2.ymax < self.ymax and and b2.xmax < self.xmax
b2.zmax < self.zmax): and b2.ymax < self.ymax
and b2.zmax < self.zmax
):
return True return True
else: else:
return False return False

View File

@ -34,14 +34,14 @@ def importStep(fileName):
Accepts a file name and loads the STEP file into a cadquery shape 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 :param fileName: The path and name of the STEP file to be imported
""" """
# Now read and return the shape # Now read and return the shape
reader = STEPControl_Reader() reader = STEPControl_Reader()
readStatus = reader.ReadFile(fileName) readStatus = reader.ReadFile(fileName)
if readStatus != OCP.IFSelect.IFSelect_RetDone: if readStatus != OCP.IFSelect.IFSelect_RetDone:
raise ValueError("STEP File could not be loaded") raise ValueError("STEP File could not be loaded")
for i in range(reader.NbRootsForTransfer()): for i in range(reader.NbRootsForTransfer()):
reader.TransferRoot(i+1) reader.TransferRoot(i + 1)
occ_shapes = [] occ_shapes = []
for i in range(reader.NbShapes()): for i in range(reader.NbShapes()):

View File

@ -1,103 +1,8 @@
from OCC.Display.WebGl.x3dom_renderer import X3DExporter from IPython.display import SVG
from OCC.Core.gp import gp_Quaternion, gp_Vec
from uuid import uuid4
from math import tan
from xml.etree import ElementTree
from .geom import BoundBox from .exporters import toString, ExportTypes
BOILERPLATE = \
'''
<link rel='stylesheet' type='text/css' href='http://www.x3dom.org/download/x3dom.css'></link>
<div style='height: {height}px; width: 100%;' width='100%' height='{height}px'>
<x3d style='height: {height}px; width: 100%;' id='{id}' width='100%' height='{height}px'>
<scene>
<Viewpoint position='{x},{y},{z}' centerOfRotation='{x0} {y0} {z0}' orientation='{rot}' fieldOfView='{fov}'></Viewpoint>
{src}
</scene>
</x3d>
</div>
<script>
if (document.getElementById('X3DOM_JS_MODULE') == null){{
var scr = document.createElement('script');
head = document.head || document.getElementsByTagName('head')[0];
scr.src = 'http://www.x3dom.org/download/x3dom.js';
scr.async = false;
scr.id = 'X3DOM_JS_MODULE';
scr.onload = function () {{
x3dom.reload();
}}
head.insertBefore(scr, head.lastChild);
}}
else if (typeof x3dom != 'undefined') {{ //call reload only if x3dom already loaded
x3dom.reload();
}}
//document.getElementById('{id}').runtime.fitAll() def display(shape):
</script>
'''
#https://stackoverflow.com/questions/950087/how-do-i-include-a-javascript-file-in-another-javascript-file return SVG(toString(shape, ExportTypes.SVG))
#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 <Scene> 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))

File diff suppressed because it is too large Load Diff

View File

@ -21,9 +21,22 @@ import re
import math import math
from cadquery import Vector, Edge, Vertex, Face, Solid, Shell, Compound from cadquery import Vector, Edge, Vertex, Face, Solid, Shell, Compound
from collections import defaultdict from collections import defaultdict
from pyparsing import Literal, Word, nums, Optional, Combine, oneOf, upcaseTokens,\ from pyparsing import (
CaselessLiteral, Group, infixNotation, opAssoc, Forward,\ Literal,
ZeroOrMore, Keyword Word,
nums,
Optional,
Combine,
oneOf,
upcaseTokens,
CaselessLiteral,
Group,
infixNotation,
opAssoc,
Forward,
ZeroOrMore,
Keyword,
)
from functools import reduce from functools import reduce
@ -81,7 +94,6 @@ class NearestToPointSelector(Selector):
self.pnt = pnt self.pnt = pnt
def filter(self, objectList): def filter(self, objectList):
def dist(tShape): def dist(tShape):
return tShape.Center().sub(Vector(*self.pnt)).Length return tShape.Center().sub(Vector(*self.pnt)).Length
# if tShape.ShapeType == 'Vertex': # if tShape.ShapeType == 'Vertex':
@ -121,15 +133,18 @@ class BoxSelector(Selector):
def isInsideBox(p): def isInsideBox(p):
# using XOR for checking if x/y/z is in between regardless # using XOR for checking if x/y/z is in between regardless
# of order of x/y/z0 and x/y/z1 # of order of x/y/z0 and x/y/z1
return ((p.x < x0) ^ (p.x < x1)) and \ return (
((p.y < y0) ^ (p.y < y1)) and \ ((p.x < x0) ^ (p.x < x1))
((p.z < z0) ^ (p.z < z1)) and ((p.y < y0) ^ (p.y < y1))
and ((p.z < z0) ^ (p.z < z1))
)
for o in objectList: for o in objectList:
if self.test_boundingbox: if self.test_boundingbox:
bb = o.BoundingBox() bb = o.BoundingBox()
if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and \ if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and isInsideBox(
isInsideBox(Vector(bb.xmax, bb.ymax, bb.zmax)): Vector(bb.xmax, bb.ymax, bb.zmax)
):
result.append(o) result.append(o)
else: else:
if isInsideBox(o.Center()): if isInsideBox(o.Center()):
@ -168,7 +183,9 @@ class BaseDirSelector(Selector):
if self.test(normal): if self.test(normal):
r.append(o) 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 # an edge is parallel to a direction if its underlying geometry is plane or line
tangent = o.tangentAt() tangent = o.tangentAt()
if self.test(tangent): if self.test(tangent):
@ -247,8 +264,7 @@ class PerpendicularDirSelector(BaseDirSelector):
def test(self, vec): def test(self, vec):
angle = self.direction.getAngle(vec) angle = self.direction.getAngle(vec)
r = (abs(angle) < self.TOLERANCE) or ( r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE)
abs(angle - math.pi) < self.TOLERANCE)
return not r return not r
@ -314,17 +330,16 @@ class DirectionMinMaxSelector(Selector):
self.TOLERANCE = tolerance self.TOLERANCE = tolerance
def filter(self, objectList): def filter(self, objectList):
def distance(tShape): def distance(tShape):
return tShape.Center().dot(self.vector) return tShape.Center().dot(self.vector)
# import OrderedDict # import OrderedDict
from collections import OrderedDict from collections import OrderedDict
# make and distance to object dict # make and distance to object dict
objectDict = {distance(el): el for el in objectList} objectDict = {distance(el): el for el in objectList}
# transform it into an ordered dict # transform it into an ordered dict
objectDict = OrderedDict(sorted(list(objectDict.items()), objectDict = OrderedDict(sorted(list(objectDict.items()), key=lambda x: x[0]))
key=lambda x: x[0]))
# find out the max/min distance # find out the max/min distance
if self.directionMax: if self.directionMax:
@ -370,8 +385,9 @@ class DirectionNthSelector(ParallelDirSelector):
objectDict[round(distance(el), digits)].append(el) objectDict[round(distance(el), digits)].append(el)
# choose the Nth unique rounded distance # choose the Nth unique rounded distance
nth_distance = sorted(list(objectDict.keys()), nth_distance = sorted(list(objectDict.keys()), reverse=not self.directionMax)[
reverse=not self.directionMax)[self.N] self.N
]
# map back to original objects and return # map back to original objects and return
return objectDict[nth_distance] return objectDict[nth_distance]
@ -388,8 +404,9 @@ class BinarySelector(Selector):
self.right = right self.right = right
def filter(self, objectList): def filter(self, objectList):
return self.filterResults(self.left.filter(objectList), return self.filterResults(
self.right.filter(objectList)) self.left.filter(objectList), self.right.filter(objectList)
)
def filterResults(self, r_left, r_right): def filterResults(self, r_left, r_right):
raise NotImplementedError raise NotImplementedError
@ -445,52 +462,56 @@ def _makeGrammar():
""" """
# float definition # float definition
point = Literal('.') point = Literal(".")
plusmin = Literal('+') | Literal('-') plusmin = Literal("+") | Literal("-")
number = Word(nums) number = Word(nums)
integer = Combine(Optional(plusmin) + number) integer = Combine(Optional(plusmin) + number)
floatn = Combine(integer + Optional(point + Optional(number))) floatn = Combine(integer + Optional(point + Optional(number)))
# vector definition # vector definition
lbracket = Literal('(') lbracket = Literal("(")
rbracket = Literal(')') rbracket = Literal(")")
comma = Literal(',') comma = Literal(",")
vector = Combine(lbracket + floatn('x') + comma + vector = Combine(
floatn('y') + comma + floatn('z') + rbracket) lbracket + floatn("x") + comma + floatn("y") + comma + floatn("z") + rbracket
)
# direction definition # direction definition
simple_dir = oneOf(['X', 'Y', 'Z', 'XY', 'XZ', 'YZ']) simple_dir = oneOf(["X", "Y", "Z", "XY", "XZ", "YZ"])
direction = simple_dir('simple_dir') | vector('vector_dir') direction = simple_dir("simple_dir") | vector("vector_dir")
# CQ type definition # CQ type definition
cqtype = oneOf(['Plane', 'Cylinder', 'Sphere', 'Cone', 'Line', 'Circle', 'Arc'], cqtype = oneOf(
caseless=True) ["Plane", "Cylinder", "Sphere", "Cone", "Line", "Circle", "Arc"], caseless=True
)
cqtype = cqtype.setParseAction(upcaseTokens) cqtype = cqtype.setParseAction(upcaseTokens)
# type operator # type operator
type_op = Literal('%') type_op = Literal("%")
# direction operator # direction operator
direction_op = oneOf(['>', '<']) direction_op = oneOf([">", "<"])
# index definition # index definition
ix_number = Group(Optional('-') + Word(nums)) ix_number = Group(Optional("-") + Word(nums))
lsqbracket = Literal('[').suppress() lsqbracket = Literal("[").suppress()
rsqbracket = Literal(']').suppress() rsqbracket = Literal("]").suppress()
index = lsqbracket + ix_number('index') + rsqbracket index = lsqbracket + ix_number("index") + rsqbracket
# other operators # other operators
other_op = oneOf(['|', '#', '+', '-']) other_op = oneOf(["|", "#", "+", "-"])
# named view # named view
named_view = oneOf(['front', 'back', 'left', 'right', 'top', 'bottom']) named_view = oneOf(["front", "back", "left", "right", "top", "bottom"])
return direction('only_dir') | \ return (
(type_op('type_op') + cqtype('cq_type')) | \ direction("only_dir")
(direction_op('dir_op') + direction('dir') + Optional(index)) | \ | (type_op("type_op") + cqtype("cq_type"))
(other_op('other_op') + direction('dir')) | \ | (direction_op("dir_op") + direction("dir") + Optional(index))
named_view('named_view') | (other_op("other_op") + direction("dir"))
| named_view("named_view")
)
_grammar = _makeGrammar() # make a grammar instance _grammar = _makeGrammar() # make a grammar instance
@ -506,33 +527,34 @@ class _SimpleStringSyntaxSelector(Selector):
# define all token to object mappings # define all token to object mappings
self.axes = { self.axes = {
'X': Vector(1, 0, 0), "X": Vector(1, 0, 0),
'Y': Vector(0, 1, 0), "Y": Vector(0, 1, 0),
'Z': Vector(0, 0, 1), "Z": Vector(0, 0, 1),
'XY': Vector(1, 1, 0), "XY": Vector(1, 1, 0),
'YZ': Vector(0, 1, 1), "YZ": Vector(0, 1, 1),
'XZ': Vector(1, 0, 1) "XZ": Vector(1, 0, 1),
} }
self.namedViews = { self.namedViews = {
'front': (Vector(0, 0, 1), True), "front": (Vector(0, 0, 1), True),
'back': (Vector(0, 0, 1), False), "back": (Vector(0, 0, 1), False),
'left': (Vector(1, 0, 0), False), "left": (Vector(1, 0, 0), False),
'right': (Vector(1, 0, 0), True), "right": (Vector(1, 0, 0), True),
'top': (Vector(0, 1, 0), True), "top": (Vector(0, 1, 0), True),
'bottom': (Vector(0, 1, 0), False) "bottom": (Vector(0, 1, 0), False),
} }
self.operatorMinMax = { self.operatorMinMax = {
'>': True, ">": True,
'<': False, "<": False,
} }
self.operator = { self.operator = {
'+': DirectionSelector, "+": DirectionSelector,
'-': lambda v: DirectionSelector(-v), "-": lambda v: DirectionSelector(-v),
'#': PerpendicularDirSelector, "#": PerpendicularDirSelector,
'|': ParallelDirSelector} "|": ParallelDirSelector,
}
self.parseResults = parseResults self.parseResults = parseResults
self.mySelector = self._chooseSelector(parseResults) self.mySelector = self._chooseSelector(parseResults)
@ -541,23 +563,25 @@ class _SimpleStringSyntaxSelector(Selector):
""" """
Sets up the underlying filters accordingly Sets up the underlying filters accordingly
""" """
if 'only_dir' in pr: if "only_dir" in pr:
vec = self._getVector(pr) vec = self._getVector(pr)
return DirectionSelector(vec) return DirectionSelector(vec)
elif 'type_op' in pr: elif "type_op" in pr:
return TypeSelector(pr.cq_type) return TypeSelector(pr.cq_type)
elif 'dir_op' in pr: elif "dir_op" in pr:
vec = self._getVector(pr) vec = self._getVector(pr)
minmax = self.operatorMinMax[pr.dir_op] minmax = self.operatorMinMax[pr.dir_op]
if 'index' in pr: if "index" in pr:
return DirectionNthSelector(vec, int(''.join(pr.index.asList())), minmax) return DirectionNthSelector(
vec, int("".join(pr.index.asList())), minmax
)
else: else:
return DirectionMinMaxSelector(vec, minmax) return DirectionMinMaxSelector(vec, minmax)
elif 'other_op' in pr: elif "other_op" in pr:
vec = self._getVector(pr) vec = self._getVector(pr)
return self.operator[pr.other_op](vec) return self.operator[pr.other_op](vec)
@ -569,7 +593,7 @@ class _SimpleStringSyntaxSelector(Selector):
""" """
Translate parsed vector string into a CQ Vector Translate parsed vector string into a CQ Vector
""" """
if 'vector_dir' in pr: if "vector_dir" in pr:
vec = pr.vector_dir vec = pr.vector_dir
return Vector(float(vec.x), float(vec.y), float(vec.z)) return Vector(float(vec.x), float(vec.y), float(vec.z))
else: else:
@ -590,10 +614,10 @@ def _makeExpressionGrammar(atom):
""" """
# define operators # define operators
and_op = Literal('and') and_op = Literal("and")
or_op = Literal('or') or_op = Literal("or")
delta_op = oneOf(['exc', 'except']) delta_op = oneOf(["exc", "except"])
not_op = Literal('not') not_op = Literal("not")
def atom_callback(res): def atom_callback(res):
return _SimpleStringSyntaxSelector(res) return _SimpleStringSyntaxSelector(res)
@ -622,11 +646,15 @@ def _makeExpressionGrammar(atom):
return InverseSelector(right) return InverseSelector(right)
# construct the final grammar and set all the callbacks # construct the final grammar and set all the callbacks
expr = infixNotation(atom, expr = infixNotation(
[(and_op, 2, opAssoc.LEFT, and_callback), atom,
(or_op, 2, opAssoc.LEFT, or_callback), [
(delta_op, 2, opAssoc.LEFT, exc_callback), (and_op, 2, opAssoc.LEFT, and_callback),
(not_op, 1, opAssoc.RIGHT, not_callback)]) (or_op, 2, opAssoc.LEFT, or_callback),
(delta_op, 2, opAssoc.LEFT, exc_callback),
(not_op, 1, opAssoc.RIGHT, not_callback),
],
)
return expr return expr
@ -690,8 +718,7 @@ class StringSyntaxSelector(Selector):
Feed the input string through the parser and construct an relevant complex selector object Feed the input string through the parser and construct an relevant complex selector object
""" """
self.selectorString = selectorString self.selectorString = selectorString
parse_result = _expression_grammar.parseString(selectorString, parse_result = _expression_grammar.parseString(selectorString, parseAll=True)
parseAll=True)
self.mySelector = parse_result.asList()[0] self.mySelector = parse_result.asList()[0]
def filter(self, objectList): def filter(self, objectList):

View File

@ -20,11 +20,12 @@ requirements:
- pyparsing 2.* - pyparsing 2.*
test: test:
requires:
- pytest
source_files: source_files:
- runtests.py
- tests/ - tests/
commands: commands:
- python runtests.py - pytest -v
about: about:
summary: CadQuery fork based on PythonOCC summary: CadQuery fork based on PythonOCC

View File

@ -54,6 +54,7 @@ All 2-d operations require a **Workplane** object to be created.
Workplane.threePointArc Workplane.threePointArc
Workplane.sagittaArc Workplane.sagittaArc
Workplane.radiusArc Workplane.radiusArc
Workplane.tangentArcPoint
Workplane.rotateAndCopy Workplane.rotateAndCopy
Workplane.mirrorY Workplane.mirrorY
Workplane.mirrorX Workplane.mirrorX

View File

@ -13,71 +13,77 @@
import sys, os import sys, os
import os.path 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 import cadquery
#settings._target = None # settings._target = None
# If extensions (or modules to document with autodoc) are in another directory, # 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 # 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. # 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 ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # 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 # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # 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. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = '.rst' source_suffix = ".rst"
# The encoding of source files. # The encoding of source files.
#source_encoding = 'utf-8-sig' # source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = u'CadQuery' project = u"CadQuery"
copyright = u'Parametric Products Intellectual Holdings LLC, All Rights Reserved' copyright = u"Parametric Products Intellectual Holdings LLC, All Rights Reserved"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.0' version = "1.0"
# The full version, including alpha/beta/rc tags. # 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 # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
#language = None # language = None
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
#today = '' # today = ''
# Else, today_fmt is used as the format for a strftime call. # 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 # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # 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. # 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. # 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 # If true, the current module name will be prepended to all description
# unit titles (such as .. function::). # unit titles (such as .. function::).
@ -85,27 +91,27 @@ add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the # If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default. # output. They are ignored by default.
#show_authors = False # show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # 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. # A list of ignored prefixes for module index sorting.
#modindex_common_prefix = [] # modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
#html_theme = 'timlinux-linfiniti-sphinx' # html_theme = 'timlinux-linfiniti-sphinx'
html_theme = 'sphinx_rtd_theme' html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a 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 # further. For a list of options available for each theme, see the
# documentation. # documentation.
#html_theme_options = { # html_theme_options = {
# "headerfont": "'Open Sans',Arial,sans-serif", # "headerfont": "'Open Sans',Arial,sans-serif",
# #"bodyfont:": "'Open Sans',Arial,sans-serif", # #"bodyfont:": "'Open Sans',Arial,sans-serif",
# #"headerbg" : "{image: url('/img/bg/body.jpg');color:#000000;}", # #"headerbg" : "{image: url('/img/bg/body.jpg');color:#000000;}",
@ -115,9 +121,9 @@ html_theme = 'sphinx_rtd_theme'
## "headercolor1": "#13171A;", ## "headercolor1": "#13171A;",
# "headercolor2": "#444;", # "headercolor2": "#444;",
# "headerlinkcolor" : "#13171A;", # "headerlinkcolor" : "#13171A;",
#} # }
#agogo options # agogo options
""" """
bodyfont (CSS font family): Font for normal text. bodyfont (CSS font family): Font for normal text.
headerfont (CSS font family): Font for headings. 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. 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. # 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 # The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation". # "<project> v<release> documentation".
html_title = "CadQuery Documentation" html_title = "CadQuery Documentation"
# A shorter title for the navigation bar. Default is the same as html_title. # 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 # The name of an image file (relative to this directory) to place at the top
# of the sidebar. # 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 # 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 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large. # pixels large.
#html_favicon = None # html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here, # 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, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # 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, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # 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 # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.
#html_use_smartypants = True # html_use_smartypants = True
# Custom sidebar templates, maps document names to template names. # 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 # Additional templates that should be rendered to pages, maps page names to
# template names. # template names.
#html_additional_pages = {} # html_additional_pages = {}
# If false, no module index is generated. # If false, no module index is generated.
#html_domain_indices = True # html_domain_indices = True
# If false, no index is generated. # 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. # 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. # If true, links to the reST sources are added to the pages.
html_show_sourcelink = False html_show_sourcelink = False
@ -187,72 +193,66 @@ html_show_sourcelink = False
html_show_sphinx = False html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # 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 # If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the # contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served. # 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"). # 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. # Output file base name for HTML help builder.
htmlhelp_basename = 'CadQuerydoc' htmlhelp_basename = "CadQuerydoc"
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper', #'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt',
#'pointsize': '10pt', # Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'CadQuery.tex', u'CadQuery Documentation', ("index", "CadQuery.tex", u"CadQuery Documentation", u"David Cowden", "manual"),
u'David Cowden', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
# the title page. # the title page.
#latex_logo = None # latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts, # For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters. # not chapters.
#latex_use_parts = False # latex_use_parts = False
# If true, show page references after internal links. # If true, show page references after internal links.
#latex_show_pagerefs = False # latex_show_pagerefs = False
# If true, show URL addresses after external links. # 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. # Documents to append as an appendix to all manuals.
#latex_appendices = [] # latex_appendices = []
# If false, no module index is generated. # If false, no module index is generated.
#latex_domain_indices = True # latex_domain_indices = True
# -- Options for manual page output -------------------------------------------- # -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [("index", "cadquery", u"CadQuery Documentation", [u"David Cowden"], 1)]
('index', 'cadquery', u'CadQuery Documentation',
[u'David Cowden'], 1)
]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
#man_show_urls = False # man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------ # -- Options for Texinfo output ------------------------------------------------
@ -261,16 +261,22 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'CadQuery', u'CadQuery Documentation', (
u'David Cowden', 'CadQuery', 'A Fluent CAD api', "index",
'Miscellaneous'), "CadQuery",
u"CadQuery Documentation",
u"David Cowden",
"CadQuery",
"A Fluent CAD api",
"Miscellaneous",
),
] ]
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
#texinfo_appendices = [] # texinfo_appendices = []
# If false, no module index is generated. # If false, no module index is generated.
#texinfo_domain_indices = True # texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'. # How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote' # texinfo_show_urls = 'footnote'

View File

@ -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 center_hole_dia = 22.0
# Create a box based on the dimensions above and add a 22mm center hole # Create a box based on the dimensions above and add a 22mm center hole
result = cq.Workplane("XY").box(length, height, thickness) \ result = (cq.Workplane("XY").box(length, height, thickness)
.faces(">Z").workplane().hole(center_hole_dia) .faces(">Z").workplane().hole(center_hole_dia))
show_object(result) show_object(result)
@ -121,8 +121,8 @@ closed curve.
.. cq_plot:: .. cq_plot::
result = cq.Workplane("front").lineTo(2.0, 0).lineTo(2.0, 1.0).threePointArc((1.0, 1.5),(0.0, 1.0))\ 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) .close().extrude(0.25))
show_object(result) 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, 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). 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) result = result.extrude(0.25)
show_object(result) show_object(result)
@ -204,8 +204,8 @@ correct for small hole sizes.
.. cq_plot:: .. cq_plot::
result = cq.Workplane("front").box(3.0, 4.0, 0.25).pushPoints ( [ ( 0,0.75 ),(0, -0.75) ]) \ result = (cq.Workplane("front").box(3.0, 4.0, 0.25).pushPoints ( [ ( 0,0.75 ),(0, -0.75) ])
.polygon(6, 1.0).cutThruAll() .polygon(6, 1.0).cutThruAll())
show_object(result) show_object(result)
.. topic:: Api References .. 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.box`
* :py:meth:`Workplane` * :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 Rotated Workplanes
-------------------------- --------------------------
@ -467,9 +493,9 @@ You can create a rotated work plane by specifying angles of rotation relative to
.. cq_plot:: .. cq_plot::
result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").workplane() \ 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)) \ .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) .rect(1.5,1.5,forConstruction=True).vertices().hole(0.25))
show_object(result) show_object(result)
.. topic:: Api References .. topic:: Api References
@ -492,8 +518,8 @@ In the example below, a rectangle is drawn, and its vertices are used to locate
.. cq_plot:: .. cq_plot::
result = cq.Workplane("front").box(2, 2, 0.5).faces(">Z").workplane() \ result = (cq.Workplane("front").box(2, 2, 0.5).faces(">Z").workplane()
.rect(1.5, 1.5, forConstruction=True).vertices().hole(0.125 ) .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.125 ))
show_object(result) show_object(result)
.. topic:: Api References .. topic:: Api References
@ -538,8 +564,8 @@ and a circular section.
.. cq_plot:: .. cq_plot::
result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").circle(1.5) \ 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) .workplane(offset=3.0).rect(0.75, 0.5).loft(combine=True))
show_object(result) show_object(result)
@ -563,8 +589,8 @@ Similar to :py:meth:`Workplane.hole` , these functions operate on a list of poin
.. cq_plot:: .. cq_plot::
result = cq.Workplane(cq.Plane.XY()).box(4,2, 0.5).faces(">Z").workplane().rect(3.5, 1.5, forConstruction=True)\ 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) .vertices().cboreHole(0.125, 0.25, 0.125, depth=None))
show_object(result) 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.edges`
* :py:meth:`Workplane` * :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").workplane().circle(1).cutThruAll()
.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 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) (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) \ result = (cq.Workplane("XY").box(length,height,thickness).faces(">Z").workplane().hole(bearing_diam)
.faces(">Z").workplane() \ .faces(">Z").workplane()
.rect(length-padding,height-padding,forConstruction=True) \ .rect(length-padding,height-padding,forConstruction=True)
.vertices().cboreHole(2.4, 4.4, 2.1) .vertices().cboreHole(2.4, 4.4, 2.1))
show_object(result) 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) (L,w,t) = (20.0, 6.0, 3.0)
s = cq.Workplane("XY") s = cq.Workplane("XY")
#draw half the profile of the bottle and extrude it # Draw half the profile of the bottle and extrude it
p = s.center(-L/2.0, 0).vLine(w/2.0) \ 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) \ .threePointArc((L/2.0, w/2.0 + t),(L, w/2.0)).vLine(-w/2.0)
.mirrorX().extrude(30.0,True) .mirrorX().extrude(30.0,True))
#make the neck #make the neck
p = p.faces(">Z").workplane().circle(3.0).extrude(2.0,True) 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) oshell = oshell.edges("|Z").fillet(p_sideRadius)
#inner shell #inner shell
ishell = oshell.faces("<Z").workplane(p_thickness,True)\ ishell = (oshell.faces("<Z").workplane(p_thickness,True)
.rect((p_outerWidth - 2.0* p_thickness),(p_outerLength - 2.0*p_thickness))\ .rect((p_outerWidth - 2.0* p_thickness),(p_outerLength - 2.0*p_thickness))
.extrude((p_outerHeight - 2.0*p_thickness),False) #set combine false to produce just the new boss .extrude((p_outerHeight - 2.0*p_thickness),False) #set combine false to produce just the new boss
)
ishell = ishell.edges("|Z").fillet(p_sideRadius - p_thickness) ishell = ishell.edges("|Z").fillet(p_sideRadius - p_thickness)
#make the box outer box #make the box outer box
@ -741,10 +817,10 @@ A Parametric Enclosure
POSTWIDTH = (p_outerWidth - 2.0*p_screwpostInset) POSTWIDTH = (p_outerWidth - 2.0*p_screwpostInset)
POSTLENGTH = (p_outerLength -2.0*p_screwpostInset) POSTLENGTH = (p_outerLength -2.0*p_screwpostInset)
box = box.faces(">Z").workplane(-p_thickness)\ box = (box.faces(">Z").workplane(-p_thickness)
.rect(POSTWIDTH,POSTLENGTH,forConstruction=True)\ .rect(POSTWIDTH,POSTLENGTH,forConstruction=True)
.vertices().circle(p_screwpostOD/2.0).circle(p_screwpostID/2.0)\ .vertices().circle(p_screwpostOD/2.0).circle(p_screwpostID/2.0)
.extrude((-1.0)*(p_outerHeight + p_lipHeight -p_thickness ),True) .extrude((-1.0)*(p_outerHeight + p_lipHeight -p_thickness ),True))
#split lid into top and bottom parts #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 (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").shell(-1.0 * t) s = s.faces("<Z").shell(-1.0 * t)
# make the bumps on the top # make the bumps on the top
s = s.faces(">Z").workplane(). \ s = (s.faces(">Z").workplane().
rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) \ rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0)
.extrude(bumpHeight) .extrude(bumpHeight))
# add posts on the bottom. posts are different diameter depending on geometry # add posts on the bottom. posts are different diameter depending on geometry
# solid studs for 1 bump, tubes for multiple, none for 1x1 # solid studs for 1 bump, tubes for multiple, none for 1x1
tmp = s.faces("<Z").workplane(invert=True) tmp = s.faces("<Z").workplane(invert=True)
if lbumps > 1 and wbumps > 1: if lbumps > 1 and wbumps > 1:
tmp = tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). \ tmp = (tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True).
circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t) circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t))
elif lbumps > 1: elif lbumps > 1:
tmp = tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). \ tmp = (tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True).
circle(t).extrude(height - t) circle(t).extrude(height - t))
elif wbumps > 1: elif wbumps > 1:
tmp = tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). \ tmp = (tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True).
circle(t).extrude(height - t) circle(t).extrude(height - t))
else: else:
tmp = s tmp = s
@ -1018,12 +1094,12 @@ Braille Example
line_start_pos += Point(0, -cell_geometry.interline) line_start_pos += Point(0, -cell_geometry.interline)
r = get_cylinder_radius(cell_geometry) r = get_cylinder_radius(cell_geometry)
base = base.faces('>Z').vertices('<XY').workplane() \ base = (base.faces('>Z').vertices('<XY').workplane()
.pushPoints(dot_pos).circle(r) \ .pushPoints(dot_pos).circle(r)
.extrude(r) .extrude(r))
# Make a fillet almost the same radius to get a pseudo spherical cap. # Make a fillet almost the same radius to get a pseudo spherical cap.
base = base.faces('>Z').edges() \ base = (base.faces('>Z').edges()
.fillet(r - 0.001) .fillet(r - 0.001))
hidding_box = cq.Workplane('XY').box( hidding_box = cq.Workplane('XY').box(
base_width, base_height, base_thickness, centered=(False, False, False)) base_width, base_height, base_thickness, centered=(False, False, False))
result = hidding_box.union(base) result = hidding_box.union(base)
@ -1119,7 +1195,7 @@ This specific examples generates a helical cycloidal gear.
return hypocycloid(t,r1,r2) return hypocycloid(t,r1,r2)
# create the gear profile and extrude it # create the gear profile and extrude it
result = cq.Workplane('XY').parametricCurve(lambda t: gear(t*2*pi,6,1))\ result = (cq.Workplane('XY').parametricCurve(lambda t: gear(t*2*pi,6,1))
.twistExtrude(15,90).faces('>Z').workplane().circle(2).cutThruAll() .twistExtrude(15,90).faces('>Z').workplane().circle(2).cutThruAll())
show_object(result) show_object(result)

View File

@ -111,12 +111,19 @@ backwards in the stack to get the face as well::
You can browse stack access methods here: :ref:`stackMethods`. You can browse stack access methods here: :ref:`stackMethods`.
.. _chaining:
Chaining Chaining
--------------------------- ---------------------------
All CadQuery methods return another CadQuery object, so that you can chain the methods together fluently. Use 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. 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 The Context Solid
--------------------------- ---------------------------

View File

@ -1,14 +1,18 @@
name: cadquery name: cadquery
channels: channels:
- conda-forge
- cadquery - cadquery
- conda-forge
- defaults
dependencies: dependencies:
- python=3.6 - python>=3.6
- pythonocc-core=0.18.2 - cadquery::pythonocc-core
- oce=0.18.2
- pyparsing - pyparsing
- sphinx
- sphinx_rtd_theme
- black
- codecov
- pytest
- pytest-cov
- pip
- pip: - pip:
- "--editable=." - "--editable=."
# Documentation
- sphinx
- sphinx_rtd_theme

View File

@ -1,9 +1,9 @@
import cadquery as cq import cadquery as cq
# These can be modified rather than hardcoding values for each dimension. # These can be modified rather than hardcoding values for each dimension.
length = 80.0 # Length of the block length = 80.0 # Length of the block
height = 60.0 # Height of the block height = 60.0 # Height of the block
thickness = 10.0 # Thickness of the block thickness = 10.0 # Thickness of the block
# Create a 3D block based on the dimension variables above. # Create a 3D block based on the dimension variables above.
# 1. Establishes a workplane that an object can be built on. # 1. Establishes a workplane that an object can be built on.

View File

@ -1,10 +1,10 @@
import cadquery as cq import cadquery as cq
# These can be modified rather than hardcoding values for each dimension. # These can be modified rather than hardcoding values for each dimension.
length = 80.0 # Length of the block length = 80.0 # Length of the block
height = 60.0 # Height of the block height = 60.0 # Height of the block
thickness = 10.0 # Thickness of the block thickness = 10.0 # Thickness of the block
center_hole_dia = 22.0 # Diameter of center hole in 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. # 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. # 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. # 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. # 3. The new workplane is used to drill a hole through the block.
# 3a. The hole is automatically centered in the workplane. # 3a. The hole is automatically centered in the workplane.
result = cq.Workplane("XY").box(length, height, thickness) \ result = (
.faces(">Z").workplane().hole(center_hole_dia) cq.Workplane("XY")
.box(length, height, thickness)
.faces(">Z")
.workplane()
.hole(center_hole_dia)
)
# Displays the result of this script # Displays the result of this script
show_object(result) show_object(result)

View File

@ -1,15 +1,15 @@
import cadquery as cq import cadquery as cq
# These can be modified rather than hardcoding values for each dimension. # These can be modified rather than hardcoding values for each dimension.
length = 80.0 # Length of the block length = 80.0 # Length of the block
width = 60.0 # Width of the block width = 60.0 # Width of the block
height = 100.0 # Height of the block height = 100.0 # Height of the block
thickness = 10.0 # Thickness of the block thickness = 10.0 # Thickness of the block
center_hole_dia = 22.0 # Diameter of center hole in block center_hole_dia = 22.0 # Diameter of center hole in block
cbore_hole_diameter = 2.4 # Bolt shank/threads clearance hole diameter 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_inset = 12.0 # How far from the edge the cbored holes are set
cbore_diameter = 4.4 # Bolt head pocket hole diameter cbore_diameter = 4.4 # Bolt head pocket hole diameter
cbore_depth = 2.1 # Bolt head pocket hole depth cbore_depth = 2.1 # Bolt head pocket hole depth
# Create a 3D block based on the dimensions above and add a 22mm center hold # Create a 3D block based on the dimensions above and add a 22mm center hold
# and 4 counterbored holes for bolts # 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. # do not show up in the final displayed geometry.
# 6. The vertices of the rectangle (corners) are selected, and a counter-bored # 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). # hole is placed at each of the vertices (all 4 of them at once).
result = cq.Workplane("XY").box(length, height, thickness) \ result = (
.faces(">Z").workplane().hole(center_hole_dia) \ cq.Workplane("XY")
.faces(">Z").workplane() \ .box(length, height, thickness)
.rect(length - cbore_inset, height - cbore_inset, forConstruction=True) \ .faces(">Z")
.vertices().cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth) \ .workplane()
.edges("|Z").fillet(2.0) .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 # Displays the result of this script
show_object(result) show_object(result)

View File

@ -1,10 +1,10 @@
import cadquery as cq import cadquery as cq
# These can be modified rather than hardcoding values for each dimension. # These can be modified rather than hardcoding values for each dimension.
circle_radius = 50.0 # Radius of the plate circle_radius = 50.0 # Radius of the plate
thickness = 13.0 # Thickness of the plate thickness = 13.0 # Thickness of the plate
rectangle_width = 13.0 # Width of rectangular hole in cylindrical plate rectangle_width = 13.0 # Width of rectangular hole in cylindrical plate
rectangle_length = 19.0 # Length 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. # Extrude a cylindrical plate with a rectangular hole in the middle of it.
# 1. Establishes a workplane that an object can be built on. # 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. # plate with a rectangular hole in the center.
# 3a. circle() and rect() could be changed to any other shape to completely # 3a. circle() and rect() could be changed to any other shape to completely
# change the resulting plate and/or the hole in it. # change the resulting plate and/or the hole in it.
result = cq.Workplane("front").circle(circle_radius) \ result = (
.rect(rectangle_width, rectangle_length) \ cq.Workplane("front")
.extrude(thickness) .circle(circle_radius)
.rect(rectangle_width, rectangle_length)
.extrude(thickness)
)
# Displays the result of this script # Displays the result of this script
show_object(result) show_object(result)

View File

@ -1,8 +1,8 @@
import cadquery as cq import cadquery as cq
# These can be modified rather than hardcoding values for each dimension. # These can be modified rather than hardcoding values for each dimension.
width = 2.0 # Overall width of the plate width = 2.0 # Overall width of the plate
thickness = 0.25 # Thickness of the plate thickness = 0.25 # Thickness of the plate
# Extrude a plate outline made of lines and an arc # Extrude a plate outline made of lines and an arc
# 1. Establishes a workplane that an object can be built on. # 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 # 7a. Without the close(), the 2D sketch will be left open and the extrude
# operation will provide unpredictable results. # operation will provide unpredictable results.
# 8. The 2D sketch is extruded into a solid object of the specified thickness. # 8. The 2D sketch is extruded into a solid object of the specified thickness.
result = cq.Workplane("front").lineTo(width, 0) \ result = (
.lineTo(width, 1.0) \ cq.Workplane("front")
.threePointArc((1.0, 1.5), (0.0, 1.0)) \ .lineTo(width, 0)
.sagittaArc((-0.5, 1.0), 0.2) \ .lineTo(width, 1.0)
.radiusArc((-0.7, -0.2), -1.5) \ .threePointArc((1.0, 1.5), (0.0, 1.0))
.close().extrude(thickness) .sagittaArc((-0.5, 1.0), 0.2)
.radiusArc((-0.7, -0.2), -1.5)
.close()
.extrude(thickness)
)
# Displays the result of this script # Displays the result of this script
show_object(result) show_object(result)

View File

@ -1,8 +1,8 @@
import cadquery as cq import cadquery as cq
# These can be modified rather than hardcoding values for each dimension. # These can be modified rather than hardcoding values for each dimension.
circle_radius = 3.0 # The outside radius of the plate circle_radius = 3.0 # The outside radius of the plate
thickness = 0.25 # The thickness 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 # 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. # 1. Establishes a workplane that an object can be built on.

View File

@ -1,9 +1,9 @@
import cadquery as cq import cadquery as cq
# These can be modified rather than hardcoding values for each dimension. # 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 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 # Make a plate with 4 holes in it at various points in a polar arrangement from
# the center of the workplane. # the center of the workplane.

View File

@ -1,11 +1,11 @@
import cadquery as cq import cadquery as cq
# These can be modified rather than hardcoding values for each dimension. # These can be modified rather than hardcoding values for each dimension.
width = 3.0 # The width of the plate width = 3.0 # The width of the plate
height = 4.0 # The height of the plate height = 4.0 # The height of the plate
thickness = 0.25 # The thickness 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_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 polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points
# Create a plate with two polygons cut through it # Create a plate with two polygons cut through it
# 1. Establishes a workplane that an object can be built on. # 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 # like cutBlind() assume a positive cut direction, but cutThruAll() assumes
# instead that the cut is made from a max direction and cuts downward from # instead that the cut is made from a max direction and cuts downward from
# that max through all objects. # that max through all objects.
result = cq.Workplane("front").box(width, height, thickness) \ result = (
.pushPoints([(0, 0.75), (0, -0.75)]) \ cq.Workplane("front")
.polygon(polygon_sides, polygon_dia) \ .box(width, height, thickness)
.cutThruAll() .pushPoints([(0, 0.75), (0, -0.75)])
.polygon(polygon_sides, polygon_dia)
.cutThruAll()
)
# Displays the result of this script # Displays the result of this script
show_object(result) show_object(result)

View File

@ -6,13 +6,13 @@ import cadquery as cq
# Define the points that the polyline will be drawn to/thru # Define the points that the polyline will be drawn to/thru
pts = [ pts = [
(W/2.0, H/2.0), (W / 2.0, H / 2.0),
(W/2.0, (H/2.0 - t)), (W / 2.0, (H / 2.0 - t)),
(t/2.0, (H/2.0-t)), (t / 2.0, (H / 2.0 - t)),
(t/2.0, (t - H/2.0)), (t / 2.0, (t - H / 2.0)),
(W/2.0, (t - H/2.0)), (W / 2.0, (t - H / 2.0)),
(W/2.0, H/-2.0), (W / 2.0, H / -2.0),
(0, H/-2.0) (0, H / -2.0),
] ]
# We generate half of the I-beam outline and then mirror it to create the full # 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 # 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. # 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. # 4. The I-beam profile is extruded to the final length of the beam.
result = cq.Workplane("front").moveTo(0, H/2.0) \ result = cq.Workplane("front").moveTo(0, H / 2.0).polyline(pts).mirrorY().extrude(L)
.polyline(pts) \
.mirrorY() \
.extrude(L)
# Displays the result of this script # Displays the result of this script
show_object(result) show_object(result)

View File

@ -13,7 +13,7 @@ sPnts = [
(1.5, 1.0), (1.5, 1.0),
(1.0, 1.25), (1.0, 1.25),
(0.5, 1.0), (0.5, 1.0),
(0, 1.0) (0, 1.0),
] ]
# 2. Generate our plate with the spline feature and make sure it is a # 2. Generate our plate with the spline feature and make sure it is a

View File

@ -13,10 +13,16 @@ import cadquery as cq
# 6. Selects the vertices of the for-construction rectangle. # 6. Selects the vertices of the for-construction rectangle.
# 7. Places holes at the center of each selected vertex. # 7. Places holes at the center of each selected vertex.
# 7a. Since the workplane is rotated, this results in angled holes in the face. # 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") \ result = (
.workplane() \ cq.Workplane("front")
.transformed(offset=(0, -1.5, 1.0), rotate=(60, 0, 0)) \ .box(4.0, 4.0, 0.25)
.rect(1.5, 1.5, forConstruction=True).vertices().hole(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 # Displays the result of this script
show_object(result) show_object(result)

View File

@ -12,10 +12,15 @@ import cadquery as cq
# other geometry. # other geometry.
# 6. Selects the vertices of the for-construction rectangle. # 6. Selects the vertices of the for-construction rectangle.
# 7. Places holes at the center of each selected vertex. # 7. Places holes at the center of each selected vertex.
result = cq.Workplane("front").box(2, 2, 0.5)\ result = (
.faces(">Z").workplane() \ cq.Workplane("front")
.rect(1.5, 1.5, forConstruction=True).vertices() \ .box(2, 2, 0.5)
.hole(0.125) .faces(">Z")
.workplane()
.rect(1.5, 1.5, forConstruction=True)
.vertices()
.hole(0.125)
)
# Displays the result of this script # Displays the result of this script
show_object(result) show_object(result)

View File

@ -11,10 +11,15 @@ import cadquery as cq
# 5. Creates a workplane 3 mm above the face the circle was drawn on. # 5. Creates a workplane 3 mm above the face the circle was drawn on.
# 6. Draws a 2D circle on the new, offset workplane. # 6. Draws a 2D circle on the new, offset workplane.
# 7. Creates a loft between the circle and the rectangle. # 7. Creates a loft between the circle and the rectangle.
result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z") \ result = (
.circle(1.5).workplane(offset=3.0) \ cq.Workplane("front")
.rect(0.75, 0.5) \ .box(4.0, 4.0, 0.25)
.loft(combine=True) .faces(">Z")
.circle(1.5)
.workplane(offset=3.0)
.rect(0.75, 0.5)
.loft(combine=True)
)
# Displays the result of this script # Displays the result of this script
show_object(result) show_object(result)

View File

@ -11,9 +11,15 @@ import cadquery as cq
# function. # function.
# 5a. When the depth of the counter-sink hole is set to None, the hole will be # 5a. When the depth of the counter-sink hole is set to None, the hole will be
# cut through. # cut through.
result = cq.Workplane(cq.Plane.XY()).box(4, 2, 0.5).faces(">Z") \ result = (
.workplane().rect(3.5, 1.5, forConstruction=True) \ cq.Workplane(cq.Plane.XY())
.vertices().cskHole(0.125, 0.25, 82.0, depth=None) .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 # Displays the result of this script
show_object(result) show_object(result)

View File

@ -9,8 +9,7 @@ import cadquery as cq
# that new geometry can be built on. # that new geometry can be built on.
# 4. Draws a 2D circle on the new workplane and then uses it to cut a hole # 4. Draws a 2D circle on the new workplane and then uses it to cut a hole
# all the way through the box. # all the way through the box.
c = cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane() \ c = cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane().circle(0.25).cutThruAll()
.circle(0.25).cutThruAll()
# 5. Selects the face furthest away from the origin in the +Y axis direction. # 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. # 6. Creates an offset workplane that is set in the center of the object.

View File

@ -9,13 +9,13 @@ angle_degrees = 360.0
# Revolve a cylinder from a rectangle # Revolve a cylinder from a rectangle
# Switch comments around in this section to try the revolve operation with different parameters # 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 = 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, 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))
#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))
#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).revolve(angle_degrees,(-5,-5),(-5,5), False)
# Revolve a donut with square walls # 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 # Displays the result of this script
show_object(result) show_object(result)

View File

@ -1,11 +1,7 @@
import cadquery as cq import cadquery as cq
# Points we will use to create spline and polyline paths to sweep over # Points we will use to create spline and polyline paths to sweep over
pts = [ pts = [(0, 1), (1, 2), (2, 4)]
(0, 1),
(1, 2),
(2, 4)
]
# Spline path generated from our list of points (tuples) # Spline path generated from our list of points (tuples)
path = cq.Workplane("XZ").spline(pts) path = cq.Workplane("XZ").spline(pts)
@ -37,4 +33,4 @@ show_object(defaultSweep)
show_object(frenetShell.translate((5, 0, 0))) show_object(frenetShell.translate((5, 0, 0)))
show_object(defaultRect.translate((10, 0, 0))) show_object(defaultRect.translate((10, 0, 0)))
show_object(plineSweep.translate((15, 0, 0))) show_object(plineSweep.translate((15, 0, 0)))
show_object(arcSweep.translate((20, 0, 0))) show_object(arcSweep.translate((20, 0, 0)))

View File

@ -4,37 +4,80 @@ import cadquery as cq
path = cq.Workplane("XZ").moveTo(-10, 0).lineTo(10, 0) 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 # 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). \ defaultSweep = (
workplane(offset=10.0).circle(1.0). \ cq.Workplane("YZ")
workplane(offset=10.0).circle(2.0).sweep(path, multisection=True) .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 # We can sweep thrue different shapes
recttocircleSweep = cq.Workplane("YZ").workplane(offset=-10.0).rect(2.0, 2.0). \ recttocircleSweep = (
workplane(offset=8.0).circle(1.0).workplane(offset=4.0).circle(1.0). \ cq.Workplane("YZ")
workplane(offset=8.0).rect(2.0, 2.0).sweep(path, multisection=True) .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). \ circletorectSweep = (
workplane(offset=7.0).rect(2.0, 2.0).workplane(offset=6.0).rect(2.0, 2.0). \ cq.Workplane("YZ")
workplane(offset=7.0).circle(1.0).sweep(path, multisection=True) .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 # 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). \ specialSweep = (
sweep(path, multisection=True) 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 # 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). \ path = (
threePointArc((4, 0), (0, -4)).lineTo(-5, -4) 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 # Placement of different shapes should follow the path
# cylinder r=1.5 along first line # cylinder r=1.5 along first line
# then sweep allong arc from r=1.5 to r=1.0 # then sweep allong arc from r=1.5 to r=1.0
# then cylinder r=1.0 along last line # then cylinder r=1.0 along last line
arcSweep = cq.Workplane("YZ").workplane(offset=-5).moveTo(0, 4).circle(1.5). \ arcSweep = (
workplane(offset=5).circle(1.5). \ cq.Workplane("YZ")
moveTo(0, -8).circle(1.0). \ .workplane(offset=-5)
workplane(offset=-5).circle(1.0). \ .moveTo(0, 4)
sweep(path, multisection=True) .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 # 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(recttocircleSweep.translate((0, 10, 0)))
show_object(specialSweep.translate((0, 15, 0))) show_object(specialSweep.translate((0, 15, 0)))
show_object(arcSweep.translate((0, -5, 0))) show_object(arcSweep.translate((0, -5, 0)))

View File

@ -4,8 +4,8 @@ import cadquery as cq
##### #####
# Inputs # Inputs
###### ######
lbumps = 1 # number of bumps long lbumps = 1 # number of bumps long
wbumps = 1 # number of bumps wide wbumps = 1 # number of bumps wide
thin = True # True for thin, False for thick thin = True # True for thin, False for thick
# #
@ -22,8 +22,8 @@ else:
t = (pitch - (2 * clearance) - bumpDiam) / 2.0 t = (pitch - (2 * clearance) - bumpDiam) / 2.0
postDiam = pitch - t # works out to 6.5 postDiam = pitch - t # works out to 6.5
total_length = lbumps*pitch - 2.0*clearance total_length = lbumps * pitch - 2.0 * clearance
total_width = wbumps*pitch - 2.0*clearance total_width = wbumps * pitch - 2.0 * clearance
# make the base # make the base
s = cq.Workplane("XY").box(total_length, total_width, height) 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").shell(-1.0 * t) s = s.faces("<Z").shell(-1.0 * t)
# make the bumps on the top # make the bumps on the top
s = s.faces(">Z").workplane(). \ s = (
rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) \ s.faces(">Z")
.workplane()
.rarray(pitch, pitch, lbumps, wbumps, True)
.circle(bumpDiam / 2.0)
.extrude(bumpHeight) .extrude(bumpHeight)
)
# add posts on the bottom. posts are different diameter depending on geometry # add posts on the bottom. posts are different diameter depending on geometry
# solid studs for 1 bump, tubes for multiple, none for 1x1 # solid studs for 1 bump, tubes for multiple, none for 1x1
tmp = s.faces("<Z").workplane(invert=True) tmp = s.faces("<Z").workplane(invert=True)
if lbumps > 1 and wbumps > 1: if lbumps > 1 and wbumps > 1:
tmp = tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). \ tmp = (
circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t) tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True)
.circle(postDiam / 2.0)
.circle(bumpDiam / 2.0)
.extrude(height - t)
)
elif lbumps > 1: elif lbumps > 1:
tmp = tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). \ tmp = (
circle(t).extrude(height - t) tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True)
.circle(t)
.extrude(height - t)
)
elif wbumps > 1: elif wbumps > 1:
tmp = tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). \ tmp = (
circle(t).extrude(height - t) tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True)
.circle(t)
.extrude(height - t)
)
else: else:
tmp = s tmp = s

View File

@ -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)

View File

@ -1,3 +0,0 @@
sphinx-rtd-theme==0.1.9
travis-sphinx==1.1.0
Sphinx==1.3.1

View File

View File

@ -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())

View File

@ -15,44 +15,48 @@ import os
from setuptools import setup from setuptools import setup
#if we are building in travis, use the build number as the sub-minor version # if we are building in travis, use the build number as the sub-minor version
version = '0.5-SNAPSHOT' version = "0.5-SNAPSHOT"
if 'TRAVIS_TAG' in os.environ.keys(): if "TRAVIS_TAG" in os.environ.keys():
version= os.environ['TRAVIS_TAG'] version = os.environ["TRAVIS_TAG"]
setup( setup(
name='cadquery', name="cadquery",
version=version, version=version,
url='https://github.com/dcowden/cadquery', url="https://github.com/dcowden/cadquery",
license='Apache Public License 2.0', license="Apache Public License 2.0",
author='David Cowden', author="David Cowden",
author_email='dave.cowden@gmail.com', author_email="dave.cowden@gmail.com",
description='CadQuery is a parametric scripting language for creating and traversing CAD models', description="CadQuery is a parametric scripting language for creating and traversing CAD models",
long_description=open('README.md').read(), long_description=open("README.md").read(),
packages=['cadquery','cadquery.contrib','cadquery.occ_impl','cadquery.plugins','tests'], packages=[
install_requires=['pyparsing'], "cadquery",
"cadquery.contrib",
"cadquery.occ_impl",
"cadquery.plugins",
"tests",
],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
platforms='any', platforms="any",
test_suite='tests', test_suite="tests",
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', "Development Status :: 5 - Production/Stable",
#'Development Status :: 6 - Mature', #'Development Status :: 6 - Mature',
#'Development Status :: 7 - Inactive', #'Development Status :: 7 - Inactive',
'Intended Audience :: Developers', "Intended Audience :: Developers",
'Intended Audience :: End Users/Desktop', "Intended Audience :: End Users/Desktop",
'Intended Audience :: Information Technology', "Intended Audience :: Information Technology",
'Intended Audience :: Science/Research', "Intended Audience :: Science/Research",
'Intended Audience :: System Administrators', "Intended Audience :: System Administrators",
'License :: OSI Approved :: Apache Software License', "License :: OSI Approved :: Apache Software License",
'Operating System :: POSIX', "Operating System :: POSIX",
'Operating System :: MacOS', "Operating System :: MacOS",
'Operating System :: Unix', "Operating System :: Unix",
'Programming Language :: Python', "Programming Language :: Python",
'Topic :: Software Development :: Libraries :: Python Modules', "Topic :: Software Development :: Libraries :: Python Modules",
'Topic :: Internet', "Topic :: Internet",
'Topic :: Scientific/Engineering' "Topic :: Scientific/Engineering",
] ],
) )

View File

@ -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()

File diff suppressed because it is too large Load Diff

View File

@ -6,21 +6,23 @@ import os
def readFileAsString(fileName): def readFileAsString(fileName):
f = open(fileName, 'r') f = open(fileName, "r")
s = f.read() s = f.read()
f.close() f.close()
return s return s
def writeStringToFile(strToWrite, fileName): def writeStringToFile(strToWrite, fileName):
f = open(fileName, 'w') f = open(fileName, "w")
f.write(strToWrite) f.write(strToWrite)
f.close() f.close()
def makeUnitSquareWire(): def makeUnitSquareWire():
V = Vector 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(): def makeUnitCube():
@ -38,25 +40,23 @@ def toTuple(v):
elif type(v) == Vector: elif type(v) == Vector:
return v.toTuple() return v.toTuple()
else: else:
raise RuntimeError( raise RuntimeError("dont know how to convert type %s to tuple" % str(type(v)))
"dont know how to convert type %s to tuple" % str(type(v)))
class BaseTest(unittest.TestCase): class BaseTest(unittest.TestCase):
def assertTupleAlmostEquals(self, expected, actual, places): def assertTupleAlmostEquals(self, expected, actual, places):
for i, j in zip(actual, expected): for i, j in zip(actual, expected):
self.assertAlmostEqual(i, j, places) self.assertAlmostEqual(i, j, places)
__all__ = [ __all__ = [
'TestCadObjects', "TestCadObjects",
'TestCadQuery', "TestCadQuery",
'TestCQGI', "TestCQGI",
'TestCQSelectors', "TestCQSelectors",
'TestCQSelectors', "TestCQSelectors",
'TestExporters', "TestExporters",
'TestImporters', "TestImporters",
'TestJupyter', "TestJupyter",
'TestWorkplanes', "TestWorkplanes",
] ]

406
tests/test_cad_objects.py Normal file
View File

@ -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()

3350
tests/test_cadquery.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -42,8 +42,9 @@ class TestCQGI(BaseTest):
model = cqgi.CQModel(TESTSCRIPT) model = cqgi.CQModel(TESTSCRIPT)
metadata = model.metadata metadata = model.metadata
self.assertEqual(set(metadata.parameters.keys()), { self.assertEqual(
'height', 'width', 'a', 'b', 'foo'}) set(metadata.parameters.keys()), {"height", "width", "a", "b", "foo"}
)
def test_build_with_debug(self): def test_build_with_debug(self):
model = cqgi.CQModel(TEST_DEBUG_SCRIPT) model = cqgi.CQModel(TEST_DEBUG_SCRIPT)
@ -51,7 +52,7 @@ class TestCQGI(BaseTest):
debugItems = result.debugObjects debugItems = result.debugObjects
self.assertTrue(len(debugItems) == 2) self.assertTrue(len(debugItems) == 2)
self.assertTrue(debugItems[0].shape == "bar") 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].shape == 2.0)
self.assertTrue(debugItems[1].options == {}) self.assertTrue(debugItems[1].options == {})
@ -65,7 +66,7 @@ class TestCQGI(BaseTest):
def test_build_with_different_params(self): def test_build_with_different_params(self):
model = cqgi.CQModel(TESTSCRIPT) 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") self.assertTrue(result.results[0].shape == "3.0|3.0|bar|1.0")
def test_describe_parameters(self): def test_describe_parameters(self):
@ -76,9 +77,9 @@ class TestCQGI(BaseTest):
""" """
) )
model = cqgi.CQModel(script) 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.default_value == 2.0)
self.assertTrue(a_param.desc == 'FirstLetter') self.assertTrue(a_param.desc == "FirstLetter")
self.assertTrue(a_param.varType == cqgi.NumberParameterType) self.assertTrue(a_param.varType == cqgi.NumberParameterType)
def test_describe_parameter_invalid_doesnt_fail_script(self): def test_describe_parameter_invalid_doesnt_fail_script(self):
@ -89,8 +90,8 @@ class TestCQGI(BaseTest):
""" """
) )
model = cqgi.CQModel(script) model = cqgi.CQModel(script)
a_param = model.metadata.parameters['a'] a_param = model.metadata.parameters["a"]
self.assertTrue(a_param.name == 'a') self.assertTrue(a_param.name == "a")
def test_build_with_exception(self): def test_build_with_exception(self):
badscript = textwrap.dedent( badscript = textwrap.dedent(
@ -115,7 +116,7 @@ class TestCQGI(BaseTest):
with self.assertRaises(Exception) as context: with self.assertRaises(Exception) as context:
model = cqgi.CQModel(badscript) 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): def test_that_two_results_are_returned(self):
script = textwrap.dedent( script = textwrap.dedent(
@ -140,7 +141,7 @@ class TestCQGI(BaseTest):
show_object(h) 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") self.assertEqual(result.results[0].shape, "33.33")
def test_that_assigning_string_to_number_fails(self): def test_that_assigning_string_to_number_fails(self):
@ -150,9 +151,8 @@ class TestCQGI(BaseTest):
show_object(h) show_object(h)
""" """
) )
result = cqgi.parse(script).build({'h': "a string"}) result = cqgi.parse(script).build({"h": "a string"})
self.assertTrue(isinstance(result.exception, self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
cqgi.InvalidParameterError))
def test_that_assigning_unknown_var_fails(self): def test_that_assigning_unknown_var_fails(self):
script = textwrap.dedent( script = textwrap.dedent(
@ -162,9 +162,8 @@ class TestCQGI(BaseTest):
""" """
) )
result = cqgi.parse(script).build({'w': "var is not there"}) result = cqgi.parse(script).build({"w": "var is not there"})
self.assertTrue(isinstance(result.exception, self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
cqgi.InvalidParameterError))
def test_that_cq_objects_are_visible(self): def test_that_cq_objects_are_visible(self):
script = textwrap.dedent( 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.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): def test_that_only_top_level_vars_are_detected(self):
script = textwrap.dedent( script = textwrap.dedent(

View File

@ -12,7 +12,6 @@ from tests import BaseTest
class TestExporters(BaseTest): class TestExporters(BaseTest):
def _exportBox(self, eType, stringsToFind): def _exportBox(self, eType, stringsToFind):
""" """
Exports a test object, and then looks for Exports a test object, and then looks for
@ -21,31 +20,32 @@ class TestExporters(BaseTest):
""" """
p = Workplane("XY").box(1, 2, 3) p = Workplane("XY").box(1, 2, 3)
if eType == exporters.ExportTypes.AMF: if eType == exporters.ExportTypes.AMF:
s = io.BytesIO() s = io.BytesIO()
else: else:
s = io.StringIO() s = io.StringIO()
exporters.exportShape(p, eType, s, 0.1) exporters.exportShape(p, eType, s, 0.1)
result = '{}'.format(s.getvalue()) result = "{}".format(s.getvalue())
for q in stringsToFind: for q in stringsToFind:
self.assertTrue(result.find(q) > -1) self.assertTrue(result.find(q) > -1)
return result return result
def testSTL(self): def testSTL(self):
self._exportBox(exporters.ExportTypes.STL, ['facet normal']) self._exportBox(exporters.ExportTypes.STL, ["facet normal"])
def testSVG(self): def testSVG(self):
self._exportBox(exporters.ExportTypes.SVG, ['<svg', '<g transform']) self._exportBox(exporters.ExportTypes.SVG, ["<svg", "<g transform"])
def testAMF(self): def testAMF(self):
self._exportBox(exporters.ExportTypes.AMF, ['<amf units', '</object>']) self._exportBox(exporters.ExportTypes.AMF, ["<amf units", "</object>"])
def testSTEP(self): def testSTEP(self):
self._exportBox(exporters.ExportTypes.STEP, ['FILE_SCHEMA']) self._exportBox(exporters.ExportTypes.STEP, ["FILE_SCHEMA"])
def testTJS(self): def testTJS(self):
self._exportBox(exporters.ExportTypes.TJS, [ self._exportBox(
'vertices', 'formatVersion', 'faces']) exporters.ExportTypes.TJS, ["vertices", "formatVersion", "faces"]
)

View File

@ -36,12 +36,18 @@ class TestImporters(BaseTest):
self.assertTrue(importedShape.val().ShapeType() == "Solid") self.assertTrue(importedShape.val().ShapeType() == "Solid")
# Check the number of faces and vertices per face to make sure we have a box shape # Check the number of faces and vertices per face to make sure we have a box shape
self.assertTrue(importedShape.faces("+X").size() == self.assertTrue(
1 and importedShape.faces("+X").vertices().size() == 4) importedShape.faces("+X").size() == 1
self.assertTrue(importedShape.faces("+Y").size() == and importedShape.faces("+X").vertices().size() == 4
1 and importedShape.faces("+Y").vertices().size() == 4) )
self.assertTrue(importedShape.faces("+Z").size() == self.assertTrue(
1 and importedShape.faces("+Z").vertices().size() == 4) 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): def testSTEP(self):
""" """
@ -55,7 +61,8 @@ class TestImporters(BaseTest):
not segfault. not segfault.
""" """
tmpfile = OUTDIR + "/badSTEP.step" 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): with self.assertRaises(ValueError):
importers.importShape(importers.ImportTypes.STEP, tmpfile) importers.importShape(importers.ImportTypes.STEP, tmpfile)
@ -69,6 +76,8 @@ class TestImporters(BaseTest):
objs = importers.importShape(importers.ImportTypes.STEP, filename) objs = importers.importShape(importers.ImportTypes.STEP, filename)
self.assertEqual(2, len(objs.all())) self.assertEqual(2, len(objs.all()))
if __name__ == '__main__':
if __name__ == "__main__":
import unittest import unittest
unittest.main() unittest.main()

View File

@ -2,9 +2,10 @@ from tests import BaseTest
import cadquery import cadquery
class TestJupyter(BaseTest): class TestJupyter(BaseTest):
def test_repr_html(self): def test_repr_html(self):
cube = cadquery.Workplane('XY').box(1, 1, 1) cube = cadquery.Workplane("XY").box(1, 1, 1)
shape = cube.val() shape = cube.val()
self.assertIsInstance(shape, cadquery.occ_impl.shapes.Solid) self.assertIsInstance(shape, cadquery.occ_impl.shapes.Solid)

View File

@ -1,4 +1,4 @@
__author__ = 'dcowden' __author__ = "dcowden"
""" """
Tests for CadQuery Selectors Tests for CadQuery Selectors
@ -20,22 +20,19 @@ from cadquery import selectors
class TestCQSelectors(BaseTest): class TestCQSelectors(BaseTest):
def testWorkplaneCenter(self): def testWorkplaneCenter(self):
"Test Moving workplane center" "Test Moving workplane center"
s = Workplane(Plane.XY()) s = Workplane(Plane.XY())
# current point and world point should be equal # current point and world point should be equal
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals((0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3)
(0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3)
# move origin and confirm center moves # 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 # current point should be 0,0, but
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals((-2.0, -2.0, 0.0), s.plane.origin.toTuple(), 3)
(-2.0, -2.0, 0.0), s.plane.origin.toTuple(), 3)
def testVertices(self): def testVertices(self):
t = makeUnitSquareWire() # square box t = makeUnitSquareWire() # square box
@ -43,8 +40,7 @@ class TestCQSelectors(BaseTest):
self.assertEqual(4, c.vertices().size()) self.assertEqual(4, c.vertices().size())
self.assertEqual(4, c.edges().size()) self.assertEqual(4, c.edges().size())
self.assertEqual(0, c.vertices().edges().size() self.assertEqual(0, c.vertices().edges().size()) # no edges on any vertices
) # no edges on any vertices
# but selecting all edges still yields all vertices # but selecting all edges still yields all vertices
self.assertEqual(4, c.edges().vertices().size()) self.assertEqual(4, c.edges().vertices().size())
self.assertEqual(1, c.wires().size()) # just one wire self.assertEqual(1, c.wires().size()) # just one wire
@ -71,8 +67,7 @@ class TestCQSelectors(BaseTest):
def testFirst(self): def testFirst(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
self.assertEqual(type(c.vertices().first().val()), Vertex) self.assertEqual(type(c.vertices().first().val()), Vertex)
self.assertEqual( self.assertEqual(type(c.vertices().first().first().first().val()), Vertex)
type(c.vertices().first().first().first().val()), Vertex)
def testCompounds(self): def testCompounds(self):
c = CQ(makeUnitSquareWire()) c = CQ(makeUnitSquareWire())
@ -99,11 +94,11 @@ class TestCQSelectors(BaseTest):
def testFaceTypesFilter(self): def testFaceTypesFilter(self):
"Filters by face type" "Filters by face type"
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
self.assertEqual(c.faces().size(), c.faces('%PLANE').size()) self.assertEqual(c.faces().size(), c.faces("%PLANE").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("%sphere").size())
self.assertEqual(0, c.faces('%cone').size()) self.assertEqual(0, c.faces("%cone").size())
self.assertEqual(0, c.faces('%SPHERE').size()) self.assertEqual(0, c.faces("%SPHERE").size())
def testPerpendicularDirFilter(self): def testPerpendicularDirFilter(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
@ -131,10 +126,12 @@ class TestCQSelectors(BaseTest):
# faces parallel to Z axis # faces parallel to Z axis
self.assertEqual(2, c.faces("|Z").size()) self.assertEqual(2, c.faces("|Z").size())
# TODO: provide short names for ParallelDirSelector # TODO: provide short names for ParallelDirSelector
self.assertEqual(2, c.faces(selectors.ParallelDirSelector( self.assertEqual(
Vector((0, 0, 1)))).size()) # same thing as above 2, c.faces(selectors.ParallelDirSelector(Vector((0, 0, 1)))).size()
self.assertEqual(2, c.faces(selectors.ParallelDirSelector( ) # same thing as above
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 # just for fun, vertices on faces parallel to z
self.assertEqual(8, c.faces("|Z").vertices().size()) self.assertEqual(8, c.faces("|Z").vertices().size())
@ -178,97 +175,96 @@ class TestCQSelectors(BaseTest):
self.assertEqual(4, len(el)) self.assertEqual(4, len(el))
def testNthDistance(self): 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 # 2nd 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, -1.5) self.assertAlmostEqual(val.Center().x, -1.5)
# 2nd face with inversed selection vector # 2nd face with inversed selection vector
val = c.faces(selectors.DirectionNthSelector( val = c.faces(selectors.DirectionNthSelector(Vector(-1, 0, 0), 1)).val()
Vector(-1, 0, 0), 1)).val()
self.assertAlmostEqual(val.Center().x, 1.5) self.assertAlmostEqual(val.Center().x, 1.5)
# 2nd last face # 2nd last face
val = c.faces(selectors.DirectionNthSelector( val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), -2)).val()
Vector(1, 0, 0), -2)).val()
self.assertAlmostEqual(val.Center().x, 1.5) self.assertAlmostEqual(val.Center().x, 1.5)
# Last face # Last face
val = c.faces(selectors.DirectionNthSelector( val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), -1)).val()
Vector(1, 0, 0), -1)).val()
self.assertAlmostEqual(val.Center().x, 2.5) self.assertAlmostEqual(val.Center().x, 2.5)
# check if the selected face if normal to the specified Vector # check if the selected face if normal to the specified Vector
self.assertAlmostEqual( self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
# repeat the test using string based selector # repeat the test using string based selector
# 2nd face # 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) 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) self.assertAlmostEqual(val.Center().x, -1.5)
# 2nd face with inversed selection vector # 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) 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) self.assertAlmostEqual(val.Center().x, 1.5)
# 2nd last face # 2nd last face
val = c.faces('>X[-2]').val() val = c.faces(">X[-2]").val()
self.assertAlmostEqual(val.Center().x, 1.5) self.assertAlmostEqual(val.Center().x, 1.5)
# Last face # Last face
val = c.faces('>X[-1]').val() val = c.faces(">X[-1]").val()
self.assertAlmostEqual(val.Center().x, 2.5) self.assertAlmostEqual(val.Center().x, 2.5)
# check if the selected face if normal to the specified Vector # check if the selected face if normal to the specified Vector
self.assertAlmostEqual( self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
# test selection of multiple faces with the same distance # test selection of multiple faces with the same distance
c = Workplane('XY')\ c = (
.box(1, 4, 1, centered=(False, True, False)).faces('<Z')\ Workplane("XY")
.box(2, 2, 2, centered=(True, True, False)).faces('>Z')\ .box(1, 4, 1, centered=(False, True, False))
.faces("<Z")
.box(2, 2, 2, centered=(True, True, False))
.faces(">Z")
.box(1, 1, 1, centered=(True, True, False)) .box(1, 1, 1, centered=(True, True, False))
)
# select 2nd from the bottom (NB python indexing is 0-based) # 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) self.assertEqual(len(vals), 2)
val = c.faces('>Z[1]').val() val = c.faces(">Z[1]").val()
self.assertAlmostEqual(val.Center().z, 1) self.assertAlmostEqual(val.Center().z, 1)
# do the same but by selecting 3rd from the top # do the same but by selecting 3rd from the top
vals = c.faces('<Z[2]').vals() vals = c.faces("<Z[2]").vals()
self.assertEqual(len(vals), 2) self.assertEqual(len(vals), 2)
val = c.faces('<Z[2]').val() val = c.faces("<Z[2]").val()
self.assertAlmostEqual(val.Center().z, 1) self.assertAlmostEqual(val.Center().z, 1)
# do the same but by selecting 2nd last from the bottom # do the same but by selecting 2nd last from the bottom
vals = c.faces('<Z[-2]').vals() vals = c.faces("<Z[-2]").vals()
self.assertEqual(len(vals), 2) self.assertEqual(len(vals), 2)
val = c.faces('<Z[-2]').val() val = c.faces("<Z[-2]").val()
self.assertAlmostEqual(val.Center().z, 1) self.assertAlmostEqual(val.Center().z, 1)
# verify that <Z[-1] is equivalent to <Z # verify that <Z[-1] is equivalent to <Z
val1 = c.faces('<Z[-1]').val() val1 = c.faces("<Z[-1]").val()
val2 = c.faces('<Z').val() val2 = c.faces("<Z").val()
self.assertTupleAlmostEquals(val1.Center().toTuple(), self.assertTupleAlmostEquals(
val2.Center().toTuple(), val1.Center().toTuple(), val2.Center().toTuple(), 3
3) )
# verify that >Z[-1] is equivalent to >Z # verify that >Z[-1] is equivalent to >Z
val1 = c.faces('>Z[-1]').val() val1 = c.faces(">Z[-1]").val()
val2 = c.faces('>Z').val() val2 = c.faces(">Z").val()
self.assertTupleAlmostEquals(val1.Center().toTuple(), self.assertTupleAlmostEquals(
val2.Center().toTuple(), val1.Center().toTuple(), val2.Center().toTuple(), 3
3) )
def testNearestTo(self): def testNearestTo(self):
c = CQ(makeUnitCube()) 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.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.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.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: for d in test_data_vertices:
@ -318,11 +314,13 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3) self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3)
# test multiple vertices selection # test multiple vertices selection
vl = c.vertices(selectors.BoxSelector( vl = c.vertices(
(-0.1, -0.1, 0.9), (0.1, 1.1, 1.1))).vals() selectors.BoxSelector((-0.1, -0.1, 0.9), (0.1, 1.1, 1.1))
).vals()
self.assertEqual(2, len(vl)) self.assertEqual(2, len(vl))
vl = c.vertices(selectors.BoxSelector( vl = c.vertices(
(-0.1, -0.1, -0.1), (0.1, 1.1, 1.1))).vals() selectors.BoxSelector((-0.1, -0.1, -0.1), (0.1, 1.1, 1.1))
).vals()
self.assertEqual(4, len(vl)) self.assertEqual(4, len(vl))
# test edge selection # 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.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.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.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: for d in test_data_edges:
@ -347,11 +345,9 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3) self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3)
# test multiple edge selection # test multiple edge selection
el = c.edges(selectors.BoxSelector( el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals()
(-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals()
self.assertEqual(2, len(el)) self.assertEqual(2, len(el))
el = c.edges(selectors.BoxSelector( el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals()
(-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals()
self.assertEqual(3, len(el)) self.assertEqual(3, len(el))
# test face selection # 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.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.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.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: for d in test_data_faces:
@ -376,22 +372,23 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3) self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3)
# test multiple face selection # test multiple face selection
fl = c.faces(selectors.BoxSelector( fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals()
(0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals()
self.assertEqual(2, len(fl)) self.assertEqual(2, len(fl))
fl = c.faces(selectors.BoxSelector( fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals()
(0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals()
self.assertEqual(3, len(fl)) self.assertEqual(3, len(fl))
# test boundingbox option # test boundingbox option
el = c.edges(selectors.BoxSelector( el = c.edges(
(-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True)).vals() selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True)
).vals()
self.assertEqual(1, len(el)) self.assertEqual(1, len(el))
fl = c.faces(selectors.BoxSelector( fl = c.faces(
(0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True)).vals() selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True)
).vals()
self.assertEqual(0, len(fl)) self.assertEqual(0, len(fl))
fl = c.faces(selectors.BoxSelector( fl = c.faces(
(-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True)).vals() selectors.BoxSelector((-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True)
).vals()
self.assertEqual(1, len(fl)) self.assertEqual(1, len(fl))
def testAndSelector(self): def testAndSelector(self):
@ -400,12 +397,13 @@ class TestCQSelectors(BaseTest):
S = selectors.StringSyntaxSelector S = selectors.StringSyntaxSelector
BS = selectors.BoxSelector BS = selectors.BoxSelector
el = c.edges(selectors.AndSelector( el = c.edges(
S('|X'), BS((-2, -2, 0.1), (2, 2, 2)))).vals() selectors.AndSelector(S("|X"), BS((-2, -2, 0.1), (2, 2, 2)))
).vals()
self.assertEqual(2, len(el)) self.assertEqual(2, len(el))
# test 'and' (intersection) operator # 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)) self.assertEqual(2, len(el))
# test using extended string syntax # test using extended string syntax
@ -455,27 +453,27 @@ class TestCQSelectors(BaseTest):
S = selectors.StringSyntaxSelector S = selectors.StringSyntaxSelector
fl = c.faces(selectors.InverseSelector(S('>Z'))).vals() fl = c.faces(selectors.InverseSelector(S(">Z"))).vals()
self.assertEqual(5, len(fl)) 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)) self.assertEqual(3, len(el))
# test invert operator # test invert operator
fl = c.faces(-S('>Z')).vals() fl = c.faces(-S(">Z")).vals()
self.assertEqual(5, len(fl)) 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)) self.assertEqual(3, len(el))
# test using extended string syntax # test using extended string syntax
fl = c.faces('not >Z').vals() fl = c.faces("not >Z").vals()
self.assertEqual(5, len(fl)) 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)) self.assertEqual(3, len(el))
def testComplexStringSelector(self): def testComplexStringSelector(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
v = c.vertices('(>X and >Y) or (<X and <Y)').vals() v = c.vertices("(>X and >Y) or (<X and <Y)").vals()
self.assertEqual(4, len(v)) self.assertEqual(4, len(v))
def testFaceCount(self): def testFaceCount(self):
@ -503,25 +501,27 @@ class TestCQSelectors(BaseTest):
gram = selectors._expression_grammar gram = selectors._expression_grammar
expressions = ['+X ', expressions = [
'-Y', "+X ",
'|(1,0,0)', "-Y",
'#(1.,1.4114,-0.532)', "|(1,0,0)",
'%Plane', "#(1.,1.4114,-0.532)",
'>XZ', "%Plane",
'<Z[-2]', ">XZ",
'>(1,4,55.)[20]', "<Z[-2]",
'|XY', ">(1,4,55.)[20]",
'<YZ[0]', "|XY",
'front', "<YZ[0]",
'back', "front",
'left', "back",
'right', "left",
'top', "right",
'bottom', "top",
'not |(1,1,0) and >(0,0,1) or XY except >(1,1,1)[-1]', "bottom",
'(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)', "not |(1,1,0) and >(0,0,1) or XY except >(1,1,1)[-1]",
'not ( <X or >X or <Y or >Y )'] "(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)",
"not ( <X or >X or <Y or >Y )",
]
for e in expressions: for e in expressions:
gram.parseString(e, parseAll=True) gram.parseString(e, parseAll=True)

View File

@ -16,65 +16,70 @@ zInvAxis_ = Vector(0, 0, -1)
class TestWorkplanes(BaseTest): class TestWorkplanes(BaseTest):
def testYZPlaneOrigins(self): def testYZPlaneOrigins(self):
# xy plane-- with origin at x=0.25 # xy plane-- with origin at x=0.25
base = Vector(0.25, 0, 0) base = Vector(0.25, 0, 0)
p = Plane(base, Vector(0, 1, 0), Vector(1, 0, 0)) p = Plane(base, Vector(0, 1, 0), Vector(1, 0, 0))
# origin is always (0,0,0) in local coordinates # origin is always (0,0,0) in local coordinates
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
(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( self.assertTupleAlmostEquals(
base.toTuple(), p.toWorldCoords((0, 0)).toTuple(), 2) base.toTuple(), p.toWorldCoords((0, 0)).toTuple(), 2
)
def testXYPlaneOrigins(self): def testXYPlaneOrigins(self):
base = Vector(0, 0, 0.25) base = Vector(0, 0, 0.25)
p = Plane(base, Vector(1, 0, 0), Vector(0, 0, 1)) p = Plane(base, Vector(1, 0, 0), Vector(0, 0, 1))
# origin is always (0,0,0) in local coordinates # origin is always (0,0,0) in local coordinates
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
(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( self.assertTupleAlmostEquals(
toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2) toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2
)
def testXZPlaneOrigins(self): def testXZPlaneOrigins(self):
base = Vector(0, 0.25, 0) base = Vector(0, 0.25, 0)
p = Plane(base, Vector(0, 0, 1), Vector(0, 1, 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( 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 # origin is always (0,0,0) in local coordinates
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
(0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
def testPlaneBasics(self): def testPlaneBasics(self):
p = Plane.XY() p = Plane.XY()
# local to world # local to world
self.assertTupleAlmostEquals( 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( 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 # world to local
self.assertTupleAlmostEquals( 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( 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() p = Plane.YZ()
self.assertTupleAlmostEquals( 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 # world to local
self.assertTupleAlmostEquals( 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() p = Plane.XZ()
r = p.toWorldCoords((1, 1)).toTuple() r = p.toWorldCoords((1, 1)).toTuple()
@ -82,62 +87,68 @@ class TestWorkplanes(BaseTest):
# world to local # world to local
self.assertTupleAlmostEquals( 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): def testOffsetPlanes(self):
"Tests that a plane offset from the origin works ok too" "Tests that a plane offset from the origin works ok too"
p = Plane.XY(origin=(10.0, 10.0, 0)) p = Plane.XY(origin=(10.0, 10.0, 0))
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
(11.0, 11.0, 0.0), p.toWorldCoords((1.0, 1.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) 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 # TODO test these offsets in the other dimensions too
p = Plane.YZ(origin=(0, 2, 2)) p = Plane.YZ(origin=(0, 2, 2))
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
(0.0, 5.0, 5.0), p.toWorldCoords((3.0, 3.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) 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)) p = Plane.XZ(origin=(2, 0, 2))
r = p.toWorldCoords((1.0, 1.0)).toTuple() r = p.toWorldCoords((1.0, 1.0)).toTuple()
self.assertTupleAlmostEquals((3.0, 0.0, 3.0), r, 2) self.assertTupleAlmostEquals((3.0, 0.0, 3.0), r, 2)
self.assertTupleAlmostEquals((10.0, 10.0), p.toLocalCoords( self.assertTupleAlmostEquals(
Vector(12.0, 0.0, 12.0)).toTuple(), 2) (10.0, 10.0), p.toLocalCoords(Vector(12.0, 0.0, 12.0)).toTuple(), 2
)
def testXYPlaneBasics(self): def testXYPlaneBasics(self):
p = Plane.named('XY') p = Plane.named("XY")
self.assertTupleAlmostEquals(p.zDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.zDir.toTuple(), zAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4)
def testYZPlaneBasics(self): def testYZPlaneBasics(self):
p = Plane.named('YZ') p = Plane.named("YZ")
self.assertTupleAlmostEquals(p.zDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.zDir.toTuple(), xAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4)
def testZXPlaneBasics(self): def testZXPlaneBasics(self):
p = Plane.named('ZX') p = Plane.named("ZX")
self.assertTupleAlmostEquals(p.zDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.zDir.toTuple(), yAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4)
def testXZPlaneBasics(self): def testXZPlaneBasics(self):
p = Plane.named('XZ') p = Plane.named("XZ")
self.assertTupleAlmostEquals(p.zDir.toTuple(), yInvAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.zDir.toTuple(), yInvAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4)
def testYXPlaneBasics(self): def testYXPlaneBasics(self):
p = Plane.named('YX') p = Plane.named("YX")
self.assertTupleAlmostEquals(p.zDir.toTuple(), zInvAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.zDir.toTuple(), zInvAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4)
def testZYPlaneBasics(self): def testZYPlaneBasics(self):
p = Plane.named('ZY') p = Plane.named("ZY")
self.assertTupleAlmostEquals(p.zDir.toTuple(), xInvAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.zDir.toTuple(), xInvAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4)