Merge branch 'master' into OCP

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -20,13 +20,22 @@ template = """
</div>
"""
template_content_indent = ' '
template_content_indent = " "
def cq_directive(name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine):
def cq_directive(
name,
arguments,
options,
content,
lineno,
content_offset,
block_text,
state,
state_machine,
):
# only consider inline snippets
plot_code = '\n'.join(content)
plot_code = "\n".join(content)
# Since we don't have a filename, use a hash based on the content
# the script must define a variable called 'out', which is expected to
@ -52,22 +61,20 @@ def cq_directive(name, arguments, options, content, lineno,
lines = []
# get rid of new lines
out_svg = out_svg.replace('\n', '')
out_svg = out_svg.replace("\n", "")
txt_align = "left"
if "align" in options:
txt_align = options['align']
txt_align = options["align"]
lines.extend((template % locals()).split('\n'))
lines.extend((template % locals()).split("\n"))
lines.extend(['::', ''])
lines.extend([' %s' % row.rstrip()
for row in plot_code.split('\n')])
lines.append('')
lines.extend(["::", ""])
lines.extend([" %s" % row.rstrip() for row in plot_code.split("\n")])
lines.append("")
if len(lines):
state_machine.insert_input(
lines, state_machine.input_lines.source(0))
state_machine.insert_input(lines, state_machine.input_lines.source(0))
return []
@ -77,9 +84,10 @@ def setup(app):
setup.config = app.config
setup.confdir = app.confdir
options = {'height': directives.length_or_unitless,
'width': directives.length_or_percentage_or_unitless,
'align': directives.unchanged
}
options = {
"height": directives.length_or_unitless,
"width": directives.length_or_percentage_or_unitless,
"align": directives.unchanged,
}
app.add_directive('cq_plot', cq_directive, True, (0, 2, 0), **options)
app.add_directive("cq_plot", cq_directive, True, (0, 2, 0), **options)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,103 +1,8 @@
from OCC.Display.WebGl.x3dom_renderer import X3DExporter
from OCC.Core.gp import gp_Quaternion, gp_Vec
from uuid import uuid4
from math import tan
from xml.etree import ElementTree
from IPython.display import SVG
from .geom import BoundBox
from .exporters import toString, ExportTypes
BOILERPLATE = \
'''
<link rel='stylesheet' type='text/css' href='http://www.x3dom.org/download/x3dom.css'></link>
<div style='height: {height}px; width: 100%;' width='100%' height='{height}px'>
<x3d style='height: {height}px; width: 100%;' id='{id}' width='100%' height='{height}px'>
<scene>
<Viewpoint position='{x},{y},{z}' centerOfRotation='{x0} {y0} {z0}' orientation='{rot}' fieldOfView='{fov}'></Viewpoint>
{src}
</scene>
</x3d>
</div>
<script>
if (document.getElementById('X3DOM_JS_MODULE') == null){{
var scr = document.createElement('script');
head = document.head || document.getElementsByTagName('head')[0];
scr.src = 'http://www.x3dom.org/download/x3dom.js';
scr.async = false;
scr.id = 'X3DOM_JS_MODULE';
scr.onload = function () {{
x3dom.reload();
}}
head.insertBefore(scr, head.lastChild);
}}
else if (typeof x3dom != 'undefined') {{ //call reload only if x3dom already loaded
x3dom.reload();
}}
//document.getElementById('{id}').runtime.fitAll()
</script>
'''
def display(shape):
#https://stackoverflow.com/questions/950087/how-do-i-include-a-javascript-file-in-another-javascript-file
#better if else
ROT = (0.77,0.3,0.55,1.28)
ROT = (0.,0,0,1.)
FOV = 0.2
def add_x3d_boilerplate(src, height=400, center=(0,0,0), d=(0,0,15), fov=FOV, rot='{} {} {} {} '.format(*ROT)):
return BOILERPLATE.format(src=src,
id=uuid4(),
height=height,
x=d[0],
y=d[1],
z=d[2],
x0=center[0],
y0=center[1],
z0=center[2],
fov=fov,
rot=rot)
def x3d_display(shape,
vertex_shader=None,
fragment_shader=None,
export_edges=True,
color=(1,1,0),
specular_color=(1,1,1),
shininess=0.4,
transparency=0.4,
line_color=(0,0,0),
line_width=2.,
mesh_quality=.3):
# Export to XML <Scene> tag
exporter = X3DExporter(shape,
vertex_shader,
fragment_shader,
export_edges,
color,
specular_color,
shininess,
transparency,
line_color,
line_width,
mesh_quality)
exporter.compute()
x3d_str = exporter.to_x3dfile_string(shape_id=0)
xml_et = ElementTree.fromstring(x3d_str)
scene_tag = xml_et.find('./Scene')
# Viewport Parameters
bb = BoundBox._fromTopoDS(shape)
d = max(bb.xlen,bb.ylen,bb.zlen)
c = bb.center
vec = gp_Vec(0,0,d/1.5/tan(FOV/2))
quat = gp_Quaternion(*ROT)
vec = quat*(vec) + c.wrapped
# return boilerplate + Scene
return add_x3d_boilerplate(ElementTree.tostring(scene_tag).decode('utf-8'),
d=(vec.X(),vec.Y(),vec.Z()),
center=(c.x,c.y,c.z))
return SVG(toString(shape, ExportTypes.SVG))

File diff suppressed because it is too large Load Diff

View File

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