Skip to content

Commit d5ce132

Browse files
Added AreaNthSelector (#688)
* Added AreaNthSelector Added AreaNthSelector that is useful for nested features selection. Especially to select one of coplanar nested wires for subsequent extrusion, cutting or filleting. * LengthNthSelector * Explicitly marked _NthSelector class and _NthSelector.key(..) method as abstract using standard abc package * AreaNthSelector ignores "bad" Wires AreaNthSelector.key raises ValueError if temporary face creation fails for a wire for any reason (non-planar, non-closed). That causes _NthSelector that it inherits to ignore such wires. Done so for consistency with RadiusNthSelector that ignores anything it can not get radius from. Co-authored-by: Marcus Boyd <[email protected]>
1 parent 731122e commit d5ce132

File tree

6 files changed

+448
-4
lines changed

6 files changed

+448
-4
lines changed

cadquery/selectors.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,19 @@
1717
License along with this library; If not, see <http://www.gnu.org/licenses/>
1818
"""
1919

20+
from abc import abstractmethod, ABC
2021
import math
2122
from .occ_impl.geom import Vector
22-
from .occ_impl.shapes import Shape, Edge, Face, Wire, geom_LUT_EDGE, geom_LUT_FACE
23+
from .occ_impl.shapes import (
24+
Shape,
25+
Edge,
26+
Face,
27+
Wire,
28+
Shell,
29+
Solid,
30+
geom_LUT_EDGE,
31+
geom_LUT_FACE,
32+
)
2333
from pyparsing import (
2434
Literal,
2535
Word,
@@ -294,7 +304,7 @@ def filter(self, objectList: Sequence[Shape]) -> List[Shape]:
294304
return r
295305

296306

297-
class _NthSelector(Selector):
307+
class _NthSelector(Selector, ABC):
298308
"""
299309
An abstract class that provides the methods to select the Nth object/objects of an ordered list.
300310
"""
@@ -324,6 +334,7 @@ def filter(self, objectlist: Sequence[Shape]) -> List[Shape]:
324334

325335
return out
326336

337+
@abstractmethod
327338
def key(self, obj: Shape) -> float:
328339
"""
329340
Return the key for ordering. Can raise a ValueError if obj can not be
@@ -454,6 +465,89 @@ def filter(self, objectlist: Sequence[Shape]) -> List[Shape]:
454465
return objectlist
455466

456467

468+
class LengthNthSelector(_NthSelector):
469+
"""
470+
Select the object(s) with the Nth length
471+
472+
Applicability:
473+
All Edge and Wire objects
474+
"""
475+
476+
def key(self, obj: Shape) -> float:
477+
if isinstance(obj, (Edge, Wire)):
478+
return obj.Length()
479+
else:
480+
raise ValueError(
481+
f"LengthNthSelector supports only Edges and Wires, not {type(obj).__name__}"
482+
)
483+
484+
485+
class AreaNthSelector(_NthSelector):
486+
"""
487+
Selects the object(s) with Nth area
488+
489+
Applicability:
490+
Faces, Shells, Solids - Shape.Area() is used to compute area
491+
closed planar Wires - a temporary face is created to compute area
492+
493+
Will ignore non-planar or non-closed wires.
494+
495+
Among other things can be used to select one of
496+
the nested coplanar wires or faces.
497+
498+
For example to create a fillet on a shank:
499+
500+
result = (
501+
cq.Workplane("XY")
502+
.circle(5)
503+
.extrude(2)
504+
.circle(2)
505+
.extrude(10)
506+
.faces(">Z[-2]")
507+
.wires(AreaNthSelector(0))
508+
.fillet(2)
509+
)
510+
511+
Or to create a lip on a case seam:
512+
513+
result = (
514+
cq.Workplane("XY")
515+
.rect(20, 20)
516+
.extrude(10)
517+
.edges("|Z or <Z")
518+
.fillet(2)
519+
.faces(">Z")
520+
.shell(2)
521+
.faces(">Z")
522+
.wires(AreaNthSelector(-1))
523+
.toPending()
524+
.workplane()
525+
.offset2D(-1)
526+
.extrude(1)
527+
.faces(">Z[-2]")
528+
.wires(AreaNthSelector(0))
529+
.toPending()
530+
.workplane()
531+
.cutBlind(2)
532+
)
533+
"""
534+
535+
def key(self, obj: Shape) -> float:
536+
if isinstance(obj, (Face, Shell, Solid)):
537+
return obj.Area()
538+
elif isinstance(obj, Wire):
539+
try:
540+
return Face.makeFromWires(obj).Area()
541+
except Exception as ex:
542+
raise ValueError(
543+
f"Can not compute area of the Wire: {ex}. AreaNthSelector supports only closed planar Wires."
544+
)
545+
else:
546+
raise ValueError(
547+
f"AreaNthSelector supports only Wires, Faces, Shells and Solids, not {type(obj).__name__}"
548+
)
549+
550+
457551
class BinarySelector(Selector):
458552
"""
459553
Base class for selectors that operates with two other

doc/apireference.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ as a basis for futher operations.
177177
ParallelDirSelector
178178
DirectionSelector
179179
DirectionNthSelector
180+
LengthNthSelector
181+
AreaNthSelector
180182
RadiusNthSelector
181183
PerpendicularDirSelector
182184
TypeSelector

doc/classreference.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ Selector Classes
6666
CenterNthSelector
6767
DirectionMinMaxSelector
6868
DirectionNthSelector
69+
LengthNthSelector
70+
AreaNthSelector
6971
BinarySelector
7072
AndSelector
7173
SumSelector

examples/Ex026_Case_Seam_Lip.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import cadquery as cq
2+
from cadquery.selectors import AreaNthSelector
3+
4+
case_bottom = (
5+
cq.Workplane("XY")
6+
.rect(20, 20)
7+
.extrude(10) # solid 20x20x10 box
8+
.edges("|Z or <Z")
9+
.fillet(2) # rounding all edges except 4 edges of the top face
10+
.faces(">Z")
11+
.shell(2) # shell of thickness 2 with top face open
12+
.faces(">Z")
13+
.wires(AreaNthSelector(-1)) # selecting top outer wire
14+
.toPending()
15+
.workplane()
16+
.offset2D(-1) # creating centerline wire of case seam face
17+
.extrude(1) # covering the sell with temporary "lid"
18+
.faces(">Z[-2]")
19+
.wires(AreaNthSelector(0)) # selecting case crossection wire
20+
.toPending()
21+
.workplane()
22+
.cutBlind(2) # cutting through the "lid" leaving a lip on case seam surface
23+
)
24+
25+
# similar process repeated for the top part
26+
# but instead of "growing" an inner lip
27+
# material is removed inside case seam centerline
28+
# to create an outer lip
29+
case_top = (
30+
cq.Workplane("XY")
31+
.move(25)
32+
.rect(20, 20)
33+
.extrude(5)
34+
.edges("|Z or >Z")
35+
.fillet(2)
36+
.faces("<Z")
37+
.shell(2)
38+
.faces("<Z")
39+
.wires(AreaNthSelector(-1))
40+
.toPending()
41+
.workplane()
42+
.offset2D(-1)
43+
.cutBlind(-1)
44+
)
45+
46+
show_object(case_bottom)
47+
show_object(case_top, options={"alpha": 0.5})

tests/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ def toTuple(v):
4747

4848

4949
class BaseTest(unittest.TestCase):
50-
def assertTupleAlmostEquals(self, expected, actual, places):
50+
def assertTupleAlmostEquals(self, expected, actual, places, msg=None):
5151
for i, j in zip(actual, expected):
52-
self.assertAlmostEqual(i, j, places)
52+
self.assertAlmostEqual(i, j, places, msg=msg)
5353

5454

5555
__all__ = [

0 commit comments

Comments
 (0)