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.
Files changed (187) hide show
  1. meerk40t/balormk/balor_params.py +1 -43
  2. meerk40t/balormk/controller.py +1 -41
  3. meerk40t/balormk/device.py +16 -22
  4. meerk40t/balormk/driver.py +4 -4
  5. meerk40t/balormk/gui/balorconfig.py +2 -2
  6. meerk40t/balormk/gui/balorcontroller.py +13 -5
  7. meerk40t/balormk/gui/baloroperationproperties.py +0 -46
  8. meerk40t/balormk/gui/gui.py +17 -17
  9. meerk40t/camera/gui/camerapanel.py +18 -11
  10. meerk40t/core/cutcode/rastercut.py +3 -1
  11. meerk40t/core/cutplan.py +145 -14
  12. meerk40t/core/elements/clipboard.py +18 -9
  13. meerk40t/core/elements/element_treeops.py +320 -180
  14. meerk40t/core/elements/element_types.py +7 -2
  15. meerk40t/core/elements/elements.py +53 -27
  16. meerk40t/core/elements/geometry.py +8 -0
  17. meerk40t/core/elements/offset_clpr.py +129 -4
  18. meerk40t/core/elements/offset_mk.py +3 -1
  19. meerk40t/core/elements/shapes.py +28 -25
  20. meerk40t/core/laserjob.py +7 -0
  21. meerk40t/core/node/bootstrap.py +4 -0
  22. meerk40t/core/node/effect_hatch.py +85 -96
  23. meerk40t/core/node/effect_wobble.py +309 -0
  24. meerk40t/core/node/elem_image.py +49 -19
  25. meerk40t/core/node/elem_line.py +60 -0
  26. meerk40t/core/node/elem_rect.py +5 -3
  27. meerk40t/core/node/image_processed.py +766 -0
  28. meerk40t/core/node/image_raster.py +113 -0
  29. meerk40t/core/node/node.py +120 -1
  30. meerk40t/core/node/op_cut.py +2 -8
  31. meerk40t/core/node/op_dots.py +0 -8
  32. meerk40t/core/node/op_engrave.py +2 -18
  33. meerk40t/core/node/op_image.py +22 -35
  34. meerk40t/core/node/op_raster.py +0 -9
  35. meerk40t/core/planner.py +32 -2
  36. meerk40t/core/svg_io.py +699 -461
  37. meerk40t/core/treeop.py +191 -0
  38. meerk40t/core/undos.py +15 -1
  39. meerk40t/core/units.py +14 -4
  40. meerk40t/device/dummydevice.py +3 -2
  41. meerk40t/device/gui/defaultactions.py +43 -55
  42. meerk40t/device/gui/formatterpanel.py +58 -49
  43. meerk40t/device/gui/warningpanel.py +12 -12
  44. meerk40t/device/mixins.py +13 -0
  45. meerk40t/dxf/dxf_io.py +9 -5
  46. meerk40t/extra/ezd.py +28 -26
  47. meerk40t/extra/imageactions.py +300 -308
  48. meerk40t/extra/lbrn.py +19 -2
  49. meerk40t/fill/fills.py +6 -6
  50. meerk40t/fill/patternfill.py +1061 -1061
  51. meerk40t/fill/patterns.py +2 -6
  52. meerk40t/grbl/controller.py +168 -52
  53. meerk40t/grbl/device.py +23 -18
  54. meerk40t/grbl/driver.py +39 -0
  55. meerk40t/grbl/emulator.py +79 -19
  56. meerk40t/grbl/gcodejob.py +10 -0
  57. meerk40t/grbl/gui/grblconfiguration.py +2 -2
  58. meerk40t/grbl/gui/grblcontroller.py +24 -8
  59. meerk40t/grbl/gui/grblhardwareconfig.py +153 -0
  60. meerk40t/grbl/gui/gui.py +17 -14
  61. meerk40t/grbl/mock_connection.py +15 -34
  62. meerk40t/grbl/plugin.py +0 -4
  63. meerk40t/grbl/serial_connection.py +2 -1
  64. meerk40t/gui/about.py +8 -5
  65. meerk40t/gui/alignment.py +10 -6
  66. meerk40t/gui/basicops.py +27 -17
  67. meerk40t/gui/bufferview.py +2 -2
  68. meerk40t/gui/choicepropertypanel.py +101 -13
  69. meerk40t/gui/consolepanel.py +12 -9
  70. meerk40t/gui/devicepanel.py +38 -25
  71. meerk40t/gui/executejob.py +6 -4
  72. meerk40t/gui/help_assets/help_assets.py +13 -10
  73. meerk40t/gui/hersheymanager.py +8 -6
  74. meerk40t/gui/icons.py +1951 -3065
  75. meerk40t/gui/imagesplitter.py +14 -7
  76. meerk40t/gui/keymap.py +3 -3
  77. meerk40t/gui/laserpanel.py +151 -84
  78. meerk40t/gui/laserrender.py +61 -70
  79. meerk40t/gui/lasertoolpanel.py +8 -7
  80. meerk40t/gui/materialtest.py +3 -3
  81. meerk40t/gui/mkdebug.py +254 -1
  82. meerk40t/gui/navigationpanels.py +321 -180
  83. meerk40t/gui/notes.py +3 -3
  84. meerk40t/gui/opassignment.py +12 -12
  85. meerk40t/gui/operation_info.py +13 -13
  86. meerk40t/gui/plugin.py +5 -0
  87. meerk40t/gui/position.py +20 -18
  88. meerk40t/gui/preferences.py +21 -6
  89. meerk40t/gui/propertypanels/attributes.py +70 -22
  90. meerk40t/gui/propertypanels/blobproperty.py +2 -2
  91. meerk40t/gui/propertypanels/consoleproperty.py +2 -2
  92. meerk40t/gui/propertypanels/groupproperties.py +3 -3
  93. meerk40t/gui/propertypanels/hatchproperty.py +11 -18
  94. meerk40t/gui/propertypanels/imageproperty.py +4 -3
  95. meerk40t/gui/propertypanels/opbranchproperties.py +1 -1
  96. meerk40t/gui/propertypanels/pathproperty.py +2 -2
  97. meerk40t/gui/propertypanels/pointproperty.py +2 -2
  98. meerk40t/gui/propertypanels/propertywindow.py +4 -4
  99. meerk40t/gui/propertypanels/textproperty.py +3 -3
  100. meerk40t/gui/propertypanels/wobbleproperty.py +204 -0
  101. meerk40t/gui/ribbon.py +367 -259
  102. meerk40t/gui/scene/scene.py +31 -5
  103. meerk40t/gui/scenewidgets/elementswidget.py +12 -4
  104. meerk40t/gui/scenewidgets/gridwidget.py +2 -2
  105. meerk40t/gui/scenewidgets/laserpathwidget.py +7 -2
  106. meerk40t/gui/scenewidgets/machineoriginwidget.py +6 -2
  107. meerk40t/gui/scenewidgets/relocatewidget.py +1 -1
  108. meerk40t/gui/scenewidgets/reticlewidget.py +9 -0
  109. meerk40t/gui/scenewidgets/selectionwidget.py +12 -7
  110. meerk40t/gui/simpleui.py +95 -8
  111. meerk40t/gui/simulation.py +44 -36
  112. meerk40t/gui/spoolerpanel.py +124 -26
  113. meerk40t/gui/statusbarwidgets/defaultoperations.py +18 -6
  114. meerk40t/gui/statusbarwidgets/infowidget.py +2 -2
  115. meerk40t/gui/statusbarwidgets/opassignwidget.py +12 -12
  116. meerk40t/gui/statusbarwidgets/shapepropwidget.py +45 -18
  117. meerk40t/gui/statusbarwidgets/statusbar.py +11 -4
  118. meerk40t/gui/themes.py +78 -0
  119. meerk40t/gui/toolwidgets/toolcircle.py +2 -1
  120. meerk40t/gui/toolwidgets/toolellipse.py +2 -1
  121. meerk40t/gui/toolwidgets/toolimagecut.py +132 -0
  122. meerk40t/gui/toolwidgets/toolline.py +144 -0
  123. meerk40t/gui/toolwidgets/toolnodeedit.py +72 -145
  124. meerk40t/gui/toolwidgets/toolpoint.py +1 -1
  125. meerk40t/gui/toolwidgets/toolpolygon.py +8 -55
  126. meerk40t/gui/toolwidgets/toolrect.py +2 -1
  127. meerk40t/gui/usbconnect.py +2 -2
  128. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +2 -2
  129. meerk40t/gui/utilitywidgets/harmonograph.py +7 -7
  130. meerk40t/gui/utilitywidgets/scalewidget.py +1 -1
  131. meerk40t/gui/wordlisteditor.py +33 -18
  132. meerk40t/gui/wxmeerk40t.py +166 -66
  133. meerk40t/gui/wxmmain.py +236 -157
  134. meerk40t/gui/wxmribbon.py +49 -25
  135. meerk40t/gui/wxmscene.py +49 -38
  136. meerk40t/gui/wxmtree.py +216 -85
  137. meerk40t/gui/wxutils.py +62 -4
  138. meerk40t/image/imagetools.py +443 -15
  139. meerk40t/internal_plugins.py +2 -10
  140. meerk40t/kernel/kernel.py +12 -4
  141. meerk40t/lihuiyu/controller.py +7 -7
  142. meerk40t/lihuiyu/device.py +3 -1
  143. meerk40t/lihuiyu/driver.py +3 -0
  144. meerk40t/lihuiyu/gui/gui.py +8 -8
  145. meerk40t/lihuiyu/gui/lhyaccelgui.py +2 -2
  146. meerk40t/lihuiyu/gui/lhycontrollergui.py +73 -27
  147. meerk40t/lihuiyu/gui/lhydrivergui.py +2 -2
  148. meerk40t/lihuiyu/gui/tcpcontroller.py +22 -9
  149. meerk40t/main.py +6 -1
  150. meerk40t/moshi/controller.py +5 -5
  151. meerk40t/moshi/device.py +5 -2
  152. meerk40t/moshi/driver.py +4 -0
  153. meerk40t/moshi/gui/gui.py +8 -8
  154. meerk40t/moshi/gui/moshicontrollergui.py +24 -8
  155. meerk40t/moshi/gui/moshidrivergui.py +2 -2
  156. meerk40t/newly/controller.py +2 -0
  157. meerk40t/newly/device.py +9 -2
  158. meerk40t/newly/driver.py +4 -0
  159. meerk40t/newly/gui/gui.py +16 -17
  160. meerk40t/newly/gui/newlyconfig.py +2 -2
  161. meerk40t/newly/gui/newlycontroller.py +13 -5
  162. meerk40t/rotary/gui/gui.py +2 -2
  163. meerk40t/rotary/gui/rotarysettings.py +2 -2
  164. meerk40t/ruida/device.py +3 -0
  165. meerk40t/ruida/driver.py +4 -0
  166. meerk40t/ruida/gui/gui.py +6 -6
  167. meerk40t/ruida/gui/ruidaconfig.py +2 -2
  168. meerk40t/ruida/gui/ruidacontroller.py +13 -5
  169. meerk40t/svgelements.py +9 -9
  170. meerk40t/tools/geomstr.py +849 -153
  171. meerk40t/tools/kerftest.py +8 -4
  172. meerk40t/tools/livinghinges.py +15 -8
  173. {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/METADATA +21 -16
  174. {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/RECORD +185 -177
  175. {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/entry_points.txt +0 -1
  176. test/test_core_elements.py +8 -24
  177. test/test_file_svg.py +88 -0
  178. test/test_fill.py +9 -9
  179. test/test_geomstr.py +258 -8
  180. test/test_kernel.py +4 -0
  181. test/test_tools_rasterplotter.py +29 -0
  182. meerk40t/extra/embroider.py +0 -56
  183. meerk40t/extra/pathoptimize.py +0 -249
  184. {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/LICENSE +0 -0
  185. {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/WHEEL +0 -0
  186. {meerk40t-0.9.2000.dist-info → meerk40t-0.9.3001.dist-info}/top_level.txt +0 -0
  187. {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 clip(self, subject, split=True):
101
- clip = self.clipping_shape
102
- if split:
103
- s = subject.segments[: subject.index]
104
- c = clip.segments[: clip.index]
105
- cmaxx = np.where(
106
- np.real(c[:, 0]) > np.real(c[:, -1]),
107
- np.real(c[:, 0]),
108
- np.real(c[:, -1]),
109
- )
110
- sminx = np.where(
111
- np.real(s[:, 0]) < np.real(s[:, -1]),
112
- np.real(s[:, 0]),
113
- np.real(s[:, -1]),
114
- )
115
- cminx = np.where(
116
- np.real(c[:, 0]) < np.real(c[:, -1]),
117
- np.real(c[:, 0]),
118
- np.real(c[:, -1]),
119
- )
120
- smaxx = np.where(
121
- np.real(s[:, 0]) > np.real(s[:, -1]),
122
- np.real(s[:, 0]),
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
- smaxy = np.where(
141
- np.imag(s[:, 0]) > np.imag(s[:, -1]),
142
- np.imag(s[:, 0]),
143
- np.imag(s[:, -1]),
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
- x0, y0 = np.meshgrid(cmaxx, sminx)
146
- x1, y1 = np.meshgrid(cminx, smaxx)
147
- x2, y2 = np.meshgrid(cmaxy, sminy)
148
- x3, y3 = np.meshgrid(cminy, smaxy)
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
- cx = cw + px * 2
282
- cy = ch + py * 2
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
- row_offset = 0
293
-
294
- for c in range(start_index_x, end_index_x):
295
- x = c * cx + row_offset
296
- for r in range(start_index_y, end_index_y):
297
- y = r * cy
298
- if c % 2:
299
- y += 0.5 * cy
300
- # Don't call draw if outside of hinge area
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 PolyBool:
307
- def __init__(self):
308
- self.inputs = list()
309
- # List of each segment info about it.
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
- def add_segments(self, g):
312
- self.inputs.append(g)
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(self._split_quad(e, (t0 - last) / (1 - last)))
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(self._split_cubic(e, (t0 - last) / (1 - last)))
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
- if oinfo.real == TYPE_QUAD:
2448
- yield from self._line_quad_intersections(line1, line2)
2449
- return
2450
- if oinfo.real == TYPE_CUBIC:
2451
- yield from self._line_cubic_intersections(line1, line2)
2452
- return
2453
-
2454
- if info.real == TYPE_QUAD:
2455
- if oinfo.real == TYPE_LINE:
2456
- yield from self._line_quad_intersections(line2, line1)
2457
- return
2458
-
2459
- if info.real == TYPE_CUBIC:
2460
- if oinfo.real == TYPE_LINE:
2461
- yield from self._line_cubic_intersections(line2, line1)
2462
- return
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._find_intersections_main(segment1, segment2, fun1, fun2)
3080
+ yield from self._find_intersections_intercept(segment1, segment2, fun1, fun2)
2611
3081
 
2612
- def _find_intersections_main(
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
- at = ta[0] + float(hit[1]) * step_a # Zoomed min+segment intersected.
2688
- bt = tb[0] + float(hit[0]) * step_b
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
- a_fractional = ta_hit[i] * step_a
2691
- b_fractional = tb_hit[i] * step_b
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 at + a_fractional, bt + b_fractional
3276
+ yield at_guess, bt_guess
2695
3277
  else:
2696
- yield from self._find_intersections_main(
3278
+ yield from self._find_intersections_kross(
2697
3279
  segment1,
2698
3280
  segment2,
2699
3281
  fun1,
2700
3282
  fun2,
2701
- ta=(at, at + step_a, at + a_fractional),
2702
- tb=(bt, bt + step_b, bt + b_fractional),
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
- a = line[0]
3200
- b = line[-1]
3201
- if b.real - a.real == 0:
3202
- return float("inf")
3203
- return (b.imag - a.imag) / (b.real - a.real)
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 y_intercept(self, e):
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
- a = line[0]
3214
- b = line[-1]
3215
- if b.real - a.real == 0:
3216
- return float("inf")
3217
- im = (b.imag - a.imag) / (b.real - a.real)
3218
- return a.imag - (im * a.real)
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
- m = self.slope(e)
3289
- b = self.y_intercept(e)
3290
- if math.isnan(m) or math.isinf(m):
3291
- low = self.endpoint_min_y(e)
3292
- return low.real
3293
- return (y - b) / m
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.arc(start=s, control=c0, end=e)
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.