fromabcimportabstractmethod,ABCimportmathfrom.occ_impl.geomimportVectorfrom.occ_impl.shape_protocolsimport(ShapeProtocol,Shape1DProtocol,FaceProtocol,geom_LUT_EDGE,geom_LUT_FACE,)frompyparsingimport(pyparsing_common,Literal,Word,nums,Optional,Combine,oneOf,Group,infixNotation,opAssoc,)fromfunctoolsimportreducefromtypingimportIterable,List,Sequence,TypeVar,castShape=TypeVar("Shape",bound=ShapeProtocol)[docs]classSelector(object):""" Filters a list of objects. Filters must provide a single method that filters objects. """[docs]deffilter(self,objectList:Sequence[Shape])->List[Shape]:""" Filter the provided list. The default implementation returns the original list unfiltered. :param objectList: list to filter :type objectList: list of OCCT primitives :return: filtered list """returnlist(objectList) def__and__(self,other):returnAndSelector(self,other)def__add__(self,other):returnSumSelector(self,other)def__sub__(self,other):returnSubtractSelector(self,other)def__neg__(self):returnInverseSelector(self) [docs]classNearestToPointSelector(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 """[docs]def__init__(self,pnt):self.pnt=pnt [docs]deffilter(self,objectList:Sequence[Shape]):defdist(tShape):returntShape.Center().sub(Vector(*self.pnt)).Lengthreturn[min(objectList,key=dist)] [docs]classBoxSelector(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[docs]deffilter(self,objectList:Sequence[Shape]):result=[]x0,y0,z0=self.p0.toTuple()x1,y1,z1=self.p1.toTuple()defisInsideBox(p):# using XOR for checking if x/y/z is in between regardless# of order of x/y/z0 and x/y/z1return(((p.x<x0)^(p.x<x1))and((p.y<y0)^(p.y<y1))and((p.z<z0)^(p.z<z1)))foroinobjectList:ifself.test_boundingbox:bb=o.BoundingBox()ifisInsideBox(Vector(bb.xmin,bb.ymin,bb.zmin))andisInsideBox(Vector(bb.xmax,bb.ymax,bb.zmax)):result.append(o)else:ifisInsideBox(o.Center()):result.append(o)returnresult [docs]classBaseDirSelector(Selector):""" A selector that handles selection on the basis of a single direction vector. """def__init__(self,vector:Vector,tolerance:float=0.0001):self.direction=vectorself.tolerance=tolerance[docs]deftest(self,vec:Vector)->bool:"Test a specified vector. Subclasses override to provide other implementations"returnTrue [docs]deffilter(self,objectList:Sequence[Shape])->List[Shape]:""" 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=[]foroinobjectList:# no really good way to avoid a switch here, edges and faces are simply different!ifo.ShapeType()=="Face"ando.geomType()=="PLANE":# a face is only parallel to a direction if it is a plane, and# its normal is parallel to the dirtest_vector=cast(FaceProtocol,o).normalAt(None)elifo.ShapeType()=="Edge"ando.geomType()=="LINE":# an edge is parallel to a direction if its underlying geometry is plane or linetest_vector=cast(Shape1DProtocol,o).tangentAt()else:continueifself.test(test_vector):r.append(o)returnr [docs]classParallelDirSelector(BaseDirSelector):r""" 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 the normal parallel to the z direction, and is equivalent to:: CQ(aCube).faces("|Z") """[docs]deftest(self,vec:Vector)->bool:returnself.direction.cross(vec).Length<self.tolerance [docs]classDirectionSelector(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 the normal in the z direction, and is equivalent to:: CQ(aCube).faces("+Z") """[docs]deftest(self,vec:Vector)->bool:returnself.direction.getAngle(vec)<self.tolerance [docs]classPerpendicularDirSelector(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 the normal perpendicular to the z direction, and is equivalent to:: CQ(aCube).faces("#Z") """[docs]deftest(self,vec:Vector)->bool:returnabs(self.direction.getAngle(vec)-math.pi/2)<self.tolerance [docs]classTypeSelector(Selector):""" Selects objects having the prescribed geometry type. Applicability: Faces: PLANE, CYLINDER, CONE, SPHERE, TORUS, BEZIER, BSPLINE, REVOLUTION, EXTRUSION, OFFSET, OTHER Edges: LINE, CIRCLE, ELLIPSE, HYPERBOLA, PARABOLA, BEZIER, BSPLINE, OFFSET, OTHER You can use the string selector syntax. For example this:: CQ(aCube).faces(TypeSelector("PLANE")) will select 6 faces, and is equivalent to:: CQ(aCube).faces("%PLANE") """[docs]def__init__(self,typeString:str):self.typeString=typeString.upper() [docs]deffilter(self,objectList:Sequence[Shape])->List[Shape]:r=[]foroinobjectList:ifo.geomType()==self.typeString:r.append(o)returnr class_NthSelector(Selector,ABC):""" An abstract class that provides the methods to select the Nth object/objects of an ordered list. """def__init__(self,n:int,directionMax:bool=True,tolerance:float=0.0001):self.n=nself.directionMax=directionMaxself.tolerance=tolerancedeffilter(self,objectlist:Sequence[Shape])->List[Shape]:""" Return the nth object in the objectlist sorted by self.key and clustered if within self.tolerance. """iflen(objectlist)==0:# nothing to filterraiseValueError("Can not return the Nth element of an empty list")clustered=self.cluster(objectlist)ifnotself.directionMax:clustered.reverse()try:out=clustered[self.n]exceptIndexError:raiseIndexError(f"Attempted to access index{self.n} of a list with length{len(clustered)}")returnout@abstractmethoddefkey(self,obj:Shape)->float:""" Return the key for ordering. Can raise a ValueError if obj can not be used to create a key, which will result in obj being dropped by the clustering method. """raiseNotImplementedErrordefcluster(self,objectlist:Sequence[Shape])->List[List[Shape]]:""" Clusters the elements of objectlist if they are within tolerance. """key_and_obj=[]forobjinobjectlist:# Need to handle value errors, such as what occurs when you try to# access the radius of a straight linetry:key=self.key(obj)exceptValueError:# forget about this element and continuecontinuekey_and_obj.append((key,obj))key_and_obj.sort(key=lambdax:x[0])clustered=[[]]# type: List[List[Shape]]start=key_and_obj[0][0]forkey,objinkey_and_obj:ifabs(key-start)<=self.tolerance:clustered[-1].append(obj)else:clustered.append([obj])start=keyreturnclustered[docs]classRadiusNthSelector(_NthSelector):""" Select the object with the Nth radius. Applicability: All Edge and Wires. Will ignore any shape that can not be represented as a circle or an arc of a circle. """[docs]defkey(self,obj:Shape)->float:ifobj.ShapeType()in("Edge","Wire"):returncast(Shape1DProtocol,obj).radius()else:raiseValueError("Can not get a radius from this object") [docs]classCenterNthSelector(_NthSelector):""" Sorts objects into a list with order determined by the distance of their center projected onto the specified direction. Applicability: All Shapes. """def__init__(self,vector:Vector,n:int,directionMax:bool=True,tolerance:float=0.0001,):super().__init__(n,directionMax,tolerance)self.direction=vector[docs]defkey(self,obj:Shape)->float:returnobj.Center().dot(self.direction) [docs]classDirectionMinMaxSelector(CenterNthSelector):""" Selects objects closest or farthest in the specified direction. 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") """[docs]def__init__(self,vector:Vector,directionMax:bool=True,tolerance:float=0.0001):super().__init__(n=-1,vector=vector,directionMax=directionMax,tolerance=tolerance) # inherit from CenterNthSelector to get the CenterNthSelector.key method[docs]classDirectionNthSelector(ParallelDirSelector,CenterNthSelector):""" Filters for objects parallel (or normal) to the specified direction then returns the Nth one. Applicability: Linear Edges Planar Faces """def__init__(self,vector:Vector,n:int,directionMax:bool=True,tolerance:float=0.0001,):ParallelDirSelector.__init__(self,vector,tolerance)_NthSelector.__init__(self,n,directionMax,tolerance)[docs]deffilter(self,objectlist:Sequence[Shape])->List[Shape]:objectlist=ParallelDirSelector.filter(self,objectlist)objectlist=_NthSelector.filter(self,objectlist)returnobjectlist [docs]classLengthNthSelector(_NthSelector):""" Select the object(s) with the Nth length Applicability: All Edge and Wire objects """[docs]defkey(self,obj:Shape)->float:ifobj.ShapeType()in("Edge","Wire"):returncast(Shape1DProtocol,obj).Length()else:raiseValueError(f"LengthNthSelector supports only Edges and Wires, not{type(obj).__name__}") [docs]classAreaNthSelector(_NthSelector):""" Selects the object(s) with Nth area Applicability: - Faces, Shells, Solids - Shape.Area() is used to compute area - closed planar Wires - a temporary face is created to compute area Will ignore non-planar or non-closed wires. Among other things can be used to select one of the nested coplanar wires or faces. For example to create a fillet on a shank:: result = ( cq.Workplane("XY") .circle(5) .extrude(2) .circle(2) .extrude(10) .faces(">Z[-2]") .wires(AreaNthSelector(0)) .fillet(2) ) Or to create a lip on a case seam:: result = ( cq.Workplane("XY") .rect(20, 20) .extrude(10) .edges("|Z or <Z") .fillet(2) .faces(">Z") .shell(2) .faces(">Z") .wires(AreaNthSelector(-1)) .toPending() .workplane() .offset2D(-1) .extrude(1) .faces(">Z[-2]") .wires(AreaNthSelector(0)) .toPending() .workplane() .cutBlind(2) ) """[docs]defkey(self,obj:Shape)->float:ifobj.ShapeType()in("Face","Shell","Solid"):returnobj.Area()elifobj.ShapeType()=="Wire":try:fromcadquery.occ_impl.shapesimportFace,Wirereturnabs(Face.makeFromWires(cast(Wire,obj)).Area())exceptExceptionasex:raiseValueError(f"Can not compute area of the Wire:{ex}. AreaNthSelector supports only closed planar Wires.")else:raiseValueError(f"AreaNthSelector supports only Wires, Faces, Shells and Solids, not{type(obj).__name__}") [docs]classBinarySelector(Selector):""" Base class for selectors that operates with two other selectors. Subclass must implement the :filterResults(): method. """def__init__(self,left,right):self.left=leftself.right=right[docs]deffilter(self,objectList:Sequence[Shape]):returnself.filterResults(self.left.filter(objectList),self.right.filter(objectList)) deffilterResults(self,r_left,r_right):raiseNotImplementedError [docs]classAndSelector(BinarySelector):""" Intersection selector. Returns objects that is selected by both selectors. """deffilterResults(self,r_left,r_right):# return intersection of listsreturnlist(set(r_left)&set(r_right)) [docs]classSumSelector(BinarySelector):""" Union selector. Returns the sum of two selectors results. """deffilterResults(self,r_left,r_right):# return the union (no duplicates) of listsreturnlist(set(r_left+r_right)) [docs]classSubtractSelector(BinarySelector):""" Difference selector. Subtract results of a selector from another selectors results. """deffilterResults(self,r_left,r_right):returnlist(set(r_left)-set(r_right)) [docs]classInverseSelector(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[docs]deffilter(self,objectList:Sequence[Shape]):# note that Selector() selects everythingreturnSubtractSelector(Selector(),self.selector).filter(objectList) def_makeGrammar():""" Define the simple string selector grammar using PyParsing """# float definitionpoint=Literal(".")plusmin=Literal("+")|Literal("-")number=Word(nums)integer=Combine(Optional(plusmin)+number)floatn=Combine(integer+Optional(point+Optional(number)))# vector definitionlbracket=Literal("(")rbracket=Literal(")")comma=Literal(",")vector=Combine(lbracket+floatn("x")+comma+floatn("y")+comma+floatn("z")+rbracket,adjacent=False,)# direction definitionsimple_dir=oneOf(["X","Y","Z","XY","XZ","YZ"])direction=simple_dir("simple_dir")|vector("vector_dir")# CQ type definitioncqtype=oneOf(set(geom_LUT_EDGE.values())|set(geom_LUT_FACE.values()),caseless=True,)cqtype=cqtype.setParseAction(pyparsing_common.upcaseTokens)# type operatortype_op=Literal("%")# direction operatordirection_op=oneOf([">","<"])# center Nth operatorcenter_nth_op=oneOf([">>","<<"])# index definitionix_number=Group(Optional("-")+Word(nums))lsqbracket=Literal("[").suppress()rsqbracket=Literal("]").suppress()index=lsqbracket+ix_number("index")+rsqbracket# other operatorsother_op=oneOf(["|","#","+","-"])# named viewnamed_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))|(center_nth_op("center_nth_op")+direction("dir")+Optional(index))|(other_op("other_op")+direction("dir"))|named_view("named_view"))_grammar=_makeGrammar()# make a grammar instanceclass_SimpleStringSyntaxSelector(Selector):""" This is a private class that converts a parseResults object into a simple selector object """def__init__(self,parseResults):# define all token to object mappingsself.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),}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,">>":True,"<":False,"<<":False,}self.operator={"+":DirectionSelector,"-":lambdav:DirectionSelector(-v),"#":PerpendicularDirSelector,"|":ParallelDirSelector,}self.parseResults=parseResultsself.mySelector=self._chooseSelector(parseResults)def_chooseSelector(self,pr):""" Sets up the underlying filters accordingly """if"only_dir"inpr:vec=self._getVector(pr)returnDirectionSelector(vec)elif"type_op"inpr:returnTypeSelector(pr.cq_type)elif"dir_op"inpr:vec=self._getVector(pr)minmax=self.operatorMinMax[pr.dir_op]if"index"inpr:returnDirectionNthSelector(vec,int("".join(pr.index.asList())),minmax)else:returnDirectionMinMaxSelector(vec,minmax)elif"center_nth_op"inpr:vec=self._getVector(pr)minmax=self.operatorMinMax[pr.center_nth_op]if"index"inpr:returnCenterNthSelector(vec,int("".join(pr.index.asList())),minmax)else:returnCenterNthSelector(vec,-1,minmax)elif"other_op"inpr:vec=self._getVector(pr)returnself.operator[pr.other_op](vec)else:args=self.namedViews[pr.named_view]returnDirectionMinMaxSelector(*args)def_getVector(self,pr):""" Translate parsed vector string into a CQ Vector """if"vector_dir"inpr:vec=pr.vector_dirreturnVector(float(vec.x),float(vec.y),float(vec.z))else:returnself.axes[pr.simple_dir]deffilter(self,objectList:Sequence[Shape]):r""" selects minimum, maximum, positive or negative values relative to a direction ``[+|-|<|>|] <X|Y|Z>`` """returnself.mySelector.filter(objectList)def_makeExpressionGrammar(atom):""" Define the complex string selector grammar using PyParsing (which supports logical operations and nesting) """# define operatorsand_op=Literal("and")or_op=Literal("or")delta_op=oneOf(["exc","except"])not_op=Literal("not")defatom_callback(res):return_SimpleStringSyntaxSelector(res)# construct a simple selector from every matchedatom.setParseAction(atom_callback)# define callback functions for all operationsdefand_callback(res):# take every secend items, i.e. all operandsitems=res.asList()[0][::2]returnreduce(AndSelector,items)defor_callback(res):# take every secend items, i.e. all operandsitems=res.asList()[0][::2]returnreduce(SumSelector,items)defexc_callback(res):# take every secend items, i.e. all operandsitems=res.asList()[0][::2]returnreduce(SubtractSelector,items)defnot_callback(res):right=res.asList()[0][1]# take second item, i.e. the operandreturnInverseSelector(right)# construct the final grammar and set all the callbacksexpr=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),],)returnexpr_expression_grammar=_makeExpressionGrammar(_grammar)[docs]classStringSyntaxSelector(Selector):r""" 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 ***Modifiers*** 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`` or ``(x,y,z)`` which defines an arbitrary direction It is possible to combine simple selectors together using logical operations. The following operations are supported :and: Logical AND, e.g. >X and >Y :or: Logical OR, e.g. \|X or \|Y :not: Logical NOT, e.g. not #XY :exc(ept): Set difference (equivalent to AND NOT): \|X exc >Z Finally, it is also possible to use even more complex expressions with nesting and arbitrary number of terms, e.g. (not >X[0] and #XY) or >XY[0] Selectors are a complex topic: see :ref:`selector_reference` for more information """[docs]def__init__(self,selectorString):""" Feed the input string through the parser and construct an relevant complex selector object """self.selectorString=selectorStringparse_result=_expression_grammar.parseString(selectorString,parseAll=True)self.mySelector=parse_result.asList()[0] [docs]deffilter(self,objectList:Sequence[Shape]):""" Filter give object list through th already constructed complex selector object """returnself.mySelector.filter(objectList)