-
Notifications
You must be signed in to change notification settings - Fork 297
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
Fix some issues with Sketch.hull() #1344
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -70,8 +70,8 @@ def __init__(self, c: Point, r: float, a1: float, a2: float): | |
self.a1 = a1 | ||
self.a2 = a2 | ||
|
||
self.s = Point(r * cos(a1), r * sin(a1)) | ||
self.e = Point(r * cos(a2), r * sin(a2)) | ||
self.s = Point(c.x + r * cos(a1), c.y + r * sin(a1)) | ||
self.e = Point(c.x + r * cos(a2), c.y + r * sin(a2)) | ||
self.ac = 2 * pi - (a1 - a2) | ||
|
||
|
||
|
@@ -104,10 +104,12 @@ def convert_and_validate(edges: Iterable[Edge]) -> Tuple[List[Arc], List[Point]] | |
r = e.radius() | ||
a1, a2 = e._bounds() | ||
|
||
arcs.add(Arc(Point(c.x, c.y), r, a1, a2)) | ||
arc = Arc(Point(c.x, c.y), r, a1, a2) | ||
arcs.add(arc) | ||
points.update((arc.s, arc.e)) | ||
|
||
else: | ||
raise ValueError("Unsupported geometry {gt}") | ||
raise ValueError(f"Unsupported geometry {gt}") | ||
|
||
return list(arcs), list(points) | ||
|
||
|
@@ -187,6 +189,9 @@ def _pt_arc(p: Point, a: Arc) -> Tuple[float, float, float, float]: | |
xc, yc = a.c.x, a.c.y | ||
dx, dy = x - xc, y - yc | ||
l = sqrt(dx ** 2 + dy ** 2) | ||
ld = l ** 2 - r ** 2 | ||
if (ld < 0.001): | ||
return x, y, x, y | ||
|
||
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 | ||
|
@@ -195,33 +200,38 @@ def _pt_arc(p: Point, a: Arc) -> Tuple[float, float, float, float]: | |
|
||
return x1, y1, x2, y2 | ||
|
||
|
||
def pt_arc(p: Point, a: Arc) -> Tuple[float, Segment]: | ||
def pt_arc(angle: float, p: Point, a: Arc) -> Tuple[float, Segment]: | ||
|
||
x, y = p.x, p.y | ||
x1, y1, x2, y2 = _pt_arc(p, a) | ||
|
||
if ((x1, y1) == (x2, y2)): | ||
return inf, Segment(Point(inf, inf), Point(inf, inf)) | ||
|
||
angles = atan2p(x1 - x, y1 - y), atan2p(x2 - x, y2 - y) | ||
points = Point(x1, y1), Point(x2, y2) | ||
ix = int(argmin(angles)) | ||
ix = int(argmin([a + 2*pi if a < angle else a for a in angles])) | ||
|
||
return angles[ix], Segment(p, points[ix]) | ||
|
||
|
||
def arc_pt(a: Arc, p: Point) -> Tuple[float, Segment]: | ||
def arc_pt(angle: float, a: Arc, p: Point) -> Tuple[float, Segment]: | ||
|
||
x, y = p.x, p.y | ||
x1, y1, x2, y2 = _pt_arc(p, a) | ||
|
||
if ((x1, y1) == (x2, y2)): | ||
return inf, Segment(Point(inf, inf), Point(inf, inf)) | ||
|
||
angles = atan2p(x - x1, y - y1), atan2p(x - x2, y - y2) | ||
points = Point(x1, y1), Point(x2, y2) | ||
|
||
ix = int(argmax(angles)) | ||
ix = int(argmax([a + 2*pi if a < angle else a for a in angles])) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the angle loops around back past 0 radians, this caused the argmin/argmax of |
||
|
||
return angles[ix], Segment(points[ix], p) | ||
|
||
|
||
def arc_arc(a1: Arc, a2: Arc) -> Tuple[float, Segment]: | ||
def arc_arc(angle: float, a1: Arc, a2: Arc) -> Tuple[float, Segment]: | ||
|
||
r1 = a1.r | ||
xc1, yc1 = a1.c.x, a1.c.y | ||
|
@@ -292,24 +302,36 @@ def arc_arc(a1: Arc, a2: Arc) -> Tuple[float, Segment]: | |
Segment(Point(x12, y12), Point(x22, y22)), | ||
) | ||
|
||
return angles[ix], segments[ix] | ||
segment_angle = atan2p(segments[ix].b.x - xc2, segments[ix].b.y - yc2) | ||
if (segment_angle < a2.a1 or segment_angle > a2.a2): | ||
return inf, Segment(Point(inf, inf), Point(inf, inf)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this function that well, so this might have been from something else wrong and is now redundant, but somehow it was selecting a point not actually on the arc, so this if statement throws that out if that happens. I'd bet this isn't the best way to handle it and there's more edge cases that would fail here. |
||
|
||
return angles[ix], segments[ix] | ||
|
||
def get_angle(current: Entity, e: Entity) -> Tuple[float, Segment]: | ||
|
||
if current is e: | ||
def get_angle(angle: float, current: Entity, e: Entity) -> Tuple[float, Segment]: | ||
def center(e: Entity): | ||
if isinstance(e, Point): | ||
return e | ||
return e.c | ||
if center(current) == center(e): | ||
if (isinstance(current, Arc) and isinstance(e, Arc)): | ||
if (current.s == e.e and not e.s == current.e): | ||
return pt_pt(current.e, e.s) | ||
if (e.s == current.e and not current.s == e.e): | ||
return (current.a2 + pi) % (2 * pi), Segment(Point(inf, inf), Point(inf, inf)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These changes allow a circle that's broken into multiple arcs continue around the circle |
||
return inf, Segment(Point(inf, inf), Point(inf, inf)) | ||
|
||
if isinstance(current, Point): | ||
if isinstance(e, Point): | ||
return pt_pt(current, e) | ||
else: | ||
return pt_arc(current, e) | ||
return pt_arc(angle, current, e) | ||
else: | ||
if isinstance(e, Point): | ||
return arc_pt(current, e) | ||
return arc_pt(angle, current, e) | ||
else: | ||
return arc_arc(current, e) | ||
return arc_arc(angle, current, e) | ||
|
||
|
||
def update_hull( | ||
|
@@ -339,7 +361,8 @@ def finalize_hull(hull: Hull) -> Wire: | |
for el_p, el, el_n in zip(hull, hull[1:], hull[2:]): | ||
|
||
if isinstance(el, Segment): | ||
rv.append(Edge.makeLine(Vector(el.a.x, el.a.y), Vector(el.b.x, el.b.y))) | ||
if (not inf in [el.a.x, el.a.y, el.b.x, el.b.y]): | ||
rv.append(Edge.makeLine(Vector(el.a.x, el.a.y), Vector(el.b.x, el.b.y))) | ||
elif ( | ||
isinstance(el, Arc) | ||
and isinstance(el_p, Segment) | ||
|
@@ -348,9 +371,10 @@ def finalize_hull(hull: Hull) -> Wire: | |
a1 = degrees(atan2p(el_p.b.x - el.c.x, el_p.b.y - el.c.y)) | ||
a2 = degrees(atan2p(el_n.a.x - el.c.x, el_n.a.y - el.c.y)) | ||
|
||
rv.append( | ||
Edge.makeCircle(el.r, Vector(el.c.x, el.c.y), angle1=a1, angle2=a2) | ||
) | ||
if (a1 != a2): | ||
rv.append( | ||
Edge.makeCircle(el.r, Vector(el.c.x, el.c.y), angle1=a1, angle2=a2) | ||
) | ||
|
||
el1 = hull[1] | ||
if isinstance(el, Segment) and isinstance(el_n, Arc) and isinstance(el1, Segment): | ||
|
@@ -385,14 +409,17 @@ def find_hull(edges: Iterable[Edge]) -> Wire: | |
current_angle = 0.0 | ||
finished = False | ||
|
||
iterations = 0 | ||
|
||
# march around | ||
while not finished: | ||
while not finished and iterations < len(entities): | ||
iterations += 1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While working on this it was very easy for this to loop infinitely. Setting a sensible maximum number of iterations (if we've iterated more than the number of entities, something has gone wrong) seems better than having the system hang in an infinite loop. |
||
|
||
angles = [] | ||
segments = [] | ||
|
||
for e in entities: | ||
angle, segment = get_angle(current_e, e) | ||
angle, segment = get_angle(current_angle, current_e, e) | ||
angles.append(angle if angle >= current_angle else inf) | ||
segments.append(segment) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ def test_hull(): | |
|
||
h = hull.find_hull(edges) | ||
|
||
assert len(h.Vertices()) == 11 | ||
assert len(h.Vertices()) == 10 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Somehow my changes made this hull have one fewer vertices. Based on how the points and arcs are laid out, I'm guessing there were multiple possible next entities with the same angle and my hull changes made it skip one. There should probably be a 'tie-breaker' for choosing the smallest angle so that the system is more deterministic. (either always choosing the closer to ensure the loop always ends up closed, or choosing the further, but we'd have to be more careful about choosing the starting entity, lest we start on an entity we later skip and fail to close the loop) |
||
assert h.IsClosed() | ||
assert h.isValid() | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is when the point is on the arc's circle, so I return a no-length segment (which is later ignored). The point might not necessarily be on the arc though, so this should return a segment to the start or end of the arc.
This is taken care of elsewhere because I add the start and end points of the arc to the list of points in
convert_and_validate
, so if the next segment is between the point and a start or end point of an arc, it's handled elsewhere as apt_pt
comparison.