diff --git a/.travis.yml b/.travis.yml index 2f3e05c..3537f48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ install: - pip install coveralls - pip install Sphinx==1.3.2 - pip install travis-sphinx +- pip install pyparsing script: - coverage run --source=cadquery ./runtests.py - travis-sphinx --nowarn --source=doc build diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 14fdce2..5d81e8a 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -20,6 +20,8 @@ import re import math from cadquery import Vector,Edge,Vertex,Face,Solid,Shell,Compound +from pyparsing import Literal,Word,nums,Optional,Combine,oneOf,\ + upcaseTokens,CaselessLiteral,Group class Selector(object): @@ -418,6 +420,62 @@ class InverseSelector(Selector): # note that Selector() selects everything return SubtractSelector(Selector(), self.selector).filter(objectList) + +def _makeGrammar(): + """ + Define the string selector grammar using PyParsing + """ + + #float definition + 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) + + #direction definition + 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 = cqtype.setParseAction(upcaseTokens) + + #type operator + type_op = Literal('%') + + #direction operator + direction_op = oneOf(['>','<']) + + #index definition + ix_number = Group(Optional('-')+Word(nums)) + lsqbracket = Literal('[').suppress() + rsqbracket = Literal(']').suppress() + + index = lsqbracket + ix_number('index') + rsqbracket + + #other operators + other_op = oneOf(['|','#','+','-']) + + #named view + 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') + +_grammar = _makeGrammar() #make a grammar instance + class StringSyntaxSelector(Selector): """ Filter lists objects using a simple string syntax. All of the filters available in the string syntax @@ -465,58 +523,70 @@ class StringSyntaxSelector(Selector): 'XZ': Vector(1,0,1) } - namedViews = { - 'front': ('>','Z' ), - 'back': ('<','Z'), - 'left':('<', 'X'), - 'right': ('>', 'X'), - 'top': ('>','Y'), - 'bottom': ('<','Y') + 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) } + + self.operatorMinMax = { + '>' : True, + '<' : False, + '+' : True, + '-' : False + } + + self.operator = { + '+' : DirectionSelector, + '-' : DirectionSelector, + '#' : PerpendicularDirSelector, + '|' : ParallelDirSelector} + 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 + parsing_result = _grammar.parseString(selectorString) + self.mySelector = self._chooseSelector(parsing_result) + + def _chooseSelector(self,pr): + """ + Sets up the underlying filters accordingly + """ + if 'only_dir' in pr: + vec = self._getVector(pr) 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) + + elif 'type_op' in pr: + return TypeSelector(pr.cq_type) + + 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) + else: + return DirectionMinMaxSelector(vec,minmax) + + elif 'other_op' in pr: + vec = self._getVector(pr) + return self.operator[pr.other_op](vec) + else: - raise ValueError ("Selector String format must be [-+<>|] X|Y|Z ") - + args = self.namedViews[pr.named_view] + return DirectionMinMaxSelector(*args) + + def _getVector(self,pr): + """ + Translate parsed vector string into a CQ Vector + """ + if 'vector_dir' in pr: + vec = pr.vector_dir + return Vector(float(vec.x),float(vec.y),float(vec.z)) + else: + return self.axes[pr.simple_dir] + def filter(self,objectList): """ selects minimum, maximum, positive or negative values relative to a direction diff --git a/tests/TestCQSelectors.py b/tests/TestCQSelectors.py index 05f9314..fdb20cf 100644 --- a/tests/TestCQSelectors.py +++ b/tests/TestCQSelectors.py @@ -189,6 +189,28 @@ class TestCQSelectors(BaseTest): #check if the selected face if normal to the specified Vector self.assertAlmostEqual(val.normalAt().cross(Vector(1,0,0)).Length,0.0) + #repeat the test using string based selector + + #2nd face + val = c.faces('>(1,0,0)[1]').val() + self.assertAlmostEqual(val.Center().x,-1.5) + + #2nd face with inversed selection vector + val = c.faces('>(-1,0,0)[1]').val() + self.assertAlmostEqual(val.Center().x,1.5) + + #2nd last face + val = c.faces('>X[-2]').val() + self.assertAlmostEqual(val.Center().x,1.5) + + #Last face + val = c.faces('>X[-1]').val() + self.assertAlmostEqual(val.Center().x,2.5) + + #check if the selected face if normal to the specified Vector + self.assertAlmostEqual(val.normalAt().cross(Vector(1,0,0)).Length,0.0) + + def testNearestTo(self): c = CQ(makeUnitCube()) @@ -378,3 +400,30 @@ class TestCQSelectors(BaseTest): #make sure the vertex is the right one self.assertTupleAlmostEquals((0.0,0.0,1.0),v2.val().toTuple() ,3) + + def testGrammar(self): + """ + Test if reasonable string selector expressions parse without an error + """ + + gram = selectors._makeGrammar() + + expressions = ['+X ', + '-Y', + '|(1,0,0)', + '#(1.,1.4114,-0.532)', + '%Plane', + '>XZ', + '(1,4,55.)[20]', + '|XY', + '