meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7010__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 (445) 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 +1195 -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 +1844 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4561 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4329 -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 +933 -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/trace.py +651 -563
  66. meerk40t/core/elements/tree_commands.py +415 -409
  67. meerk40t/core/elements/undo_redo.py +116 -58
  68. meerk40t/core/elements/wordlist.py +319 -200
  69. meerk40t/core/exceptions.py +9 -9
  70. meerk40t/core/laserjob.py +220 -220
  71. meerk40t/core/logging.py +63 -63
  72. meerk40t/core/node/blobnode.py +83 -86
  73. meerk40t/core/node/bootstrap.py +105 -103
  74. meerk40t/core/node/branch_elems.py +40 -31
  75. meerk40t/core/node/branch_ops.py +45 -38
  76. meerk40t/core/node/branch_regmark.py +48 -41
  77. meerk40t/core/node/cutnode.py +29 -32
  78. meerk40t/core/node/effect_hatch.py +375 -257
  79. meerk40t/core/node/effect_warp.py +398 -0
  80. meerk40t/core/node/effect_wobble.py +441 -309
  81. meerk40t/core/node/elem_ellipse.py +404 -309
  82. meerk40t/core/node/elem_image.py +1082 -801
  83. meerk40t/core/node/elem_line.py +358 -292
  84. meerk40t/core/node/elem_path.py +259 -201
  85. meerk40t/core/node/elem_point.py +129 -102
  86. meerk40t/core/node/elem_polyline.py +310 -246
  87. meerk40t/core/node/elem_rect.py +376 -286
  88. meerk40t/core/node/elem_text.py +445 -418
  89. meerk40t/core/node/filenode.py +59 -40
  90. meerk40t/core/node/groupnode.py +138 -74
  91. meerk40t/core/node/image_processed.py +777 -766
  92. meerk40t/core/node/image_raster.py +156 -113
  93. meerk40t/core/node/layernode.py +31 -31
  94. meerk40t/core/node/mixins.py +135 -107
  95. meerk40t/core/node/node.py +1427 -1304
  96. meerk40t/core/node/nutils.py +117 -114
  97. meerk40t/core/node/op_cut.py +462 -335
  98. meerk40t/core/node/op_dots.py +296 -251
  99. meerk40t/core/node/op_engrave.py +414 -311
  100. meerk40t/core/node/op_image.py +755 -369
  101. meerk40t/core/node/op_raster.py +787 -522
  102. meerk40t/core/node/place_current.py +37 -40
  103. meerk40t/core/node/place_point.py +329 -126
  104. meerk40t/core/node/refnode.py +58 -47
  105. meerk40t/core/node/rootnode.py +225 -219
  106. meerk40t/core/node/util_console.py +48 -48
  107. meerk40t/core/node/util_goto.py +84 -65
  108. meerk40t/core/node/util_home.py +61 -61
  109. meerk40t/core/node/util_input.py +102 -102
  110. meerk40t/core/node/util_output.py +102 -102
  111. meerk40t/core/node/util_wait.py +65 -65
  112. meerk40t/core/parameters.py +709 -707
  113. meerk40t/core/planner.py +875 -785
  114. meerk40t/core/plotplanner.py +656 -652
  115. meerk40t/core/space.py +120 -113
  116. meerk40t/core/spoolers.py +706 -705
  117. meerk40t/core/svg_io.py +1836 -1549
  118. meerk40t/core/treeop.py +534 -445
  119. meerk40t/core/undos.py +278 -124
  120. meerk40t/core/units.py +784 -680
  121. meerk40t/core/view.py +393 -322
  122. meerk40t/core/webhelp.py +62 -62
  123. meerk40t/core/wordlist.py +513 -504
  124. meerk40t/cylinder/cylinder.py +247 -0
  125. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  126. meerk40t/cylinder/gui/gui.py +24 -0
  127. meerk40t/device/__init__.py +1 -1
  128. meerk40t/device/basedevice.py +322 -123
  129. meerk40t/device/devicechoices.py +50 -0
  130. meerk40t/device/dummydevice.py +163 -128
  131. meerk40t/device/gui/defaultactions.py +618 -602
  132. meerk40t/device/gui/effectspanel.py +114 -0
  133. meerk40t/device/gui/formatterpanel.py +253 -290
  134. meerk40t/device/gui/warningpanel.py +337 -260
  135. meerk40t/device/mixins.py +13 -13
  136. meerk40t/dxf/__init__.py +1 -1
  137. meerk40t/dxf/dxf_io.py +766 -554
  138. meerk40t/dxf/plugin.py +47 -35
  139. meerk40t/external_plugins.py +79 -79
  140. meerk40t/external_plugins_build.py +28 -28
  141. meerk40t/extra/cag.py +112 -116
  142. meerk40t/extra/coolant.py +403 -0
  143. meerk40t/extra/encode_detect.py +198 -0
  144. meerk40t/extra/ezd.py +1165 -1165
  145. meerk40t/extra/hershey.py +835 -340
  146. meerk40t/extra/imageactions.py +322 -316
  147. meerk40t/extra/inkscape.py +630 -622
  148. meerk40t/extra/lbrn.py +424 -424
  149. meerk40t/extra/outerworld.py +284 -0
  150. meerk40t/extra/param_functions.py +1542 -1556
  151. meerk40t/extra/potrace.py +257 -253
  152. meerk40t/extra/serial_exchange.py +118 -0
  153. meerk40t/extra/updater.py +602 -453
  154. meerk40t/extra/vectrace.py +147 -146
  155. meerk40t/extra/winsleep.py +83 -83
  156. meerk40t/extra/xcs_reader.py +597 -0
  157. meerk40t/fill/fills.py +781 -335
  158. meerk40t/fill/patternfill.py +1061 -1061
  159. meerk40t/fill/patterns.py +614 -567
  160. meerk40t/grbl/control.py +87 -87
  161. meerk40t/grbl/controller.py +990 -903
  162. meerk40t/grbl/device.py +1081 -768
  163. meerk40t/grbl/driver.py +989 -771
  164. meerk40t/grbl/emulator.py +532 -497
  165. meerk40t/grbl/gcodejob.py +783 -767
  166. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  167. meerk40t/grbl/gui/grblcontroller.py +485 -271
  168. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  169. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  170. meerk40t/grbl/gui/gui.py +147 -116
  171. meerk40t/grbl/interpreter.py +44 -44
  172. meerk40t/grbl/loader.py +22 -22
  173. meerk40t/grbl/mock_connection.py +56 -56
  174. meerk40t/grbl/plugin.py +294 -264
  175. meerk40t/grbl/serial_connection.py +93 -88
  176. meerk40t/grbl/tcp_connection.py +81 -79
  177. meerk40t/grbl/ws_connection.py +112 -0
  178. meerk40t/gui/__init__.py +1 -1
  179. meerk40t/gui/about.py +2042 -296
  180. meerk40t/gui/alignment.py +1644 -1608
  181. meerk40t/gui/autoexec.py +199 -0
  182. meerk40t/gui/basicops.py +791 -670
  183. meerk40t/gui/bufferview.py +77 -71
  184. meerk40t/gui/busy.py +170 -133
  185. meerk40t/gui/choicepropertypanel.py +1673 -1469
  186. meerk40t/gui/consolepanel.py +706 -542
  187. meerk40t/gui/devicepanel.py +687 -581
  188. meerk40t/gui/dialogoptions.py +110 -107
  189. meerk40t/gui/executejob.py +316 -306
  190. meerk40t/gui/fonts.py +90 -90
  191. meerk40t/gui/functionwrapper.py +252 -0
  192. meerk40t/gui/gui_mixins.py +729 -0
  193. meerk40t/gui/guicolors.py +205 -182
  194. meerk40t/gui/help_assets/help_assets.py +218 -201
  195. meerk40t/gui/helper.py +154 -0
  196. meerk40t/gui/hersheymanager.py +1430 -846
  197. meerk40t/gui/icons.py +3422 -2747
  198. meerk40t/gui/imagesplitter.py +555 -508
  199. meerk40t/gui/keymap.py +354 -344
  200. meerk40t/gui/laserpanel.py +892 -806
  201. meerk40t/gui/laserrender.py +1470 -1232
  202. meerk40t/gui/lasertoolpanel.py +805 -793
  203. meerk40t/gui/magnetoptions.py +436 -0
  204. meerk40t/gui/materialmanager.py +2917 -0
  205. meerk40t/gui/materialtest.py +1722 -1694
  206. meerk40t/gui/mkdebug.py +646 -359
  207. meerk40t/gui/mwindow.py +163 -140
  208. meerk40t/gui/navigationpanels.py +2605 -2467
  209. meerk40t/gui/notes.py +143 -142
  210. meerk40t/gui/opassignment.py +414 -410
  211. meerk40t/gui/operation_info.py +310 -299
  212. meerk40t/gui/plugin.py +494 -328
  213. meerk40t/gui/position.py +714 -669
  214. meerk40t/gui/preferences.py +901 -650
  215. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  216. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  217. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  218. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  219. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  220. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  221. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  222. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  223. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  224. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  225. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  226. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  227. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  228. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  229. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  230. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  231. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  232. meerk40t/gui/propertypanels/textproperty.py +770 -755
  233. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  234. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  235. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  236. meerk40t/gui/ribbon.py +2468 -2210
  237. meerk40t/gui/scene/scene.py +1100 -1051
  238. meerk40t/gui/scene/sceneconst.py +22 -22
  239. meerk40t/gui/scene/scenepanel.py +439 -349
  240. meerk40t/gui/scene/scenespacewidget.py +365 -365
  241. meerk40t/gui/scene/widget.py +518 -505
  242. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  243. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  244. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  245. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  246. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  247. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  248. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  249. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  250. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  251. meerk40t/gui/scenewidgets/rectselectwidget.py +589 -346
  252. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  253. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  254. meerk40t/gui/scenewidgets/selectionwidget.py +2952 -2756
  255. meerk40t/gui/simpleui.py +357 -333
  256. meerk40t/gui/simulation.py +2431 -2094
  257. meerk40t/gui/snapoptions.py +208 -203
  258. meerk40t/gui/spoolerpanel.py +1227 -1180
  259. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  260. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  261. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  262. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  263. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  264. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  265. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  266. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  267. meerk40t/gui/themes.py +200 -78
  268. meerk40t/gui/tips.py +591 -0
  269. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  270. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  271. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  272. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  273. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  274. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  275. meerk40t/gui/toolwidgets/toolline.py +39 -144
  276. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  277. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  278. meerk40t/gui/toolwidgets/toolmeasure.py +160 -216
  279. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  280. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  281. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  282. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  283. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  284. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  285. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  286. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  287. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  288. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  289. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  290. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  291. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  292. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  293. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  294. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  295. meerk40t/gui/usbconnect.py +98 -91
  296. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  297. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  298. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  299. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  300. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  301. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  302. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  303. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  304. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  305. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  306. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  307. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  308. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  309. meerk40t/gui/wordlisteditor.py +985 -931
  310. meerk40t/gui/wxmeerk40t.py +1444 -1169
  311. meerk40t/gui/wxmmain.py +5578 -4112
  312. meerk40t/gui/wxmribbon.py +1591 -1076
  313. meerk40t/gui/wxmscene.py +1635 -1453
  314. meerk40t/gui/wxmtree.py +2410 -2089
  315. meerk40t/gui/wxutils.py +1769 -1099
  316. meerk40t/gui/zmatrix.py +102 -102
  317. meerk40t/image/__init__.py +1 -1
  318. meerk40t/image/dither.py +429 -0
  319. meerk40t/image/imagetools.py +2778 -2269
  320. meerk40t/internal_plugins.py +150 -130
  321. meerk40t/kernel/__init__.py +63 -12
  322. meerk40t/kernel/channel.py +259 -212
  323. meerk40t/kernel/context.py +538 -538
  324. meerk40t/kernel/exceptions.py +41 -41
  325. meerk40t/kernel/functions.py +463 -414
  326. meerk40t/kernel/jobs.py +100 -100
  327. meerk40t/kernel/kernel.py +3809 -3571
  328. meerk40t/kernel/lifecycles.py +71 -71
  329. meerk40t/kernel/module.py +49 -49
  330. meerk40t/kernel/service.py +147 -147
  331. meerk40t/kernel/settings.py +383 -343
  332. meerk40t/lihuiyu/controller.py +883 -876
  333. meerk40t/lihuiyu/device.py +1181 -1069
  334. meerk40t/lihuiyu/driver.py +1466 -1372
  335. meerk40t/lihuiyu/gui/gui.py +127 -106
  336. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  337. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  338. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  339. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  340. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  341. meerk40t/lihuiyu/interpreter.py +53 -53
  342. meerk40t/lihuiyu/laserspeed.py +450 -450
  343. meerk40t/lihuiyu/loader.py +90 -90
  344. meerk40t/lihuiyu/parser.py +404 -404
  345. meerk40t/lihuiyu/plugin.py +101 -102
  346. meerk40t/lihuiyu/tcp_connection.py +111 -109
  347. meerk40t/main.py +231 -165
  348. meerk40t/moshi/builder.py +788 -781
  349. meerk40t/moshi/controller.py +505 -499
  350. meerk40t/moshi/device.py +495 -442
  351. meerk40t/moshi/driver.py +862 -696
  352. meerk40t/moshi/gui/gui.py +78 -76
  353. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  354. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  355. meerk40t/moshi/plugin.py +43 -43
  356. meerk40t/network/console_server.py +102 -57
  357. meerk40t/network/kernelserver.py +10 -9
  358. meerk40t/network/tcp_server.py +142 -140
  359. meerk40t/network/udp_server.py +103 -77
  360. meerk40t/network/web_server.py +390 -0
  361. meerk40t/newly/controller.py +1158 -1144
  362. meerk40t/newly/device.py +874 -732
  363. meerk40t/newly/driver.py +540 -412
  364. meerk40t/newly/gui/gui.py +219 -188
  365. meerk40t/newly/gui/newlyconfig.py +116 -101
  366. meerk40t/newly/gui/newlycontroller.py +193 -186
  367. meerk40t/newly/gui/operationproperties.py +51 -51
  368. meerk40t/newly/mock_connection.py +82 -82
  369. meerk40t/newly/newly_params.py +56 -56
  370. meerk40t/newly/plugin.py +1214 -1246
  371. meerk40t/newly/usb_connection.py +322 -322
  372. meerk40t/rotary/gui/gui.py +52 -46
  373. meerk40t/rotary/gui/rotarysettings.py +240 -232
  374. meerk40t/rotary/rotary.py +202 -98
  375. meerk40t/ruida/control.py +291 -91
  376. meerk40t/ruida/controller.py +138 -1088
  377. meerk40t/ruida/device.py +672 -231
  378. meerk40t/ruida/driver.py +534 -472
  379. meerk40t/ruida/emulator.py +1494 -1491
  380. meerk40t/ruida/exceptions.py +4 -4
  381. meerk40t/ruida/gui/gui.py +71 -76
  382. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  383. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  384. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  385. meerk40t/ruida/loader.py +54 -52
  386. meerk40t/ruida/mock_connection.py +57 -109
  387. meerk40t/ruida/plugin.py +124 -87
  388. meerk40t/ruida/rdjob.py +2084 -945
  389. meerk40t/ruida/serial_connection.py +116 -0
  390. meerk40t/ruida/tcp_connection.py +146 -0
  391. meerk40t/ruida/udp_connection.py +73 -0
  392. meerk40t/svgelements.py +9671 -9669
  393. meerk40t/tools/driver_to_path.py +584 -579
  394. meerk40t/tools/geomstr.py +5583 -4680
  395. meerk40t/tools/jhfparser.py +357 -292
  396. meerk40t/tools/kerftest.py +904 -890
  397. meerk40t/tools/livinghinges.py +1168 -1033
  398. meerk40t/tools/pathtools.py +987 -949
  399. meerk40t/tools/pmatrix.py +234 -0
  400. meerk40t/tools/pointfinder.py +942 -942
  401. meerk40t/tools/polybool.py +940 -940
  402. meerk40t/tools/rasterplotter.py +1660 -547
  403. meerk40t/tools/shxparser.py +989 -901
  404. meerk40t/tools/ttfparser.py +726 -446
  405. meerk40t/tools/zinglplotter.py +595 -593
  406. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/LICENSE +21 -21
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/METADATA +150 -139
  408. meerk40t-0.9.7010.dist-info/RECORD +445 -0
  409. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/WHEEL +1 -1
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/top_level.txt +0 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/zip-safe +1 -1
  412. meerk40t/balormk/elementlightjob.py +0 -159
  413. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  414. test/bootstrap.py +0 -63
  415. test/test_cli.py +0 -12
  416. test/test_core_cutcode.py +0 -418
  417. test/test_core_elements.py +0 -144
  418. test/test_core_plotplanner.py +0 -397
  419. test/test_core_viewports.py +0 -312
  420. test/test_drivers_grbl.py +0 -108
  421. test/test_drivers_lihuiyu.py +0 -443
  422. test/test_drivers_newly.py +0 -113
  423. test/test_element_degenerate_points.py +0 -43
  424. test/test_elements_classify.py +0 -97
  425. test/test_elements_penbox.py +0 -22
  426. test/test_file_svg.py +0 -176
  427. test/test_fill.py +0 -155
  428. test/test_geomstr.py +0 -1523
  429. test/test_geomstr_nodes.py +0 -18
  430. test/test_imagetools_actualize.py +0 -306
  431. test/test_imagetools_wizard.py +0 -258
  432. test/test_kernel.py +0 -200
  433. test/test_laser_speeds.py +0 -3303
  434. test/test_length.py +0 -57
  435. test/test_lifecycle.py +0 -66
  436. test/test_operations.py +0 -251
  437. test/test_operations_hatch.py +0 -57
  438. test/test_ruida.py +0 -19
  439. test/test_spooler.py +0 -22
  440. test/test_tools_rasterplotter.py +0 -29
  441. test/test_wobble.py +0 -133
  442. test/test_zingl.py +0 -124
  443. {test → meerk40t/cylinder}/__init__.py +0 -0
  444. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  445. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.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
+ """