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,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