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,912 +1,934 @@
1
- """
2
- This adds console commands that deal with the creation of an offset
3
-
4
- Minimal integration of the Clipper2 library by Angus Johnson
5
- https://github.com/AngusJohnson/Clipper2
6
- via the pyclipr library of Luke Parry
7
- https://github.com/drlukeparry/pyclipr
8
- """
9
-
10
-
11
- def plugin(kernel, lifecycle=None):
12
- _ = kernel.translation
13
- if lifecycle == "invalidate":
14
- try:
15
- import pyclipr
16
- except ImportError:
17
- # print ("Clipper plugin could not load because pyclipr is not installed.")
18
- return True
19
-
20
- if lifecycle == "postboot":
21
- init_commands(kernel)
22
-
23
-
24
- def init_commands(kernel):
25
- import numpy as np
26
- import pyclipr
27
-
28
- from meerk40t.core.node.node import Linejoin, Node
29
- from meerk40t.core.units import UNITS_PER_PIXEL, Length
30
- from meerk40t.tools.geomstr import Geomstr
31
-
32
- self = kernel.elements
33
-
34
- _ = kernel.translation
35
-
36
- class ClipperOffset:
37
- """
38
- Wraps around the pyclpr interface to clipper offset (inflate paths).
39
-
40
- Typical invocation:
41
- data = (node1, node2,)
42
- offs = ClipperOffset(interpolation=500)
43
- offs.add_nodes(data)
44
- offset = float(Length("2mm"))
45
- offs.process_data(offset, jointype="round", separate=False)
46
- for geom in offs.result_geometry():
47
- newnode = self.elem_branch.add(geometry=geom, type="elem path")
48
-
49
- """
50
-
51
- def __init__(self, interpolation=None):
52
- self.np_list = []
53
- self.polygon_list = []
54
- self._interpolation = None
55
- self.interpolation = interpolation
56
- self.any_open = False
57
- # Create a clipper object
58
- self.clipr_offset = pyclipr.ClipperOffset()
59
- self.newpath = None
60
- self._factor = 1000
61
- self.tolerance = 0.25
62
- self.factor = self._factor
63
-
64
- # @staticmethod
65
- # def testroutine():
66
- # # Tuple definition of a path
67
- # path = [(0.0, 0.), (100, 0), (100, 100), (0, 100), (0, 0)]
68
- # # Create an offsetting object
69
- # po = pyclipr.ClipperOffset()
70
- # # Set the scale factor to convert to internal integer representation
71
- # po.scaleFactor = int(1000)
72
- # # add the path - ensuring to use Polygon for the endType argument
73
- # npp = np.array(path)
74
- # po.addPath(npp, pyclipr.JoinType.Miter, pyclipr.EndType.Polygon)
75
- # # Apply the offsetting operation using a delta.
76
- # offsetSquare = po.execute(10.0)
77
- # print ("test for polygon...")
78
- # print (npp)
79
- # print (offsetSquare)
80
- # print ("done...")
81
- # po.clear()
82
- # path=[ (100, 100), (1500, 100), (100, 1500), (1500, 1500) ]
83
- # path = [(25801, 51602), (129005, 51602), (25801, 129005), (129005, 129005)]
84
- # po.scaleFactor = int(1000)
85
- # # add the path - ensuring to use Polygon for the endType argument
86
- # npp = np.array(path)
87
- # po.addPath(npp, pyclipr.JoinType.Miter, pyclipr.EndType.Square)
88
- # # Apply the offsetting operation using a delta.
89
- # offsetSquare = po.execute(10.0)
90
- # print ("test for polyline...")
91
- # print (npp)
92
- # print (offsetSquare)
93
- # print ("done...")
94
-
95
- @property
96
- def interpolation(self):
97
- return self._interpolation
98
-
99
- @interpolation.setter
100
- def interpolation(self, value):
101
- if value is None:
102
- value = 500
103
- self._interpolation = 500
104
-
105
- @property
106
- def factor(self):
107
- return self._factor
108
-
109
- @factor.setter
110
- def factor(self, value):
111
- self._factor = value
112
- self.clipr_offset.scaleFactor = self._factor
113
-
114
- def clear(self):
115
- self.np_list = []
116
- self.polygon_list = []
117
-
118
- def add_geometries(self, geomlist):
119
- for g in geomlist:
120
- for subg in g.as_contiguous():
121
- node_points = list(subg.as_interpolated_points(self.interpolation))
122
- flag = subg.is_closed()
123
- # print (node_points, flag)
124
- self.np_list.append(node_points)
125
- self.polygon_list.append(flag)
126
-
127
- def add_nodes(self, nodelist):
128
- # breaks down the path to a list of subgeometries.
129
- self.clear()
130
- # Set the scale factor to convert to internal integer representation
131
- # As mks internal variable representation is already based on tats
132
- # that should not be necessary
133
- bounds = Node.union_bounds(nodelist)
134
- factor1 = 1000
135
- factor2 = 1
136
- # Structures below 500 tats sidelength are ignored...
137
- if bounds[2] > 100000 or bounds[3] > 100000:
138
- factor1 = 1
139
- factor2 = 1000
140
- elif bounds[2] > 10000 or bounds[3] > 10000:
141
- factor1 = 10
142
- factor2 = 100
143
- elif bounds[2] > 1000 or bounds[3] > 1000:
144
- factor1 = 100
145
- factor2 = 10
146
- self.factor = factor1
147
- self.tolerance = 0.5 * factor2 * 0.5 * factor2
148
-
149
- geom_list = []
150
- for node in nodelist:
151
- # print (f"Looking at {node.type} - {node.label}")
152
- if hasattr(node, "as_geometry"):
153
- # Let's get list of points with the
154
- # required interpolation density
155
- g = node.as_geometry()
156
- geom_list.append(g)
157
- else:
158
- bb = node.bounds
159
- if bb is None:
160
- # Node has no bounds or space, therefore no clipline.
161
- continue
162
- g = Geomstr.rect(
163
- bb[0], bb[1], bb[2] - bb[0], bb[3] - bb[1], rx=0, ry=0
164
- )
165
- geom_list.append(g)
166
- self.add_geometries(geom_list)
167
-
168
- def add_path(self, path):
169
- # breaks down the path to a list of subgeometries.
170
- self.clear()
171
- # Set the scale factor to convert to internal integer representation
172
- # As mks internal variable representation is already based on tats
173
- # that should not be necessary
174
- bounds = path.bbox(transformed=True)
175
- factor1 = 1000
176
- if bounds[2] > 100000 or bounds[3] > 100000:
177
- factor1 = 1
178
- elif bounds[2] > 10000 or bounds[3] > 10000:
179
- factor1 = 10
180
- elif bounds[2] > 1000 or bounds[3] > 1000:
181
- factor1 = 100
182
- factor2 = 1000 / factor1
183
- self.factor = factor1
184
- self.tolerance = 0.5 * factor2 * 0.5 * factor2
185
- geom_list = []
186
- g = Geomstr.svg(path)
187
- geom_list.append(g)
188
- self.add_geometries(geom_list)
189
-
190
- def process_data(self, offset, jointype="round", separate=False):
191
- self.clipr_offset.clear()
192
- self.newpath = None
193
- if jointype.startswith("r"): # round
194
- pyc_jointype = pyclipr.JoinType.Round
195
- elif jointype.startswith("s"): # square
196
- pyc_jointype = pyclipr.JoinType.Square
197
- else:
198
- pyc_jointype = pyclipr.JoinType.Miter
199
- for node_points, is_polygon in zip(self.np_list, self.polygon_list):
200
- # There may be a smarter way to do this, but geomstr
201
- # provides an array of complex numbers. pyclipr on the other
202
- # hand would like to have points as (x, y) and not as (x + y * 1j)
203
- complex_array = np.array(node_points)
204
- temp = np.column_stack((complex_array.real, complex_array.imag))
205
- np_points = temp.astype(int)
206
-
207
- # add the path - ensuring to use Polygon for the endType argument
208
-
209
- if is_polygon:
210
- pyc_endtype = pyclipr.EndType.Polygon
211
- else:
212
- pyc_endtype = pyclipr.EndType.Square
213
-
214
- self.clipr_offset.addPath(np_points, pyc_jointype, pyc_endtype)
215
- if separate:
216
- # Apply the offsetting operation using a delta.
217
- newp = self.clipr_offset.execute(offset)
218
- if self.newpath is None:
219
- self.newpath = list()
220
- self.newpath.append(newp)
221
- self.clipr_offset.clear()
222
-
223
- if not separate:
224
- # Apply the offsetting operation using a delta.
225
- self.newpath = self.clipr_offset.execute(offset)
226
-
227
- def result_geometry(self):
228
- if len(self.newpath) == 0:
229
- # print(f"Collapsed clipline for {node.type}:{node.label}\n{np_points}")
230
- return None
231
- if isinstance(self.newpath[0], (tuple, list)):
232
- # Can execute directly
233
- target = self.newpath
234
- else:
235
- # Create a temporary list
236
- target = (self.newpath,)
237
-
238
- idx = 0
239
- for newp in target:
240
- # print (f"Type of newp: {type(newp).__name__}")
241
- # print(newp)
242
- for subp in newp:
243
- # print (f"Type of subp: {type(subp).__name__}")
244
- # print (subp)
245
- result_list = []
246
- pt_count = len(subp)
247
- # print (f"{idx}#: {pt_count} pts")
248
- idx += 1
249
- if pt_count < 2:
250
- continue
251
- # Sometimes we get artifacts: a small array
252
- # with very small structures.
253
- # We try to identify and to discard them
254
- # Structures below 500 tats sidelength are ignored...
255
- maxd = 0
256
- lastpt = None
257
- had_error = False
258
- for pt in subp:
259
- if lastpt is not None:
260
- try:
261
- dx = abs(lastpt[0] - pt[0])
262
- dy = abs(lastpt[1] - pt[1])
263
- except IndexError:
264
- # Invalid structure! Ignore
265
- had_error = True
266
- break
267
- maxd += dx * dx + dy * dy
268
- lastpt = pt
269
- if maxd > self.tolerance:
270
- break
271
-
272
- # print (f"Substructure: {maxd:.3f} ({self.tolerance:.3f})")
273
- if had_error or maxd < self.tolerance:
274
- # print (f"Artifact ignored: {maxd:.3f}")
275
- continue
276
-
277
- for pt in subp:
278
- result_list.append(pt[0])
279
- result_list.append(pt[1])
280
- try:
281
- p1x = result_list[0]
282
- p1y = result_list[1]
283
- p2x = result_list[-2]
284
- p2y = result_list[-1]
285
- dx = abs(p1x - p2x)
286
- dy = abs(p1y - p2y)
287
- if dx > 10 or dy > 10:
288
- result_list.append(p1x)
289
- result_list.append(p1y)
290
- except IndexError:
291
- # channel(f"Invalid clipline for {node.type}:{node.label}")
292
- continue
293
- geom = Geomstr.lines(*result_list)
294
- yield geom
295
-
296
- class ClipperCAG:
297
- """
298
- Wraps around the pyclpr interface to clipper to run clip operations:
299
- supported:
300
- method: Union, Difference, Intersect, Xor
301
- filltype: EvenOdd, Positive, Negative, NonZero
302
-
303
- Typical invocation:
304
- data = (node1, node2,)
305
- cag = ClipperCAG(interpolation=500)
306
- cag.add_nodes(data)
307
- cag.process_data(method="union", filltype="EvenOdd")
308
- geom = cag.result_geometry()
309
- newnode = self.elem_branch.add(geometry=geom, type="elem path")
310
-
311
- """
312
-
313
- def __init__(self, interpolation=None):
314
- # Create a clipper object
315
- self.clipr_clipper = pyclipr.Clipper()
316
-
317
- self.np_list = []
318
- self.polygon_list = []
319
- self._interpolation = None
320
- self.interpolation = interpolation
321
- self.any_open = False
322
- self.newpath = None
323
- self._factor = 1000
324
-
325
- self.factor = self._factor
326
- self.tolerance = 0.5 * 0.5
327
-
328
- # @staticmethod
329
- # def testroutine():
330
- # # Tuple definition of a path
331
- # path_clip = [(0.0, 0.), (0, 105.1234), (100, 105.1234), (100, 0), (0, 0)]
332
- # open1 = False
333
- # # path_subject = [(0, 0), (0, 50), (100, 50), (100, 0), (0,0)]
334
- # path_subject = [(0, 0), (300, 300)]
335
- # open2 = True
336
-
337
- # # Create a clipping object
338
- # pc = pyclipr.Clipper()
339
- # pc.scaleFactor = int(1000)
340
-
341
- # # Add the paths to the clipping object. Ensure the subject and clip arguments are set to differentiate
342
- # # the paths during the Boolean operation. The final argument specifies if the path is
343
- # # open.
344
- # pc.addPath(np.array(path_subject), pyclipr.PathType.Subject, open2)
345
- # pc.addPath(np.array(path_clip), pyclipr.PathType.Clip, open1)
346
-
347
- # """ Test Polygon Clipping """
348
- # # Below returns paths
349
- # out1 = pc.execute(pyclipr.ClipType.Intersection, pyclipr.FillType.EvenOdd)
350
- # out2 = pc.execute(pyclipr.ClipType.Union, pyclipr.FillType.EvenOdd)
351
- # out3 = pc.execute(pyclipr.ClipType.Difference, pyclipr.FillType.EvenOdd)
352
- # out4 = pc.execute(pyclipr.ClipType.Xor, pyclipr.FillType.EvenOdd)
353
- # # Return open paths...
354
- # out5 = pc.execute(pyclipr.ClipType.Union, pyclipr.FillType.EvenOdd, returnOpenPaths=True)
355
- # print("In:")
356
- # print (path_clip)
357
- # print (path_subject)
358
- # print ("intersect")
359
- # print (out1)
360
- # print ("union")
361
- # print (out2)
362
- # print ("difference")
363
- # print (out3)
364
- # print ("xor")
365
- # print (out4)
366
- # print ("union with open paths")
367
- # print (out5)
368
-
369
- @property
370
- def interpolation(self):
371
- return self._interpolation
372
-
373
- @interpolation.setter
374
- def interpolation(self, value):
375
- if value is None:
376
- value = 500
377
- self._interpolation = 500
378
-
379
- @property
380
- def factor(self):
381
- return self._factor
382
-
383
- @factor.setter
384
- def factor(self, value):
385
- self._factor = value
386
- self.clipr_clipper.scaleFactor = self._factor
387
-
388
- def clear(self):
389
- self.np_list = []
390
- self.polygon_list = []
391
-
392
- def add_nodes(self, nodelist):
393
- # breaks down the path to a list of subgeometries.
394
- self.clear()
395
- # Set the scale factor to convert to internal integer representation
396
- # As mks internal variable representation is already based on tats
397
- # that should not be necessary
398
- bounds = Node.union_bounds(nodelist)
399
- factor1 = int(1000)
400
- if bounds[2] > 100000 or bounds[3] > 100000:
401
- factor1 = int(1)
402
- elif bounds[2] > 10000 or bounds[3] > 10000:
403
- factor1 = int(10)
404
- elif bounds[2] > 1000 or bounds[3] > 1000:
405
- factor1 = int(100)
406
- factor2 = 1000 / factor1
407
- self.factor = factor1
408
- self.tolerance = 0.5 * factor2 * 0.5 * factor2
409
- for node in nodelist:
410
- # print (f"Looking at {node.type} - {node.label}")
411
- if hasattr(node, "as_geometry"):
412
- # Let's get list of points with the
413
- # required interpolation density
414
- g = node.as_geometry()
415
- idx = 0
416
- for subg in g.as_contiguous():
417
- node_points = list(
418
- subg.as_interpolated_points(self.interpolation)
419
- )
420
- flag = subg.is_closed()
421
- # print (node_points, flag)
422
- self.np_list.append(node_points)
423
- self.polygon_list.append(flag)
424
- # print (f"Adding structure #{idx} with {len(node_points)} pts")
425
- idx += 1
426
- else:
427
- bb = node.bounds
428
- if bb is None:
429
- # Node has no bounds or space, therefore no clipline.
430
- continue
431
- node_points = (
432
- bb[0] + bb[1] * 1j,
433
- bb[0] + bb[3] * 1j,
434
- bb[2] + bb[3] * 1j,
435
- bb[2] + bb[1] * 1j,
436
- bb[0] + bb[1] * 1j,
437
- )
438
- self.np_list.append(node_points)
439
- self.polygon_list.append(True)
440
-
441
- def _add_data(self):
442
- self.clipr_clipper.clear()
443
- first = True
444
- self.any_open = False
445
- for node_points, is_polygon in zip(self.np_list, self.polygon_list):
446
- # print (f"Add {'polygon' if is_polygon else 'polyline'}: {node_points}")
447
-
448
- # There may be a smarter way to do this, but geomstr
449
- # provides an array of complex numbers. pyclipr on the other
450
- # hand would like to have points as (x, y) and not as (x + y * 1j)
451
- complex_array = np.array(node_points)
452
- temp = np.column_stack((complex_array.real, complex_array.imag))
453
- np_points = temp.astype(int)
454
-
455
- if first:
456
- first = False
457
- pyc_pathtype = pyclipr.PathType.Subject
458
- else:
459
- pyc_pathtype = pyclipr.PathType.Clip
460
-
461
- # print (f"Add path {pyc_pathtype} with {is_polygon}: {len(np_points)} pts")
462
- if not is_polygon:
463
- self.any_open = True
464
- self.clipr_clipper.addPath(np_points, pyc_pathtype, not is_polygon)
465
-
466
- def process_data(self, method, filltype):
467
- self._add_data()
468
- if method.startswith("d"):
469
- pyc_method = pyclipr.ClipType.Difference
470
- elif method.startswith("i"):
471
- pyc_method = pyclipr.ClipType.Intersection
472
- elif method.startswith("x"):
473
- pyc_method = pyclipr.ClipType.Xor
474
- else:
475
- pyc_method = pyclipr.ClipType.Union
476
- # Definition of enumerators has changed, so let#s try to be backwards compatible
477
- if filltype.startswith("no") or filltype.startswith("z"):
478
- try:
479
- pyc_filltype = pyclipr.FillRule.NonZero
480
- except AttributeError:
481
- pyc_filltype = pyclipr.FillType.NonZero
482
- elif filltype.startswith("p") or filltype.startswith("+"):
483
- try:
484
- pyc_filltype = pyclipr.FillRule.Positive
485
- except AttributeError:
486
- pyc_filltype = pyclipr.FillType.Positive
487
- elif filltype.startswith("ne") or filltype.startswith("-"):
488
- try:
489
- pyc_filltype = pyclipr.FillRule.Negative
490
- except AttributeError:
491
- pyc_filltype = pyclipr.FillType.Negative
492
- else:
493
- try:
494
- pyc_filltype = pyclipr.FillRule.EvenOdd
495
- except AttributeError:
496
- pyc_filltype = pyclipr.FillType.EvenOdd
497
-
498
- if self.any_open and pyc_method in (pyclipr.ClipType.Union,):
499
- self.newpath = self.clipr_clipper.execute(
500
- pyc_method, pyc_filltype, returnOpenPaths=True
501
- )
502
- else:
503
- self.newpath = self.clipr_clipper.execute(pyc_method, pyc_filltype)
504
-
505
- def result_geometry(self):
506
- if len(self.newpath) == 0:
507
- # print(f"Collapsed clipline for {node.type}:{node.label}\n{np_points}")
508
- return None
509
- if isinstance(self.newpath[0], (tuple, list)):
510
- # Can execute directly
511
- target = self.newpath
512
- else:
513
- # Create a temporary list
514
- target = (self.newpath,)
515
-
516
- idx = 0
517
- allgeom = None
518
- for newp in target:
519
- # print (f"Type of newp: {type(newp).__name__}")
520
- # print(newp)
521
- for subp in newp:
522
- # print (f"Type of subp: {type(subp).__name__}")
523
- # print (subp)
524
- result_list = []
525
- pt_count = len(subp)
526
- # print (f"{idx}#: {pt_count} pts")
527
- idx += 1
528
- if pt_count < 2:
529
- continue
530
- # Sometimes we get artifacts: a small array
531
- # with very small structures.
532
- # We try to identify and to discard them
533
- # 500 x 500
534
- maxd = 0
535
- lastpt = None
536
- had_error = False
537
- for pt in subp:
538
- if lastpt is not None:
539
- try:
540
- dx = abs(lastpt[0] - pt[0])
541
- dy = abs(lastpt[1] - pt[1])
542
- except IndexError:
543
- # Invalid structure! Ignore
544
- had_error = True
545
- break
546
- maxd += dx * dx + dy * dy
547
- lastpt = pt
548
- if maxd > self.tolerance:
549
- break
550
-
551
- if had_error or maxd < self.tolerance:
552
- # print (f"Artifact ignored: {maxd:.3f}")
553
- continue
554
-
555
- for pt in subp:
556
- result_list.append(pt[0])
557
- result_list.append(pt[1])
558
- try:
559
- p1x = result_list[0]
560
- p1y = result_list[1]
561
- p2x = result_list[-2]
562
- p2y = result_list[-1]
563
- dx = abs(p1x - p2x)
564
- dy = abs(p1y - p2y)
565
- if dx > 10 or dy > 10:
566
- result_list.append(p1x)
567
- result_list.append(p1y)
568
- except IndexError:
569
- # channel(f"Invalid clipline for {node.type}:{node.label}")
570
- continue
571
- geom = Geomstr.lines(*result_list)
572
- if allgeom is None:
573
- allgeom = geom
574
- else:
575
- # Add a end marker
576
- allgeom.end()
577
- allgeom.append(geom)
578
- # print (geom)
579
- yield allgeom
580
-
581
- def offset_path(self, path, offset_value=0):
582
- # As this oveloading a regular method in a class
583
- # it needs to have the very same definition (including the class
584
- # reference self)
585
- offs = ClipperOffset(interpolation=500)
586
- offs.add_path(path)
587
- offs.process_data(offset_value, jointype="round", separate=False)
588
- p = None
589
- for g in offs.result_geometry():
590
- if g is not None:
591
- p = g.as_path()
592
- break
593
- if p is None:
594
- p = path
595
- return p
596
-
597
- classify_new = self.post_classify
598
-
599
- # We are patching the class responsible for Cut nodes in general,
600
- # so that any new instance of this class will be able to use the
601
- # new functionality.
602
- from meerk40t.core.node.op_cut import CutOpNode
603
-
604
- CutOpNode.offset_routine = offset_path
605
-
606
- @self.console_argument(
607
- "offset",
608
- type=str,
609
- help=_(
610
- "offset to line mm (positive values to left/outside, negative values to right/inside)"
611
- ),
612
- )
613
- @self.console_option(
614
- "jointype", "j", type=str, help=_("join type: round, miter, square")
615
- )
616
- @self.console_option(
617
- "separate",
618
- "s",
619
- action="store_true",
620
- type=bool,
621
- help=_("deal with subpaths separately"),
622
- )
623
- @self.console_option(
624
- "interpolation", "i", type=int, help=_("interpolation points per segment")
625
- )
626
- @self.console_command(
627
- "offset",
628
- help=_("create an offset path for any of the given elements"),
629
- input_type=(None, "elements"),
630
- output_type="elements",
631
- )
632
- def element_offset_path(
633
- command,
634
- channel,
635
- _,
636
- offset=None,
637
- jointype=None,
638
- separate=None,
639
- interpolation=None,
640
- data=None,
641
- post=None,
642
- **kwargs,
643
- ):
644
- if data is None:
645
- data = list(self.elems(emphasized=True))
646
- if len(data) == 0:
647
- channel(_("No elements selected"))
648
- return "elements", data
649
- if interpolation is None:
650
- interpolation = 500
651
- if separate is None:
652
- separate = False
653
- if offset is None:
654
- offset = 0
655
- else:
656
- try:
657
- ll = Length(offset)
658
- offset = float(ll)
659
- except ValueError:
660
- offset = 0
661
- if offset == 0.0:
662
- channel("Invalid offset, nothing to do")
663
- return
664
- if jointype is None:
665
- jointype = "miter"
666
- jointype = jointype.lower()
667
- default_stroke = None
668
- for node in data:
669
- if hasattr(node, "stroke"):
670
- default_stroke = node.stroke
671
- break
672
- if default_stroke is None:
673
- default_stroke = self._default_stroke
674
- data_out = []
675
- c_off = ClipperOffset(interpolation=interpolation)
676
- c_off.add_nodes(data)
677
- c_off.process_data(offset, jointype=jointype, separate=separate)
678
- for geom in c_off.result_geometry():
679
- if geom is not None:
680
- newnode = self.elem_branch.add(
681
- geometry=geom, type="elem path", stroke=default_stroke
682
- )
683
- newnode.stroke_width = UNITS_PER_PIXEL
684
- newnode.linejoin = Linejoin.JOIN_ROUND
685
- newnode.label = f"Offset: {Length(offset).length_mm}"
686
- data_out.append(newnode)
687
-
688
- # Newly created! Classification needed?
689
- if len(data_out) > 0:
690
- post.append(classify_new(data_out))
691
- self.signal("refresh_scene", "Scene")
692
- return "elements", data_out
693
-
694
- # Pocketing
695
- @self.console_argument(
696
- "offset",
697
- type=str,
698
- help=_(
699
- "offset to line mm (negative values to left/outside, positive values to right/inside)"
700
- ),
701
- )
702
- @self.console_option(
703
- "jointype", "j", type=str, help=_("join type: round, miter, square")
704
- )
705
- @self.console_option(
706
- "separate",
707
- "s",
708
- action="store_true",
709
- type=bool,
710
- help=_("deal with subpaths separately"),
711
- )
712
- @self.console_option(
713
- "repeats",
714
- "r",
715
- type=int,
716
- help=_("amount of repetitions, 0=until area is fully filled"),
717
- )
718
- @self.console_option(
719
- "interpolation", "i", type=int, help=_("interpolation points per segment")
720
- )
721
- @self.console_command(
722
- "pocket",
723
- help=_("create a pocketing path for any of the given elements"),
724
- input_type=(None, "elements"),
725
- output_type="elements",
726
- )
727
- def element_pocket_path(
728
- command,
729
- channel,
730
- _,
731
- offset=None,
732
- jointype=None,
733
- separate=None,
734
- repeats=0,
735
- interpolation=None,
736
- data=None,
737
- post=None,
738
- **kwargs,
739
- ):
740
- if data is None:
741
- data = list(self.elems(emphasized=True))
742
- if len(data) == 0:
743
- channel(_("No elements selected"))
744
- return "elements", data
745
- if interpolation is None:
746
- interpolation = 500
747
- if separate is None:
748
- separate = False
749
- if offset is None:
750
- offset = 0
751
- else:
752
- try:
753
- ll = Length(offset)
754
- offset = float(ll)
755
- except ValueError:
756
- offset = 0
757
- if offset == 0.0:
758
- channel("Invalid offset, nothing to do")
759
- return
760
- # Our definition for an offset is different this time
761
- offset *= -1
762
- if repeats is None or repeats < 0:
763
- repeats = 0
764
- if offset > 0 and repeats == 0:
765
- channel(
766
- "You need to provide the -r parameter to set the amount of repetitions"
767
- )
768
- return
769
-
770
- if jointype is None:
771
- jointype = "miter"
772
- jointype = jointype.lower()
773
- default_stroke = None
774
- for node in data:
775
- if hasattr(node, "stroke"):
776
- default_stroke = node.stroke
777
- break
778
- if default_stroke is None:
779
- default_stroke = self._default_stroke
780
- data_out = []
781
- rep_count = 0
782
- mydata = [e for e in data]
783
- while (rep_count < repeats or repeats == 0) and len(mydata) > 0:
784
- rep_count += 1
785
- c_off = ClipperOffset(interpolation=interpolation)
786
- c_off.add_nodes(mydata)
787
- c_off.process_data(offset, jointype=jointype, separate=separate)
788
- mydata.clear()
789
- for geom in c_off.result_geometry():
790
- if geom is not None:
791
- newnode = self.elem_branch.add(
792
- geometry=geom, type="elem path", stroke=default_stroke
793
- )
794
- newnode.stroke_width = UNITS_PER_PIXEL
795
- newnode.linejoin = Linejoin.JOIN_ROUND
796
- newnode.label = f"Offset: {Length(offset).length_mm}"
797
- mydata.append(newnode)
798
- data_out.append(newnode)
799
-
800
- # Newly created! Classification needed?
801
- if len(data_out) > 0:
802
- post.append(classify_new(data_out))
803
- self.signal("refresh_scene", "Scene")
804
- return "elements", data_out
805
-
806
- # ---- Let's add some CAG commands....
807
- @self.console_argument(
808
- "method",
809
- type=str,
810
- help=_("method to use (one of union, difference, intersection, xor)"),
811
- )
812
- @self.console_option(
813
- "filltype",
814
- "f",
815
- type=str,
816
- help=_("filltype to use (one of evenodd, nonzero, negative, positive)"),
817
- )
818
- @self.console_option(
819
- "interpolation", "i", type=int, help=_("interpolation points per segment")
820
- )
821
- @self.console_option(
822
- "keep",
823
- "k",
824
- action="store_true",
825
- type=bool,
826
- help=_("keep the original elements, will be removed by default"),
827
- )
828
- @self.console_command(
829
- "clipper",
830
- help=_("create a logical combination of the given elements"),
831
- input_type=(None, "elements"),
832
- output_type="elements",
833
- )
834
- def element_clipper(
835
- command,
836
- channel,
837
- _,
838
- method=None,
839
- filltype=None,
840
- interpolation=None,
841
- keep=None,
842
- data=None,
843
- post=None,
844
- **kwargs,
845
- ):
846
- if data is None:
847
- data = list(self.elems(emphasized=True))
848
- if len(data) == 0:
849
- channel(_("No elements selected"))
850
- return "elements", data
851
- # Sort data according to selection data so that first selected element becomes the master
852
- data.sort(key=lambda n: n.emphasized_time)
853
- firstnode = data[0]
854
-
855
- if interpolation is None:
856
- interpolation = 500
857
- if method is None:
858
- method = "union"
859
- method = method.lower()
860
- if filltype is None:
861
- filltype = "evenodd"
862
- filltype = filltype.lower()
863
- if keep is None:
864
- keep = False
865
-
866
- if method.startswith("d"):
867
- long_method = "Difference"
868
- elif method.startswith("i"):
869
- long_method = "Intersection"
870
- elif method.startswith("x"):
871
- long_method = "Xor"
872
- else:
873
- long_method = "Union"
874
-
875
- if filltype.startswith("no") or filltype.startswith("z"):
876
- long_filltype = "NonZero"
877
- elif filltype.startswith("p") or filltype.startswith("+"):
878
- long_filltype = "Positive"
879
- elif filltype.startswith("ne") or filltype.startswith("-"):
880
- long_filltype = "Negative"
881
- else:
882
- long_filltype = "EvenOdd"
883
-
884
- channel(f"Method={long_method}, filltype={long_filltype}")
885
-
886
- data_out = list()
887
-
888
- # Create a clipper object
889
- clipper = ClipperCAG(interpolation=interpolation)
890
- clipper.add_nodes(data)
891
- # Perform the clip operation
892
- clipper.process_data(method=method, filltype=filltype)
893
- for geom in clipper.result_geometry():
894
- if geom is not None:
895
- newnode = self.elem_branch.add(
896
- geometry=geom, type="elem path", stroke=firstnode.stroke
897
- )
898
- newnode.stroke_width = UNITS_PER_PIXEL
899
- newnode.linejoin = Linejoin.JOIN_ROUND
900
- newnode.label = f"{long_method} of {firstnode.id if firstnode.label is None else firstnode.label}"
901
- data_out.append(newnode)
902
-
903
- # Newly created! Classification needed?
904
- if len(data_out) > 0:
905
- post.append(classify_new(data_out))
906
- self.signal("refresh_scene", "Scene")
907
- if not keep:
908
- self.remove_nodes(data)
909
-
910
- return "elements", data_out
911
-
912
- # --------------------------- END COMMANDS ------------------------------
1
+ """
2
+ This adds console commands that deal with the creation of an offset
3
+
4
+ Minimal integration of the Clipper2 library by Angus Johnson
5
+ https://github.com/AngusJohnson/Clipper2
6
+ via the pyclipr library of Luke Parry
7
+ https://github.com/drlukeparry/pyclipr
8
+ """
9
+
10
+
11
+ def plugin(kernel, lifecycle=None):
12
+ _ = kernel.translation
13
+ if lifecycle == "invalidate":
14
+ try:
15
+ import pyclipr
16
+ except ImportError:
17
+ # print ("Clipper plugin could not load because pyclipr is not installed.")
18
+ return True
19
+
20
+ if lifecycle == "postboot":
21
+ init_commands(kernel)
22
+
23
+
24
+ def init_commands(kernel):
25
+ import numpy as np
26
+ import pyclipr
27
+
28
+ from meerk40t.core.node.node import Linejoin, Node
29
+ from meerk40t.core.units import UNITS_PER_PIXEL, Length
30
+ from meerk40t.tools.geomstr import Geomstr
31
+
32
+ self = kernel.elements
33
+
34
+ _ = kernel.translation
35
+
36
+ class ClipperOffset:
37
+ """
38
+ Wraps around the pyclpr interface to clipper offset (inflate paths).
39
+
40
+ Typical invocation:
41
+ data = (node1, node2,)
42
+ offs = ClipperOffset(interpolation=500)
43
+ offs.add_nodes(data)
44
+ offset = float(Length("2mm"))
45
+ offs.process_data(offset, jointype="round", separate=False)
46
+ for geom in offs.result_geometry():
47
+ newnode = self.elem_branch.add(geometry=geom, type="elem path")
48
+
49
+ """
50
+
51
+ def __init__(self, interpolation=None):
52
+ self.np_list = []
53
+ self.polygon_list = []
54
+ self._interpolation = None
55
+ self.interpolation = interpolation
56
+ self.any_open = False
57
+ # Create a clipper object
58
+ self.clipr_offset = pyclipr.ClipperOffset()
59
+ self.newpath = None
60
+ self._factor = 1000
61
+ self.tolerance = 0.25
62
+ self.factor = self._factor
63
+
64
+ # @staticmethod
65
+ # def testroutine():
66
+ # # Tuple definition of a path
67
+ # path = [(0.0, 0.), (100, 0), (100, 100), (0, 100), (0, 0)]
68
+ # # Create an offsetting object
69
+ # po = pyclipr.ClipperOffset()
70
+ # # Set the scale factor to convert to internal integer representation
71
+ # po.scaleFactor = int(1000)
72
+ # # add the path - ensuring to use Polygon for the endType argument
73
+ # npp = np.array(path)
74
+ # po.addPath(npp, pyclipr.JoinType.Miter, pyclipr.EndType.Polygon)
75
+ # # Apply the offsetting operation using a delta.
76
+ # offsetSquare = po.execute(10.0)
77
+ # print ("test for polygon...")
78
+ # print (npp)
79
+ # print (offsetSquare)
80
+ # print ("done...")
81
+ # po.clear()
82
+ # path=[ (100, 100), (1500, 100), (100, 1500), (1500, 1500) ]
83
+ # path = [(25801, 51602), (129005, 51602), (25801, 129005), (129005, 129005)]
84
+ # po.scaleFactor = int(1000)
85
+ # # add the path - ensuring to use Polygon for the endType argument
86
+ # npp = np.array(path)
87
+ # po.addPath(npp, pyclipr.JoinType.Miter, pyclipr.EndType.Square)
88
+ # # Apply the offsetting operation using a delta.
89
+ # offsetSquare = po.execute(10.0)
90
+ # print ("test for polyline...")
91
+ # print (npp)
92
+ # print (offsetSquare)
93
+ # print ("done...")
94
+
95
+ @property
96
+ def interpolation(self):
97
+ return self._interpolation
98
+
99
+ @interpolation.setter
100
+ def interpolation(self, value):
101
+ if value is None:
102
+ value = 500
103
+ self._interpolation = value
104
+
105
+ @property
106
+ def factor(self):
107
+ return self._factor
108
+
109
+ @factor.setter
110
+ def factor(self, value):
111
+ self._factor = value
112
+ self.clipr_offset.scaleFactor = self._factor
113
+
114
+ def clear(self):
115
+ self.np_list = []
116
+ self.polygon_list = []
117
+
118
+ def add_geometries(self, geomlist):
119
+ for g in geomlist:
120
+ for subg in g.as_contiguous():
121
+ node_points = list(subg.as_interpolated_points(self.interpolation))
122
+ flag = subg.is_closed()
123
+ # print (node_points, flag)
124
+ self.np_list.append(node_points)
125
+ self.polygon_list.append(flag)
126
+
127
+ def add_nodes(self, nodelist):
128
+ # breaks down the path to a list of subgeometries.
129
+ self.clear()
130
+ # Set the scale factor to convert to internal integer representation
131
+ # As mks internal variable representation is already based on tats
132
+ # that should not be necessary
133
+ bounds = Node.union_bounds(nodelist)
134
+ factor1 = 1000
135
+ factor2 = 1
136
+ # Structures below 500 tats sidelength are ignored...
137
+ if bounds[2] > 100000 or bounds[3] > 100000:
138
+ factor1 = 1
139
+ factor2 = 1000
140
+ elif bounds[2] > 10000 or bounds[3] > 10000:
141
+ factor1 = 10
142
+ factor2 = 100
143
+ elif bounds[2] > 1000 or bounds[3] > 1000:
144
+ factor1 = 100
145
+ factor2 = 10
146
+ self.factor = factor1
147
+ self.tolerance = 0.5 * factor2 * 0.5 * factor2
148
+
149
+ geom_list = []
150
+ for node in nodelist:
151
+ # print (f"Looking at {node.type} - {node.display_label()}")
152
+ if hasattr(node, "as_geometry"):
153
+ # Let's get list of points with the
154
+ # required interpolation density
155
+ g = node.as_geometry()
156
+ geom_list.append(g)
157
+ else:
158
+ bb = node.bounds
159
+ if bb is None:
160
+ # Node has no bounds or space, therefore no clipline.
161
+ continue
162
+ g = Geomstr.rect(
163
+ bb[0], bb[1], bb[2] - bb[0], bb[3] - bb[1], rx=0, ry=0
164
+ )
165
+ geom_list.append(g)
166
+ self.add_geometries(geom_list)
167
+
168
+ def add_path(self, path):
169
+ # breaks down the path to a list of subgeometries.
170
+ self.clear()
171
+ # Set the scale factor to convert to internal integer representation
172
+ # As mks internal variable representation is already based on tats
173
+ # that should not be necessary
174
+ bounds = path.bbox(transformed=True)
175
+ factor1 = 1000
176
+ if bounds[2] > 100000 or bounds[3] > 100000:
177
+ factor1 = 1
178
+ elif bounds[2] > 10000 or bounds[3] > 10000:
179
+ factor1 = 10
180
+ elif bounds[2] > 1000 or bounds[3] > 1000:
181
+ factor1 = 100
182
+ factor2 = 1000 / factor1
183
+ self.factor = factor1
184
+ self.tolerance = 0.5 * factor2 * 0.5 * factor2
185
+ geom_list = []
186
+ g = Geomstr.svg(path)
187
+ if g.index:
188
+ geom_list.append(g)
189
+ self.add_geometries(geom_list)
190
+
191
+ def process_data(self, offset, jointype="round", separate=False):
192
+ self.clipr_offset.clear()
193
+ self.newpath = None
194
+ if jointype.startswith("r"): # round
195
+ pyc_jointype = pyclipr.JoinType.Round
196
+ elif jointype.startswith("s"): # square
197
+ pyc_jointype = pyclipr.JoinType.Square
198
+ else:
199
+ pyc_jointype = pyclipr.JoinType.Miter
200
+ for node_points, is_polygon in zip(self.np_list, self.polygon_list):
201
+ # There may be a smarter way to do this, but geomstr
202
+ # provides an array of complex numbers. pyclipr on the other
203
+ # hand would like to have points as (x, y) and not as (x + y * 1j)
204
+ complex_array = np.array(node_points)
205
+ temp = np.column_stack((complex_array.real, complex_array.imag))
206
+ np_points = temp.astype(int)
207
+
208
+ # add the path - ensuring to use Polygon for the endType argument
209
+
210
+ if is_polygon:
211
+ pyc_endtype = pyclipr.EndType.Polygon
212
+ else:
213
+ pyc_endtype = pyclipr.EndType.Square
214
+
215
+ self.clipr_offset.addPath(np_points, pyc_jointype, pyc_endtype)
216
+ if separate:
217
+ # Apply the offsetting operation using a delta.
218
+ newp = self.clipr_offset.execute(offset)
219
+ if self.newpath is None:
220
+ self.newpath = list()
221
+ self.newpath.append(newp)
222
+ self.clipr_offset.clear()
223
+
224
+ if not separate:
225
+ # Apply the offsetting operation using a delta.
226
+ self.newpath = self.clipr_offset.execute(offset)
227
+
228
+ def result_geometry(self):
229
+ if len(self.newpath) == 0:
230
+ # print(f"Collapsed clipline for {node.type}:{node.display_label()}\n{np_points}")
231
+ return None
232
+ if isinstance(self.newpath[0], (tuple, list)):
233
+ # Can execute directly
234
+ target = self.newpath
235
+ else:
236
+ # Create a temporary list
237
+ target = (self.newpath,)
238
+
239
+ idx = 0
240
+ for newp in target:
241
+ # print (f"Type of newp: {type(newp).__name__}")
242
+ # print(newp)
243
+ for subp in newp:
244
+ # print (f"Type of subp: {type(subp).__name__}")
245
+ # print (subp)
246
+ result_list = []
247
+ pt_count = len(subp)
248
+ # print (f"{idx}#: {pt_count} pts")
249
+ idx += 1
250
+ if pt_count < 2:
251
+ continue
252
+ # Sometimes we get artifacts: a small array
253
+ # with very small structures.
254
+ # We try to identify and to discard them
255
+ # Structures below 500 tats sidelength are ignored...
256
+ maxd = 0
257
+ lastpt = None
258
+ had_error = False
259
+ for pt in subp:
260
+ if lastpt is not None:
261
+ try:
262
+ dx = abs(lastpt[0] - pt[0])
263
+ dy = abs(lastpt[1] - pt[1])
264
+ except IndexError:
265
+ # Invalid structure! Ignore
266
+ had_error = True
267
+ break
268
+ maxd += dx * dx + dy * dy
269
+ lastpt = pt
270
+ if maxd > self.tolerance:
271
+ break
272
+
273
+ # print (f"Substructure: {maxd:.3f} ({self.tolerance:.3f})")
274
+ if had_error or maxd < self.tolerance:
275
+ # print (f"Artifact ignored: {maxd:.3f}")
276
+ continue
277
+
278
+ for pt in subp:
279
+ result_list.append(pt[0])
280
+ result_list.append(pt[1])
281
+ try:
282
+ p1x = result_list[0]
283
+ p1y = result_list[1]
284
+ p2x = result_list[-2]
285
+ p2y = result_list[-1]
286
+ dx = abs(p1x - p2x)
287
+ dy = abs(p1y - p2y)
288
+ if dx > 10 or dy > 10:
289
+ result_list.append(p1x)
290
+ result_list.append(p1y)
291
+ except IndexError:
292
+ # channel(f"Invalid clipline for {node.type}:{node.display_label()}")
293
+ continue
294
+ geom = Geomstr.lines(*result_list)
295
+ yield geom
296
+
297
+ class ClipperCAG:
298
+ """
299
+ Wraps around the pyclpr interface to clipper to run clip operations:
300
+ supported:
301
+ method: Union, Difference, Intersect, Xor
302
+ filltype: EvenOdd, Positive, Negative, NonZero
303
+
304
+ Typical invocation:
305
+ data = (node1, node2,)
306
+ cag = ClipperCAG(interpolation=500)
307
+ cag.add_nodes(data)
308
+ cag.process_data(method="union", filltype="EvenOdd")
309
+ geom = cag.result_geometry()
310
+ newnode = self.elem_branch.add(geometry=geom, type="elem path")
311
+
312
+ """
313
+
314
+ def __init__(self, interpolation=None):
315
+ # Create a clipper object
316
+ self.clipr_clipper = pyclipr.Clipper()
317
+
318
+ self.np_list = []
319
+ self.polygon_list = []
320
+ self.first = 0
321
+ self._interpolation = None
322
+ self.interpolation = interpolation
323
+ self.any_open = False
324
+ self.newpath = None
325
+ self._factor = 1000
326
+
327
+ self.factor = self._factor
328
+ self.tolerance = 0.5 * 0.5
329
+
330
+ # @staticmethod
331
+ # def testroutine():
332
+ # # Tuple definition of a path
333
+ # path_clip = [(0.0, 0.), (0, 105.1234), (100, 105.1234), (100, 0), (0, 0)]
334
+ # open1 = False
335
+ # # path_subject = [(0, 0), (0, 50), (100, 50), (100, 0), (0,0)]
336
+ # path_subject = [(0, 0), (300, 300)]
337
+ # open2 = True
338
+
339
+ # # Create a clipping object
340
+ # pc = pyclipr.Clipper()
341
+ # pc.scaleFactor = int(1000)
342
+
343
+ # # Add the paths to the clipping object. Ensure the subject and clip arguments are set to differentiate
344
+ # # the paths during the Boolean operation. The final argument specifies if the path is
345
+ # # open.
346
+ # pc.addPath(np.array(path_subject), pyclipr.PathType.Subject, open2)
347
+ # pc.addPath(np.array(path_clip), pyclipr.PathType.Clip, open1)
348
+
349
+ # """ Test Polygon Clipping """
350
+ # # Below returns paths
351
+ # out1 = pc.execute(pyclipr.ClipType.Intersection, pyclipr.FillType.EvenOdd)
352
+ # out2 = pc.execute(pyclipr.ClipType.Union, pyclipr.FillType.EvenOdd)
353
+ # out3 = pc.execute(pyclipr.ClipType.Difference, pyclipr.FillType.EvenOdd)
354
+ # out4 = pc.execute(pyclipr.ClipType.Xor, pyclipr.FillType.EvenOdd)
355
+ # # Return open paths...
356
+ # out5 = pc.execute(pyclipr.ClipType.Union, pyclipr.FillType.EvenOdd, returnOpenPaths=True)
357
+ # print("In:")
358
+ # print (path_clip)
359
+ # print (path_subject)
360
+ # print ("intersect")
361
+ # print (out1)
362
+ # print ("union")
363
+ # print (out2)
364
+ # print ("difference")
365
+ # print (out3)
366
+ # print ("xor")
367
+ # print (out4)
368
+ # print ("union with open paths")
369
+ # print (out5)
370
+
371
+ @property
372
+ def interpolation(self):
373
+ return self._interpolation
374
+
375
+ @interpolation.setter
376
+ def interpolation(self, value):
377
+ if value is None:
378
+ value = 500
379
+ self._interpolation = value
380
+
381
+ @property
382
+ def factor(self):
383
+ return self._factor
384
+
385
+ @factor.setter
386
+ def factor(self, value):
387
+ self._factor = value
388
+ self.clipr_clipper.scaleFactor = self._factor
389
+
390
+ def clear(self):
391
+ self.np_list = []
392
+ self.polygon_list = []
393
+ self.first = 0
394
+
395
+ def add_nodes(self, nodelist):
396
+ # breaks down the path to a list of subgeometries.
397
+ self.clear()
398
+ # Set the scale factor to convert to internal integer representation
399
+ # As mks internal variable representation is already based on tats
400
+ # that should not be necessary
401
+ bounds = Node.union_bounds(nodelist)
402
+ factor1 = int(1000)
403
+ if bounds[2] > 100000 or bounds[3] > 100000:
404
+ factor1 = int(1)
405
+ elif bounds[2] > 10000 or bounds[3] > 10000:
406
+ factor1 = int(10)
407
+ elif bounds[2] > 1000 or bounds[3] > 1000:
408
+ factor1 = int(100)
409
+ factor2 = 1000 / factor1
410
+ self.factor = factor1
411
+ self.tolerance = 0.5 * factor2 * 0.5 * factor2
412
+ first = True
413
+ self.first = 0
414
+ for node in nodelist:
415
+ # print (f"Looking at {node.type} - {node.display_label()}")
416
+ if hasattr(node, "as_geometry"):
417
+ # Let's get list of points with the
418
+ # required interpolation density
419
+ g = node.as_geometry()
420
+ idx = 0
421
+ for subg in g.as_contiguous():
422
+ if not subg:
423
+ continue
424
+ node_points = list(
425
+ subg.as_interpolated_points(self.interpolation)
426
+ )
427
+ flag = subg.is_closed()
428
+ # print (node_points, flag)
429
+ self.np_list.append(node_points)
430
+ self.polygon_list.append(flag)
431
+ if first:
432
+ self.first += 1
433
+ # print (f"Adding structure #{idx} with {len(node_points)} pts")
434
+ idx += 1
435
+ else:
436
+ bb = node.bounds
437
+ if bb is None:
438
+ # Node has no bounds or space, therefore no clipline.
439
+ continue
440
+ node_points = (
441
+ bb[0] + bb[1] * 1j,
442
+ bb[0] + bb[3] * 1j,
443
+ bb[2] + bb[3] * 1j,
444
+ bb[2] + bb[1] * 1j,
445
+ bb[0] + bb[1] * 1j,
446
+ )
447
+ self.np_list.append(node_points)
448
+ self.polygon_list.append(True)
449
+ if first:
450
+ self.first += 1
451
+
452
+ first = False
453
+
454
+ def _add_data(self):
455
+ self.clipr_clipper.clear()
456
+ first = True
457
+ self.any_open = False
458
+ idx = 0
459
+ for node_points, is_polygon in zip(self.np_list, self.polygon_list):
460
+ # print (f"Add {'polygon' if is_polygon else 'polyline'}: {node_points}")
461
+
462
+ # There may be a smarter way to do this, but geomstr
463
+ # provides an array of complex numbers. pyclipr on the other
464
+ # hand would like to have points as (x, y) and not as (x + y * 1j)
465
+ complex_array = np.array(node_points)
466
+ temp = np.column_stack((complex_array.real, complex_array.imag))
467
+ np_points = temp.astype(int)
468
+
469
+ if idx < self.first:
470
+ pyc_pathtype = pyclipr.PathType.Subject
471
+ else:
472
+ pyc_pathtype = pyclipr.PathType.Clip
473
+
474
+ # print (f"Add path {pyc_pathtype} with {is_polygon}: {len(np_points)} pts")
475
+ if not is_polygon:
476
+ self.any_open = True
477
+ self.clipr_clipper.addPath(np_points, pyc_pathtype, not is_polygon)
478
+ idx += 1
479
+
480
+ def process_data(self, method, filltype):
481
+ self._add_data()
482
+ if method.startswith("d"):
483
+ pyc_method = pyclipr.ClipType.Difference
484
+ elif method.startswith("i"):
485
+ pyc_method = pyclipr.ClipType.Intersection
486
+ elif method.startswith("x"):
487
+ pyc_method = pyclipr.ClipType.Xor
488
+ else:
489
+ pyc_method = pyclipr.ClipType.Union
490
+ # Definition of enumerators has changed, so let#s try to be backwards compatible
491
+ if filltype.startswith("no") or filltype.startswith("z"):
492
+ try:
493
+ pyc_filltype = pyclipr.FillRule.NonZero
494
+ except AttributeError:
495
+ pyc_filltype = pyclipr.FillType.NonZero
496
+ elif filltype.startswith("p") or filltype.startswith("+"):
497
+ try:
498
+ pyc_filltype = pyclipr.FillRule.Positive
499
+ except AttributeError:
500
+ pyc_filltype = pyclipr.FillType.Positive
501
+ elif filltype.startswith("ne") or filltype.startswith("-"):
502
+ try:
503
+ pyc_filltype = pyclipr.FillRule.Negative
504
+ except AttributeError:
505
+ pyc_filltype = pyclipr.FillType.Negative
506
+ else:
507
+ try:
508
+ pyc_filltype = pyclipr.FillRule.EvenOdd
509
+ except AttributeError:
510
+ pyc_filltype = pyclipr.FillType.EvenOdd
511
+
512
+ if self.any_open and pyc_method in (pyclipr.ClipType.Union,):
513
+ self.newpath = self.clipr_clipper.execute(
514
+ pyc_method, pyc_filltype, returnOpenPaths=True
515
+ )
516
+ else:
517
+ self.newpath = self.clipr_clipper.execute(pyc_method, pyc_filltype)
518
+
519
+ def result_geometry(self):
520
+ if len(self.newpath) == 0:
521
+ # print(f"Collapsed clipline for {node.type}:{node.display_label()}\n{np_points}")
522
+ return None
523
+ if isinstance(self.newpath[0], (tuple, list)):
524
+ # Can execute directly
525
+ target = self.newpath
526
+ else:
527
+ # Create a temporary list
528
+ target = (self.newpath,)
529
+
530
+ idx = 0
531
+ allgeom = None
532
+ for newp in target:
533
+ # print (f"Type of newp: {type(newp).__name__}")
534
+ # print(newp)
535
+ for subp in newp:
536
+ # print (f"Type of subp: {type(subp).__name__}")
537
+ # print (subp)
538
+ result_list = []
539
+ pt_count = len(subp)
540
+ # print (f"{idx}#: {pt_count} pts")
541
+ idx += 1
542
+ if pt_count < 2:
543
+ continue
544
+ # Sometimes we get artifacts: a small array
545
+ # with very small structures.
546
+ # We try to identify and to discard them
547
+ # 500 x 500
548
+ maxd = 0
549
+ lastpt = None
550
+ had_error = False
551
+ for pt in subp:
552
+ if lastpt is not None:
553
+ try:
554
+ dx = abs(lastpt[0] - pt[0])
555
+ dy = abs(lastpt[1] - pt[1])
556
+ except IndexError:
557
+ # Invalid structure! Ignore
558
+ had_error = True
559
+ break
560
+ maxd += dx * dx + dy * dy
561
+ lastpt = pt
562
+ if maxd > self.tolerance:
563
+ break
564
+
565
+ if had_error or maxd < self.tolerance:
566
+ # print (f"Artifact ignored: {maxd:.3f}")
567
+ continue
568
+
569
+ for pt in subp:
570
+ result_list.append(pt[0])
571
+ result_list.append(pt[1])
572
+ try:
573
+ p1x = result_list[0]
574
+ p1y = result_list[1]
575
+ p2x = result_list[-2]
576
+ p2y = result_list[-1]
577
+ dx = abs(p1x - p2x)
578
+ dy = abs(p1y - p2y)
579
+ if dx > 10 or dy > 10:
580
+ result_list.append(p1x)
581
+ result_list.append(p1y)
582
+ except IndexError:
583
+ # channel(f"Invalid clipline for {node.type}:{node.display_label()}")
584
+ continue
585
+ geom = Geomstr.lines(*result_list)
586
+ if allgeom is None:
587
+ allgeom = geom
588
+ else:
589
+ # Add a end marker
590
+ allgeom.end()
591
+ allgeom.append(geom)
592
+ # print (geom)
593
+ yield allgeom
594
+
595
+ def offset_path(self, path, offset_value=0):
596
+ # As this oveloading a regular method in a class
597
+ # it needs to have the very same definition (including the class
598
+ # reference self)
599
+ offs = ClipperOffset(interpolation=500)
600
+ offs.add_path(path)
601
+ offs.process_data(offset_value, jointype="round", separate=False)
602
+ rp = None
603
+ # Attention geometry is already at device resolution, so we need to use a small tolerance
604
+ for geo in offs.result_geometry():
605
+ g = geo.simplify(tolerance=0.1)
606
+ if g is not None:
607
+ p = g.as_path()
608
+ if rp is None:
609
+ rp = p
610
+ else:
611
+ rp += p
612
+ if rp is None:
613
+ rp = path
614
+ return rp
615
+
616
+ classify_new = self.post_classify
617
+
618
+ # We are patching the class responsible for Cut nodes in general,
619
+ # so that any new instance of this class will be able to use the
620
+ # new functionality.
621
+ from meerk40t.core.node.op_cut import CutOpNode
622
+
623
+ CutOpNode.offset_routine = offset_path
624
+
625
+ @self.console_argument(
626
+ "offset",
627
+ type=str,
628
+ help=_(
629
+ "offset to line mm (positive values to left/outside, negative values to right/inside)"
630
+ ),
631
+ )
632
+ @self.console_option(
633
+ "jointype", "j", type=str, help=_("join type: round, miter, square")
634
+ )
635
+ @self.console_option(
636
+ "separate",
637
+ "s",
638
+ action="store_true",
639
+ type=bool,
640
+ help=_("deal with subpaths separately"),
641
+ )
642
+ @self.console_option(
643
+ "interpolation", "i", type=int, help=_("interpolation points per segment")
644
+ )
645
+ @self.console_command(
646
+ "offset",
647
+ help=_("create an offset path for any of the given elements"),
648
+ input_type=(None, "elements"),
649
+ output_type="elements",
650
+ )
651
+ def element_offset_path(
652
+ command,
653
+ channel,
654
+ _,
655
+ offset=None,
656
+ jointype=None,
657
+ separate=None,
658
+ interpolation=None,
659
+ data=None,
660
+ post=None,
661
+ **kwargs,
662
+ ):
663
+ if data is None:
664
+ data = list(self.elems(emphasized=True))
665
+ if len(data) == 0:
666
+ channel(_("No elements selected"))
667
+ return "elements", data
668
+ if interpolation is None:
669
+ interpolation = 500
670
+ if separate is None:
671
+ separate = False
672
+ if offset is None:
673
+ offset = 0
674
+ else:
675
+ try:
676
+ ll = Length(offset)
677
+ offset = float(ll)
678
+ except ValueError:
679
+ offset = 0
680
+ if offset == 0.0:
681
+ channel("Invalid offset, nothing to do")
682
+ return
683
+ if jointype is None:
684
+ jointype = "miter"
685
+ jointype = jointype.lower()
686
+ default_stroke = None
687
+ for node in data:
688
+ if hasattr(node, "stroke"):
689
+ default_stroke = node.stroke
690
+ break
691
+ if default_stroke is None:
692
+ default_stroke = self._default_stroke
693
+ data_out = []
694
+ c_off = ClipperOffset(interpolation=interpolation)
695
+ c_off.add_nodes(data)
696
+ c_off.process_data(offset, jointype=jointype, separate=separate)
697
+ for geom in c_off.result_geometry():
698
+ if geom is not None:
699
+ newnode = self.elem_branch.add(
700
+ geometry=geom, type="elem path", stroke=default_stroke
701
+ )
702
+ newnode.stroke_width = UNITS_PER_PIXEL
703
+ newnode.linejoin = Linejoin.JOIN_ROUND
704
+ newnode.label = f"Offset: {Length(offset).length_mm}"
705
+ data_out.append(newnode)
706
+
707
+ # Newly created! Classification needed?
708
+ if len(data_out) > 0:
709
+ post.append(classify_new(data_out))
710
+ self.signal("refresh_scene", "Scene")
711
+ return "elements", data_out
712
+
713
+ # Pocketing
714
+ @self.console_argument(
715
+ "offset",
716
+ type=str,
717
+ help=_(
718
+ "offset to line mm (negative values to left/outside, positive values to right/inside)"
719
+ ),
720
+ )
721
+ @self.console_option(
722
+ "jointype", "j", type=str, help=_("join type: round, miter, square")
723
+ )
724
+ @self.console_option(
725
+ "separate",
726
+ "s",
727
+ action="store_true",
728
+ type=bool,
729
+ help=_("deal with subpaths separately"),
730
+ )
731
+ @self.console_option(
732
+ "repeats",
733
+ "r",
734
+ type=int,
735
+ help=_("amount of repetitions, 0=until area is fully filled"),
736
+ )
737
+ @self.console_option(
738
+ "interpolation", "i", type=int, help=_("interpolation points per segment")
739
+ )
740
+ @self.console_command(
741
+ "pocket",
742
+ help=_("create a pocketing path for any of the given elements"),
743
+ input_type=(None, "elements"),
744
+ output_type="elements",
745
+ )
746
+ def element_pocket_path(
747
+ command,
748
+ channel,
749
+ _,
750
+ offset=None,
751
+ jointype=None,
752
+ separate=None,
753
+ repeats=0,
754
+ interpolation=None,
755
+ data=None,
756
+ post=None,
757
+ **kwargs,
758
+ ):
759
+ if data is None:
760
+ data = list(self.elems(emphasized=True))
761
+ if len(data) == 0:
762
+ channel(_("No elements selected"))
763
+ return "elements", data
764
+ if interpolation is None:
765
+ interpolation = 500
766
+ if separate is None:
767
+ separate = False
768
+ if offset is None:
769
+ offset = 0
770
+ else:
771
+ try:
772
+ ll = Length(offset)
773
+ offset = float(ll)
774
+ except ValueError:
775
+ offset = 0
776
+ if offset == 0.0:
777
+ channel("Invalid offset, nothing to do")
778
+ return
779
+ # Our definition for an offset is different this time
780
+ offset *= -1
781
+ if repeats is None or repeats < 0:
782
+ repeats = 0
783
+ if offset > 0 and repeats == 0:
784
+ channel(
785
+ "You need to provide the -r parameter to set the amount of repetitions"
786
+ )
787
+ return
788
+
789
+ if jointype is None:
790
+ jointype = "miter"
791
+ jointype = jointype.lower()
792
+ default_stroke = None
793
+ for node in data:
794
+ if hasattr(node, "stroke"):
795
+ default_stroke = node.stroke
796
+ break
797
+ if default_stroke is None:
798
+ default_stroke = self._default_stroke
799
+ data_out = []
800
+ rep_count = 0
801
+ mydata = [e for e in data]
802
+ while (rep_count < repeats or repeats == 0) and len(mydata) > 0:
803
+ rep_count += 1
804
+ c_off = ClipperOffset(interpolation=interpolation)
805
+ c_off.add_nodes(mydata)
806
+ c_off.process_data(offset, jointype=jointype, separate=separate)
807
+ mydata.clear()
808
+ for geom in c_off.result_geometry():
809
+ if geom is not None:
810
+ newnode = self.elem_branch.add(
811
+ geometry=geom, type="elem path", stroke=default_stroke
812
+ )
813
+ newnode.stroke_width = UNITS_PER_PIXEL
814
+ newnode.linejoin = Linejoin.JOIN_ROUND
815
+ newnode.label = f"Offset: {Length(offset).length_mm}"
816
+ mydata.append(newnode)
817
+ data_out.append(newnode)
818
+
819
+ # Newly created! Classification needed?
820
+ if len(data_out) > 0:
821
+ post.append(classify_new(data_out))
822
+ self.signal("refresh_scene", "Scene")
823
+ return "elements", data_out
824
+
825
+ # ---- Let's add some CAG commands....
826
+ @self.console_argument(
827
+ "method",
828
+ type=str,
829
+ help=_("method to use (one of union, difference, intersection, xor)"),
830
+ )
831
+ @self.console_option(
832
+ "filltype",
833
+ "f",
834
+ type=str,
835
+ help=_("filltype to use (one of evenodd, nonzero, negative, positive)"),
836
+ )
837
+ @self.console_option(
838
+ "interpolation", "i", type=int, help=_("interpolation points per segment")
839
+ )
840
+ @self.console_option(
841
+ "keep",
842
+ "k",
843
+ action="store_true",
844
+ type=bool,
845
+ help=_("keep the original elements, will be removed by default"),
846
+ )
847
+ @self.console_command(
848
+ "clipper",
849
+ help=_("create a logical combination of the given elements"),
850
+ input_type=(None, "elements"),
851
+ output_type="elements",
852
+ )
853
+ def element_clipper(
854
+ command,
855
+ channel,
856
+ _,
857
+ method=None,
858
+ filltype=None,
859
+ interpolation=None,
860
+ keep=None,
861
+ data=None,
862
+ post=None,
863
+ **kwargs,
864
+ ):
865
+ if data is None:
866
+ data = list(self.elems(emphasized=True))
867
+ if len(data) == 0:
868
+ channel(_("No elements selected"))
869
+ return "elements", data
870
+ # Sort data according to selection data so that first selected element becomes the master
871
+ data.sort(key=lambda n: n.emphasized_time)
872
+ firstnode = data[0]
873
+
874
+ if interpolation is None:
875
+ interpolation = 500
876
+ if method is None:
877
+ method = "union"
878
+ method = method.lower()
879
+ if keep is None:
880
+ keep = False
881
+
882
+ if method.startswith("d"):
883
+ long_method = "Difference"
884
+ elif method.startswith("i"):
885
+ long_method = "Intersection"
886
+ elif method.startswith("x"):
887
+ long_method = "Xor"
888
+ else:
889
+ long_method = "Union"
890
+
891
+ if filltype is None:
892
+ filltype = "evenodd" if method != "union" else "nonzero"
893
+ filltype = filltype.lower()
894
+
895
+ if filltype.startswith("no") or filltype.startswith("z"):
896
+ long_filltype = "NonZero"
897
+ elif filltype.startswith("p") or filltype.startswith("+"):
898
+ long_filltype = "Positive"
899
+ elif filltype.startswith("ne") or filltype.startswith("-"):
900
+ long_filltype = "Negative"
901
+ else:
902
+ long_filltype = "EvenOdd"
903
+
904
+ channel(f"Method={long_method}, filltype={long_filltype}")
905
+
906
+ data_out = list()
907
+
908
+ # Create a clipper object
909
+ clipper = ClipperCAG(interpolation=interpolation)
910
+ clipper.add_nodes(data)
911
+ # Perform the clip operation
912
+ clipper.process_data(method=method, filltype=filltype)
913
+ # _("Create clipper data")
914
+ with self.undoscope("Create clipper data"):
915
+ for geom in clipper.result_geometry():
916
+ if geom is not None:
917
+ newnode = self.elem_branch.add(
918
+ geometry=geom, type="elem path", stroke=firstnode.stroke
919
+ )
920
+ newnode.stroke_width = UNITS_PER_PIXEL
921
+ newnode.linejoin = Linejoin.JOIN_ROUND
922
+ newnode.label = f"{long_method} of {firstnode.id if firstnode.label is None else firstnode.display_label()}"
923
+ data_out.append(newnode)
924
+
925
+ # Newly created! Classification needed?
926
+ if len(data_out) > 0:
927
+ post.append(classify_new(data_out))
928
+ self.signal("refresh_scene", "Scene")
929
+ if not keep:
930
+ self.remove_nodes(data)
931
+
932
+ return "elements", data_out
933
+
934
+ # --------------------------- END COMMANDS ------------------------------