meerk40t 0.9.2000__py2.py3-none-any.whl → 0.9.3001__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meerk40t/balormk/balor_params.py +1 -43
- meerk40t/balormk/controller.py +1 -41
- meerk40t/balormk/device.py +16 -22
- meerk40t/balormk/driver.py +4 -4
- meerk40t/balormk/gui/balorconfig.py +2 -2
- meerk40t/balormk/gui/balorcontroller.py +13 -5
- meerk40t/balormk/gui/baloroperationproperties.py +0 -46
- meerk40t/balormk/gui/gui.py +17 -17
- meerk40t/camera/gui/camerapanel.py +18 -11
- meerk40t/core/cutcode/rastercut.py +3 -1
- meerk40t/core/cutplan.py +145 -14
- meerk40t/core/elements/clipboard.py +18 -9
- meerk40t/core/elements/element_treeops.py +320 -180
- meerk40t/core/elements/element_types.py +7 -2
- meerk40t/core/elements/elements.py +53 -27
- meerk40t/core/elements/geometry.py +8 -0
- meerk40t/core/elements/offset_clpr.py +129 -4
- meerk40t/core/elements/offset_mk.py +3 -1
- meerk40t/core/elements/shapes.py +28 -25
- meerk40t/core/laserjob.py +7 -0
- meerk40t/core/node/bootstrap.py +4 -0
- meerk40t/core/node/effect_hatch.py +85 -96
- meerk40t/core/node/effect_wobble.py +309 -0
- meerk40t/core/node/elem_image.py +49 -19
- meerk40t/core/node/elem_line.py +60 -0
- meerk40t/core/node/elem_rect.py +5 -3
- meerk40t/core/node/image_processed.py +766 -0
- meerk40t/core/node/image_raster.py +113 -0
- meerk40t/core/node/node.py +120 -1
- meerk40t/core/node/op_cut.py +2 -8
- meerk40t/core/node/op_dots.py +0 -8
- meerk40t/core/node/op_engrave.py +2 -18
- meerk40t/core/node/op_image.py +22 -35
- meerk40t/core/node/op_raster.py +0 -9
- meerk40t/core/planner.py +32 -2
- meerk40t/core/svg_io.py +699 -461
- meerk40t/core/treeop.py +191 -0
- meerk40t/core/undos.py +15 -1
- meerk40t/core/units.py +14 -4
- meerk40t/device/dummydevice.py +3 -2
- meerk40t/device/gui/defaultactions.py +43 -55
- meerk40t/device/gui/formatterpanel.py +58 -49
- meerk40t/device/gui/warningpanel.py +12 -12
- meerk40t/device/mixins.py +13 -0
- meerk40t/dxf/dxf_io.py +9 -5
- meerk40t/extra/ezd.py +28 -26
- meerk40t/extra/imageactions.py +300 -308
- meerk40t/extra/lbrn.py +19 -2
- meerk40t/fill/fills.py +6 -6
- meerk40t/fill/patternfill.py +1061 -1061
- meerk40t/fill/patterns.py +2 -6
- meerk40t/grbl/controller.py +168 -52
- meerk40t/grbl/device.py +23 -18
- meerk40t/grbl/driver.py +39 -0
- meerk40t/grbl/emulator.py +79 -19
- meerk40t/grbl/gcodejob.py +10 -0
- meerk40t/grbl/gui/grblconfiguration.py +2 -2
- meerk40t/grbl/gui/grblcontroller.py +24 -8
- meerk40t/grbl/gui/grblhardwareconfig.py +153 -0
- meerk40t/grbl/gui/gui.py +17 -14
- meerk40t/grbl/mock_connection.py +15 -34
- meerk40t/grbl/plugin.py +0 -4
- meerk40t/grbl/serial_connection.py +2 -1
- meerk40t/gui/about.py +8 -5
- meerk40t/gui/alignment.py +10 -6
- meerk40t/gui/basicops.py +27 -17
- meerk40t/gui/bufferview.py +2 -2
- meerk40t/gui/choicepropertypanel.py +101 -13
- meerk40t/gui/consolepanel.py +12 -9
- meerk40t/gui/devicepanel.py +38 -25
- meerk40t/gui/executejob.py +6 -4
- meerk40t/gui/help_assets/help_assets.py +13 -10
- meerk40t/gui/hersheymanager.py +8 -6
- meerk40t/gui/icons.py +1951 -3065
- meerk40t/gui/imagesplitter.py +14 -7
- meerk40t/gui/keymap.py +3 -3
- meerk40t/gui/laserpanel.py +151 -84
- meerk40t/gui/laserrender.py +61 -70
- meerk40t/gui/lasertoolpanel.py +8 -7
- meerk40t/gui/materialtest.py +3 -3
- meerk40t/gui/mkdebug.py +254 -1
- meerk40t/gui/navigationpanels.py +321 -180
- meerk40t/gui/notes.py +3 -3
- meerk40t/gui/opassignment.py +12 -12
- meerk40t/gui/operation_info.py +13 -13
- meerk40t/gui/plugin.py +5 -0
- meerk40t/gui/position.py +20 -18
- meerk40t/gui/preferences.py +21 -6
- meerk40t/gui/propertypanels/attributes.py +70 -22
- meerk40t/gui/propertypanels/blobproperty.py +2 -2
- meerk40t/gui/propertypanels/consoleproperty.py +2 -2
- meerk40t/gui/propertypanels/groupproperties.py +3 -3
- meerk40t/gui/propertypanels/hatchproperty.py +11 -18
- meerk40t/gui/propertypanels/imageproperty.py +4 -3
- meerk40t/gui/propertypanels/opbranchproperties.py +1 -1
- meerk40t/gui/propertypanels/pathproperty.py +2 -2
- meerk40t/gui/propertypanels/pointproperty.py +2 -2
- meerk40t/gui/propertypanels/propertywindow.py +4 -4
- meerk40t/gui/propertypanels/textproperty.py +3 -3
- meerk40t/gui/propertypanels/wobbleproperty.py +204 -0
- meerk40t/gui/ribbon.py +367 -259
- meerk40t/gui/scene/scene.py +31 -5
- meerk40t/gui/scenewidgets/elementswidget.py +12 -4
- meerk40t/gui/scenewidgets/gridwidget.py +2 -2
- meerk40t/gui/scenewidgets/laserpathwidget.py +7 -2
- meerk40t/gui/scenewidgets/machineoriginwidget.py +6 -2
- meerk40t/gui/scenewidgets/relocatewidget.py +1 -1
- meerk40t/gui/scenewidgets/reticlewidget.py +9 -0
- meerk40t/gui/scenewidgets/selectionwidget.py +12 -7
- meerk40t/gui/simpleui.py +95 -8
- meerk40t/gui/simulation.py +44 -36
- meerk40t/gui/spoolerpanel.py +124 -26
- meerk40t/gui/statusbarwidgets/defaultoperations.py +18 -6
- meerk40t/gui/statusbarwidgets/infowidget.py +2 -2
- meerk40t/gui/statusbarwidgets/opassignwidget.py +12 -12
- meerk40t/gui/statusbarwidgets/shapepropwidget.py +45 -18
- meerk40t/gui/statusbarwidgets/statusbar.py +11 -4
- meerk40t/gui/themes.py +78 -0
- meerk40t/gui/toolwidgets/toolcircle.py +2 -1
- meerk40t/gui/toolwidgets/toolellipse.py +2 -1
- meerk40t/gui/toolwidgets/toolimagecut.py +132 -0
- meerk40t/gui/toolwidgets/toolline.py +144 -0
- meerk40t/gui/toolwidgets/toolnodeedit.py +72 -145
- meerk40t/gui/toolwidgets/toolpoint.py +1 -1
- meerk40t/gui/toolwidgets/toolpolygon.py +8 -55
- meerk40t/gui/toolwidgets/toolrect.py +2 -1
- meerk40t/gui/usbconnect.py +2 -2
- meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +2 -2
- meerk40t/gui/utilitywidgets/harmonograph.py +7 -7
- meerk40t/gui/utilitywidgets/scalewidget.py +1 -1
- meerk40t/gui/wordlisteditor.py +33 -18
- meerk40t/gui/wxmeerk40t.py +166 -66
- meerk40t/gui/wxmmain.py +236 -157
- meerk40t/gui/wxmribbon.py +49 -25
- meerk40t/gui/wxmscene.py +49 -38
- meerk40t/gui/wxmtree.py +216 -85
- meerk40t/gui/wxutils.py +62 -4
- meerk40t/image/imagetools.py +443 -15
- meerk40t/internal_plugins.py +2 -10
- meerk40t/kernel/kernel.py +12 -4
- meerk40t/lihuiyu/controller.py +7 -7
- meerk40t/lihuiyu/device.py +3 -1
- meerk40t/lihuiyu/driver.py +3 -0
- meerk40t/lihuiyu/gui/gui.py +8 -8
- meerk40t/lihuiyu/gui/lhyaccelgui.py +2 -2
- meerk40t/lihuiyu/gui/lhycontrollergui.py +73 -27
- meerk40t/lihuiyu/gui/lhydrivergui.py +2 -2
- meerk40t/lihuiyu/gui/tcpcontroller.py +22 -9
- meerk40t/main.py +6 -1
- meerk40t/moshi/controller.py +5 -5
- meerk40t/moshi/device.py +5 -2
- meerk40t/moshi/driver.py +4 -0
- meerk40t/moshi/gui/gui.py +8 -8
- meerk40t/moshi/gui/moshicontrollergui.py +24 -8
- meerk40t/moshi/gui/moshidrivergui.py +2 -2
- meerk40t/newly/controller.py +2 -0
- meerk40t/newly/device.py +9 -2
- meerk40t/newly/driver.py +4 -0
- meerk40t/newly/gui/gui.py +16 -17
- meerk40t/newly/gui/newlyconfig.py +2 -2
- meerk40t/newly/gui/newlycontroller.py +13 -5
- meerk40t/rotary/gui/gui.py +2 -2
- meerk40t/rotary/gui/rotarysettings.py +2 -2
- meerk40t/ruida/device.py +3 -0
- meerk40t/ruida/driver.py +4 -0
- meerk40t/ruida/gui/gui.py +6 -6
- meerk40t/ruida/gui/ruidaconfig.py +2 -2
- meerk40t/ruida/gui/ruidacontroller.py +13 -5
- meerk40t/svgelements.py +9 -9
- meerk40t/tools/geomstr.py +849 -153
- meerk40t/tools/kerftest.py +8 -4
- meerk40t/tools/livinghinges.py +15 -8
- {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/METADATA +21 -16
- {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/RECORD +185 -177
- {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/entry_points.txt +0 -1
- test/test_core_elements.py +8 -24
- test/test_file_svg.py +88 -0
- test/test_fill.py +9 -9
- test/test_geomstr.py +258 -8
- test/test_kernel.py +4 -0
- test/test_tools_rasterplotter.py +29 -0
- meerk40t/extra/embroider.py +0 -56
- meerk40t/extra/pathoptimize.py +0 -249
- {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/LICENSE +0 -0
- {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/WHEEL +0 -0
- {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/top_level.txt +0 -0
- {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/zip-safe +0 -0
meerk40t/tools/geomstr.py
CHANGED
@@ -97,90 +97,115 @@ class Clip:
|
|
97
97
|
self.clipping_shape = shape
|
98
98
|
self.bounds = shape.bbox()
|
99
99
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
np.real(s[:, -1]),
|
124
|
-
)
|
125
|
-
cmaxy = np.where(
|
126
|
-
np.imag(c[:, 0]) > np.imag(c[:, -1]),
|
127
|
-
np.imag(c[:, 0]),
|
128
|
-
np.imag(c[:, -1]),
|
129
|
-
)
|
130
|
-
sminy = np.where(
|
131
|
-
np.imag(s[:, 0]) < np.imag(s[:, -1]),
|
132
|
-
np.imag(s[:, 0]),
|
133
|
-
np.imag(s[:, -1]),
|
134
|
-
)
|
135
|
-
cminy = np.where(
|
136
|
-
np.imag(c[:, 0]) < np.imag(c[:, -1]),
|
137
|
-
np.imag(c[:, 0]),
|
138
|
-
np.imag(c[:, -1]),
|
100
|
+
def _splits(self, subject, clip):
|
101
|
+
"""
|
102
|
+
Calculate the splits in `subject` by the clip. This should return a list of t positions with the list being
|
103
|
+
as long as the number of segments in subject. Finds all intersections between subject and clip and the given
|
104
|
+
split positions (of subject) that would make the intersection list non-existant.
|
105
|
+
|
106
|
+
@param subject:
|
107
|
+
@param clip:
|
108
|
+
@return:
|
109
|
+
"""
|
110
|
+
cminx, cminy, cmaxx, cmaxy = clip.aabb()
|
111
|
+
sminx, sminy, smaxx, smaxy = subject.aabb()
|
112
|
+
x0, y0 = np.meshgrid(cmaxx, sminx)
|
113
|
+
x1, y1 = np.meshgrid(cminx, smaxx)
|
114
|
+
x2, y2 = np.meshgrid(cmaxy, sminy)
|
115
|
+
x3, y3 = np.meshgrid(cminy, smaxy)
|
116
|
+
|
117
|
+
checks = np.dstack(
|
118
|
+
(
|
119
|
+
x0 > y0,
|
120
|
+
x1 < y1,
|
121
|
+
x2 > y2,
|
122
|
+
x3 < y3,
|
139
123
|
)
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
124
|
+
).all(axis=2)
|
125
|
+
splits = [list() for _ in range(len(subject))]
|
126
|
+
for s0, s1 in sorted(np.argwhere(checks), key=lambda e: e[0], reverse=True):
|
127
|
+
splits[s0].extend(
|
128
|
+
[t for t, _ in subject.intersections(int(s0), clip.segments[s1])]
|
144
129
|
)
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
130
|
+
return splits
|
131
|
+
|
132
|
+
def _splits_brute(self, subject, clip):
|
133
|
+
"""
|
134
|
+
Find the subject clip splits by brute force (for debug testing).
|
135
|
+
|
136
|
+
@param subject:
|
137
|
+
@param clip:
|
138
|
+
@return:
|
139
|
+
"""
|
140
|
+
splits = [list() for _ in range(len(subject))]
|
141
|
+
for s0 in range(len(subject)):
|
142
|
+
for s1 in range(len(clip)):
|
143
|
+
for t0, t1 in subject.intersections(int(s0), clip.segments[s1]):
|
144
|
+
splits[s0].append(t0)
|
145
|
+
|
146
|
+
return splits
|
147
|
+
|
148
|
+
def inside(self, subject):
|
149
|
+
"""
|
150
|
+
Modifies subject to only contain the segments found inside the given clip.
|
151
|
+
@param subject:
|
152
|
+
@param clip:
|
153
|
+
@return:
|
154
|
+
"""
|
155
|
+
clip = self.clipping_shape
|
156
|
+
c = Geomstr()
|
157
|
+
# Pip currently only works with line segments
|
158
|
+
for sp in clip.as_subpaths():
|
159
|
+
for segs in sp.as_interpolated_segments(interpolate=100):
|
160
|
+
c.polyline(segs)
|
161
|
+
c.end()
|
162
|
+
sb = Scanbeam(c)
|
149
163
|
|
150
|
-
checks = np.dstack(
|
151
|
-
(
|
152
|
-
x0 > y0,
|
153
|
-
x1 < y1,
|
154
|
-
x2 > y2,
|
155
|
-
x3 < y3,
|
156
|
-
)
|
157
|
-
).all(axis=2)
|
158
|
-
# new_subject = Geomstr(s)
|
159
|
-
for s0, s1 in sorted(np.argwhere(checks), key=lambda e: e[0], reverse=True):
|
160
|
-
splits0 = [
|
161
|
-
t for t, _ in subject.intersections(int(s0), clip.segments[s1])
|
162
|
-
]
|
163
|
-
if len(splits0):
|
164
|
-
split_lines = list(subject.split(s0, splits0))
|
165
|
-
subject.replace(s0, s0, split_lines)
|
166
|
-
|
167
|
-
# Previous bruteforce.
|
168
|
-
# for i in range(clip.index):
|
169
|
-
# for c in range(subject.index - 1, -1, -1):
|
170
|
-
# for t0, t1 in sorted(
|
171
|
-
# list(subject.intersections(c, clip.segments[i])),
|
172
|
-
# key=lambda t: t[0],
|
173
|
-
# reverse=True,
|
174
|
-
# ):
|
175
|
-
# subject.split(c, t0)
|
176
|
-
|
177
|
-
sb = Scanbeam(clip)
|
178
164
|
mid_points = subject.position(slice(subject.index), 0.5)
|
179
165
|
r = np.where(sb.points_in_polygon(mid_points))
|
166
|
+
|
180
167
|
subject.segments = subject.segments[r]
|
181
168
|
subject.index = len(subject.segments)
|
182
169
|
return subject
|
183
170
|
|
171
|
+
def polycut(self, subject, breaks=False):
|
172
|
+
"""
|
173
|
+
Performs polycut on the subject using the preset clipping shape. This only prevents intersections making all
|
174
|
+
intersections into divided segments.
|
175
|
+
|
176
|
+
@param subject:
|
177
|
+
@param breaks: should the polycut insert overt breaks.
|
178
|
+
@return:
|
179
|
+
"""
|
180
|
+
clip = self.clipping_shape
|
181
|
+
splits = self._splits(subject, clip)
|
182
|
+
# splits2 = self._splits_brute(subject, clip)
|
183
|
+
# for q1, q2 in zip(splits, splits2):
|
184
|
+
# assert(q1, q2)
|
185
|
+
|
186
|
+
for s0 in range(len(splits) - 1, -1, -1):
|
187
|
+
s = splits[s0]
|
188
|
+
if not s:
|
189
|
+
continue
|
190
|
+
split_lines = list(subject.split(s0, s, breaks=breaks))
|
191
|
+
subject.replace(s0, s0, split_lines)
|
192
|
+
subject.validate()
|
193
|
+
return subject
|
194
|
+
|
195
|
+
def clip(self, subject, split=True):
|
196
|
+
"""
|
197
|
+
Clip algorithm works in 3 steps. First find the splits between the subject and clip and split the subject at
|
198
|
+
all positions where it intersects clip. Remove any subject line segment whose midpoint is not found within
|
199
|
+
clip.
|
200
|
+
|
201
|
+
@param subject:
|
202
|
+
@param split:
|
203
|
+
@return:
|
204
|
+
"""
|
205
|
+
if split:
|
206
|
+
subject = self.polycut(subject)
|
207
|
+
return self.inside(subject)
|
208
|
+
|
184
209
|
|
185
210
|
class Pattern:
|
186
211
|
def __init__(self, geomstr=None):
|
@@ -194,7 +219,6 @@ class Pattern:
|
|
194
219
|
self.cell_height = y1 - y0
|
195
220
|
self.padding_x = 0
|
196
221
|
self.padding_y = 0
|
197
|
-
self.extend_pattern = False
|
198
222
|
|
199
223
|
def create_from_pattern(self, pattern, a=None, b=None, *args, **kwargs):
|
200
224
|
"""
|
@@ -278,38 +302,314 @@ class Pattern:
|
|
278
302
|
ch = self.cell_height
|
279
303
|
px = self.padding_x
|
280
304
|
py = self.padding_y
|
281
|
-
|
282
|
-
|
283
|
-
start_index_x = math.floor(x0 / cx) - 1
|
284
|
-
start_index_y = math.floor(y0 / cy) - 1
|
285
|
-
end_index_x = math.ceil(x1 / cx) + 1
|
286
|
-
end_index_y = math.ceil(y1 / cy) + 1
|
287
|
-
|
288
|
-
if self.extend_pattern:
|
289
|
-
row_offset = -0.5 * self.cell_width
|
290
|
-
start_index_x -= 1
|
305
|
+
if abs(cw + 2 * px) <= 1e-4:
|
306
|
+
cols = 1
|
291
307
|
else:
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
308
|
+
cols = int(((x1 - x0) + cw) / (cw + 2 * px)) + 1
|
309
|
+
if abs(ch + 2 * py) <= 1e-4:
|
310
|
+
rows = 1
|
311
|
+
else:
|
312
|
+
rows = int(((y1 - y0) + ch) / (ch + 2 * py)) + 1
|
313
|
+
|
314
|
+
cols = max(1, cols - 2)
|
315
|
+
rows = max(1, rows - 2)
|
316
|
+
|
317
|
+
start_value_x = 0
|
318
|
+
col = 0
|
319
|
+
x_offset = (col + start_value_x) * (cw + 2 * px)
|
320
|
+
x = x0 + x_offset
|
321
|
+
while x >= x0 - cw and x < x1:
|
322
|
+
start_value_x -= 1
|
323
|
+
x_offset = (col + start_value_x) * (cw + 2 * px)
|
324
|
+
x = x0 + x_offset
|
325
|
+
# print (f"X-lower bound: sx={start_value_x}, x={x:.2f}, x0={x0:.2f}, x1={x1:.2f}")
|
326
|
+
|
327
|
+
end_value_x = 0
|
328
|
+
col = cols - 1
|
329
|
+
x_offset = (col + end_value_x) * (cw + 2 * px)
|
330
|
+
x = x0 + x_offset
|
331
|
+
while x >= x0 and x < x1:
|
332
|
+
end_value_x += 1
|
333
|
+
x_offset = (col + end_value_x) * (cw + 2 * px)
|
334
|
+
x = x0 + x_offset
|
335
|
+
# print (f"X-upper bound: ex={end_value_x}, x={x:.2f}, x0={x0:.2f}, x1={x1:.2f}")
|
336
|
+
|
337
|
+
start_value_y = 0
|
338
|
+
row = 0
|
339
|
+
y_offset = (row + start_value_y) * (ch + 2 * py)
|
340
|
+
y = y0 + y_offset
|
341
|
+
while y >= y0 - ch and y < y1:
|
342
|
+
start_value_y -= 1
|
343
|
+
y_offset = (row + start_value_y) * (ch + 2 * py)
|
344
|
+
y = y0 + y_offset
|
345
|
+
# print (f"Y-lower bound: sy={start_value_y}, y={y:.2f}, y0={y0:.2f}, y1={y1:.2f}")
|
346
|
+
|
347
|
+
end_value_y = 0
|
348
|
+
row = rows - 1
|
349
|
+
y_offset = (row + end_value_y) * (ch + 2 * py)
|
350
|
+
y = y0 + y_offset
|
351
|
+
while y >= y0 and y < y1:
|
352
|
+
end_value_y += 1
|
353
|
+
y_offset = (row + end_value_y) * (ch + 2 * py)
|
354
|
+
y = y0 + y_offset
|
355
|
+
# print (f"Y-upper bound: ey={end_value_y}, y={y:.2f}, y0={y0:.2f}, y1={y1:.2f}")
|
356
|
+
|
357
|
+
# print (f"Cols={cols}, s_x={start_value_x}, e_x={end_value_x}")
|
358
|
+
# print (f"Rows={rows}, s_y={start_value_y}, e_y={end_value_y}")
|
359
|
+
|
360
|
+
# start_value_x -= 2
|
361
|
+
# start_value_y -= 2
|
362
|
+
# end_value_x += 1
|
363
|
+
# end_value_y += 1
|
364
|
+
|
365
|
+
top_left_x = x0
|
366
|
+
for col in range(start_value_x, cols + end_value_x, 1):
|
367
|
+
x_offset = col * (cw + 2 * px)
|
368
|
+
x = top_left_x + x_offset
|
369
|
+
|
370
|
+
top_left_y = y0
|
371
|
+
for row in range(start_value_y, rows + end_value_y, 1):
|
372
|
+
y_offset = row * (ch + 2 * py)
|
373
|
+
if col % 2:
|
374
|
+
y_offset += (ch + 2 * py) / 2
|
375
|
+
y = top_left_y + y_offset
|
376
|
+
|
301
377
|
m = Matrix.scale(cw, ch)
|
302
378
|
m *= Matrix.translate(x - self.offset_x, y - self.offset_y)
|
303
379
|
yield self.geomstr.as_transformed(m)
|
304
380
|
|
305
381
|
|
306
|
-
class
|
307
|
-
def __init__(self):
|
308
|
-
self.
|
309
|
-
|
382
|
+
class BeamTable:
|
383
|
+
def __init__(self, geom):
|
384
|
+
self.geometry = geom
|
385
|
+
self._nb_events = None
|
386
|
+
self._nb_scan = None
|
387
|
+
self.intersections = Geomstr()
|
388
|
+
|
389
|
+
def sort_key(self, e):
|
390
|
+
return e[0].real, e[0].imag, ~e[1]
|
391
|
+
|
392
|
+
def compute_beam(self):
|
393
|
+
g = self.geometry
|
394
|
+
gs = g.segments
|
395
|
+
events = []
|
396
|
+
# Add start and end events.
|
397
|
+
for i in range(g.index):
|
398
|
+
if gs[i][2] != TYPE_LINE:
|
399
|
+
continue
|
400
|
+
if (gs[i][0].imag, gs[i][0].real) < (gs[i][-1].imag, gs[i][-1].real):
|
401
|
+
events.append((g.segments[i][0], i, None))
|
402
|
+
events.append((g.segments[i][-1], ~i, None))
|
403
|
+
else:
|
404
|
+
events.append((g.segments[i][0], ~i, None))
|
405
|
+
events.append((g.segments[i][-1], i, None))
|
406
|
+
|
407
|
+
# Sort start and end events.
|
408
|
+
events.sort(key=self.sort_key)
|
409
|
+
|
410
|
+
def check_intersection(q, r, y):
|
411
|
+
"""
|
412
|
+
Check for intersections between p and r, at y.
|
413
|
+
|
414
|
+
p must occur before r in the sorted actives.
|
415
|
+
|
416
|
+
y is used to ensure this is a future point.
|
417
|
+
@param q: lower-active value
|
418
|
+
@param r: higher-active value
|
419
|
+
@param y: y value to not be equal
|
420
|
+
@return:
|
421
|
+
"""
|
422
|
+
try:
|
423
|
+
for t1, t2 in g.intersections(q, r):
|
424
|
+
if t1 in (0, 1) and t2 in (0, 1):
|
425
|
+
continue
|
426
|
+
pt_intersect = g.position(q, t1)
|
427
|
+
if y < pt_intersect.imag:
|
428
|
+
events.append((pt_intersect, 0, (q, r)))
|
429
|
+
self.intersections.point(pt_intersect)
|
430
|
+
events.sort(key=self.sort_key)
|
431
|
+
except AttributeError:
|
432
|
+
pass
|
433
|
+
|
434
|
+
# Store currently active segments.
|
435
|
+
actives = []
|
310
436
|
|
311
|
-
|
312
|
-
|
437
|
+
# Store previously active segments
|
438
|
+
active_lists = []
|
439
|
+
|
440
|
+
largest_actives = 0
|
441
|
+
for pt, index, swap in events:
|
442
|
+
x_pos = pt.real
|
443
|
+
y_pos = pt.imag
|
444
|
+
if isinstance(swap, tuple):
|
445
|
+
idx1, idx2 = swap
|
446
|
+
pos1 = actives.index(idx1)
|
447
|
+
pos2 = actives.index(idx2)
|
448
|
+
actives[pos1], actives[pos2] = actives[pos2], actives[pos1]
|
449
|
+
# We swapped pos1 and pos2 so we must check the outer values of this swap.
|
450
|
+
try:
|
451
|
+
check_intersection(actives[pos1 - 1], actives[pos1], y_pos)
|
452
|
+
except IndexError:
|
453
|
+
pass
|
454
|
+
try:
|
455
|
+
check_intersection(actives[pos2], actives[pos2 + 1], y_pos)
|
456
|
+
except IndexError:
|
457
|
+
pass
|
458
|
+
elif index >= 0:
|
459
|
+
# Index is being inserted, find x-position sorted.
|
460
|
+
lines = g.segments[actives]
|
461
|
+
a = lines[:, 0]
|
462
|
+
b = lines[:, -1]
|
463
|
+
|
464
|
+
old_np_seterr = np.seterr(invalid="ignore", divide="ignore")
|
465
|
+
try:
|
466
|
+
# If horizontal slope is undefined. But, all x-ints are at x since x0=x1
|
467
|
+
m = (b.imag - a.imag) / (b.real - a.real)
|
468
|
+
y0 = a.imag - (m * a.real)
|
469
|
+
x_intercepts = np.where(~np.isinf(m), (y_pos - y0) / m, a.real)
|
470
|
+
finally:
|
471
|
+
np.seterr(**old_np_seterr)
|
472
|
+
idx = np.searchsorted(x_intercepts, np.imag(pt))
|
473
|
+
actives.insert(idx, index)
|
474
|
+
if len(actives) > largest_actives:
|
475
|
+
largest_actives = len(actives)
|
476
|
+
|
477
|
+
# Check intersections between idx, idx + 1
|
478
|
+
try:
|
479
|
+
check_intersection(index, actives[idx + 1], y_pos)
|
480
|
+
except IndexError:
|
481
|
+
pass
|
482
|
+
|
483
|
+
# Check intersections between idx, idx - 1
|
484
|
+
try:
|
485
|
+
check_intersection(actives[idx - 1], index, y_pos)
|
486
|
+
except IndexError:
|
487
|
+
pass
|
488
|
+
else:
|
489
|
+
remove_index = actives.index(~index)
|
490
|
+
# Check intersections between idx-1, idx+ 1
|
491
|
+
try:
|
492
|
+
check_intersection(actives[idx - 1], actives[idx + 1], y_pos)
|
493
|
+
except IndexError:
|
494
|
+
pass
|
495
|
+
del actives[remove_index]
|
496
|
+
|
497
|
+
active_lists.append(list(actives))
|
498
|
+
|
499
|
+
active_lists.append([])
|
500
|
+
self._nb_events = [(e.imag, e.real) for e, _, _ in events]
|
501
|
+
self._nb_scan = np.zeros((len(active_lists), largest_actives), dtype=int)
|
502
|
+
self._nb_scan -= 1
|
503
|
+
for i, active in enumerate(active_lists):
|
504
|
+
self._nb_scan[i, 0 : len(active)] = active
|
505
|
+
|
506
|
+
def compute_beam_brute(self):
|
507
|
+
g = self.geometry
|
508
|
+
gs = g.segments
|
509
|
+
events = []
|
510
|
+
# Add start and end events.
|
511
|
+
for i in range(g.index):
|
512
|
+
if gs[i][2] != TYPE_LINE:
|
513
|
+
continue
|
514
|
+
if (gs[i][0].real, gs[i][0].imag) < (gs[i][-1].real, gs[i][-1].imag):
|
515
|
+
events.append((g.segments[i][0], i, None))
|
516
|
+
events.append((g.segments[i][-1], ~i, None))
|
517
|
+
else:
|
518
|
+
events.append((g.segments[i][0], ~i, None))
|
519
|
+
events.append((g.segments[i][-1], i, None))
|
520
|
+
|
521
|
+
wh, p, ta, tb = g.brute_line_intersections()
|
522
|
+
for w, pos in zip(wh, p):
|
523
|
+
events.append((pos, 0, w))
|
524
|
+
self.intersections.point(pos)
|
525
|
+
|
526
|
+
# Sort start, end, intersections events.
|
527
|
+
events.sort(key=self.sort_key)
|
528
|
+
|
529
|
+
# Store currently active segments.
|
530
|
+
actives = []
|
531
|
+
|
532
|
+
scanline = None
|
533
|
+
|
534
|
+
def x_ints(e):
|
535
|
+
return g.x_intercept(e, np.real(scanline))
|
536
|
+
|
537
|
+
# Store previously active segments
|
538
|
+
active_lists = []
|
539
|
+
real_events = []
|
540
|
+
|
541
|
+
largest_actives = 0
|
542
|
+
|
543
|
+
for i in range(len(events)):
|
544
|
+
event = events[i]
|
545
|
+
pt, index, swap = event
|
546
|
+
|
547
|
+
try:
|
548
|
+
next, _, _ = events[i + 1]
|
549
|
+
scanline = (pt + next) / 2
|
550
|
+
except IndexError:
|
551
|
+
next = complex(float("inf"), float("inf"))
|
552
|
+
scanline = next
|
553
|
+
|
554
|
+
if swap is not None:
|
555
|
+
pass
|
556
|
+
elif index >= 0:
|
557
|
+
actives.append(index)
|
558
|
+
else:
|
559
|
+
remove_index = actives.index(~index)
|
560
|
+
del actives[remove_index]
|
561
|
+
|
562
|
+
if pt != next:
|
563
|
+
if len(actives) > largest_actives:
|
564
|
+
largest_actives = len(actives)
|
565
|
+
actives.sort(key=x_ints)
|
566
|
+
real_events.append(pt)
|
567
|
+
active_lists.append(list(actives))
|
568
|
+
|
569
|
+
self._nb_events = real_events
|
570
|
+
self._nb_scan = np.zeros((len(active_lists), largest_actives), dtype=int)
|
571
|
+
self._nb_scan -= 1
|
572
|
+
for i, active in enumerate(active_lists):
|
573
|
+
self._nb_scan[i, 0 : len(active)] = active
|
574
|
+
|
575
|
+
def points_in_polygon(self, e):
|
576
|
+
if self._nb_scan is None:
|
577
|
+
self.compute_beam_brute()
|
578
|
+
idx = np.searchsorted(self._nb_events, e)
|
579
|
+
actives = self._nb_scan[idx]
|
580
|
+
line = self.geometry.segments[actives]
|
581
|
+
a = line[:, :, 0]
|
582
|
+
a = np.where(actives == -1, np.nan + np.nan * 1j, a)
|
583
|
+
b = line[:, :, -1]
|
584
|
+
b = np.where(actives == -1, np.nan + np.nan * 1j, b)
|
585
|
+
|
586
|
+
q = self.geometry.y_intercept(actives, np.real(e))
|
587
|
+
# print(q)
|
588
|
+
|
589
|
+
old_np_seterr = np.seterr(invalid="ignore", divide="ignore")
|
590
|
+
try:
|
591
|
+
# If horizontal slope is undefined. But, all x-ints are at x since x0=x1
|
592
|
+
m = (b.real - a.real) / (b.imag - a.imag)
|
593
|
+
y0 = a.real - (m * a.imag)
|
594
|
+
ys = np.reshape(np.repeat(np.real(e), y0.shape[1]), y0.shape)
|
595
|
+
y_intercepts = np.where(~np.isinf(m), (ys - y0) / m, a.imag)
|
596
|
+
finally:
|
597
|
+
np.seterr(**old_np_seterr)
|
598
|
+
xs = np.reshape(np.repeat(np.imag(e), y0.shape[1]), y0.shape)
|
599
|
+
results = np.sum(y_intercepts <= xs, axis=1)
|
600
|
+
results %= 2
|
601
|
+
return results
|
602
|
+
|
603
|
+
def actives_at(self, value):
|
604
|
+
from bisect import bisect
|
605
|
+
|
606
|
+
if not self._nb_scan:
|
607
|
+
self.compute_beam_brute()
|
608
|
+
idx = np.searchsorted(self._nb_events, value)
|
609
|
+
# idx = bisect(self._nb_events, (value.imag, value.real))
|
610
|
+
actives = self._nb_scan[idx - 1]
|
611
|
+
aw = np.argwhere(actives != -1)[:, 0]
|
612
|
+
return actives[aw]
|
313
613
|
|
314
614
|
|
315
615
|
class Scanbeam:
|
@@ -1070,6 +1370,81 @@ class Geomstr:
|
|
1070
1370
|
geometry.rotate(-angle)
|
1071
1371
|
return geometry
|
1072
1372
|
|
1373
|
+
@classmethod
|
1374
|
+
def wobble(cls, algorithm, outer, radius, interval, speed):
|
1375
|
+
from meerk40t.fill.fills import Wobble
|
1376
|
+
|
1377
|
+
w = Wobble(algorithm, radius=radius, speed=speed, interval=interval)
|
1378
|
+
|
1379
|
+
geometry = cls()
|
1380
|
+
for segments in outer.as_interpolated_segments(interpolate=50):
|
1381
|
+
points = []
|
1382
|
+
last = None
|
1383
|
+
for pt in segments:
|
1384
|
+
if last is not None:
|
1385
|
+
points.extend(
|
1386
|
+
[
|
1387
|
+
complex(wx, wy)
|
1388
|
+
for wx, wy in w(last.real, last.imag, pt.real, pt.imag)
|
1389
|
+
]
|
1390
|
+
)
|
1391
|
+
last = pt
|
1392
|
+
geometry.append(Geomstr.lines(*points))
|
1393
|
+
return geometry
|
1394
|
+
|
1395
|
+
@classmethod
|
1396
|
+
def wobble_slowtooth(cls, outer, radius, interval, speed):
|
1397
|
+
from meerk40t.fill.fills import slowtooth as algorithm
|
1398
|
+
|
1399
|
+
return cls.wobble(algorithm, outer, radius, interval, speed)
|
1400
|
+
|
1401
|
+
@classmethod
|
1402
|
+
def wobble_gear(cls, outer, radius, interval, speed):
|
1403
|
+
from meerk40t.fill.fills import gear as algorithm
|
1404
|
+
|
1405
|
+
return cls.wobble(algorithm, outer, radius, interval, speed)
|
1406
|
+
|
1407
|
+
@classmethod
|
1408
|
+
def wobble_jigsaw(cls, outer, radius, interval, speed):
|
1409
|
+
from meerk40t.fill.fills import jigsaw as algorithm
|
1410
|
+
|
1411
|
+
return cls.wobble(algorithm, outer, radius, interval, speed)
|
1412
|
+
|
1413
|
+
@classmethod
|
1414
|
+
def wobble_sawtooth(cls, outer, radius, interval, speed):
|
1415
|
+
from meerk40t.fill.fills import sawtooth as algorithm
|
1416
|
+
|
1417
|
+
return cls.wobble(algorithm, outer, radius, interval, speed)
|
1418
|
+
|
1419
|
+
@classmethod
|
1420
|
+
def wobble_sinewave(cls, outer, radius, interval, speed):
|
1421
|
+
from meerk40t.fill.fills import sinewave as algorithm
|
1422
|
+
|
1423
|
+
return cls.wobble(algorithm, outer, radius, interval, speed)
|
1424
|
+
|
1425
|
+
@classmethod
|
1426
|
+
def wobble_circle_left(cls, outer, radius, interval, speed):
|
1427
|
+
from meerk40t.fill.fills import circle_left as algorithm
|
1428
|
+
|
1429
|
+
return cls.wobble(algorithm, outer, radius, interval, speed)
|
1430
|
+
|
1431
|
+
@classmethod
|
1432
|
+
def wobble_circle_right(cls, outer, radius, interval, speed):
|
1433
|
+
from meerk40t.fill.fills import circle_right as algorithm
|
1434
|
+
|
1435
|
+
return cls.wobble(algorithm, outer, radius, interval, speed)
|
1436
|
+
|
1437
|
+
@classmethod
|
1438
|
+
def wobble_circle(cls, outer, radius, interval, speed):
|
1439
|
+
from meerk40t.fill.fills import circle as algorithm
|
1440
|
+
|
1441
|
+
return cls.wobble(algorithm, outer, radius, interval, speed)
|
1442
|
+
|
1443
|
+
def flag_settings(self):
|
1444
|
+
for i in range(self.index):
|
1445
|
+
info = self.segments[i][2]
|
1446
|
+
self.segments[i][2] = complex(info.real, i)
|
1447
|
+
|
1073
1448
|
def copies(self, n):
|
1074
1449
|
segs = self.segments[: self.index]
|
1075
1450
|
self.segments = np.vstack([segs] * n)
|
@@ -1198,6 +1573,11 @@ class Geomstr:
|
|
1198
1573
|
self.allocate_at_position(e, space)
|
1199
1574
|
self.segments[e : e + space] = lines
|
1200
1575
|
|
1576
|
+
def append_lines(self, lines):
|
1577
|
+
self._ensure_capacity(self.index + len(lines))
|
1578
|
+
self.segments[self.index : self.index + len(lines)] = lines
|
1579
|
+
self.index += len(lines)
|
1580
|
+
|
1201
1581
|
def append(self, other):
|
1202
1582
|
self._ensure_capacity(self.index + other.index + 1)
|
1203
1583
|
if self.index != 0:
|
@@ -1207,6 +1587,25 @@ class Geomstr:
|
|
1207
1587
|
]
|
1208
1588
|
self.index += other.index
|
1209
1589
|
|
1590
|
+
def validate(self):
|
1591
|
+
infos = self.segments[: self.index, 2]
|
1592
|
+
|
1593
|
+
starts = self.segments[: self.index, 0]
|
1594
|
+
q = np.where(np.real(infos).astype(int) & 0b1000)
|
1595
|
+
assert not np.any(np.isnan(starts[q]))
|
1596
|
+
|
1597
|
+
ends = self.segments[: self.index, 4]
|
1598
|
+
q = np.where(np.real(infos).astype(int) & 0b0001)
|
1599
|
+
assert not np.any(np.isnan(ends[q]))
|
1600
|
+
|
1601
|
+
c1 = self.segments[: self.index, 1]
|
1602
|
+
q = np.where(np.real(infos).astype(int) & 0b0100)
|
1603
|
+
assert not np.any(np.isnan(c1[q]))
|
1604
|
+
|
1605
|
+
c2 = self.segments[: self.index, 3]
|
1606
|
+
q = np.where(np.real(infos).astype(int) & 0b0010)
|
1607
|
+
assert not np.any(np.isnan(c2[q]))
|
1608
|
+
|
1210
1609
|
#######################
|
1211
1610
|
# Geometric Primitives
|
1212
1611
|
#######################
|
@@ -1570,6 +1969,36 @@ class Geomstr:
|
|
1570
1969
|
r.translate(p1.real, p1.imag)
|
1571
1970
|
return r
|
1572
1971
|
|
1972
|
+
def divide(self, other):
|
1973
|
+
"""
|
1974
|
+
Divide the current closed point shape by the other.
|
1975
|
+
|
1976
|
+
This should probably use the other part doing to splitting to create a proper divide. So if cut with a bezier
|
1977
|
+
we would get a bezier segment making the connection on both sides of the shape.
|
1978
|
+
|
1979
|
+
@param other:
|
1980
|
+
@return:
|
1981
|
+
"""
|
1982
|
+
closed = self.is_closed()
|
1983
|
+
c = Clip(other)
|
1984
|
+
polycut = c.polycut(self, breaks=True)
|
1985
|
+
|
1986
|
+
geoms = list()
|
1987
|
+
g = Geomstr()
|
1988
|
+
geoms.append(g)
|
1989
|
+
for e in polycut.segments[: self.index]:
|
1990
|
+
if e[2].real == TYPE_END:
|
1991
|
+
g = Geomstr()
|
1992
|
+
geoms.append(g)
|
1993
|
+
else:
|
1994
|
+
g.append_lines([e])
|
1995
|
+
if closed and len(geoms) >= 2:
|
1996
|
+
first = geoms[0]
|
1997
|
+
last = geoms[-1]
|
1998
|
+
del geoms[-1]
|
1999
|
+
first.insert(0, last.segments[: last.index])
|
2000
|
+
return geoms
|
2001
|
+
|
1573
2002
|
def round_corners(self, amount=0.2):
|
1574
2003
|
"""
|
1575
2004
|
Round segment corners.
|
@@ -1692,6 +2121,33 @@ class Geomstr:
|
|
1692
2121
|
# Universal Functions
|
1693
2122
|
#######################
|
1694
2123
|
|
2124
|
+
def aabb(self):
|
2125
|
+
"""
|
2126
|
+
Calculate the per-segment `Axis Aligned Bounding Box` of each individual segment
|
2127
|
+
|
2128
|
+
@return:
|
2129
|
+
"""
|
2130
|
+
c = self.segments[: self.index]
|
2131
|
+
infos = np.real(c[:, 2]).astype(int)
|
2132
|
+
|
2133
|
+
xs = np.dstack(
|
2134
|
+
(
|
2135
|
+
np.real(c[:, 0]),
|
2136
|
+
np.real(c[:, 4]),
|
2137
|
+
np.where(infos & 0b0100, np.real(c[:, 1]), np.real(c[:, 0])),
|
2138
|
+
np.where(infos & 0b0010, np.real(c[:, 3]), np.real(c[:, 4])),
|
2139
|
+
)
|
2140
|
+
)
|
2141
|
+
ys = np.dstack(
|
2142
|
+
(
|
2143
|
+
np.imag(c[:, 0]),
|
2144
|
+
np.imag(c[:, 4]),
|
2145
|
+
np.where(infos & 0b0100, np.imag(c[:, 1]), np.imag(c[:, 0])),
|
2146
|
+
np.where(infos & 0b0010, np.imag(c[:, 3]), np.imag(c[:, 4])),
|
2147
|
+
)
|
2148
|
+
)
|
2149
|
+
return xs.min(axis=2), ys.min(axis=2), xs.max(axis=2), ys.max(axis=2)
|
2150
|
+
|
1695
2151
|
def bbox(self, mx=None, e=None):
|
1696
2152
|
"""
|
1697
2153
|
Get the bounds of the given geom primitive
|
@@ -2103,7 +2559,7 @@ class Geomstr:
|
|
2103
2559
|
|
2104
2560
|
return quad(_abs_derivative, 0.0, 1.0, epsabs=1e-12, limit=1000)[0]
|
2105
2561
|
|
2106
|
-
def split(self, e, t):
|
2562
|
+
def split(self, e, t, breaks=False):
|
2107
2563
|
"""
|
2108
2564
|
Splits individual geom e at position t [0-1]
|
2109
2565
|
|
@@ -2123,19 +2579,27 @@ class Geomstr:
|
|
2123
2579
|
mid = self.towards(start, end, t)
|
2124
2580
|
if isinstance(mid, complex):
|
2125
2581
|
yield start, control, info, control2, mid
|
2582
|
+
if breaks:
|
2583
|
+
yield mid, mid, complex(TYPE_END, info.imag), mid, mid
|
2126
2584
|
yield mid, control, info, control2, end
|
2127
2585
|
else:
|
2128
2586
|
# Mid is an array of complexes
|
2129
2587
|
yield start, control, info, control2, mid[0]
|
2130
2588
|
for i in range(1, len(mid)):
|
2589
|
+
if breaks:
|
2590
|
+
yield mid[i - 1], mid[i - 1], complex(TYPE_END, info.imag), mid[
|
2591
|
+
i - 1
|
2592
|
+
], mid[i - 1]
|
2131
2593
|
yield mid[i - 1], control, info, control2, mid[i]
|
2594
|
+
if breaks:
|
2595
|
+
yield mid[-1], 0, complex(TYPE_END, info.imag), 0, mid[-1]
|
2132
2596
|
yield mid[-1], control, info, control2, end
|
2133
2597
|
if info.real == TYPE_QUAD:
|
2134
|
-
yield from self._split_quad(e, t)
|
2598
|
+
yield from self._split_quad(e, t, breaks=breaks)
|
2135
2599
|
if info.real == TYPE_CUBIC:
|
2136
|
-
yield from self._split_cubic(e, t)
|
2600
|
+
yield from self._split_cubic(e, t, breaks=breaks)
|
2137
2601
|
|
2138
|
-
def _split_quad(self, e, t):
|
2602
|
+
def _split_quad(self, e, t, breaks):
|
2139
2603
|
"""
|
2140
2604
|
Performs deCasteljau's algorithm unrolled.
|
2141
2605
|
"""
|
@@ -2153,7 +2617,9 @@ class Geomstr:
|
|
2153
2617
|
last = 0.0
|
2154
2618
|
for t0 in sorted(t):
|
2155
2619
|
# Thanks tiger.
|
2156
|
-
splits = list(
|
2620
|
+
splits = list(
|
2621
|
+
self._split_quad(e, (t0 - last) / (1 - last), breaks=breaks)
|
2622
|
+
)
|
2157
2623
|
last = t0
|
2158
2624
|
yield splits[0]
|
2159
2625
|
e = splits[1]
|
@@ -2164,9 +2630,10 @@ class Geomstr:
|
|
2164
2630
|
r1_1 = t * (end - control) + control
|
2165
2631
|
r2 = t * (r1_1 - r1_0) + r1_0
|
2166
2632
|
yield start, r1_0, info, r1_0, r2
|
2633
|
+
# yield r2, 0, complex(TYPE_END, info.imag), 0, r2
|
2167
2634
|
yield r2, r1_1, info, r1_1, end
|
2168
2635
|
|
2169
|
-
def _split_cubic(self, e, t):
|
2636
|
+
def _split_cubic(self, e, t, breaks=False):
|
2170
2637
|
if (
|
2171
2638
|
not isinstance(e, (np.ndarray, tuple, list))
|
2172
2639
|
or len(e) == 0
|
@@ -2180,7 +2647,9 @@ class Geomstr:
|
|
2180
2647
|
t = np.sort(t)
|
2181
2648
|
last = 0.0
|
2182
2649
|
for t0 in sorted(t):
|
2183
|
-
splits = list(
|
2650
|
+
splits = list(
|
2651
|
+
self._split_cubic(e, (t0 - last) / (1 - last), breaks=breaks)
|
2652
|
+
)
|
2184
2653
|
last = t0
|
2185
2654
|
yield splits[0]
|
2186
2655
|
e = splits[1]
|
@@ -2194,6 +2663,7 @@ class Geomstr:
|
|
2194
2663
|
r2_1 = t * (r1_2 - r1_1) + r1_1
|
2195
2664
|
r3 = t * (r2_1 - r2_0) + r2_0
|
2196
2665
|
yield start, r1_0, info, r2_0, r3
|
2666
|
+
# yield r3, 0, complex(TYPE_END, info.imag), 0, r3
|
2197
2667
|
yield r3, r2_1, info, r1_2, end
|
2198
2668
|
|
2199
2669
|
def normal(self, e, t):
|
@@ -2444,22 +2914,22 @@ class Geomstr:
|
|
2444
2914
|
if oinfo.real == TYPE_LINE:
|
2445
2915
|
yield from self._line_line_intersections(line1, line2)
|
2446
2916
|
return
|
2447
|
-
|
2448
|
-
|
2449
|
-
|
2450
|
-
|
2451
|
-
|
2452
|
-
|
2453
|
-
|
2454
|
-
if info.real == TYPE_QUAD:
|
2455
|
-
|
2456
|
-
|
2457
|
-
|
2458
|
-
|
2459
|
-
if info.real == TYPE_CUBIC:
|
2460
|
-
|
2461
|
-
|
2462
|
-
|
2917
|
+
# if oinfo.real == TYPE_QUAD:
|
2918
|
+
# yield from self._line_quad_intersections(line1, line2)
|
2919
|
+
# return
|
2920
|
+
# if oinfo.real == TYPE_CUBIC:
|
2921
|
+
# yield from self._line_cubic_intersections(line1, line2)
|
2922
|
+
# return
|
2923
|
+
#
|
2924
|
+
# if info.real == TYPE_QUAD:
|
2925
|
+
# if oinfo.real == TYPE_LINE:
|
2926
|
+
# yield from self._line_quad_intersections(line2, line1)
|
2927
|
+
# return
|
2928
|
+
#
|
2929
|
+
# if info.real == TYPE_CUBIC:
|
2930
|
+
# if oinfo.real == TYPE_LINE:
|
2931
|
+
# yield from self._line_cubic_intersections(line2, line1)
|
2932
|
+
# return
|
2463
2933
|
yield from self._find_intersections(line1, line2)
|
2464
2934
|
|
2465
2935
|
def _line_line_intersections(self, line1, line2):
|
@@ -2607,9 +3077,9 @@ class Geomstr:
|
|
2607
3077
|
fun2 = self._get_segment_function(segment2[2].real)
|
2608
3078
|
if fun1 is None or fun2 is None:
|
2609
3079
|
return # Only shapes can intersect. We don't do point x point.
|
2610
|
-
yield from self.
|
3080
|
+
yield from self._find_intersections_intercept(segment1, segment2, fun1, fun2)
|
2611
3081
|
|
2612
|
-
def
|
3082
|
+
def _find_intersections_intercept(
|
2613
3083
|
self,
|
2614
3084
|
segment1,
|
2615
3085
|
segment2,
|
@@ -2684,22 +3154,134 @@ class Geomstr:
|
|
2684
3154
|
tb_hit = qb[hits] / denom[hits]
|
2685
3155
|
|
2686
3156
|
for i, hit in enumerate(where_hit):
|
2687
|
-
|
2688
|
-
|
3157
|
+
# Zoomed min+segment intersected.
|
3158
|
+
# Fractional guess within intersected segment
|
3159
|
+
at_guess = ta[0] + (hit[1] + ta_hit[i]) * step_a
|
3160
|
+
bt_guess = tb[0] + (hit[0] + tb_hit[i]) * step_b
|
3161
|
+
|
3162
|
+
if depth == enhancements:
|
3163
|
+
# We've enhanced as best as we can, yield the current + segment t-value to our answer
|
3164
|
+
yield at_guess, bt_guess
|
3165
|
+
else:
|
3166
|
+
yield from self._find_intersections_intercept(
|
3167
|
+
segment1,
|
3168
|
+
segment2,
|
3169
|
+
fun1,
|
3170
|
+
fun2,
|
3171
|
+
ta=(at_guess - step_a / 2, at_guess + step_a / 2, at_guess),
|
3172
|
+
tb=(bt_guess - step_b / 2, bt_guess + step_b / 2, bt_guess),
|
3173
|
+
samples=enhance_samples,
|
3174
|
+
depth=depth + 1,
|
3175
|
+
enhancements=enhancements,
|
3176
|
+
enhance_samples=enhance_samples,
|
3177
|
+
)
|
3178
|
+
|
3179
|
+
def _find_intersections_kross(
|
3180
|
+
self,
|
3181
|
+
segment1,
|
3182
|
+
segment2,
|
3183
|
+
fun1,
|
3184
|
+
fun2,
|
3185
|
+
samples=50,
|
3186
|
+
ta=(0.0, 1.0, None),
|
3187
|
+
tb=(0.0, 1.0, None),
|
3188
|
+
depth=0,
|
3189
|
+
enhancements=2,
|
3190
|
+
enhance_samples=50,
|
3191
|
+
):
|
3192
|
+
"""
|
3193
|
+
Calculate intersections by linearized polyline intersections with enhancements.
|
3194
|
+
We calculate probable intersections by linearizing our segment into `sample` polylines
|
3195
|
+
we then find those intersecting segments and the range of t where those intersections
|
3196
|
+
could have occurred and then subdivide those segments in a series of enhancements to
|
3197
|
+
find their intersections with increased precision.
|
3198
|
+
|
3199
|
+
This code is fast, but it could fail by both finding a rare phantom intersection (if there
|
3200
|
+
is a low or no enhancements) or by failing to find a real intersection. Because the polylines
|
3201
|
+
approximation did not intersect in the base case.
|
3202
|
+
|
3203
|
+
At a resolution of about 1e-15 the intersection calculations become unstable and intersection
|
3204
|
+
candidates can duplicate or become lost. We terminate at that point and give the last best
|
3205
|
+
guess.
|
3206
|
+
|
3207
|
+
:param segment1:
|
3208
|
+
:param segment2:
|
3209
|
+
:param samples:
|
3210
|
+
:param ta:
|
3211
|
+
:param tb:
|
3212
|
+
:param depth:
|
3213
|
+
:param enhancements:
|
3214
|
+
:param enhance_samples:
|
3215
|
+
:return:
|
3216
|
+
"""
|
3217
|
+
assert samples >= 2
|
3218
|
+
a = np.linspace(ta[0], ta[1], num=samples)
|
3219
|
+
b = np.linspace(tb[0], tb[1], num=samples)
|
3220
|
+
step_a = a[1] - a[0]
|
3221
|
+
step_b = b[1] - b[0]
|
3222
|
+
j = fun1(segment1, a)
|
3223
|
+
k = fun2(segment2, b)
|
3224
|
+
|
3225
|
+
p0 = j[:-1]
|
3226
|
+
d0 = j[1:] - j[:-1]
|
3227
|
+
p1 = k[:-1]
|
3228
|
+
d1 = k[1:] - k[:-1]
|
3229
|
+
|
3230
|
+
ap0, ap1 = np.meshgrid(p0, p1)
|
3231
|
+
ad0, ad1 = np.meshgrid(d0, d1)
|
3232
|
+
e = ap1 - ap0
|
3233
|
+
ex = np.real(e)
|
3234
|
+
ey = np.imag(e)
|
3235
|
+
d0x = np.real(ad0)
|
3236
|
+
d0y = np.imag(ad0)
|
3237
|
+
d1x = np.real(ad1)
|
3238
|
+
d1y = np.imag(ad1)
|
3239
|
+
|
3240
|
+
kross = (d0x * d1y) - (d0y * d1x)
|
3241
|
+
# sqkross = kross * kross
|
3242
|
+
# sqLen0 = np.real(ad0) * np.real(ad0) + np.imag(ad0) * np.imag(ad0)
|
3243
|
+
# sqLen1 = np.real(ad1) * np.real(ad1) + np.imag(ad1) * np.imag(ad1)
|
3244
|
+
s = ((ex * d1y) - (ey * d1x)) / kross
|
3245
|
+
t = ((ex * d0y) - (ey * d0x)) / kross
|
3246
|
+
hits = np.dstack(
|
3247
|
+
(
|
3248
|
+
# sqkross > 0.01 * sqLen0 * sqLen1,
|
3249
|
+
s >= 0,
|
3250
|
+
s <= 1,
|
3251
|
+
t >= 0,
|
3252
|
+
t <= 1,
|
3253
|
+
)
|
3254
|
+
).all(axis=2)
|
3255
|
+
where_hit = np.argwhere(hits)
|
3256
|
+
|
3257
|
+
# pos = ap0[hits] + s[hits] * ad0[hits]
|
3258
|
+
if len(where_hit) != 1 and step_a < 1e-10:
|
3259
|
+
# We're hits are becoming unstable give last best value.
|
3260
|
+
if ta[2] is not None and tb[2] is not None:
|
3261
|
+
yield ta[2], tb[2]
|
3262
|
+
return
|
3263
|
+
|
3264
|
+
# Calculate the t values for the intersections
|
3265
|
+
ta_hit = s[hits]
|
3266
|
+
tb_hit = t[hits]
|
3267
|
+
|
3268
|
+
for i, hit in enumerate(where_hit):
|
3269
|
+
# Zoomed min+segment intersected.
|
2689
3270
|
# Fractional guess within intersected segment
|
2690
|
-
|
2691
|
-
|
3271
|
+
at_guess = ta[0] + (hit[1] + ta_hit[i]) * step_a
|
3272
|
+
bt_guess = tb[0] + (hit[0] + tb_hit[i]) * step_b
|
3273
|
+
|
2692
3274
|
if depth == enhancements:
|
2693
3275
|
# We've enhanced as best as we can, yield the current + segment t-value to our answer
|
2694
|
-
yield
|
3276
|
+
yield at_guess, bt_guess
|
2695
3277
|
else:
|
2696
|
-
yield from self.
|
3278
|
+
yield from self._find_intersections_kross(
|
2697
3279
|
segment1,
|
2698
3280
|
segment2,
|
2699
3281
|
fun1,
|
2700
3282
|
fun2,
|
2701
|
-
ta=(
|
2702
|
-
tb=(
|
3283
|
+
ta=(at_guess - step_a / 2, at_guess + step_a / 2, at_guess),
|
3284
|
+
tb=(bt_guess - step_b / 2, bt_guess + step_b / 2, bt_guess),
|
2703
3285
|
samples=enhance_samples,
|
2704
3286
|
depth=depth + 1,
|
2705
3287
|
enhancements=enhancements,
|
@@ -2735,6 +3317,57 @@ class Geomstr:
|
|
2735
3317
|
finally:
|
2736
3318
|
np.seterr(**old_np_seterr)
|
2737
3319
|
|
3320
|
+
def brute_line_intersections(self):
|
3321
|
+
"""
|
3322
|
+
Brute line intersections finds all the intersections of all the lines in the geomstr with brute force.
|
3323
|
+
|
3324
|
+
@return: intersection-indexes, position, t-values
|
3325
|
+
"""
|
3326
|
+
geoms = self.segments[: self.index]
|
3327
|
+
infos = np.real(geoms[:, 2]).astype(int)
|
3328
|
+
q = np.where(infos == TYPE_LINE)
|
3329
|
+
starts = geoms[q][:, 0]
|
3330
|
+
ends = geoms[q][:, -1]
|
3331
|
+
lines = np.dstack((starts, ends))[0]
|
3332
|
+
x, y = np.triu_indices(len(starts), 1)
|
3333
|
+
j = lines[x]
|
3334
|
+
k = lines[y]
|
3335
|
+
a1 = j[:, 0]
|
3336
|
+
ax1 = np.real(a1)
|
3337
|
+
ay1 = np.imag(a1)
|
3338
|
+
b1 = k[:, 0]
|
3339
|
+
bx1 = np.real(b1)
|
3340
|
+
by1 = np.imag(b1)
|
3341
|
+
a2 = j[:, 1]
|
3342
|
+
ax2 = np.real(a2)
|
3343
|
+
ay2 = np.imag(a2)
|
3344
|
+
b2 = k[:, 1]
|
3345
|
+
bx2 = np.real(b2)
|
3346
|
+
by2 = np.imag(b2)
|
3347
|
+
|
3348
|
+
denom = (by2 - by1) * (ax2 - ax1) - (bx2 - bx1) * (ay2 - ay1)
|
3349
|
+
qa = (bx2 - bx1) * (ay1 - by1) - (by2 - by1) * (ax1 - bx1)
|
3350
|
+
qb = (ax2 - ax1) * (ay1 - by1) - (ay2 - ay1) * (ax1 - bx1)
|
3351
|
+
hits = np.dstack(
|
3352
|
+
(
|
3353
|
+
denom != 0, # Cannot be parallel.
|
3354
|
+
np.sign(denom) == np.sign(qa), # D and Qa must have same sign.
|
3355
|
+
np.sign(denom) == np.sign(qb), # D and Qb must have same sign.
|
3356
|
+
abs(denom) >= abs(qa), # D >= Qa (else not between 0 - 1)
|
3357
|
+
abs(denom) >= abs(qb), # D >= Qb (else not between 0 - 1)
|
3358
|
+
)
|
3359
|
+
)
|
3360
|
+
hits = hits.all(axis=2)[0]
|
3361
|
+
|
3362
|
+
where_hits = np.dstack((x[hits], y[hits]))[0]
|
3363
|
+
ta_hit = qa[hits] / denom[hits]
|
3364
|
+
tb_hit = qb[hits] / denom[hits]
|
3365
|
+
|
3366
|
+
x_vals = ax1[hits] + ta_hit * (ax2[hits] - ax1[hits])
|
3367
|
+
y_vals = ay1[hits] + ta_hit * (ay2[hits] - ay1[hits])
|
3368
|
+
|
3369
|
+
return where_hits, x_vals + y_vals * 1j, ta_hit, tb_hit
|
3370
|
+
|
2738
3371
|
#######################
|
2739
3372
|
# Geom Tranformations
|
2740
3373
|
#######################
|
@@ -3196,13 +3829,20 @@ class Geomstr:
|
|
3196
3829
|
@return:
|
3197
3830
|
"""
|
3198
3831
|
line = self.segments[e]
|
3199
|
-
|
3200
|
-
|
3201
|
-
|
3202
|
-
|
3203
|
-
|
3832
|
+
if len(line.shape) == 2:
|
3833
|
+
a = line[:, 0]
|
3834
|
+
b = line[:, -1]
|
3835
|
+
else:
|
3836
|
+
a = line[0]
|
3837
|
+
b = line[-1]
|
3838
|
+
old_np_seterr = np.seterr(invalid="ignore", divide="ignore")
|
3839
|
+
try:
|
3840
|
+
m = (b.imag - a.imag) / (b.real - a.real)
|
3841
|
+
finally:
|
3842
|
+
np.seterr(**old_np_seterr)
|
3843
|
+
return m
|
3204
3844
|
|
3205
|
-
def
|
3845
|
+
def y_at_axis(self, e):
|
3206
3846
|
"""
|
3207
3847
|
y_intercept value between start and end points.
|
3208
3848
|
|
@@ -3210,12 +3850,18 @@ class Geomstr:
|
|
3210
3850
|
@return:
|
3211
3851
|
"""
|
3212
3852
|
line = self.segments[e]
|
3213
|
-
|
3214
|
-
|
3215
|
-
|
3216
|
-
|
3217
|
-
|
3218
|
-
|
3853
|
+
if len(line.shape) == 2:
|
3854
|
+
a = line[:, 0]
|
3855
|
+
b = line[:, -1]
|
3856
|
+
else:
|
3857
|
+
a = line[0]
|
3858
|
+
b = line[-1]
|
3859
|
+
old_np_seterr = np.seterr(invalid="ignore", divide="ignore")
|
3860
|
+
try:
|
3861
|
+
im = (b.imag - a.imag) / (b.real - a.real)
|
3862
|
+
return a.imag - (im * a.real)
|
3863
|
+
finally:
|
3864
|
+
np.seterr(**old_np_seterr)
|
3219
3865
|
|
3220
3866
|
def endpoint_min_y(self, e):
|
3221
3867
|
"""
|
@@ -3285,12 +3931,45 @@ class Geomstr:
|
|
3285
3931
|
@param y:
|
3286
3932
|
@return:
|
3287
3933
|
"""
|
3288
|
-
|
3289
|
-
|
3290
|
-
|
3291
|
-
|
3292
|
-
|
3293
|
-
|
3934
|
+
line = self.segments[e]
|
3935
|
+
if len(line.shape) == 2:
|
3936
|
+
a = line[:, 0]
|
3937
|
+
b = line[:, -1]
|
3938
|
+
else:
|
3939
|
+
a = line[0]
|
3940
|
+
b = line[-1]
|
3941
|
+
old_np_seterr = np.seterr(invalid="ignore", divide="ignore")
|
3942
|
+
try:
|
3943
|
+
# If horizontal slope is undefined. But, all x-ints are at x since x0=x1
|
3944
|
+
m = (b.imag - a.imag) / (b.real - a.real)
|
3945
|
+
y0 = a.imag - (m * a.real)
|
3946
|
+
return np.where(~np.isinf(m), (y - y0) / m, a.real)
|
3947
|
+
finally:
|
3948
|
+
np.seterr(**old_np_seterr)
|
3949
|
+
|
3950
|
+
def y_intercept(self, e, x):
|
3951
|
+
"""
|
3952
|
+
Gives the y_intercept of a line at a specific value of x.
|
3953
|
+
|
3954
|
+
@param e:
|
3955
|
+
@param x:
|
3956
|
+
@return:
|
3957
|
+
"""
|
3958
|
+
line = self.segments[e]
|
3959
|
+
if len(line.shape) == 2:
|
3960
|
+
a = line[:, 0]
|
3961
|
+
b = line[:, -1]
|
3962
|
+
else:
|
3963
|
+
a = line[0]
|
3964
|
+
b = line[-1]
|
3965
|
+
old_np_seterr = np.seterr(invalid="ignore", divide="ignore")
|
3966
|
+
try:
|
3967
|
+
# If vertical slope is undefined. But, all y-ints are at y since y0=y1
|
3968
|
+
m = (b.real - a.real) / (b.imag - a.imag)
|
3969
|
+
x0 = a.real - (m * a.imag)
|
3970
|
+
return np.where(~np.isinf(m), (x - x0) / m, a.imag)
|
3971
|
+
finally:
|
3972
|
+
np.seterr(**old_np_seterr)
|
3294
3973
|
|
3295
3974
|
#######################
|
3296
3975
|
# Geometry Window Functions
|
@@ -3315,7 +3994,8 @@ class Geomstr:
|
|
3315
3994
|
elif np.real(i) == TYPE_CUBIC:
|
3316
3995
|
path.cubic(c0, c1, e)
|
3317
3996
|
elif np.real(i) == TYPE_ARC:
|
3318
|
-
path.
|
3997
|
+
path.append(Arc(start=s, control=c0, end=e))
|
3998
|
+
# path.arc(start=s, control=c0, end=e)
|
3319
3999
|
elif np.real(i) == TYPE_POINT:
|
3320
4000
|
path.move(s)
|
3321
4001
|
path.closed()
|
@@ -3403,6 +4083,22 @@ class Geomstr:
|
|
3403
4083
|
if last != self.index:
|
3404
4084
|
yield Geomstr(self.segments[last : self.index])
|
3405
4085
|
|
4086
|
+
def render(self, buffer=10, scale=1):
|
4087
|
+
sb = Scanbeam(self)
|
4088
|
+
nx, ny, mx, my = self.bbox()
|
4089
|
+
px, py = np.mgrid[
|
4090
|
+
nx - buffer : mx + buffer : scale, ny - buffer : my + buffer : scale
|
4091
|
+
]
|
4092
|
+
ppx = px + 1j * py
|
4093
|
+
pxs = ppx.ravel()
|
4094
|
+
data = sb.points_in_polygon(pxs)
|
4095
|
+
|
4096
|
+
from PIL import Image
|
4097
|
+
|
4098
|
+
size = ppx.shape[::-1]
|
4099
|
+
databytes = np.packbits(data)
|
4100
|
+
return Image.frombytes(mode="1", size=size, data=databytes)
|
4101
|
+
|
3406
4102
|
def draw(self, draw, offset_x, offset_y):
|
3407
4103
|
"""
|
3408
4104
|
Though not a requirement, this draws with the given ImageDraw api found in Pillow.
|