meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7020__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. meerk40t/__init__.py +1 -1
  2. meerk40t/balormk/balor_params.py +167 -167
  3. meerk40t/balormk/clone_loader.py +457 -457
  4. meerk40t/balormk/controller.py +1566 -1512
  5. meerk40t/balormk/cylindermod.py +64 -0
  6. meerk40t/balormk/device.py +966 -1959
  7. meerk40t/balormk/driver.py +778 -591
  8. meerk40t/balormk/galvo_commands.py +1194 -0
  9. meerk40t/balormk/gui/balorconfig.py +237 -111
  10. meerk40t/balormk/gui/balorcontroller.py +191 -184
  11. meerk40t/balormk/gui/baloroperationproperties.py +116 -115
  12. meerk40t/balormk/gui/corscene.py +845 -0
  13. meerk40t/balormk/gui/gui.py +179 -147
  14. meerk40t/balormk/livelightjob.py +466 -382
  15. meerk40t/balormk/mock_connection.py +131 -109
  16. meerk40t/balormk/plugin.py +133 -135
  17. meerk40t/balormk/usb_connection.py +306 -301
  18. meerk40t/camera/__init__.py +1 -1
  19. meerk40t/camera/camera.py +514 -397
  20. meerk40t/camera/gui/camerapanel.py +1241 -1095
  21. meerk40t/camera/gui/gui.py +58 -58
  22. meerk40t/camera/plugin.py +441 -399
  23. meerk40t/ch341/__init__.py +27 -27
  24. meerk40t/ch341/ch341device.py +628 -628
  25. meerk40t/ch341/libusb.py +595 -589
  26. meerk40t/ch341/mock.py +171 -171
  27. meerk40t/ch341/windriver.py +157 -157
  28. meerk40t/constants.py +13 -0
  29. meerk40t/core/__init__.py +1 -1
  30. meerk40t/core/bindalias.py +550 -539
  31. meerk40t/core/core.py +47 -47
  32. meerk40t/core/cutcode/cubiccut.py +73 -73
  33. meerk40t/core/cutcode/cutcode.py +315 -312
  34. meerk40t/core/cutcode/cutgroup.py +141 -137
  35. meerk40t/core/cutcode/cutobject.py +192 -185
  36. meerk40t/core/cutcode/dwellcut.py +37 -37
  37. meerk40t/core/cutcode/gotocut.py +29 -29
  38. meerk40t/core/cutcode/homecut.py +29 -29
  39. meerk40t/core/cutcode/inputcut.py +34 -34
  40. meerk40t/core/cutcode/linecut.py +33 -33
  41. meerk40t/core/cutcode/outputcut.py +34 -34
  42. meerk40t/core/cutcode/plotcut.py +335 -335
  43. meerk40t/core/cutcode/quadcut.py +61 -61
  44. meerk40t/core/cutcode/rastercut.py +168 -148
  45. meerk40t/core/cutcode/waitcut.py +34 -34
  46. meerk40t/core/cutplan.py +1843 -1316
  47. meerk40t/core/drivers.py +330 -329
  48. meerk40t/core/elements/align.py +801 -669
  49. meerk40t/core/elements/branches.py +1858 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4595 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4315 -3617
  54. meerk40t/core/elements/files.py +117 -64
  55. meerk40t/core/elements/geometry.py +473 -224
  56. meerk40t/core/elements/grid.py +467 -316
  57. meerk40t/core/elements/materials.py +158 -94
  58. meerk40t/core/elements/notes.py +50 -38
  59. meerk40t/core/elements/offset_clpr.py +934 -912
  60. meerk40t/core/elements/offset_mk.py +963 -955
  61. meerk40t/core/elements/penbox.py +339 -267
  62. meerk40t/core/elements/placements.py +300 -83
  63. meerk40t/core/elements/render.py +785 -687
  64. meerk40t/core/elements/shapes.py +2618 -2092
  65. meerk40t/core/elements/testcases.py +105 -0
  66. meerk40t/core/elements/trace.py +651 -563
  67. meerk40t/core/elements/tree_commands.py +415 -409
  68. meerk40t/core/elements/undo_redo.py +116 -58
  69. meerk40t/core/elements/wordlist.py +319 -200
  70. meerk40t/core/exceptions.py +9 -9
  71. meerk40t/core/laserjob.py +220 -220
  72. meerk40t/core/logging.py +63 -63
  73. meerk40t/core/node/blobnode.py +83 -86
  74. meerk40t/core/node/bootstrap.py +105 -103
  75. meerk40t/core/node/branch_elems.py +40 -31
  76. meerk40t/core/node/branch_ops.py +45 -38
  77. meerk40t/core/node/branch_regmark.py +48 -41
  78. meerk40t/core/node/cutnode.py +29 -32
  79. meerk40t/core/node/effect_hatch.py +375 -257
  80. meerk40t/core/node/effect_warp.py +398 -0
  81. meerk40t/core/node/effect_wobble.py +441 -309
  82. meerk40t/core/node/elem_ellipse.py +404 -309
  83. meerk40t/core/node/elem_image.py +1082 -801
  84. meerk40t/core/node/elem_line.py +358 -292
  85. meerk40t/core/node/elem_path.py +259 -201
  86. meerk40t/core/node/elem_point.py +129 -102
  87. meerk40t/core/node/elem_polyline.py +310 -246
  88. meerk40t/core/node/elem_rect.py +376 -286
  89. meerk40t/core/node/elem_text.py +445 -418
  90. meerk40t/core/node/filenode.py +59 -40
  91. meerk40t/core/node/groupnode.py +138 -74
  92. meerk40t/core/node/image_processed.py +777 -766
  93. meerk40t/core/node/image_raster.py +156 -113
  94. meerk40t/core/node/layernode.py +31 -31
  95. meerk40t/core/node/mixins.py +135 -107
  96. meerk40t/core/node/node.py +1427 -1304
  97. meerk40t/core/node/nutils.py +117 -114
  98. meerk40t/core/node/op_cut.py +463 -335
  99. meerk40t/core/node/op_dots.py +296 -251
  100. meerk40t/core/node/op_engrave.py +414 -311
  101. meerk40t/core/node/op_image.py +755 -369
  102. meerk40t/core/node/op_raster.py +787 -522
  103. meerk40t/core/node/place_current.py +37 -40
  104. meerk40t/core/node/place_point.py +329 -126
  105. meerk40t/core/node/refnode.py +58 -47
  106. meerk40t/core/node/rootnode.py +225 -219
  107. meerk40t/core/node/util_console.py +48 -48
  108. meerk40t/core/node/util_goto.py +84 -65
  109. meerk40t/core/node/util_home.py +61 -61
  110. meerk40t/core/node/util_input.py +102 -102
  111. meerk40t/core/node/util_output.py +102 -102
  112. meerk40t/core/node/util_wait.py +65 -65
  113. meerk40t/core/parameters.py +709 -707
  114. meerk40t/core/planner.py +875 -785
  115. meerk40t/core/plotplanner.py +656 -652
  116. meerk40t/core/space.py +120 -113
  117. meerk40t/core/spoolers.py +706 -705
  118. meerk40t/core/svg_io.py +1836 -1549
  119. meerk40t/core/treeop.py +534 -445
  120. meerk40t/core/undos.py +278 -124
  121. meerk40t/core/units.py +784 -680
  122. meerk40t/core/view.py +393 -322
  123. meerk40t/core/webhelp.py +62 -62
  124. meerk40t/core/wordlist.py +513 -504
  125. meerk40t/cylinder/cylinder.py +247 -0
  126. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  127. meerk40t/cylinder/gui/gui.py +24 -0
  128. meerk40t/device/__init__.py +1 -1
  129. meerk40t/device/basedevice.py +322 -123
  130. meerk40t/device/devicechoices.py +50 -0
  131. meerk40t/device/dummydevice.py +163 -128
  132. meerk40t/device/gui/defaultactions.py +618 -602
  133. meerk40t/device/gui/effectspanel.py +114 -0
  134. meerk40t/device/gui/formatterpanel.py +253 -290
  135. meerk40t/device/gui/warningpanel.py +337 -260
  136. meerk40t/device/mixins.py +13 -13
  137. meerk40t/dxf/__init__.py +1 -1
  138. meerk40t/dxf/dxf_io.py +766 -554
  139. meerk40t/dxf/plugin.py +47 -35
  140. meerk40t/external_plugins.py +79 -79
  141. meerk40t/external_plugins_build.py +28 -28
  142. meerk40t/extra/cag.py +112 -116
  143. meerk40t/extra/coolant.py +403 -0
  144. meerk40t/extra/encode_detect.py +204 -0
  145. meerk40t/extra/ezd.py +1165 -1165
  146. meerk40t/extra/hershey.py +834 -340
  147. meerk40t/extra/imageactions.py +322 -316
  148. meerk40t/extra/inkscape.py +628 -622
  149. meerk40t/extra/lbrn.py +424 -424
  150. meerk40t/extra/outerworld.py +283 -0
  151. meerk40t/extra/param_functions.py +1542 -1556
  152. meerk40t/extra/potrace.py +257 -253
  153. meerk40t/extra/serial_exchange.py +118 -0
  154. meerk40t/extra/updater.py +602 -453
  155. meerk40t/extra/vectrace.py +147 -146
  156. meerk40t/extra/winsleep.py +83 -83
  157. meerk40t/extra/xcs_reader.py +597 -0
  158. meerk40t/fill/fills.py +781 -335
  159. meerk40t/fill/patternfill.py +1061 -1061
  160. meerk40t/fill/patterns.py +614 -567
  161. meerk40t/grbl/control.py +87 -87
  162. meerk40t/grbl/controller.py +990 -903
  163. meerk40t/grbl/device.py +1084 -768
  164. meerk40t/grbl/driver.py +989 -771
  165. meerk40t/grbl/emulator.py +532 -497
  166. meerk40t/grbl/gcodejob.py +783 -767
  167. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  168. meerk40t/grbl/gui/grblcontroller.py +485 -271
  169. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  170. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  171. meerk40t/grbl/gui/gui.py +147 -116
  172. meerk40t/grbl/interpreter.py +44 -44
  173. meerk40t/grbl/loader.py +22 -22
  174. meerk40t/grbl/mock_connection.py +56 -56
  175. meerk40t/grbl/plugin.py +294 -264
  176. meerk40t/grbl/serial_connection.py +93 -88
  177. meerk40t/grbl/tcp_connection.py +81 -79
  178. meerk40t/grbl/ws_connection.py +112 -0
  179. meerk40t/gui/__init__.py +1 -1
  180. meerk40t/gui/about.py +2042 -296
  181. meerk40t/gui/alignment.py +1644 -1608
  182. meerk40t/gui/autoexec.py +199 -0
  183. meerk40t/gui/basicops.py +791 -670
  184. meerk40t/gui/bufferview.py +77 -71
  185. meerk40t/gui/busy.py +232 -133
  186. meerk40t/gui/choicepropertypanel.py +1662 -1469
  187. meerk40t/gui/consolepanel.py +706 -542
  188. meerk40t/gui/devicepanel.py +687 -581
  189. meerk40t/gui/dialogoptions.py +110 -107
  190. meerk40t/gui/executejob.py +316 -306
  191. meerk40t/gui/fonts.py +90 -90
  192. meerk40t/gui/functionwrapper.py +252 -0
  193. meerk40t/gui/gui_mixins.py +729 -0
  194. meerk40t/gui/guicolors.py +205 -182
  195. meerk40t/gui/help_assets/help_assets.py +218 -201
  196. meerk40t/gui/helper.py +154 -0
  197. meerk40t/gui/hersheymanager.py +1440 -846
  198. meerk40t/gui/icons.py +3422 -2747
  199. meerk40t/gui/imagesplitter.py +555 -508
  200. meerk40t/gui/keymap.py +354 -344
  201. meerk40t/gui/laserpanel.py +897 -806
  202. meerk40t/gui/laserrender.py +1470 -1232
  203. meerk40t/gui/lasertoolpanel.py +805 -793
  204. meerk40t/gui/magnetoptions.py +436 -0
  205. meerk40t/gui/materialmanager.py +2944 -0
  206. meerk40t/gui/materialtest.py +1722 -1694
  207. meerk40t/gui/mkdebug.py +646 -359
  208. meerk40t/gui/mwindow.py +163 -140
  209. meerk40t/gui/navigationpanels.py +2605 -2467
  210. meerk40t/gui/notes.py +143 -142
  211. meerk40t/gui/opassignment.py +414 -410
  212. meerk40t/gui/operation_info.py +310 -299
  213. meerk40t/gui/plugin.py +500 -328
  214. meerk40t/gui/position.py +714 -669
  215. meerk40t/gui/preferences.py +901 -650
  216. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  217. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  218. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  219. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  220. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  221. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  222. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  223. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  224. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  225. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  226. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  227. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  228. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  229. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  230. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  231. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  232. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  233. meerk40t/gui/propertypanels/textproperty.py +770 -755
  234. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  235. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  236. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  237. meerk40t/gui/ribbon.py +2471 -2210
  238. meerk40t/gui/scene/scene.py +1100 -1051
  239. meerk40t/gui/scene/sceneconst.py +22 -22
  240. meerk40t/gui/scene/scenepanel.py +439 -349
  241. meerk40t/gui/scene/scenespacewidget.py +365 -365
  242. meerk40t/gui/scene/widget.py +518 -505
  243. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  244. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  245. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  246. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  247. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  248. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  249. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  250. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  251. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  252. meerk40t/gui/scenewidgets/rectselectwidget.py +592 -346
  253. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  254. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  255. meerk40t/gui/scenewidgets/selectionwidget.py +2958 -2756
  256. meerk40t/gui/simpleui.py +362 -333
  257. meerk40t/gui/simulation.py +2451 -2094
  258. meerk40t/gui/snapoptions.py +208 -203
  259. meerk40t/gui/spoolerpanel.py +1227 -1180
  260. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  261. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  262. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  263. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  264. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  265. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  266. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  267. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  268. meerk40t/gui/themes.py +200 -78
  269. meerk40t/gui/tips.py +590 -0
  270. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  271. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  272. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  273. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  274. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  275. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  276. meerk40t/gui/toolwidgets/toolline.py +39 -144
  277. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  278. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  279. meerk40t/gui/toolwidgets/toolmeasure.py +163 -216
  280. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  281. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  282. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  283. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  284. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  285. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  286. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  287. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  288. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  289. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  290. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  291. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  292. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  293. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  294. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  295. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  296. meerk40t/gui/usbconnect.py +98 -91
  297. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  298. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  299. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  300. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  301. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  302. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  303. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  304. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  305. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  306. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  307. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  308. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  309. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  310. meerk40t/gui/wordlisteditor.py +985 -931
  311. meerk40t/gui/wxmeerk40t.py +1447 -1169
  312. meerk40t/gui/wxmmain.py +5644 -4112
  313. meerk40t/gui/wxmribbon.py +1591 -1076
  314. meerk40t/gui/wxmscene.py +1631 -1453
  315. meerk40t/gui/wxmtree.py +2416 -2089
  316. meerk40t/gui/wxutils.py +1769 -1099
  317. meerk40t/gui/zmatrix.py +102 -102
  318. meerk40t/image/__init__.py +1 -1
  319. meerk40t/image/dither.py +429 -0
  320. meerk40t/image/imagetools.py +2793 -2269
  321. meerk40t/internal_plugins.py +150 -130
  322. meerk40t/kernel/__init__.py +63 -12
  323. meerk40t/kernel/channel.py +259 -212
  324. meerk40t/kernel/context.py +538 -538
  325. meerk40t/kernel/exceptions.py +41 -41
  326. meerk40t/kernel/functions.py +463 -414
  327. meerk40t/kernel/jobs.py +100 -100
  328. meerk40t/kernel/kernel.py +3828 -3571
  329. meerk40t/kernel/lifecycles.py +71 -71
  330. meerk40t/kernel/module.py +49 -49
  331. meerk40t/kernel/service.py +147 -147
  332. meerk40t/kernel/settings.py +383 -343
  333. meerk40t/lihuiyu/controller.py +883 -876
  334. meerk40t/lihuiyu/device.py +1181 -1069
  335. meerk40t/lihuiyu/driver.py +1466 -1372
  336. meerk40t/lihuiyu/gui/gui.py +127 -106
  337. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  338. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  339. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  340. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  341. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  342. meerk40t/lihuiyu/interpreter.py +53 -53
  343. meerk40t/lihuiyu/laserspeed.py +450 -450
  344. meerk40t/lihuiyu/loader.py +90 -90
  345. meerk40t/lihuiyu/parser.py +404 -404
  346. meerk40t/lihuiyu/plugin.py +101 -102
  347. meerk40t/lihuiyu/tcp_connection.py +111 -109
  348. meerk40t/main.py +231 -165
  349. meerk40t/moshi/builder.py +788 -781
  350. meerk40t/moshi/controller.py +505 -499
  351. meerk40t/moshi/device.py +495 -442
  352. meerk40t/moshi/driver.py +862 -696
  353. meerk40t/moshi/gui/gui.py +78 -76
  354. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  355. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  356. meerk40t/moshi/plugin.py +43 -43
  357. meerk40t/network/console_server.py +140 -57
  358. meerk40t/network/kernelserver.py +10 -9
  359. meerk40t/network/tcp_server.py +142 -140
  360. meerk40t/network/udp_server.py +103 -77
  361. meerk40t/network/web_server.py +404 -0
  362. meerk40t/newly/controller.py +1158 -1144
  363. meerk40t/newly/device.py +874 -732
  364. meerk40t/newly/driver.py +540 -412
  365. meerk40t/newly/gui/gui.py +219 -188
  366. meerk40t/newly/gui/newlyconfig.py +116 -101
  367. meerk40t/newly/gui/newlycontroller.py +193 -186
  368. meerk40t/newly/gui/operationproperties.py +51 -51
  369. meerk40t/newly/mock_connection.py +82 -82
  370. meerk40t/newly/newly_params.py +56 -56
  371. meerk40t/newly/plugin.py +1214 -1246
  372. meerk40t/newly/usb_connection.py +322 -322
  373. meerk40t/rotary/gui/gui.py +52 -46
  374. meerk40t/rotary/gui/rotarysettings.py +240 -232
  375. meerk40t/rotary/rotary.py +202 -98
  376. meerk40t/ruida/control.py +291 -91
  377. meerk40t/ruida/controller.py +138 -1088
  378. meerk40t/ruida/device.py +676 -231
  379. meerk40t/ruida/driver.py +534 -472
  380. meerk40t/ruida/emulator.py +1494 -1491
  381. meerk40t/ruida/exceptions.py +4 -4
  382. meerk40t/ruida/gui/gui.py +71 -76
  383. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  384. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  385. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  386. meerk40t/ruida/loader.py +54 -52
  387. meerk40t/ruida/mock_connection.py +57 -109
  388. meerk40t/ruida/plugin.py +124 -87
  389. meerk40t/ruida/rdjob.py +2084 -945
  390. meerk40t/ruida/serial_connection.py +116 -0
  391. meerk40t/ruida/tcp_connection.py +146 -0
  392. meerk40t/ruida/udp_connection.py +73 -0
  393. meerk40t/svgelements.py +9671 -9669
  394. meerk40t/tools/driver_to_path.py +584 -579
  395. meerk40t/tools/geomstr.py +5583 -4680
  396. meerk40t/tools/jhfparser.py +357 -292
  397. meerk40t/tools/kerftest.py +904 -890
  398. meerk40t/tools/livinghinges.py +1168 -1033
  399. meerk40t/tools/pathtools.py +987 -949
  400. meerk40t/tools/pmatrix.py +234 -0
  401. meerk40t/tools/pointfinder.py +942 -942
  402. meerk40t/tools/polybool.py +941 -940
  403. meerk40t/tools/rasterplotter.py +1660 -547
  404. meerk40t/tools/shxparser.py +1047 -901
  405. meerk40t/tools/ttfparser.py +726 -446
  406. meerk40t/tools/zinglplotter.py +595 -593
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +21 -21
  408. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +150 -139
  409. meerk40t-0.9.7020.dist-info/RECORD +446 -0
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +1 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -1
  412. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +1 -1
  413. meerk40t/balormk/elementlightjob.py +0 -159
  414. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  415. test/bootstrap.py +0 -63
  416. test/test_cli.py +0 -12
  417. test/test_core_cutcode.py +0 -418
  418. test/test_core_elements.py +0 -144
  419. test/test_core_plotplanner.py +0 -397
  420. test/test_core_viewports.py +0 -312
  421. test/test_drivers_grbl.py +0 -108
  422. test/test_drivers_lihuiyu.py +0 -443
  423. test/test_drivers_newly.py +0 -113
  424. test/test_element_degenerate_points.py +0 -43
  425. test/test_elements_classify.py +0 -97
  426. test/test_elements_penbox.py +0 -22
  427. test/test_file_svg.py +0 -176
  428. test/test_fill.py +0 -155
  429. test/test_geomstr.py +0 -1523
  430. test/test_geomstr_nodes.py +0 -18
  431. test/test_imagetools_actualize.py +0 -306
  432. test/test_imagetools_wizard.py +0 -258
  433. test/test_kernel.py +0 -200
  434. test/test_laser_speeds.py +0 -3303
  435. test/test_length.py +0 -57
  436. test/test_lifecycle.py +0 -66
  437. test/test_operations.py +0 -251
  438. test/test_operations_hatch.py +0 -57
  439. test/test_ruida.py +0 -19
  440. test/test_spooler.py +0 -22
  441. test/test_tools_rasterplotter.py +0 -29
  442. test/test_wobble.py +0 -133
  443. test/test_zingl.py +0 -124
  444. {test → meerk40t/cylinder}/__init__.py +0 -0
  445. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  446. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/entry_points.txt +0 -0
@@ -1,949 +1,987 @@
1
- from math import isinf, isnan
2
-
3
- from meerk40t.svgelements import Point
4
-
5
-
6
- class GraphNode(Point):
7
- """
8
- GraphNodes are nodes within the graph that store a list of connections between points.
9
- """
10
-
11
- def __init__(self, x, y=None):
12
- Point.__init__(self, x, y)
13
- self.connections = []
14
- self.visited = 0
15
- self.value = None
16
-
17
-
18
- class Segment:
19
- """
20
- Graphing segments are connections between nodes on the graph that store, their start and end nodes, active state
21
- for use within the monotonic vector filling. The type of segment it is. The index of the segment around the closed
22
- shape. A list of bisectors (to calculate the rung attachments).
23
- """
24
-
25
- def __init__(self, a, b, index=0):
26
- self.visited = 0
27
- self.a = a
28
- self.b = b
29
- self.active = False
30
- self.value = "RUNG"
31
- self.index = index
32
- self.bisectors = []
33
- self.object = None
34
-
35
- def __len__(self):
36
- # [False, i, p0, p1, high, low, m, b, path]
37
- return 9
38
-
39
- def __str__(self):
40
- return f"Segment({str(self.a)},{str(self.b)},{str(self.index)},type='{self.value}')"
41
-
42
- def __getitem__(self, item):
43
- if item == 0:
44
- return self.active
45
- if item == 1:
46
- return self.index
47
- if item == 2:
48
- return self.a
49
- if item == 3:
50
- return self.b
51
- if item == 4:
52
- if self.a.y > self.b.y:
53
- return self.a
54
- else:
55
- return self.b
56
- if item == 5:
57
- if self.a.y < self.b.y:
58
- return self.a
59
- else:
60
- return self.b
61
- if item == 6:
62
- if self.b[0] - self.a[0] == 0:
63
- return float("inf")
64
- return (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
65
- if item == 7:
66
- if self.b[0] - self.a[0] == 0:
67
- return float("inf")
68
- im = (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
69
- return self.a[1] - (im * self.a[0])
70
- if item == 8:
71
- return self.object
72
-
73
- def intersect(self, segment):
74
- return Segment.line_intersect(
75
- self.a[0],
76
- self.a[1],
77
- self.b[0],
78
- self.b[1],
79
- segment.a[0],
80
- segment.a[1],
81
- segment.b[0],
82
- segment.b[1],
83
- )
84
-
85
- def sort_bisectors(self):
86
- def distance(a):
87
- return self.a.distance_to(a)
88
-
89
- self.bisectors.sort(key=distance)
90
-
91
- def get_intercept(self, y):
92
- im = (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
93
- ib = self.a[1] - (im * self.a[0])
94
- if isnan(im) or isinf(im):
95
- return self.a[0]
96
- return (y - ib) / im
97
-
98
- @staticmethod
99
- def line_intersect(x1, y1, x2, y2, x3, y3, x4, y4):
100
- denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
101
- if denom == 0:
102
- return None # Parallel.
103
- ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom
104
- ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom
105
- if 0.0 <= ua <= 1.0 and 0.0 <= ub <= 1.0:
106
- return (x1 + ua * (x2 - x1)), (y1 + ua * (y2 - y1))
107
- return None
108
-
109
-
110
- class Graph:
111
- """
112
- A graph is a set of nodes and their connections. The nodes are points within 2d space and any number of segments
113
- can connect any number of points. There is no order established by the graph. And for our uses here all graphs will
114
- end up not only being Eulerian but Euloopian. All nodes should have even numbers of connecting segments so that any
115
- walk will always return to the end location.
116
-
117
- """
118
-
119
- def __init__(self):
120
- self.nodes = []
121
- self.links = []
122
-
123
- def add_shape(self, series, close=True):
124
- """
125
- Adds a closed shape point series to the graph in a single connected vertex path.
126
- """
127
- first_node = None
128
- last_node = None
129
- for i in range(len(series)):
130
- m = series[i]
131
- current_node = self.new_node(m)
132
- if i == 0:
133
- first_node = current_node
134
- if last_node is not None:
135
- segment = self.link(last_node, current_node)
136
- segment.index = i
137
- segment.value = "EDGE"
138
- last_node = current_node
139
- if close:
140
- segment = self.link(last_node, first_node)
141
- segment.index = len(series)
142
- segment.value = "EDGE"
143
-
144
- @staticmethod
145
- def monotone_fill(graph, outlines, min, max, distance):
146
- """
147
- Find all line segments that intersect with the graph segments in the shape outlines. Add the links from right to
148
- left side of the intersected paths. Add the bisectors to the segments that are bisected.
149
-
150
- Sort all the bisectors and create a graph of monotone rungs and the edges that connect those rungs. Use this
151
- graph rather than original outline graph used to find the intersections.
152
-
153
- Adds into graph, a graph of all monotone rungs, and the path of edge nodes that connected those intersections.
154
- """
155
- crawler = VectorMontonizer(low_value=min, high_value=max, start=min)
156
- for outline in outlines:
157
- crawler.add_segment_events(outline.links)
158
- itr = 0
159
- while crawler.current_is_valid_range():
160
- crawler.scanline_increment(distance)
161
- y = crawler.scanline
162
- actives = crawler.actives()
163
- for i in range(1, len(actives), 2):
164
- left_segment = actives[i - 1]
165
- right_segment = actives[i]
166
- left_segment_x = crawler.intercept(left_segment)
167
- right_segment_x = crawler.intercept(right_segment)
168
- left_node = graph.new_node((left_segment_x, y))
169
- right_node = graph.new_node((right_segment_x, y))
170
- row = graph.link(left_node, right_node)
171
- row.value = "RUNG"
172
- row.index = itr
173
- left_segment.bisectors.append(left_node)
174
- right_segment.bisectors.append(right_node)
175
- itr += 1
176
- for outline in outlines:
177
- itr = 0
178
- previous = None
179
- first = None
180
- for i in range(len(outline.links)):
181
- s = outline.links[i]
182
- if len(s.bisectors) == 0:
183
- continue
184
- s.sort_bisectors()
185
- for bi in s.bisectors:
186
- if previous is not None:
187
- segment = graph.link(previous, bi)
188
- segment.value = "EDGE"
189
- segment.index = itr
190
- itr += 1
191
- else:
192
- first = bi
193
- previous = bi
194
- s.bisectors.clear()
195
- if previous is not None and first is not None:
196
- segment = graph.link(previous, first)
197
- segment.value = "EDGE"
198
- segment.index = itr
199
-
200
- def new_node(self, point):
201
- """
202
- Create and add a new node to the graph at the given point.
203
- """
204
- g = GraphNode(point)
205
- self.nodes.append(g)
206
- return g
207
-
208
- def new_edge(self, a, b):
209
- """
210
- Create an edge connection between a and b.
211
- """
212
- s = Segment(a, b)
213
- self.links.append(s)
214
- return s
215
-
216
- def detach(self, segment):
217
- """
218
- Remove the segment and links from the graph.
219
- """
220
- self.links.remove(segment)
221
- segment.a.connections.remove(segment)
222
- segment.b.connections.remove(segment)
223
-
224
- def link(self, a, b):
225
- """
226
- Creates a new edge linking the points a and be and adds the newly created link to the graph.
227
- """
228
- segment = self.new_edge(a, b)
229
- segment.a.connections.append(segment)
230
- segment.b.connections.append(segment)
231
- return segment
232
-
233
- def double(self):
234
- """
235
- Makes any graph Eulerian. Any graph that is doubled is by definition Eulerian.
236
-
237
- This is not used by the algorithm.
238
- @return:
239
- """
240
- for i in range(len(self.links)):
241
- s = self.links[i]
242
- second_copy = self.link(s.a, s.b)
243
- if s.value == "RUNG":
244
- second_copy.value = "SCAFFOLD_RUNG"
245
- else:
246
- second_copy.value = "SCAFFOLD"
247
- second_copy.index = None
248
-
249
- def double_odd_edge(self):
250
- """
251
- Makes any outline path an Eularian path, by doubling every other edge. As each node connects with 1 rung, and
252
- two edges this will double 1 of those edges in every instance, giving a total of 4 connections. This is makes
253
- the graph Eulerian.
254
-
255
- @return:
256
- """
257
- for i in range(len(self.links)):
258
- segment = self.links[i]
259
- if segment.value == "EDGE" and segment.index & 1:
260
- second_copy = self.link(segment.a, segment.b)
261
- second_copy.value = "SCAFFOLD"
262
- second_copy.index = None
263
-
264
- def walk(self, points):
265
- """
266
- We have an Eulerian graph we must walk through the graph in any direction. This results in a point series that
267
- will cross every segment once.
268
-
269
- Some segments are marked scaffolding or classes of edge that are not necessary. These are removed for parsimony.
270
-
271
- """
272
- if len(self.nodes) == 0:
273
- return
274
- walker = GraphWalker(self)
275
- walker.make_walk()
276
- walker.clip_scaffold_ends()
277
- walker.clip_scaffold_loops()
278
- walker.add_walk(points)
279
- return points
280
-
281
- def is_eulerian(self):
282
- ends = 0
283
- for n in self.nodes:
284
- if len(n.connections) & 1:
285
- ends += 1
286
- if ends > 2:
287
- return False
288
- return True
289
-
290
- def is_euloopian(self):
291
- for n in self.nodes:
292
- if len(n.connections) & 1:
293
- return False
294
- return True
295
-
296
-
297
- class GraphWalker:
298
- """
299
- Graph Walker takes a graph object and finds walks within it.
300
-
301
- If the graph is discontinuous it will find no segment between these elements and add a None segment between them.
302
- """
303
-
304
- def __init__(self, graph):
305
- self.graph = graph
306
- self.walk = list()
307
- self.flip_start = None
308
- self.flip_end = None
309
-
310
- def other_node_for_segment(self, current_node, next_segment):
311
- """
312
- Segments have two nodes, this finds the other side of the given segment.
313
- """
314
- if current_node is next_segment.a:
315
- return next_segment.b
316
- else:
317
- return next_segment.a
318
-
319
- def reset_visited(self):
320
- for e in self.walk:
321
- if e is None:
322
- continue
323
- e.visited = 0
324
-
325
- def make_walk(self):
326
- """
327
- Create the walk out of the current graph. Picks any start point and begins. Note if there
328
- are odd node elements anywhere
329
- """
330
- itr = 0
331
- for g in self.graph.nodes:
332
- if not g.visited:
333
- if itr != 0:
334
- self.walk.append(None) # Segment is None. There is no link here.
335
- self.make_walk_node(g)
336
- itr += 1
337
-
338
- def make_walk_node(self, g):
339
- """
340
- Starting from the given start node it makes a complete walk in an Eulerian circuit.
341
-
342
- It adds the first loop from the start node, then walks its looped walk adding
343
- any additional loops it finds to the current loop.
344
- @param g:
345
- @return:
346
- """
347
- start = len(self.walk)
348
- self.walk.append(g)
349
- self.add_loop(start, g)
350
-
351
- i = start
352
- while i < len(self.walk):
353
- node = self.walk[i]
354
- unused = self.find_unused_connection(node)
355
- if unused is None:
356
- i += 2
357
- continue
358
- self.add_loop(i, node)
359
- # i += 2
360
-
361
- def add_loop(self, index, node):
362
- """
363
- Adds a loop from the current graph node, without revisiting any nodes.
364
- Returns the altered index caused by adding that loop.
365
-
366
- Travels along unused connections until no more travel is possible. If properly Eulerian,
367
- this will only happen when it is looped back on itself.
368
-
369
- @param index: index we are adding loop to.
370
- @param node: Node to find alternative path through.
371
- @return: new index after loop is added to the walk.
372
- """
373
- index += 1
374
- i = index
375
- while True:
376
- node.visited += 1
377
- unused = self.find_unused_connection(node)
378
- if unused is None:
379
- break
380
- segment = node.connections[unused]
381
- self.walk.insert(i, segment)
382
- i += 1
383
- segment.visited += 1
384
- node = self.other_node_for_segment(node, segment)
385
- self.walk.insert(i, node)
386
- i += 1
387
- return i - index
388
-
389
- def find_unused_connection(self, node):
390
- """
391
- Finds the first unused edge segment within the graph node, or None if all connections are used.
392
-
393
- @param node: Node to find unused edge segment within.
394
- @return: index of node connection within the graphnode
395
- """
396
- value = None
397
- for index, c in enumerate(node.connections):
398
- if not c.visited:
399
- if value is None:
400
- value = index
401
- if c.value == "RUNG":
402
- return index
403
- return value
404
-
405
- def add_walk(self, points):
406
- """
407
- Adds nodes within the walk to the points given to it.
408
-
409
- @param points:
410
- @return:
411
- """
412
- for i in range(0, len(self.walk), 2):
413
- segment = self.walk[i - 1]
414
- # The first time segment will be the last value (a node) which will set value to none. This is fine.
415
- point = self.walk[i]
416
- if segment is None:
417
- points.append(None)
418
- else:
419
- point.value = (
420
- segment.value
421
- ) # This doesn't work, nodes are repeated, so they can't store unique values.
422
- points.append(point)
423
-
424
- def remove_loop(self, from_pos, to_pos):
425
- """
426
- Removes values between the two given points.
427
- Since start and end are the same node, it leaves one in place.
428
-
429
- @param from_pos:
430
- @param to_pos:
431
- @return:
432
- """
433
- if from_pos == to_pos:
434
- return 0
435
- min_pos = min(from_pos, to_pos)
436
- max_pos = max(from_pos, to_pos)
437
- del self.walk[min_pos:max_pos]
438
- return max_pos - min_pos
439
-
440
- def remove_biggest_loop_in_range(self, start, end):
441
- """
442
- Checks scaffolding walk for loops, and removes them if detected.
443
-
444
- It resets the visited values for the scaffold walk.
445
- It iterates from the outside to the center, setting the visited value for each node.
446
-
447
- If it finds a marked node, that is the biggest loop within the given walk.
448
- @param start:
449
- @param end:
450
- @return:
451
- """
452
- for i in range(start, end + 2, 2):
453
- n = self.get_node(i)
454
- n.visited = None
455
- for i in range(0, int((end - start) // 2), 2):
456
- left = start + i
457
- right = end - i
458
- s = self.get_node(left)
459
- if s.visited is not None:
460
- return self.remove_loop(left, s.visited)
461
- # Loop Detected.
462
- if left == right:
463
- break
464
- s.visited = left
465
- e = self.get_node(right)
466
- if e.visited is not None:
467
- return self.remove_loop(right, e.visited)
468
- # Loop Detected.
469
- e.visited = right
470
- return 0
471
-
472
- def clip_scaffold_loops(self):
473
- """
474
- Removes loops consisting of scaffolding from the walk.
475
-
476
- Clips unneeded scaffolding.
477
-
478
- @return:
479
- """
480
- start = 0
481
- index = 0
482
- ie = len(self.walk)
483
- while index < ie:
484
- try:
485
- segment = self.walk[index + 1]
486
- except IndexError:
487
- self.remove_biggest_loop_in_range(start, index)
488
- return
489
- if segment is None or segment.value == "RUNG":
490
- # Segment is essential.
491
- if start != index:
492
- ie -= self.remove_biggest_loop_in_range(start, index)
493
- start = index + 2
494
- index += 2
495
-
496
- def remove_scaffold_ends_in_range(self, start, end):
497
- new_end = end
498
- limit = start + 2
499
- while new_end >= limit:
500
- j_segment = self.walk[new_end - 1]
501
- if j_segment is None or j_segment.value == "RUNG":
502
- if new_end == end:
503
- break
504
- del self.walk[new_end + 1 : end + 1]
505
- end = new_end
506
- break
507
- new_end -= 2
508
- new_start = start
509
- limit = end - 2
510
- while new_start <= limit:
511
- j_segment = self.walk[new_start + 1]
512
- if j_segment is None or j_segment.value == "RUNG":
513
- if new_start == start:
514
- break
515
- del self.walk[start:new_start]
516
- break
517
- new_start += 2
518
-
519
- def clip_scaffold_ends(self):
520
- """Finds contiguous regions, and calls removeScaffoldEnds on that range."""
521
- end = len(self.walk) - 1
522
- index = end
523
- while index >= 0:
524
- try:
525
- segment = self.walk[index - 1]
526
- except IndexError:
527
- self.remove_scaffold_ends_in_range(index, end)
528
- return
529
- if segment is None:
530
- self.remove_scaffold_ends_in_range(index, end)
531
- end = index - 2
532
- index -= 2
533
-
534
- def two_opt(self):
535
- """
536
- Unused
537
- """
538
- v = self.get_value()
539
- while True:
540
- new_value = self.two_opt_cycle(v)
541
- if v == new_value:
542
- break
543
-
544
- def two_opt_cycle(self, value):
545
- """
546
- Unused
547
- """
548
- if len(self.walk) == 0:
549
- return 0
550
- swap_start = 0
551
- walk_end = len(self.walk)
552
- while swap_start < walk_end:
553
- swap_element = self.walk[swap_start]
554
- m = swap_element.visited
555
- swap_end = swap_start + 2
556
- while swap_end < walk_end:
557
- current_element = self.walk[swap_end]
558
- if swap_element == current_element:
559
- m -= 1
560
- self.flip_start = swap_start + 1
561
- self.flip_end = swap_end - 1
562
- new_value = self.get_value()
563
- if new_value > value:
564
- value = new_value
565
- self.walk[swap_start + 1 : swap_end] = self.walk[
566
- swap_start + 1 : swap_end : -1
567
- ] # reverse
568
- else:
569
- self.flip_start = None
570
- self.flip_end = None
571
- if m == 0:
572
- break
573
- swap_end += 2
574
- swap_start += 2
575
- return value
576
-
577
- def get_segment(self, index):
578
- """
579
- Unused
580
- """
581
- if (
582
- self.flip_start is not None
583
- and self.flip_end is not None
584
- and self.flip_start <= index <= self.flip_end
585
- ):
586
- return self.walk[self.flip_end - (index - self.flip_start)]
587
- return self.walk[index]
588
-
589
- def get_node(self, index):
590
- """
591
- Unused
592
- """
593
- if (
594
- self.flip_start is not None
595
- and self.flip_end is not None
596
- and self.flip_start <= index <= self.flip_end
597
- ):
598
- return self.walk[self.flip_end - (index - self.flip_start)]
599
- try:
600
- return self.walk[index]
601
- except IndexError:
602
- return None
603
-
604
- def get_value(self):
605
- """
606
- Path values with flip.
607
- @return: Flipped path value.
608
- """
609
- if len(self.walk) == 0:
610
- return 0
611
- value = 0
612
- start = 0
613
- end = len(self.walk) - 1
614
- while start < end:
615
- i_segment = self.get_segment(start + 1)
616
- if i_segment.value == "RUNG":
617
- break
618
- start += 2
619
- while end >= 2:
620
- i_segment = self.get_segment(end - 1)
621
- if i_segment.value == "RUNG":
622
- break
623
- end -= 2
624
- j = start
625
- while j < end:
626
- j_node = self.get_node(j)
627
- j += 1
628
- j_segment = self.get_segment(j)
629
- j += 1
630
- if j_segment.value != "RUNG":
631
- # if the node connector is not critical, try to find and skip a loop
632
- k = j
633
- while k < end:
634
- k_node = self.get_node(k)
635
- k += 1
636
- k_segment = self.get_segment(k)
637
- k += 1
638
- if k_segment.value == "RUNG":
639
- break
640
- if k_node == j_node:
641
- # Only skippable nodes existed before returned to original node, so skip that loop.
642
- value += (k - j) * 10
643
- j = k
644
- j_segment = k_segment
645
- break
646
- if j_segment.value == "SCAFFOLD":
647
- value -= j_segment.a.distance_sq(j_segment.b)
648
- elif j_segment.value == "RUNG":
649
- value -= j_segment.a.distance_sq(j_segment.b)
650
- return value
651
-
652
-
653
- class VectorMontonizer:
654
- """
655
- Sorts all segments according to their highest y values. Steps through the values in order
656
- each step activates and deactivates the segments that are encountered such that it always has a list
657
- of active segments. Sorting the active segments according to their x-intercepts gives a list of all
658
- points that a ray would strike passing through that shape. Every other such area is filled. These are
659
- given rungs, and connected to intercept points.
660
- """
661
-
662
- def __init__(
663
- self, low_value=-float("inf"), high_value=float("inf"), start=-float("inf")
664
- ):
665
- self._event_index = 0
666
- self._events = []
667
- self._dirty_event_sort = True
668
-
669
- self._actives = []
670
- self._dirty_actives_sort = True
671
-
672
- self._dirty_scanline = True
673
-
674
- self.scanline = start
675
- self.valid_low = low_value
676
- self.valid_high = high_value
677
-
678
- self.scanbeam_low = float("inf")
679
- self.scanbeam_high = -float("inf")
680
-
681
- def add_segment_events(self, links):
682
- """
683
- Add segment to be processed. This segment should already exist and have the correct type
684
- @param links:
685
- @return:
686
- """
687
- self._dirty_scanline = True
688
- self._dirty_event_sort = True
689
- self._dirty_actives_sort = True
690
- for s in links:
691
- self._events.append((s[4].y, s)) # High
692
- self._events.append((s[5].y, s)) # Low
693
-
694
- def add_polyline(self, path):
695
- """
696
- Add segments in the form of a connected path. These positions are read and segments are created for these
697
- points.
698
-
699
- @param path:
700
- @return:
701
- """
702
- self._dirty_scanline = True
703
- self._dirty_event_sort = True
704
- self._dirty_actives_sort = True
705
- for i in range(len(path) - 1):
706
- p0 = path[i]
707
- p1 = path[i + 1]
708
- if p0.y > p1.y:
709
- high = p0
710
- low = p1
711
- else:
712
- high = p1
713
- low = p0
714
-
715
- # b = low.y - (m * low.x)
716
- if self.valid_low > high.y:
717
- # Cluster before range.
718
- continue
719
- if self.valid_high < low.y:
720
- # Cluster after range.
721
- continue
722
- seg = Segment(p0, p1)
723
- # cluster = [False, i, p0, p1, high, low, m, b, path]
724
- if self.valid_low < low.y:
725
- self._events.append((low.y, seg))
726
- if self.valid_high > high.y:
727
- self._events.append((high.y, seg))
728
- if high.y >= self.scanline >= low.y:
729
- seg.active = True
730
- self._actives.append(seg)
731
-
732
- def current_is_valid_range(self):
733
- return self.valid_high >= self.scanline >= self.valid_low
734
-
735
- def scanline_increment(self, delta):
736
- self.scanline_to(self.scanline + delta)
737
- self._sort_actives()
738
- return self.current_is_valid_range()
739
-
740
- def scanline_to(self, scan):
741
- """
742
- Move the scanline to the scan position.
743
- @param scan:
744
- @return:
745
- """
746
- self._dirty_actives_sort = True
747
- self._sort_events()
748
- self._find_scanbeam()
749
-
750
- while self._below_scanbeam(scan):
751
- c = self.scanbeam_higher()
752
- if c.active:
753
- c.active = False
754
- self._actives.remove(c)
755
- else:
756
- c.active = True
757
- self._actives.append(c)
758
-
759
- while self._above_scanbeam(scan):
760
- c = self.scanbeam_lower()
761
- if c.active:
762
- c.active = False
763
- self._actives.remove(c)
764
- else:
765
- c.active = True
766
- self._actives.append(c)
767
-
768
- self.scanline = scan
769
-
770
- def is_point_inside(self, x, y, tolerance=0):
771
- """
772
- Determine if the x/y point is with the segments of a closed shape polygon.
773
-
774
- This assumes that add_polyline added a closed point class.
775
- @param x: x location of point
776
- @param y: y location of point
777
- @param tolerance: wiggle room
778
- @return:
779
- """
780
- self.scanline_to(y)
781
- self._sort_actives()
782
- for i in range(1, len(self._actives), 2):
783
- prior = self._actives[i - 1]
784
- after = self._actives[i]
785
- if (
786
- self.intercept(prior, y) - tolerance
787
- <= x
788
- <= self.intercept(after, y) + tolerance
789
- ):
790
- return True
791
- return False
792
-
793
- def actives(self):
794
- """
795
- Get the active list at the current scanline.
796
-
797
- @return:
798
- """
799
- self._sort_actives()
800
- return self._actives
801
-
802
- def event_range(self):
803
- """
804
- Returns the range of events from the lowest to the highest in y-value.
805
-
806
- @return:
807
- """
808
- if len(self._events) == 0:
809
- return None, None
810
- self._sort_events()
811
- y_min = self._events[0][0]
812
- y_max = self._events[-1][0]
813
- return y_min, y_max
814
-
815
- def _sort_events(self):
816
- if not self._dirty_event_sort:
817
- return
818
- self._events.sort(key=lambda e: e[0])
819
- self._dirty_event_sort = False
820
-
821
- def _sort_actives(self):
822
- if not self._dirty_actives_sort:
823
- return
824
- self._actives.sort(key=self.intercept)
825
- self._dirty_actives_sort = False
826
-
827
- def intercept(self, e, y=None):
828
- if y is None:
829
- y = self.scanline
830
- m = e[6]
831
- b = e[7]
832
- if isnan(m) or isinf(m):
833
- low = e[5]
834
- return low.x
835
- return (y - b) / m
836
-
837
- def _find_scanbeam(self):
838
- if not self._dirty_scanline:
839
- return
840
- self._dirty_scanline = False
841
- self._sort_events()
842
-
843
- self._event_index = -1
844
- self.scanbeam_high = -float("inf")
845
- self.scanbeam_lower()
846
-
847
- while self._above_scanbeam(self.scanline):
848
- self.scanbeam_lower()
849
-
850
- def within_scanbeam(self, v):
851
- """
852
- Is the value within the current scanbeam?
853
- @param v:
854
- @return:
855
- """
856
-
857
- return not self._below_scanbeam(v) and not self._above_scanbeam(v)
858
-
859
- def _below_scanbeam(self, v):
860
- """
861
- Is the value below the current scanbeam?
862
- @param v:
863
- @return:
864
- """
865
- return v < self.scanbeam_low
866
-
867
- def _above_scanbeam(self, v):
868
- """
869
- Is the value above the current scanbeam?
870
-
871
- @param v:
872
- @return:
873
- """
874
- return v > self.scanbeam_high
875
-
876
- def scanbeam_lower(self):
877
- """
878
- Move the scanbeam lower through the events.
879
-
880
- @return:
881
- """
882
- self._event_index += 1
883
- self.scanbeam_low = self.scanbeam_high
884
- if self._event_index < len(self._events):
885
- self.scanbeam_high = self._events[self._event_index][0]
886
- else:
887
- self.scanbeam_high = float("inf")
888
- if self._event_index > 0:
889
- return self._events[self._event_index - 1][1]
890
- else:
891
- return None
892
-
893
- def scanbeam_higher(self):
894
- """
895
- Move the scanbeam higher in the events.
896
-
897
- @return:
898
- """
899
- self._event_index -= 1
900
- self.scanbeam_high = self.scanbeam_low
901
- if self._event_index > 0:
902
- self.scanbeam_low = self._events[self._event_index - 1][0]
903
- else:
904
- self.scanbeam_low = -float("inf")
905
- return self._events[self._event_index][1]
906
-
907
-
908
- class EulerianFill:
909
- """Eulerian fill given some outline shapes, creates a fill."""
910
-
911
- def __init__(self, distance):
912
- self.distance = distance
913
- self.outlines = []
914
-
915
- def __iadd__(self, other):
916
- self.outlines.append(other)
917
- return self
918
-
919
- def estimate(self):
920
- min_y = float("inf")
921
- max_y = -float("inf")
922
- for outline in self.outlines:
923
- o_min_y = min([p[1] for p in outline])
924
- o_max_y = max([p[1] for p in outline])
925
- min_y = min(min_y, o_min_y)
926
- max_y = max(max_y, o_max_y)
927
- try:
928
- return (max_y - min_y) / self.distance
929
- except ZeroDivisionError:
930
- return float("inf")
931
-
932
- def get_fill(self):
933
- min_y = float("inf")
934
- max_y = -float("inf")
935
- outline_graphs = list()
936
- for outline in self.outlines:
937
- outline_graph = Graph()
938
- outline_graph.add_shape(outline, True)
939
- o_min_y = min([p[1] for p in outline])
940
- o_max_y = max([p[1] for p in outline])
941
- min_y = min(min_y, o_min_y)
942
- max_y = max(max_y, o_max_y)
943
- outline_graphs.append(outline_graph)
944
- graph = Graph()
945
- Graph.monotone_fill(graph, outline_graphs, min_y, max_y, self.distance)
946
- graph.double_odd_edge()
947
- walk = list()
948
- graph.walk(walk)
949
- return walk
1
+ from math import isinf, isnan
2
+
3
+ from meerk40t.svgelements import Point
4
+
5
+
6
+ class GraphNode(Point):
7
+ """
8
+ GraphNodes are nodes within the graph that store a list of connections between points.
9
+ """
10
+
11
+ def __init__(self, x, y=None):
12
+ Point.__init__(self, x, y)
13
+ self.connections = []
14
+ self.visited = 0
15
+ self.value = None
16
+
17
+
18
+ class Segment:
19
+ """
20
+ Graphing segments are connections between nodes on the graph that store, their start and end nodes, active state
21
+ for use within the monotonic vector filling. The type of segment it is. The index of the segment around the closed
22
+ shape. A list of bisectors (to calculate the rung attachments).
23
+ """
24
+
25
+ def __init__(self, a, b, index=0):
26
+ self.visited = 0
27
+ self.a = a
28
+ self.b = b
29
+ self.active = False
30
+ self.value = "RUNG"
31
+ self.index = index
32
+ self.bisectors = []
33
+ self.object = None
34
+
35
+ def __len__(self):
36
+ # [False, i, p0, p1, high, low, m, b, path]
37
+ return 9
38
+
39
+ def __str__(self):
40
+ return f"Segment({str(self.a)},{str(self.b)},{str(self.index)},type='{self.value}')"
41
+
42
+ def __getitem__(self, item):
43
+ if item == 0:
44
+ return self.active
45
+ if item == 1:
46
+ return self.index
47
+ if item == 2:
48
+ return self.a
49
+ if item == 3:
50
+ return self.b
51
+ if item == 4:
52
+ if self.a.y > self.b.y:
53
+ return self.a
54
+ else:
55
+ return self.b
56
+ if item == 5:
57
+ if self.a.y < self.b.y:
58
+ return self.a
59
+ else:
60
+ return self.b
61
+ if item == 6:
62
+ if self.b[0] - self.a[0] == 0:
63
+ return float("inf")
64
+ return (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
65
+ if item == 7:
66
+ if self.b[0] - self.a[0] == 0:
67
+ return float("inf")
68
+ im = (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
69
+ return self.a[1] - (im * self.a[0])
70
+ if item == 8:
71
+ return self.object
72
+
73
+ def intersect(self, segment):
74
+ return Segment.line_intersect(
75
+ self.a[0],
76
+ self.a[1],
77
+ self.b[0],
78
+ self.b[1],
79
+ segment.a[0],
80
+ segment.a[1],
81
+ segment.b[0],
82
+ segment.b[1],
83
+ )
84
+
85
+ def sort_bisectors(self):
86
+ def distance(a):
87
+ return self.a.distance_to(a)
88
+
89
+ self.bisectors.sort(key=distance)
90
+
91
+ def get_intercept(self, y):
92
+ im = (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
93
+ ib = self.a[1] - (im * self.a[0])
94
+ if isnan(im) or isinf(im):
95
+ return self.a[0]
96
+ return (y - ib) / im
97
+
98
+ @staticmethod
99
+ def line_intersect(x1, y1, x2, y2, x3, y3, x4, y4):
100
+ denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
101
+ if denom == 0:
102
+ return None # Parallel.
103
+ ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom
104
+ ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom
105
+ if 0.0 <= ua <= 1.0 and 0.0 <= ub <= 1.0:
106
+ return (x1 + ua * (x2 - x1)), (y1 + ua * (y2 - y1))
107
+ return None
108
+
109
+
110
+ class Graph:
111
+ """
112
+ A graph is a set of nodes and their connections. The nodes are points within 2d space and any number of segments
113
+ can connect any number of points. There is no order established by the graph. And for our uses here all graphs will
114
+ end up not only being Eulerian but Euloopian. All nodes should have even numbers of connecting segments so that any
115
+ walk will always return to the end location.
116
+
117
+ """
118
+
119
+ def __init__(self):
120
+ self.nodes = []
121
+ self.links = []
122
+
123
+ def add_shape(self, series, close=True):
124
+ """
125
+ Adds a closed shape point series to the graph in a single connected vertex path.
126
+ """
127
+ first_node = None
128
+ last_node = None
129
+ for i in range(len(series)):
130
+ m = series[i]
131
+ current_node = self.new_node(m)
132
+ if i == 0:
133
+ first_node = current_node
134
+ if last_node is not None:
135
+ segment = self.link(last_node, current_node)
136
+ segment.index = i
137
+ segment.value = "EDGE"
138
+ last_node = current_node
139
+ if close:
140
+ segment = self.link(last_node, first_node)
141
+ segment.index = len(series)
142
+ segment.value = "EDGE"
143
+
144
+ @staticmethod
145
+ def monotone_fill(graph, outlines, min, max, distance):
146
+ """
147
+ Find all line segments that intersect with the graph segments in the shape outlines. Add the links from right to
148
+ left side of the intersected paths. Add the bisectors to the segments that are bisected.
149
+
150
+ Sort all the bisectors and create a graph of monotone rungs and the edges that connect those rungs. Use this
151
+ graph rather than original outline graph used to find the intersections.
152
+
153
+ Adds into graph, a graph of all monotone rungs, and the path of edge nodes that connected those intersections.
154
+ """
155
+ crawler = VectorMontonizer(low_value=min, high_value=max, start=min)
156
+ for outline in outlines:
157
+ crawler.add_segment_events(outline.links)
158
+ itr = 0
159
+ while crawler.current_is_valid_range():
160
+ crawler.scanline_increment(distance)
161
+ y = crawler.scanline
162
+ actives = crawler.actives()
163
+ for i in range(1, len(actives), 2):
164
+ left_segment = actives[i - 1]
165
+ right_segment = actives[i]
166
+ left_segment_x = crawler.intercept(left_segment)
167
+ right_segment_x = crawler.intercept(right_segment)
168
+ left_node = graph.new_node((left_segment_x, y))
169
+ right_node = graph.new_node((right_segment_x, y))
170
+ row = graph.link(left_node, right_node)
171
+ row.value = "RUNG"
172
+ row.index = itr
173
+ left_segment.bisectors.append(left_node)
174
+ right_segment.bisectors.append(right_node)
175
+ itr += 1
176
+ for outline in outlines:
177
+ itr = 0
178
+ previous = None
179
+ first = None
180
+ for i in range(len(outline.links)):
181
+ s = outline.links[i]
182
+ if len(s.bisectors) == 0:
183
+ continue
184
+ s.sort_bisectors()
185
+ for bi in s.bisectors:
186
+ if previous is not None:
187
+ segment = graph.link(previous, bi)
188
+ segment.value = "EDGE"
189
+ segment.index = itr
190
+ itr += 1
191
+ else:
192
+ first = bi
193
+ previous = bi
194
+ s.bisectors.clear()
195
+ if previous is not None and first is not None:
196
+ segment = graph.link(previous, first)
197
+ segment.value = "EDGE"
198
+ segment.index = itr
199
+
200
+ def new_node(self, point):
201
+ """
202
+ Create and add a new node to the graph at the given point.
203
+ """
204
+ g = GraphNode(point)
205
+ self.nodes.append(g)
206
+ return g
207
+
208
+ def new_edge(self, a, b):
209
+ """
210
+ Create an edge connection between a and b.
211
+ """
212
+ s = Segment(a, b)
213
+ self.links.append(s)
214
+ return s
215
+
216
+ def detach(self, segment):
217
+ """
218
+ Remove the segment and links from the graph.
219
+ """
220
+ self.links.remove(segment)
221
+ segment.a.connections.remove(segment)
222
+ segment.b.connections.remove(segment)
223
+
224
+ def link(self, a, b):
225
+ """
226
+ Creates a new edge linking the points a and be and adds the newly created link to the graph.
227
+ """
228
+ segment = self.new_edge(a, b)
229
+ segment.a.connections.append(segment)
230
+ segment.b.connections.append(segment)
231
+ return segment
232
+
233
+ def double(self):
234
+ """
235
+ Makes any graph Eulerian. Any graph that is doubled is by definition Eulerian.
236
+
237
+ This is not used by the algorithm.
238
+ @return:
239
+ """
240
+ for i in range(len(self.links)):
241
+ s = self.links[i]
242
+ second_copy = self.link(s.a, s.b)
243
+ if s.value == "RUNG":
244
+ second_copy.value = "SCAFFOLD_RUNG"
245
+ else:
246
+ second_copy.value = "SCAFFOLD"
247
+ second_copy.index = None
248
+
249
+ def double_odd_edge(self):
250
+ """
251
+ Makes any outline path an Eularian path, by doubling every other edge. As each node connects with 1 rung, and
252
+ two edges this will double 1 of those edges in every instance, giving a total of 4 connections. This is makes
253
+ the graph Eulerian.
254
+
255
+ @return:
256
+ """
257
+ for i in range(len(self.links)):
258
+ segment = self.links[i]
259
+ if segment.value == "EDGE" and segment.index & 1:
260
+ second_copy = self.link(segment.a, segment.b)
261
+ second_copy.value = "SCAFFOLD"
262
+ second_copy.index = None
263
+
264
+ def walk(self, points):
265
+ """
266
+ We have an Eulerian graph we must walk through the graph in any direction. This results in a point series that
267
+ will cross every segment once.
268
+
269
+ Some segments are marked scaffolding or classes of edge that are not necessary. These are removed for parsimony.
270
+
271
+ """
272
+ if len(self.nodes) == 0:
273
+ return
274
+ walker = GraphWalker(self)
275
+ walker.make_walk()
276
+ walker.clip_scaffold_ends()
277
+ walker.clip_scaffold_loops()
278
+ walker.add_walk(points)
279
+ return points
280
+
281
+ def is_eulerian(self):
282
+ ends = 0
283
+ for n in self.nodes:
284
+ if len(n.connections) & 1:
285
+ ends += 1
286
+ if ends > 2:
287
+ return False
288
+ return True
289
+
290
+ def is_euloopian(self):
291
+ for n in self.nodes:
292
+ if len(n.connections) & 1:
293
+ return False
294
+ return True
295
+
296
+
297
+ class GraphWalker:
298
+ """
299
+ Graph Walker takes a graph object and finds walks within it.
300
+
301
+ If the graph is discontinuous it will find no segment between these elements and add a None segment between them.
302
+ """
303
+
304
+ def __init__(self, graph):
305
+ self.graph = graph
306
+ self.walk = list()
307
+ self.flip_start = None
308
+ self.flip_end = None
309
+
310
+ def other_node_for_segment(self, current_node, next_segment):
311
+ """
312
+ Segments have two nodes, this finds the other side of the given segment.
313
+ """
314
+ if current_node is next_segment.a:
315
+ return next_segment.b
316
+ else:
317
+ return next_segment.a
318
+
319
+ def reset_visited(self):
320
+ for e in self.walk:
321
+ if e is None:
322
+ continue
323
+ e.visited = 0
324
+
325
+ def make_walk(self):
326
+ """
327
+ Create the walk out of the current graph. Picks any start point and begins. Note if there
328
+ are odd node elements anywhere
329
+ """
330
+ itr = 0
331
+ for g in self.graph.nodes:
332
+ if not g.visited:
333
+ if itr != 0:
334
+ self.walk.append(None) # Segment is None. There is no link here.
335
+ self.make_walk_node(g)
336
+ itr += 1
337
+
338
+ def make_walk_node(self, g):
339
+ """
340
+ Starting from the given start node it makes a complete walk in an Eulerian circuit.
341
+
342
+ It adds the first loop from the start node, then walks its looped walk adding
343
+ any additional loops it finds to the current loop.
344
+ @param g:
345
+ @return:
346
+ """
347
+ start = len(self.walk)
348
+ self.walk.append(g)
349
+ self.add_loop(start, g)
350
+
351
+ i = start
352
+ while i < len(self.walk):
353
+ node = self.walk[i]
354
+ unused = self.find_unused_connection(node)
355
+ if unused is None:
356
+ i += 2
357
+ continue
358
+ self.add_loop(i, node)
359
+ # i += 2
360
+
361
+ def add_loop(self, index, node):
362
+ """
363
+ Adds a loop from the current graph node, without revisiting any nodes.
364
+ Returns the altered index caused by adding that loop.
365
+
366
+ Travels along unused connections until no more travel is possible. If properly Eulerian,
367
+ this will only happen when it is looped back on itself.
368
+
369
+ @param index: index we are adding loop to.
370
+ @param node: Node to find alternative path through.
371
+ @return: new index after loop is added to the walk.
372
+ """
373
+ index += 1
374
+ i = index
375
+ while True:
376
+ node.visited += 1
377
+ unused = self.find_unused_connection(node)
378
+ if unused is None:
379
+ break
380
+ segment = node.connections[unused]
381
+ self.walk.insert(i, segment)
382
+ i += 1
383
+ segment.visited += 1
384
+ node = self.other_node_for_segment(node, segment)
385
+ self.walk.insert(i, node)
386
+ i += 1
387
+ return i - index
388
+
389
+ def find_unused_connection(self, node):
390
+ """
391
+ Finds the first unused edge segment within the graph node, or None if all connections are used.
392
+
393
+ @param node: Node to find unused edge segment within.
394
+ @return: index of node connection within the graphnode
395
+ """
396
+ value = None
397
+ for index, c in enumerate(node.connections):
398
+ if not c.visited:
399
+ if value is None:
400
+ value = index
401
+ if c.value == "RUNG":
402
+ return index
403
+ return value
404
+
405
+ def add_walk(self, points):
406
+ """
407
+ Adds nodes within the walk to the points given to it.
408
+
409
+ @param points:
410
+ @return:
411
+ """
412
+ for i in range(0, len(self.walk), 2):
413
+ segment = self.walk[i - 1]
414
+ # The first time segment will be the last value (a node) which will set value to none. This is fine.
415
+ point = self.walk[i]
416
+ if segment is None:
417
+ points.append(None)
418
+ else:
419
+ point.value = (
420
+ segment.value
421
+ ) # This doesn't work, nodes are repeated, so they can't store unique values.
422
+ points.append(point)
423
+
424
+ def remove_loop(self, from_pos, to_pos):
425
+ """
426
+ Removes values between the two given points.
427
+ Since start and end are the same node, it leaves one in place.
428
+
429
+ @param from_pos:
430
+ @param to_pos:
431
+ @return:
432
+ """
433
+ if from_pos == to_pos:
434
+ return 0
435
+ min_pos = min(from_pos, to_pos)
436
+ max_pos = max(from_pos, to_pos)
437
+ del self.walk[min_pos:max_pos]
438
+ return max_pos - min_pos
439
+
440
+ def remove_biggest_loop_in_range(self, start, end):
441
+ """
442
+ Checks scaffolding walk for loops, and removes them if detected.
443
+
444
+ It resets the visited values for the scaffold walk.
445
+ It iterates from the outside to the center, setting the visited value for each node.
446
+
447
+ If it finds a marked node, that is the biggest loop within the given walk.
448
+ @param start:
449
+ @param end:
450
+ @return:
451
+ """
452
+ for i in range(start, end + 2, 2):
453
+ n = self.get_node(i)
454
+ n.visited = None
455
+ for i in range(0, int((end - start) // 2), 2):
456
+ left = start + i
457
+ right = end - i
458
+ s = self.get_node(left)
459
+ if s.visited is not None:
460
+ return self.remove_loop(left, s.visited)
461
+ # Loop Detected.
462
+ if left == right:
463
+ break
464
+ s.visited = left
465
+ e = self.get_node(right)
466
+ if e.visited is not None:
467
+ return self.remove_loop(right, e.visited)
468
+ # Loop Detected.
469
+ e.visited = right
470
+ return 0
471
+
472
+ def clip_scaffold_loops(self):
473
+ """
474
+ Removes loops consisting of scaffolding from the walk.
475
+
476
+ Clips unneeded scaffolding.
477
+
478
+ @return:
479
+ """
480
+ start = 0
481
+ index = 0
482
+ ie = len(self.walk)
483
+ while index < ie:
484
+ try:
485
+ segment = self.walk[index + 1]
486
+ except IndexError:
487
+ self.remove_biggest_loop_in_range(start, index)
488
+ return
489
+ if segment is None or segment.value == "RUNG":
490
+ # Segment is essential.
491
+ if start != index:
492
+ ie -= self.remove_biggest_loop_in_range(start, index)
493
+ start = index + 2
494
+ index += 2
495
+
496
+ def remove_scaffold_ends_in_range(self, start, end):
497
+ new_end = end
498
+ limit = start + 2
499
+ while new_end >= limit:
500
+ j_segment = self.walk[new_end - 1]
501
+ if j_segment is None or j_segment.value == "RUNG":
502
+ if new_end == end:
503
+ break
504
+ del self.walk[new_end + 1 : end + 1]
505
+ end = new_end
506
+ break
507
+ new_end -= 2
508
+ new_start = start
509
+ limit = end - 2
510
+ while new_start <= limit:
511
+ j_segment = self.walk[new_start + 1]
512
+ if j_segment is None or j_segment.value == "RUNG":
513
+ if new_start == start:
514
+ break
515
+ del self.walk[start:new_start]
516
+ break
517
+ new_start += 2
518
+
519
+ def clip_scaffold_ends(self):
520
+ """Finds contiguous regions, and calls removeScaffoldEnds on that range."""
521
+ end = len(self.walk) - 1
522
+ index = end
523
+ while index >= 0:
524
+ try:
525
+ segment = self.walk[index - 1]
526
+ except IndexError:
527
+ self.remove_scaffold_ends_in_range(index, end)
528
+ return
529
+ if segment is None:
530
+ self.remove_scaffold_ends_in_range(index, end)
531
+ end = index - 2
532
+ index -= 2
533
+
534
+ def two_opt(self):
535
+ """
536
+ Unused
537
+ """
538
+ v = self.get_value()
539
+ while True:
540
+ new_value = self.two_opt_cycle(v)
541
+ if v == new_value:
542
+ break
543
+
544
+ def two_opt_cycle(self, value):
545
+ """
546
+ Unused
547
+ """
548
+ if len(self.walk) == 0:
549
+ return 0
550
+ swap_start = 0
551
+ walk_end = len(self.walk)
552
+ while swap_start < walk_end:
553
+ swap_element = self.walk[swap_start]
554
+ m = swap_element.visited
555
+ swap_end = swap_start + 2
556
+ while swap_end < walk_end:
557
+ current_element = self.walk[swap_end]
558
+ if swap_element == current_element:
559
+ m -= 1
560
+ self.flip_start = swap_start + 1
561
+ self.flip_end = swap_end - 1
562
+ new_value = self.get_value()
563
+ if new_value > value:
564
+ value = new_value
565
+ self.walk[swap_start + 1 : swap_end] = self.walk[
566
+ swap_start + 1 : swap_end : -1
567
+ ] # reverse
568
+ else:
569
+ self.flip_start = None
570
+ self.flip_end = None
571
+ if m == 0:
572
+ break
573
+ swap_end += 2
574
+ swap_start += 2
575
+ return value
576
+
577
+ def get_segment(self, index):
578
+ """
579
+ Unused
580
+ """
581
+ if (
582
+ self.flip_start is not None
583
+ and self.flip_end is not None
584
+ and self.flip_start <= index <= self.flip_end
585
+ ):
586
+ return self.walk[self.flip_end - (index - self.flip_start)]
587
+ return self.walk[index]
588
+
589
+ def get_node(self, index):
590
+ """
591
+ Unused
592
+ """
593
+ if (
594
+ self.flip_start is not None
595
+ and self.flip_end is not None
596
+ and self.flip_start <= index <= self.flip_end
597
+ ):
598
+ return self.walk[self.flip_end - (index - self.flip_start)]
599
+ try:
600
+ return self.walk[index]
601
+ except IndexError:
602
+ return None
603
+
604
+ def get_value(self):
605
+ """
606
+ Path values with flip.
607
+ @return: Flipped path value.
608
+ """
609
+ if len(self.walk) == 0:
610
+ return 0
611
+ value = 0
612
+ start = 0
613
+ end = len(self.walk) - 1
614
+ while start < end:
615
+ i_segment = self.get_segment(start + 1)
616
+ if i_segment.value == "RUNG":
617
+ break
618
+ start += 2
619
+ while end >= 2:
620
+ i_segment = self.get_segment(end - 1)
621
+ if i_segment.value == "RUNG":
622
+ break
623
+ end -= 2
624
+ j = start
625
+ while j < end:
626
+ j_node = self.get_node(j)
627
+ j += 1
628
+ j_segment = self.get_segment(j)
629
+ j += 1
630
+ if j_segment.value != "RUNG":
631
+ # if the node connector is not critical, try to find and skip a loop
632
+ k = j
633
+ while k < end:
634
+ k_node = self.get_node(k)
635
+ k += 1
636
+ k_segment = self.get_segment(k)
637
+ k += 1
638
+ if k_segment.value == "RUNG":
639
+ break
640
+ if k_node == j_node:
641
+ # Only skippable nodes existed before returned to original node, so skip that loop.
642
+ value += (k - j) * 10
643
+ j = k
644
+ j_segment = k_segment
645
+ break
646
+ if j_segment.value == "SCAFFOLD":
647
+ value -= j_segment.a.distance_sq(j_segment.b)
648
+ elif j_segment.value == "RUNG":
649
+ value -= j_segment.a.distance_sq(j_segment.b)
650
+ return value
651
+
652
+
653
+ class VectorMontonizer:
654
+ """
655
+ Sorts all segments according to their highest y values. Steps through the values in order
656
+ each step activates and deactivates the segments that are encountered such that it always has a list
657
+ of active segments. Sorting the active segments according to their x-intercepts gives a list of all
658
+ points that a ray would strike passing through that shape. Every other such area is filled. These are
659
+ given rungs, and connected to intercept points.
660
+ """
661
+
662
+ def __init__(
663
+ self, low_value=-float("inf"), high_value=float("inf"), start=-float("inf")
664
+ ):
665
+ self._event_index = 0
666
+ self._events = []
667
+ self._dirty_event_sort = True
668
+
669
+ self._actives = []
670
+ self._dirty_actives_sort = True
671
+
672
+ self._dirty_scanline = True
673
+
674
+ self.scanline = start
675
+ self.valid_low = low_value
676
+ self.valid_high = high_value
677
+
678
+ self.scanbeam_low = float("inf")
679
+ self.scanbeam_high = -float("inf")
680
+
681
+ def add_segment_events(self, links):
682
+ """
683
+ Add segment to be processed. This segment should already exist and have the correct type
684
+ @param links:
685
+ @return:
686
+ """
687
+ self._dirty_scanline = True
688
+ self._dirty_event_sort = True
689
+ self._dirty_actives_sort = True
690
+ for s in links:
691
+ self._events.append((s[4].y, s)) # High
692
+ self._events.append((s[5].y, s)) # Low
693
+
694
+ def add_polyline(self, path):
695
+ """
696
+ Add segments in the form of a connected path. These positions are read and segments are created for these
697
+ points.
698
+
699
+ @param path:
700
+ @return:
701
+ """
702
+ self._dirty_scanline = True
703
+ self._dirty_event_sort = True
704
+ self._dirty_actives_sort = True
705
+ for i in range(len(path) - 1):
706
+ p0 = path[i]
707
+ p1 = path[i + 1]
708
+ if p0.y > p1.y:
709
+ high = p0
710
+ low = p1
711
+ else:
712
+ high = p1
713
+ low = p0
714
+
715
+ # b = low.y - (m * low.x)
716
+ if self.valid_low > high.y:
717
+ # Cluster before range.
718
+ continue
719
+ if self.valid_high < low.y:
720
+ # Cluster after range.
721
+ continue
722
+ seg = Segment(p0, p1)
723
+ # cluster = [False, i, p0, p1, high, low, m, b, path]
724
+ if self.valid_low < low.y:
725
+ self._events.append((low.y, seg))
726
+ if self.valid_high > high.y:
727
+ self._events.append((high.y, seg))
728
+ if high.y >= self.scanline >= low.y:
729
+ seg.active = True
730
+ self._actives.append(seg)
731
+
732
+ def add_pointlist(self, path):
733
+ """
734
+ Add segments in the form of a connected path. These positions are read and segments are created for these
735
+ points.
736
+
737
+ @param path:
738
+ @return:
739
+ """
740
+ self._dirty_scanline = True
741
+ self._dirty_event_sort = True
742
+ self._dirty_actives_sort = True
743
+ for i in range(len(path) - 1):
744
+ p0 = Point(path[i])
745
+ p1 = Point(path[i + 1])
746
+ if p0.y > p1.y:
747
+ high = p0
748
+ low = p1
749
+ else:
750
+ high = p1
751
+ low = p0
752
+
753
+ # b = low.y - (m * low.x)
754
+ if self.valid_low > high.y:
755
+ # Cluster before range.
756
+ continue
757
+ if self.valid_high < low.y:
758
+ # Cluster after range.
759
+ continue
760
+ seg = Segment(p0, p1)
761
+ # cluster = [False, i, p0, p1, high, low, m, b, path]
762
+ if self.valid_low < low.y:
763
+ self._events.append((low.y, seg))
764
+ if self.valid_high > high.y:
765
+ self._events.append((high.y, seg))
766
+ if high.y >= self.scanline >= low.y:
767
+ seg.active = True
768
+ self._actives.append(seg)
769
+
770
+ def current_is_valid_range(self):
771
+ return self.valid_high >= self.scanline >= self.valid_low
772
+
773
+ def scanline_increment(self, delta):
774
+ self.scanline_to(self.scanline + delta)
775
+ self._sort_actives()
776
+ return self.current_is_valid_range()
777
+
778
+ def scanline_to(self, scan):
779
+ """
780
+ Move the scanline to the scan position.
781
+ @param scan:
782
+ @return:
783
+ """
784
+ self._dirty_actives_sort = True
785
+ self._sort_events()
786
+ self._find_scanbeam()
787
+
788
+ while self._below_scanbeam(scan):
789
+ c = self.scanbeam_higher()
790
+ if c.active:
791
+ c.active = False
792
+ self._actives.remove(c)
793
+ else:
794
+ c.active = True
795
+ self._actives.append(c)
796
+
797
+ while self._above_scanbeam(scan):
798
+ c = self.scanbeam_lower()
799
+ if c.active:
800
+ c.active = False
801
+ self._actives.remove(c)
802
+ else:
803
+ c.active = True
804
+ self._actives.append(c)
805
+
806
+ self.scanline = scan
807
+
808
+ def is_point_inside(self, x, y, tolerance=0):
809
+ """
810
+ Determine if the x/y point is with the segments of a closed shape polygon.
811
+
812
+ This assumes that add_polyline added a closed point class.
813
+ @param x: x location of point
814
+ @param y: y location of point
815
+ @param tolerance: wiggle room
816
+ @return:
817
+ """
818
+ self.scanline_to(y)
819
+ self._sort_actives()
820
+ for i in range(1, len(self._actives), 2):
821
+ prior = self._actives[i - 1]
822
+ after = self._actives[i]
823
+ if (
824
+ self.intercept(prior, y) - tolerance
825
+ <= x
826
+ <= self.intercept(after, y) + tolerance
827
+ ):
828
+ return True
829
+ return False
830
+
831
+ def actives(self):
832
+ """
833
+ Get the active list at the current scanline.
834
+
835
+ @return:
836
+ """
837
+ self._sort_actives()
838
+ return self._actives
839
+
840
+ def event_range(self):
841
+ """
842
+ Returns the range of events from the lowest to the highest in y-value.
843
+
844
+ @return:
845
+ """
846
+ if len(self._events) == 0:
847
+ return None, None
848
+ self._sort_events()
849
+ y_min = self._events[0][0]
850
+ y_max = self._events[-1][0]
851
+ return y_min, y_max
852
+
853
+ def _sort_events(self):
854
+ if not self._dirty_event_sort:
855
+ return
856
+ self._events.sort(key=lambda e: e[0])
857
+ self._dirty_event_sort = False
858
+
859
+ def _sort_actives(self):
860
+ if not self._dirty_actives_sort:
861
+ return
862
+ self._actives.sort(key=self.intercept)
863
+ self._dirty_actives_sort = False
864
+
865
+ def intercept(self, e, y=None):
866
+ if y is None:
867
+ y = self.scanline
868
+ m = e[6]
869
+ b = e[7]
870
+ if isnan(m) or isinf(m):
871
+ low = e[5]
872
+ return low.x
873
+ return (y - b) / m
874
+
875
+ def _find_scanbeam(self):
876
+ if not self._dirty_scanline:
877
+ return
878
+ self._dirty_scanline = False
879
+ self._sort_events()
880
+
881
+ self._event_index = -1
882
+ self.scanbeam_high = -float("inf")
883
+ self.scanbeam_lower()
884
+
885
+ while self._above_scanbeam(self.scanline):
886
+ self.scanbeam_lower()
887
+
888
+ def within_scanbeam(self, v):
889
+ """
890
+ Is the value within the current scanbeam?
891
+ @param v:
892
+ @return:
893
+ """
894
+
895
+ return not self._below_scanbeam(v) and not self._above_scanbeam(v)
896
+
897
+ def _below_scanbeam(self, v):
898
+ """
899
+ Is the value below the current scanbeam?
900
+ @param v:
901
+ @return:
902
+ """
903
+ return v < self.scanbeam_low
904
+
905
+ def _above_scanbeam(self, v):
906
+ """
907
+ Is the value above the current scanbeam?
908
+
909
+ @param v:
910
+ @return:
911
+ """
912
+ return v > self.scanbeam_high
913
+
914
+ def scanbeam_lower(self):
915
+ """
916
+ Move the scanbeam lower through the events.
917
+
918
+ @return:
919
+ """
920
+ self._event_index += 1
921
+ self.scanbeam_low = self.scanbeam_high
922
+ if self._event_index < len(self._events):
923
+ self.scanbeam_high = self._events[self._event_index][0]
924
+ else:
925
+ self.scanbeam_high = float("inf")
926
+ if self._event_index > 0:
927
+ return self._events[self._event_index - 1][1]
928
+ else:
929
+ return None
930
+
931
+ def scanbeam_higher(self):
932
+ """
933
+ Move the scanbeam higher in the events.
934
+
935
+ @return:
936
+ """
937
+ self._event_index -= 1
938
+ self.scanbeam_high = self.scanbeam_low
939
+ if self._event_index > 0:
940
+ self.scanbeam_low = self._events[self._event_index - 1][0]
941
+ else:
942
+ self.scanbeam_low = -float("inf")
943
+ return self._events[self._event_index][1]
944
+
945
+
946
+ class EulerianFill:
947
+ """Eulerian fill given some outline shapes, creates a fill."""
948
+
949
+ def __init__(self, distance):
950
+ self.distance = distance
951
+ self.outlines = []
952
+
953
+ def __iadd__(self, other):
954
+ self.outlines.append(other)
955
+ return self
956
+
957
+ def estimate(self):
958
+ min_y = float("inf")
959
+ max_y = -float("inf")
960
+ for outline in self.outlines:
961
+ o_min_y = min([p[1] for p in outline])
962
+ o_max_y = max([p[1] for p in outline])
963
+ min_y = min(min_y, o_min_y)
964
+ max_y = max(max_y, o_max_y)
965
+ try:
966
+ return (max_y - min_y) / self.distance
967
+ except ZeroDivisionError:
968
+ return float("inf")
969
+
970
+ def get_fill(self):
971
+ min_y = float("inf")
972
+ max_y = -float("inf")
973
+ outline_graphs = list()
974
+ for outline in self.outlines:
975
+ outline_graph = Graph()
976
+ outline_graph.add_shape(outline, True)
977
+ o_min_y = min([p[1] for p in outline])
978
+ o_max_y = max([p[1] for p in outline])
979
+ min_y = min(min_y, o_min_y)
980
+ max_y = max(max_y, o_max_y)
981
+ outline_graphs.append(outline_graph)
982
+ graph = Graph()
983
+ Graph.monotone_fill(graph, outline_graphs, min_y, max_y, self.distance)
984
+ graph.double_odd_edge()
985
+ walk = list()
986
+ graph.walk(walk)
987
+ return walk