From bcea03d4d6c9f1d6209465cb5cedfc388010721a Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 27 Apr 2016 12:25:24 -0400 Subject: [PATCH 01/19] Added v0.5.2 Info --- changes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changes.md b/changes.md index 0d23132..5feab54 100644 --- a/changes.md +++ b/changes.md @@ -84,3 +84,7 @@ v0.5.1 ------ * Mirroring fixes (thanks @huskier) * Added a mirroring example (thanks @huskier) + +v0.5.2 +------ + * Added the sweep operation #33 From 32160d16a26de9cbb9a67c4190c36b2166abd0f8 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 22 May 2016 20:31:27 +0200 Subject: [PATCH 02/19] Symmetric extrude with respect to the workplane Added additional parameter (both=Flase by default) to the extrude method which allows to symmetrically extrude with respect to he current workplane --- cadquery/cq.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 2ac9d7e..b2d3a50 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1967,7 +1967,7 @@ class Workplane(CQ): if clean: newS = newS.clean() return newS - def extrude(self, distance, combine=True, clean=True): + def extrude(self, distance, combine=True, clean=True, both=False): """ Use all un-extruded wires in the parent chain to create a prismatic solid. @@ -1975,6 +1975,7 @@ class Workplane(CQ): :type distance: float, negative means opposite the normal direction :param boolean combine: True to combine the resulting solid with parent solids if found. :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :param boolean both: extrude in both directions symmetrically :return: a CQ object with the resulting solid selected. extrude always *adds* material to a part. @@ -1990,8 +1991,9 @@ class Workplane(CQ): Support for non-prismatic extrusion ( IE, sweeping along a profile, not just perpendicular to the plane extrude to surface. this is quite tricky since the surface selected may not be planar - """ - r = self._extrude(distance) # returns a Solid (or a compound if there were multiple) + """ + r = self._extrude(distance,both=both) # returns a Solid (or a compound if there were multiple) + if combine: newS = self._combineWithBase(r) else: @@ -2254,11 +2256,12 @@ class Workplane(CQ): return self.newObject([r]) - def _extrude(self, distance): + def _extrude(self, distance, both=False): """ Make a prismatic solid from the existing set of pending wires. :param distance: distance to extrude + :param boolean both: extrude in both directions symmetrically :return: a FreeCAD solid, suitable for boolean operations. This method is a utility method, primarily for plugin and internal use. @@ -2305,6 +2308,10 @@ class Workplane(CQ): for ws in wireSets: thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) toFuse.append(thisObj) + + if both: + thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir.multiply(-1.)) + toFuse.append(thisObj) return Compound.makeCompound(toFuse) From 58683d0eb015c101bca7f385b0a18049919b819a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 22 May 2016 21:39:54 +0200 Subject: [PATCH 03/19] Testcase for symmetric extrusion Added a test case for symmetric extrusion operation. --- tests/TestCadQuery.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index 7947ee1..f12165a 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -1387,3 +1387,27 @@ class TestCadQuery(BaseTest): result =topOfLid.union(bottom) self.saveModel(result) + + def testExtrude(self): + """ + Test symmetric extrude + """ + r = 1. + h = 1. + decimal_places = 9. + + #extrude symmetrically + s = Workplane("XY").circle(r).extrude(h,both=True) + + top_face = s.faces(">Z") + bottom_face = s.faces(" Date: Mon, 23 May 2016 10:33:23 -0400 Subject: [PATCH 04/19] Added v1.0.0 changes section. --- changes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changes.md b/changes.md index 5feab54..bd6c42a 100644 --- a/changes.md +++ b/changes.md @@ -88,3 +88,7 @@ v0.5.1 v0.5.2 ------ * Added the sweep operation #33 + +v1.0.0 (unreleased) +------ + * Added an option to do symmetric extrusion about the workplane (thanks @adam-urbanczyk) From a54a81938c6a49ada276fabbeef9d332e6b851b6 Mon Sep 17 00:00:00 2001 From: Jeremy Mack Wright Date: Mon, 23 May 2016 12:17:04 -0400 Subject: [PATCH 05/19] Changed travis.yml to use FreeCAD stable rather than daily PPA. --- .coverage | Bin 8468 -> 10072 bytes .travis.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.coverage b/.coverage index 0a4ac1cdc4f9f8b8c7b1f3762d27598e6120b97c..09e0f7502127456d239b7b6310992fbfa7889686 100644 GIT binary patch literal 10072 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 literal 8468 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 diff --git a/.travis.yml b/.travis.yml index e4e38b0..2f3e05c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python before_install: -- sudo add-apt-repository -y ppa:freecad-maintainers/freecad-daily +- sudo add-apt-repository -y ppa:freecad-maintainers/freecad-stable - sudo apt-get update -qq install: - sudo apt-get install -y freecad freecad-doc From 875b64a63939a8ed769b0ea74631c53244710865 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 23 May 2016 22:01:05 +0200 Subject: [PATCH 06/19] Started working on nth selector Implementing using ordered dict --- cadquery/selectors.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cadquery/selectors.py b/cadquery/selectors.py index be07d7b..246db30 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -306,12 +306,20 @@ class DirectionMinMaxSelector(Selector): # pnt = tShape.Center() #return pnt.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(objectDict.items(), + key=lambda x: x[0])) + # find out the max/min distance if self.directionMax: - d = max(map(distance, objectList)) + d = objectDict.keys()[-1] else: - d = min(map(distance, objectList)) - + d = objectDict.keys()[0] + # return all objects at the max/min distance (within a tolerance) return filter(lambda o: abs(d - distance(o)) < self.TOLERANCE, objectList) From 1ac5937f963c95b58857013512ff8ed1b6b8c82b Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 2 Jun 2016 21:49:15 +0200 Subject: [PATCH 07/19] Preliminary implementation of Nth selector Not yet tested --- cadquery/selectors.py | 58 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 246db30..81a7ba2 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -323,6 +323,64 @@ class DirectionMinMaxSelector(Selector): # return all objects at the max/min distance (within a tolerance) return filter(lambda o: abs(d - distance(o)) < self.TOLERANCE, objectList) +class DirectionNthSelector(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, n, directionMax=True, tolerance=0.0001): + self.vector = vector + self.max = max + self.directionMax = directionMax + self.TOLERANCE = tolerance + if directionMax: + self.N = n + else: + self.N = -n + + 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) + + #make and distance to object dict + objectDict = {distance(el) : el for el in objectList} + #calculate how many digits of precision do we need + digits = int(1/self.TOLERANCE) + # create a rounded distance to original distance mapping (implicitly perfroms unique operation) + dist_round_dist = {round(d,digits) : d for d in objectDict.keys()} + # choose the Nth unique rounded distance + nth_d = dist_round_dist[sorted(dist_round_dist.keys())[self.N]] + + # map back to original objects and return + return [objectDict[d] for d in objectDict.keys() if abs(d-nth_d) < self.TOLERANCE] + class BinarySelector(Selector): """ Base class for selectors that operates with two other From 788bc58244dac82201282c72c447633c71e9de9e Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 5 Jun 2016 16:54:03 +0200 Subject: [PATCH 08/19] DirectionNthSelector inherits from ParallelDirSelector DirectionNthSelector will only consider objects parallel | normal to the specified direction --- cadquery/selectors.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 81a7ba2..0fe1f76 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -323,7 +323,7 @@ class DirectionMinMaxSelector(Selector): # return all objects at the max/min distance (within a tolerance) return filter(lambda o: abs(d - distance(o)) < self.TOLERANCE, objectList) -class DirectionNthSelector(Selector): +class DirectionNthSelector(ParallelDirSelector): """ Selects objects closest or farthest in the specified direction Used for faces, points, and edges @@ -350,19 +350,21 @@ class DirectionNthSelector(Selector): """ def __init__(self, vector, n, directionMax=True, tolerance=0.0001): - self.vector = vector + self.direction = vector self.max = max self.directionMax = directionMax self.TOLERANCE = tolerance if directionMax: - self.N = n + self.N = n #do we want indexing from 0 or from 1? else: self.N = -n - + def filter(self,objectList): + #select first the objects that are normal/parallel to a given dir + objectList = super(DirectionNthSelector,self).filter(objectList) def distance(tShape): - return tShape.Center().dot(self.vector) + return tShape.Center().dot(self.direction) #if tShape.ShapeType == 'Vertex': # pnt = tShape.Point #else: From 6678d3f5468031585d3b5ddd2be8e30fd786e9e0 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 5 Jun 2016 16:54:29 +0200 Subject: [PATCH 09/19] Implemented test-case for DirectionNthSelector --- tests/TestCQSelectors.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/TestCQSelectors.py b/tests/TestCQSelectors.py index f90e14b..7843b12 100644 --- a/tests/TestCQSelectors.py +++ b/tests/TestCQSelectors.py @@ -166,7 +166,25 @@ class TestCQSelectors(BaseTest): # test the case of multiple objects at the same distance el = c.edges(" Date: Sun, 5 Jun 2016 21:26:26 +0200 Subject: [PATCH 10/19] Updated docstring for DirectionNthSelector and extended the related test-case --- cadquery/selectors.py | 25 ++++--------------------- tests/TestCQSelectors.py | 6 +++++- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 0fe1f76..14fdce2 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -325,29 +325,12 @@ class DirectionMinMaxSelector(Selector): class DirectionNthSelector(ParallelDirSelector): """ - Selects objects closest or farthest in the specified direction - Used for faces, points, and edges + Selects nth object parallel (or normal) to the specified direction + Used for faces 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. - + Linear Edges + Planar Faces """ def __init__(self, vector, n, directionMax=True, tolerance=0.0001): self.direction = vector diff --git a/tests/TestCQSelectors.py b/tests/TestCQSelectors.py index 7843b12..05f9314 100644 --- a/tests/TestCQSelectors.py +++ b/tests/TestCQSelectors.py @@ -174,6 +174,10 @@ class TestCQSelectors(BaseTest): val = c.faces(selectors.DirectionNthSelector(Vector(1,0,0),1)).val() self.assertAlmostEqual(val.Center().x,-1.5) + #2nd face with inversed selection vector + val = c.faces(selectors.DirectionNthSelector(Vector(-1,0,0),1)).val() + self.assertAlmostEqual(val.Center().x,1.5) + #2nd last face val = c.faces(selectors.DirectionNthSelector(Vector(1,0,0),-2)).val() self.assertAlmostEqual(val.Center().x,1.5) @@ -182,7 +186,7 @@ class TestCQSelectors(BaseTest): val = c.faces(selectors.DirectionNthSelector(Vector(1,0,0),-1)).val() self.assertAlmostEqual(val.Center().x,2.5) - #check if the selected face if normal to given Vector + #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): From 7d7591719b9385f63a8efa92365e7033425cd481 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 20 Jun 2016 21:48:25 +0200 Subject: [PATCH 11/19] Initial implementation of the selector grammar using PyParsing For now the grammar is defined. It is not yet used in the StringSyntaxSelector --- cadquery/selectors.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 14fdce2..6cad19f 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -20,6 +20,7 @@ import re import math from cadquery import Vector,Edge,Vertex,Face,Solid,Shell,Compound +from pyparsing import Literal,Word,nums,Optional,Combine,oneOf class Selector(object): @@ -418,6 +419,7 @@ class InverseSelector(Selector): # 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 @@ -455,6 +457,8 @@ class StringSyntaxSelector(Selector): """ def __init__(self,selectorString): + + self._expr = self._makeGrammar() self.axes = { 'X': Vector(1,0,0), @@ -486,6 +490,49 @@ class StringSyntaxSelector(Selector): else: raise ValueError ("Selector String format must be [-+<>|#%] X|Y|Z ") + def _makeGrammar(self): + ''' + 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 + comma + floatn + comma + floatn + rbracket) + + #direction definition + direction = oneOf(['X','Y','Z','XY','XZ','YZ']) | vector + direction = direction.setResultsName('dir') + + #CQ type definition + cqtype = oneOf(['Plane','Cylinder','Sphere','Line','Circle','Arc']) + + #type operator + type_op = Literal('%') + + #direction operator + direction_op = oneOf(['>','<']) + + #index definition + ix_number = Optional('-')+Word(nums) + lsqbracket = Literal('[').suppress() + rsqbracket = Literal(']').suppress() + + index = lsqbracket + ix_number + rsqbracket + index = index.setResultsName('index') + + #other operators + other_op = oneOf(['|','#','+','-']) + + return (type_op('op') + cqtype('cqtype')) | (direction_op('op') + direction + Optional(index)) | (other_op('op') + direction) def _chooseSelector(self,selType,selAxis): """Sets up the underlying filters accordingly""" From 42d109325fe8a22716a7077b00d2c51c26da7bc3 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 20 Jun 2016 21:54:03 +0200 Subject: [PATCH 12/19] Change .travis.yml to include pyparsing --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2f3e05c..332476e 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 From f8c377c2f75cc098d3e0146bb85343b9e677ebce Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 20 Jun 2016 22:03:25 +0200 Subject: [PATCH 13/19] Fix typo in .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 332476e..3537f48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: - pip install coveralls - pip install Sphinx==1.3.2 - pip install travis-sphinx -- :pip install pyparsing +- pip install pyparsing script: - coverage run --source=cadquery ./runtests.py - travis-sphinx --nowarn --source=doc build From bde0fddc05396d63916092de08677591b3e80a06 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 21 Jun 2016 20:30:36 +0200 Subject: [PATCH 14/19] Initial almost working implementation of pyparsing based string selector Two test cases are still failing --- cadquery/selectors.py | 198 ++++++++++++++++++++++-------------------- 1 file changed, 106 insertions(+), 92 deletions(-) diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 6cad19f..7caa47c 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -420,6 +420,58 @@ class InverseSelector(Selector): 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','Line','Circle','Arc']) + + #type operator + type_op = Literal('%') + + #direction operator + direction_op = oneOf(['>','<']) + + #index definition + ix_number = Optional('-')+Word(nums) + lsqbracket = Literal('[').suppress() + rsqbracket = Literal(']').suppress() + + index = lsqbracket + ix_number + rsqbracket + + #other operators + other_op = oneOf(['|','#','+','-']) + + #named view + named_view = oneOf(['front','back','left','right','top','bottom']) + + return (type_op('type_op') + cqtype('cqtype')) | \ + (direction_op('dir_op') + direction('dir') + Optional(index('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 @@ -457,8 +509,6 @@ class StringSyntaxSelector(Selector): """ def __init__(self,selectorString): - - self._expr = self._makeGrammar() self.axes = { 'X': Vector(1,0,0), @@ -469,101 +519,65 @@ 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 ) + parsing_result = _grammar.parseString(selectorString) + self.mySelector = self._chooseSelector(parsing_result) + + def _chooseSelector(self,pr): + """ + Sets up the underlying filters accordingly + """ + if '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(pr.index),minmax) else: - self.mySelector = self._chooseSelector(m.groups()[0],m.groups()[1]) + 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 ") - - def _makeGrammar(self): - ''' - 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 + comma + floatn + comma + floatn + rbracket) - - #direction definition - direction = oneOf(['X','Y','Z','XY','XZ','YZ']) | vector - direction = direction.setResultsName('dir') - - #CQ type definition - cqtype = oneOf(['Plane','Cylinder','Sphere','Line','Circle','Arc']) - - #type operator - type_op = Literal('%') - - #direction operator - direction_op = oneOf(['>','<']) - - #index definition - ix_number = Optional('-')+Word(nums) - lsqbracket = Literal('[').suppress() - rsqbracket = Literal(']').suppress() - - index = lsqbracket + ix_number + rsqbracket - index = index.setResultsName('index') - - #other operators - other_op = oneOf(['|','#','+','-']) - - return (type_op('op') + cqtype('cqtype')) | (direction_op('op') + direction + Optional(index)) | (other_op('op') + direction) - - 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) + 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: + return Vector(float(pr.x),float(pr.y),float(pr.z)) else: - raise ValueError ("Selector String format must be [-+<>|] X|Y|Z ") - + return self.axes[pr.simple_dir] + def filter(self,objectList): """ selects minimum, maximum, positive or negative values relative to a direction From 76a2207a6eac88585c01c94c218957be1296839c Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 21 Jun 2016 20:30:53 +0200 Subject: [PATCH 15/19] Added simple test case for the string selector grammar --- tests/TestCQSelectors.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/TestCQSelectors.py b/tests/TestCQSelectors.py index 05f9314..4fa2b72 100644 --- a/tests/TestCQSelectors.py +++ b/tests/TestCQSelectors.py @@ -378,3 +378,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', + ' Date: Tue, 21 Jun 2016 20:50:28 +0200 Subject: [PATCH 16/19] First fully working implementation of the pyparsing based StringSelector Fixed all failing test cases from the previous commit: extended the grammat to handle upper and lowercase CQ types and fixed some typos --- cadquery/selectors.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 7caa47c..0183a83 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -20,7 +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 +from pyparsing import Literal,Word,nums,Optional,Combine,oneOf,\ + upcaseTokens,CaselessLiteral class Selector(object): @@ -444,8 +445,10 @@ def _makeGrammar(): direction = simple_dir('simple_dir') | vector('vector_dir') #CQ type definition - cqtype = oneOf(['Plane','Cylinder','Sphere','Line','Circle','Arc']) - + cqtype = oneOf(['Plane','Cylinder','Sphere','Cone','Line','Circle','Arc'], + caseless=True) + cqtype = cqtype.setParseAction(upcaseTokens) + #type operator type_op = Literal('%') @@ -465,7 +468,8 @@ def _makeGrammar(): #named view named_view = oneOf(['front','back','left','right','top','bottom']) - return (type_op('type_op') + cqtype('cqtype')) | \ + return direction('only_dir') | \ + (type_op('type_op') + cqtype('cq_type')) | \ (direction_op('dir_op') + direction('dir') + Optional(index('index'))) | \ (other_op('other_op') + direction('dir')) | \ named_view('named_view') @@ -549,7 +553,11 @@ class StringSyntaxSelector(Selector): """ Sets up the underlying filters accordingly """ - if 'type_op' in pr: + if 'only_dir' in pr: + vec = self._getVector(pr) + return DirectionSelector(vec) + + elif 'type_op' in pr: return TypeSelector(pr.cq_type) elif 'dir_op' in pr: From f6d532328f4ae530e4596081bca266295298d2e9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 29 Jun 2016 21:03:30 +0200 Subject: [PATCH 17/19] Extend testNthDistance to test DirectionNthSelector using the new string syntax too --- tests/TestCQSelectors.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/TestCQSelectors.py b/tests/TestCQSelectors.py index 4fa2b72..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()) From 5b0b5c0e96d1db65cb138a994fcc24e63c982c0a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 29 Jun 2016 21:04:28 +0200 Subject: [PATCH 18/19] Fix wrong handling of index in the string syntax selector --- cadquery/selectors.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 0183a83..5d81e8a 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -21,7 +21,7 @@ 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 + upcaseTokens,CaselessLiteral,Group class Selector(object): @@ -456,11 +456,11 @@ def _makeGrammar(): direction_op = oneOf(['>','<']) #index definition - ix_number = Optional('-')+Word(nums) + ix_number = Group(Optional('-')+Word(nums)) lsqbracket = Literal('[').suppress() rsqbracket = Literal(']').suppress() - index = lsqbracket + ix_number + rsqbracket + index = lsqbracket + ix_number('index') + rsqbracket #other operators other_op = oneOf(['|','#','+','-']) @@ -470,7 +470,7 @@ def _makeGrammar(): return direction('only_dir') | \ (type_op('type_op') + cqtype('cq_type')) | \ - (direction_op('dir_op') + direction('dir') + Optional(index('index'))) | \ + (direction_op('dir_op') + direction('dir') + Optional(index)) | \ (other_op('other_op') + direction('dir')) | \ named_view('named_view') @@ -565,7 +565,7 @@ class StringSyntaxSelector(Selector): minmax = self.operatorMinMax[pr.dir_op] if 'index' in pr: - return DirectionNthSelector(vec,int(pr.index),minmax) + return DirectionNthSelector(vec,int(''.join(pr.index.asList())),minmax) else: return DirectionMinMaxSelector(vec,minmax) @@ -582,7 +582,8 @@ class StringSyntaxSelector(Selector): Translate parsed vector string into a CQ Vector """ if 'vector_dir' in pr: - return Vector(float(pr.x),float(pr.y),float(pr.z)) + vec = pr.vector_dir + return Vector(float(vec.x),float(vec.y),float(vec.z)) else: return self.axes[pr.simple_dir] From 7bb52eb859fcca4150e255b0b3ac4a13e4181f90 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 29 Jun 2016 22:30:32 -0400 Subject: [PATCH 19/19] Update changes.md --- changes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changes.md b/changes.md index bd6c42a..8a4cd0b 100644 --- a/changes.md +++ b/changes.md @@ -92,3 +92,4 @@ v0.5.2 v1.0.0 (unreleased) ------ * Added an option to do symmetric extrusion about the workplane (thanks @adam-urbanczyk) + * Extended selector syntax to include Nth selector and re-implemented selectors using pyparsing (thanks @adam-urbanczyk)