meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7020__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 (446) hide show
  1. meerk40t/__init__.py +1 -1
  2. meerk40t/balormk/balor_params.py +167 -167
  3. meerk40t/balormk/clone_loader.py +457 -457
  4. meerk40t/balormk/controller.py +1566 -1512
  5. meerk40t/balormk/cylindermod.py +64 -0
  6. meerk40t/balormk/device.py +966 -1959
  7. meerk40t/balormk/driver.py +778 -591
  8. meerk40t/balormk/galvo_commands.py +1194 -0
  9. meerk40t/balormk/gui/balorconfig.py +237 -111
  10. meerk40t/balormk/gui/balorcontroller.py +191 -184
  11. meerk40t/balormk/gui/baloroperationproperties.py +116 -115
  12. meerk40t/balormk/gui/corscene.py +845 -0
  13. meerk40t/balormk/gui/gui.py +179 -147
  14. meerk40t/balormk/livelightjob.py +466 -382
  15. meerk40t/balormk/mock_connection.py +131 -109
  16. meerk40t/balormk/plugin.py +133 -135
  17. meerk40t/balormk/usb_connection.py +306 -301
  18. meerk40t/camera/__init__.py +1 -1
  19. meerk40t/camera/camera.py +514 -397
  20. meerk40t/camera/gui/camerapanel.py +1241 -1095
  21. meerk40t/camera/gui/gui.py +58 -58
  22. meerk40t/camera/plugin.py +441 -399
  23. meerk40t/ch341/__init__.py +27 -27
  24. meerk40t/ch341/ch341device.py +628 -628
  25. meerk40t/ch341/libusb.py +595 -589
  26. meerk40t/ch341/mock.py +171 -171
  27. meerk40t/ch341/windriver.py +157 -157
  28. meerk40t/constants.py +13 -0
  29. meerk40t/core/__init__.py +1 -1
  30. meerk40t/core/bindalias.py +550 -539
  31. meerk40t/core/core.py +47 -47
  32. meerk40t/core/cutcode/cubiccut.py +73 -73
  33. meerk40t/core/cutcode/cutcode.py +315 -312
  34. meerk40t/core/cutcode/cutgroup.py +141 -137
  35. meerk40t/core/cutcode/cutobject.py +192 -185
  36. meerk40t/core/cutcode/dwellcut.py +37 -37
  37. meerk40t/core/cutcode/gotocut.py +29 -29
  38. meerk40t/core/cutcode/homecut.py +29 -29
  39. meerk40t/core/cutcode/inputcut.py +34 -34
  40. meerk40t/core/cutcode/linecut.py +33 -33
  41. meerk40t/core/cutcode/outputcut.py +34 -34
  42. meerk40t/core/cutcode/plotcut.py +335 -335
  43. meerk40t/core/cutcode/quadcut.py +61 -61
  44. meerk40t/core/cutcode/rastercut.py +168 -148
  45. meerk40t/core/cutcode/waitcut.py +34 -34
  46. meerk40t/core/cutplan.py +1843 -1316
  47. meerk40t/core/drivers.py +330 -329
  48. meerk40t/core/elements/align.py +801 -669
  49. meerk40t/core/elements/branches.py +1858 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4595 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4315 -3617
  54. meerk40t/core/elements/files.py +117 -64
  55. meerk40t/core/elements/geometry.py +473 -224
  56. meerk40t/core/elements/grid.py +467 -316
  57. meerk40t/core/elements/materials.py +158 -94
  58. meerk40t/core/elements/notes.py +50 -38
  59. meerk40t/core/elements/offset_clpr.py +934 -912
  60. meerk40t/core/elements/offset_mk.py +963 -955
  61. meerk40t/core/elements/penbox.py +339 -267
  62. meerk40t/core/elements/placements.py +300 -83
  63. meerk40t/core/elements/render.py +785 -687
  64. meerk40t/core/elements/shapes.py +2618 -2092
  65. meerk40t/core/elements/testcases.py +105 -0
  66. meerk40t/core/elements/trace.py +651 -563
  67. meerk40t/core/elements/tree_commands.py +415 -409
  68. meerk40t/core/elements/undo_redo.py +116 -58
  69. meerk40t/core/elements/wordlist.py +319 -200
  70. meerk40t/core/exceptions.py +9 -9
  71. meerk40t/core/laserjob.py +220 -220
  72. meerk40t/core/logging.py +63 -63
  73. meerk40t/core/node/blobnode.py +83 -86
  74. meerk40t/core/node/bootstrap.py +105 -103
  75. meerk40t/core/node/branch_elems.py +40 -31
  76. meerk40t/core/node/branch_ops.py +45 -38
  77. meerk40t/core/node/branch_regmark.py +48 -41
  78. meerk40t/core/node/cutnode.py +29 -32
  79. meerk40t/core/node/effect_hatch.py +375 -257
  80. meerk40t/core/node/effect_warp.py +398 -0
  81. meerk40t/core/node/effect_wobble.py +441 -309
  82. meerk40t/core/node/elem_ellipse.py +404 -309
  83. meerk40t/core/node/elem_image.py +1082 -801
  84. meerk40t/core/node/elem_line.py +358 -292
  85. meerk40t/core/node/elem_path.py +259 -201
  86. meerk40t/core/node/elem_point.py +129 -102
  87. meerk40t/core/node/elem_polyline.py +310 -246
  88. meerk40t/core/node/elem_rect.py +376 -286
  89. meerk40t/core/node/elem_text.py +445 -418
  90. meerk40t/core/node/filenode.py +59 -40
  91. meerk40t/core/node/groupnode.py +138 -74
  92. meerk40t/core/node/image_processed.py +777 -766
  93. meerk40t/core/node/image_raster.py +156 -113
  94. meerk40t/core/node/layernode.py +31 -31
  95. meerk40t/core/node/mixins.py +135 -107
  96. meerk40t/core/node/node.py +1427 -1304
  97. meerk40t/core/node/nutils.py +117 -114
  98. meerk40t/core/node/op_cut.py +463 -335
  99. meerk40t/core/node/op_dots.py +296 -251
  100. meerk40t/core/node/op_engrave.py +414 -311
  101. meerk40t/core/node/op_image.py +755 -369
  102. meerk40t/core/node/op_raster.py +787 -522
  103. meerk40t/core/node/place_current.py +37 -40
  104. meerk40t/core/node/place_point.py +329 -126
  105. meerk40t/core/node/refnode.py +58 -47
  106. meerk40t/core/node/rootnode.py +225 -219
  107. meerk40t/core/node/util_console.py +48 -48
  108. meerk40t/core/node/util_goto.py +84 -65
  109. meerk40t/core/node/util_home.py +61 -61
  110. meerk40t/core/node/util_input.py +102 -102
  111. meerk40t/core/node/util_output.py +102 -102
  112. meerk40t/core/node/util_wait.py +65 -65
  113. meerk40t/core/parameters.py +709 -707
  114. meerk40t/core/planner.py +875 -785
  115. meerk40t/core/plotplanner.py +656 -652
  116. meerk40t/core/space.py +120 -113
  117. meerk40t/core/spoolers.py +706 -705
  118. meerk40t/core/svg_io.py +1836 -1549
  119. meerk40t/core/treeop.py +534 -445
  120. meerk40t/core/undos.py +278 -124
  121. meerk40t/core/units.py +784 -680
  122. meerk40t/core/view.py +393 -322
  123. meerk40t/core/webhelp.py +62 -62
  124. meerk40t/core/wordlist.py +513 -504
  125. meerk40t/cylinder/cylinder.py +247 -0
  126. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  127. meerk40t/cylinder/gui/gui.py +24 -0
  128. meerk40t/device/__init__.py +1 -1
  129. meerk40t/device/basedevice.py +322 -123
  130. meerk40t/device/devicechoices.py +50 -0
  131. meerk40t/device/dummydevice.py +163 -128
  132. meerk40t/device/gui/defaultactions.py +618 -602
  133. meerk40t/device/gui/effectspanel.py +114 -0
  134. meerk40t/device/gui/formatterpanel.py +253 -290
  135. meerk40t/device/gui/warningpanel.py +337 -260
  136. meerk40t/device/mixins.py +13 -13
  137. meerk40t/dxf/__init__.py +1 -1
  138. meerk40t/dxf/dxf_io.py +766 -554
  139. meerk40t/dxf/plugin.py +47 -35
  140. meerk40t/external_plugins.py +79 -79
  141. meerk40t/external_plugins_build.py +28 -28
  142. meerk40t/extra/cag.py +112 -116
  143. meerk40t/extra/coolant.py +403 -0
  144. meerk40t/extra/encode_detect.py +204 -0
  145. meerk40t/extra/ezd.py +1165 -1165
  146. meerk40t/extra/hershey.py +834 -340
  147. meerk40t/extra/imageactions.py +322 -316
  148. meerk40t/extra/inkscape.py +628 -622
  149. meerk40t/extra/lbrn.py +424 -424
  150. meerk40t/extra/outerworld.py +283 -0
  151. meerk40t/extra/param_functions.py +1542 -1556
  152. meerk40t/extra/potrace.py +257 -253
  153. meerk40t/extra/serial_exchange.py +118 -0
  154. meerk40t/extra/updater.py +602 -453
  155. meerk40t/extra/vectrace.py +147 -146
  156. meerk40t/extra/winsleep.py +83 -83
  157. meerk40t/extra/xcs_reader.py +597 -0
  158. meerk40t/fill/fills.py +781 -335
  159. meerk40t/fill/patternfill.py +1061 -1061
  160. meerk40t/fill/patterns.py +614 -567
  161. meerk40t/grbl/control.py +87 -87
  162. meerk40t/grbl/controller.py +990 -903
  163. meerk40t/grbl/device.py +1084 -768
  164. meerk40t/grbl/driver.py +989 -771
  165. meerk40t/grbl/emulator.py +532 -497
  166. meerk40t/grbl/gcodejob.py +783 -767
  167. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  168. meerk40t/grbl/gui/grblcontroller.py +485 -271
  169. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  170. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  171. meerk40t/grbl/gui/gui.py +147 -116
  172. meerk40t/grbl/interpreter.py +44 -44
  173. meerk40t/grbl/loader.py +22 -22
  174. meerk40t/grbl/mock_connection.py +56 -56
  175. meerk40t/grbl/plugin.py +294 -264
  176. meerk40t/grbl/serial_connection.py +93 -88
  177. meerk40t/grbl/tcp_connection.py +81 -79
  178. meerk40t/grbl/ws_connection.py +112 -0
  179. meerk40t/gui/__init__.py +1 -1
  180. meerk40t/gui/about.py +2042 -296
  181. meerk40t/gui/alignment.py +1644 -1608
  182. meerk40t/gui/autoexec.py +199 -0
  183. meerk40t/gui/basicops.py +791 -670
  184. meerk40t/gui/bufferview.py +77 -71
  185. meerk40t/gui/busy.py +232 -133
  186. meerk40t/gui/choicepropertypanel.py +1662 -1469
  187. meerk40t/gui/consolepanel.py +706 -542
  188. meerk40t/gui/devicepanel.py +687 -581
  189. meerk40t/gui/dialogoptions.py +110 -107
  190. meerk40t/gui/executejob.py +316 -306
  191. meerk40t/gui/fonts.py +90 -90
  192. meerk40t/gui/functionwrapper.py +252 -0
  193. meerk40t/gui/gui_mixins.py +729 -0
  194. meerk40t/gui/guicolors.py +205 -182
  195. meerk40t/gui/help_assets/help_assets.py +218 -201
  196. meerk40t/gui/helper.py +154 -0
  197. meerk40t/gui/hersheymanager.py +1440 -846
  198. meerk40t/gui/icons.py +3422 -2747
  199. meerk40t/gui/imagesplitter.py +555 -508
  200. meerk40t/gui/keymap.py +354 -344
  201. meerk40t/gui/laserpanel.py +897 -806
  202. meerk40t/gui/laserrender.py +1470 -1232
  203. meerk40t/gui/lasertoolpanel.py +805 -793
  204. meerk40t/gui/magnetoptions.py +436 -0
  205. meerk40t/gui/materialmanager.py +2944 -0
  206. meerk40t/gui/materialtest.py +1722 -1694
  207. meerk40t/gui/mkdebug.py +646 -359
  208. meerk40t/gui/mwindow.py +163 -140
  209. meerk40t/gui/navigationpanels.py +2605 -2467
  210. meerk40t/gui/notes.py +143 -142
  211. meerk40t/gui/opassignment.py +414 -410
  212. meerk40t/gui/operation_info.py +310 -299
  213. meerk40t/gui/plugin.py +500 -328
  214. meerk40t/gui/position.py +714 -669
  215. meerk40t/gui/preferences.py +901 -650
  216. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  217. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  218. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  219. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  220. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  221. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  222. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  223. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  224. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  225. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  226. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  227. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  228. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  229. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  230. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  231. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  232. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  233. meerk40t/gui/propertypanels/textproperty.py +770 -755
  234. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  235. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  236. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  237. meerk40t/gui/ribbon.py +2471 -2210
  238. meerk40t/gui/scene/scene.py +1100 -1051
  239. meerk40t/gui/scene/sceneconst.py +22 -22
  240. meerk40t/gui/scene/scenepanel.py +439 -349
  241. meerk40t/gui/scene/scenespacewidget.py +365 -365
  242. meerk40t/gui/scene/widget.py +518 -505
  243. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  244. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  245. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  246. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  247. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  248. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  249. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  250. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  251. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  252. meerk40t/gui/scenewidgets/rectselectwidget.py +592 -346
  253. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  254. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  255. meerk40t/gui/scenewidgets/selectionwidget.py +2958 -2756
  256. meerk40t/gui/simpleui.py +362 -333
  257. meerk40t/gui/simulation.py +2451 -2094
  258. meerk40t/gui/snapoptions.py +208 -203
  259. meerk40t/gui/spoolerpanel.py +1227 -1180
  260. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  261. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  262. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  263. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  264. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  265. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  266. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  267. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  268. meerk40t/gui/themes.py +200 -78
  269. meerk40t/gui/tips.py +590 -0
  270. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  271. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  272. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  273. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  274. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  275. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  276. meerk40t/gui/toolwidgets/toolline.py +39 -144
  277. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  278. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  279. meerk40t/gui/toolwidgets/toolmeasure.py +163 -216
  280. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  281. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  282. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  283. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  284. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  285. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  286. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  287. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  288. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  289. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  290. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  291. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  292. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  293. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  294. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  295. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  296. meerk40t/gui/usbconnect.py +98 -91
  297. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  298. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  299. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  300. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  301. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  302. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  303. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  304. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  305. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  306. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  307. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  308. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  309. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  310. meerk40t/gui/wordlisteditor.py +985 -931
  311. meerk40t/gui/wxmeerk40t.py +1447 -1169
  312. meerk40t/gui/wxmmain.py +5644 -4112
  313. meerk40t/gui/wxmribbon.py +1591 -1076
  314. meerk40t/gui/wxmscene.py +1631 -1453
  315. meerk40t/gui/wxmtree.py +2416 -2089
  316. meerk40t/gui/wxutils.py +1769 -1099
  317. meerk40t/gui/zmatrix.py +102 -102
  318. meerk40t/image/__init__.py +1 -1
  319. meerk40t/image/dither.py +429 -0
  320. meerk40t/image/imagetools.py +2793 -2269
  321. meerk40t/internal_plugins.py +150 -130
  322. meerk40t/kernel/__init__.py +63 -12
  323. meerk40t/kernel/channel.py +259 -212
  324. meerk40t/kernel/context.py +538 -538
  325. meerk40t/kernel/exceptions.py +41 -41
  326. meerk40t/kernel/functions.py +463 -414
  327. meerk40t/kernel/jobs.py +100 -100
  328. meerk40t/kernel/kernel.py +3828 -3571
  329. meerk40t/kernel/lifecycles.py +71 -71
  330. meerk40t/kernel/module.py +49 -49
  331. meerk40t/kernel/service.py +147 -147
  332. meerk40t/kernel/settings.py +383 -343
  333. meerk40t/lihuiyu/controller.py +883 -876
  334. meerk40t/lihuiyu/device.py +1181 -1069
  335. meerk40t/lihuiyu/driver.py +1466 -1372
  336. meerk40t/lihuiyu/gui/gui.py +127 -106
  337. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  338. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  339. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  340. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  341. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  342. meerk40t/lihuiyu/interpreter.py +53 -53
  343. meerk40t/lihuiyu/laserspeed.py +450 -450
  344. meerk40t/lihuiyu/loader.py +90 -90
  345. meerk40t/lihuiyu/parser.py +404 -404
  346. meerk40t/lihuiyu/plugin.py +101 -102
  347. meerk40t/lihuiyu/tcp_connection.py +111 -109
  348. meerk40t/main.py +231 -165
  349. meerk40t/moshi/builder.py +788 -781
  350. meerk40t/moshi/controller.py +505 -499
  351. meerk40t/moshi/device.py +495 -442
  352. meerk40t/moshi/driver.py +862 -696
  353. meerk40t/moshi/gui/gui.py +78 -76
  354. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  355. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  356. meerk40t/moshi/plugin.py +43 -43
  357. meerk40t/network/console_server.py +140 -57
  358. meerk40t/network/kernelserver.py +10 -9
  359. meerk40t/network/tcp_server.py +142 -140
  360. meerk40t/network/udp_server.py +103 -77
  361. meerk40t/network/web_server.py +404 -0
  362. meerk40t/newly/controller.py +1158 -1144
  363. meerk40t/newly/device.py +874 -732
  364. meerk40t/newly/driver.py +540 -412
  365. meerk40t/newly/gui/gui.py +219 -188
  366. meerk40t/newly/gui/newlyconfig.py +116 -101
  367. meerk40t/newly/gui/newlycontroller.py +193 -186
  368. meerk40t/newly/gui/operationproperties.py +51 -51
  369. meerk40t/newly/mock_connection.py +82 -82
  370. meerk40t/newly/newly_params.py +56 -56
  371. meerk40t/newly/plugin.py +1214 -1246
  372. meerk40t/newly/usb_connection.py +322 -322
  373. meerk40t/rotary/gui/gui.py +52 -46
  374. meerk40t/rotary/gui/rotarysettings.py +240 -232
  375. meerk40t/rotary/rotary.py +202 -98
  376. meerk40t/ruida/control.py +291 -91
  377. meerk40t/ruida/controller.py +138 -1088
  378. meerk40t/ruida/device.py +676 -231
  379. meerk40t/ruida/driver.py +534 -472
  380. meerk40t/ruida/emulator.py +1494 -1491
  381. meerk40t/ruida/exceptions.py +4 -4
  382. meerk40t/ruida/gui/gui.py +71 -76
  383. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  384. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  385. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  386. meerk40t/ruida/loader.py +54 -52
  387. meerk40t/ruida/mock_connection.py +57 -109
  388. meerk40t/ruida/plugin.py +124 -87
  389. meerk40t/ruida/rdjob.py +2084 -945
  390. meerk40t/ruida/serial_connection.py +116 -0
  391. meerk40t/ruida/tcp_connection.py +146 -0
  392. meerk40t/ruida/udp_connection.py +73 -0
  393. meerk40t/svgelements.py +9671 -9669
  394. meerk40t/tools/driver_to_path.py +584 -579
  395. meerk40t/tools/geomstr.py +5583 -4680
  396. meerk40t/tools/jhfparser.py +357 -292
  397. meerk40t/tools/kerftest.py +904 -890
  398. meerk40t/tools/livinghinges.py +1168 -1033
  399. meerk40t/tools/pathtools.py +987 -949
  400. meerk40t/tools/pmatrix.py +234 -0
  401. meerk40t/tools/pointfinder.py +942 -942
  402. meerk40t/tools/polybool.py +941 -940
  403. meerk40t/tools/rasterplotter.py +1660 -547
  404. meerk40t/tools/shxparser.py +1047 -901
  405. meerk40t/tools/ttfparser.py +726 -446
  406. meerk40t/tools/zinglplotter.py +595 -593
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +21 -21
  408. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +150 -139
  409. meerk40t-0.9.7020.dist-info/RECORD +446 -0
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +1 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -1
  412. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +1 -1
  413. meerk40t/balormk/elementlightjob.py +0 -159
  414. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  415. test/bootstrap.py +0 -63
  416. test/test_cli.py +0 -12
  417. test/test_core_cutcode.py +0 -418
  418. test/test_core_elements.py +0 -144
  419. test/test_core_plotplanner.py +0 -397
  420. test/test_core_viewports.py +0 -312
  421. test/test_drivers_grbl.py +0 -108
  422. test/test_drivers_lihuiyu.py +0 -443
  423. test/test_drivers_newly.py +0 -113
  424. test/test_element_degenerate_points.py +0 -43
  425. test/test_elements_classify.py +0 -97
  426. test/test_elements_penbox.py +0 -22
  427. test/test_file_svg.py +0 -176
  428. test/test_fill.py +0 -155
  429. test/test_geomstr.py +0 -1523
  430. test/test_geomstr_nodes.py +0 -18
  431. test/test_imagetools_actualize.py +0 -306
  432. test/test_imagetools_wizard.py +0 -258
  433. test/test_kernel.py +0 -200
  434. test/test_laser_speeds.py +0 -3303
  435. test/test_length.py +0 -57
  436. test/test_lifecycle.py +0 -66
  437. test/test_operations.py +0 -251
  438. test/test_operations_hatch.py +0 -57
  439. test/test_ruida.py +0 -19
  440. test/test_spooler.py +0 -22
  441. test/test_tools_rasterplotter.py +0 -29
  442. test/test_wobble.py +0 -133
  443. test/test_zingl.py +0 -124
  444. {test → meerk40t/cylinder}/__init__.py +0 -0
  445. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  446. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/entry_points.txt +0 -0
@@ -1,547 +1,1660 @@
1
- """
2
- The RasterPlotter is a plotter that maps particular raster pixels to directional and raster
3
- methods. This class should be expanded to cover most raster situations.
4
-
5
- The X_AXIS / Y_AXIS flag determines whether we raster across the X_AXIS or Y_AXIS. Standard
6
- right-to-left rastering starting at the top edge on the left is the default. This would be
7
- in the upper left hand corner proceeding right, and stepping towards bottom each scanline.
8
-
9
- If the X_AXIS is set, the edge being used can be either TOP or BOTTOM. That flag means edge
10
- with X_AXIS rastering. However, in Y_AXIS rastering the start edge can either be right-edge
11
- or left-edge, for this the RIGHT and LEFT flags are used. However, the start point on either
12
- edge can be TOP or BOTTOM if we're starting on a RIGHT or LEFT edge. So those flags mark
13
- that. The same is true for RIGHT or LEFT on a TOP or BOTTOM edge.
14
-
15
- The TOP, RIGHT, LEFT, BOTTOM combined give you the starting corner.
16
-
17
- The rasters can either be BIDIRECTIONAL or UNIDIRECTIONAL meaning they raster on both swings
18
- or only on forward swing.
19
- """
20
-
21
-
22
- class RasterPlotter:
23
- def __init__(
24
- self,
25
- data,
26
- width,
27
- height,
28
- horizontal=True,
29
- start_minimum_y=True,
30
- start_minimum_x=True,
31
- bidirectional=True,
32
- use_integers=True,
33
- skip_pixel=0,
34
- overscan=0,
35
- offset_x=0,
36
- offset_y=0,
37
- step_x=1,
38
- step_y=1,
39
- filter=None,
40
- ):
41
- """
42
- Initialization for the Raster Plotter function. This should set all the needed parameters for plotting.
43
-
44
- @param data: pixel data accessed through data[x,y] parameters
45
- @param width: Width of the given data.
46
- @param height: Height of the given data.
47
- @param horizontal: Flags for how the pixel traversal should be conducted.
48
- @param start_minimum_y: Flags for how the pixel traversal should be conducted.
49
- @param start_minimum_x: Flags for how the pixel traversal should be conducted.
50
- @param bidirectional: Flags for how the pixel traversal should be conducted.
51
- @param skip_pixel: Skip pixel. If this value is the pixel value, we skip travel in that direction.
52
- @param use_integers: return integer values rather than floating point values.
53
- @param overscan: The extra amount of padding to add to the end scanline.
54
- @param offset_x: The offset in x of the rastering location. This will be added to x values returned in plot.
55
- @param offset_y: The offset in y of the rastering location. This will be added to y values returned in plot.
56
- @param step_x: The amount units per pixel.
57
- @param step_y: The amount scanline gap.
58
- @param filter: Pixel filter is called for each pixel to transform or alter it as needed. The actual
59
- implementation is agnostic regarding what data is provided. The filter is expected
60
- to convert the data[x,y] into some form which will be expressed by plot. Unless skipped as
61
- part of the skip pixel.
62
- """
63
- self.data = data
64
- self.width = width
65
- self.height = height
66
- self.horizontal = horizontal
67
- self.start_minimum_y = start_minimum_y
68
- self.start_minimum_x = start_minimum_x
69
- self.bidirectional = bidirectional
70
- self.use_integers = use_integers
71
- self.skip_pixel = skip_pixel
72
- if horizontal:
73
- self.overscan = round(overscan / float(step_x))
74
- else:
75
- self.overscan = round(overscan / float(step_y))
76
- self.offset_x = offset_x
77
- self.offset_y = offset_y
78
- self.step_x = step_x
79
- self.step_y = step_y
80
- self.filter = filter
81
- self.initial_x, self.initial_y = self.calculate_first_pixel()
82
- self.final_x, self.final_y = self.calculate_last_pixel()
83
-
84
- def px(self, x, y):
85
- """
86
- Returns the filtered pixel
87
-
88
- @param x:
89
- @param y:
90
- @return: Filtered Pixel
91
- """
92
- if 0 <= y < self.height and 0 <= x < self.width:
93
- if self.filter is None:
94
- return self.data[x, y]
95
- return self.filter(self.data[x, y])
96
- raise IndexError
97
-
98
- def leftmost_not_equal(self, y):
99
- """
100
- Determine the leftmost pixel that is not equal to the skip_pixel value.
101
-
102
- if all pixels skipped returns None
103
- """
104
- for x in range(0, self.width):
105
- pixel = self.px(x, y)
106
- if pixel != self.skip_pixel:
107
- return x
108
- return None
109
-
110
- def topmost_not_equal(self, x):
111
- """
112
- Determine the topmost pixel that is not equal to the skip_pixel value
113
-
114
- if all pixels skipped returns None
115
- """
116
- for y in range(0, self.height):
117
- pixel = self.px(x, y)
118
- if pixel != self.skip_pixel:
119
- return y
120
- return None
121
-
122
- def rightmost_not_equal(self, y):
123
- """
124
- Determine the rightmost pixel that is not equal to the skip_pixel value
125
-
126
- if all pixels skipped returns None
127
- """
128
- for x in range(self.width - 1, -1, -1):
129
- pixel = self.px(x, y)
130
- if pixel != self.skip_pixel:
131
- return x
132
- return None
133
-
134
- def bottommost_not_equal(self, x):
135
- """
136
- Determine the bottommost pixel that is not equal to the skip_pixel value
137
-
138
- if all pixels skipped returns None
139
- """
140
- for y in range(self.height - 1, -1, -1):
141
- pixel = self.px(x, y)
142
- if pixel != self.skip_pixel:
143
- return y
144
- return None
145
-
146
- def nextcolor_left(self, x, y, default=None):
147
- """
148
- Determine the next pixel change going left from the (x,y) point.
149
- If no next pixel is found default is returned.
150
- """
151
- if x <= -1:
152
- return default
153
- if x == 0:
154
- return -1
155
- if x == self.width:
156
- return self.width - 1
157
- if self.width < x:
158
- return self.width
159
-
160
- v = self.px(x, y)
161
- for ix in range(x, -1, -1):
162
- pixel = self.px(ix, y)
163
- if pixel != v:
164
- return ix
165
- return 0
166
-
167
- def nextcolor_top(self, x, y, default=None):
168
- """
169
- Determine the next pixel change going top from the (x,y) point.
170
- If no next pixel is found default is returned.
171
- """
172
- if y <= -1:
173
- return default
174
- if y == 0:
175
- return -1
176
- if y == self.height:
177
- return self.height - 1
178
- if self.height < y:
179
- return self.height
180
-
181
- v = self.px(x, y)
182
- for iy in range(y, -1, -1):
183
- pixel = self.px(x, iy)
184
- if pixel != v:
185
- return iy
186
- return 0
187
-
188
- def nextcolor_right(self, x, y, default=None):
189
- """
190
- Determine the next pixel change going right from the (x,y) point.
191
- If no next pixel is found default is returned.
192
- """
193
- if x < -1:
194
- return -1
195
- if x == -1:
196
- return 0
197
- if x == self.width - 1:
198
- return self.width
199
- if self.width <= x:
200
- return default
201
-
202
- v = self.px(x, y)
203
- for ix in range(x, self.width):
204
- pixel = self.px(ix, y)
205
- if pixel != v:
206
- return ix
207
- return self.width - 1
208
-
209
- def nextcolor_bottom(self, x, y, default=None):
210
- """
211
- Determine the next pixel change going bottom from the (x,y) point.
212
- If no next pixel is found default is returned.
213
- """
214
- if y < -1:
215
- return -1
216
- if y == -1:
217
- return 0
218
- if y == self.height - 1:
219
- return self.height
220
- if self.height <= y:
221
- return default
222
-
223
- v = self.px(x, y)
224
- for iy in range(y, self.height):
225
- pixel = self.px(x, iy)
226
- if pixel != v:
227
- return iy
228
- return self.height - 1
229
-
230
- def calculate_next_horizontal_pixel(self, y, dy=1, leftmost_pixel=True):
231
- """
232
- Find the horizontal extreme at the given y-scanline, stepping by dy in the target image.
233
- This can be done on either the leftmost (True) or rightmost (False).
234
-
235
- @param y: y-scanline
236
- @param dy: dy-step amount (usually should be -1 or 1)
237
- @param leftmost_pixel: find pixel from the left
238
- @return:
239
- """
240
- try:
241
- if leftmost_pixel:
242
- while True:
243
- x = self.leftmost_not_equal(y)
244
- if x is not None:
245
- break
246
- y += dy
247
- else:
248
- while True:
249
- x = self.rightmost_not_equal(y)
250
- if x is not None:
251
- break
252
- y += dy
253
- except IndexError:
254
- # Remaining image is blank
255
- return None, None
256
- return x, y
257
-
258
- def calculate_next_vertical_pixel(self, x, dx=1, topmost_pixel=True):
259
- """
260
- Find the vertical extreme at the given x-scanline, stepping by dx in the target image.
261
- This can be done on either the topmost (True) or bottommost (False).
262
-
263
- @param x: x-scanline
264
- @param dx: dx-step amount (usually should be -1 or 1)
265
- @param topmost_pixel: find the pixel from the top
266
- @return:
267
- """
268
- try:
269
- if topmost_pixel:
270
- while True:
271
- y = self.topmost_not_equal(x)
272
- if y is not None:
273
- break
274
- x += dx
275
- else:
276
- while True:
277
- # find that the bottommost pixel in that row.
278
- y = self.bottommost_not_equal(x)
279
-
280
- if y is not None:
281
- # This is a valid pixel.
282
- break
283
- # No pixel in that row was valid. Move to the next row.
284
- x += dx
285
- except IndexError:
286
- # Remaining image was blank, there are no more relevant pixels.
287
- return None, None
288
- return x, y
289
-
290
- def calculate_first_pixel(self):
291
- """
292
- Find the first non-skipped pixel in the rastering.
293
-
294
- This takes into account the traversal values of X_AXIS or Y_AXIS and BOTTOM and RIGHT
295
- The start edge and the start point.
296
-
297
- @return: x,y coordinates of first pixel.
298
- """
299
- if self.horizontal:
300
- y = 0 if self.start_minimum_y else self.height - 1
301
- dy = 1 if self.start_minimum_y else -1
302
- x, y = self.calculate_next_horizontal_pixel(y, dy, self.start_minimum_x)
303
- return x, y
304
- else:
305
- x = 0 if self.start_minimum_x else self.width - 1
306
- dx = 1 if self.start_minimum_x else -1
307
- x, y = self.calculate_next_vertical_pixel(x, dx, self.start_minimum_y)
308
- return x, y
309
-
310
- def calculate_last_pixel(self):
311
- """
312
- Find the last non-skipped pixel in the rastering.
313
-
314
- First and last scanlines start from the same side when scanline count is odd
315
-
316
- @return: x,y coordinates of last pixel.
317
- """
318
- if self.horizontal:
319
- y = self.height - 1 if self.start_minimum_y else 0
320
- dy = -1 if self.start_minimum_y else 1
321
- start_on_left = (
322
- self.start_minimum_x if self.width & 1 else not self.start_minimum_x
323
- )
324
- x, y = self.calculate_next_horizontal_pixel(y, dy, start_on_left)
325
- return x, y
326
- else:
327
- x = self.width - 1 if self.start_minimum_x else 0
328
- dx = -1 if self.start_minimum_x else 1
329
- start_on_top = (
330
- self.start_minimum_y if self.height & 1 else not self.start_minimum_y
331
- )
332
- x, y = self.calculate_next_vertical_pixel(x, dx, start_on_top)
333
- return x, y
334
-
335
- def initial_position(self):
336
- """
337
- Returns raw initial position for the relevant pixel within the data.
338
- @return: initial position within the data.
339
- """
340
- if self.use_integers:
341
- return int(round(self.initial_x)), int(round(self.initial_y))
342
- else:
343
- return self.initial_x, self.initial_y
344
-
345
- def initial_position_in_scene(self):
346
- """
347
- Returns the initial position for this within the scene. Taking into account start corner, and step size.
348
- @return: initial position within scene. The first plot location.
349
- """
350
- if self.initial_x is None: # image is blank.
351
- if self.use_integers:
352
- return int(round(self.offset_x)), int(round(self.offset_y))
353
- else:
354
- return self.offset_x, self.offset_y
355
- if self.use_integers:
356
- return (
357
- int(round(self.offset_x + self.initial_x * self.step_x)),
358
- int(round(self.offset_y + self.initial_y * self.step_y)),
359
- )
360
- else:
361
- return (
362
- self.offset_x + self.initial_x * self.step_x,
363
- self.offset_y + self.initial_y * self.step_y,
364
- )
365
-
366
- def final_position_in_scene(self):
367
- """
368
- Returns best guess of final position relative to the scene offset. Taking into account start corner, and parity
369
- of the width and height.
370
- @return:
371
- """
372
- if self.final_x is None: # image is blank.
373
- if self.use_integers:
374
- return int(round(self.offset_x)), int(round(self.offset_y))
375
- else:
376
- return self.offset_x, self.offset_y
377
- if self.use_integers:
378
- return (
379
- int(round(self.offset_x + self.final_x * self.step_x)),
380
- int(round(self.offset_y + self.final_y * self.step_y)),
381
- )
382
- else:
383
- return (
384
- self.offset_x + self.final_x * self.step_x,
385
- self.offset_y + self.final_y * self.step_y,
386
- )
387
-
388
- def plot(self):
389
- """
390
- Plot the values yielded by following the given raster plotter in the traversal defined.
391
- """
392
- offset_x = self.offset_x
393
- offset_y = self.offset_y
394
- step_x = self.step_x
395
- step_y = self.step_y
396
- if self.initial_x is None:
397
- # There is no image.
398
- return
399
- if self.use_integers:
400
- for x, y, on in self._plot_pixels():
401
- yield int(round(offset_x + step_x * x)), int(
402
- round(offset_y + y * step_y)
403
- ), on
404
- else:
405
- for x, y, on in self._plot_pixels():
406
- yield offset_x + step_x * x, offset_y + y * step_y, on
407
-
408
- def _plot_pixels(self):
409
- if self.horizontal:
410
- yield from self._plot_horizontal()
411
- else:
412
- yield from self._plot_vertical()
413
-
414
- def _plot_vertical(self):
415
- """
416
- This code is for vertical rastering.
417
-
418
- @return:
419
- """
420
- width = self.width
421
- unidirectional = not self.bidirectional
422
- skip_pixel = self.skip_pixel
423
-
424
- x, y = self.initial_position()
425
- dx = 1 if self.start_minimum_x else -1
426
- dy = 1 if self.start_minimum_y else -1
427
-
428
- yield x, y, 0
429
- while 0 <= x < width:
430
- lower_bound = self.topmost_not_equal(x)
431
- if lower_bound is None:
432
- x += dx
433
- yield x, y, 0
434
- continue
435
- upper_bound = self.bottommost_not_equal(x)
436
- traveling_bottom = self.start_minimum_y if unidirectional else dy >= 0
437
- next_traveling_bottom = self.start_minimum_y if unidirectional else dy <= 0
438
-
439
- next_x, next_y = self.calculate_next_vertical_pixel(
440
- x + dx, dx, topmost_pixel=next_traveling_bottom
441
- )
442
- if next_y is not None:
443
- # If we have a next scanline, we must end after the last pixel of that scanline too.
444
- upper_bound = max(next_y, upper_bound) + self.overscan
445
- lower_bound = min(next_y, lower_bound) - self.overscan
446
-
447
- if traveling_bottom:
448
- while y < upper_bound:
449
- try:
450
- pixel = self.px(x, y)
451
- except IndexError:
452
- pixel = 0
453
- y = self.nextcolor_bottom(x, y, upper_bound)
454
- y = min(y, upper_bound)
455
- if pixel == skip_pixel:
456
- yield x, y, 0
457
- else:
458
- yield x, y, pixel
459
- else:
460
- while lower_bound < y:
461
- try:
462
- pixel = self.px(x, y)
463
- except IndexError:
464
- pixel = 0
465
- y = self.nextcolor_top(x, y, lower_bound)
466
- y = max(y, lower_bound)
467
- if pixel == skip_pixel:
468
- yield x, y, 0
469
- else:
470
- yield x, y, pixel
471
-
472
- if next_y is None:
473
- # remaining image is blank, we stop right here.
474
- return
475
- yield next_x, y, 0
476
- if y != next_y:
477
- yield next_x, next_y, 0
478
- x = next_x
479
- y = next_y
480
- dy = -dy
481
-
482
- def _plot_horizontal(self):
483
- """
484
- This code is horizontal rastering.
485
-
486
- @return:
487
- """
488
- height = self.height
489
- unidirectional = not self.bidirectional
490
- skip_pixel = self.skip_pixel
491
-
492
- x, y = self.initial_position()
493
- dx = 1 if self.start_minimum_x else -1
494
- dy = 1 if self.start_minimum_y else -1
495
- yield x, y, 0
496
- while 0 <= y < height:
497
- lower_bound = self.leftmost_not_equal(y)
498
- if lower_bound is None:
499
- y += dy
500
- yield x, y, 0
501
- continue
502
- upper_bound = self.rightmost_not_equal(y)
503
- traveling_right = self.start_minimum_x if unidirectional else dx >= 0
504
- next_traveling_right = self.start_minimum_x if unidirectional else dx <= 0
505
-
506
- next_x, next_y = self.calculate_next_horizontal_pixel(
507
- y + dy, dy, leftmost_pixel=next_traveling_right
508
- )
509
- if next_x is not None:
510
- # If we have a next scanline, we must end after the last pixel of that scanline too.
511
- upper_bound = max(next_x, upper_bound) + self.overscan
512
- lower_bound = min(next_x, lower_bound) - self.overscan
513
-
514
- if traveling_right:
515
- while x < upper_bound:
516
- try:
517
- pixel = self.px(x, y)
518
- except IndexError:
519
- pixel = 0
520
- x = self.nextcolor_right(x, y, upper_bound)
521
- x = min(x, upper_bound)
522
- if pixel == skip_pixel:
523
- yield x, y, 0
524
- else:
525
- yield x, y, pixel
526
- else:
527
- while lower_bound < x:
528
- try:
529
- pixel = self.px(x, y)
530
- except IndexError:
531
- pixel = 0
532
- x = self.nextcolor_left(x, y, lower_bound)
533
- x = max(x, lower_bound)
534
- if pixel == skip_pixel:
535
- yield x, y, 0
536
- else:
537
- yield x, y, pixel
538
-
539
- if next_y is None:
540
- # remaining image is blank, we stop right here.
541
- return
542
- yield x, next_y, 0
543
- if x != next_x:
544
- yield next_x, next_y, 0
545
- x = next_x
546
- y = next_y
547
- dx = -dx
1
+ """
2
+ The RasterPlotter is a plotter that maps particular raster pixels to directional and raster
3
+ methods. This class should be expanded to cover most raster situations.
4
+
5
+ The X_AXIS / Y_AXIS flag determines whether we raster across the X_AXIS or Y_AXIS. Standard
6
+ right-to-left rastering starting at the top edge on the left is the default. This would be
7
+ in the upper left hand corner proceeding right, and stepping towards bottom each scanline.
8
+
9
+ If the X_AXIS is set, the edge being used can be either TOP or BOTTOM. That flag means edge
10
+ with X_AXIS rastering. However, in Y_AXIS rastering the start edge can either be right-edge
11
+ or left-edge, for this the RIGHT and LEFT flags are used. However, the start point on either
12
+ edge can be TOP or BOTTOM if we're starting on a RIGHT or LEFT edge. So those flags mark
13
+ that. The same is true for RIGHT or LEFT on a TOP or BOTTOM edge.
14
+
15
+ The TOP, RIGHT, LEFT, BOTTOM combined give you the starting corner.
16
+
17
+ The rasters can either be BIDIRECTIONAL or UNIDIRECTIONAL meaning they raster on both swings
18
+ or only on forward swing.
19
+ """
20
+
21
+ import numpy as np
22
+ from math import sqrt
23
+ from time import perf_counter, sleep
24
+ from meerk40t.constants import (
25
+ RASTER_T2B,
26
+ RASTER_B2T,
27
+ RASTER_R2L,
28
+ RASTER_L2R,
29
+ RASTER_HATCH,
30
+ RASTER_GREEDY_H,
31
+ RASTER_GREEDY_V,
32
+ RASTER_CROSSOVER,
33
+ RASTER_SPIRAL,
34
+ )
35
+
36
+
37
+ class RasterPlotter:
38
+ def __init__(
39
+ self,
40
+ data,
41
+ width,
42
+ height,
43
+ direction=0,
44
+ horizontal=True,
45
+ start_minimum_y=True,
46
+ start_minimum_x=True,
47
+ bidirectional=True,
48
+ use_integers=True,
49
+ skip_pixel=0,
50
+ overscan=0,
51
+ offset_x=0,
52
+ offset_y=0,
53
+ step_x=1,
54
+ step_y=1,
55
+ filter=None,
56
+ laserspot=0,
57
+ special=None,
58
+ ):
59
+ """
60
+ Initialization for the Raster Plotter function. This should set all the needed parameters for plotting.
61
+
62
+ @param data: pixel data accessed through data[x,y] parameters
63
+ @param width: Width of the given data.
64
+ @param height: Height of the given data.
65
+ @param horizontal: Flags for how the pixel traversal should be conducted.
66
+ @param start_minimum_y: Flags for how the pixel traversal should be conducted.
67
+ @param start_minimum_x: Flags for how the pixel traversal should be conducted.
68
+ @param bidirectional: Flags for how the pixel traversal should be conducted.
69
+ @param skip_pixel: Skip pixel. If this value is the pixel value, we skip travel in that direction.
70
+ @param use_integers: return integer values rather than floating point values.
71
+ @param overscan: The extra amount of padding to add to the end scanline.
72
+ @param offset_x: The offset in x of the rastering location. This will be added to x values returned in plot.
73
+ @param offset_y: The offset in y of the rastering location. This will be added to y values returned in plot.
74
+ @param step_x: The amount units per pixel.
75
+ @param step_y: The amount scanline gap.
76
+ @param filter: Pixel filter is called for each pixel to transform or alter it as needed. The actual
77
+ implementation is agnostic regarding what data is provided. The filter is expected
78
+ to convert the data[x,y] into some form which will be expressed by plot. Unless skipped as
79
+ part of the skip pixel.
80
+ @param direction: 0 top 2 bottom, 1 bottom 2 top, 2 right 2 left, 3 left 2 right, 4 greedy path optimisation, 5 crossover
81
+ a) greedy will keep the main direction, a bit time intensive for larger images
82
+ b) crossover draws first all majority lines, then all majority columns
83
+ (to be decided by looping over the matrix and looking at the amount
84
+ of black pixels on the same line / on the same column), timewise okayish
85
+ @param laserspot: the laserbeam diameter in pixels (low dpi = irrelevant, high dpi very relevant)
86
+ @param special: a dict of special treatment instructions for the different algorithms
87
+ """
88
+ # Don't try to plot from two different sources at the same time...
89
+ self._locked = False
90
+
91
+ if special is None:
92
+ special = {}
93
+ self.debug_level = 0 # 0 Nothing, 1 file creation, 2 file + summary, 3 file + summary + details
94
+ self.data = data
95
+ self.width = width
96
+ self.height = height
97
+ self.direction = direction
98
+ self._cache = None
99
+ parameters = {
100
+ # Provide an override for the minimumx / minimumy / horizontal / bidirectional
101
+ RASTER_T2B: (None, True, True, None), # top to bottom
102
+ RASTER_B2T: (None, False, True, None), # bottom to top
103
+ RASTER_R2L: (False, None, False, None), # right to left
104
+ RASTER_L2R: (True, None, False, None), # left to right
105
+ RASTER_HATCH: (None, None, None, None), # crossraster (one of the two)
106
+ RASTER_GREEDY_H: (None, None, None, True), # greedy neighbour horizontal
107
+ RASTER_GREEDY_V: (None, None, None, True), # greedy neighbour
108
+ RASTER_CROSSOVER: (None, None, None, True), # true crossover
109
+ }
110
+ def_x, def_y, def_hor, def_bidir = parameters.get(direction, (None, None, None, None))
111
+ self.start_minimum_x = start_minimum_x if def_x is None else def_x
112
+ self.start_minimum_y = start_minimum_y if def_y is None else def_y
113
+ self.horizontal = horizontal if def_hor is None else def_hor
114
+ self.bidirectional = bidirectional if def_bidir is None else def_bidir
115
+
116
+ self.use_integers = use_integers
117
+ self.skip_pixel = skip_pixel
118
+ # laserspot = width in pixels, so the surrounding logic needs to calculate it
119
+ # We consider an overlap only in the enclosed square of the circle
120
+ # and calculate the overlap in pixels to the left / to the right
121
+ self.overlap = int(laserspot / sqrt(2) / 2)
122
+ # self.overlap = 1
123
+ self.special = dict(special) # Copy it so it wont be changed
124
+ if horizontal:
125
+ self.overscan = round(overscan / float(step_x))
126
+ else:
127
+ self.overscan = round(overscan / float(step_y))
128
+ self.offset_x = offset_x
129
+ self.offset_y = offset_y
130
+ self.step_x = step_x
131
+ self.step_y = step_y
132
+ self.filter = filter
133
+ self.initial_x, self.initial_y = self.calculate_first_pixel()
134
+ self.final_x, self.final_y = self.calculate_last_pixel()
135
+ self._distance_travel = 0
136
+ self._distance_burn = 0
137
+
138
+ def __repr__(self):
139
+ methods = (
140
+ "Top2Bottom",
141
+ "Bottom2Top",
142
+ "Right2Left",
143
+ "Left2Right",
144
+ "Hatch",
145
+ "Greedy Neighbor Hor",
146
+ "Greedy Neighbor Ver",
147
+ "Crossover",
148
+ "Spiral",
149
+ )
150
+ if 0 <= self.direction < len(methods):
151
+ s_meth = f"Rasterplotter ({self.width}x{self.height}): {methods[self.direction]} ({self.direction})"
152
+ else:
153
+ s_meth = f"Rasterplotter ({self.width}x{self.height}): Unknown {self.direction}"
154
+ s_direc = 'Bidirectional' if self.bidirectional else 'Unidirectional'
155
+ s_axis = 'horizontal' if self.horizontal else 'vertical'
156
+ s_ystart = 'top' if self.start_minimum_y else 'bottom'
157
+ s_xstart = 'left' if self.start_minimum_x else 'right'
158
+ return f"{s_meth}, {s_direc} {s_axis} plot starting at {s_ystart}-{s_xstart}"
159
+
160
+ def reset(self):
161
+ self._cache = None
162
+
163
+ def px(self, x, y):
164
+ """
165
+ Returns the filtered pixel
166
+
167
+ @param x:
168
+ @param y:
169
+ @return: Filtered Pixel
170
+ """
171
+ if 0 <= y < self.height and 0 <= x < self.width:
172
+ if self.filter is None:
173
+ return self.data[x, y]
174
+ return self.filter(self.data[x, y])
175
+ raise IndexError
176
+
177
+ def leftmost_not_equal(self, y):
178
+ """
179
+ Determine the leftmost pixel that is not equal to the skip_pixel value.
180
+
181
+ if all pixels skipped returns None
182
+ """
183
+ for x in range(0, self.width):
184
+ pixel = self.px(x, y)
185
+ if pixel != self.skip_pixel:
186
+ return x
187
+ return None
188
+
189
+ def topmost_not_equal(self, x):
190
+ """
191
+ Determine the topmost pixel that is not equal to the skip_pixel value
192
+
193
+ if all pixels skipped returns None
194
+ """
195
+ for y in range(0, self.height):
196
+ pixel = self.px(x, y)
197
+ if pixel != self.skip_pixel:
198
+ return y
199
+ return None
200
+
201
+ def rightmost_not_equal(self, y):
202
+ """
203
+ Determine the rightmost pixel that is not equal to the skip_pixel value
204
+
205
+ if all pixels skipped returns None
206
+ """
207
+ for x in range(self.width - 1, -1, -1):
208
+ pixel = self.px(x, y)
209
+ if pixel != self.skip_pixel:
210
+ return x
211
+ return None
212
+
213
+ def bottommost_not_equal(self, x):
214
+ """
215
+ Determine the bottommost pixel that is not equal to the skip_pixel value
216
+
217
+ if all pixels skipped returns None
218
+ """
219
+ for y in range(self.height - 1, -1, -1):
220
+ pixel = self.px(x, y)
221
+ if pixel != self.skip_pixel:
222
+ return y
223
+ return None
224
+
225
+ @property
226
+ def distance_travel(self):
227
+ return self._distance_travel
228
+
229
+ @property
230
+ def distance_burn(self):
231
+ return self._distance_burn
232
+
233
+ def nextcolor_left(self, x, y, default=None):
234
+ """
235
+ Determine the next pixel change going left from the (x,y) point.
236
+ If no next pixel is found default is returned.
237
+ """
238
+ if x <= -1:
239
+ return default
240
+ if x == 0:
241
+ return -1
242
+ if x == self.width:
243
+ return self.width - 1
244
+ if self.width < x:
245
+ return self.width
246
+
247
+ v = self.px(x, y)
248
+ for ix in range(x, -1, -1):
249
+ pixel = self.px(ix, y)
250
+ if pixel != v:
251
+ return ix
252
+ return 0
253
+
254
+ def nextcolor_top(self, x, y, default=None):
255
+ """
256
+ Determine the next pixel change going top from the (x,y) point.
257
+ If no next pixel is found default is returned.
258
+ """
259
+ if y <= -1:
260
+ return default
261
+ if y == 0:
262
+ return -1
263
+ if y == self.height:
264
+ return self.height - 1
265
+ if self.height < y:
266
+ return self.height
267
+
268
+ v = self.px(x, y)
269
+ for iy in range(y, -1, -1):
270
+ pixel = self.px(x, iy)
271
+ if pixel != v:
272
+ return iy
273
+ return 0
274
+
275
+ def nextcolor_right(self, x, y, default=None):
276
+ """
277
+ Determine the next pixel change going right from the (x,y) point.
278
+ If no next pixel is found default is returned.
279
+ """
280
+ if x < -1:
281
+ return -1
282
+ if x == -1:
283
+ return 0
284
+ if x == self.width - 1:
285
+ return self.width
286
+ if self.width <= x:
287
+ return default
288
+
289
+ v = self.px(x, y)
290
+ for ix in range(x, self.width):
291
+ pixel = self.px(ix, y)
292
+ if pixel != v:
293
+ return ix
294
+ return self.width - 1
295
+
296
+ def nextcolor_bottom(self, x, y, default=None):
297
+ """
298
+ Determine the next pixel change going bottom from the (x,y) point.
299
+ If no next pixel is found default is returned.
300
+ """
301
+ if y < -1:
302
+ return -1
303
+ if y == -1:
304
+ return 0
305
+ if y == self.height - 1:
306
+ return self.height
307
+ if self.height <= y:
308
+ return default
309
+
310
+ v = self.px(x, y)
311
+ for iy in range(y, self.height):
312
+ pixel = self.px(x, iy)
313
+ if pixel != v:
314
+ return iy
315
+ return self.height - 1
316
+
317
+ def calculate_next_horizontal_pixel(self, y, dy=1, leftmost_pixel=True):
318
+ """
319
+ Find the horizontal extreme at the given y-scanline, stepping by dy in the target image.
320
+ This can be done on either the leftmost (True) or rightmost (False).
321
+
322
+ @param y: y-scanline
323
+ @param dy: dy-step amount (usually should be -1 or 1)
324
+ @param leftmost_pixel: find pixel from the left
325
+ @return:
326
+ """
327
+ try:
328
+ if leftmost_pixel:
329
+ while True:
330
+ x = self.leftmost_not_equal(y)
331
+ if x is not None:
332
+ break
333
+ y += dy
334
+ else:
335
+ while True:
336
+ x = self.rightmost_not_equal(y)
337
+ if x is not None:
338
+ break
339
+ y += dy
340
+ except IndexError:
341
+ # Remaining image is blank
342
+ return None, None
343
+ return x, y
344
+
345
+ def calculate_next_vertical_pixel(self, x, dx=1, topmost_pixel=True):
346
+ """
347
+ Find the vertical extreme at the given x-scanline, stepping by dx in the target image.
348
+ This can be done on either the topmost (True) or bottommost (False).
349
+
350
+ @param x: x-scanline
351
+ @param dx: dx-step amount (usually should be -1 or 1)
352
+ @param topmost_pixel: find the pixel from the top
353
+ @return:
354
+ """
355
+ try:
356
+ if topmost_pixel:
357
+ while True:
358
+ y = self.topmost_not_equal(x)
359
+ if y is not None:
360
+ break
361
+ x += dx
362
+ else:
363
+ while True:
364
+ # find that the bottommost pixel in that row.
365
+ y = self.bottommost_not_equal(x)
366
+
367
+ if y is not None:
368
+ # This is a valid pixel.
369
+ break
370
+ # No pixel in that row was valid. Move to the next row.
371
+ x += dx
372
+ except IndexError:
373
+ # Remaining image was blank, there are no more relevant pixels.
374
+ return None, None
375
+ return x, y
376
+
377
+ def calculate_first_pixel(self):
378
+ """
379
+ Find the first non-skipped pixel in the rastering.
380
+
381
+ This takes into account the traversal values of X_AXIS or Y_AXIS and BOTTOM and RIGHT
382
+ The start edge and the start point.
383
+
384
+ @return: x,y coordinates of first pixel.
385
+ """
386
+ if self.horizontal:
387
+ y = 0 if self.start_minimum_y else self.height - 1
388
+ dy = 1 if self.start_minimum_y else -1
389
+ x, y = self.calculate_next_horizontal_pixel(y, dy, self.start_minimum_x)
390
+ return x, y
391
+ else:
392
+ x = 0 if self.start_minimum_x else self.width - 1
393
+ dx = 1 if self.start_minimum_x else -1
394
+ x, y = self.calculate_next_vertical_pixel(x, dx, self.start_minimum_y)
395
+ return x, y
396
+
397
+ def calculate_last_pixel(self):
398
+ """
399
+ Find the last non-skipped pixel in the rastering.
400
+
401
+ First and last scanlines start from the same side when scanline count is odd
402
+
403
+ @return: x,y coordinates of last pixel.
404
+ """
405
+ if self.horizontal:
406
+ y = self.height - 1 if self.start_minimum_y else 0
407
+ dy = -1 if self.start_minimum_y else 1
408
+ start_on_left = (
409
+ self.start_minimum_x if self.width & 1 else not self.start_minimum_x
410
+ )
411
+ x, y = self.calculate_next_horizontal_pixel(y, dy, start_on_left)
412
+ return x, y
413
+ else:
414
+ x = self.width - 1 if self.start_minimum_x else 0
415
+ dx = -1 if self.start_minimum_x else 1
416
+ start_on_top = (
417
+ self.start_minimum_y if self.height & 1 else not self.start_minimum_y
418
+ )
419
+ x, y = self.calculate_next_vertical_pixel(x, dx, start_on_top)
420
+ return x, y
421
+
422
+ def initial_position(self):
423
+ """
424
+ Returns raw initial position for the relevant pixel within the data.
425
+ @return: initial position within the data.
426
+ """
427
+ if self.use_integers:
428
+ return int(round(self.initial_x)), int(round(self.initial_y))
429
+ else:
430
+ return self.initial_x, self.initial_y
431
+
432
+ def initial_position_in_scene(self):
433
+ """
434
+ Returns the initial position for this within the scene. Taking into account start corner, and step size.
435
+ @return: initial position within scene. The first plot location.
436
+ """
437
+ if self.initial_x is None: # image is blank.
438
+ if self.use_integers:
439
+ return int(round(self.offset_x)), int(round(self.offset_y))
440
+ else:
441
+ return self.offset_x, self.offset_y
442
+ if self.use_integers:
443
+ return (
444
+ int(round(self.offset_x + self.initial_x * self.step_x)),
445
+ int(round(self.offset_y + self.initial_y * self.step_y)),
446
+ )
447
+ else:
448
+ return (
449
+ self.offset_x + self.initial_x * self.step_x,
450
+ self.offset_y + self.initial_y * self.step_y,
451
+ )
452
+
453
+ def final_position_in_scene(self):
454
+ """
455
+ Returns best guess of final position relative to the scene offset. Taking into account start corner, and parity
456
+ of the width and height.
457
+ @return:
458
+ """
459
+ if self.final_x is None: # image is blank.
460
+ if self.use_integers:
461
+ return int(round(self.offset_x)), int(round(self.offset_y))
462
+ else:
463
+ return self.offset_x, self.offset_y
464
+ if self.use_integers:
465
+ return (
466
+ int(round(self.offset_x + self.final_x * self.step_x)),
467
+ int(round(self.offset_y + self.final_y * self.step_y)),
468
+ )
469
+ else:
470
+ return (
471
+ self.offset_x + self.final_x * self.step_x,
472
+ self.offset_y + self.final_y * self.step_y,
473
+ )
474
+
475
+ def plot(self):
476
+ """
477
+ Plot the values yielded by following the given raster plotter in the traversal defined.
478
+ """
479
+ while self._locked:
480
+ sleep(0.1)
481
+ self._locked = True
482
+ offset_x = self.offset_x
483
+ offset_y = self.offset_y
484
+ step_x = self.step_x
485
+ step_y = self.step_y
486
+ self._distance_travel = 0
487
+ self._distance_burn = 0
488
+
489
+ if self.initial_x is None:
490
+ # There is no image.
491
+ return
492
+ # Debug code....
493
+ methods = (
494
+ "Top2Bottom",
495
+ "Bottom2Top",
496
+ "Right2Left",
497
+ "Left2Right",
498
+ "Hatch",
499
+ "Greedy Neighbor Hor",
500
+ "Greedy Neighbor Ver",
501
+ "Crossover",
502
+ "Spiral",
503
+ )
504
+ testmethods = (
505
+ "Test: Horizontal Rectangle",
506
+ "Test: Vertical Rectangle",
507
+ "Test: Horizontal Snake",
508
+ "Test: Vertical Snake",
509
+ "Test: Spiral",
510
+ )
511
+ if self._cache is None:
512
+ if self.debug_level > 0:
513
+ try:
514
+ if self.direction >= 0:
515
+ m = methods[self.direction]
516
+ else:
517
+ m = testmethods[abs(self.direction) - 1]
518
+ s_meth = f"Method: {m} ({self.direction})"
519
+ except IndexError:
520
+ s_meth = f"Method: Unknown {self.direction}"
521
+ print (s_meth)
522
+ data = list(self._plot_pixels())
523
+ from platform import system
524
+ defaultdir = "c:\\temp\\" if system() == "Windows" else ""
525
+ has_duplicates = 0
526
+ tstamp = int(perf_counter() * 100)
527
+ with open(f"{defaultdir}plot_{tstamp}.txt", mode="w") as f:
528
+ f.write(f"0.9.7\n{s_meth}\n{'Bidirectional' if self.bidirectional else 'Unidirectional'} {'horizontal' if self.horizontal else 'vertical'} plot starting at {'top' if self.start_minimum_y else 'bottom'}-{'left' if self.start_minimum_x else 'right'}\n")
529
+ f.write(f"Overscan: {self.overscan:.2f}, Stepx={step_x:.2f}, Stepy={step_y:.2f}\n")
530
+ f.write(f"Image dimensions: {self.width}x{self.height}\n")
531
+ f.write(f"Startpoint: {self.initial_x}, {self.initial_y}\n")
532
+ f.write(f"Overlapping pixels to any side: {self.overlap}\n")
533
+ if self.special:
534
+ f.write(f"Special instructions:\n")
535
+ for key, value in self.special.items():
536
+ f.write(f" {key} = {value}\n")
537
+ f.write("----------------------------------------------------------------------\n")
538
+ test_dict = {}
539
+ lastx = self.initial_x
540
+ lasty = self.initial_y
541
+ failed = False
542
+ for lineno, (x, y, on) in enumerate(data, start=1):
543
+ if lastx is not None:
544
+ dx = x - lastx
545
+ dy = y - lasty
546
+ if dx != 0 and dy != 0: # and abs(dx) != abs(dy):
547
+ f.write (f"You f**ed up! No zigzag movement from line {lineno - 1} to {lineno}: {lastx}, {lasty} -> {x}, {y}\n")
548
+ print (f"You f**ed up! No zigzag movement from line {lineno - 1} to {lineno}: {lastx}, {lasty} -> {x}, {y}")
549
+ failed = True
550
+ lastx = x
551
+ lasty = y
552
+ if not failed:
553
+ f.write("Good news, no zig-zag movements identified!\n")
554
+ f.write("----------------------------------------------------------------------\n")
555
+ for lineno, (x, y, on) in enumerate(data, start=1):
556
+ if x is None or y is None:
557
+ continue
558
+ key = f"{x} - {y}"
559
+ if key in test_dict:
560
+ f.write (f"Duplicate coordinates in list at ({x}, {y})! 1st: #{test_dict[key][0]}, on={test_dict[key][1]}, 2nd: #{lineno}, on={on}\n")
561
+ has_duplicates += 1
562
+ else:
563
+ test_dict[key] = (lineno, on)
564
+ if has_duplicates:
565
+ f.write("----------------------------------------------------------------------\n")
566
+ for lineno, (x, y, on) in enumerate(data, start=1):
567
+ f.write(f"{lineno}: {x}, {y}, {on}\n")
568
+ if has_duplicates:
569
+ print(f"Attention: the generated plot has {has_duplicates} duplicate coordinate values!")
570
+ print(f"{'Bidirectional' if self.bidirectional else 'Unidirectional'} {'horizontal' if self.horizontal else 'vertical'} plot starting at {'top' if self.start_minimum_y else 'bottom'}-{'left' if self.start_minimum_x else 'right'}")
571
+ print(f"Overscan: {self.overscan:.2f}, Stepx={step_x:.2f}, Stepy={step_y:.2f}")
572
+ print(f"Image dimensions: {self.width}x{self.height}")
573
+ else:
574
+ data = list(self._plot_pixels())
575
+ self._cache = data
576
+ else:
577
+ data = self._cache
578
+ last_x = offset_x
579
+ last_y = offset_y
580
+ if self.use_integers:
581
+ for x, y, on in data:
582
+ if x is None or y is None:
583
+ # Passthrough
584
+ yield x, y, on
585
+ else:
586
+ nx = int(round(offset_x + step_x * x))
587
+ ny = int(round(offset_y + y * step_y))
588
+ self._distance_burn += 0 if on == 0 else sqrt( (nx - last_x) * (nx-last_x) + (ny - last_y) * (ny - last_y) )
589
+ self._distance_travel += 0 if on != 0 else sqrt( (nx - last_x) * (nx-last_x) + (ny - last_y) * (ny - last_y) )
590
+ yield nx, ny, on
591
+ last_x = nx
592
+ last_y = ny
593
+ else:
594
+ for x, y, on in data:
595
+ if x is None or y is None:
596
+ # Passthrough
597
+ yield x, y, on
598
+ else:
599
+ nx = round(offset_x + step_x * x)
600
+ ny = round(offset_y + y * step_y)
601
+ self._distance_burn += 0 if on == 0 else sqrt( (nx - last_x) * (nx-last_x) + (ny - last_y) * (ny - last_y) )
602
+ self._distance_travel += 0 if on != 0 else sqrt( (nx - last_x) * (nx-last_x) + (ny - last_y) * (ny - last_y) )
603
+ yield offset_x + step_x * x, offset_y + y * step_y, on
604
+ last_x = nx
605
+ last_y = ny
606
+ self._locked = False
607
+
608
+ def _plot_pixels(self):
609
+ legacy = self.special.get("legacy", False)
610
+ if self.direction in (RASTER_GREEDY_H, RASTER_GREEDY_V):
611
+ yield from self._plot_greedy_neighbour(horizontal=self.horizontal)
612
+ elif self.direction == RASTER_CROSSOVER:
613
+ yield from self._plot_crossover()
614
+ elif self.direction == RASTER_SPIRAL:
615
+ yield from self._plot_spiral()
616
+ # elif self.direction < 0:
617
+ # yield from self.testpattern_generator()
618
+ elif self.horizontal:
619
+ if legacy:
620
+ yield from self._legacy_plot_horizontal()
621
+ else:
622
+ yield from self._plot_horizontal()
623
+ else:
624
+ if legacy:
625
+ yield from self._legacy_plot_vertical()
626
+ else:
627
+ yield from self._plot_vertical()
628
+ # yield from self._plot_vertical()
629
+
630
+ def _debug_data(self, force=False):
631
+ if self.debug_level < 3 and not force:
632
+ return
633
+ BLANK = 255
634
+ for y in range(self.height):
635
+ msg:str = f"{y:3d}: "
636
+ for x in range(self.width):
637
+ msg += "." if self.data[x, y] == BLANK else "X"
638
+ print (msg)
639
+
640
+ def _get_pixel_chains(self, xy:int, is_x : bool) -> list:
641
+ last_pixel = None
642
+ segments = []
643
+ upper = self.width if is_x else self.height
644
+ for idx in range(upper):
645
+ pixel = self.px(idx, xy) if is_x else self.px(xy, idx)
646
+ on = 0 if pixel == self.skip_pixel else pixel
647
+ if on:
648
+ if on == last_pixel:
649
+ segments[-1][1] = idx
650
+ else:
651
+ segments.append ([idx, idx, on])
652
+ last_pixel = on
653
+ return segments
654
+
655
+ def _consume_pixel_chains(self, segments:list, xy:int, is_x : bool):
656
+ BLANK = 255
657
+ # for x in range(5):
658
+ # msg1 = f"{x}: "
659
+ # msg2 = ""
660
+ # for y in range(5):
661
+ # msg1 += "." if self.data[x, y] == BLANK else "X"
662
+ # msg2 += f" {self.data[x, y]}"
663
+ # print (msg1, msg2)
664
+
665
+ for seg in segments:
666
+ c_start = seg[0]
667
+ c_end = seg[1]
668
+ for idx in range(c_start, c_end + 1):
669
+ if is_x:
670
+ px = idx
671
+ py = xy
672
+ else:
673
+ px = xy
674
+ py = idx
675
+ for x_idx in range(-self.overlap, self.overlap + 1):
676
+ for y_idx in range(-self.overlap, self.overlap + 1):
677
+ nx = px + x_idx
678
+ ny = py + y_idx
679
+ if nx < 0 or nx >= self.width or ny < 0 or ny >= self.height:
680
+ continue
681
+ self.data[nx, ny] = BLANK
682
+ self._debug_data()
683
+
684
+ def _plot_vertical(self):
685
+ """
686
+ This code is for vertical rastering.
687
+ We are looking first for all consecutive pixel chains with the same pixel value
688
+ Then we loop through the segments and yield the 'end edge' of the 'last' pixel
689
+ 'end edge' and 'last' are dependent on the sweep direction.
690
+ There is one peculiarity though that is required for K40 lasers:
691
+ a) We may only move from one yielded (x,y,on) tuple in a pure horizontal or pure
692
+ vertical fashion (we could as well go perfectly diagonal but we are not using
693
+ this feature). So at the end of one sweepline we need to change to the
694
+ next scanline by going directly up/down/left/right and then move to the first
695
+ relevant pixel.
696
+ b) we need to take care that we are not landing on the same pixel twice. So we move
697
+ outside the new chain to avoid this
698
+ """
699
+ unidirectional = not self.bidirectional
700
+
701
+ dx = 1 if self.start_minimum_x else -1
702
+ dy = 1 if self.start_minimum_y else -1
703
+ lower = min(self.initial_x, self.final_x)
704
+ upper = max(self.initial_x, self.final_x)
705
+ last_x = self.initial_x
706
+ last_y = self.initial_y
707
+ x = lower if self.start_minimum_x else upper
708
+ first = True
709
+ while lower <= x <= upper:
710
+ segments = self._get_pixel_chains(x, False)
711
+ self._consume_pixel_chains(segments, x, False)
712
+ if segments:
713
+ if dy > 0:
714
+ # from top to bottom
715
+ idx = 0
716
+ start = 0
717
+ end = 1
718
+ edge_start = -0.5
719
+ edge_end = 0.5
720
+ else:
721
+ idx = len(segments) - 1
722
+ end = 0
723
+ start = 1
724
+ edge_start = 0.5
725
+ edge_end = -0.5
726
+ # Goto next column, but make sure we end up outside our chain
727
+ # We consider as well the overscan value
728
+ overscan_top = 0 if dy >= 0 else self.overscan
729
+ overscan_bottom = 0 if dy <= 0 else self.overscan
730
+ if not first and (segments[0][0] - overscan_top <= last_y <= segments[-1][1] + overscan_bottom):
731
+ # inside the chain!
732
+ # So lets move a bit to the side
733
+ if dy > 0:
734
+ if self.bidirectional:
735
+ # Previous was sweep from right to left, so we go beyond first point
736
+ last_y = segments[0][0] - overscan_top - 1
737
+ else:
738
+ # We go beyond last point
739
+ last_y = segments[-1][1] + overscan_bottom + 1
740
+ else:
741
+ # Previous was sweep from left to right, so we go beyond last point
742
+ last_y = segments[-1][1] + overscan_bottom + 1
743
+ last_x = x - dx
744
+ yield last_x, last_y, 0
745
+ last_x = x
746
+ yield last_x, last_y, 0
747
+ while 0 <= idx < len(segments):
748
+ sy = segments[idx][start] + edge_start
749
+ ey = segments[idx][end] + edge_end
750
+ on = segments[idx][2]
751
+ if last_y != sy:
752
+ yield last_x, sy, 0
753
+ last_y = ey
754
+ yield last_x, last_y, on
755
+ idx += dy
756
+ if self.overscan:
757
+ last_y += dy * self.overscan
758
+ yield last_x, last_y, 0
759
+ if not unidirectional:
760
+ dy = -dy
761
+ first = False
762
+ else:
763
+ # Just climb the line, and don't change directions
764
+ last_x = x
765
+ yield last_x, last_y, 0
766
+
767
+ x += dx
768
+
769
+ def _plot_horizontal(self):
770
+ """
771
+ This code is horizontal rastering.
772
+ We are looking first for all consecutive pixel chains with the same pixel value
773
+ Then we loop through the segments and yield the 'end edge' of the 'last' pixel
774
+ 'end edge' and 'last' are dependent on the sweep direction.
775
+ There is one peculiarity though that is required for K40 lasers:
776
+ a) We may only move from one yielded (x,y,on) tuple in a pure horizontal or pure
777
+ vertical fashion (we could as well go perfectly diagonal but we are not using
778
+ this feature). So at the end of one sweepline we need to change to the
779
+ next scanline by going directly up/down/left/right and then move to the first
780
+ relevant pixel.
781
+ b) we need to take care that we are not landing on the same pixel twice. So we move
782
+ outside the new chain to avoid this
783
+ """
784
+ unidirectional = not self.bidirectional
785
+
786
+ dx = 1 if self.start_minimum_x else -1
787
+ dy = 1 if self.start_minimum_y else -1
788
+ last_x = self.initial_x
789
+ last_y = self.initial_y
790
+ lower = min(self.initial_y, self.final_y)
791
+ upper = max(self.initial_y, self.final_y)
792
+ y = lower if self.start_minimum_y else upper
793
+ first = True
794
+ while lower <= y <= upper:
795
+ segments = self._get_pixel_chains(y, True)
796
+ self._consume_pixel_chains(segments, y, True)
797
+ if segments:
798
+ if dx > 0:
799
+ # from left to right
800
+ idx = 0
801
+ start = 0
802
+ end = 1
803
+ edge_start = -0.5
804
+ edge_end = 0.5
805
+ else:
806
+ idx = len(segments) - 1
807
+ end = 0
808
+ start = 1
809
+ edge_start = 0.5
810
+ edge_end = -0.5
811
+ if last_x is None:
812
+ last_x = segments[idx][start] + edge_start
813
+ # Goto next line, but make sure we end up outside our chain
814
+ # We consider as well the overscan value
815
+ overscan_left = 0 if dx >= 0 else self.overscan
816
+ overscan_right = 0 if dx <= 0 else self.overscan
817
+ if not first and (segments[0][0] - overscan_left <= last_x <= segments[-1][1] + overscan_right):
818
+ # inside the chain!
819
+ # So lets move a bit to the side
820
+ if dx > 0:
821
+ if self.bidirectional:
822
+ # Previous was sweep from right to left, so we go beyond first point
823
+ last_x = segments[0][0] - overscan_left - 1
824
+ else:
825
+ # We go beyond last point
826
+ last_x = segments[-1][1] + overscan_right + 1
827
+ else:
828
+ # Previous was sweep from left to right, so we go beyond last point
829
+ last_x = segments[-1][1] + overscan_right + 1
830
+ yield last_x, y - dy, 0
831
+ last_y = y
832
+ yield last_x, last_y, 0
833
+ while 0 <= idx < len(segments):
834
+ sx = segments[idx][start] + edge_start
835
+ ex = segments[idx][end] + edge_end
836
+ on = segments[idx][2]
837
+ if last_x != sx:
838
+ yield sx, last_y, 0
839
+ last_x = ex
840
+ yield last_x, last_y, on
841
+ idx += dx
842
+ if self.overscan:
843
+ last_x += dx * self.overscan
844
+ yield last_x, last_y, 0
845
+ if not unidirectional:
846
+ dx = -dx
847
+ first = False
848
+ else:
849
+ # Just climb the line, and don't change directions
850
+ last_y = y
851
+ yield last_x, last_y, 0
852
+ y += dy
853
+
854
+ # Legacy code for the m2nano - yes this has deficits for low dpi but it seems
855
+ # to be finetuned to the needs of the m2nano controller.
856
+ # This will be called if the appropriate device setting is in place
857
+ def _legacy_plot_vertical(self):
858
+ """
859
+ This code is for vertical rastering.
860
+
861
+ @return:
862
+ """
863
+ width = self.width
864
+ unidirectional = not self.bidirectional
865
+ skip_pixel = self.skip_pixel
866
+
867
+ x, y = self.initial_position()
868
+ dx = 1 if self.start_minimum_x else -1
869
+ dy = 1 if self.start_minimum_y else -1
870
+
871
+ yield x, y, 0
872
+ while 0 <= x < width:
873
+ lower_bound = self.topmost_not_equal(x)
874
+ if lower_bound is None:
875
+ x += dx
876
+ yield x, y, 0
877
+ continue
878
+ upper_bound = self.bottommost_not_equal(x)
879
+ traveling_bottom = self.start_minimum_y if unidirectional else dy >= 0
880
+ next_traveling_bottom = self.start_minimum_y if unidirectional else dy <= 0
881
+
882
+ next_x, next_y = self.calculate_next_vertical_pixel(
883
+ x + dx, dx, topmost_pixel=next_traveling_bottom
884
+ )
885
+ if next_y is not None:
886
+ # If we have a next scanline, we must end after the last pixel of that scanline too.
887
+ upper_bound = max(next_y, upper_bound) + self.overscan
888
+ lower_bound = min(next_y, lower_bound) - self.overscan
889
+ pixel_chain = []
890
+ last_y = lower_bound if traveling_bottom else upper_bound
891
+ if traveling_bottom:
892
+ while y < upper_bound:
893
+ try:
894
+ pixel = self.px(x, y)
895
+ except IndexError:
896
+ pixel = 0
897
+ y = self.nextcolor_bottom(x, y, upper_bound)
898
+ y = min(y, upper_bound)
899
+ if pixel == skip_pixel:
900
+ yield x, y, 0
901
+ else:
902
+ yield x, y, pixel
903
+ pixel_chain.append([last_y, y, pixel])
904
+ last_y = y + 1
905
+ else:
906
+ while lower_bound < y:
907
+ try:
908
+ pixel = self.px(x, y)
909
+ except IndexError:
910
+ pixel = 0
911
+ y = self.nextcolor_top(x, y, lower_bound)
912
+ y = max(y, lower_bound)
913
+ if pixel == skip_pixel:
914
+ yield x, y, 0
915
+ else:
916
+ yield x, y, pixel
917
+ pixel_chain.append([y, last_y, pixel])
918
+ last_y = y - 1
919
+ if pixel_chain:
920
+ self._consume_pixel_chains(pixel_chain, x, False)
921
+ if next_y is None:
922
+ # remaining image is blank, we stop right here.
923
+ return
924
+ yield next_x, y, 0
925
+ if y != next_y:
926
+ yield next_x, next_y, 0
927
+ x = next_x
928
+ y = next_y
929
+ dy = -dy
930
+
931
+ def _legacy_plot_horizontal(self):
932
+ """
933
+ This code is horizontal rastering.
934
+
935
+ @return:
936
+ """
937
+ height = self.height
938
+ unidirectional = not self.bidirectional
939
+ skip_pixel = self.skip_pixel
940
+
941
+ x, y = self.initial_position()
942
+ dx = 1 if self.start_minimum_x else -1
943
+ dy = 1 if self.start_minimum_y else -1
944
+ yield x, y, 0
945
+ while 0 <= y < height:
946
+ lower_bound = self.leftmost_not_equal(y)
947
+ if lower_bound is None:
948
+ y += dy
949
+ yield x, y, 0
950
+ continue
951
+ upper_bound = self.rightmost_not_equal(y)
952
+ traveling_right = self.start_minimum_x if unidirectional else dx >= 0
953
+ next_traveling_right = self.start_minimum_x if unidirectional else dx <= 0
954
+
955
+ next_x, next_y = self.calculate_next_horizontal_pixel(
956
+ y + dy, dy, leftmost_pixel=next_traveling_right
957
+ )
958
+ if next_x is not None:
959
+ # If we have a next scanline, we must end after the last pixel of that scanline too.
960
+ upper_bound = max(next_x, upper_bound) + self.overscan
961
+ lower_bound = min(next_x, lower_bound) - self.overscan
962
+ pixel_chain = []
963
+ last_x = lower_bound if traveling_right else upper_bound
964
+ if traveling_right:
965
+ while x < upper_bound:
966
+ try:
967
+ pixel = self.px(x, y)
968
+ except IndexError:
969
+ pixel = 0
970
+ x = self.nextcolor_right(x, y, upper_bound)
971
+ x = min(x, upper_bound)
972
+ if pixel == skip_pixel:
973
+ yield x, y, 0
974
+ else:
975
+ yield x, y, pixel
976
+ pixel_chain.append([last_x, x, pixel])
977
+ last_x = x + 1
978
+ else:
979
+ while lower_bound < x:
980
+ try:
981
+ pixel = self.px(x, y)
982
+ except IndexError:
983
+ pixel = 0
984
+ x = self.nextcolor_left(x, y, lower_bound)
985
+ x = max(x, lower_bound)
986
+ if pixel == skip_pixel:
987
+ yield x, y, 0
988
+ else:
989
+ yield x, y, pixel
990
+ pixel_chain.append([x, last_x, pixel])
991
+ last_x = x - 1
992
+ if pixel_chain:
993
+ self._consume_pixel_chains(pixel_chain, y, True)
994
+ if next_y is None:
995
+ # remaining image is blank, we stop right here.
996
+ return
997
+ yield x, next_y, 0
998
+ if x != next_x:
999
+ yield next_x, next_y, 0
1000
+ x = next_x
1001
+ y = next_y
1002
+ dx = -dx
1003
+
1004
+ def _plot_greedy_neighbour(self, horizontal: bool = True):
1005
+ """
1006
+ Distance Matrix Function: The distance_matrix function calculates the squared distances from the
1007
+ current point to all other points, applying a penalty to the y-distances to prefer movements in the x-direction.
1008
+
1009
+ Initialization: We initialize the visited array to keep track of visited segments, the path list
1010
+ to store the order of segments along with the relevant point (start or end),
1011
+ and the current_point as the starting point.
1012
+
1013
+ Sliding Window: We use a sliding window to limit the number of segments we need to consider at each step.
1014
+ The window size is controlled by the window_size parameter and is applied to both x and y coordinates.
1015
+
1016
+ Main Loop: In the main loop, we calculate the distances from the current point to all unvisited points
1017
+ within the sliding window using the distance_matrix function.
1018
+ We then use np.argmin to find the index of the smallest distance.
1019
+ We update the current_point to the next point and mark the segment as visited.
1020
+ The path list is updated with the segment index and whether the start or end point is relevant.
1021
+ """
1022
+
1023
+ def walk_segments(segments, horizontal=True, xy_penalty=1, width=1, height=1):
1024
+ n:int = len(segments)
1025
+ visited = np.zeros(n, dtype=bool)
1026
+ path = []
1027
+ window_size = 10
1028
+ current_point = np.array(segments[0][0], dtype=float)
1029
+ segment_points = np.array([point for segment in segments for point in segment], dtype=float)
1030
+ mask = ~visited.repeat(2)
1031
+ while len(path) < n:
1032
+ # Find the range of segments within the x- and y-window
1033
+ x_min = current_point[0] - window_size
1034
+ x_max = current_point[0] + window_size
1035
+ y_min = current_point[1] - window_size
1036
+ y_max = current_point[1] + window_size
1037
+ unvisited_indices = np.where(
1038
+ (segment_points[:, 0] >= x_min) &
1039
+ (segment_points[:, 0] <= x_max) &
1040
+ (segment_points[:, 1] >= y_min) &
1041
+ (segment_points[:, 1] <= y_max) &
1042
+ mask
1043
+ )[0]
1044
+ if len(unvisited_indices) == 0:
1045
+ # If no segments are within the window, expand the window
1046
+ window_size *= 2
1047
+ # print (f"Did not find points: now window: {window_size}")
1048
+ if window_size <= 2* height or window_size <= 2 * width: # Safety belt
1049
+ continue
1050
+
1051
+ unvisited_points = segment_points[unvisited_indices]
1052
+
1053
+ # distances = distance_matrix(unvisited_points, current_point, y_penalty)
1054
+ diff = unvisited_points - current_point
1055
+ if horizontal:
1056
+ diff[:, 1] *= xy_penalty # Apply penalty to y-distances
1057
+ else:
1058
+ diff[:, 0] *= xy_penalty # Apply penalty to x-distances
1059
+
1060
+ distances = np.sum(diff ** 2, axis=1) # Return squared distances
1061
+
1062
+ min_distance_idx = np.argmin(distances)
1063
+ next_segment = unvisited_indices[min_distance_idx] // 2
1064
+
1065
+ if not visited[next_segment]:
1066
+ visited[next_segment] = True
1067
+ # mask = ~visited.repeat(2)
1068
+ mask[2 * next_segment] = False
1069
+ mask[2 * next_segment + 1] = False
1070
+ if min_distance_idx % 2 == 0:
1071
+ path.append((next_segment, 'end'))
1072
+ current_point = segment_points[next_segment * 2 + 1] # Move to the other endpoint
1073
+ else:
1074
+ path.append((next_segment, 'start'))
1075
+ current_point = segment_points[next_segment * 2] # Move to the other endpoint
1076
+ window_size = 10 # Reset window size
1077
+
1078
+ return path
1079
+
1080
+ t0 = perf_counter()
1081
+ # An experimental routine
1082
+ if horizontal:
1083
+ dy = 1 if self.start_minimum_y else -1
1084
+ lower = min(self.initial_y, self.final_y)
1085
+ upper = max(self.initial_y, self.final_y)
1086
+ y = lower if self.start_minimum_y else upper
1087
+ else:
1088
+ dx = 1 if self.start_minimum_x else -1
1089
+ lower = min(self.initial_x, self.final_x)
1090
+ upper = max(self.initial_x, self.final_x)
1091
+ x = lower if self.start_minimum_x else upper
1092
+
1093
+ line_parts = []
1094
+ on_parts = []
1095
+ if self.debug_level > 2:
1096
+ print (f"{'horizontal' if horizontal else 'Vertical'} for {self.width}x{self.height} image. {'y' if horizontal else 'x'} from {lower} to {upper}")
1097
+ if horizontal:
1098
+ while lower <= y <= upper:
1099
+ segments = self._get_pixel_chains(y, True)
1100
+ self._consume_pixel_chains(segments, y, True)
1101
+ for seg in segments:
1102
+ # Append (xstart, y), (xend, y), on
1103
+ line_parts.append( ( (seg[0], y), (seg[1], y) ) )
1104
+ on_parts.append(seg[2])
1105
+ y += dy
1106
+ else:
1107
+ while lower <= x <= upper:
1108
+ segments = self._get_pixel_chains(x, False)
1109
+ self._consume_pixel_chains(segments, x, False)
1110
+ for seg in segments:
1111
+ # Append (xstart, y), (xend, y), on
1112
+ line_parts.append( ( (x, seg[0]), (x, seg[1]) ) )
1113
+ on_parts.append(seg[2])
1114
+ x += dx
1115
+ if self.debug_level > 2:
1116
+ print (f"Created {len(line_parts)} segments")
1117
+ t1 = perf_counter()
1118
+ penalty = 3 if self.special.get("gantry", False) else 1
1119
+ path = walk_segments(line_parts, horizontal=horizontal, xy_penalty=penalty, width=self.width, height=self.height)
1120
+ # print("Order of segments:", path)
1121
+ t2 = perf_counter()
1122
+ if horizontal:
1123
+ last_x = self.initial_x
1124
+ last_y = lower
1125
+ else:
1126
+ last_x = lower
1127
+ last_y = self.initial_y
1128
+ for idx, mode in path:
1129
+ if mode == "end":
1130
+ # end was closer
1131
+ (ex, ey), (sx, sy) = line_parts[idx]
1132
+ else:
1133
+ (sx, sy), (ex, ey) = line_parts[idx]
1134
+ on = on_parts[idx]
1135
+ if horizontal:
1136
+ dx = ex - sx
1137
+ if dx >= 0:
1138
+ # from left to right
1139
+ edge_start = -0.5
1140
+ edge_end = 0.5
1141
+ else:
1142
+ edge_start = 0.5
1143
+ edge_end = -0.5
1144
+ sx += edge_start
1145
+ ex += edge_end
1146
+ if sy != last_y:
1147
+ last_y = sy
1148
+ yield last_x, last_y, 0
1149
+ if last_x != sx:
1150
+ yield sx, last_y, 0
1151
+ else:
1152
+ dy = ey - sy
1153
+ if dy >= 0:
1154
+ # from left to right
1155
+ edge_start = -0.5
1156
+ edge_end = 0.5
1157
+ else:
1158
+ edge_start = 0.5
1159
+ edge_end = -0.5
1160
+ sy += edge_start
1161
+ ey += edge_end
1162
+ if sx != last_x:
1163
+ last_x = sx
1164
+ yield last_x, last_y, 0
1165
+ if last_y != sy:
1166
+ last_y = sy
1167
+ yield sx, sy, 0
1168
+
1169
+ yield ex, ey, on
1170
+ last_x = ex
1171
+ last_y = ey
1172
+ t3 = perf_counter()
1173
+ if self.debug_level > 1:
1174
+ print (f"Overall time for {'horizontal' if horizontal else 'vertical'} consumption: {t3-t0:.2f}s - created: {len(line_parts)} segments")
1175
+ print (f"Computation: {t2-t0:.2f}s - Chain creation:{t1 - t0:.2f}s, Walk: {t2 - t1:.2f}s")
1176
+ self.final_x = last_x
1177
+ self.final_y = last_y
1178
+
1179
+ def _plot_spiral(self):
1180
+
1181
+
1182
+ rows = self.height
1183
+ cols = self.width
1184
+ center_row, center_col = rows // 2, cols // 2
1185
+ self.initial_x = center_col
1186
+ self.initial_y = center_row
1187
+
1188
+ directions = [(1, 0), (0, 1), (-1, 0), (0, -1)] # Right, Down, Left, Up
1189
+ edges = [(0.5, 0), (0, 0.5), (-0.5, 0), (0, -0.5)]
1190
+ direction_index = 0
1191
+ steps = 1
1192
+
1193
+ row, col = center_row, center_col
1194
+ # is the very first pixel an on?
1195
+ last_x = col
1196
+ last_y = row
1197
+ count = 1
1198
+ pixel = self.px(col, row)
1199
+ if pixel == self.skip_pixel:
1200
+ pixel = 0
1201
+ last_pixel = pixel
1202
+ if pixel:
1203
+ yield col - 0.5, row, 0
1204
+ last_x = col + 0.5
1205
+ yield last_x, row, pixel
1206
+ while count < rows * cols:
1207
+ for _ in range(2):
1208
+ segments = []
1209
+ dx, dy = edges[direction_index]
1210
+ edge_start_x = -dx
1211
+ edge_start_y = -dy
1212
+ edge_end_x = dx
1213
+ edge_end_y = dy
1214
+ # msg = f"[({col}, {row}) - {steps}] "
1215
+ for _ in range(steps):
1216
+ row += directions[direction_index][1]
1217
+ col += directions[direction_index][0]
1218
+ if 0 <= row < rows and 0 <= col < cols:
1219
+ pixel = self.px(col, row)
1220
+ on = 0 if pixel == self.skip_pixel else pixel
1221
+ # msg = f"{msg} {'X' if on else '.'}"
1222
+ if on:
1223
+ if on == last_pixel and len(segments):
1224
+ segments[-1][1] = (col, row)
1225
+ else:
1226
+ segments.append ([(col, row), (col, row), on])
1227
+
1228
+ last_pixel = on
1229
+ count += 1
1230
+ if count == rows * cols:
1231
+ break
1232
+ # Deal with segments
1233
+ # print (msg)
1234
+ # print (segments)
1235
+ for (sx, sy), (ex, ey), pixel in segments:
1236
+ sx += edge_start_x
1237
+ sy += edge_start_y
1238
+ ex += edge_end_x
1239
+ ey += edge_end_y
1240
+ if last_y != sy:
1241
+ yield last_x, sy, 0
1242
+ last_y = sy
1243
+ if last_x != sx:
1244
+ yield sx, last_y, 0
1245
+ last_x = ex
1246
+ last_y = ey
1247
+ yield last_x, last_y, pixel
1248
+ self.final_x, self.final_y = last_x, last_y
1249
+ # Now we need to empty overlapping pixels...
1250
+ if self.overlap > 0:
1251
+ BLANK = 255
1252
+ for (start_x, start_y), (end_x, end_y), on in segments:
1253
+ sx = min(start_x, end_x)
1254
+ ex = max(start_x, end_x)
1255
+ sy = min(start_y, end_y)
1256
+ ey = max(start_y, end_y)
1257
+
1258
+ if direction_index in (0, 2): # horizontal
1259
+ for y_idx in range(-self.overlap, self.overlap + 1):
1260
+ ny = sy + y_idx
1261
+ for nx in range(sx, ex + 1):
1262
+ if 0 <= nx < self.width and 0 <= ny < self.height:
1263
+ self.data[nx, ny] = BLANK
1264
+ else:
1265
+ for x_idx in range(-self.overlap, self.overlap + 1):
1266
+ nx = sx + x_idx
1267
+ for ny in range(sy, ey + 1):
1268
+ if 0 <= nx < self.width and 0 <= ny < self.height:
1269
+ self.data[nx, ny] = BLANK
1270
+
1271
+
1272
+ direction_index = (direction_index + 1) % 4
1273
+ steps += 1
1274
+
1275
+
1276
+ def _plot_crossover(self):
1277
+ """
1278
+ This algorithm scans through the image looking for the row or the column with the most pixels.
1279
+ It will hand back this information together with a precompiled list of
1280
+ state changes (start, end, pixel), ie a non-blank pixel with a different on value
1281
+ than the previous pixel
1282
+ It will then clean the row / column and start again.
1283
+ This happens until no more non-empty rows / cols can be found
1284
+
1285
+ The receiving routine takes these values, orders them according to
1286
+ type (row/col) and row/col number and generates plot instructions
1287
+ that will be yielded.
1288
+
1289
+ Yields:
1290
+ list of tuples with (x, y, on)
1291
+ """
1292
+ ROW=0
1293
+ COL=1
1294
+
1295
+ def process_image(image):
1296
+ # We will modify the image to keep track of deleted rows and columns
1297
+ # Get the dimensions of the image
1298
+ rows, cols = image.shape
1299
+ # We prefer cols (which is the x-axis and that is normally
1300
+ # slightly advantageous for gantry lasers)
1301
+ if self.special.get("gantry", False):
1302
+ colfactor = 1.0
1303
+ rowfactor = 0.8
1304
+ else:
1305
+ colfactor = 1.0
1306
+ rowfactor = 1.0
1307
+
1308
+ # Initialize a list to store the results
1309
+ results = []
1310
+
1311
+ # Iterate through the matrix, we cover all rows and cols
1312
+ colidx = 0
1313
+ rowidx = 0
1314
+ covered_row = [None] * rows
1315
+ covered_col = [None] * cols
1316
+ stored_row = np.zeros(rows)
1317
+ stored_col = np.zeros(cols)
1318
+ stored_row_len = np.zeros(rows)
1319
+ stored_col_len = np.zeros(cols)
1320
+ recalc_row = True
1321
+ recalc_col = True
1322
+ while True:
1323
+ if recalc_col:
1324
+ for i in range(cols):
1325
+ col_len = cols
1326
+ if covered_col[i] is None:
1327
+ nonzero_indices = np.nonzero(image[:, i])[0]
1328
+ count = len(nonzero_indices)
1329
+ if count == 0:
1330
+ covered_col[i] = True
1331
+ else:
1332
+ col_len = nonzero_indices[-1] - nonzero_indices[0] + 1
1333
+ covered_col[i] = False
1334
+ stored_col[i] = count
1335
+ stored_col_len[i] = col_len
1336
+ if recalc_row:
1337
+ for i in range(rows):
1338
+ row_len = rows
1339
+ if covered_row[i] is None:
1340
+ nonzero_indices = np.nonzero(image[i, :])[0]
1341
+ count = len(nonzero_indices)
1342
+ if count == 0:
1343
+ covered_row[i] = True
1344
+ else:
1345
+ row_len = nonzero_indices[-1] - nonzero_indices[0] + 1
1346
+ covered_row[i] = False
1347
+ stored_row[i] = count
1348
+ stored_row_len[i] = row_len
1349
+
1350
+ colidx = np.argmax(stored_col)
1351
+ rowidx = np.argmax(stored_row)
1352
+
1353
+ col_count = stored_col[colidx]
1354
+ col_len = stored_col_len[colidx]
1355
+ row_count = stored_row[rowidx]
1356
+ row_len = stored_row_len[rowidx]
1357
+
1358
+ if row_count == col_count == 0:
1359
+ break
1360
+ # Determine whether there are more pixels in the row or column
1361
+
1362
+ row_ratio = row_count * row_count / row_len * rowfactor
1363
+ col_ratio = col_count * col_count / col_len * colfactor
1364
+ # print (f"Col #{rowidx}: {int(row_count):3d} pixel over {int(row_len):3d} length, ratio: {row_ratio:.3f} {'winner' if row_ratio >= col_ratio else 'loser'}")
1365
+ # print (f"Row #{colidx}: {int(col_count):3d} pixel over {int(col_len):3d} length, ratio: {col_ratio:.3f} {'winner' if row_ratio < col_ratio else 'loser'}")
1366
+ # if row_count >= col_count:
1367
+ if row_ratio >= col_ratio:
1368
+ last_pixel = None
1369
+ segments = []
1370
+ # msg = ""
1371
+ for idx in range(cols):
1372
+ on = image[rowidx, idx]
1373
+ # msg = f"{msg}{'X' if on else '.'}"
1374
+ if on:
1375
+ if not covered_col[idx]:
1376
+ covered_col[idx] = None # needs recalc
1377
+ if on == last_pixel:
1378
+ segments[-1][1] = idx
1379
+ else:
1380
+ segments.append ([idx, idx, on])
1381
+ last_pixel = on
1382
+ results.append((COL, rowidx, segments)) # Intentionally so, as the numpy array has x and y exchanged
1383
+ # print (f"Col #{rowidx}: {msg} -> {segments}")
1384
+
1385
+ # Clear the column
1386
+ image[rowidx,:] = 0
1387
+ covered_row[rowidx] = True
1388
+ stored_row[rowidx] = 0
1389
+ for rc in range(self.overlap):
1390
+ r = rowidx - rc
1391
+ if 0 <= r < rows:
1392
+ image[r,:] = 0
1393
+ covered_row[r] = True
1394
+ stored_row[r] = 0
1395
+ r = rowidx + rc
1396
+ if 0 <= r < rows:
1397
+ image[r,:] = 0
1398
+ covered_row[r] = True
1399
+ stored_row[r] = 0
1400
+ recalc_col = True
1401
+ else:
1402
+ last_pixel = None
1403
+ segments = []
1404
+ # msg = ""
1405
+ for idx in range(rows):
1406
+ on = image[idx, colidx]
1407
+ # msg = f"{msg}{'X' if on else '.'}"
1408
+ if on:
1409
+ if not covered_row[idx]:
1410
+ covered_row[idx] = None # needs recalc
1411
+ if on == last_pixel:
1412
+ segments[-1][1] = idx
1413
+ else:
1414
+ segments.append ([idx, idx, on])
1415
+ last_pixel = on
1416
+ results.append((ROW, colidx, segments))
1417
+ # print (f"Row #{colidx}: {msg} -> {segments}")
1418
+ # Clear the row
1419
+ image[:, colidx] = 0
1420
+ covered_col[colidx] = True
1421
+ stored_col[colidx] = 0
1422
+ for rc in range(self.overlap):
1423
+ r = colidx - rc
1424
+ if 0 <= r < cols:
1425
+ image[:, r] = 0
1426
+ covered_col[r] = True
1427
+ stored_col[r] = 0
1428
+ r = rowidx + rc
1429
+ if 0 <= r < cols:
1430
+ image[:, r] = 0
1431
+ covered_col[r] = True
1432
+ stored_col[r] = 0
1433
+ recalc_row = True
1434
+ if self.debug_level > 1:
1435
+ for cidx in range(cols):
1436
+ msg = ""
1437
+ for ridx in range(rows):
1438
+ on = image[ridx, cidx]
1439
+ msg = f"{msg}{'X' if on else '.'}"
1440
+ print (f"{cidx:3d}: {msg}")
1441
+
1442
+ return results
1443
+
1444
+ t0 = perf_counter()
1445
+ # initialize the matrix
1446
+ image = np.empty((self.width, self.height))
1447
+ # Apply filter and eliminate skip_pixel
1448
+ for x in range(self.width):
1449
+ for y in range(self.height):
1450
+ px = self.px(x, y)
1451
+ if px==self.skip_pixel:
1452
+ px = 0
1453
+ image[x, y] = px
1454
+ t1 = perf_counter()
1455
+ results = process_image(image)
1456
+ results.sort()
1457
+ t2 = perf_counter()
1458
+ dx = +1
1459
+ dy = +1
1460
+ first_x = 0
1461
+ first_y = 0
1462
+ last_x = self.initial_x
1463
+ last_y = self.initial_y
1464
+ yield last_x, last_y, 0
1465
+ for mode, xy, segments in results:
1466
+ # eliminate data and swap direction
1467
+ if self.special.get("mode_filter", "") == "ROW" and mode != ROW:
1468
+ continue
1469
+ if self.special.get("mode_filter", "") == "COL" and mode != COL:
1470
+ continue
1471
+
1472
+ if not segments:
1473
+ continue
1474
+ # NB: Axis change indication is no longer required,
1475
+ # the m2nano does not support it, the other devices do not need it...
1476
+ if mode == ROW:
1477
+ if xy != last_y:
1478
+ last_y = xy
1479
+ yield last_x, last_y, 0
1480
+ if dx > 0:
1481
+ # from left to right
1482
+ idx = 0
1483
+ start = 0
1484
+ end = 1
1485
+ edge_start = -0.5
1486
+ edge_end = 0.5
1487
+ else:
1488
+ idx = len(segments) - 1
1489
+ end = 0
1490
+ start = 1
1491
+ edge_start = 0.5
1492
+ edge_end = -0.5
1493
+ while 0 <= idx < len(segments):
1494
+ sx = segments[idx][start] + edge_start
1495
+ ex = segments[idx][end] + edge_end
1496
+ on = segments[idx][2]
1497
+ if last_x != sx:
1498
+ yield sx, last_y, 0
1499
+ last_x = ex
1500
+ yield last_x, last_y, on
1501
+ idx += dx
1502
+ else:
1503
+ if xy != last_x:
1504
+ last_x = xy
1505
+ yield last_x, last_y, 0
1506
+ if dy > 0:
1507
+ # from top to bottom
1508
+ idx = 0
1509
+ start = 0
1510
+ end = 1
1511
+ edge_start = -0.5
1512
+ edge_end = 0.5
1513
+ else:
1514
+ idx = len(segments) - 1
1515
+ end = 0
1516
+ start = 1
1517
+ edge_start = 0.5
1518
+ edge_end = -0.5
1519
+ while 0 <= idx < len(segments):
1520
+ sy = segments[idx][start] + edge_start
1521
+ ey = segments[idx][end] + edge_end
1522
+ on = segments[idx][2]
1523
+ if last_y != sy:
1524
+ yield last_x, sy, 0
1525
+ last_y = ey
1526
+ yield last_x, last_y, on
1527
+ idx += dy
1528
+ if self.bidirectional:
1529
+ if mode == ROW:
1530
+ dx = -dx
1531
+ else: # column
1532
+ dy = -dy
1533
+
1534
+ # We need to set the final values so that the rastercut is able to carry on
1535
+ self.final_x = last_x
1536
+ self.final_y = last_y
1537
+ t3 = perf_counter()
1538
+ if self.debug_level > 1:
1539
+ print (f"Overall time for crossover consumption: {t3-t0:.2f}s")
1540
+ print (f"Computation: {t2 - t0:.2f}s - Array creation:{t1 - t0:.2f}s, Algorithm: {t2 - t1:.2f}s")
1541
+ """
1542
+ # Testpattern generation
1543
+ def testpattern_generator(self):
1544
+ def rectangle_h():
1545
+ # simple rectangle
1546
+ self.initial_x = 0
1547
+ self.initial_y = 0
1548
+ self.final_x = 0
1549
+ self.final_y = 0
1550
+ self.horizontal = True
1551
+
1552
+ yield 0, 0, off
1553
+ yield self.width - 1, 0, on
1554
+
1555
+ yield self.width - 1, self.height - 1, on
1556
+
1557
+ yield 0, self.height - 1, on
1558
+
1559
+ yield 0, 0, on
1560
+
1561
+ def rectangle_v():
1562
+ # simple rectangle but start with y movements first
1563
+ self.initial_x = 0
1564
+ self.initial_y = 0
1565
+ self.final_x = 0
1566
+ self.final_y = 0
1567
+ self.horizontal = True
1568
+
1569
+ yield 0, 0, off
1570
+ yield 0, self.height - 1, on
1571
+
1572
+ yield self.width - 1, self.height - 1, on
1573
+
1574
+ yield self.width - 1, 0, on
1575
+
1576
+ yield 0, 0, on
1577
+
1578
+
1579
+ def snake_h():
1580
+ # horizontal snake
1581
+ self.initial_x = 0
1582
+ self.initial_y = 0
1583
+ x = 0
1584
+ y = 0
1585
+ self.horizontal = True
1586
+ yield 0, 0, off
1587
+ wd = self.width - 1
1588
+ left = True
1589
+ while y < self.height - 2:
1590
+ x = wd if left else 0
1591
+ self.horizontal = True
1592
+ yield x, y, on
1593
+ self.horizontal = False
1594
+ yield x, y + 2, on
1595
+ left = not left
1596
+ y += 2
1597
+ self.final_x = x
1598
+ self.final_y = y
1599
+
1600
+ def snake_v():
1601
+ # vertical snake
1602
+ self.initial_x = 0
1603
+ self.initial_y = 0
1604
+ x = 0
1605
+ yield 0, 0, off
1606
+ ht = self.height - 1
1607
+ top = True
1608
+ while x < self.width - 2:
1609
+ y = ht if top else 0
1610
+ self.horizontal = False
1611
+ yield x, y, on
1612
+ self.horizontal = True
1613
+ yield x + 2, y, on
1614
+ top = not top
1615
+ x += 2
1616
+ self.final_x = x
1617
+ self.final_y = y
1618
+
1619
+ def spiral():
1620
+ # Spiral to inside
1621
+ self.initial_x = 0
1622
+ self.initial_y = 0
1623
+ yield 0, 0, off
1624
+
1625
+ x = -2 # start
1626
+ y = 0
1627
+ width = self.width + 1
1628
+ height = self.height - 1
1629
+ while width > 0 and height > 0:
1630
+ x += width
1631
+ y += 0
1632
+ self.horizontal = True
1633
+ yield x, y, on
1634
+ x += 0
1635
+ y += height
1636
+ self.horizontal = False
1637
+ yield x, y, on
1638
+ x -=(width - 2)
1639
+ y += 0
1640
+ self.horizontal = True
1641
+ yield x, y, on
1642
+ x += 0
1643
+ y -= (height - 2)
1644
+ self.horizontal = False
1645
+ yield x, y, on
1646
+ width -= 4
1647
+ height -= 4
1648
+ self.final_x = x
1649
+ self.final_y = y
1650
+
1651
+ on = self.filter(0)
1652
+ off = 0
1653
+ # print (f"on={on}, off={off}")
1654
+ method = abs(self.direction) - 1
1655
+ methods = (rectangle_h, rectangle_v, snake_h, snake_v, spiral)
1656
+ try:
1657
+ yield from methods[method]()
1658
+ except IndexError:
1659
+ print (f"Unknown testgenerator for {self.direction}")
1660
+ """