Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Radius selector #504

Merged
merged 4 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@
GeomFill_CorrectedFrenet,
GeomFill_TrihedronLaw,
)

# for catching exceptions
from OCP.Standard import Standard_NoSuchObject, Standard_Failure

from math import pi, sqrt
import warnings

Expand Down Expand Up @@ -1053,6 +1057,22 @@ def Length(self: Mixin1DProtocol) -> float:

return GCPnts_AbscissaPoint.Length_s(self._geomAdaptor())

def radius(self: Mixin1DProtocol) -> float:
"""
Calculate the radius.

Note that when applied to a Wire, the radius is simply the radius of the first edge.

:return: radius
:raises ValueError: if kernel can not reduce the shape to a circular edge
"""
geom = self._geomAdaptor()
try:
circ = geom.Circle()
except (Standard_NoSuchObject, Standard_Failure) as e:
raise ValueError("Shape could not be reduced to a circle") from e
return circ.Radius()

def IsClosed(self: Mixin1DProtocol) -> bool:

return BRep_Tool.IsClosed_s(self.wrapped)
Expand Down
45 changes: 45 additions & 0 deletions cadquery/selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions doc/apireference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ as a basis for futher operations.
ParallelDirSelector
DirectionSelector
DirectionNthSelector
RadiusNthSelector
PerpendicularDirSelector
TypeSelector
DirectionMinMaxSelector
Expand Down
1 change: 1 addition & 0 deletions doc/classreference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Selector Classes
ParallelDirSelector
DirectionSelector
DirectionNthSelector
RadiusNthSelector
PerpendicularDirSelector
TypeSelector
DirectionMinMaxSelector
Expand Down
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ channels:
dependencies:
- python>=3.6
- ipython
- ocp
- ocp=7.4
- pyparsing
- sphinx=3.2.1
- sphinx_rtd_theme
Expand Down
62 changes: 62 additions & 0 deletions tests/test_cad_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,68 @@ def testLocation(self):
== loc3.wrapped.Transformation().TranslationPart().Z()
)

def testEdgeWrapperRadius(self):

# get a radius from a simple circle
e0 = Edge.makeCircle(2.4)
self.assertAlmostEqual(e0.radius(), 2.4)

# radius of an arc
e1 = Edge.makeCircle(1.8, pnt=(5, 6, 7), dir=(1, 1, 1), angle1=20, angle2=30)
self.assertAlmostEqual(e1.radius(), 1.8)

# test value errors
e2 = Edge.makeEllipse(10, 20)
with self.assertRaises(ValueError):
e2.radius()

# radius from a wire
w0 = Wire.makeCircle(10, Vector(1, 2, 3), (-1, 0, 1))
self.assertAlmostEqual(w0.radius(), 10)

# radius from a wire with multiple edges
rad = 2.3
pnt = (7, 8, 9)
direction = (1, 0.5, 0.1)
w1 = Wire.assembleEdges(
[
Edge.makeCircle(rad, pnt, direction, 0, 10),
Edge.makeCircle(rad, pnt, direction, 10, 25),
Edge.makeCircle(rad, pnt, direction, 25, 230),
]
)
self.assertAlmostEqual(w1.radius(), rad)

# test value error from wire
w2 = Wire.makePolygon([Vector(-1, 0, 0), Vector(0, 1, 0), Vector(1, -1, 0),])
with self.assertRaises(ValueError):
w2.radius()

# (I think) the radius of a wire is the radius of it's first edge.
# Since this is stated in the docstring better make sure.
no_rad = Wire.assembleEdges(
[
Edge.makeLine(Vector(0, 0, 0), Vector(0, 1, 0)),
Edge.makeCircle(1.0, angle1=90, angle2=270),
]
)
with self.assertRaises(ValueError):
no_rad.radius()
yes_rad = Wire.assembleEdges(
[
Edge.makeCircle(1.0, angle1=90, angle2=270),
Edge.makeLine(Vector(0, -1, 0), Vector(0, 1, 0)),
]
)
self.assertAlmostEqual(yes_rad.radius(), 1.0)
many_rad = Wire.assembleEdges(
[
Edge.makeCircle(1.0, angle1=0, angle2=180),
Edge.makeCircle(3.0, pnt=Vector(2, 0, 0), angle1=180, angle2=359),
]
)
self.assertAlmostEqual(many_rad.radius(), 1.0)


if __name__ == "__main__":
unittest.main()
78 changes: 78 additions & 0 deletions tests/test_selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down