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,912 +1,933 @@
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 filltype is None:
880
+ filltype = "evenodd"
881
+ filltype = filltype.lower()
882
+ if keep is None:
883
+ keep = False
884
+
885
+ if method.startswith("d"):
886
+ long_method = "Difference"
887
+ elif method.startswith("i"):
888
+ long_method = "Intersection"
889
+ elif method.startswith("x"):
890
+ long_method = "Xor"
891
+ else:
892
+ long_method = "Union"
893
+
894
+ if filltype.startswith("no") or filltype.startswith("z"):
895
+ long_filltype = "NonZero"
896
+ elif filltype.startswith("p") or filltype.startswith("+"):
897
+ long_filltype = "Positive"
898
+ elif filltype.startswith("ne") or filltype.startswith("-"):
899
+ long_filltype = "Negative"
900
+ else:
901
+ long_filltype = "EvenOdd"
902
+
903
+ channel(f"Method={long_method}, filltype={long_filltype}")
904
+
905
+ data_out = list()
906
+
907
+ # Create a clipper object
908
+ clipper = ClipperCAG(interpolation=interpolation)
909
+ clipper.add_nodes(data)
910
+ # Perform the clip operation
911
+ clipper.process_data(method=method, filltype=filltype)
912
+ # _("Create clipper data")
913
+ with self.undoscope("Create clipper data"):
914
+ for geom in clipper.result_geometry():
915
+ if geom is not None:
916
+ newnode = self.elem_branch.add(
917
+ geometry=geom, type="elem path", stroke=firstnode.stroke
918
+ )
919
+ newnode.stroke_width = UNITS_PER_PIXEL
920
+ newnode.linejoin = Linejoin.JOIN_ROUND
921
+ newnode.label = f"{long_method} of {firstnode.id if firstnode.label is None else firstnode.display_label()}"
922
+ data_out.append(newnode)
923
+
924
+ # Newly created! Classification needed?
925
+ if len(data_out) > 0:
926
+ post.append(classify_new(data_out))
927
+ self.signal("refresh_scene", "Scene")
928
+ if not keep:
929
+ self.remove_nodes(data)
930
+
931
+ return "elements", data_out
932
+
933
+ # --------------------------- END COMMANDS ------------------------------