-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathprinter.py
More file actions
218 lines (165 loc) · 8.09 KB
/
printer.py
File metadata and controls
218 lines (165 loc) · 8.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
from collections import defaultdict
from typing import Collection
from itertools import groupby
from fastcore.basics import first
from Geometry3D import Vector
from rich.pretty import pretty_repr
from util import attrhelper
from gcode_geom import GPoint, GSegment, GHalfLine
from logger import rprint
from geometry_helpers import visibility, too_close
from gcode_geom.utils import angsort
from steps import Steps
class Printer:
def __init__(self, initial_thread_path:GHalfLine):
#The current path of the thread: the current thread anchor and the
# direction of the thread.
self._thread_path = initial_thread_path
self.target:Vector|GPoint = None
#Create attributes which call Printer.attr_changed on change
x = property(**attrhelper('head_loc.x'))
y = property(**attrhelper('head_loc.y'))
z = property(**attrhelper('head_loc.z'))
@property
def anchor(self): return self.thread_path.point
@property
def xy(self): return self.x, self.y
@property
def xyz(self): return self.x, self.y, self.z
def __repr__(self):
return f'Printer(🧵={self.thread_path})'
@property
def thread_path(self): return self._thread_path
@thread_path.setter
def thread_path(self, new_path):
#This bit just does debug printing
if new_path.point != self.thread_path.point and new_path.angle != self.thread_path.angle:
raise ValueError("Simultaneously setting both point and angle for thread not allowed")
if new_path.point != self.thread_path.point:
rprint(f'[green]****[/] Move thread at angle {self.thread_path.angle}°'
f' from {self.thread_path.point} to {new_path.point}')
if new_path.angle != self.thread_path.angle:
rprint(f'[green]****[/] Rotate thread at point {self.thread_path.point}'
f' from {self.thread_path.angle}° to {new_path.angle}°')
#Assign even if they're the same, just in case the new one is a copy or
# something
self._thread_path = new_path
def move_thread_to(self, new_anchor:GPoint):
self.thread_path = GHalfLine(new_anchor, self.thread_path.vector)
def rotate_thread_to(self, target:Vector|GPoint):
self.target = target
self.thread_path = GHalfLine(self.thread_path.point, target)
def avoid_and_print(self, steps: Steps, avoid: Collection[GSegment]|None=None, extra_message='', avoid_by=1):
"""Loop to print everything in `avoid` without thread intersections."""
avoid = set(avoid or [])
repeats = 0
while avoid:
repeats += 1
rprint(f'Avoid and print {len(avoid)} segments, iteration {repeats}')
if repeats > 5: raise ValueError("Too many repeats")
with steps.new_step(f"Move thread to avoid printing over it with {len(avoid)} segments?" + extra_message) as s:
if isecs := self.thread_avoid(avoid, avoid_by):
rprint(f"{len(isecs)} thread/segment intersections")
avoid -= isecs
if s.thread_path == s.original_thread_path:
rprint(f'No change in thread path in step {s}, marking it as not valid')
s.valid = False
if avoid:
with steps.new_step(f"Print {len(avoid)} segments thread doesn't intersect" + extra_message) as s:
s.add(avoid)
if not isecs: break
avoid = isecs
rprint('Finished avoid and print')
def thread_avoid(self, avoid: Collection[GSegment], avoid_by=1) -> set[GSegment]:
"""Move thread_path to try to make the thread's trajectory avoid the
segments in `avoid` by at least `avoid_by`. Return any printed segments
that could not be avoided."""
assert(avoid)
avoid = set(avoid)
rprint(f'Avoiding {len(avoid)} segments with thread {self.thread_path}')
anchor = self.thread_path.point
#If there's only one segment in avoid, and the anchor point is either on it
# or within `avoid_by` of it, move the thread to be perpindicular to it.
if len(avoid) == 1:
seg = first(avoid)
if (anchor in seg or
too_close(anchor, seg.start_point, avoid_by) or
too_close(anchor, seg.end_point, avoid_by)):
#Get the two perpendicular half-lines to the segment
perp1 = GHalfLine(anchor, Vector(-seg.line.dv[1], seg.line.dv[0], 0))
#perp2 = GHalfLine(anchor, Vector(seg.line.dv[1], -seg.line.dv[0], 0))
### BUG: for now, just pick the first one
#Find the perpendicular path that requires the least movement from
# the current path
rprint(f'[yellow]WARNING:[/] anchor {anchor} is too close to or on top of only segment {seg}')
self.thread_path = perp1# if abs(ang_diff(perp1.angle, self.thread_path.angle)) else perp2
return set()
if not (isecs := self.thread_path.intersecting(avoid)):
#Thread is already not intersecting segments in `avoid`, but we want to try
# to move it so it's not very close to the ends of the segments either.
#If avoid_by isn't > 0, then we don't need to do anything else, so we're done.
if avoid_by <= 0:
return isecs
rprint(f'No intersections, ensure thread avoids segments by at least {avoid_by} mm')
#All printed segment starts & ends, minus the thread's start point
#endpoints = set(flatten(avoid)) - {self.thread_path.point}
#We can't avoid points that are too close to the anchor - not physically possible
#avoidables = set(ep for ep in endpoints if anchor.distance(ep) > avoid_by)
#Find segments where one or more of the endpoints are too close to the
# thread path and label them as intersecting (but not if that endpoint is
# next to the anchor, as it's not physically possible to move the thread
# to get far enough away.)
isecs = {seg for seg in avoid
if any(too_close(self.thread_path, ep, avoid_by)
for ep in seg[:] if not too_close(anchor, ep, avoid_by))}
#If none of the end points are closer than `avoid_by` to the
# thread, return the empty set to indicate we didn't have a problem
# avoiding any of them.
#If the segments that are too close are only a subset of those we were
# trying to avoid, return those.
if not isecs or isecs != avoid:
return isecs
rprint(f'{len(isecs)} segments too close to thread path, same as we wanted to avoid')
else:
#Let's try to simply drop every segment the thread intersects or that comes
# too close to the thread.
rprint(f' {len(isecs)} intersections')
#Add to `isecs` every segment in `avoid` that is too close to the thread
isecs.update({seg for seg in (avoid - isecs) if
too_close(self.thread_path, seg.start_point, by=avoid_by) or
too_close(self.thread_path, seg.end_point, by=avoid_by)})
#If there is anything left, return `isecs` so we can subtract from `avoid` in the caller
if isecs != avoid:
return isecs
#If we got here, either the thread intersects printed segments, or it's too
# close to printed segment endpoints, so we have to try to move the thread.
rprint('Thread must be moved to avoid segments')
vis = visibility(anchor, avoid, avoid_by)
vis_segs = defaultdict(set)
for vp, segs in vis.items():
vis_segs[vp].update(segs)
rprint(f' {len(vis)} potential visibility points')
if len(vis) < 5:
rprint(pretty_repr(vis))
#We only have 1 segment to avoid but can't avoid it. Let's pretend we did
# and see what happens.
if len(avoid) == 1 and len(vis_segs) == 1 and first(vis_segs) == first(avoid):
rprint(f"Can't avoid only segment {first(avoid)}, giving up on trying")
return set()
#Get all of the visibility points with N intersections, where N is the
# smallest number of intersections
_, vis_points = next(groupby(vis, key=lambda k:len(vis[k])))
#Then sort by distance from the thread
vis_points = angsort(list(vis_points), ref=self.thread_path)
#Now move the thread to overlap the closest one
self.rotate_thread_to(vis_points[0])
#If the visibility point with the smallest number of intersections still
# intersects the segments in `avoid`, it's probably too close to avoid.
if vis[vis_points[0]] == avoid:
rprint("Result of visibility:", vis[vis_points[0]], "is the same thing we tried to avoid:",
avoid, indent=4)
self._debug_quickplot_args = dict(gc_segs=avoid, anchor=anchor, thread_ring=self.thread_path)
raise ValueError("thread_avoid() couldn't avoid; try running\n"
"plot_helpers.quickplot(**threader.layer_steps[-1].printer._debug_quickplot_args);")
#Return the set of segments that we intersected
return vis[vis_points[0]]