diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 75394d56f..761abbf84 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -300,6 +300,51 @@ def filter(self, objectList): return r +class RadiusNthSelector(Selector): + """ + 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. + """ + + def __init__(self, n, directionMax=True, tolerance=0.0001): + self.N = n + self.directionMax = directionMax + self.TOLERANCE = tolerance + + def filter(self, objectList): + # calculate how many digits of precision do we need + digits = -math.floor(math.log10(self.TOLERANCE)) + + # make a radius dict + # this is one to many mapping so I am using a default dict with list + objectDict = defaultdict(list) + for el in objectList: + try: + rad = el.radius() + except ValueError: + continue + objectDict[round(rad, digits)].append(el) + + # choose the Nth unique rounded distance + sortedObjectList = sorted( + list(objectDict.keys()), reverse=not self.directionMax + ) + try: + nth_distance = sortedObjectList[self.N] + except IndexError: + raise IndexError( + f"Attempted to access the {self.N}-th radius in a list {len(sortedObjectList)} long" + ) + + # map back to original objects and return + return objectDict[nth_distance] + + class DirectionMinMaxSelector(Selector): """ Selects objects closest or farthest in the specified direction diff --git a/doc/apireference.rst b/doc/apireference.rst index cd1ea622b..039e72c29 100644 --- a/doc/apireference.rst +++ b/doc/apireference.rst @@ -176,6 +176,7 @@ as a basis for futher operations. ParallelDirSelector DirectionSelector DirectionNthSelector + RadiusNthSelector PerpendicularDirSelector TypeSelector DirectionMinMaxSelector diff --git a/doc/classreference.rst b/doc/classreference.rst index 8076da813..551734b05 100644 --- a/doc/classreference.rst +++ b/doc/classreference.rst @@ -61,6 +61,7 @@ Selector Classes ParallelDirSelector DirectionSelector DirectionNthSelector + RadiusNthSelector PerpendicularDirSelector TypeSelector DirectionMinMaxSelector diff --git a/tests/test_selectors.py b/tests/test_selectors.py index 464587d27..e0de1de92 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -391,6 +391,84 @@ def testBox(self): ).vals() self.assertEqual(1, len(fl)) + def testRadiusNthSelector(self): + part = ( + Workplane() + .box(10, 10, 1) + .edges(">(1, 1, 0) and |Z") + .fillet(1) + .edges(">(-1, 1, 0) and |Z") + .fillet(1) + .edges(">(-1, -1, 0) and |Z") + .fillet(2) + .edges(">(1, -1, 0) and |Z") + .fillet(3) + .faces(">Z") + ) + # smallest radius is 1.0 + self.assertAlmostEqual( + part.edges(selectors.RadiusNthSelector(0)).val().radius(), 1.0 + ) + # there are two edges with the smallest radius + self.assertEqual(len(part.edges(selectors.RadiusNthSelector(0)).vals()), 2) + # next radius is 2.0 + self.assertAlmostEqual( + part.edges(selectors.RadiusNthSelector(1)).val().radius(), 2.0 + ) + # largest radius is 3.0 + self.assertAlmostEqual( + part.edges(selectors.RadiusNthSelector(-1)).val().radius(), 3.0 + ) + # accessing index 3 should be an IndexError + with self.assertRaises(IndexError): + part.edges(selectors.RadiusNthSelector(3)) + # reversed + self.assertAlmostEqual( + part.edges(selectors.RadiusNthSelector(0, directionMax=False)) + .val() + .radius(), + 3.0, + ) + + # test the selector on wires + wire_circles = ( + Workplane() + .circle(2) + .moveTo(10, 0) + .circle(2) + .moveTo(20, 0) + .circle(4) + .consolidateWires() + ) + self.assertEqual( + len(wire_circles.wires(selectors.RadiusNthSelector(0)).vals()), 2 + ) + self.assertEqual( + len(wire_circles.wires(selectors.RadiusNthSelector(1)).vals()), 1 + ) + self.assertAlmostEqual( + wire_circles.wires(selectors.RadiusNthSelector(0)).val().radius(), 2 + ) + self.assertAlmostEqual( + wire_circles.wires(selectors.RadiusNthSelector(1)).val().radius(), 4 + ) + + # a polygon with rounded corners has a radius, according to OCCT + loop_wire = Wire.makePolygon( + [Vector(-10, 0, 0), Vector(0, 10, 0), Vector(10, 0, 0),] + ) + loop_workplane = ( + Workplane().add(loop_wire.offset2D(1)).add(loop_wire.offset2D(2)) + ) + self.assertAlmostEqual( + loop_workplane.wires(selectors.RadiusNthSelector(0)).val().radius(), 1.0 + ) + self.assertAlmostEqual( + loop_workplane.wires(selectors.RadiusNthSelector(1)).val().radius(), 2.0 + ) + with self.assertRaises(IndexError): + loop_workplane.wires(selectors.RadiusNthSelector(2)) + def testAndSelector(self): c = CQ(makeUnitCube())