Skip to content
Open
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
69 changes: 39 additions & 30 deletions cadquery/hull.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Tuple, Union, Iterable, Set
from typing import Dict, List, Tuple, Union, Iterable, Set
from math import pi, sin, cos, atan2, sqrt, inf, degrees
from numpy import lexsort, argmin, argmax

Expand Down Expand Up @@ -77,16 +77,6 @@ def __init__(self, c: Point, r: float, a1: float, a2: float):
self.e = Point(c.x + r * cos(a2), c.y + r * sin(a2))
self.ac = 2 * pi - (a1 - a2)

def __hash__(self):

return hash((self.c, self.r, self.a1, self.a2))

def __eq__(self, other):

return type(self) == type(other) and (
(self.c, self.r, self.a1, self.a2) == (other.c, other.r, other.a1, other.a2)
)


def atan2p(x, y):

Expand All @@ -100,7 +90,7 @@ def atan2p(x, y):

def convert_and_validate(edges: Iterable[Edge]) -> Tuple[List[Arc], List[Point]]:

arcs: Set[Arc] = set()
arcs: Dict[Tuple[Point, float], Arc] = {}
points: Set[Point] = set()

for e in edges:
Expand All @@ -116,13 +106,18 @@ def convert_and_validate(edges: Iterable[Edge]) -> Tuple[List[Arc], List[Point]]
c = e.arcCenter()
r = e.radius()
a1, a2 = e._bounds()
p = Point(c.x, c.y)

if (p, r) in arcs:
a = arcs[p, r]
a1, a2 = min(a.a1, a1), max(a.a2, a2)

arcs.add(Arc(Point(c.x, c.y), r, a1, a2))
arcs[p, r] = Arc(p, r, a1, a2)

else:
raise ValueError("Unsupported geometry {gt}")

return list(arcs), list(points)
return list(arcs.values()), list(points)


def select_lowest_point(points: Points) -> Tuple[Point, int]:
Expand Down Expand Up @@ -192,6 +187,10 @@ def pt_pt(p1: Point, p2: Point) -> Tuple[float, Segment]:
return angle, Segment(p1, p2)


class NoTangent(Exception):
pass


def _pt_arc(p: Point, a: Arc) -> Tuple[float, float, float, float]:

x, y = p.x, p.y
Expand All @@ -201,6 +200,9 @@ def _pt_arc(p: Point, a: Arc) -> Tuple[float, float, float, float]:
dx, dy = x - xc, y - yc
l = sqrt(dx ** 2 + dy ** 2)

if l <= r:
raise NoTangent

x1 = r ** 2 / l ** 2 * dx - r / l ** 2 * sqrt(l ** 2 - r ** 2) * dy + xc
y1 = r ** 2 / l ** 2 * dy + r / l ** 2 * sqrt(l ** 2 - r ** 2) * dx + yc
x2 = r ** 2 / l ** 2 * dx + r / l ** 2 * sqrt(l ** 2 - r ** 2) * dy + xc
Expand Down Expand Up @@ -308,21 +310,27 @@ def arc_arc(a1: Arc, a2: Arc) -> Tuple[float, Segment]:
return angles[ix], segments[ix]


NO_TANGENT = inf, Segment(Point(inf, inf), Point(inf, inf))


def get_angle(current: Entity, e: Entity) -> Tuple[float, Segment]:

if current is e:
return inf, Segment(Point(inf, inf), Point(inf, inf))

if isinstance(current, Point):
if isinstance(e, Point):
return pt_pt(current, e)
return NO_TANGENT

try:
if isinstance(current, Point):
if isinstance(e, Point):
return pt_pt(current, e)
else:
return pt_arc(current, e)
else:
return pt_arc(current, e)
else:
if isinstance(e, Point):
return arc_pt(current, e)
else:
return arc_arc(current, e)
if isinstance(e, Point):
return arc_pt(current, e)
else:
return arc_arc(current, e)
except NoTangent:
return NO_TANGENT


def update_hull(
Expand Down Expand Up @@ -386,11 +394,6 @@ def find_hull(edges: Iterable[Edge]) -> Wire:
# split into arcs and points
arcs, points = convert_and_validate(edges)

# a lone circle is its own hull
if len(arcs) == 1 and not points:
a = arcs[0]
return Wire.assembleEdges([Edge.makeCircle(a.r, Vector(a.c.x, a.c.y))])

# select the starting element
start = select_lowest(arcs, points)
rv.append(start)
Expand Down Expand Up @@ -418,6 +421,12 @@ def find_hull(edges: Iterable[Edge]) -> Wire:
next_ix = int(argmin(angles))

if angles[next_ix] == inf:
# nothing reaches the largest circle: everything else is inside it
if len(rv) == 1 and start is max(arcs, key=lambda a: a.r, default=None):
return Wire.assembleEdges(
[Edge.makeCircle(start.r, Vector(start.c.x, start.c.y))]
)

raise ValueError("Hull could not be closed")

current_e, current_angle, finished = update_hull(
Expand Down
53 changes: 45 additions & 8 deletions tests/test_hull.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,14 @@ def test_collinear():

def test_eq():

a = hull.Arc(hull.Point(0.0, 0.0), 1.0, 0.0, 2 * pi)
b = hull.Arc(hull.Point(0.0, 0.0), 1.0, 0.0, 2 * pi)
p = hull.Point(0.0, 0.0)

assert a == b
assert hash(a) == hash(b)
assert p == hull.Point(0.0, 0.0)
assert hash(p) == hash(hull.Point(0.0, 0.0))

assert a != hull.Arc(hull.Point(0.0, 0.0), 2.0, 0.0, 2 * pi)
assert a != p
assert p != a
assert a != None
assert p != hull.Point(1.0, 0.0)
assert p != hull.Arc(p, 1.0, 0.0, 2 * pi)
assert p != None


def test_lines_only():
Expand Down Expand Up @@ -119,3 +115,44 @@ def test_arc_endpoints():

assert (a.s.x, a.s.y) == pytest.approx((11.0, 20.0))
assert (a.e.x, a.e.y) == pytest.approx((9.0, 20.0))


@pytest.mark.parametrize(
"inner",
[
cq.Edge.makeCircle(5.0, (2, 0, 0)),
cq.Edge.makeLine(cq.Vector(-3, 0), cq.Vector(3, 0)),
cq.Edge.makeLine(cq.Vector(0, 20), cq.Vector(0, 10)),
],
ids=["circle", "line", "line from the circle"],
)
def test_geometry_inside_circle(inner):
outer = [cq.Edge.makeCircle(20.0, (0, 0, 0)), cq.Edge.makeCircle(20.0, (60, 5, 0))]

assert area(outer + [inner]) == pytest.approx(area(outer))


def test_circle_with_nested_only():
edges = [cq.Edge.makeCircle(20.0, (0, 0, 0)), cq.Edge.makeCircle(5.0, (2, 0, 0))]

assert area(edges) == pytest.approx(400 * pi)


def test_coincident_arcs():
# the start is the circle's bottom only if the halves are read as one circle
halves = [
cq.Edge.makeCircle(5.0, (0, 0, 0), angle1=0, angle2=180),
cq.Edge.makeCircle(5.0, (0, 0, 0), angle1=180, angle2=360),
]
segment = cq.Edge.makeLine(cq.Vector(-2, -3), cq.Vector(2, -3))

for order in permutations(halves):
assert area(list(order) + [segment]) == pytest.approx(25 * pi)


def test_intersecting_circles():
# a fuse splits each circle at the seam and at the intersections
edges = cq.Sketch().push([(-19, 0), (19, 0)]).circle(35).reset()._faces.Edges()

assert len(edges) == 6
assert area(edges) == pytest.approx(38 * 70 + pi * 35 ** 2)
Loading