removed extraneous build directory
This commit is contained in:
parent
7384d9b046
commit
e7d8a92e0a
|
@ -1,21 +0,0 @@
|
||||||
#these items point to the freecad implementation
|
|
||||||
from .freecad_impl.geom import Plane,BoundBox,Vector,Matrix,sortWiresByBuildOrder
|
|
||||||
from .freecad_impl.shapes import Shape,Vertex,Edge,Face,Wire,Solid,Shell,Compound
|
|
||||||
from .freecad_impl import exporters
|
|
||||||
from .freecad_impl import importers
|
|
||||||
|
|
||||||
#these items are the common implementation
|
|
||||||
|
|
||||||
#the order of these matter
|
|
||||||
from .selectors import *
|
|
||||||
from .cq import *
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'CQ','Workplane','plugins','selectors','Plane','BoundBox','Matrix','Vector','sortWiresByBuildOrder',
|
|
||||||
'Shape','Vertex','Edge','Wire','Solid','Shell','Compound','exporters', 'importers',
|
|
||||||
'NearestToPointSelector','ParallelDirSelector','DirectionSelector','PerpendicularDirSelector',
|
|
||||||
'TypeSelector','DirectionMinMaxSelector','StringSyntaxSelector','Selector','plugins'
|
|
||||||
]
|
|
||||||
|
|
||||||
__version__ = "0.3.0"
|
|
|
@ -1,18 +0,0 @@
|
||||||
"""
|
|
||||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
|
||||||
|
|
||||||
This file is part of CadQuery.
|
|
||||||
|
|
||||||
CadQuery is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
CadQuery is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
|
||||||
"""
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,85 +0,0 @@
|
||||||
"""
|
|
||||||
A special directive for including a cq object.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import traceback
|
|
||||||
from cadquery import *
|
|
||||||
from cadquery import cqgi
|
|
||||||
import StringIO
|
|
||||||
from docutils.parsers.rst import directives
|
|
||||||
|
|
||||||
template = """
|
|
||||||
|
|
||||||
.. raw:: html
|
|
||||||
|
|
||||||
<div class="cq" style="text-align:%(txt_align)s;float:left;">
|
|
||||||
%(out_svg)s
|
|
||||||
</div>
|
|
||||||
<div style="clear:both;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
"""
|
|
||||||
template_content_indent = ' '
|
|
||||||
|
|
||||||
|
|
||||||
def cq_directive(name, arguments, options, content, lineno,
|
|
||||||
content_offset, block_text, state, state_machine):
|
|
||||||
# only consider inline snippets
|
|
||||||
plot_code = '\n'.join(content)
|
|
||||||
|
|
||||||
# Since we don't have a filename, use a hash based on the content
|
|
||||||
# the script must define a variable called 'out', which is expected to
|
|
||||||
# be a CQ object
|
|
||||||
out_svg = "Your Script Did not assign call build_output() function!"
|
|
||||||
|
|
||||||
try:
|
|
||||||
_s = StringIO.StringIO()
|
|
||||||
result = cqgi.parse(plot_code).build()
|
|
||||||
|
|
||||||
if result.success:
|
|
||||||
exporters.exportShape(result.first_result, "SVG", _s)
|
|
||||||
out_svg = _s.getvalue()
|
|
||||||
else:
|
|
||||||
raise result.exception
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
out_svg = traceback.format_exc()
|
|
||||||
|
|
||||||
# now out
|
|
||||||
# Now start generating the lines of output
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
# get rid of new lines
|
|
||||||
out_svg = out_svg.replace('\n', '')
|
|
||||||
|
|
||||||
txt_align = "left"
|
|
||||||
if "align" in options:
|
|
||||||
txt_align = options['align']
|
|
||||||
|
|
||||||
lines.extend((template % locals()).split('\n'))
|
|
||||||
|
|
||||||
lines.extend(['::', ''])
|
|
||||||
lines.extend([' %s' % row.rstrip()
|
|
||||||
for row in plot_code.split('\n')])
|
|
||||||
lines.append('')
|
|
||||||
|
|
||||||
if len(lines):
|
|
||||||
state_machine.insert_input(
|
|
||||||
lines, state_machine.input_lines.source(0))
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def setup(app):
|
|
||||||
setup.app = app
|
|
||||||
setup.config = app.config
|
|
||||||
setup.confdir = app.confdir
|
|
||||||
|
|
||||||
options = {'height': directives.length_or_unitless,
|
|
||||||
'width': directives.length_or_percentage_or_unitless,
|
|
||||||
'align': directives.unchanged
|
|
||||||
}
|
|
||||||
|
|
||||||
app.add_directive('cq_plot', cq_directive, True, (0, 2, 0), **options)
|
|
|
@ -1,425 +0,0 @@
|
||||||
"""
|
|
||||||
The CadQuery Gateway Interface.
|
|
||||||
Provides classes and tools for executing CadQuery scripts
|
|
||||||
"""
|
|
||||||
import ast
|
|
||||||
import traceback
|
|
||||||
import time
|
|
||||||
import cadquery
|
|
||||||
|
|
||||||
CQSCRIPT = "<cqscript>"
|
|
||||||
|
|
||||||
def parse(script_source):
|
|
||||||
"""
|
|
||||||
Parses the script as a model, and returns a model.
|
|
||||||
|
|
||||||
If you would prefer to access the underlying model without building it,
|
|
||||||
for example, to inspect its available parameters, construct a CQModel object.
|
|
||||||
|
|
||||||
:param script_source: the script to run. Must be a valid cadquery script
|
|
||||||
:return: a CQModel object that defines the script and allows execution
|
|
||||||
|
|
||||||
"""
|
|
||||||
model = CQModel(script_source)
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
class CQModel(object):
|
|
||||||
"""
|
|
||||||
Represents a Cadquery Script.
|
|
||||||
|
|
||||||
After construction, the metadata property contains
|
|
||||||
a ScriptMetaData object, which describes the model in more detail,
|
|
||||||
and can be used to retrive the parameters defined by the model.
|
|
||||||
|
|
||||||
the build method can be used to generate a 3d model
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, script_source):
|
|
||||||
"""
|
|
||||||
Create an object by parsing the supplied python script.
|
|
||||||
:param script_source: a python script to parse
|
|
||||||
"""
|
|
||||||
self.metadata = ScriptMetadata()
|
|
||||||
self.ast_tree = ast.parse(script_source, CQSCRIPT)
|
|
||||||
self.script_source = script_source
|
|
||||||
self._find_vars()
|
|
||||||
# TODO: pick up other scirpt metadata:
|
|
||||||
# describe
|
|
||||||
# pick up validation methods
|
|
||||||
|
|
||||||
def _find_vars(self):
|
|
||||||
"""
|
|
||||||
Parse the script, and populate variables that appear to be
|
|
||||||
overridable.
|
|
||||||
"""
|
|
||||||
#assumption here: we assume that variable declarations
|
|
||||||
#are only at the top level of the script. IE, we'll ignore any
|
|
||||||
#variable definitions at lower levels of the script
|
|
||||||
|
|
||||||
#we dont want to use the visit interface because here we excplicitly
|
|
||||||
#want to walk only the top level of the tree.
|
|
||||||
assignment_finder = ConstantAssignmentFinder(self.metadata)
|
|
||||||
|
|
||||||
for node in self.ast_tree.body:
|
|
||||||
if isinstance(node, ast.Assign):
|
|
||||||
assignment_finder.visit_Assign(node)
|
|
||||||
|
|
||||||
|
|
||||||
def validate(self, params):
|
|
||||||
"""
|
|
||||||
Determine if the supplied parameters are valid.
|
|
||||||
NOT IMPLEMENTED YET-- raises NotImplementedError
|
|
||||||
:param params: a dictionary of parameters
|
|
||||||
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("not yet implemented")
|
|
||||||
|
|
||||||
def build(self, build_parameters=None):
|
|
||||||
"""
|
|
||||||
Executes the script, using the optional parameters to override those in the model
|
|
||||||
:param build_parameters: a dictionary of variables. The variables must be
|
|
||||||
assignable to the underlying variable type.
|
|
||||||
:raises: Nothing. If there is an exception, it will be on the exception property of the result.
|
|
||||||
This is the interface so that we can return other information on the result, such as the build time
|
|
||||||
:return: a BuildResult object, which includes the status of the result, and either
|
|
||||||
a resulting shape or an exception
|
|
||||||
"""
|
|
||||||
if not build_parameters:
|
|
||||||
build_parameters = {}
|
|
||||||
|
|
||||||
start = time.clock()
|
|
||||||
result = BuildResult()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.set_param_values(build_parameters)
|
|
||||||
collector = ScriptCallback()
|
|
||||||
env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \
|
|
||||||
.add_entry("build_object", collector.build_object).build()
|
|
||||||
|
|
||||||
c = compile(self.ast_tree, CQSCRIPT, 'exec')
|
|
||||||
exec (c, env)
|
|
||||||
if collector.has_results():
|
|
||||||
result.set_success_result(collector.outputObjects)
|
|
||||||
else:
|
|
||||||
raise NoOutputError("Script did not call build_object-- no output available.")
|
|
||||||
except Exception, ex:
|
|
||||||
print "Error Executing Script:"
|
|
||||||
result.set_failure_result(ex)
|
|
||||||
traceback.print_exc()
|
|
||||||
print "Full Text of Script:"
|
|
||||||
print self.script_source
|
|
||||||
|
|
||||||
end = time.clock()
|
|
||||||
result.buildTime = end - start
|
|
||||||
return result
|
|
||||||
|
|
||||||
def set_param_values(self, params):
|
|
||||||
model_parameters = self.metadata.parameters
|
|
||||||
|
|
||||||
for k, v in params.iteritems():
|
|
||||||
if k not in model_parameters:
|
|
||||||
raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k)
|
|
||||||
|
|
||||||
p = model_parameters[k]
|
|
||||||
p.set_value(v)
|
|
||||||
|
|
||||||
|
|
||||||
class BuildResult(object):
|
|
||||||
"""
|
|
||||||
The result of executing a CadQuery script.
|
|
||||||
The success property contains whether the exeuction was successful.
|
|
||||||
|
|
||||||
If successful, the results property contains a list of all results,
|
|
||||||
and the first_result property contains the first result.
|
|
||||||
|
|
||||||
If unsuccessful, the exception property contains a reference to
|
|
||||||
the stack trace that occurred.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self.buildTime = None
|
|
||||||
self.results = []
|
|
||||||
self.first_result = None
|
|
||||||
self.success = False
|
|
||||||
self.exception = None
|
|
||||||
|
|
||||||
def set_failure_result(self, ex):
|
|
||||||
self.exception = ex
|
|
||||||
self.success = False
|
|
||||||
|
|
||||||
def set_success_result(self, results):
|
|
||||||
self.results = results
|
|
||||||
self.first_result = self.results[0]
|
|
||||||
self.success = True
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptMetadata(object):
|
|
||||||
"""
|
|
||||||
Defines the metadata for a parsed CQ Script.
|
|
||||||
the parameters property is a dict of InputParameter objects.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self.parameters = {}
|
|
||||||
|
|
||||||
def add_script_parameter(self, p):
|
|
||||||
self.parameters[p.name] = p
|
|
||||||
|
|
||||||
|
|
||||||
class ParameterType(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NumberParameterType(ParameterType):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class StringParameterType(ParameterType):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BooleanParameterType(ParameterType):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InputParameter:
|
|
||||||
"""
|
|
||||||
Defines a parameter that can be supplied when the model is executed.
|
|
||||||
|
|
||||||
Name, varType, and default_value are always available, because they are computed
|
|
||||||
from a variable assignment line of code:
|
|
||||||
|
|
||||||
The others are only available if the script has used define_parameter() to
|
|
||||||
provide additional metadata
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
#: the default value for the variable.
|
|
||||||
self.default_value = None
|
|
||||||
|
|
||||||
#: the name of the parameter.
|
|
||||||
self.name = None
|
|
||||||
|
|
||||||
#: type of the variable: BooleanParameter, StringParameter, NumericParameter
|
|
||||||
self.varType = None
|
|
||||||
|
|
||||||
#: help text describing the variable. Only available if the script used describe_parameter()
|
|
||||||
self.shortDesc = None
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#: valid values for the variable. Only available if the script used describe_parameter()
|
|
||||||
self.valid_values = []
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.ast_node = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create(ast_node, var_name, var_type, default_value, valid_values=None, short_desc=None):
|
|
||||||
|
|
||||||
if valid_values is None:
|
|
||||||
valid_values = []
|
|
||||||
|
|
||||||
p = InputParameter()
|
|
||||||
p.ast_node = ast_node
|
|
||||||
p.default_value = default_value
|
|
||||||
p.name = var_name
|
|
||||||
if short_desc is None:
|
|
||||||
p.shortDesc = var_name
|
|
||||||
else:
|
|
||||||
p.shortDesc = short_desc
|
|
||||||
p.varType = var_type
|
|
||||||
p.valid_values = valid_values
|
|
||||||
return p
|
|
||||||
|
|
||||||
def set_value(self, new_value):
|
|
||||||
|
|
||||||
if len(self.valid_values) > 0 and new_value not in self.valid_values:
|
|
||||||
raise InvalidParameterError(
|
|
||||||
"Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} "
|
|
||||||
.format(str(new_value), self.name, str(self.valid_values)))
|
|
||||||
|
|
||||||
if self.varType == NumberParameterType:
|
|
||||||
try:
|
|
||||||
f = float(new_value)
|
|
||||||
self.ast_node.n = f
|
|
||||||
except ValueError:
|
|
||||||
raise InvalidParameterError(
|
|
||||||
"Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric."
|
|
||||||
.format(str(new_value), self.name))
|
|
||||||
|
|
||||||
elif self.varType == StringParameterType:
|
|
||||||
self.ast_node.s = str(new_value)
|
|
||||||
elif self.varType == BooleanParameterType:
|
|
||||||
if new_value:
|
|
||||||
self.ast_node.id = 'True'
|
|
||||||
else:
|
|
||||||
self.ast_node.id = 'False'
|
|
||||||
else:
|
|
||||||
raise ValueError("Unknown Type of var: ", str(self.varType))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "InputParameter: {name=%s, type=%s, defaultValue=%s" % (
|
|
||||||
self.name, str(self.varType), str(self.default_value))
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptCallback(object):
|
|
||||||
"""
|
|
||||||
Allows a script to communicate with the container
|
|
||||||
the build_object() method is exposed to CQ scripts, to allow them
|
|
||||||
to return objects to the execution environment
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.outputObjects = []
|
|
||||||
|
|
||||||
def build_object(self, shape):
|
|
||||||
"""
|
|
||||||
return an object to the executing environment
|
|
||||||
:param shape: a cadquery object
|
|
||||||
"""
|
|
||||||
self.outputObjects.append(shape)
|
|
||||||
|
|
||||||
def describe_parameter(self,var, valid_values, short_desc):
|
|
||||||
"""
|
|
||||||
Not yet implemented: allows a script to document
|
|
||||||
extra metadata about the parameters
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def add_error(self, param, field_list):
|
|
||||||
"""
|
|
||||||
Not implemented yet: allows scripts to indicate that there are problems with inputs
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def has_results(self):
|
|
||||||
return len(self.outputObjects) > 0
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidParameterError(Exception):
|
|
||||||
"""
|
|
||||||
Raised when an attempt is made to provide a new parameter value
|
|
||||||
that cannot be assigned to the model
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NoOutputError(Exception):
|
|
||||||
"""
|
|
||||||
Raised when the script does not execute the build_object() method to
|
|
||||||
return a solid
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptExecutionError(Exception):
|
|
||||||
"""
|
|
||||||
Represents a script syntax error.
|
|
||||||
Useful for helping clients pinpoint issues with the script
|
|
||||||
interactively
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, line=None, message=None):
|
|
||||||
if line is None:
|
|
||||||
self.line = 0
|
|
||||||
else:
|
|
||||||
self.line = line
|
|
||||||
|
|
||||||
if message is None:
|
|
||||||
self.message = "Unknown Script Error"
|
|
||||||
else:
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def full_message(self):
|
|
||||||
return self.__repr__()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.__repr__()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "ScriptError [Line %s]: %s" % (self.line, self.message)
|
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentBuilder(object):
|
|
||||||
"""
|
|
||||||
Builds an execution environment for a cadquery script.
|
|
||||||
The environment includes the builtins, as well as
|
|
||||||
the other methods the script will need.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self.env = {}
|
|
||||||
|
|
||||||
def with_real_builtins(self):
|
|
||||||
return self.with_builtins(__builtins__)
|
|
||||||
|
|
||||||
def with_builtins(self, env_dict):
|
|
||||||
self.env['__builtins__'] = env_dict
|
|
||||||
return self
|
|
||||||
|
|
||||||
def with_cadquery_objects(self):
|
|
||||||
self.env['cadquery'] = cadquery
|
|
||||||
self.env['cq'] = cadquery
|
|
||||||
return self
|
|
||||||
|
|
||||||
def add_entry(self, name, value):
|
|
||||||
self.env[name] = value
|
|
||||||
return self
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
return self.env
|
|
||||||
|
|
||||||
|
|
||||||
class ConstantAssignmentFinder(ast.NodeTransformer):
|
|
||||||
"""
|
|
||||||
Visits a parse tree, and adds script parameters to the cqModel
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cq_model):
|
|
||||||
self.cqModel = cq_model
|
|
||||||
|
|
||||||
def handle_assignment(self, var_name, value_node):
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
|
|
||||||
if type(value_node) == ast.Num:
|
|
||||||
self.cqModel.add_script_parameter(
|
|
||||||
InputParameter.create(value_node, var_name, NumberParameterType, value_node.n))
|
|
||||||
elif type(value_node) == ast.Str:
|
|
||||||
self.cqModel.add_script_parameter(
|
|
||||||
InputParameter.create(value_node, var_name, StringParameterType, value_node.s))
|
|
||||||
elif type(value_node == ast.Name):
|
|
||||||
if value_node.id == 'True':
|
|
||||||
self.cqModel.add_script_parameter(
|
|
||||||
InputParameter.create(value_node, var_name, BooleanParameterType, True))
|
|
||||||
elif value_node.id == 'False':
|
|
||||||
self.cqModel.add_script_parameter(
|
|
||||||
InputParameter.create(value_node, var_name, BooleanParameterType, True))
|
|
||||||
except:
|
|
||||||
print "Unable to handle assignment for variable '%s'" % var_name
|
|
||||||
pass
|
|
||||||
|
|
||||||
def visit_Assign(self, node):
|
|
||||||
|
|
||||||
try:
|
|
||||||
left_side = node.targets[0]
|
|
||||||
|
|
||||||
#do not handle attribute assignments
|
|
||||||
if isinstance(left_side,ast.Attribute):
|
|
||||||
return
|
|
||||||
|
|
||||||
if type(node.value) in [ast.Num, ast.Str, ast.Name]:
|
|
||||||
self.handle_assignment(left_side.id, node.value)
|
|
||||||
elif type(node.value) == ast.Tuple:
|
|
||||||
# we have a multi-value assignment
|
|
||||||
for n, v in zip(left_side.elts, node.value.elts):
|
|
||||||
self.handle_assignment(n.id, v)
|
|
||||||
except:
|
|
||||||
traceback.print_exc()
|
|
||||||
print "Unable to handle assignment for node '%s'" % ast.dump(left_side)
|
|
||||||
|
|
||||||
return node
|
|
|
@ -1,112 +0,0 @@
|
||||||
"""
|
|
||||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
|
||||||
|
|
||||||
This file is part of CadQuery.
|
|
||||||
|
|
||||||
CadQuery is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
CadQuery is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
|
||||||
"""
|
|
||||||
import os, sys
|
|
||||||
|
|
||||||
|
|
||||||
def _fc_path():
|
|
||||||
"""Find FreeCAD"""
|
|
||||||
_PATH = ""
|
|
||||||
if _PATH:
|
|
||||||
return _PATH
|
|
||||||
|
|
||||||
#look for FREECAD_LIB env variable
|
|
||||||
if os.environ.has_key('FREECAD_LIB'):
|
|
||||||
_PATH = os.environ.get('FREECAD_LIB')
|
|
||||||
if os.path.exists( _PATH):
|
|
||||||
return _PATH
|
|
||||||
|
|
||||||
if sys.platform.startswith('linux'):
|
|
||||||
#Make some dangerous assumptions...
|
|
||||||
for _PATH in [
|
|
||||||
os.path.join(os.path.expanduser("~"), "lib/freecad/lib"),
|
|
||||||
"/usr/local/lib/freecad/lib",
|
|
||||||
"/usr/lib/freecad/lib",
|
|
||||||
"/opt/freecad/lib/",
|
|
||||||
"/usr/bin/freecad/lib",
|
|
||||||
"/usr/lib/freecad",
|
|
||||||
]:
|
|
||||||
if os.path.exists(_PATH):
|
|
||||||
return _PATH
|
|
||||||
|
|
||||||
elif sys.platform.startswith('win'):
|
|
||||||
#try all the usual suspects
|
|
||||||
for _PATH in [
|
|
||||||
"c:/Program Files/FreeCAD0.12/bin",
|
|
||||||
"c:/Program Files/FreeCAD0.13/bin",
|
|
||||||
"c:/Program Files/FreeCAD0.14/bin",
|
|
||||||
"c:/Program Files/FreeCAD0.15/bin",
|
|
||||||
"c:/Program Files/FreeCAD0.16/bin",
|
|
||||||
"c:/Program Files/FreeCAD0.17/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD0.12/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD0.13/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD0.14/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD0.15/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD0.16/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD0.17/bin",
|
|
||||||
"c:/apps/FreeCAD0.12/bin",
|
|
||||||
"c:/apps/FreeCAD0.13/bin",
|
|
||||||
"c:/apps/FreeCAD0.14/bin",
|
|
||||||
"c:/apps/FreeCAD0.15/bin",
|
|
||||||
"c:/apps/FreeCAD0.16/bin",
|
|
||||||
"c:/apps/FreeCAD0.17/bin",
|
|
||||||
"c:/Program Files/FreeCAD 0.12/bin",
|
|
||||||
"c:/Program Files/FreeCAD 0.13/bin",
|
|
||||||
"c:/Program Files/FreeCAD 0.14/bin",
|
|
||||||
"c:/Program Files/FreeCAD 0.15/bin",
|
|
||||||
"c:/Program Files/FreeCAD 0.16/bin",
|
|
||||||
"c:/Program Files/FreeCAD 0.17/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD 0.12/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD 0.13/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD 0.14/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD 0.15/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD 0.16/bin",
|
|
||||||
"c:/Program Files (x86)/FreeCAD 0.17/bin",
|
|
||||||
"c:/apps/FreeCAD 0.12/bin",
|
|
||||||
"c:/apps/FreeCAD 0.13/bin",
|
|
||||||
"c:/apps/FreeCAD 0.14/bin",
|
|
||||||
"c:/apps/FreeCAD 0.15/bin",
|
|
||||||
"c:/apps/FreeCAD 0.16/bin",
|
|
||||||
"c:/apps/FreeCAD 0.17/bin",
|
|
||||||
]:
|
|
||||||
if os.path.exists(_PATH):
|
|
||||||
return _PATH
|
|
||||||
elif sys.platform.startswith('darwin'):
|
|
||||||
#Assume we're dealing with a Mac
|
|
||||||
for _PATH in [
|
|
||||||
"/Applications/FreeCAD.app/Contents/lib",
|
|
||||||
os.path.join(os.path.expanduser("~"), "Library/Application Support/FreeCAD/lib"),
|
|
||||||
]:
|
|
||||||
if os.path.exists(_PATH):
|
|
||||||
return _PATH
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#Make sure that the correct FreeCAD path shows up in Python's system path
|
|
||||||
no_library_path = ImportError('cadquery was unable to determine freecads library path')
|
|
||||||
try:
|
|
||||||
import FreeCAD
|
|
||||||
except ImportError:
|
|
||||||
path = _fc_path()
|
|
||||||
if path:
|
|
||||||
sys.path.insert(0, path)
|
|
||||||
try:
|
|
||||||
import FreeCAD
|
|
||||||
except ImportError:
|
|
||||||
raise no_library_path
|
|
||||||
else: raise no_library_path
|
|
|
@ -1,392 +0,0 @@
|
||||||
import cadquery
|
|
||||||
|
|
||||||
import FreeCAD
|
|
||||||
import Drawing
|
|
||||||
|
|
||||||
import tempfile, os, StringIO
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
import xml.etree.cElementTree as ET
|
|
||||||
except ImportError:
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
|
|
||||||
class ExportTypes:
|
|
||||||
STL = "STL"
|
|
||||||
STEP = "STEP"
|
|
||||||
AMF = "AMF"
|
|
||||||
SVG = "SVG"
|
|
||||||
TJS = "TJS"
|
|
||||||
|
|
||||||
|
|
||||||
class UNITS:
|
|
||||||
MM = "mm"
|
|
||||||
IN = "in"
|
|
||||||
|
|
||||||
|
|
||||||
def toString(shape, exportType, tolerance=0.1):
|
|
||||||
s = StringIO.StringIO()
|
|
||||||
exportShape(shape, exportType, s, tolerance)
|
|
||||||
return s.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
def exportShape(shape,exportType,fileLike,tolerance=0.1):
|
|
||||||
"""
|
|
||||||
:param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery
|
|
||||||
object, the first value is exported
|
|
||||||
:param exportFormat: the exportFormat to use
|
|
||||||
:param tolerance: the tolerance, in model units
|
|
||||||
:param fileLike: a file like object to which the content will be written.
|
|
||||||
The object should be already open and ready to write. The caller is responsible
|
|
||||||
for closing the object
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
if isinstance(shape,cadquery.CQ):
|
|
||||||
shape = shape.val()
|
|
||||||
|
|
||||||
if exportType == ExportTypes.TJS:
|
|
||||||
#tessellate the model
|
|
||||||
tess = shape.tessellate(tolerance)
|
|
||||||
|
|
||||||
mesher = JsonMesh() #warning: needs to be changed to remove buildTime and exportTime!!!
|
|
||||||
#add vertices
|
|
||||||
for vec in tess[0]:
|
|
||||||
mesher.addVertex(vec.x, vec.y, vec.z)
|
|
||||||
|
|
||||||
#add faces
|
|
||||||
for f in tess[1]:
|
|
||||||
mesher.addTriangleFace(f[0],f[1], f[2])
|
|
||||||
fileLike.write( mesher.toJson())
|
|
||||||
elif exportType == ExportTypes.SVG:
|
|
||||||
fileLike.write(getSVG(shape.wrapped))
|
|
||||||
elif exportType == ExportTypes.AMF:
|
|
||||||
tess = shape.tessellate(tolerance)
|
|
||||||
aw = AmfWriter(tess).writeAmf(fileLike)
|
|
||||||
else:
|
|
||||||
|
|
||||||
#all these types required writing to a file and then
|
|
||||||
#re-reading. this is due to the fact that FreeCAD writes these
|
|
||||||
(h, outFileName) = tempfile.mkstemp()
|
|
||||||
#weird, but we need to close this file. the next step is going to write to
|
|
||||||
#it from c code, so it needs to be closed.
|
|
||||||
os.close(h)
|
|
||||||
|
|
||||||
if exportType == ExportTypes.STEP:
|
|
||||||
shape.exportStep(outFileName)
|
|
||||||
elif exportType == ExportTypes.STL:
|
|
||||||
shape.wrapped.exportStl(outFileName)
|
|
||||||
else:
|
|
||||||
raise ValueError("No idea how i got here")
|
|
||||||
|
|
||||||
res = readAndDeleteFile(outFileName)
|
|
||||||
fileLike.write(res)
|
|
||||||
|
|
||||||
def readAndDeleteFile(fileName):
|
|
||||||
"""
|
|
||||||
read data from file provided, and delete it when done
|
|
||||||
return the contents as a string
|
|
||||||
"""
|
|
||||||
res = ""
|
|
||||||
with open(fileName,'r') as f:
|
|
||||||
res = f.read()
|
|
||||||
|
|
||||||
os.remove(fileName)
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def guessUnitOfMeasure(shape):
|
|
||||||
"""
|
|
||||||
Guess the unit of measure of a shape.
|
|
||||||
"""
|
|
||||||
bb = shape.BoundBox
|
|
||||||
|
|
||||||
dimList = [ bb.XLength, bb.YLength,bb.ZLength ]
|
|
||||||
#no real part would likely be bigger than 10 inches on any side
|
|
||||||
if max(dimList) > 10:
|
|
||||||
return UNITS.MM
|
|
||||||
|
|
||||||
#no real part would likely be smaller than 0.1 mm on all dimensions
|
|
||||||
if min(dimList) < 0.1:
|
|
||||||
return UNITS.IN
|
|
||||||
|
|
||||||
#no real part would have the sum of its dimensions less than about 5mm
|
|
||||||
if sum(dimList) < 10:
|
|
||||||
return UNITS.IN
|
|
||||||
|
|
||||||
return UNITS.MM
|
|
||||||
|
|
||||||
|
|
||||||
class AmfWriter(object):
|
|
||||||
def __init__(self,tessellation):
|
|
||||||
|
|
||||||
self.units = "mm"
|
|
||||||
self.tessellation = tessellation
|
|
||||||
|
|
||||||
def writeAmf(self,outFile):
|
|
||||||
amf = ET.Element('amf',units=self.units)
|
|
||||||
#TODO: if result is a compound, we need to loop through them
|
|
||||||
object = ET.SubElement(amf,'object',id="0")
|
|
||||||
mesh = ET.SubElement(object,'mesh')
|
|
||||||
vertices = ET.SubElement(mesh,'vertices')
|
|
||||||
volume = ET.SubElement(mesh,'volume')
|
|
||||||
|
|
||||||
#add vertices
|
|
||||||
for v in self.tessellation[0]:
|
|
||||||
vtx = ET.SubElement(vertices,'vertex')
|
|
||||||
coord = ET.SubElement(vtx,'coordinates')
|
|
||||||
x = ET.SubElement(coord,'x')
|
|
||||||
x.text = str(v.x)
|
|
||||||
y = ET.SubElement(coord,'y')
|
|
||||||
y.text = str(v.y)
|
|
||||||
z = ET.SubElement(coord,'z')
|
|
||||||
z.text = str(v.z)
|
|
||||||
|
|
||||||
#add triangles
|
|
||||||
for t in self.tessellation[1]:
|
|
||||||
triangle = ET.SubElement(volume,'triangle')
|
|
||||||
v1 = ET.SubElement(triangle,'v1')
|
|
||||||
v1.text = str(t[0])
|
|
||||||
v2 = ET.SubElement(triangle,'v2')
|
|
||||||
v2.text = str(t[1])
|
|
||||||
v3 = ET.SubElement(triangle,'v3')
|
|
||||||
v3.text = str(t[2])
|
|
||||||
|
|
||||||
|
|
||||||
ET.ElementTree(amf).write(outFile,encoding='ISO-8859-1')
|
|
||||||
|
|
||||||
"""
|
|
||||||
Objects that represent
|
|
||||||
three.js JSON object notation
|
|
||||||
https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0
|
|
||||||
"""
|
|
||||||
class JsonMesh(object):
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
self.vertices = [];
|
|
||||||
self.faces = [];
|
|
||||||
self.nVertices = 0;
|
|
||||||
self.nFaces = 0;
|
|
||||||
|
|
||||||
def addVertex(self,x,y,z):
|
|
||||||
self.nVertices += 1;
|
|
||||||
self.vertices.extend([x,y,z]);
|
|
||||||
|
|
||||||
#add triangle composed of the three provided vertex indices
|
|
||||||
def addTriangleFace(self, i,j,k):
|
|
||||||
#first position means justa simple triangle
|
|
||||||
self.nFaces += 1;
|
|
||||||
self.faces.extend([0,int(i),int(j),int(k)]);
|
|
||||||
|
|
||||||
"""
|
|
||||||
Get a json model from this model.
|
|
||||||
For now we'll forget about colors, vertex normals, and all that stuff
|
|
||||||
"""
|
|
||||||
def toJson(self):
|
|
||||||
return JSON_TEMPLATE % {
|
|
||||||
'vertices' : str(self.vertices),
|
|
||||||
'faces' : str(self.faces),
|
|
||||||
'nVertices': self.nVertices,
|
|
||||||
'nFaces' : self.nFaces
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
def getPaths(freeCadSVG):
|
|
||||||
"""
|
|
||||||
freeCad svg is worthless-- except for paths, which are fairly useful
|
|
||||||
this method accepts svg from fReeCAD and returns a list of strings suitable for inclusion in a path element
|
|
||||||
returns two lists-- one list of visible lines, and one list of hidden lines
|
|
||||||
|
|
||||||
HACK ALERT!!!!!
|
|
||||||
FreeCAD does not give a way to determine which lines are hidden and which are not
|
|
||||||
the only way to tell is that hidden lines are in a <g> with 0.15 stroke and visible are 0.35 stroke.
|
|
||||||
so we actually look for that as a way to parse.
|
|
||||||
|
|
||||||
to make it worse, elementTree xpath attribute selectors do not work in python 2.6, and we
|
|
||||||
cannot use python 2.7 due to freecad. So its necessary to look for the pure strings! ick!
|
|
||||||
"""
|
|
||||||
|
|
||||||
hiddenPaths = []
|
|
||||||
visiblePaths = []
|
|
||||||
if len(freeCadSVG) > 0:
|
|
||||||
#yuk, freecad returns svg fragments. stupid stupid
|
|
||||||
fullDoc = "<root>%s</root>" % freeCadSVG
|
|
||||||
e = ET.ElementTree(ET.fromstring(fullDoc))
|
|
||||||
segments = e.findall(".//g")
|
|
||||||
for s in segments:
|
|
||||||
paths = s.findall("path")
|
|
||||||
|
|
||||||
if s.get("stroke-width") == "0.15": #hidden line HACK HACK HACK
|
|
||||||
mylist = hiddenPaths
|
|
||||||
else:
|
|
||||||
mylist = visiblePaths
|
|
||||||
|
|
||||||
for p in paths:
|
|
||||||
mylist.append(p.get("d"))
|
|
||||||
return (hiddenPaths,visiblePaths)
|
|
||||||
else:
|
|
||||||
return ([],[])
|
|
||||||
|
|
||||||
|
|
||||||
def getSVG(shape,opts=None):
|
|
||||||
"""
|
|
||||||
Export a shape to SVG
|
|
||||||
"""
|
|
||||||
|
|
||||||
d = {'width':800,'height':240,'marginLeft':200,'marginTop':20}
|
|
||||||
|
|
||||||
if opts:
|
|
||||||
d.update(opts)
|
|
||||||
|
|
||||||
#need to guess the scale and the coordinate center
|
|
||||||
uom = guessUnitOfMeasure(shape)
|
|
||||||
|
|
||||||
width=float(d['width'])
|
|
||||||
height=float(d['height'])
|
|
||||||
marginLeft=float(d['marginLeft'])
|
|
||||||
marginTop=float(d['marginTop'])
|
|
||||||
|
|
||||||
#TODO: provide option to give 3 views
|
|
||||||
viewVector = FreeCAD.Base.Vector(-1.75,1.1,5)
|
|
||||||
(visibleG0,visibleG1,hiddenG0,hiddenG1) = Drawing.project(shape,viewVector)
|
|
||||||
|
|
||||||
(hiddenPaths,visiblePaths) = getPaths(Drawing.projectToSVG(shape,viewVector,"ShowHiddenLines")) #this param is totally undocumented!
|
|
||||||
|
|
||||||
#get bounding box -- these are all in 2-d space
|
|
||||||
bb = visibleG0.BoundBox
|
|
||||||
bb.add(visibleG1.BoundBox)
|
|
||||||
bb.add(hiddenG0.BoundBox)
|
|
||||||
bb.add(hiddenG1.BoundBox)
|
|
||||||
|
|
||||||
#width pixels for x, height pixesl for y
|
|
||||||
unitScale = min( width / bb.XLength * 0.75 , height / bb.YLength * 0.75 )
|
|
||||||
|
|
||||||
#compute amount to translate-- move the top left into view
|
|
||||||
(xTranslate,yTranslate) = ( (0 - bb.XMin) + marginLeft/unitScale ,(0- bb.YMax) - marginTop/unitScale)
|
|
||||||
|
|
||||||
#compute paths ( again -- had to strip out freecad crap )
|
|
||||||
hiddenContent = ""
|
|
||||||
for p in hiddenPaths:
|
|
||||||
hiddenContent += PATHTEMPLATE % p
|
|
||||||
|
|
||||||
visibleContent = ""
|
|
||||||
for p in visiblePaths:
|
|
||||||
visibleContent += PATHTEMPLATE % p
|
|
||||||
|
|
||||||
svg = SVG_TEMPLATE % (
|
|
||||||
{
|
|
||||||
"unitScale" : str(unitScale),
|
|
||||||
"strokeWidth" : str(1.0/unitScale),
|
|
||||||
"hiddenContent" : hiddenContent ,
|
|
||||||
"visibleContent" :visibleContent,
|
|
||||||
"xTranslate" : str(xTranslate),
|
|
||||||
"yTranslate" : str(yTranslate),
|
|
||||||
"width" : str(width),
|
|
||||||
"height" : str(height),
|
|
||||||
"textboxY" :str(height - 30),
|
|
||||||
"uom" : str(uom)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
#svg = SVG_TEMPLATE % (
|
|
||||||
# {"content": projectedContent}
|
|
||||||
#)
|
|
||||||
return svg
|
|
||||||
|
|
||||||
|
|
||||||
def exportSVG(shape, fileName):
|
|
||||||
"""
|
|
||||||
accept a cadquery shape, and export it to the provided file
|
|
||||||
TODO: should use file-like objects, not a fileName, and/or be able to return a string instead
|
|
||||||
export a view of a part to svg
|
|
||||||
"""
|
|
||||||
|
|
||||||
svg = getSVG(shape.val().wrapped)
|
|
||||||
f = open(fileName,'w')
|
|
||||||
f.write(svg)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
JSON_TEMPLATE= """\
|
|
||||||
{
|
|
||||||
"metadata" :
|
|
||||||
{
|
|
||||||
"formatVersion" : 3,
|
|
||||||
"generatedBy" : "ParametricParts",
|
|
||||||
"vertices" : %(nVertices)d,
|
|
||||||
"faces" : %(nFaces)d,
|
|
||||||
"normals" : 0,
|
|
||||||
"colors" : 0,
|
|
||||||
"uvs" : 0,
|
|
||||||
"materials" : 1,
|
|
||||||
"morphTargets" : 0
|
|
||||||
},
|
|
||||||
|
|
||||||
"scale" : 1.0,
|
|
||||||
|
|
||||||
"materials": [ {
|
|
||||||
"DbgColor" : 15658734,
|
|
||||||
"DbgIndex" : 0,
|
|
||||||
"DbgName" : "Material",
|
|
||||||
"colorAmbient" : [0.0, 0.0, 0.0],
|
|
||||||
"colorDiffuse" : [0.6400000190734865, 0.10179081114814892, 0.126246120426746],
|
|
||||||
"colorSpecular" : [0.5, 0.5, 0.5],
|
|
||||||
"shading" : "Lambert",
|
|
||||||
"specularCoef" : 50,
|
|
||||||
"transparency" : 1.0,
|
|
||||||
"vertexColors" : false
|
|
||||||
}],
|
|
||||||
|
|
||||||
"vertices": %(vertices)s,
|
|
||||||
|
|
||||||
"morphTargets": [],
|
|
||||||
|
|
||||||
"normals": [],
|
|
||||||
|
|
||||||
"colors": [],
|
|
||||||
|
|
||||||
"uvs": [[]],
|
|
||||||
|
|
||||||
"faces": %(faces)s
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="%(width)s"
|
|
||||||
height="%(height)s"
|
|
||||||
|
|
||||||
>
|
|
||||||
<g transform="scale(%(unitScale)s, -%(unitScale)s) translate(%(xTranslate)s,%(yTranslate)s)" stroke-width="%(strokeWidth)s" fill="none">
|
|
||||||
<!-- hidden lines -->
|
|
||||||
<g stroke="rgb(160, 160, 160)" fill="none" stroke-dasharray="%(strokeWidth)s,%(strokeWidth)s" >
|
|
||||||
%(hiddenContent)s
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- solid lines -->
|
|
||||||
<g stroke="rgb(0, 0, 0)" fill="none">
|
|
||||||
%(visibleContent)s
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g transform="translate(20,%(textboxY)s)" stroke="rgb(0,0,255)">
|
|
||||||
<line x1="30" y1="-30" x2="75" y2="-33" stroke-width="3" stroke="#000000" />
|
|
||||||
<text x="80" y="-30" style="stroke:#000000">X </text>
|
|
||||||
|
|
||||||
<line x1="30" y1="-30" x2="30" y2="-75" stroke-width="3" stroke="#000000" />
|
|
||||||
<text x="25" y="-85" style="stroke:#000000">Y </text>
|
|
||||||
|
|
||||||
<line x1="30" y1="-30" x2="58" y2="-15" stroke-width="3" stroke="#000000" />
|
|
||||||
<text x="65" y="-5" style="stroke:#000000">Z </text>
|
|
||||||
<!--
|
|
||||||
<line x1="0" y1="0" x2="%(unitScale)s" y2="0" stroke-width="3" />
|
|
||||||
<text x="0" y="20" style="stroke:#000000">1 %(uom)s </text>
|
|
||||||
-->
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
"""
|
|
||||||
|
|
||||||
PATHTEMPLATE="\t\t\t<path d=\"%s\" />\n"
|
|
||||||
|
|
|
@ -1,647 +0,0 @@
|
||||||
"""
|
|
||||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
|
||||||
|
|
||||||
This file is part of CadQuery.
|
|
||||||
|
|
||||||
CadQuery is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
CadQuery is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import cadquery
|
|
||||||
import FreeCAD
|
|
||||||
import Part as FreeCADPart
|
|
||||||
|
|
||||||
|
|
||||||
def sortWiresByBuildOrder(wireList, plane, result=[]):
|
|
||||||
"""Tries to determine how wires should be combined into faces.
|
|
||||||
|
|
||||||
Assume:
|
|
||||||
The wires make up one or more faces, which could have 'holes'
|
|
||||||
Outer wires are listed ahead of inner wires
|
|
||||||
there are no wires inside wires inside wires
|
|
||||||
( IE, islands -- we can deal with that later on )
|
|
||||||
none of the wires are construction wires
|
|
||||||
|
|
||||||
Compute:
|
|
||||||
one or more sets of wires, with the outer wire listed first, and inner
|
|
||||||
ones
|
|
||||||
|
|
||||||
Returns, list of lists.
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
|
|
||||||
remainingWires = list(wireList)
|
|
||||||
while remainingWires:
|
|
||||||
outerWire = remainingWires.pop(0)
|
|
||||||
group = [outerWire]
|
|
||||||
otherWires = list(remainingWires)
|
|
||||||
for w in otherWires:
|
|
||||||
if plane.isWireInside(outerWire, w):
|
|
||||||
group.append(w)
|
|
||||||
remainingWires.remove(w)
|
|
||||||
result.append(group)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class Vector(object):
|
|
||||||
"""Create a 3-dimensional vector
|
|
||||||
|
|
||||||
:param args: a 3-d vector, with x-y-z parts.
|
|
||||||
|
|
||||||
you can either provide:
|
|
||||||
* nothing (in which case the null vector is return)
|
|
||||||
* a FreeCAD vector
|
|
||||||
* a vector ( in which case it is copied )
|
|
||||||
* a 3-tuple
|
|
||||||
* three float values, x, y, and z
|
|
||||||
"""
|
|
||||||
def __init__(self, *args):
|
|
||||||
if len(args) == 3:
|
|
||||||
fV = FreeCAD.Base.Vector(args[0], args[1], args[2])
|
|
||||||
elif len(args) == 1:
|
|
||||||
if isinstance(args[0], Vector):
|
|
||||||
fV = args[0].wrapped
|
|
||||||
elif isinstance(args[0], tuple):
|
|
||||||
fV = FreeCAD.Base.Vector(args[0][0], args[0][1], args[0][2])
|
|
||||||
elif isinstance(args[0], FreeCAD.Base.Vector):
|
|
||||||
fV = args[0]
|
|
||||||
else:
|
|
||||||
fV = args[0]
|
|
||||||
elif len(args) == 0:
|
|
||||||
fV = FreeCAD.Base.Vector(0, 0, 0)
|
|
||||||
else:
|
|
||||||
raise ValueError("Expected three floats, FreeCAD Vector, or 3-tuple")
|
|
||||||
|
|
||||||
self._wrapped = fV
|
|
||||||
|
|
||||||
@property
|
|
||||||
def x(self):
|
|
||||||
return self.wrapped.x
|
|
||||||
|
|
||||||
@property
|
|
||||||
def y(self):
|
|
||||||
return self.wrapped.y
|
|
||||||
|
|
||||||
@property
|
|
||||||
def z(self):
|
|
||||||
return self.wrapped.z
|
|
||||||
|
|
||||||
@property
|
|
||||||
def Length(self):
|
|
||||||
return self.wrapped.Length
|
|
||||||
|
|
||||||
@property
|
|
||||||
def wrapped(self):
|
|
||||||
return self._wrapped
|
|
||||||
|
|
||||||
def toTuple(self):
|
|
||||||
return (self.x, self.y, self.z)
|
|
||||||
|
|
||||||
# TODO: is it possible to create a dynamic proxy without all this code?
|
|
||||||
def cross(self, v):
|
|
||||||
return Vector(self.wrapped.cross(v.wrapped))
|
|
||||||
|
|
||||||
def dot(self, v):
|
|
||||||
return self.wrapped.dot(v.wrapped)
|
|
||||||
|
|
||||||
def sub(self, v):
|
|
||||||
return Vector(self.wrapped.sub(v.wrapped))
|
|
||||||
|
|
||||||
def add(self, v):
|
|
||||||
return Vector(self.wrapped.add(v.wrapped))
|
|
||||||
|
|
||||||
def multiply(self, scale):
|
|
||||||
"""Return a copy multiplied by the provided scalar"""
|
|
||||||
tmp_fc_vector = FreeCAD.Base.Vector(self.wrapped)
|
|
||||||
return Vector(tmp_fc_vector.multiply(scale))
|
|
||||||
|
|
||||||
def normalized(self):
|
|
||||||
"""Return a normalized version of this vector"""
|
|
||||||
tmp_fc_vector = FreeCAD.Base.Vector(self.wrapped)
|
|
||||||
tmp_fc_vector.normalize()
|
|
||||||
return Vector(tmp_fc_vector)
|
|
||||||
|
|
||||||
def Center(self):
|
|
||||||
"""Return the vector itself
|
|
||||||
|
|
||||||
The center of myself is myself.
|
|
||||||
Provided so that vectors, vertexes, and other shapes all support a
|
|
||||||
common interface, when Center() is requested for all objects on the
|
|
||||||
stack.
|
|
||||||
"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
def getAngle(self, v):
|
|
||||||
return self.wrapped.getAngle(v.wrapped)
|
|
||||||
|
|
||||||
def distanceToLine(self):
|
|
||||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
|
||||||
|
|
||||||
def projectToLine(self):
|
|
||||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
|
||||||
|
|
||||||
def distanceToPlane(self):
|
|
||||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
|
||||||
|
|
||||||
def projectToPlane(self):
|
|
||||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
|
||||||
|
|
||||||
def __add__(self, v):
|
|
||||||
return self.add(v)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.wrapped.__repr__()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.wrapped.__str__()
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return self.wrapped.__ne__(other)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.wrapped.__eq__(other)
|
|
||||||
|
|
||||||
|
|
||||||
class Matrix:
|
|
||||||
"""A 3d , 4x4 transformation matrix.
|
|
||||||
|
|
||||||
Used to move geometry in space.
|
|
||||||
"""
|
|
||||||
def __init__(self, matrix=None):
|
|
||||||
if matrix is None:
|
|
||||||
self.wrapped = FreeCAD.Base.Matrix()
|
|
||||||
else:
|
|
||||||
self.wrapped = matrix
|
|
||||||
|
|
||||||
def rotateX(self, angle):
|
|
||||||
self.wrapped.rotateX(angle)
|
|
||||||
|
|
||||||
def rotateY(self, angle):
|
|
||||||
self.wrapped.rotateY(angle)
|
|
||||||
|
|
||||||
|
|
||||||
class Plane(object):
|
|
||||||
"""A 2D coordinate system in space
|
|
||||||
|
|
||||||
A 2D coordinate system in space, with the x-y axes on the plane, and a
|
|
||||||
particular point as the origin.
|
|
||||||
|
|
||||||
A plane allows the use of 2-d coordinates, which are later converted to
|
|
||||||
global, 3d coordinates when the operations are complete.
|
|
||||||
|
|
||||||
Frequently, it is not necessary to create work planes, as they can be
|
|
||||||
created automatically from faces.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def named(cls, stdName, origin=(0, 0, 0)):
|
|
||||||
"""Create a predefined Plane based on the conventional names.
|
|
||||||
|
|
||||||
:param stdName: one of (XY|YZ|ZX|XZ|YX|ZY|front|back|left|right|top|bottom)
|
|
||||||
:type stdName: string
|
|
||||||
:param origin: the desired origin, specified in global coordinates
|
|
||||||
:type origin: 3-tuple of the origin of the new plane, in global coorindates.
|
|
||||||
|
|
||||||
Available named planes are as follows. Direction references refer to
|
|
||||||
the global directions.
|
|
||||||
|
|
||||||
=========== ======= ======= ======
|
|
||||||
Name xDir yDir zDir
|
|
||||||
=========== ======= ======= ======
|
|
||||||
XY +x +y +z
|
|
||||||
YZ +y +z +x
|
|
||||||
ZX +z +x +y
|
|
||||||
XZ +x +z -y
|
|
||||||
YX +y +x -z
|
|
||||||
ZY +z +y -x
|
|
||||||
front +x +y +z
|
|
||||||
back -x +y -z
|
|
||||||
left +z +y -x
|
|
||||||
right -z +y +x
|
|
||||||
top +x -z +y
|
|
||||||
bottom +x +z -y
|
|
||||||
=========== ======= ======= ======
|
|
||||||
"""
|
|
||||||
|
|
||||||
namedPlanes = {
|
|
||||||
# origin, xDir, normal
|
|
||||||
'XY': Plane(origin, (1, 0, 0), (0, 0, 1)),
|
|
||||||
'YZ': Plane(origin, (0, 1, 0), (1, 0, 0)),
|
|
||||||
'ZX': Plane(origin, (0, 0, 1), (0, 1, 0)),
|
|
||||||
'XZ': Plane(origin, (1, 0, 0), (0, -1, 0)),
|
|
||||||
'YX': Plane(origin, (0, 1, 0), (0, 0, -1)),
|
|
||||||
'ZY': Plane(origin, (0, 0, 1), (-1, 0, 0)),
|
|
||||||
'front': Plane(origin, (1, 0, 0), (0, 0, 1)),
|
|
||||||
'back': Plane(origin, (-1, 0, 0), (0, 0, -1)),
|
|
||||||
'left': Plane(origin, (0, 0, 1), (-1, 0, 0)),
|
|
||||||
'right': Plane(origin, (0, 0, -1), (1, 0, 0)),
|
|
||||||
'top': Plane(origin, (1, 0, 0), (0, 1, 0)),
|
|
||||||
'bottom': Plane(origin, (1, 0, 0), (0, -1, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
return namedPlanes[stdName]
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError('Supported names are {}'.format(
|
|
||||||
namedPlanes.keys()))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
|
|
||||||
plane = Plane.named('XY', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def YZ(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)):
|
|
||||||
plane = Plane.named('YZ', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def ZX(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
|
|
||||||
plane = Plane.named('ZX', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def XZ(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
|
|
||||||
plane = Plane.named('XZ', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def YX(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)):
|
|
||||||
plane = Plane.named('YX', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def ZY(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
|
|
||||||
plane = Plane.named('ZY', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def front(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
|
|
||||||
plane = Plane.named('front', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def back(cls, origin=(0, 0, 0), xDir=Vector(-1, 0, 0)):
|
|
||||||
plane = Plane.named('back', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def left(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
|
|
||||||
plane = Plane.named('left', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def right(cls, origin=(0, 0, 0), xDir=Vector(0, 0, -1)):
|
|
||||||
plane = Plane.named('right', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def top(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
|
|
||||||
plane = Plane.named('top', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
|
|
||||||
plane = Plane.named('bottom', origin)
|
|
||||||
plane._setPlaneDir(xDir)
|
|
||||||
return plane
|
|
||||||
|
|
||||||
def __init__(self, origin, xDir, normal):
|
|
||||||
"""Create a Plane with an arbitrary orientation
|
|
||||||
|
|
||||||
TODO: project x and y vectors so they work even if not orthogonal
|
|
||||||
:param origin: the origin
|
|
||||||
:type origin: a three-tuple of the origin, in global coordinates
|
|
||||||
:param xDir: a vector representing the xDirection.
|
|
||||||
:type xDir: a three-tuple representing a vector, or a FreeCAD Vector
|
|
||||||
:param normal: the normal direction for the new plane
|
|
||||||
:type normal: a FreeCAD Vector
|
|
||||||
:raises: ValueError if the specified xDir is not orthogonal to the provided normal.
|
|
||||||
:return: a plane in the global space, with the xDirection of the plane in the specified direction.
|
|
||||||
"""
|
|
||||||
normal = Vector(normal)
|
|
||||||
if (normal.Length == 0.0):
|
|
||||||
raise ValueError('normal should be non null')
|
|
||||||
self.zDir = normal.normalized()
|
|
||||||
xDir = Vector(xDir)
|
|
||||||
if (xDir.Length == 0.0):
|
|
||||||
raise ValueError('xDir should be non null')
|
|
||||||
self._setPlaneDir(xDir)
|
|
||||||
|
|
||||||
self.invZDir = self.zDir.multiply(-1.0)
|
|
||||||
|
|
||||||
self.origin = origin
|
|
||||||
|
|
||||||
@property
|
|
||||||
def origin(self):
|
|
||||||
return self._origin
|
|
||||||
|
|
||||||
@origin.setter
|
|
||||||
def origin(self, value):
|
|
||||||
self._origin = Vector(value)
|
|
||||||
self._calcTransforms()
|
|
||||||
|
|
||||||
def setOrigin2d(self, x, y):
|
|
||||||
"""
|
|
||||||
Set a new origin in the plane itself
|
|
||||||
|
|
||||||
Set a new origin in the plane itself. The plane's orientation and
|
|
||||||
xDrection are unaffected.
|
|
||||||
|
|
||||||
:param float x: offset in the x direction
|
|
||||||
:param float y: offset in the y direction
|
|
||||||
:return: void
|
|
||||||
|
|
||||||
The new coordinates are specified in terms of the current 2-d system.
|
|
||||||
As an example:
|
|
||||||
|
|
||||||
p = Plane.XY()
|
|
||||||
p.setOrigin2d(2, 2)
|
|
||||||
p.setOrigin2d(2, 2)
|
|
||||||
|
|
||||||
results in a plane with its origin at (x, y) = (4, 4) in global
|
|
||||||
coordinates. Both operations were relative to local coordinates of the
|
|
||||||
plane.
|
|
||||||
"""
|
|
||||||
self.origin = self.toWorldCoords((x, y))
|
|
||||||
|
|
||||||
def isWireInside(self, baseWire, testWire):
|
|
||||||
"""Determine if testWire is inside baseWire
|
|
||||||
|
|
||||||
Determine if testWire is inside baseWire, after both wires are projected
|
|
||||||
into the current plane.
|
|
||||||
|
|
||||||
:param baseWire: a reference wire
|
|
||||||
:type baseWire: a FreeCAD wire
|
|
||||||
:param testWire: another wire
|
|
||||||
:type testWire: a FreeCAD wire
|
|
||||||
:return: True if testWire is inside baseWire, otherwise False
|
|
||||||
|
|
||||||
If either wire does not lie in the current plane, it is projected into
|
|
||||||
the plane first.
|
|
||||||
|
|
||||||
*WARNING*: This method is not 100% reliable. It uses bounding box
|
|
||||||
tests, but needs more work to check for cases when curves are complex.
|
|
||||||
|
|
||||||
Future Enhancements:
|
|
||||||
* Discretizing points along each curve to provide a more reliable
|
|
||||||
test.
|
|
||||||
"""
|
|
||||||
# TODO: also use a set of points along the wire to test as well.
|
|
||||||
# TODO: would it be more efficient to create objects in the local
|
|
||||||
# coordinate system, and then transform to global
|
|
||||||
# coordinates upon extrusion?
|
|
||||||
|
|
||||||
tBaseWire = baseWire.transformGeometry(self.fG)
|
|
||||||
tTestWire = testWire.transformGeometry(self.fG)
|
|
||||||
|
|
||||||
# These bounding boxes will have z=0, since we transformed them into the
|
|
||||||
# space of the plane.
|
|
||||||
bb = tBaseWire.BoundingBox()
|
|
||||||
tb = tTestWire.BoundingBox()
|
|
||||||
|
|
||||||
# findOutsideBox actually inspects both ways, here we only want to
|
|
||||||
# know if one is inside the other
|
|
||||||
return bb == BoundBox.findOutsideBox2D(bb, tb)
|
|
||||||
|
|
||||||
def toLocalCoords(self, obj):
|
|
||||||
"""Project the provided coordinates onto this plane
|
|
||||||
|
|
||||||
:param obj: an object or vector to convert
|
|
||||||
:type vector: a vector or shape
|
|
||||||
:return: an object of the same type, but converted to local coordinates
|
|
||||||
|
|
||||||
|
|
||||||
Most of the time, the z-coordinate returned will be zero, because most
|
|
||||||
operations based on a plane are all 2-d. Occasionally, though, 3-d
|
|
||||||
points outside of the current plane are transformed. One such example is
|
|
||||||
:py:meth:`Workplane.box`, where 3-d corners of a box are transformed to
|
|
||||||
orient the box in space correctly.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isinstance(obj, Vector):
|
|
||||||
return Vector(self.fG.multiply(obj.wrapped))
|
|
||||||
elif isinstance(obj, cadquery.Shape):
|
|
||||||
return obj.transformShape(self.rG)
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
"Don't know how to convert type {} to local coordinates".format(
|
|
||||||
type(obj)))
|
|
||||||
|
|
||||||
def toWorldCoords(self, tuplePoint):
|
|
||||||
"""Convert a point in local coordinates to global coordinates
|
|
||||||
|
|
||||||
:param tuplePoint: point in local coordinates to convert.
|
|
||||||
:type tuplePoint: a 2 or three tuple of float. The third value is taken to be zero if not supplied.
|
|
||||||
:return: a Vector in global coordinates
|
|
||||||
"""
|
|
||||||
if isinstance(tuplePoint, Vector):
|
|
||||||
v = tuplePoint
|
|
||||||
elif len(tuplePoint) == 2:
|
|
||||||
v = Vector(tuplePoint[0], tuplePoint[1], 0)
|
|
||||||
else:
|
|
||||||
v = Vector(tuplePoint)
|
|
||||||
return Vector(self.rG.multiply(v.wrapped))
|
|
||||||
|
|
||||||
def rotated(self, rotate=(0, 0, 0)):
|
|
||||||
"""Returns a copy of this plane, rotated about the specified axes
|
|
||||||
|
|
||||||
Since the z axis is always normal the plane, rotating around Z will
|
|
||||||
always produce a plane that is parallel to this one.
|
|
||||||
|
|
||||||
The origin of the workplane is unaffected by the rotation.
|
|
||||||
|
|
||||||
Rotations are done in order x, y, z. If you need a different order,
|
|
||||||
manually chain together multiple rotate() commands.
|
|
||||||
|
|
||||||
:param rotate: Vector [xDegrees, yDegrees, zDegrees]
|
|
||||||
:return: a copy of this plane rotated as requested.
|
|
||||||
"""
|
|
||||||
rotate = Vector(rotate)
|
|
||||||
# Convert to radians.
|
|
||||||
rotate = rotate.multiply(math.pi / 180.0)
|
|
||||||
|
|
||||||
# Compute rotation matrix.
|
|
||||||
m = FreeCAD.Base.Matrix()
|
|
||||||
m.rotateX(rotate.x)
|
|
||||||
m.rotateY(rotate.y)
|
|
||||||
m.rotateZ(rotate.z)
|
|
||||||
|
|
||||||
# Compute the new plane.
|
|
||||||
newXdir = Vector(m.multiply(self.xDir.wrapped))
|
|
||||||
newZdir = Vector(m.multiply(self.zDir.wrapped))
|
|
||||||
|
|
||||||
return Plane(self.origin, newXdir, newZdir)
|
|
||||||
|
|
||||||
def rotateShapes(self, listOfShapes, rotationMatrix):
|
|
||||||
"""Rotate the listOfShapes by the supplied rotationMatrix
|
|
||||||
|
|
||||||
@param listOfShapes is a list of shape objects
|
|
||||||
@param rotationMatrix is a geom.Matrix object.
|
|
||||||
returns a list of shape objects rotated according to the rotationMatrix.
|
|
||||||
"""
|
|
||||||
# Compute rotation matrix (global --> local --> rotate --> global).
|
|
||||||
# rm = self.plane.fG.multiply(matrix).multiply(self.plane.rG)
|
|
||||||
# rm = self.computeTransform(rotationMatrix)
|
|
||||||
|
|
||||||
# There might be a better way, but to do this rotation takes 3 steps:
|
|
||||||
# - transform geometry to local coordinates
|
|
||||||
# - then rotate about x
|
|
||||||
# - then transform back to global coordinates.
|
|
||||||
|
|
||||||
resultWires = []
|
|
||||||
for w in listOfShapes:
|
|
||||||
mirrored = w.transformGeometry(rotationMatrix.wrapped)
|
|
||||||
|
|
||||||
# If the first vertex of the second wire is not coincident with the
|
|
||||||
# first or last vertices of the first wire we have to fix the wire
|
|
||||||
# so that it will mirror correctly.
|
|
||||||
if ((mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[0].X and
|
|
||||||
mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[0].Y and
|
|
||||||
mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[0].Z) or
|
|
||||||
(mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[-1].X and
|
|
||||||
mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[-1].Y and
|
|
||||||
mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[-1].Z)):
|
|
||||||
|
|
||||||
resultWires.append(mirrored)
|
|
||||||
else:
|
|
||||||
# Make sure that our mirrored edges meet up and are ordered
|
|
||||||
# properly.
|
|
||||||
aEdges = w.wrapped.Edges
|
|
||||||
aEdges.extend(mirrored.wrapped.Edges)
|
|
||||||
comp = FreeCADPart.Compound(aEdges)
|
|
||||||
mirroredWire = comp.connectEdgesToWires(False).Wires[0]
|
|
||||||
|
|
||||||
resultWires.append(cadquery.Shape.cast(mirroredWire))
|
|
||||||
|
|
||||||
return resultWires
|
|
||||||
|
|
||||||
def _setPlaneDir(self, xDir):
|
|
||||||
"""Set the vectors parallel to the plane, i.e. xDir and yDir"""
|
|
||||||
if (self.zDir.dot(xDir) > 1e-5):
|
|
||||||
raise ValueError('xDir must be parralel to the plane')
|
|
||||||
xDir = Vector(xDir)
|
|
||||||
self.xDir = xDir.normalized()
|
|
||||||
self.yDir = self.zDir.cross(self.xDir).normalized()
|
|
||||||
|
|
||||||
def _calcTransforms(self):
|
|
||||||
"""Computes transformation matrices to convert between coordinates
|
|
||||||
|
|
||||||
Computes transformation matrices to convert between local and global
|
|
||||||
coordinates.
|
|
||||||
"""
|
|
||||||
# r is the forward transformation matrix from world to local coordinates
|
|
||||||
# ok i will be really honest, i cannot understand exactly why this works
|
|
||||||
# something bout the order of the translation and the rotation.
|
|
||||||
# the double-inverting is strange, and I don't understand it.
|
|
||||||
r = FreeCAD.Base.Matrix()
|
|
||||||
|
|
||||||
# Forward transform must rotate and adjust for origin.
|
|
||||||
(r.A11, r.A12, r.A13) = (self.xDir.x, self.xDir.y, self.xDir.z)
|
|
||||||
(r.A21, r.A22, r.A23) = (self.yDir.x, self.yDir.y, self.yDir.z)
|
|
||||||
(r.A31, r.A32, r.A33) = (self.zDir.x, self.zDir.y, self.zDir.z)
|
|
||||||
|
|
||||||
invR = r.inverse()
|
|
||||||
invR.A14 = self.origin.x
|
|
||||||
invR.A24 = self.origin.y
|
|
||||||
invR.A34 = self.origin.z
|
|
||||||
|
|
||||||
self.rG = invR
|
|
||||||
self.fG = invR.inverse()
|
|
||||||
|
|
||||||
def computeTransform(self, tMatrix):
|
|
||||||
"""Computes the 2-d projection of the supplied matrix"""
|
|
||||||
|
|
||||||
return Matrix(self.fG.multiply(tMatrix.wrapped).multiply(self.rG))
|
|
||||||
|
|
||||||
|
|
||||||
class BoundBox(object):
|
|
||||||
"""A BoundingBox for an object or set of objects. Wraps the FreeCAD one"""
|
|
||||||
def __init__(self, bb):
|
|
||||||
self.wrapped = bb
|
|
||||||
self.xmin = bb.XMin
|
|
||||||
self.xmax = bb.XMax
|
|
||||||
self.xlen = bb.XLength
|
|
||||||
self.ymin = bb.YMin
|
|
||||||
self.ymax = bb.YMax
|
|
||||||
self.ylen = bb.YLength
|
|
||||||
self.zmin = bb.ZMin
|
|
||||||
self.zmax = bb.ZMax
|
|
||||||
self.zlen = bb.ZLength
|
|
||||||
self.center = Vector(bb.Center)
|
|
||||||
self.DiagonalLength = bb.DiagonalLength
|
|
||||||
|
|
||||||
def add(self, obj):
|
|
||||||
"""Returns a modified (expanded) bounding box
|
|
||||||
|
|
||||||
obj can be one of several things:
|
|
||||||
1. a 3-tuple corresponding to x,y, and z amounts to add
|
|
||||||
2. a vector, containing the x,y,z values to add
|
|
||||||
3. another bounding box, where a new box will be created that
|
|
||||||
encloses both.
|
|
||||||
|
|
||||||
This bounding box is not changed.
|
|
||||||
"""
|
|
||||||
tmp = FreeCAD.Base.BoundBox(self.wrapped)
|
|
||||||
if isinstance(obj, tuple):
|
|
||||||
tmp.add(obj[0], obj[1], obj[2])
|
|
||||||
elif isinstance(obj, Vector):
|
|
||||||
tmp.add(obj.fV)
|
|
||||||
elif isinstance(obj, BoundBox):
|
|
||||||
tmp.add(obj.wrapped)
|
|
||||||
|
|
||||||
return BoundBox(tmp)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def findOutsideBox2D(cls, b1, b2):
|
|
||||||
"""Compares bounding boxes
|
|
||||||
|
|
||||||
Compares bounding boxes. Returns none if neither is inside the other.
|
|
||||||
Returns the outer one if either is outside the other.
|
|
||||||
|
|
||||||
BoundBox.isInside works in 3d, but this is a 2d bounding box, so it
|
|
||||||
doesn't work correctly plus, there was all kinds of rounding error in
|
|
||||||
the built-in implementation i do not understand.
|
|
||||||
"""
|
|
||||||
fc_bb1 = b1.wrapped
|
|
||||||
fc_bb2 = b2.wrapped
|
|
||||||
if (fc_bb1.XMin < fc_bb2.XMin and
|
|
||||||
fc_bb1.XMax > fc_bb2.XMax and
|
|
||||||
fc_bb1.YMin < fc_bb2.YMin and
|
|
||||||
fc_bb1.YMax > fc_bb2.YMax):
|
|
||||||
return b1
|
|
||||||
|
|
||||||
if (fc_bb2.XMin < fc_bb1.XMin and
|
|
||||||
fc_bb2.XMax > fc_bb1.XMax and
|
|
||||||
fc_bb2.YMin < fc_bb1.YMin and
|
|
||||||
fc_bb2.YMax > fc_bb1.YMax):
|
|
||||||
return b2
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def isInside(self, anotherBox):
|
|
||||||
"""Is the provided bounding box inside this one?"""
|
|
||||||
return self.wrapped.isInside(anotherBox.wrapped)
|
|
|
@ -1,71 +0,0 @@
|
||||||
|
|
||||||
import cadquery
|
|
||||||
from .shapes import Shape
|
|
||||||
|
|
||||||
import FreeCAD
|
|
||||||
import Part
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import urllib as urlreader
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
class ImportTypes:
|
|
||||||
STEP = "STEP"
|
|
||||||
|
|
||||||
class UNITS:
|
|
||||||
MM = "mm"
|
|
||||||
IN = "in"
|
|
||||||
|
|
||||||
|
|
||||||
def importShape(importType, fileName):
|
|
||||||
"""
|
|
||||||
Imports a file based on the type (STEP, STL, etc)
|
|
||||||
:param importType: The type of file that we're importing
|
|
||||||
:param fileName: THe name of the file that we're importing
|
|
||||||
"""
|
|
||||||
|
|
||||||
#Check to see what type of file we're working with
|
|
||||||
if importType == ImportTypes.STEP:
|
|
||||||
return importStep(fileName)
|
|
||||||
|
|
||||||
|
|
||||||
#Loads a STEP file into a CQ.Workplane object
|
|
||||||
def importStep(fileName):
|
|
||||||
"""
|
|
||||||
Accepts a file name and loads the STEP file into a cadquery shape
|
|
||||||
:param fileName: The path and name of the STEP file to be imported
|
|
||||||
"""
|
|
||||||
#Now read and return the shape
|
|
||||||
try:
|
|
||||||
#print fileName
|
|
||||||
rshape = Part.read(fileName)
|
|
||||||
|
|
||||||
#Make sure that we extract all the solids
|
|
||||||
solids = []
|
|
||||||
for solid in rshape.Solids:
|
|
||||||
solids.append(Shape.cast(solid))
|
|
||||||
|
|
||||||
return cadquery.Workplane("XY").newObject(solids)
|
|
||||||
except:
|
|
||||||
raise ValueError("STEP File Could not be loaded")
|
|
||||||
|
|
||||||
#Loads a STEP file from an URL into a CQ.Workplane object
|
|
||||||
def importStepFromURL(url):
|
|
||||||
#Now read and return the shape
|
|
||||||
try:
|
|
||||||
webFile = urlreader.urlopen(url)
|
|
||||||
tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False)
|
|
||||||
tempFile.write(webFile.read())
|
|
||||||
webFile.close()
|
|
||||||
tempFile.close()
|
|
||||||
|
|
||||||
rshape = Part.read(tempFile.name)
|
|
||||||
|
|
||||||
#Make sure that we extract all the solids
|
|
||||||
solids = []
|
|
||||||
for solid in rshape.Solids:
|
|
||||||
solids.append(Shape.cast(solid))
|
|
||||||
|
|
||||||
return cadquery.Workplane("XY").newObject(solids)
|
|
||||||
except:
|
|
||||||
raise ValueError("STEP File from the URL: " + url + " Could not be loaded")
|
|
|
@ -1,982 +0,0 @@
|
||||||
"""
|
|
||||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
|
||||||
|
|
||||||
This file is part of CadQuery.
|
|
||||||
|
|
||||||
CadQuery is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
CadQuery is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
Wrapper Classes for FreeCAD
|
|
||||||
These classes provide a stable interface for 3d objects,
|
|
||||||
independent of the FreeCAD interface.
|
|
||||||
|
|
||||||
Future work might include use of pythonOCC, OCC, or even
|
|
||||||
another CAD kernel directly, so this interface layer is quite important.
|
|
||||||
|
|
||||||
Funny, in java this is one of those few areas where i'd actually spend the time
|
|
||||||
to make an interface and an implementation, but for new these are just rolled together
|
|
||||||
|
|
||||||
This interface layer provides three distinct values:
|
|
||||||
|
|
||||||
1. It allows us to avoid changing key api points if we change underlying implementations.
|
|
||||||
It would be a disaster if script and plugin authors had to change models because we
|
|
||||||
changed implementations
|
|
||||||
|
|
||||||
2. Allow better documentation. One of the reasons FreeCAD is no more popular is because
|
|
||||||
its docs are terrible. This allows us to provide good documentation via docstrings
|
|
||||||
for each wrapper
|
|
||||||
|
|
||||||
3. Work around bugs. there are a quite a feb bugs in free this layer allows fixing them
|
|
||||||
|
|
||||||
4. allows for enhanced functionality. Many objects are missing features we need. For example
|
|
||||||
we need a 'forConstruction' flag on the Wire object. this allows adding those kinds of things
|
|
||||||
|
|
||||||
5. allow changing interfaces when we'd like. there are few cases where the FreeCAD api is not
|
|
||||||
very user friendly: we like to change those when necessary. As an example, in the FreeCAD api,
|
|
||||||
all factory methods are on the 'Part' object, but it is very useful to know what kind of
|
|
||||||
object each one returns, so these are better grouped by the type of object they return.
|
|
||||||
(who would know that Part.makeCircle() returns an Edge, but Part.makePolygon() returns a Wire ?
|
|
||||||
"""
|
|
||||||
from cadquery import Vector, BoundBox
|
|
||||||
import FreeCAD
|
|
||||||
import Part as FreeCADPart
|
|
||||||
|
|
||||||
|
|
||||||
class Shape(object):
|
|
||||||
"""
|
|
||||||
Represents a shape in the system.
|
|
||||||
Wrappers the FreeCAD api
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, obj):
|
|
||||||
self.wrapped = obj
|
|
||||||
self.forConstruction = False
|
|
||||||
|
|
||||||
# Helps identify this solid through the use of an ID
|
|
||||||
self.label = ""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def cast(cls, obj, forConstruction=False):
|
|
||||||
"Returns the right type of wrapper, given a FreeCAD object"
|
|
||||||
s = obj.ShapeType
|
|
||||||
if type(obj) == FreeCAD.Base.Vector:
|
|
||||||
return Vector(obj)
|
|
||||||
tr = None
|
|
||||||
|
|
||||||
# TODO: there is a clever way to do this i'm sure with a lookup
|
|
||||||
# but it is not a perfect mapping, because we are trying to hide
|
|
||||||
# a bit of the complexity of Compounds in FreeCAD.
|
|
||||||
if s == 'Vertex':
|
|
||||||
tr = Vertex(obj)
|
|
||||||
elif s == 'Edge':
|
|
||||||
tr = Edge(obj)
|
|
||||||
elif s == 'Wire':
|
|
||||||
tr = Wire(obj)
|
|
||||||
elif s == 'Face':
|
|
||||||
tr = Face(obj)
|
|
||||||
elif s == 'Shell':
|
|
||||||
tr = Shell(obj)
|
|
||||||
elif s == 'Solid':
|
|
||||||
tr = Solid(obj)
|
|
||||||
elif s == 'Compound':
|
|
||||||
#compound of solids, lets return a solid instead
|
|
||||||
if len(obj.Solids) > 1:
|
|
||||||
tr = Solid(obj)
|
|
||||||
elif len(obj.Solids) == 1:
|
|
||||||
tr = Solid(obj.Solids[0])
|
|
||||||
elif len(obj.Wires) > 0:
|
|
||||||
tr = Wire(obj)
|
|
||||||
else:
|
|
||||||
tr = Compound(obj)
|
|
||||||
else:
|
|
||||||
raise ValueError("cast:unknown shape type %s" % s)
|
|
||||||
|
|
||||||
tr.forConstruction = forConstruction
|
|
||||||
return tr
|
|
||||||
|
|
||||||
# TODO: all these should move into the exporters folder.
|
|
||||||
# we dont need a bunch of exporting code stored in here!
|
|
||||||
#
|
|
||||||
def exportStl(self, fileName):
|
|
||||||
self.wrapped.exportStl(fileName)
|
|
||||||
|
|
||||||
def exportStep(self, fileName):
|
|
||||||
self.wrapped.exportStep(fileName)
|
|
||||||
|
|
||||||
def exportShape(self, fileName, fileFormat):
|
|
||||||
if fileFormat == ExportFormats.STL:
|
|
||||||
self.wrapped.exportStl(fileName)
|
|
||||||
elif fileFormat == ExportFormats.BREP:
|
|
||||||
self.wrapped.exportBrep(fileName)
|
|
||||||
elif fileFormat == ExportFormats.STEP:
|
|
||||||
self.wrapped.exportStep(fileName)
|
|
||||||
elif fileFormat == ExportFormats.AMF:
|
|
||||||
# not built into FreeCAD
|
|
||||||
#TODO: user selected tolerance
|
|
||||||
tess = self.wrapped.tessellate(0.1)
|
|
||||||
aw = amfUtils.AMFWriter(tess)
|
|
||||||
aw.writeAmf(fileName)
|
|
||||||
elif fileFormat == ExportFormats.IGES:
|
|
||||||
self.wrapped.exportIges(fileName)
|
|
||||||
else:
|
|
||||||
raise ValueError("Unknown export format: %s" % format)
|
|
||||||
|
|
||||||
def geomType(self):
|
|
||||||
"""
|
|
||||||
Gets the underlying geometry type
|
|
||||||
:return: a string according to the geometry type.
|
|
||||||
|
|
||||||
Implementations can return any values desired, but the
|
|
||||||
values the user uses in type filters should correspond to these.
|
|
||||||
|
|
||||||
As an example, if a user does::
|
|
||||||
|
|
||||||
CQ(object).faces("%mytype")
|
|
||||||
|
|
||||||
The expectation is that the geomType attribute will return 'mytype'
|
|
||||||
|
|
||||||
The return values depend on the type of the shape:
|
|
||||||
|
|
||||||
Vertex: always 'Vertex'
|
|
||||||
Edge: LINE, ARC, CIRCLE, SPLINE
|
|
||||||
Face: PLANE, SPHERE, CONE
|
|
||||||
Solid: 'Solid'
|
|
||||||
Shell: 'Shell'
|
|
||||||
Compound: 'Compound'
|
|
||||||
Wire: 'Wire'
|
|
||||||
"""
|
|
||||||
return self.wrapped.ShapeType
|
|
||||||
|
|
||||||
def isType(self, obj, strType):
|
|
||||||
"""
|
|
||||||
Returns True if the shape is the specified type, false otherwise
|
|
||||||
|
|
||||||
contrast with ShapeType, which will raise an exception
|
|
||||||
if the provide object is not a shape at all
|
|
||||||
"""
|
|
||||||
if hasattr(obj, 'ShapeType'):
|
|
||||||
return obj.ShapeType == strType
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def hashCode(self):
|
|
||||||
return self.wrapped.hashCode()
|
|
||||||
|
|
||||||
def isNull(self):
|
|
||||||
return self.wrapped.isNull()
|
|
||||||
|
|
||||||
def isSame(self, other):
|
|
||||||
return self.wrapped.isSame(other.wrapped)
|
|
||||||
|
|
||||||
def isEqual(self, other):
|
|
||||||
return self.wrapped.isEqual(other.wrapped)
|
|
||||||
|
|
||||||
def isValid(self):
|
|
||||||
return self.wrapped.isValid()
|
|
||||||
|
|
||||||
def BoundingBox(self):
|
|
||||||
return BoundBox(self.wrapped.BoundBox)
|
|
||||||
|
|
||||||
def Center(self):
|
|
||||||
# A Part.Shape object doesn't have the CenterOfMass function, but it's wrapped Solid(s) does
|
|
||||||
if isinstance(self.wrapped, FreeCADPart.Shape):
|
|
||||||
# If there are no Solids, we're probably dealing with a Face or something similar
|
|
||||||
if len(self.Solids()) == 0:
|
|
||||||
return Vector(self.wrapped.CenterOfMass)
|
|
||||||
elif len(self.Solids()) == 1:
|
|
||||||
return Vector(self.Solids()[0].wrapped.CenterOfMass)
|
|
||||||
elif len(self.Solids()) > 1:
|
|
||||||
return self.CombinedCenter(self.Solids())
|
|
||||||
elif isinstance(self.wrapped, FreeCADPart.Solid):
|
|
||||||
return Vector(self.wrapped.CenterOfMass)
|
|
||||||
else:
|
|
||||||
raise ValueError("Cannot find the center of %s object type" % str(type(self.Solids()[0].wrapped)))
|
|
||||||
|
|
||||||
def CenterOfBoundBox(self):
|
|
||||||
if isinstance(self.wrapped, FreeCADPart.Shape):
|
|
||||||
# If there are no Solids, we're probably dealing with a Face or something similar
|
|
||||||
if len(self.Solids()) == 0:
|
|
||||||
return Vector(self.wrapped.BoundBox.Center)
|
|
||||||
elif len(self.Solids()) == 1:
|
|
||||||
return Vector(self.Solids()[0].wrapped.BoundBox.Center)
|
|
||||||
elif len(self.Solids()) > 1:
|
|
||||||
return self.CombinedCenterOfBoundBox(self.Solids())
|
|
||||||
elif isinstance(self.wrapped, FreeCADPart.Solid):
|
|
||||||
return Vector(self.wrapped.BoundBox.Center)
|
|
||||||
else:
|
|
||||||
raise ValueError("Cannot find the center(BoundBox's) of %s object type" % str(type(self.Solids()[0].wrapped)))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def CombinedCenter(objects):
|
|
||||||
"""
|
|
||||||
Calculates the center of mass of multiple objects.
|
|
||||||
|
|
||||||
:param objects: a list of objects with mass
|
|
||||||
"""
|
|
||||||
total_mass = sum(o.wrapped.Mass for o in objects)
|
|
||||||
weighted_centers = [o.wrapped.CenterOfMass.multiply(o.wrapped.Mass) for o in objects]
|
|
||||||
|
|
||||||
sum_wc = weighted_centers[0]
|
|
||||||
for wc in weighted_centers[1:] :
|
|
||||||
sum_wc = sum_wc.add(wc)
|
|
||||||
|
|
||||||
return Vector(sum_wc.multiply(1./total_mass))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def CombinedCenterOfBoundBox(objects):
|
|
||||||
"""
|
|
||||||
Calculates the center of BoundBox of multiple objects.
|
|
||||||
|
|
||||||
:param objects: a list of objects with mass 1
|
|
||||||
"""
|
|
||||||
total_mass = len(objects)
|
|
||||||
weighted_centers = [o.wrapped.BoundBox.Center.multiply(1.0) for o in objects]
|
|
||||||
|
|
||||||
sum_wc = weighted_centers[0]
|
|
||||||
for wc in weighted_centers[1:] :
|
|
||||||
sum_wc = sum_wc.add(wc)
|
|
||||||
|
|
||||||
return Vector(sum_wc.multiply(1./total_mass))
|
|
||||||
|
|
||||||
def Closed(self):
|
|
||||||
return self.wrapped.Closed
|
|
||||||
|
|
||||||
def ShapeType(self):
|
|
||||||
return self.wrapped.ShapeType
|
|
||||||
|
|
||||||
def Vertices(self):
|
|
||||||
return [Vertex(i) for i in self.wrapped.Vertexes]
|
|
||||||
|
|
||||||
def Edges(self):
|
|
||||||
return [Edge(i) for i in self.wrapped.Edges]
|
|
||||||
|
|
||||||
def Compounds(self):
|
|
||||||
return [Compound(i) for i in self.wrapped.Compounds]
|
|
||||||
|
|
||||||
def Wires(self):
|
|
||||||
return [Wire(i) for i in self.wrapped.Wires]
|
|
||||||
|
|
||||||
def Faces(self):
|
|
||||||
return [Face(i) for i in self.wrapped.Faces]
|
|
||||||
|
|
||||||
def Shells(self):
|
|
||||||
return [Shell(i) for i in self.wrapped.Shells]
|
|
||||||
|
|
||||||
def Solids(self):
|
|
||||||
return [Solid(i) for i in self.wrapped.Solids]
|
|
||||||
|
|
||||||
def Area(self):
|
|
||||||
return self.wrapped.Area
|
|
||||||
|
|
||||||
def Length(self):
|
|
||||||
return self.wrapped.Length
|
|
||||||
|
|
||||||
def rotate(self, startVector, endVector, angleDegrees):
|
|
||||||
"""
|
|
||||||
Rotates a shape around an axis
|
|
||||||
:param startVector: start point of rotation axis either a 3-tuple or a Vector
|
|
||||||
:param endVector: end point of rotation axis, either a 3-tuple or a Vector
|
|
||||||
:param angleDegrees: angle to rotate, in degrees
|
|
||||||
:return: a copy of the shape, rotated
|
|
||||||
"""
|
|
||||||
if type(startVector) == tuple:
|
|
||||||
startVector = Vector(startVector)
|
|
||||||
|
|
||||||
if type(endVector) == tuple:
|
|
||||||
endVector = Vector(endVector)
|
|
||||||
|
|
||||||
tmp = self.wrapped.copy()
|
|
||||||
tmp.rotate(startVector.wrapped, endVector.wrapped, angleDegrees)
|
|
||||||
return Shape.cast(tmp)
|
|
||||||
|
|
||||||
def translate(self, vector):
|
|
||||||
|
|
||||||
if type(vector) == tuple:
|
|
||||||
vector = Vector(vector)
|
|
||||||
tmp = self.wrapped.copy()
|
|
||||||
tmp.translate(vector.wrapped)
|
|
||||||
return Shape.cast(tmp)
|
|
||||||
|
|
||||||
def scale(self, factor):
|
|
||||||
tmp = self.wrapped.copy()
|
|
||||||
tmp.scale(factor)
|
|
||||||
return Shape.cast(tmp)
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return Shape.cast(self.wrapped.copy())
|
|
||||||
|
|
||||||
def transformShape(self, tMatrix):
|
|
||||||
"""
|
|
||||||
tMatrix is a matrix object.
|
|
||||||
returns a copy of the ojbect, transformed by the provided matrix,
|
|
||||||
with all objects keeping their type
|
|
||||||
"""
|
|
||||||
tmp = self.wrapped.copy()
|
|
||||||
tmp.transformShape(tMatrix)
|
|
||||||
r = Shape.cast(tmp)
|
|
||||||
r.forConstruction = self.forConstruction
|
|
||||||
return r
|
|
||||||
|
|
||||||
def transformGeometry(self, tMatrix):
|
|
||||||
"""
|
|
||||||
tMatrix is a matrix object.
|
|
||||||
|
|
||||||
returns a copy of the object, but with geometry transformed insetad of just
|
|
||||||
rotated.
|
|
||||||
|
|
||||||
WARNING: transformGeometry will sometimes convert lines and circles to splines,
|
|
||||||
but it also has the ability to handle skew and stretching transformations.
|
|
||||||
|
|
||||||
If your transformation is only translation and rotation, it is safer to use transformShape,
|
|
||||||
which doesnt change the underlying type of the geometry, but cannot handle skew transformations
|
|
||||||
"""
|
|
||||||
tmp = self.wrapped.copy()
|
|
||||||
tmp = tmp.transformGeometry(tMatrix)
|
|
||||||
return Shape.cast(tmp)
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return self.wrapped.hashCode()
|
|
||||||
|
|
||||||
|
|
||||||
class Vertex(Shape):
|
|
||||||
"""
|
|
||||||
A Single Point in Space
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, obj, forConstruction=False):
|
|
||||||
"""
|
|
||||||
Create a vertex from a FreeCAD Vertex
|
|
||||||
"""
|
|
||||||
self.wrapped = obj
|
|
||||||
self.forConstruction = forConstruction
|
|
||||||
self.X = obj.X
|
|
||||||
self.Y = obj.Y
|
|
||||||
self.Z = obj.Z
|
|
||||||
|
|
||||||
# Helps identify this solid through the use of an ID
|
|
||||||
self.label = ""
|
|
||||||
|
|
||||||
def toTuple(self):
|
|
||||||
return (self.X, self.Y, self.Z)
|
|
||||||
|
|
||||||
def Center(self):
|
|
||||||
"""
|
|
||||||
The center of a vertex is itself!
|
|
||||||
"""
|
|
||||||
return Vector(self.wrapped.Point)
|
|
||||||
|
|
||||||
|
|
||||||
class Edge(Shape):
|
|
||||||
"""
|
|
||||||
A trimmed curve that represents the border of a face
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, obj):
|
|
||||||
"""
|
|
||||||
An Edge
|
|
||||||
"""
|
|
||||||
self.wrapped = obj
|
|
||||||
# self.startPoint = None
|
|
||||||
# self.endPoint = None
|
|
||||||
|
|
||||||
self.edgetypes = {
|
|
||||||
FreeCADPart.Line: 'LINE',
|
|
||||||
FreeCADPart.ArcOfCircle: 'ARC',
|
|
||||||
FreeCADPart.Circle: 'CIRCLE'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helps identify this solid through the use of an ID
|
|
||||||
self.label = ""
|
|
||||||
|
|
||||||
def geomType(self):
|
|
||||||
t = type(self.wrapped.Curve)
|
|
||||||
if self.edgetypes.has_key(t):
|
|
||||||
return self.edgetypes[t]
|
|
||||||
else:
|
|
||||||
return "Unknown Edge Curve Type: %s" % str(t)
|
|
||||||
|
|
||||||
def startPoint(self):
|
|
||||||
"""
|
|
||||||
|
|
||||||
:return: a vector representing the start poing of this edge
|
|
||||||
|
|
||||||
Note, circles may have the start and end points the same
|
|
||||||
"""
|
|
||||||
# work around freecad bug where valueAt is unreliable
|
|
||||||
curve = self.wrapped.Curve
|
|
||||||
return Vector(curve.value(self.wrapped.ParameterRange[0]))
|
|
||||||
|
|
||||||
def endPoint(self):
|
|
||||||
"""
|
|
||||||
|
|
||||||
:return: a vector representing the end point of this edge.
|
|
||||||
|
|
||||||
Note, circles may have the start and end points the same
|
|
||||||
|
|
||||||
"""
|
|
||||||
# warning: easier syntax in freecad of <Edge>.valueAt(<Edge>.ParameterRange[1]) has
|
|
||||||
# a bug with curves other than arcs, but using the underlying curve directly seems to work
|
|
||||||
# that's the solution i'm using below
|
|
||||||
curve = self.wrapped.Curve
|
|
||||||
v = Vector(curve.value(self.wrapped.ParameterRange[1]))
|
|
||||||
return v
|
|
||||||
|
|
||||||
def tangentAt(self, locationVector=None):
|
|
||||||
"""
|
|
||||||
Compute tangent vector at the specified location.
|
|
||||||
:param locationVector: location to use. Use the center point if None
|
|
||||||
:return: tangent vector
|
|
||||||
"""
|
|
||||||
if locationVector is None:
|
|
||||||
locationVector = self.Center()
|
|
||||||
|
|
||||||
p = self.wrapped.Curve.parameter(locationVector.wrapped)
|
|
||||||
return Vector(self.wrapped.tangentAt(p))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeCircle(cls, radius, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=360.0, angle2=360):
|
|
||||||
return Edge(FreeCADPart.makeCircle(radius, toVector(pnt), toVector(dir), angle1, angle2))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeSpline(cls, listOfVector):
|
|
||||||
"""
|
|
||||||
Interpolate a spline through the provided points.
|
|
||||||
:param cls:
|
|
||||||
:param listOfVector: a list of Vectors that represent the points
|
|
||||||
:return: an Edge
|
|
||||||
"""
|
|
||||||
vecs = [v.wrapped for v in listOfVector]
|
|
||||||
|
|
||||||
spline = FreeCADPart.BSplineCurve()
|
|
||||||
spline.interpolate(vecs, False)
|
|
||||||
return Edge(spline.toShape())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeThreePointArc(cls, v1, v2, v3):
|
|
||||||
"""
|
|
||||||
Makes a three point arc through the provided points
|
|
||||||
:param cls:
|
|
||||||
:param v1: start vector
|
|
||||||
:param v2: middle vector
|
|
||||||
:param v3: end vector
|
|
||||||
:return: an edge object through the three points
|
|
||||||
"""
|
|
||||||
arc = FreeCADPart.Arc(v1.wrapped, v2.wrapped, v3.wrapped)
|
|
||||||
e = Edge(arc.toShape())
|
|
||||||
return e # arcane and undocumented, this creates an Edge object
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeLine(cls, v1, v2):
|
|
||||||
"""
|
|
||||||
Create a line between two points
|
|
||||||
:param v1: Vector that represents the first point
|
|
||||||
:param v2: Vector that represents the second point
|
|
||||||
:return: A linear edge between the two provided points
|
|
||||||
"""
|
|
||||||
return Edge(FreeCADPart.makeLine(v1.toTuple(), v2.toTuple()))
|
|
||||||
|
|
||||||
|
|
||||||
class Wire(Shape):
|
|
||||||
"""
|
|
||||||
A series of connected, ordered Edges, that typically bounds a Face
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, obj):
|
|
||||||
"""
|
|
||||||
A Wire
|
|
||||||
"""
|
|
||||||
self.wrapped = obj
|
|
||||||
|
|
||||||
# Helps identify this solid through the use of an ID
|
|
||||||
self.label = ""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def combine(cls, listOfWires):
|
|
||||||
"""
|
|
||||||
Attempt to combine a list of wires into a new wire.
|
|
||||||
the wires are returned in a list.
|
|
||||||
:param cls:
|
|
||||||
:param listOfWires:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return Shape.cast(FreeCADPart.Wire([w.wrapped for w in listOfWires]))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def assembleEdges(cls, listOfEdges):
|
|
||||||
"""
|
|
||||||
Attempts to build a wire that consists of the edges in the provided list
|
|
||||||
:param cls:
|
|
||||||
:param listOfEdges: a list of Edge objects
|
|
||||||
:return: a wire with the edges assembled
|
|
||||||
"""
|
|
||||||
fCEdges = [a.wrapped for a in listOfEdges]
|
|
||||||
|
|
||||||
wa = Wire(FreeCADPart.Wire(fCEdges))
|
|
||||||
return wa
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeCircle(cls, radius, center, normal):
|
|
||||||
"""
|
|
||||||
Makes a Circle centered at the provided point, having normal in the provided direction
|
|
||||||
:param radius: floating point radius of the circle, must be > 0
|
|
||||||
:param center: vector representing the center of the circle
|
|
||||||
:param normal: vector representing the direction of the plane the circle should lie in
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
w = Wire(FreeCADPart.Wire([FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped)]))
|
|
||||||
return w
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makePolygon(cls, listOfVertices, forConstruction=False):
|
|
||||||
# convert list of tuples into Vectors.
|
|
||||||
w = Wire(FreeCADPart.makePolygon([i.wrapped for i in listOfVertices]))
|
|
||||||
w.forConstruction = forConstruction
|
|
||||||
return w
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeHelix(cls, pitch, height, radius, angle=360.0):
|
|
||||||
"""
|
|
||||||
Make a helix with a given pitch, height and radius
|
|
||||||
By default a cylindrical surface is used to create the helix. If
|
|
||||||
the fourth parameter is set (the apex given in degree) a conical surface is used instead'
|
|
||||||
"""
|
|
||||||
return Wire(FreeCADPart.makeHelix(pitch, height, radius, angle))
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
"""This method is not implemented yet."""
|
|
||||||
return self
|
|
||||||
|
|
||||||
class Face(Shape):
|
|
||||||
"""
|
|
||||||
a bounded surface that represents part of the boundary of a solid
|
|
||||||
"""
|
|
||||||
def __init__(self, obj):
|
|
||||||
|
|
||||||
self.wrapped = obj
|
|
||||||
|
|
||||||
self.facetypes = {
|
|
||||||
# TODO: bezier,bspline etc
|
|
||||||
FreeCADPart.Plane: 'PLANE',
|
|
||||||
FreeCADPart.Sphere: 'SPHERE',
|
|
||||||
FreeCADPart.Cone: 'CONE'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helps identify this solid through the use of an ID
|
|
||||||
self.label = ""
|
|
||||||
|
|
||||||
def geomType(self):
|
|
||||||
t = type(self.wrapped.Surface)
|
|
||||||
if self.facetypes.has_key(t):
|
|
||||||
return self.facetypes[t]
|
|
||||||
else:
|
|
||||||
return "Unknown Face Surface Type: %s" % str(t)
|
|
||||||
|
|
||||||
def normalAt(self, locationVector=None):
|
|
||||||
"""
|
|
||||||
Computes the normal vector at the desired location on the face.
|
|
||||||
|
|
||||||
:returns: a vector representing the direction
|
|
||||||
:param locationVector: the location to compute the normal at. If none, the center of the face is used.
|
|
||||||
:type locationVector: a vector that lies on the surface.
|
|
||||||
"""
|
|
||||||
if locationVector == None:
|
|
||||||
locationVector = self.Center()
|
|
||||||
(u, v) = self.wrapped.Surface.parameter(locationVector.wrapped)
|
|
||||||
|
|
||||||
return Vector(self.wrapped.normalAt(u, v).normalize())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makePlane(cls, length, width, basePnt=None, dir=None):
|
|
||||||
return Face(FreeCADPart.makePlan(length, width, toVector(basePnt), toVector(dir)))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeRuledSurface(cls, edgeOrWire1, edgeOrWire2, dist=None):
|
|
||||||
"""
|
|
||||||
'makeRuledSurface(Edge|Wire,Edge|Wire) -- Make a ruled surface
|
|
||||||
Create a ruled surface out of two edges or wires. If wires are used then
|
|
||||||
these must have the same
|
|
||||||
"""
|
|
||||||
return Shape.cast(FreeCADPart.makeRuledSurface(edgeOrWire1.obj, edgeOrWire2.obj, dist))
|
|
||||||
|
|
||||||
def cut(self, faceToCut):
|
|
||||||
"Remove a face from another one"
|
|
||||||
return Shape.cast(self.obj.cut(faceToCut.obj))
|
|
||||||
|
|
||||||
def fuse(self, faceToJoin):
|
|
||||||
return Shape.cast(self.obj.fuse(faceToJoin.obj))
|
|
||||||
|
|
||||||
def intersect(self, faceToIntersect):
|
|
||||||
"""
|
|
||||||
computes the intersection between the face and the supplied one.
|
|
||||||
The result could be a face or a compound of faces
|
|
||||||
"""
|
|
||||||
return Shape.cast(self.obj.common(faceToIntersect.obj))
|
|
||||||
|
|
||||||
|
|
||||||
class Shell(Shape):
|
|
||||||
"""
|
|
||||||
the outer boundary of a surface
|
|
||||||
"""
|
|
||||||
def __init__(self, wrapped):
|
|
||||||
"""
|
|
||||||
A Shell
|
|
||||||
"""
|
|
||||||
self.wrapped = wrapped
|
|
||||||
|
|
||||||
# Helps identify this solid through the use of an ID
|
|
||||||
self.label = ""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeShell(cls, listOfFaces):
|
|
||||||
return Shell(FreeCADPart.makeShell([i.obj for i in listOfFaces]))
|
|
||||||
|
|
||||||
|
|
||||||
class Solid(Shape):
|
|
||||||
"""
|
|
||||||
a single solid
|
|
||||||
"""
|
|
||||||
def __init__(self, obj):
|
|
||||||
"""
|
|
||||||
A Solid
|
|
||||||
"""
|
|
||||||
self.wrapped = obj
|
|
||||||
|
|
||||||
# Helps identify this solid through the use of an ID
|
|
||||||
self.label = ""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def isSolid(cls, obj):
|
|
||||||
"""
|
|
||||||
Returns true if the object is a FreeCAD solid, false otherwise
|
|
||||||
"""
|
|
||||||
if hasattr(obj, 'ShapeType'):
|
|
||||||
if obj.ShapeType == 'Solid' or \
|
|
||||||
(obj.ShapeType == 'Compound' and len(obj.Solids) > 0):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeBox(cls, length, width, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)):
|
|
||||||
"""
|
|
||||||
makeBox(length,width,height,[pnt,dir]) -- Make a box located in pnt with the dimensions (length,width,height)
|
|
||||||
By default pnt=Vector(0,0,0) and dir=Vector(0,0,1)'
|
|
||||||
"""
|
|
||||||
return Shape.cast(FreeCADPart.makeBox(length, width, height, pnt.wrapped, dir.wrapped))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeCone(cls, radius1, radius2, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360):
|
|
||||||
"""
|
|
||||||
Make a cone with given radii and height
|
|
||||||
By default pnt=Vector(0,0,0),
|
|
||||||
dir=Vector(0,0,1) and angle=360'
|
|
||||||
"""
|
|
||||||
return Shape.cast(FreeCADPart.makeCone(radius1, radius2, height, pnt.wrapped, dir.wrapped, angleDegrees))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeCylinder(cls, radius, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360):
|
|
||||||
"""
|
|
||||||
makeCylinder(radius,height,[pnt,dir,angle]) --
|
|
||||||
Make a cylinder with a given radius and height
|
|
||||||
By default pnt=Vector(0,0,0),dir=Vector(0,0,1) and angle=360'
|
|
||||||
"""
|
|
||||||
return Shape.cast(FreeCADPart.makeCylinder(radius, height, pnt.wrapped, dir.wrapped, angleDegrees))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeTorus(cls, radius1, radius2, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None):
|
|
||||||
"""
|
|
||||||
makeTorus(radius1,radius2,[pnt,dir,angle1,angle2,angle]) --
|
|
||||||
Make a torus with agiven radii and angles
|
|
||||||
By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0
|
|
||||||
,angle1=360 and angle=360'
|
|
||||||
"""
|
|
||||||
return Shape.cast(FreeCADPart.makeTorus(radius1, radius2, pnt, dir, angleDegrees1, angleDegrees2))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def sweep(cls, profileWire, pathWire):
|
|
||||||
"""
|
|
||||||
make a solid by sweeping the profileWire along the specified path
|
|
||||||
:param cls:
|
|
||||||
:param profileWire:
|
|
||||||
:param pathWire:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
# needs to use freecad wire.makePipe or makePipeShell
|
|
||||||
# needs to allow free-space wires ( those not made from a workplane )
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeLoft(cls, listOfWire, ruled=False):
|
|
||||||
"""
|
|
||||||
makes a loft from a list of wires
|
|
||||||
The wires will be converted into faces when possible-- it is presumed that nobody ever actually
|
|
||||||
wants to make an infinitely thin shell for a real FreeCADPart.
|
|
||||||
"""
|
|
||||||
# the True flag requests building a solid instead of a shell.
|
|
||||||
|
|
||||||
return Shape.cast(FreeCADPart.makeLoft([i.wrapped for i in listOfWire], True, ruled))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=None, dir=None):
|
|
||||||
"""
|
|
||||||
Make a wedge located in pnt
|
|
||||||
By default pnt=Vector(0,0,0) and dir=Vector(0,0,1)
|
|
||||||
"""
|
|
||||||
return Shape.cast(
|
|
||||||
FreeCADPart.makeWedge(xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt, dir))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeSphere(cls, radius, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None, angleDegrees3=None):
|
|
||||||
"""
|
|
||||||
Make a sphere with a given radius
|
|
||||||
By default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360
|
|
||||||
"""
|
|
||||||
return Shape.cast(FreeCADPart.makeSphere(radius, pnt.wrapped, dir.wrapped, angleDegrees1, angleDegrees2, angleDegrees3))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def extrudeLinearWithRotation(cls, outerWire, innerWires, vecCenter, vecNormal, angleDegrees):
|
|
||||||
"""
|
|
||||||
Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector.
|
|
||||||
|
|
||||||
Though the signature may appear to be similar enough to extrudeLinear to merit combining them, the
|
|
||||||
construction methods used here are different enough that they should be separate.
|
|
||||||
|
|
||||||
At a high level, the steps followed are:
|
|
||||||
(1) accept a set of wires
|
|
||||||
(2) create another set of wires like this one, but which are transformed and rotated
|
|
||||||
(3) create a ruledSurface between the sets of wires
|
|
||||||
(4) create a shell and compute the resulting object
|
|
||||||
|
|
||||||
:param outerWire: the outermost wire, a cad.Wire
|
|
||||||
:param innerWires: a list of inner wires, a list of cad.Wire
|
|
||||||
:param vecCenter: the center point about which to rotate. the axis of rotation is defined by
|
|
||||||
vecNormal, located at vecCenter. ( a cad.Vector )
|
|
||||||
:param vecNormal: a vector along which to extrude the wires ( a cad.Vector )
|
|
||||||
:param angleDegrees: the angle to rotate through while extruding
|
|
||||||
:return: a cad.Solid object
|
|
||||||
"""
|
|
||||||
|
|
||||||
# from this point down we are dealing with FreeCAD wires not cad.wires
|
|
||||||
startWires = [outerWire.wrapped] + [i.wrapped for i in innerWires]
|
|
||||||
endWires = []
|
|
||||||
p1 = vecCenter.wrapped
|
|
||||||
p2 = vecCenter.add(vecNormal).wrapped
|
|
||||||
|
|
||||||
# make translated and rotated copy of each wire
|
|
||||||
for w in startWires:
|
|
||||||
w2 = w.copy()
|
|
||||||
w2.translate(vecNormal.wrapped)
|
|
||||||
w2.rotate(p1, p2, angleDegrees)
|
|
||||||
endWires.append(w2)
|
|
||||||
|
|
||||||
# make a ruled surface for each set of wires
|
|
||||||
sides = []
|
|
||||||
for w1, w2 in zip(startWires, endWires):
|
|
||||||
rs = FreeCADPart.makeRuledSurface(w1, w2)
|
|
||||||
sides.append(rs)
|
|
||||||
|
|
||||||
#make faces for the top and bottom
|
|
||||||
startFace = FreeCADPart.Face(startWires)
|
|
||||||
endFace = FreeCADPart.Face(endWires)
|
|
||||||
|
|
||||||
#collect all the faces from the sides
|
|
||||||
faceList = [startFace]
|
|
||||||
for s in sides:
|
|
||||||
faceList.extend(s.Faces)
|
|
||||||
faceList.append(endFace)
|
|
||||||
|
|
||||||
shell = FreeCADPart.makeShell(faceList)
|
|
||||||
solid = FreeCADPart.makeSolid(shell)
|
|
||||||
return Shape.cast(solid)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def extrudeLinear(cls, outerWire, innerWires, vecNormal):
|
|
||||||
"""
|
|
||||||
Attempt to extrude the list of wires into a prismatic solid in the provided direction
|
|
||||||
|
|
||||||
:param outerWire: the outermost wire
|
|
||||||
:param innerWires: a list of inner wires
|
|
||||||
:param vecNormal: a vector along which to extrude the wires
|
|
||||||
:return: a Solid object
|
|
||||||
|
|
||||||
The wires must not intersect
|
|
||||||
|
|
||||||
Extruding wires is very non-trivial. Nested wires imply very different geometry, and
|
|
||||||
there are many geometries that are invalid. In general, the following conditions must be met:
|
|
||||||
|
|
||||||
* all wires must be closed
|
|
||||||
* there cannot be any intersecting or self-intersecting wires
|
|
||||||
* wires must be listed from outside in
|
|
||||||
* more than one levels of nesting is not supported reliably
|
|
||||||
|
|
||||||
This method will attempt to sort the wires, but there is much work remaining to make this method
|
|
||||||
reliable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# one would think that fusing faces into a compound and then extruding would work,
|
|
||||||
# but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
|
|
||||||
# but then cutting it from the main solid fails with BRep_NotDone.
|
|
||||||
#the work around is to extrude each and then join the resulting solids, which seems to work
|
|
||||||
|
|
||||||
#FreeCAD allows this in one operation, but others might not
|
|
||||||
freeCADWires = [outerWire.wrapped]
|
|
||||||
for w in innerWires:
|
|
||||||
freeCADWires.append(w.wrapped)
|
|
||||||
|
|
||||||
f = FreeCADPart.Face(freeCADWires)
|
|
||||||
result = f.extrude(vecNormal.wrapped)
|
|
||||||
|
|
||||||
return Shape.cast(result)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def revolve(cls, outerWire, innerWires, angleDegrees, axisStart, axisEnd):
|
|
||||||
"""
|
|
||||||
Attempt to revolve the list of wires into a solid in the provided direction
|
|
||||||
|
|
||||||
:param outerWire: the outermost wire
|
|
||||||
:param innerWires: a list of inner wires
|
|
||||||
:param angleDegrees: the angle to revolve through.
|
|
||||||
:type angleDegrees: float, anything less than 360 degrees will leave the shape open
|
|
||||||
:param axisStart: the start point of the axis of rotation
|
|
||||||
:type axisStart: tuple, a two tuple
|
|
||||||
:param axisEnd: the end point of the axis of rotation
|
|
||||||
:type axisEnd: tuple, a two tuple
|
|
||||||
:return: a Solid object
|
|
||||||
|
|
||||||
The wires must not intersect
|
|
||||||
|
|
||||||
* all wires must be closed
|
|
||||||
* there cannot be any intersecting or self-intersecting wires
|
|
||||||
* wires must be listed from outside in
|
|
||||||
* more than one levels of nesting is not supported reliably
|
|
||||||
* the wire(s) that you're revolving cannot be centered
|
|
||||||
|
|
||||||
This method will attempt to sort the wires, but there is much work remaining to make this method
|
|
||||||
reliable.
|
|
||||||
"""
|
|
||||||
freeCADWires = [outerWire.wrapped]
|
|
||||||
|
|
||||||
for w in innerWires:
|
|
||||||
freeCADWires.append(w.wrapped)
|
|
||||||
|
|
||||||
f = FreeCADPart.Face(freeCADWires)
|
|
||||||
|
|
||||||
rotateCenter = FreeCAD.Base.Vector(axisStart)
|
|
||||||
rotateAxis = FreeCAD.Base.Vector(axisEnd)
|
|
||||||
|
|
||||||
#Convert our axis end vector into to something FreeCAD will understand (an axis specification vector)
|
|
||||||
rotateAxis = rotateCenter.sub(rotateAxis)
|
|
||||||
|
|
||||||
#FreeCAD wants a rotation center and then an axis to rotate around rather than an axis of rotation
|
|
||||||
result = f.revolve(rotateCenter, rotateAxis, angleDegrees)
|
|
||||||
|
|
||||||
return Shape.cast(result)
|
|
||||||
|
|
||||||
def tessellate(self, tolerance):
|
|
||||||
return self.wrapped.tessellate(tolerance)
|
|
||||||
|
|
||||||
def intersect(self, toIntersect):
|
|
||||||
"""
|
|
||||||
computes the intersection between this solid and the supplied one
|
|
||||||
The result could be a face or a compound of faces
|
|
||||||
"""
|
|
||||||
return Shape.cast(self.wrapped.common(toIntersect.wrapped))
|
|
||||||
|
|
||||||
def cut(self, solidToCut):
|
|
||||||
"Remove a solid from another one"
|
|
||||||
return Shape.cast(self.wrapped.cut(solidToCut.wrapped))
|
|
||||||
|
|
||||||
def fuse(self, solidToJoin):
|
|
||||||
return Shape.cast(self.wrapped.fuse(solidToJoin.wrapped))
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
"""Clean faces by removing splitter edges."""
|
|
||||||
r = self.wrapped.removeSplitter()
|
|
||||||
# removeSplitter() returns a generic Shape type, cast to actual type of object
|
|
||||||
r = FreeCADPart.cast_to_shape(r)
|
|
||||||
return Shape.cast(r)
|
|
||||||
|
|
||||||
def fillet(self, radius, edgeList):
|
|
||||||
"""
|
|
||||||
Fillets the specified edges of this solid.
|
|
||||||
:param radius: float > 0, the radius of the fillet
|
|
||||||
:param edgeList: a list of Edge objects, which must belong to this solid
|
|
||||||
:return: Filleted solid
|
|
||||||
"""
|
|
||||||
nativeEdges = [e.wrapped for e in edgeList]
|
|
||||||
return Shape.cast(self.wrapped.makeFillet(radius, nativeEdges))
|
|
||||||
|
|
||||||
def chamfer(self, length, length2, edgeList):
|
|
||||||
"""
|
|
||||||
Chamfers the specified edges of this solid.
|
|
||||||
:param length: length > 0, the length (length) of the chamfer
|
|
||||||
:param length2: length2 > 0, optional parameter for asymmetrical chamfer. Should be `None` if not required.
|
|
||||||
:param edgeList: a list of Edge objects, which must belong to this solid
|
|
||||||
:return: Chamfered solid
|
|
||||||
"""
|
|
||||||
nativeEdges = [e.wrapped for e in edgeList]
|
|
||||||
# note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API
|
|
||||||
if length2:
|
|
||||||
return Shape.cast(self.wrapped.makeChamfer(length, length2, nativeEdges))
|
|
||||||
else:
|
|
||||||
return Shape.cast(self.wrapped.makeChamfer(length, nativeEdges))
|
|
||||||
|
|
||||||
def shell(self, faceList, thickness, tolerance=0.0001):
|
|
||||||
"""
|
|
||||||
make a shelled solid of given by removing the list of faces
|
|
||||||
|
|
||||||
:param faceList: list of face objects, which must be part of the solid.
|
|
||||||
:param thickness: floating point thickness. positive shells outwards, negative shells inwards
|
|
||||||
:param tolerance: modelling tolerance of the method, default=0.0001
|
|
||||||
:return: a shelled solid
|
|
||||||
|
|
||||||
**WARNING** The underlying FreeCAD implementation can very frequently have problems
|
|
||||||
with shelling complex geometries!
|
|
||||||
"""
|
|
||||||
nativeFaces = [f.wrapped for f in faceList]
|
|
||||||
return Shape.cast(self.wrapped.makeThickness(nativeFaces, thickness, tolerance))
|
|
||||||
|
|
||||||
|
|
||||||
class Compound(Shape):
|
|
||||||
"""
|
|
||||||
a collection of disconnected solids
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, obj):
|
|
||||||
"""
|
|
||||||
An Edge
|
|
||||||
"""
|
|
||||||
self.wrapped = obj
|
|
||||||
|
|
||||||
# Helps identify this solid through the use of an ID
|
|
||||||
self.label = ""
|
|
||||||
|
|
||||||
def Center(self):
|
|
||||||
return self.Center()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def makeCompound(cls, listOfShapes):
|
|
||||||
"""
|
|
||||||
Create a compound out of a list of shapes
|
|
||||||
"""
|
|
||||||
solids = [s.wrapped for s in listOfShapes]
|
|
||||||
c = FreeCADPart.Compound(solids)
|
|
||||||
return Shape.cast(c)
|
|
||||||
|
|
||||||
def fuse(self, toJoin):
|
|
||||||
return Shape.cast(self.wrapped.fuse(toJoin.wrapped))
|
|
||||||
|
|
||||||
def tessellate(self, tolerance):
|
|
||||||
return self.wrapped.tessellate(tolerance)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
"""This method is not implemented yet."""
|
|
||||||
return self
|
|
|
@ -1,18 +0,0 @@
|
||||||
"""
|
|
||||||
CadQuery
|
|
||||||
Copyright (C) 2015 Parametric Products Intellectual Holdings, LLC
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
"""
|
|
|
@ -1,474 +0,0 @@
|
||||||
"""
|
|
||||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
|
||||||
|
|
||||||
This file is part of CadQuery.
|
|
||||||
|
|
||||||
CadQuery is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
CadQuery is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import math
|
|
||||||
from cadquery import Vector,Edge,Vertex,Face,Solid,Shell,Compound
|
|
||||||
|
|
||||||
|
|
||||||
class Selector(object):
|
|
||||||
"""
|
|
||||||
Filters a list of objects
|
|
||||||
|
|
||||||
Filters must provide a single method that filters objects.
|
|
||||||
"""
|
|
||||||
def filter(self,objectList):
|
|
||||||
"""
|
|
||||||
Filter the provided list
|
|
||||||
:param objectList: list to filter
|
|
||||||
:type objectList: list of FreeCAD primatives
|
|
||||||
:return: filtered list
|
|
||||||
|
|
||||||
The default implementation returns the original list unfiltered
|
|
||||||
|
|
||||||
"""
|
|
||||||
return objectList
|
|
||||||
|
|
||||||
def __and__(self, other):
|
|
||||||
return AndSelector(self, other)
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
return SumSelector(self, other)
|
|
||||||
|
|
||||||
def __sub__(self, other):
|
|
||||||
return SubtractSelector(self, other)
|
|
||||||
|
|
||||||
def __neg__(self):
|
|
||||||
return InverseSelector(self)
|
|
||||||
|
|
||||||
class NearestToPointSelector(Selector):
|
|
||||||
"""
|
|
||||||
Selects object nearest the provided point.
|
|
||||||
|
|
||||||
If the object is a vertex or point, the distance
|
|
||||||
is used. For other kinds of shapes, the center of mass
|
|
||||||
is used to to compute which is closest.
|
|
||||||
|
|
||||||
Applicability: All Types of Shapes
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
CQ(aCube).vertices(NearestToPointSelector((0,1,0))
|
|
||||||
|
|
||||||
returns the vertex of the unit cube closest to the point x=0,y=1,z=0
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self,pnt ):
|
|
||||||
self.pnt = pnt
|
|
||||||
def filter(self,objectList):
|
|
||||||
|
|
||||||
def dist(tShape):
|
|
||||||
return tShape.Center().sub(Vector(*self.pnt)).Length
|
|
||||||
#if tShape.ShapeType == 'Vertex':
|
|
||||||
# return tShape.Point.sub(toVector(self.pnt)).Length
|
|
||||||
#else:
|
|
||||||
# return tShape.CenterOfMass.sub(toVector(self.pnt)).Length
|
|
||||||
|
|
||||||
return [ min(objectList,key=dist) ]
|
|
||||||
|
|
||||||
class BoxSelector(Selector):
|
|
||||||
"""
|
|
||||||
Selects objects inside the 3D box defined by 2 points.
|
|
||||||
|
|
||||||
If `boundingbox` is True only the objects that have their bounding
|
|
||||||
box inside the given box is selected. Otherwise only center point
|
|
||||||
of the object is tested.
|
|
||||||
|
|
||||||
Applicability: all types of shapes
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
CQ(aCube).edges(BoxSelector((0,1,0), (1,2,1))
|
|
||||||
"""
|
|
||||||
def __init__(self, point0, point1, boundingbox=False):
|
|
||||||
self.p0 = Vector(*point0)
|
|
||||||
self.p1 = Vector(*point1)
|
|
||||||
self.test_boundingbox = boundingbox
|
|
||||||
|
|
||||||
def filter(self, objectList):
|
|
||||||
|
|
||||||
result = []
|
|
||||||
x0, y0, z0 = self.p0.toTuple()
|
|
||||||
x1, y1, z1 = self.p1.toTuple()
|
|
||||||
|
|
||||||
def isInsideBox(p):
|
|
||||||
# using XOR for checking if x/y/z is in between regardless
|
|
||||||
# of order of x/y/z0 and x/y/z1
|
|
||||||
return ((p.x < x0) ^ (p.x < x1)) and \
|
|
||||||
((p.y < y0) ^ (p.y < y1)) and \
|
|
||||||
((p.z < z0) ^ (p.z < z1))
|
|
||||||
|
|
||||||
for o in objectList:
|
|
||||||
if self.test_boundingbox:
|
|
||||||
bb = o.BoundingBox()
|
|
||||||
if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and \
|
|
||||||
isInsideBox(Vector(bb.xmax, bb.ymax, bb.zmax)):
|
|
||||||
result.append(o)
|
|
||||||
else:
|
|
||||||
if isInsideBox(o.Center()):
|
|
||||||
result.append(o)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
class BaseDirSelector(Selector):
|
|
||||||
"""
|
|
||||||
A selector that handles selection on the basis of a single
|
|
||||||
direction vector
|
|
||||||
"""
|
|
||||||
def __init__(self,vector,tolerance=0.0001 ):
|
|
||||||
self.direction = vector
|
|
||||||
self.TOLERANCE = tolerance
|
|
||||||
|
|
||||||
def test(self,vec):
|
|
||||||
"Test a specified vector. Subclasses override to provide other implementations"
|
|
||||||
return True
|
|
||||||
|
|
||||||
def filter(self,objectList):
|
|
||||||
"""
|
|
||||||
There are lots of kinds of filters, but
|
|
||||||
for planes they are always based on the normal of the plane,
|
|
||||||
and for edges on the tangent vector along the edge
|
|
||||||
"""
|
|
||||||
r = []
|
|
||||||
for o in objectList:
|
|
||||||
#no really good way to avoid a switch here, edges and faces are simply different!
|
|
||||||
|
|
||||||
if type(o) == Face:
|
|
||||||
# a face is only parallell to a direction if it is a plane, and its normal is parallel to the dir
|
|
||||||
normal = o.normalAt(None)
|
|
||||||
|
|
||||||
if self.test(normal):
|
|
||||||
r.append(o)
|
|
||||||
elif type(o) == Edge and o.geomType() == 'LINE':
|
|
||||||
#an edge is parallel to a direction if it is a line, and the line is parallel to the dir
|
|
||||||
tangent = o.tangentAt(None)
|
|
||||||
if self.test(tangent):
|
|
||||||
r.append(o)
|
|
||||||
|
|
||||||
return r
|
|
||||||
|
|
||||||
class ParallelDirSelector(BaseDirSelector):
|
|
||||||
"""
|
|
||||||
Selects objects parallel with the provided direction
|
|
||||||
|
|
||||||
Applicability:
|
|
||||||
Linear Edges
|
|
||||||
Planar Faces
|
|
||||||
|
|
||||||
Use the string syntax shortcut \|(X|Y|Z) if you want to select
|
|
||||||
based on a cardinal direction.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
CQ(aCube).faces(ParallelDirSelector((0,0,1))
|
|
||||||
|
|
||||||
selects faces with a normals in the z direction, and is equivalent to::
|
|
||||||
|
|
||||||
CQ(aCube).faces("|Z")
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test(self,vec):
|
|
||||||
return self.direction.cross(vec).Length < self.TOLERANCE
|
|
||||||
|
|
||||||
class DirectionSelector(BaseDirSelector):
|
|
||||||
"""
|
|
||||||
Selects objects aligned with the provided direction
|
|
||||||
|
|
||||||
Applicability:
|
|
||||||
Linear Edges
|
|
||||||
Planar Faces
|
|
||||||
|
|
||||||
Use the string syntax shortcut +/-(X|Y|Z) if you want to select
|
|
||||||
based on a cardinal direction.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
CQ(aCube).faces(DirectionSelector((0,0,1))
|
|
||||||
|
|
||||||
selects faces with a normals in the z direction, and is equivalent to::
|
|
||||||
|
|
||||||
CQ(aCube).faces("+Z")
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test(self,vec):
|
|
||||||
return abs(self.direction.getAngle(vec) < self.TOLERANCE)
|
|
||||||
|
|
||||||
class PerpendicularDirSelector(BaseDirSelector):
|
|
||||||
"""
|
|
||||||
Selects objects perpendicular with the provided direction
|
|
||||||
|
|
||||||
Applicability:
|
|
||||||
Linear Edges
|
|
||||||
Planar Faces
|
|
||||||
|
|
||||||
Use the string syntax shortcut #(X|Y|Z) if you want to select
|
|
||||||
based on a cardinal direction.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
CQ(aCube).faces(PerpendicularDirSelector((0,0,1))
|
|
||||||
|
|
||||||
selects faces with a normals perpendicular to the z direction, and is equivalent to::
|
|
||||||
|
|
||||||
CQ(aCube).faces("#Z")
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test(self,vec):
|
|
||||||
angle = self.direction.getAngle(vec)
|
|
||||||
r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE )
|
|
||||||
return not r
|
|
||||||
|
|
||||||
|
|
||||||
class TypeSelector(Selector):
|
|
||||||
"""
|
|
||||||
Selects objects of the prescribed topological type.
|
|
||||||
|
|
||||||
Applicability:
|
|
||||||
Faces: Plane,Cylinder,Sphere
|
|
||||||
Edges: Line,Circle,Arc
|
|
||||||
|
|
||||||
You can use the shortcut selector %(PLANE|SPHERE|CONE) for faces,
|
|
||||||
and %(LINE|ARC|CIRCLE) for edges.
|
|
||||||
|
|
||||||
For example this::
|
|
||||||
|
|
||||||
CQ(aCube).faces ( TypeSelector("PLANE") )
|
|
||||||
|
|
||||||
will select 6 faces, and is equivalent to::
|
|
||||||
|
|
||||||
CQ(aCube).faces( "%PLANE" )
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self,typeString):
|
|
||||||
self.typeString = typeString.upper()
|
|
||||||
|
|
||||||
def filter(self,objectList):
|
|
||||||
r = []
|
|
||||||
for o in objectList:
|
|
||||||
if o.geomType() == self.typeString:
|
|
||||||
r.append(o)
|
|
||||||
return r
|
|
||||||
|
|
||||||
class DirectionMinMaxSelector(Selector):
|
|
||||||
"""
|
|
||||||
Selects objects closest or farthest in the specified direction
|
|
||||||
Used for faces, points, and edges
|
|
||||||
|
|
||||||
Applicability:
|
|
||||||
All object types. for a vertex, its point is used. for all other kinds
|
|
||||||
of objects, the center of mass of the object is used.
|
|
||||||
|
|
||||||
You can use the string shortcuts >(X|Y|Z) or <(X|Y|Z) if you want to
|
|
||||||
select based on a cardinal direction.
|
|
||||||
|
|
||||||
For example this::
|
|
||||||
|
|
||||||
CQ(aCube).faces ( DirectionMinMaxSelector((0,0,1),True )
|
|
||||||
|
|
||||||
Means to select the face having the center of mass farthest in the positive z direction,
|
|
||||||
and is the same as:
|
|
||||||
|
|
||||||
CQ(aCube).faces( ">Z" )
|
|
||||||
|
|
||||||
Future Enhancements:
|
|
||||||
provide a nicer way to select in arbitrary directions. IE, a bit more code could
|
|
||||||
allow '>(0,0,1)' to work.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, vector, directionMax=True, tolerance=0.0001):
|
|
||||||
self.vector = vector
|
|
||||||
self.max = max
|
|
||||||
self.directionMax = directionMax
|
|
||||||
self.TOLERANCE = tolerance
|
|
||||||
def filter(self,objectList):
|
|
||||||
|
|
||||||
def distance(tShape):
|
|
||||||
return tShape.Center().dot(self.vector)
|
|
||||||
#if tShape.ShapeType == 'Vertex':
|
|
||||||
# pnt = tShape.Point
|
|
||||||
#else:
|
|
||||||
# pnt = tShape.Center()
|
|
||||||
#return pnt.dot(self.vector)
|
|
||||||
|
|
||||||
# find out the max/min distance
|
|
||||||
if self.directionMax:
|
|
||||||
d = max(map(distance, objectList))
|
|
||||||
else:
|
|
||||||
d = min(map(distance, objectList))
|
|
||||||
|
|
||||||
# return all objects at the max/min distance (within a tolerance)
|
|
||||||
return filter(lambda o: abs(d - distance(o)) < self.TOLERANCE, objectList)
|
|
||||||
|
|
||||||
class BinarySelector(Selector):
|
|
||||||
"""
|
|
||||||
Base class for selectors that operates with two other
|
|
||||||
selectors. Subclass must implement the :filterResults(): method.
|
|
||||||
"""
|
|
||||||
def __init__(self, left, right):
|
|
||||||
self.left = left
|
|
||||||
self.right = right
|
|
||||||
|
|
||||||
def filter(self, objectList):
|
|
||||||
return self.filterResults(self.left.filter(objectList),
|
|
||||||
self.right.filter(objectList))
|
|
||||||
|
|
||||||
def filterResults(self, r_left, r_right):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
class AndSelector(BinarySelector):
|
|
||||||
"""
|
|
||||||
Intersection selector. Returns objects that is selected by both selectors.
|
|
||||||
"""
|
|
||||||
def filterResults(self, r_left, r_right):
|
|
||||||
# return intersection of lists
|
|
||||||
return list(set(r_left) & set(r_right))
|
|
||||||
|
|
||||||
class SumSelector(BinarySelector):
|
|
||||||
"""
|
|
||||||
Union selector. Returns the sum of two selectors results.
|
|
||||||
"""
|
|
||||||
def filterResults(self, r_left, r_right):
|
|
||||||
# return the union (no duplicates) of lists
|
|
||||||
return list(set(r_left + r_right))
|
|
||||||
|
|
||||||
class SubtractSelector(BinarySelector):
|
|
||||||
"""
|
|
||||||
Difference selector. Substract results of a selector from another
|
|
||||||
selectors results.
|
|
||||||
"""
|
|
||||||
def filterResults(self, r_left, r_right):
|
|
||||||
return list(set(r_left) - set(r_right))
|
|
||||||
|
|
||||||
class InverseSelector(Selector):
|
|
||||||
"""
|
|
||||||
Inverts the selection of given selector. In other words, selects
|
|
||||||
all objects that is not selected by given selector.
|
|
||||||
"""
|
|
||||||
def __init__(self, selector):
|
|
||||||
self.selector = selector
|
|
||||||
|
|
||||||
def filter(self, objectList):
|
|
||||||
# note that Selector() selects everything
|
|
||||||
return SubtractSelector(Selector(), self.selector).filter(objectList)
|
|
||||||
|
|
||||||
class StringSyntaxSelector(Selector):
|
|
||||||
"""
|
|
||||||
Filter lists objects using a simple string syntax. All of the filters available in the string syntax
|
|
||||||
are also available ( usually with more functionality ) through the creation of full-fledged
|
|
||||||
selector objects. see :py:class:`Selector` and its subclasses
|
|
||||||
|
|
||||||
Filtering works differently depending on the type of object list being filtered.
|
|
||||||
|
|
||||||
:param selectorString: A two-part selector string, [selector][axis]
|
|
||||||
|
|
||||||
:return: objects that match the specified selector
|
|
||||||
|
|
||||||
***Modfiers*** are ``('|','+','-','<','>','%')``
|
|
||||||
|
|
||||||
:\|:
|
|
||||||
parallel to ( same as :py:class:`ParallelDirSelector` ). Can return multiple objects.
|
|
||||||
:#:
|
|
||||||
perpendicular to (same as :py:class:`PerpendicularDirSelector` )
|
|
||||||
:+:
|
|
||||||
positive direction (same as :py:class:`DirectionSelector` )
|
|
||||||
:-:
|
|
||||||
negative direction (same as :py:class:`DirectionSelector` )
|
|
||||||
:>:
|
|
||||||
maximize (same as :py:class:`DirectionMinMaxSelector` with directionMax=True)
|
|
||||||
:<:
|
|
||||||
minimize (same as :py:class:`DirectionMinMaxSelector` with directionMax=False )
|
|
||||||
:%:
|
|
||||||
curve/surface type (same as :py:class:`TypeSelector`)
|
|
||||||
|
|
||||||
***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ``
|
|
||||||
|
|
||||||
Selectors are a complex topic: see :ref:`selector_reference` for more information
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self,selectorString):
|
|
||||||
|
|
||||||
self.axes = {
|
|
||||||
'X': Vector(1,0,0),
|
|
||||||
'Y': Vector(0,1,0),
|
|
||||||
'Z': Vector(0,0,1),
|
|
||||||
'XY': Vector(1,1,0),
|
|
||||||
'YZ': Vector(0,1,1),
|
|
||||||
'XZ': Vector(1,0,1)
|
|
||||||
}
|
|
||||||
|
|
||||||
namedViews = {
|
|
||||||
'front': ('>','Z' ),
|
|
||||||
'back': ('<','Z'),
|
|
||||||
'left':('<', 'X'),
|
|
||||||
'right': ('>', 'X'),
|
|
||||||
'top': ('>','Y'),
|
|
||||||
'bottom': ('<','Y')
|
|
||||||
}
|
|
||||||
self.selectorString = selectorString
|
|
||||||
r = re.compile("\s*([-\+<>\|\%#])*\s*(\w+)\s*",re.IGNORECASE)
|
|
||||||
m = r.match(selectorString)
|
|
||||||
|
|
||||||
if m != None:
|
|
||||||
if namedViews.has_key(selectorString):
|
|
||||||
(a,b) = namedViews[selectorString]
|
|
||||||
self.mySelector = self._chooseSelector(a,b )
|
|
||||||
else:
|
|
||||||
self.mySelector = self._chooseSelector(m.groups()[0],m.groups()[1])
|
|
||||||
else:
|
|
||||||
raise ValueError ("Selector String format must be [-+<>|#%] X|Y|Z ")
|
|
||||||
|
|
||||||
|
|
||||||
def _chooseSelector(self,selType,selAxis):
|
|
||||||
"""Sets up the underlying filters accordingly"""
|
|
||||||
|
|
||||||
if selType == "%":
|
|
||||||
return TypeSelector(selAxis)
|
|
||||||
|
|
||||||
#all other types need to select axis as a vector
|
|
||||||
#get the axis vector first, will throw an except if an unknown axis is used
|
|
||||||
try:
|
|
||||||
vec = self.axes[selAxis]
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError ("Axis value %s not allowed: must be one of %s" % (selAxis, str(self.axes)))
|
|
||||||
|
|
||||||
if selType in (None, "+"):
|
|
||||||
#use direction filter
|
|
||||||
return DirectionSelector(vec)
|
|
||||||
elif selType == '-':
|
|
||||||
#just use the reverse of the direction vector
|
|
||||||
return DirectionSelector(vec.multiply(-1.0))
|
|
||||||
elif selType == "|":
|
|
||||||
return ParallelDirSelector(vec)
|
|
||||||
elif selType == ">":
|
|
||||||
return DirectionMinMaxSelector(vec,True)
|
|
||||||
elif selType == "<":
|
|
||||||
return DirectionMinMaxSelector(vec,False)
|
|
||||||
elif selType == '#':
|
|
||||||
return PerpendicularDirSelector(vec)
|
|
||||||
else:
|
|
||||||
raise ValueError ("Selector String format must be [-+<>|] X|Y|Z ")
|
|
||||||
|
|
||||||
def filter(self,objectList):
|
|
||||||
"""
|
|
||||||
selects minimum, maximum, positive or negative values relative to a direction
|
|
||||||
[+\|-\|<\|>\|] \<X\|Y\|Z>
|
|
||||||
"""
|
|
||||||
return self.mySelector.filter(objectList)
|
|
|
@ -1,170 +0,0 @@
|
||||||
"""
|
|
||||||
Tests CQGI functionality
|
|
||||||
|
|
||||||
Currently, this includes:
|
|
||||||
Parsing a script, and detecting its available variables
|
|
||||||
Altering the values at runtime
|
|
||||||
defining a build_object function to return results
|
|
||||||
"""
|
|
||||||
|
|
||||||
from cadquery import cqgi
|
|
||||||
from tests import BaseTest
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
TESTSCRIPT = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
height=2.0
|
|
||||||
width=3.0
|
|
||||||
(a,b) = (1.0,1.0)
|
|
||||||
foo="bar"
|
|
||||||
|
|
||||||
result = "%s|%s|%s|%s" % ( str(height) , str(width) , foo , str(a) )
|
|
||||||
build_object(result)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCQGI(BaseTest):
|
|
||||||
def test_parser(self):
|
|
||||||
model = cqgi.CQModel(TESTSCRIPT)
|
|
||||||
metadata = model.metadata
|
|
||||||
|
|
||||||
self.assertEquals(set(metadata.parameters.keys()), {'height', 'width', 'a', 'b', 'foo'})
|
|
||||||
|
|
||||||
def test_build_with_empty_params(self):
|
|
||||||
model = cqgi.CQModel(TESTSCRIPT)
|
|
||||||
result = model.build()
|
|
||||||
|
|
||||||
self.assertTrue(result.success)
|
|
||||||
self.assertTrue(len(result.results) == 1)
|
|
||||||
self.assertTrue(result.results[0] == "2.0|3.0|bar|1.0")
|
|
||||||
|
|
||||||
def test_build_with_different_params(self):
|
|
||||||
model = cqgi.CQModel(TESTSCRIPT)
|
|
||||||
result = model.build({'height': 3.0})
|
|
||||||
self.assertTrue(result.results[0] == "3.0|3.0|bar|1.0")
|
|
||||||
|
|
||||||
def test_build_with_exception(self):
|
|
||||||
badscript = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
raise ValueError("ERROR")
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
model = cqgi.CQModel(badscript)
|
|
||||||
result = model.build({})
|
|
||||||
self.assertFalse(result.success)
|
|
||||||
self.assertIsNotNone(result.exception)
|
|
||||||
self.assertTrue(result.exception.message == "ERROR")
|
|
||||||
|
|
||||||
def test_that_invalid_syntax_in_script_fails_immediately(self):
|
|
||||||
badscript = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
this doesnt even compile
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
with self.assertRaises(Exception) as context:
|
|
||||||
model = cqgi.CQModel(badscript)
|
|
||||||
|
|
||||||
self.assertTrue('invalid syntax' in context.exception)
|
|
||||||
|
|
||||||
def test_that_two_results_are_returned(self):
|
|
||||||
script = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
h = 1
|
|
||||||
build_object(h)
|
|
||||||
h = 2
|
|
||||||
build_object(h)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
model = cqgi.CQModel(script)
|
|
||||||
result = model.build({})
|
|
||||||
self.assertEquals(2, len(result.results))
|
|
||||||
self.assertEquals(1, result.results[0])
|
|
||||||
self.assertEquals(2, result.results[1])
|
|
||||||
|
|
||||||
def test_that_assinging_number_to_string_works(self):
|
|
||||||
script = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
h = "this is a string"
|
|
||||||
build_object(h)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
result = cqgi.parse(script).build( {'h': 33.33})
|
|
||||||
self.assertEquals(result.results[0], "33.33")
|
|
||||||
|
|
||||||
def test_that_assigning_string_to_number_fails(self):
|
|
||||||
script = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
h = 20.0
|
|
||||||
build_object(h)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
result = cqgi.parse(script).build( {'h': "a string"})
|
|
||||||
self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
|
|
||||||
|
|
||||||
def test_that_assigning_unknown_var_fails(self):
|
|
||||||
script = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
h = 20.0
|
|
||||||
build_object(h)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
result = cqgi.parse(script).build( {'w': "var is not there"})
|
|
||||||
self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
|
|
||||||
|
|
||||||
def test_that_not_calling_build_object_raises_error(self):
|
|
||||||
script = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
h = 20.0
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
result = cqgi.parse(script).build()
|
|
||||||
self.assertTrue(isinstance(result.exception, cqgi.NoOutputError))
|
|
||||||
|
|
||||||
def test_that_cq_objects_are_visible(self):
|
|
||||||
script = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
r = cadquery.Workplane('XY').box(1,2,3)
|
|
||||||
build_object(r)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
result = cqgi.parse(script).build()
|
|
||||||
self.assertTrue(result.success)
|
|
||||||
self.assertIsNotNone(result.first_result)
|
|
||||||
|
|
||||||
def test_setting_boolean_variable(self):
|
|
||||||
script = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
h = True
|
|
||||||
build_object( "*%s*" % str(h) )
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
#result = cqgi.execute(script)
|
|
||||||
result = cqgi.parse(script).build({'h': False})
|
|
||||||
|
|
||||||
self.assertTrue(result.success)
|
|
||||||
self.assertEquals(result.first_result,'*False*')
|
|
||||||
|
|
||||||
def test_that_only_top_level_vars_are_detected(self):
|
|
||||||
script = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
h = 1.0
|
|
||||||
w = 2.0
|
|
||||||
|
|
||||||
def do_stuff():
|
|
||||||
x = 1
|
|
||||||
y = 2
|
|
||||||
|
|
||||||
build_object( "result" )
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
model = cqgi.parse(script)
|
|
||||||
|
|
||||||
self.assertEquals(2, len(model.metadata.parameters))
|
|
|
@ -1,358 +0,0 @@
|
||||||
__author__ = 'dcowden'
|
|
||||||
|
|
||||||
"""
|
|
||||||
Tests for CadQuery Selectors
|
|
||||||
|
|
||||||
These tests do not construct any solids, they test only selectors that query
|
|
||||||
an existing solid
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import unittest,sys
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
#my modules
|
|
||||||
from tests import BaseTest,makeUnitCube,makeUnitSquareWire
|
|
||||||
from cadquery import *
|
|
||||||
from cadquery import selectors
|
|
||||||
|
|
||||||
class TestCQSelectors(BaseTest):
|
|
||||||
|
|
||||||
|
|
||||||
def testWorkplaneCenter(self):
|
|
||||||
"Test Moving workplane center"
|
|
||||||
s = Workplane(Plane.XY())
|
|
||||||
|
|
||||||
#current point and world point should be equal
|
|
||||||
self.assertTupleAlmostEquals((0.0,0.0,0.0),s.plane.origin.toTuple(),3)
|
|
||||||
|
|
||||||
#move origin and confirm center moves
|
|
||||||
s.center(-2.0,-2.0)
|
|
||||||
|
|
||||||
#current point should be 0,0, but
|
|
||||||
|
|
||||||
self.assertTupleAlmostEquals((-2.0,-2.0,0.0),s.plane.origin.toTuple(),3)
|
|
||||||
|
|
||||||
|
|
||||||
def testVertices(self):
|
|
||||||
t = makeUnitSquareWire() # square box
|
|
||||||
c = CQ(t)
|
|
||||||
|
|
||||||
self.assertEqual(4,c.vertices().size() )
|
|
||||||
self.assertEqual(4,c.edges().size() )
|
|
||||||
self.assertEqual(0,c.vertices().edges().size() ) #no edges on any vertices
|
|
||||||
self.assertEqual(4,c.edges().vertices().size() ) #but selecting all edges still yields all vertices
|
|
||||||
self.assertEqual(1,c.wires().size()) #just one wire
|
|
||||||
self.assertEqual(0,c.faces().size())
|
|
||||||
self.assertEqual(0,c.vertices().faces().size()) #odd combinations all work but yield no results
|
|
||||||
self.assertEqual(0,c.edges().faces().size())
|
|
||||||
self.assertEqual(0,c.edges().vertices().faces().size())
|
|
||||||
|
|
||||||
def testEnd(self):
|
|
||||||
c = CQ(makeUnitSquareWire())
|
|
||||||
self.assertEqual(4,c.vertices().size() ) #4 because there are 4 vertices
|
|
||||||
self.assertEqual(1,c.vertices().end().size() ) #1 because we started with 1 wire
|
|
||||||
|
|
||||||
def testAll(self):
|
|
||||||
"all returns a list of CQ objects, so that you can iterate over them individually"
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
self.assertEqual(6,c.faces().size())
|
|
||||||
self.assertEqual(6,len(c.faces().all()))
|
|
||||||
self.assertEqual(4,c.faces().all()[0].vertices().size() )
|
|
||||||
|
|
||||||
def testFirst(self):
|
|
||||||
c = CQ( makeUnitCube())
|
|
||||||
self.assertEqual(type(c.vertices().first().val()),Vertex)
|
|
||||||
self.assertEqual(type(c.vertices().first().first().first().val()),Vertex)
|
|
||||||
|
|
||||||
def testCompounds(self):
|
|
||||||
c = CQ(makeUnitSquareWire())
|
|
||||||
self.assertEqual(0,c.compounds().size() )
|
|
||||||
self.assertEqual(0,c.shells().size() )
|
|
||||||
self.assertEqual(0,c.solids().size() )
|
|
||||||
|
|
||||||
def testSolid(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
#make sure all the counts are right for a cube
|
|
||||||
self.assertEqual(1,c.solids().size() )
|
|
||||||
self.assertEqual(6,c.faces().size() )
|
|
||||||
self.assertEqual(12,c.edges().size())
|
|
||||||
self.assertEqual(8,c.vertices().size() )
|
|
||||||
self.assertEqual(0,c.compounds().size())
|
|
||||||
|
|
||||||
#now any particular face should result in 4 edges and four vertices
|
|
||||||
self.assertEqual(4,c.faces().first().edges().size() )
|
|
||||||
self.assertEqual(1,c.faces().first().size() )
|
|
||||||
self.assertEqual(4,c.faces().first().vertices().size() )
|
|
||||||
|
|
||||||
self.assertEqual(4,c.faces().last().edges().size() )
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def testFaceTypesFilter(self):
|
|
||||||
"Filters by face type"
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
self.assertEqual(c.faces().size(), c.faces('%PLANE').size())
|
|
||||||
self.assertEqual(c.faces().size(), c.faces('%plane').size())
|
|
||||||
self.assertEqual(0, c.faces('%sphere').size())
|
|
||||||
self.assertEqual(0, c.faces('%cone').size())
|
|
||||||
self.assertEqual(0, c.faces('%SPHERE').size())
|
|
||||||
|
|
||||||
def testPerpendicularDirFilter(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
self.assertEqual(8,c.edges("#Z").size() ) #8 edges are perp. to z
|
|
||||||
self.assertEqual(4, c.faces("#Z").size()) #4 faces are perp to z too!
|
|
||||||
|
|
||||||
def testFaceDirFilter(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
#a cube has one face in each direction
|
|
||||||
self.assertEqual(1, c.faces("+Z").size())
|
|
||||||
self.assertEqual(1, c.faces("-Z").size())
|
|
||||||
self.assertEqual(1, c.faces("+X").size())
|
|
||||||
self.assertEqual(1, c.faces("X").size()) #should be same as +X
|
|
||||||
self.assertEqual(1, c.faces("-X").size())
|
|
||||||
self.assertEqual(1, c.faces("+Y").size())
|
|
||||||
self.assertEqual(1, c.faces("-Y").size())
|
|
||||||
self.assertEqual(0, c.faces("XY").size())
|
|
||||||
|
|
||||||
def testParallelPlaneFaceFilter(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
#faces parallel to Z axis
|
|
||||||
self.assertEqual(2, c.faces("|Z").size())
|
|
||||||
#TODO: provide short names for ParallelDirSelector
|
|
||||||
self.assertEqual(2, c.faces(selectors.ParallelDirSelector(Vector((0,0,1)))).size()) #same thing as above
|
|
||||||
self.assertEqual(2, c.faces(selectors.ParallelDirSelector(Vector((0,0,-1)))).size()) #same thing as above
|
|
||||||
|
|
||||||
#just for fun, vertices on faces parallel to z
|
|
||||||
self.assertEqual(8, c.faces("|Z").vertices().size())
|
|
||||||
|
|
||||||
def testParallelEdgeFilter(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
self.assertEqual(4, c.edges("|Z").size())
|
|
||||||
self.assertEqual(4, c.edges("|X").size())
|
|
||||||
self.assertEqual(4, c.edges("|Y").size())
|
|
||||||
|
|
||||||
def testMaxDistance(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
#should select the topmost face
|
|
||||||
self.assertEqual(1, c.faces(">Z").size())
|
|
||||||
self.assertEqual(4, c.faces(">Z").vertices().size())
|
|
||||||
|
|
||||||
#vertices should all be at z=1, if this is the top face
|
|
||||||
self.assertEqual(4, len(c.faces(">Z").vertices().vals() ))
|
|
||||||
for v in c.faces(">Z").vertices().vals():
|
|
||||||
self.assertAlmostEqual(1.0,v.Z,3)
|
|
||||||
|
|
||||||
# test the case of multiple objects at the same distance
|
|
||||||
el = c.edges("<Z").vals()
|
|
||||||
self.assertEqual(4, len(el))
|
|
||||||
|
|
||||||
def testMinDistance(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
#should select the topmost face
|
|
||||||
self.assertEqual(1, c.faces("<Z").size())
|
|
||||||
self.assertEqual(4, c.faces("<Z").vertices().size())
|
|
||||||
|
|
||||||
#vertices should all be at z=1, if this is the top face
|
|
||||||
self.assertEqual(4, len(c.faces("<Z").vertices().vals() ))
|
|
||||||
for v in c.faces("<Z").vertices().vals():
|
|
||||||
self.assertAlmostEqual(0.0,v.Z,3)
|
|
||||||
|
|
||||||
# test the case of multiple objects at the same distance
|
|
||||||
el = c.edges("<Z").vals()
|
|
||||||
self.assertEqual(4, len(el))
|
|
||||||
|
|
||||||
def testNearestTo(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
#nearest vertex to origin is (0,0,0)
|
|
||||||
t = (0.1,0.1,0.1)
|
|
||||||
|
|
||||||
v = c.vertices(selectors.NearestToPointSelector(t)).vals()[0]
|
|
||||||
self.assertTupleAlmostEquals((0.0,0.0,0.0),(v.X,v.Y,v.Z),3)
|
|
||||||
|
|
||||||
t = (0.1,0.1,0.2)
|
|
||||||
#nearest edge is the vertical side edge, 0,0,0 -> 0,0,1
|
|
||||||
e = c.edges(selectors.NearestToPointSelector(t)).vals()[0]
|
|
||||||
v = c.edges(selectors.NearestToPointSelector(t)).vertices().vals()
|
|
||||||
self.assertEqual(2,len(v))
|
|
||||||
|
|
||||||
#nearest solid is myself
|
|
||||||
s = c.solids(selectors.NearestToPointSelector(t)).vals()
|
|
||||||
self.assertEqual(1,len(s))
|
|
||||||
|
|
||||||
def testBox(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
# test vertice selection
|
|
||||||
test_data_vertices = [
|
|
||||||
# box point0, box point1, selected vertice
|
|
||||||
((0.9, 0.9, 0.9), (1.1, 1.1, 1.1), (1.0, 1.0, 1.0)),
|
|
||||||
((-0.1, 0.9, 0.9), (0.9, 1.1, 1.1), (0.0, 1.0, 1.0)),
|
|
||||||
((-0.1, -0.1, 0.9), (0.1, 0.1, 1.1), (0.0, 0.0, 1.0)),
|
|
||||||
((-0.1, -0.1, -0.1), (0.1, 0.1, 0.1), (0.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.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))
|
|
||||||
]
|
|
||||||
|
|
||||||
for d in test_data_vertices:
|
|
||||||
vl = c.vertices(selectors.BoxSelector(d[0], d[1])).vals()
|
|
||||||
self.assertEqual(1, len(vl))
|
|
||||||
v = vl[0]
|
|
||||||
self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3)
|
|
||||||
|
|
||||||
# this time box points are swapped
|
|
||||||
vl = c.vertices(selectors.BoxSelector(d[1], d[0])).vals()
|
|
||||||
self.assertEqual(1, len(vl))
|
|
||||||
v = vl[0]
|
|
||||||
self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3)
|
|
||||||
|
|
||||||
# test multiple vertices selection
|
|
||||||
vl = c.vertices(selectors.BoxSelector((-0.1, -0.1, 0.9),(0.1, 1.1, 1.1))).vals()
|
|
||||||
self.assertEqual(2, len(vl))
|
|
||||||
vl = c.vertices(selectors.BoxSelector((-0.1, -0.1, -0.1),(0.1, 1.1, 1.1))).vals()
|
|
||||||
self.assertEqual(4, len(vl))
|
|
||||||
|
|
||||||
# test edge selection
|
|
||||||
test_data_edges = [
|
|
||||||
# box point0, box point1, edge center
|
|
||||||
((0.4, -0.1, -0.1), (0.6, 0.1, 0.1), (0.5, 0.0, 0.0)),
|
|
||||||
((-0.1, -0.1, 0.4), (0.1, 0.1, 0.6), (0.0, 0.0, 0.5)),
|
|
||||||
((0.9, 0.9, 0.4), (1.1, 1.1, 0.6), (1.0, 1.0, 0.5)),
|
|
||||||
((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0))
|
|
||||||
]
|
|
||||||
|
|
||||||
for d in test_data_edges:
|
|
||||||
el = c.edges(selectors.BoxSelector(d[0], d[1])).vals()
|
|
||||||
self.assertEqual(1, len(el))
|
|
||||||
ec = el[0].Center()
|
|
||||||
self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3)
|
|
||||||
|
|
||||||
# test again by swapping box points
|
|
||||||
el = c.edges(selectors.BoxSelector(d[1], d[0])).vals()
|
|
||||||
self.assertEqual(1, len(el))
|
|
||||||
ec = el[0].Center()
|
|
||||||
self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3)
|
|
||||||
|
|
||||||
# test multiple edge selection
|
|
||||||
el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals()
|
|
||||||
self.assertEqual(2, len(el))
|
|
||||||
el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals()
|
|
||||||
self.assertEqual(3, len(el))
|
|
||||||
|
|
||||||
# test face selection
|
|
||||||
test_data_faces = [
|
|
||||||
# box point0, box point1, face center
|
|
||||||
((0.4, -0.1, 0.4), (0.6, 0.1, 0.6), (0.5, 0.0, 0.5)),
|
|
||||||
((0.9, 0.4, 0.4), (1.1, 0.6, 0.6), (1.0, 0.5, 0.5)),
|
|
||||||
((0.4, 0.4, 0.9), (0.6, 0.6, 1.1), (0.5, 0.5, 1.0)),
|
|
||||||
((0.4, 0.4, -0.1), (0.6, 0.6, 0.1), (0.5, 0.5, 0.0))
|
|
||||||
]
|
|
||||||
|
|
||||||
for d in test_data_faces:
|
|
||||||
fl = c.faces(selectors.BoxSelector(d[0], d[1])).vals()
|
|
||||||
self.assertEqual(1, len(fl))
|
|
||||||
fc = fl[0].Center()
|
|
||||||
self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3)
|
|
||||||
|
|
||||||
# test again by swapping box points
|
|
||||||
fl = c.faces(selectors.BoxSelector(d[1], d[0])).vals()
|
|
||||||
self.assertEqual(1, len(fl))
|
|
||||||
fc = fl[0].Center()
|
|
||||||
self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3)
|
|
||||||
|
|
||||||
# test multiple face selection
|
|
||||||
fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals()
|
|
||||||
self.assertEqual(2, len(fl))
|
|
||||||
fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals()
|
|
||||||
self.assertEqual(3, len(fl))
|
|
||||||
|
|
||||||
# test boundingbox option
|
|
||||||
el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True)).vals()
|
|
||||||
self.assertEqual(1, len(el))
|
|
||||||
fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True)).vals()
|
|
||||||
self.assertEqual(0, len(fl))
|
|
||||||
fl = c.faces(selectors.BoxSelector((-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True)).vals()
|
|
||||||
self.assertEqual(1, len(fl))
|
|
||||||
|
|
||||||
def testAndSelector(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
S = selectors.StringSyntaxSelector
|
|
||||||
BS = selectors.BoxSelector
|
|
||||||
|
|
||||||
el = c.edges(selectors.AndSelector(S('|X'), BS((-2,-2,0.1), (2,2,2)))).vals()
|
|
||||||
self.assertEqual(2, len(el))
|
|
||||||
|
|
||||||
# test 'and' (intersection) operator
|
|
||||||
el = c.edges(S('|X') & BS((-2,-2,0.1), (2,2,2))).vals()
|
|
||||||
self.assertEqual(2, len(el))
|
|
||||||
|
|
||||||
def testSumSelector(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
S = selectors.StringSyntaxSelector
|
|
||||||
|
|
||||||
fl = c.faces(selectors.SumSelector(S(">Z"), S("<Z"))).vals()
|
|
||||||
self.assertEqual(2, len(fl))
|
|
||||||
el = c.edges(selectors.SumSelector(S("|X"), S("|Y"))).vals()
|
|
||||||
self.assertEqual(8, len(el))
|
|
||||||
|
|
||||||
# test the sum operator
|
|
||||||
fl = c.faces(S(">Z") + S("<Z")).vals()
|
|
||||||
self.assertEqual(2, len(fl))
|
|
||||||
el = c.edges(S("|X") + S("|Y")).vals()
|
|
||||||
self.assertEqual(8, len(el))
|
|
||||||
|
|
||||||
def testSubtractSelector(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
S = selectors.StringSyntaxSelector
|
|
||||||
|
|
||||||
fl = c.faces(selectors.SubtractSelector(S("#Z"), S(">X"))).vals()
|
|
||||||
self.assertEqual(3, len(fl))
|
|
||||||
|
|
||||||
# test the subtract operator
|
|
||||||
fl = c.faces(S("#Z") - S(">X")).vals()
|
|
||||||
self.assertEqual(3, len(fl))
|
|
||||||
|
|
||||||
def testInverseSelector(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
S = selectors.StringSyntaxSelector
|
|
||||||
|
|
||||||
fl = c.faces(selectors.InverseSelector(S('>Z'))).vals()
|
|
||||||
self.assertEqual(5, len(fl))
|
|
||||||
el = c.faces('>Z').edges(selectors.InverseSelector(S('>X'))).vals()
|
|
||||||
self.assertEqual(3, len(el))
|
|
||||||
|
|
||||||
# test invert operator
|
|
||||||
fl = c.faces(-S('>Z')).vals()
|
|
||||||
self.assertEqual(5, len(fl))
|
|
||||||
el = c.faces('>Z').edges(-S('>X')).vals()
|
|
||||||
self.assertEqual(3, len(el))
|
|
||||||
|
|
||||||
def testFaceCount(self):
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
self.assertEqual( 6, c.faces().size() )
|
|
||||||
self.assertEqual( 2, c.faces("|Z").size() )
|
|
||||||
|
|
||||||
def testVertexFilter(self):
|
|
||||||
"test selecting vertices on a face"
|
|
||||||
c = CQ(makeUnitCube())
|
|
||||||
|
|
||||||
#TODO: filters work ok, but they are in global coordinates which sux. it would be nice
|
|
||||||
#if they were available in coordinates local to the selected face
|
|
||||||
|
|
||||||
v2 = c.faces("+Z").vertices("<XY")
|
|
||||||
self.assertEqual(1,v2.size() ) #another way
|
|
||||||
#make sure the vertex is the right one
|
|
||||||
|
|
||||||
self.assertTupleAlmostEquals((0.0,0.0,1.0),v2.val().toTuple() ,3)
|
|
|
@ -1,86 +0,0 @@
|
||||||
#system modules
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from tests import BaseTest
|
|
||||||
import FreeCAD
|
|
||||||
import Part
|
|
||||||
|
|
||||||
|
|
||||||
from cadquery import *
|
|
||||||
|
|
||||||
class TestCadObjects(BaseTest):
|
|
||||||
|
|
||||||
def testVectorConstructors(self):
|
|
||||||
v1 = Vector(1, 2, 3)
|
|
||||||
v2 = Vector((1, 2, 3))
|
|
||||||
v3 = Vector(FreeCAD.Base.Vector(1, 2, 3))
|
|
||||||
|
|
||||||
for v in [v1, v2, v3]:
|
|
||||||
self.assertTupleAlmostEquals((1, 2, 3), v.toTuple(), 4)
|
|
||||||
|
|
||||||
def testVertex(self):
|
|
||||||
"""
|
|
||||||
Tests basic vertex functions
|
|
||||||
"""
|
|
||||||
v = Vertex(Part.Vertex(1, 1, 1))
|
|
||||||
self.assertEqual(1, v.X)
|
|
||||||
self.assertEquals(Vector, type(v.Center()))
|
|
||||||
|
|
||||||
def testBasicBoundingBox(self):
|
|
||||||
v = Vertex(Part.Vertex(1, 1, 1))
|
|
||||||
v2 = Vertex(Part.Vertex(2, 2, 2))
|
|
||||||
self.assertEquals(BoundBox, type(v.BoundingBox()))
|
|
||||||
self.assertEquals(BoundBox, type(v2.BoundingBox()))
|
|
||||||
|
|
||||||
bb1 = v.BoundingBox().add(v2.BoundingBox())
|
|
||||||
|
|
||||||
self.assertEquals(bb1.xlen, 1.0)
|
|
||||||
|
|
||||||
def testEdgeWrapperCenter(self):
|
|
||||||
e = Edge(Part.makeCircle(2.0, FreeCAD.Base.Vector(1, 2, 3)))
|
|
||||||
|
|
||||||
self.assertTupleAlmostEquals((1.0, 2.0, 3.0), e.Center().toTuple(), 3)
|
|
||||||
|
|
||||||
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.assertEquals(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.assertEquals(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 testTranslate(self):
|
|
||||||
e = Shape.cast(Part.makeCircle(2.0, FreeCAD.Base.Vector(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(Part.makeLine((0, 0, 0), (1, 1, 0)))
|
|
||||||
self.assertEquals(2, len(e.Vertices()))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,43 +0,0 @@
|
||||||
"""
|
|
||||||
Tests basic workplane functionality
|
|
||||||
"""
|
|
||||||
#core modules
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
#my modules
|
|
||||||
from cadquery import *
|
|
||||||
from cadquery import exporters
|
|
||||||
from tests import BaseTest
|
|
||||||
|
|
||||||
class TestExporters(BaseTest):
|
|
||||||
|
|
||||||
def _exportBox(self,eType,stringsToFind):
|
|
||||||
"""
|
|
||||||
Exports a test object, and then looks for
|
|
||||||
all of the supplied strings to be in the result
|
|
||||||
returns the result in case the case wants to do more checks also
|
|
||||||
"""
|
|
||||||
p = Workplane("XY").box(1,2,3)
|
|
||||||
s = StringIO.StringIO()
|
|
||||||
exporters.exportShape(p,eType,s,0.1)
|
|
||||||
|
|
||||||
result = s.getvalue()
|
|
||||||
#print result
|
|
||||||
for q in stringsToFind:
|
|
||||||
self.assertTrue(result.find(q) > -1 )
|
|
||||||
return result
|
|
||||||
|
|
||||||
def testSTL(self):
|
|
||||||
self._exportBox(exporters.ExportTypes.STL,['facet normal'])
|
|
||||||
|
|
||||||
def testSVG(self):
|
|
||||||
self._exportBox(exporters.ExportTypes.SVG,['<svg','<g transform'])
|
|
||||||
|
|
||||||
def testAMF(self):
|
|
||||||
self._exportBox(exporters.ExportTypes.AMF,['<amf units','</object>'])
|
|
||||||
|
|
||||||
def testSTEP(self):
|
|
||||||
self._exportBox(exporters.ExportTypes.STEP,['FILE_SCHEMA'])
|
|
||||||
|
|
||||||
def testTJS(self):
|
|
||||||
self._exportBox(exporters.ExportTypes.TJS,['vertices','formatVersion','faces'])
|
|
|
@ -1,54 +0,0 @@
|
||||||
"""
|
|
||||||
Tests file importers such as STEP
|
|
||||||
"""
|
|
||||||
#core modules
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
from cadquery import *
|
|
||||||
from cadquery import exporters
|
|
||||||
from cadquery import importers
|
|
||||||
from tests import BaseTest
|
|
||||||
|
|
||||||
#where unit test output will be saved
|
|
||||||
import sys
|
|
||||||
if sys.platform.startswith("win"):
|
|
||||||
OUTDIR = "c:/temp"
|
|
||||||
else:
|
|
||||||
OUTDIR = "/tmp"
|
|
||||||
|
|
||||||
|
|
||||||
class TestImporters(BaseTest):
|
|
||||||
def importBox(self, importType, fileName):
|
|
||||||
"""
|
|
||||||
Exports a simple box to a STEP file and then imports it again
|
|
||||||
:param importType: The type of file we're importing (STEP, STL, etc)
|
|
||||||
:param fileName: The path and name of the file to write to
|
|
||||||
"""
|
|
||||||
#We're importing a STEP file
|
|
||||||
if importType == importers.ImportTypes.STEP:
|
|
||||||
#We first need to build a simple shape to export
|
|
||||||
shape = Workplane("XY").box(1, 2, 3).val()
|
|
||||||
|
|
||||||
#Export the shape to a temporary file
|
|
||||||
shape.exportStep(fileName)
|
|
||||||
|
|
||||||
# Reimport the shape from the new STEP file
|
|
||||||
importedShape = importers.importShape(importType,fileName)
|
|
||||||
|
|
||||||
#Check to make sure we got a solid back
|
|
||||||
self.assertTrue(importedShape.val().ShapeType() == "Solid")
|
|
||||||
|
|
||||||
#Check the number of faces and vertices per face to make sure we have a box shape
|
|
||||||
self.assertTrue(importedShape.faces("+X").size() == 1 and importedShape.faces("+X").vertices().size() == 4)
|
|
||||||
self.assertTrue(importedShape.faces("+Y").size() == 1 and importedShape.faces("+Y").vertices().size() == 4)
|
|
||||||
self.assertTrue(importedShape.faces("+Z").size() == 1 and importedShape.faces("+Z").vertices().size() == 4)
|
|
||||||
|
|
||||||
def testSTEP(self):
|
|
||||||
"""
|
|
||||||
Tests STEP file import
|
|
||||||
"""
|
|
||||||
self.importBox(importers.ImportTypes.STEP, OUTDIR + "/tempSTEP.step")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import unittest
|
|
||||||
unittest.main()
|
|
|
@ -1,125 +0,0 @@
|
||||||
"""
|
|
||||||
Tests basic workplane functionality
|
|
||||||
"""
|
|
||||||
#core modules
|
|
||||||
|
|
||||||
#my modules
|
|
||||||
from cadquery import *
|
|
||||||
from tests import BaseTest,toTuple
|
|
||||||
|
|
||||||
xAxis_ = Vector(1, 0, 0)
|
|
||||||
yAxis_ = Vector(0, 1, 0)
|
|
||||||
zAxis_ = Vector(0, 0, 1)
|
|
||||||
xInvAxis_ = Vector(-1, 0, 0)
|
|
||||||
yInvAxis_ = Vector(0, -1, 0)
|
|
||||||
zInvAxis_ = Vector(0, 0, -1)
|
|
||||||
|
|
||||||
class TestWorkplanes(BaseTest):
|
|
||||||
|
|
||||||
def testYZPlaneOrigins(self):
|
|
||||||
#xy plane-- with origin at x=0.25
|
|
||||||
base = Vector(0.25,0,0)
|
|
||||||
p = Plane(base, Vector(0,1,0), Vector(1,0,0))
|
|
||||||
|
|
||||||
#origin is always (0,0,0) in local coordinates
|
|
||||||
self.assertTupleAlmostEquals((0,0,0), p.toLocalCoords(p.origin).toTuple() ,2 )
|
|
||||||
|
|
||||||
#(0,0,0) is always the original base in global coordinates
|
|
||||||
self.assertTupleAlmostEquals(base.toTuple(), p.toWorldCoords((0,0)).toTuple() ,2 )
|
|
||||||
|
|
||||||
def testXYPlaneOrigins(self):
|
|
||||||
base = Vector(0,0,0.25)
|
|
||||||
p = Plane(base, Vector(1,0,0), Vector(0,0,1))
|
|
||||||
|
|
||||||
#origin is always (0,0,0) in local coordinates
|
|
||||||
self.assertTupleAlmostEquals((0,0,0), p.toLocalCoords(p.origin).toTuple() ,2 )
|
|
||||||
|
|
||||||
#(0,0,0) is always the original base in global coordinates
|
|
||||||
self.assertTupleAlmostEquals(toTuple(base), p.toWorldCoords((0,0)).toTuple() ,2 )
|
|
||||||
|
|
||||||
def testXZPlaneOrigins(self):
|
|
||||||
base = Vector(0,0.25,0)
|
|
||||||
p = Plane(base, Vector(0,0,1), Vector(0,1,0))
|
|
||||||
|
|
||||||
#(0,0,0) is always the original base in global coordinates
|
|
||||||
self.assertTupleAlmostEquals(toTuple(base), p.toWorldCoords((0,0)).toTuple() ,2 )
|
|
||||||
|
|
||||||
#origin is always (0,0,0) in local coordinates
|
|
||||||
self.assertTupleAlmostEquals((0,0,0), p.toLocalCoords(p.origin).toTuple() ,2 )
|
|
||||||
|
|
||||||
def testPlaneBasics(self):
|
|
||||||
p = Plane.XY()
|
|
||||||
#local to world
|
|
||||||
self.assertTupleAlmostEquals((1.0,1.0,0),p.toWorldCoords((1,1)).toTuple(),2 )
|
|
||||||
self.assertTupleAlmostEquals((-1.0,-1.0,0), p.toWorldCoords((-1,-1)).toTuple(),2 )
|
|
||||||
|
|
||||||
#world to local
|
|
||||||
self.assertTupleAlmostEquals((-1.0,-1.0), p.toLocalCoords(Vector(-1,-1,0)).toTuple() ,2 )
|
|
||||||
self.assertTupleAlmostEquals((1.0,1.0), p.toLocalCoords(Vector(1,1,0)).toTuple() ,2 )
|
|
||||||
|
|
||||||
p = Plane.YZ()
|
|
||||||
self.assertTupleAlmostEquals((0,1.0,1.0),p.toWorldCoords((1,1)).toTuple() ,2 )
|
|
||||||
|
|
||||||
#world to local
|
|
||||||
self.assertTupleAlmostEquals((1.0,1.0), p.toLocalCoords(Vector(0,1,1)).toTuple() ,2 )
|
|
||||||
|
|
||||||
p = Plane.XZ()
|
|
||||||
r = p.toWorldCoords((1,1)).toTuple()
|
|
||||||
self.assertTupleAlmostEquals((1.0,0.0,1.0),r ,2 )
|
|
||||||
|
|
||||||
#world to local
|
|
||||||
self.assertTupleAlmostEquals((1.0,1.0), p.toLocalCoords(Vector(1,0,1)).toTuple() ,2 )
|
|
||||||
|
|
||||||
def testOffsetPlanes(self):
|
|
||||||
"Tests that a plane offset from the origin works ok too"
|
|
||||||
p = Plane.XY(origin=(10.0,10.0,0))
|
|
||||||
|
|
||||||
|
|
||||||
self.assertTupleAlmostEquals((11.0,11.0,0.0),p.toWorldCoords((1.0,1.0)).toTuple(),2 )
|
|
||||||
self.assertTupleAlmostEquals((2.0,2.0), p.toLocalCoords(Vector(12.0,12.0,0)).toTuple() ,2 )
|
|
||||||
|
|
||||||
#TODO test these offsets in the other dimensions too
|
|
||||||
p = Plane.YZ(origin=(0,2,2))
|
|
||||||
self.assertTupleAlmostEquals((0.0,5.0,5.0), p.toWorldCoords((3.0,3.0)).toTuple() ,2 )
|
|
||||||
self.assertTupleAlmostEquals((10,10.0,0.0), p.toLocalCoords(Vector(0.0,12.0,12.0)).toTuple() ,2 )
|
|
||||||
|
|
||||||
p = Plane.XZ(origin=(2,0,2))
|
|
||||||
r = p.toWorldCoords((1.0,1.0)).toTuple()
|
|
||||||
self.assertTupleAlmostEquals((3.0,0.0,3.0),r ,2 )
|
|
||||||
self.assertTupleAlmostEquals((10.0,10.0), p.toLocalCoords(Vector(12.0,0.0,12.0)).toTuple() ,2 )
|
|
||||||
|
|
||||||
def testXYPlaneBasics(self):
|
|
||||||
p = Plane.named('XY')
|
|
||||||
self.assertTupleAlmostEquals(p.zDir.toTuple(), zAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4)
|
|
||||||
|
|
||||||
def testYZPlaneBasics(self):
|
|
||||||
p = Plane.named('YZ')
|
|
||||||
self.assertTupleAlmostEquals(p.zDir.toTuple(), xAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4)
|
|
||||||
|
|
||||||
def testZXPlaneBasics(self):
|
|
||||||
p = Plane.named('ZX')
|
|
||||||
self.assertTupleAlmostEquals(p.zDir.toTuple(), yAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4)
|
|
||||||
|
|
||||||
def testXZPlaneBasics(self):
|
|
||||||
p = Plane.named('XZ')
|
|
||||||
self.assertTupleAlmostEquals(p.zDir.toTuple(), yInvAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4)
|
|
||||||
|
|
||||||
def testYXPlaneBasics(self):
|
|
||||||
p = Plane.named('YX')
|
|
||||||
self.assertTupleAlmostEquals(p.zDir.toTuple(), zInvAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4)
|
|
||||||
|
|
||||||
def testZYPlaneBasics(self):
|
|
||||||
p = Plane.named('ZY')
|
|
||||||
self.assertTupleAlmostEquals(p.zDir.toTuple(), xInvAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4)
|
|
||||||
self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4)
|
|
|
@ -1,54 +0,0 @@
|
||||||
from cadquery import *
|
|
||||||
import unittest
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
import FreeCAD
|
|
||||||
|
|
||||||
import Part as P
|
|
||||||
from FreeCAD import Vector as V
|
|
||||||
|
|
||||||
|
|
||||||
def readFileAsString(fileName):
|
|
||||||
f= open(fileName, 'r')
|
|
||||||
s = f.read()
|
|
||||||
f.close()
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def writeStringToFile(strToWrite, fileName):
|
|
||||||
f = open(fileName, 'w')
|
|
||||||
f.write(strToWrite)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
|
|
||||||
def makeUnitSquareWire():
|
|
||||||
return Solid.cast(P.makePolygon([V(0, 0, 0), V(1, 0, 0), V(1, 1, 0), V(0, 1, 0), V(0, 0, 0)]))
|
|
||||||
|
|
||||||
|
|
||||||
def makeUnitCube():
|
|
||||||
return makeCube(1.0)
|
|
||||||
|
|
||||||
|
|
||||||
def makeCube(size):
|
|
||||||
return Solid.makeBox(size, size, size)
|
|
||||||
|
|
||||||
|
|
||||||
def toTuple(v):
|
|
||||||
"""convert a vector or a vertex to a 3-tuple: x,y,z"""
|
|
||||||
pnt = v
|
|
||||||
if type(v) == FreeCAD.Base.Vector:
|
|
||||||
return (v.Point.x, v.Point.y, v.Point.z)
|
|
||||||
elif type(v) == Vector:
|
|
||||||
return v.toTuple()
|
|
||||||
else:
|
|
||||||
raise RuntimeError("dont know how to convert type %s to tuple" % str(type(v)) )
|
|
||||||
|
|
||||||
|
|
||||||
class BaseTest(unittest.TestCase):
|
|
||||||
|
|
||||||
def assertTupleAlmostEquals(self, expected, actual, places):
|
|
||||||
for i, j in zip(actual, expected):
|
|
||||||
self.assertAlmostEquals(i, j, places)
|
|
||||||
|
|
||||||
__all__ = ['TestCadObjects', 'TestCadQuery', 'TestCQSelectors', 'TestWorkplanes', 'TestExporters', 'TestCQSelectors', 'TestImporters','TestCQGI']
|
|
Loading…
Reference in New Issue
Block a user