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,955 +1,963 @@
1
- """
2
- This adds console commands that deal with the creation of an offset
3
- """
4
- from copy import copy
5
- from math import atan2, tau
6
-
7
- from meerk40t.core.node.node import Linejoin
8
- from meerk40t.core.units import UNITS_PER_PIXEL, Length
9
- from meerk40t.svgelements import (
10
- Arc,
11
- Close,
12
- CubicBezier,
13
- Line,
14
- Move,
15
- Path,
16
- Point,
17
- QuadraticBezier,
18
- )
19
- from meerk40t.tools.geomstr import Geomstr
20
-
21
- """
22
- The following routines deal with the offset of an SVG path at a given distance D.
23
- An offset or parallel curve can easily be established:
24
- - for a line segment by another line parallel and in distance D:
25
- Establish the two normals with length D on the end points and
26
- create the two new endpoints
27
- - for an arc segment: elongate rx and ry by D
28
- To establish an offset for a quadratic or cubic bezier by another cubic bezier
29
- is not possible so this requires approximation.
30
- An acceptable approximation is proposed by Tiller and Hanson:
31
- P1 start point
32
- P2 end point
33
- C1 control point 1
34
- C2 control point 2
35
- You create the offset version of these 3 lines and look for their intersections:
36
- - offset to (P1 C1) -> helper 1
37
- - offset to (C1 C2) -> helper 2
38
- - offset to (P2 C2) -> helper 3
39
- we establish P1-new
40
- the intersections between helper 1 and helper 2 is our new control point C1-new
41
- the intersections between helper 2 and helper 3 is our new control point C2-new
42
-
43
-
44
-
45
- A good visual representation can be seen here:
46
- https://feirell.github.io/offset-bezier/
47
-
48
- The algorithm deals with the challenge as follows:
49
- a) It walks through the subpaths of a given path so that we have a continuous curve
50
- b) It looks at the different segment typs and deals with them,
51
- generating a new offseted segement
52
- c) Finally it stitches those segments together, treating for the simplifaction
53
- """
54
-
55
-
56
- def norm_vector(p1, p2, target_len):
57
- line_vector = p2 - p1
58
- # if line_vector.x == 0 and line_vector.y == 0:
59
- # return Point(target_len, 0)
60
- factor = target_len
61
- normal_vector = Point(-1 * line_vector.y, line_vector.x)
62
- normlen = abs(normal_vector)
63
- if normlen != 0:
64
- factor = target_len / normlen
65
- normal_vector *= factor
66
- return normal_vector
67
-
68
-
69
- def is_clockwise(path, start=0):
70
- def poly_clockwise(poly):
71
- """
72
- returns True if the polygon is clockwise ordered, false if not
73
- """
74
-
75
- total = (
76
- poly[-1].x * poly[0].y - poly[0].x * poly[-1].y
77
- ) # last point to first point
78
- for i in range(len(poly) - 1):
79
- total += poly[i].x * poly[i + 1].y - poly[i + 1].x * poly[i].y
80
-
81
- if total <= 0:
82
- return True
83
- else:
84
- return False
85
-
86
- poly = []
87
- idx = start
88
- while idx < len(path._segments):
89
- seg = path._segments[idx]
90
- if isinstance(seg, (Arc, Line, QuadraticBezier, CubicBezier)):
91
- if len(poly) == 0:
92
- poly.append(seg.start)
93
- poly.append(seg.end)
94
- else:
95
- if len(poly) > 0:
96
- break
97
- idx += 1
98
- if len(poly) == 0:
99
- res = True
100
- else:
101
- res = poly_clockwise(poly)
102
- return res
103
-
104
-
105
- def linearize_segment(segment, interpolation=500, reduce=True):
106
- slope_tolerance = 0.001
107
- s = []
108
- delta = 1.0 / interpolation
109
- lastpt = None
110
- t = 0
111
- last_slope = None
112
- while t <= 1:
113
- appendit = True
114
- np = segment.point(t)
115
- if lastpt is not None:
116
- dx = lastpt.x - np.x
117
- dy = lastpt.y - np.y
118
- if abs(dx) < 1e-6 and abs(dy) < 1e-6:
119
- appendit = False
120
- # identical points!
121
- else:
122
- this_slope = atan2(dy, dx)
123
- if last_slope is not None:
124
- if abs(last_slope - this_slope) < slope_tolerance:
125
- # Combine segments, ie get rid of mid point
126
- this_slope = last_slope
127
- appendit = False
128
- last_slope = this_slope
129
-
130
- if appendit or not reduce:
131
- s.append(np)
132
- else:
133
- s[-1] = np
134
- t += delta
135
- lastpt = np
136
- if s[-1] != segment.end:
137
- np = Point(segment.end)
138
- s.append(np)
139
- # print (f"linearize: {type(segment).__name__}")
140
- # print (f"Start: ({segment.start.x:.0f}, {segment.start.y:.0f}) - ({s[0].x:.0f}, {s[0].y:.0f})")
141
- # print (f"End: ({segment.end.x:.0f}, {segment.end.y:.0f}) - ({s[-1].x:.0f}, {s[-1].y:.0f})")
142
- return s
143
-
144
-
145
- def offset_point_array(points, offset):
146
- result = list()
147
- p0 = None
148
- for idx, p1 in enumerate(points):
149
- if idx > 0:
150
- nv = norm_vector(p0, p1, offset)
151
- result.append(p0 + nv)
152
- result.append(p1 + nv)
153
- p0 = Point(p1)
154
- for idx in range(3, len(result)):
155
- w = result[idx - 3]
156
- z = result[idx - 2]
157
- x = result[idx - 1]
158
- y = result[idx]
159
- p_i, s, t = intersect_line_segments(w, z, x, y)
160
- if p_i is None:
161
- continue
162
- result[idx - 2] = Point(p_i)
163
- result[idx - 1] = Point(p_i)
164
- return result
165
-
166
-
167
- def offset_arc(segment, offset=0, linearize=False, interpolation=500):
168
- if not isinstance(segment, Arc):
169
- return None
170
- newsegments = list()
171
- if linearize:
172
- s = linearize_segment(segment, interpolation=interpolation, reduce=True)
173
- s = offset_point_array(s, offset)
174
- for idx in range(1, len(s)):
175
- seg = Line(
176
- start=Point(s[idx - 1][0], s[idx - 1][1]),
177
- end=Point(s[idx][0], s[idx][1]),
178
- )
179
- newsegments.append(seg)
180
- else:
181
- centerpt = Point(segment.center)
182
- startpt = centerpt.polar_to(
183
- angle=centerpt.angle_to(segment.start),
184
- distance=centerpt.distance_to(segment.start) + offset,
185
- )
186
- endpt = centerpt.polar_to(
187
- angle=centerpt.angle_to(segment.end),
188
- distance=centerpt.distance_to(segment.end) + offset,
189
- )
190
- newseg = Arc(
191
- startpt,
192
- endpt,
193
- centerpt,
194
- # ccw=ccw,
195
- )
196
- newsegments.append(newseg)
197
- return newsegments
198
-
199
-
200
- def offset_line(segment, offset=0):
201
- if not isinstance(segment, Line):
202
- return None
203
- newseg = copy(segment)
204
- normal_vector = norm_vector(segment.start, segment.end, offset)
205
- newseg.start += normal_vector
206
- newseg.end += normal_vector
207
- # print (f"Old= ({segment.start.x:.0f}, {segment.start.y:.0f})-({segment.end.x:.0f}, {segment.end.y:.0f})")
208
- # print (f"New= ({newsegment.start.x:.0f}, {newsegment.start.y:.0f})-({newsegment.end.x:.0f}, {newsegment.end.y:.0f})")
209
- return [newseg]
210
-
211
-
212
- def offset_quad(segment, offset=0, linearize=False, interpolation=500):
213
- if not isinstance(segment, QuadraticBezier):
214
- return None
215
- cubic = CubicBezier(
216
- start=segment.start,
217
- control1=segment.start + 2 / 3 * (segment.control - segment.start),
218
- control2=segment.end + 2 / 3 * (segment.control - segment.end),
219
- end=segment.end,
220
- )
221
- newsegments = offset_cubic(cubic, offset, linearize, interpolation)
222
-
223
- return newsegments
224
-
225
-
226
- def offset_cubic(segment, offset=0, linearize=False, interpolation=500):
227
- """
228
- To establish an offset for a quadratic or cubic bezier by another cubic bezier
229
- is not possible so this requires approximation.
230
- An acceptable approximation is proposed by Tiller and Hanson:
231
- P1 start point
232
- P2 end point
233
- C1 control point 1
234
- C2 control point 2
235
- You create the offset version of these 3 lines and look for their intersections:
236
- - offset to (P1 C1) -> helper 1
237
- - offset to (C1 C2) -> helper 2
238
- - offset to (P2 C2) -> helper 3
239
- we establish P1-new
240
- the intersections between helper 1 and helper 2 is our new control point C1-new
241
- the intersections between helper 2 and helper 3 is our new control point C2-new
242
-
243
- Beware, this has limitations! It's not dealing well with curves that have cusps
244
- """
245
-
246
- if not isinstance(segment, CubicBezier):
247
- return None
248
- newsegments = list()
249
- if linearize:
250
- s = linearize_segment(segment, interpolation=interpolation, reduce=True)
251
- s = offset_point_array(s, offset)
252
- for idx in range(1, len(s)):
253
- seg = Line(
254
- start=Point(s[idx - 1][0], s[idx - 1][1]),
255
- end=Point(s[idx][0], s[idx][1]),
256
- )
257
- newsegments.append(seg)
258
- else:
259
- newseg = copy(segment)
260
- if segment.control1 == segment.start:
261
- p1 = segment.control2
262
- else:
263
- p1 = segment.control1
264
- normal_vector1 = norm_vector(segment.start, p1, offset)
265
- if segment.control2 == segment.end:
266
- p1 = segment.control1
267
- else:
268
- p1 = segment.control2
269
- normal_vector2 = norm_vector(p1, segment.end, offset)
270
- normal_vector3 = norm_vector(segment.control1, segment.control2, offset)
271
-
272
- newseg.start += normal_vector1
273
- newseg.end += normal_vector2
274
-
275
- v = segment.start + normal_vector1
276
- w = segment.control1 + normal_vector1
277
- x = segment.control1 + normal_vector3
278
- y = segment.control2 + normal_vector3
279
- intersect, s, t = intersect_line_segments(v, w, x, y)
280
- if intersect is None:
281
- # Fallback
282
- intersect = segment.control1 + 0.5 * (normal_vector1 + normal_vector3)
283
- newseg.control1 = intersect
284
-
285
- x = segment.control2 + normal_vector2
286
- y = segment.end + normal_vector2
287
- v = segment.control1 + normal_vector3
288
- w = segment.control2 + normal_vector3
289
- intersect, s, t = intersect_line_segments(v, w, x, y)
290
- if intersect is None:
291
- # Fallback
292
- intersect = segment.control2 + 0.5 * (normal_vector2 + normal_vector3)
293
- newseg.control2 = intersect
294
- # print (f"Old: start=({segment.start.x:.0f}, {segment.start.y:.0f}), c1=({segment.control1.x:.0f}, {segment.control1.y:.0f}), c2=({segment.control2.x:.0f}, {segment.control2.y:.0f}), end=({segment.end.x:.0f}, {segment.end.y:.0f})")
295
- # print (f"New: start=({newsegment.start.x:.0f}, {newsegment.start.y:.0f}), c1=({newsegment.control1.x:.0f}, {newsegment.control1.y:.0f}), c2=({newsegment.control2.x:.0f}, {newsegment.control2.y:.0f}), end=({newsegment.end.x:.0f}, {newsegment.end.y:.0f})")
296
- newsegments.append(newseg)
297
- return newsegments
298
-
299
-
300
- def intersect_line_segments(w, z, x, y):
301
- """
302
- We establish the intersection between two lines given by
303
- line1 = (w, z), line2 = (x, y)
304
- We define the first line by the equation w + s * (z - w)
305
- We define the second line by the equation x + t * (y - x)
306
- We give back the intersection and the values for s and t
307
- out of these two equations at the intersection point.
308
- Notabene: if the intersection is on the two line segments
309
- then s and t need to be between 0 and 1.
310
-
311
- Args:
312
- w (Point): Start point of the first line segment
313
- z (Point): End point of the second line segment
314
- x (Point): Start point of the first line segment
315
- y (Point): End point of the second line segment
316
- Returns three values: P, s, t
317
- P: Point of intersection, None if the two lines have no intersection
318
- S: Value for s in P = w + s * (z - w)
319
- T: Value for t in P = x + t * (y - x)
320
-
321
- ( w1 ) ( z1 - w1 ) ( x1 ) ( y1 - x1 )
322
- ( ) + t ( ) = ( ) + s ( )
323
- ( w2 ) ( z2 - w2 ) ( y1 ) ( y2 - x2 )
324
-
325
- ( w1 - x1 ) ( y1 - x1 ) ( z1 - w1 )
326
- ( ) = s ( ) - t ( )
327
- ( w2 - x2 ) ( y2 - x2 ) ( z2 - w2 )
328
-
329
- ( w1 - x1 ) ( y1 - x1 -z1 + w1 ) ( s )
330
- ( ) = ( ) ( )
331
- ( w2 - x2 ) ( y2 - x2 -z2 + w2 ) ( t )
332
-
333
- """
334
- a = y.x - x.x
335
- b = -z.x + w.x
336
- c = y.y - x.y
337
- d = -z.y + w.y
338
- """
339
- The inverse matrix of
340
- (a b) 1 (d -b)
341
- = -------- * ( )
342
- (c d) ad - bc (-c a)
343
- """
344
- deter = a * d - b * c
345
- if abs(deter) < 1.0e-8:
346
- # They don't have an interference
347
- return None, None, None
348
-
349
- s = 1 / deter * (d * (w.x - x.x) + -b * (w.y - x.y))
350
- t = 1 / deter * (-c * (w.x - x.x) + a * (w.y - x.y))
351
- p1 = w + t * (z - w)
352
- p2 = x + s * (y - x)
353
- # print (f"p1 = ({p1.x:.3f}, {p1.y:.3f})")
354
- # print (f"p2 = ({p2.x:.3f}, {p2.y:.3f})")
355
- p = p1
356
- return p, s, t
357
-
358
-
359
- def offset_path(self, path, offset_value=0):
360
- # As this oveloading a regular method in a class
361
- # it needs to have the very same definition (including the class
362
- # reference self)
363
- p = path_offset(
364
- path,
365
- offset_value=-offset_value,
366
- radial_connector=True,
367
- linearize=True,
368
- interpolation=500,
369
- )
370
- if p is None:
371
- p = path
372
- return p
373
-
374
-
375
- def path_offset(
376
- path, offset_value=0, radial_connector=False, linearize=True, interpolation=500
377
- ):
378
- MINIMAL_LEN = 5
379
-
380
- def stitch_segments_at_index(
381
- offset, stitchpath, seg1_end, orgintersect, radial=False, closed=False
382
- ):
383
- point_added = 0
384
- left_end = seg1_end
385
- lp = len(stitchpath)
386
- right_start = left_end + 1
387
- if right_start >= lp:
388
- if not closed:
389
- return point_added
390
- # Look for the first segment
391
- right_start = right_start % lp
392
- while not isinstance(
393
- stitchpath._segments[right_start],
394
- (Arc, Line, QuadraticBezier, CubicBezier),
395
- ):
396
- right_start += 1
397
- seg1 = stitchpath._segments[left_end]
398
- seg2 = stitchpath._segments[right_start]
399
-
400
- # print (f"Stitch {left_end}: {type(seg1).__name__}, {right_start}: {type(seg2).__name__} - max={len(stitchpath._segments)}")
401
- needs_connector = False
402
- if isinstance(seg1, Close):
403
- # Close will be dealt with differently...
404
- return point_added
405
- if isinstance(seg1, Move):
406
- seg1.end = Point(seg2.start)
407
- return point_added
408
-
409
- if isinstance(seg1, Line):
410
- needs_connector = True
411
- if isinstance(seg2, Line):
412
- p, s, t = intersect_line_segments(
413
- Point(seg1.start),
414
- Point(seg1.end),
415
- Point(seg2.start),
416
- Point(seg2.end),
417
- )
418
- if p is not None:
419
- # We have an intersection
420
- if 0 <= abs(s) <= 1 and 0 <= abs(t) <= 1:
421
- # We shorten the segments accordingly.
422
- seg1.end = Point(p)
423
- seg2.start = Point(p)
424
- if right_start > 0 and isinstance(
425
- stitchpath._segments[right_start - 1], Move
426
- ):
427
- stitchpath._segments[right_start - 1].end = Point(p)
428
- needs_connector = False
429
- # print ("Used internal intersect")
430
- elif not radial:
431
- # is the intersect too far away for our purposes?
432
- odist = orgintersect.distance_to(p)
433
- if odist > abs(offset):
434
- angle = orgintersect.angle_to(p)
435
- p = orgintersect.polar_to(angle, abs(offset))
436
-
437
- newseg1 = Line(seg1.end, p)
438
- newseg2 = Line(p, seg2.start)
439
- stitchpath._segments.insert(left_end + 1, newseg2)
440
- stitchpath._segments.insert(left_end + 1, newseg1)
441
- point_added = 2
442
- needs_connector = False
443
- # print ("Used shortened external intersect")
444
- else:
445
- seg1.end = Point(p)
446
- seg2.start = Point(p)
447
- if right_start > 0 and isinstance(
448
- stitchpath._segments[right_start - 1], Move
449
- ):
450
- stitchpath._segments[right_start - 1].end = Point(p)
451
- needs_connector = False
452
- # print ("Used external intersect")
453
- elif isinstance(seg1, Move):
454
- needs_connector = False
455
- else: # Arc, Quad and Cubic Bezier
456
- needs_connector = True
457
- if isinstance(seg2, Line):
458
- needs_connector = True
459
- elif isinstance(seg2, Move):
460
- needs_connector = False
461
-
462
- if needs_connector and seg1.end != seg2.start:
463
- """
464
- There is a fundamental challenge to this naiive implementation:
465
- if the offset gets bigger you will get intersections of previous segments
466
- which will effectively defeat it. You will end up with connection lines
467
- reaching back creating a loop. Right now there's no real good way
468
- to deal with it:
469
- a) if it would be just the effort to create an offset of your path you
470
- can apply an intersection algorithm like Bentley-Ottman to identify
471
- intersections and remove them (or even simpler just use the
472
- Point.convex_hull method in svgelements).
473
- *BUT*
474
- b) this might defeat the initial purpose of the routine to get some kerf
475
- compensation. So you are effectively eliminating cutlines from your design
476
- which may not be what you want.
477
-
478
- So we try to avoid that by just looking at two consecutive path segments
479
- as these were by definition continuous.
480
- """
481
-
482
- if radial:
483
- # print ("Inserted an arc")
484
- # Let's check whether the distance of these points is smaller
485
- # than the radius
486
-
487
- angle = seg1.end.angle_to(seg1.start) - seg1.end.angle_to(seg2.start)
488
- while angle < 0:
489
- angle += tau
490
- while angle > tau:
491
- angle -= tau
492
- # print (f"Angle: {angle:.2f} ({angle / tau * 360.0:.1f})")
493
- startpt = Point(seg1.end)
494
- endpt = Point(seg2.start)
495
-
496
- if angle >= tau / 2:
497
- ccw = True
498
- else:
499
- ccw = False
500
- # print ("Generate connect-arc")
501
- connect_seg = Arc(
502
- start=startpt, end=endpt, center=Point(orgintersect), ccw=ccw
503
- )
504
- clen = connect_seg.length(error=1e-2)
505
- # print (f"Ratio: {clen / abs(tau * offset):.2f}")
506
- if clen > abs(tau * offset / 2):
507
- # That seems strange...
508
- connect_seg = Line(startpt, endpt)
509
- else:
510
- # print ("Inserted a Line")
511
- connect_seg = Line(Point(seg1.end), Point(seg2.start))
512
- stitchpath._segments.insert(left_end + 1, connect_seg)
513
- point_added = 1
514
- elif needs_connector:
515
- # print ("Need connector but end points were identical")
516
- pass
517
- else:
518
- # print ("No connector needed")
519
- pass
520
- return point_added
521
-
522
- def close_subpath(radial, sub_path, firstidx, lastidx, offset, orgintersect):
523
- # from time import perf_counter
524
- seg1 = None
525
- seg2 = None
526
- very_first = None
527
- very_last = None
528
- # t_start = perf_counter()
529
- idx = firstidx
530
- while idx < len(sub_path._segments) and very_first is None:
531
- seg = sub_path._segments[idx]
532
- if seg.start is not None:
533
- very_first = Point(seg.start)
534
- seg1 = seg
535
- break
536
- idx += 1
537
- idx = lastidx
538
- while idx >= 0 and very_last is None:
539
- seg = sub_path._segments[idx]
540
- if seg.end is not None:
541
- seg2 = seg
542
- very_last = Point(seg.end)
543
- break
544
- idx -= 1
545
- if very_first is None or very_last is None:
546
- return
547
- # print (f"{perf_counter()-t_start:.3f} Found first and last")
548
- seglen = very_first.distance_to(very_last)
549
- if seglen > MINIMAL_LEN:
550
- p, s, t = intersect_line_segments(
551
- Point(seg1.start),
552
- Point(seg1.end),
553
- Point(seg2.start),
554
- Point(seg2.end),
555
- )
556
- if p is not None:
557
- # We have an intersection and shorten the segments accordingly.
558
- d = orgintersect.distance_to(p)
559
- if 0 <= abs(s) <= 1 and 0 <= abs(t) <= 1:
560
- seg1.start = Point(p)
561
- seg2.end = Point(p)
562
- # print (f"{perf_counter()-t_start:.3f} Close subpath by adjusting inner lines, d={d:.2f} vs. offs={offset:.2f}")
563
- elif d >= abs(offset):
564
- if radial:
565
- # print (f"{perf_counter()-t_start:.3f} Insert an arc")
566
- # Let's check whether the distance of these points is smaller
567
- # than the radius
568
-
569
- angle = seg1.end.angle_to(seg1.start) - seg1.end.angle_to(
570
- seg2.start
571
- )
572
- while angle < 0:
573
- angle += tau
574
- while angle > tau:
575
- angle -= tau
576
- # print (f"{perf_counter()-t_start:.3f} Angle: {angle:.2f} ({angle / tau * 360.0:.1f})")
577
- startpt = Point(seg2.end)
578
- endpt = Point(seg1.start)
579
-
580
- if angle >= tau / 2:
581
- ccw = True
582
- else:
583
- ccw = False
584
- # print (f"{perf_counter()-t_start:.3f} Generate connect-arc")
585
- # print (f"{perf_counter()-t_start:.3f} s={startpt}, e={endpt}, c={orgintersect}, ccw={ccw}")
586
- segment = Arc(
587
- start=startpt,
588
- end=endpt,
589
- center=Point(orgintersect),
590
- ccw=ccw,
591
- )
592
- # print (f"{perf_counter()-t_start:.3f} Now calculating length")
593
- clen = segment.length(error=1e-2)
594
- # print (f"{perf_counter()-t_start:.3f} Ratio: {clen / abs(tau * offset):.2f}")
595
- if clen > abs(tau * offset / 2):
596
- # That seems strange...
597
- segment = Line(startpt, endpt)
598
- # print(f"{perf_counter()-t_start:.3f} Inserting segment at {lastidx + 1}...")
599
- sub_path._segments.insert(lastidx + 1, segment)
600
- # print(f"{perf_counter()-t_start:.3f} Done.")
601
-
602
- else:
603
- p = orgintersect.polar_to(
604
- angle=orgintersect.angle_to(p),
605
- distance=abs(offset),
606
- )
607
- segment = Line(p, seg1.start)
608
- sub_path._segments.insert(lastidx + 1, segment)
609
- segment = Line(seg2.end, p)
610
- sub_path._segments.insert(lastidx + 1, segment)
611
- # sub_path._segments.insert(firstidx, segment)
612
- # print (f"Close subpath with interim pt, d={d:.2f} vs. offs={offset:.2f}")
613
- else:
614
- seg1.start = Point(p)
615
- seg2.end = Point(p)
616
- # print (f"Close subpath by adjusting lines, d={d:.2f} vs. offs={offset:.2f}")
617
- else:
618
- segment = Line(very_last, very_first)
619
- sub_path._segments.insert(lastidx + 1, segment)
620
- # print ("Fallback case, just create line")
621
-
622
- def dis(pt):
623
- if pt is None:
624
- return "None"
625
- else:
626
- return f"({pt.x:.0f}, {pt.y:.0f})"
627
-
628
- results = []
629
- # This needs to be a continuous path
630
- spct = 0
631
- for subpath in path.as_subpaths():
632
- spct += 1
633
- # print (f"Subpath {spct}")
634
- p = Path(subpath)
635
- if not linearize:
636
- # p.approximate_arcs_with_cubics()
637
- pass
638
- offset = offset_value
639
- # # No offset bigger than half the path size, otherwise stuff will get crazy
640
- # if offset > 0:
641
- # bb = p.bbox()
642
- # offset = min(offset, bb[2] - bb[0])
643
- # offset = min(offset, bb[3] - bb[1])
644
- is_closed = False
645
- # Lets check the first and last valid point. If they are identical
646
- # we consider this to be a closed path even if it has no closed indicator.
647
- firstp_start = None
648
- lastp = None
649
- idx = 0
650
- while (idx < len(p)) and not isinstance(
651
- p._segments[idx], (Arc, Line, QuadraticBezier, CubicBezier)
652
- ):
653
- idx += 1
654
- firstp_start = Point(p._segments[idx].start)
655
- idx = len(p._segments) - 1
656
- while idx >= 0 and not isinstance(
657
- p._segments[idx], (Arc, Line, QuadraticBezier, CubicBezier)
658
- ):
659
- idx -= 1
660
- lastp = Point(p._segments[idx].end)
661
- if firstp_start.distance_to(lastp) < 1e-3:
662
- is_closed = True
663
- # print ("Seems to be closed!")
664
- # We need to establish if this is a closed path and if the first segment goes counterclockwise
665
- cw = False
666
- if not is_closed:
667
- for idx in range(len(p._segments) - 1, -1, -1):
668
- if isinstance(p._segments[idx], Close):
669
- is_closed = True
670
- break
671
- if is_closed:
672
- cw = is_clockwise(p, 0)
673
- if cw:
674
- offset = -1 * offset_value
675
- # print (f"Subpath: closed={is_closed}, clockwise={cw}")
676
- # Remember the complete subshape (could be multiple segements due to linearization)
677
- last_point = None
678
- first_point = None
679
- is_closed = False
680
- helper1 = None
681
- helper2 = None
682
- for idx in range(len(p._segments) - 1, -1, -1):
683
- segment = p._segments[idx]
684
- # print (f"Deal with seg {idx}: {type(segment).__name__} - {first_point}, {last_point}, {is_closed}")
685
- if isinstance(segment, Close):
686
- # Lets add an additional line and replace the closed segment by this new segment
687
- # Look for the last two valid segments
688
- last_point = None
689
- first_point = None
690
- pt_last = None
691
- pt_first = None
692
- idx1 = idx - 1
693
- while idx1 >= 0:
694
- if isinstance(
695
- p._segments[idx1], (Arc, Line, QuadraticBezier, CubicBezier)
696
- ):
697
- pt_last = Point(p._segments[idx1].end)
698
- break
699
- idx1 -= 1
700
- idx1 -= 1
701
- while idx1 >= 0:
702
- if isinstance(
703
- p._segments[idx1], (Arc, Line, QuadraticBezier, CubicBezier)
704
- ):
705
- pt_first = Point(p._segments[idx1].start)
706
- else:
707
- break
708
- idx1 -= 1
709
- if pt_last is not None and pt_first is not None:
710
- segment = Line(pt_last, pt_first)
711
- p._segments[idx] = segment
712
- last_point = idx
713
- is_closed = True
714
- cw = is_clockwise(p, max(0, idx1))
715
- if cw:
716
- offset = -1 * offset_value
717
- else:
718
- # Invalid close?! Remove it
719
- p._segments.pop(idx)
720
- if last_point is not None:
721
- last_point -= 1
722
- continue
723
- elif isinstance(segment, Move):
724
- if last_point is not None and first_point is not None and is_closed:
725
- seglen = p._segments[first_point].start.distance_to(
726
- p._segments[last_point].end
727
- )
728
- if seglen > MINIMAL_LEN:
729
- close_subpath(
730
- radial_connector,
731
- p,
732
- first_point,
733
- last_point,
734
- offset,
735
- helper2,
736
- )
737
- last_point = None
738
- first_point = None
739
- if segment.start is not None and segment.end is not None:
740
- seglen = segment.start.distance_to(segment.end)
741
- if seglen < MINIMAL_LEN:
742
- # print (f"Skipped: {seglen}")
743
- p._segments.pop(idx)
744
- if last_point is not None:
745
- last_point -= 1
746
- continue
747
- first_point = idx
748
- if last_point is None:
749
- last_point = idx
750
- is_closed = False
751
- offset = offset_value
752
- # We need to establish if this is a closed path and if it goes counterclockwise
753
- # Let establish the range and check whether this is closed
754
- idx1 = last_point - 1
755
- fpt = None
756
- while idx1 >= 0:
757
- seg = p._segments[idx1]
758
- if isinstance(seg, (Line, Arc, QuadraticBezier, CubicBezier)):
759
- fpt = seg.start
760
- idx1 -= 1
761
- if fpt is not None and segment.end.distance_to(fpt) < MINIMAL_LEN:
762
- is_closed = True
763
- cw = is_clockwise(p, max(0, idx1))
764
- if cw:
765
- offset = -1 * offset_value
766
- # print ("Seems to be closed!")
767
- # print (f"Regular point: {idx}, {type(segment).__name__}, {first_point}, {last_point}, {is_closed}")
768
- helper1 = Point(p._segments[idx].end)
769
- helper2 = Point(p._segments[idx].start)
770
- left_end = idx
771
- # print (f"Segment to deal with: {type(segment).__name__}")
772
- if isinstance(segment, Arc):
773
- arclinearize = linearize
774
- # Arc is not working, so we always linearize
775
- arclinearize = True
776
- newsegment = offset_arc(segment, offset, arclinearize, interpolation)
777
- if newsegment is None or len(newsegment) == 0:
778
- continue
779
- left_end = idx - 1 + len(newsegment)
780
- last_point += len(newsegment) - 1
781
- p._segments[idx] = newsegment[0]
782
- for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
783
- p._segments.insert(idx + 1, newsegment[nidx])
784
- elif isinstance(segment, QuadraticBezier):
785
- newsegment = offset_quad(segment, offset, linearize, interpolation)
786
- if newsegment is None or len(newsegment) == 0:
787
- continue
788
- left_end = idx - 1 + len(newsegment)
789
- last_point += len(newsegment) - 1
790
- p._segments[idx] = newsegment[0]
791
- for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
792
- p._segments.insert(idx + 1, newsegment[nidx])
793
- elif isinstance(segment, CubicBezier):
794
- newsegment = offset_cubic(segment, offset, linearize, interpolation)
795
- if newsegment is None or len(newsegment) == 0:
796
- continue
797
- left_end = idx - 1 + len(newsegment)
798
- last_point += len(newsegment) - 1
799
- p._segments[idx] = newsegment[0]
800
- for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
801
- p._segments.insert(idx + 1, newsegment[nidx])
802
- elif isinstance(segment, Line):
803
- newsegment = offset_line(segment, offset)
804
- if newsegment is None or len(newsegment) == 0:
805
- continue
806
- left_end = idx - 1 + len(newsegment)
807
- last_point += len(newsegment) - 1
808
- p._segments[idx] = newsegment[0]
809
- for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
810
- p._segments.insert(idx + 1, newsegment[nidx])
811
- stitched = stitch_segments_at_index(
812
- offset, p, left_end, helper1, radial=radial_connector
813
- )
814
- if last_point is not None:
815
- last_point += stitched
816
- if last_point is not None and first_point is not None and is_closed:
817
- seglen = p._segments[first_point].start.distance_to(
818
- p._segments[last_point].end
819
- )
820
- if seglen > MINIMAL_LEN:
821
- close_subpath(
822
- radial_connector, p, first_point, last_point, offset, helper2
823
- )
824
-
825
- results.append(p)
826
-
827
- if len(results) == 0:
828
- # Strange, should never happen
829
- return path
830
- result = results[0]
831
- for idx in range(1, len(results)):
832
- result += results[idx]
833
- # result.approximate_arcs_with_cubics()
834
- return result
835
-
836
-
837
- def plugin(kernel, lifecycle=None):
838
- _ = kernel.translation
839
- if lifecycle == "postboot":
840
- init_commands(kernel)
841
-
842
-
843
- def init_commands(kernel):
844
- self = kernel.elements
845
-
846
- _ = kernel.translation
847
-
848
- classify_new = self.post_classify
849
- # We are patching the class responsible for Cut nodes in general,
850
- # so that any new instance of this class will be able to use the
851
- # new functionality.
852
- # Notabene: this may be overloaded by another routine (like from pyclipr)
853
- # at a later time.
854
- from meerk40t.core.node.op_cut import CutOpNode
855
-
856
- CutOpNode.offset_routine = offset_path
857
-
858
- @self.console_argument(
859
- "offset",
860
- type=str,
861
- help=_(
862
- "offset to line mm (positive values to left/outside, negative values to right/inside)"
863
- ),
864
- )
865
- @self.console_option(
866
- "radial", "r", action="store_true", type=bool, help=_("radial connector")
867
- )
868
- @self.console_option(
869
- "native",
870
- "n",
871
- action="store_true",
872
- type=bool,
873
- help=_("native path offset (use at you own risk)"),
874
- )
875
- @self.console_option(
876
- "interpolation", "i", type=int, help=_("interpolation points per segment")
877
- )
878
- @self.console_command(
879
- ("offset2", "offset"),
880
- help=_("create an offset path for any of the given elements, old algorithm"),
881
- input_type=(None, "elements"),
882
- output_type="elements",
883
- )
884
- def element_offset_path(
885
- command,
886
- channel,
887
- _,
888
- offset=None,
889
- radial=None,
890
- native=False,
891
- interpolation=None,
892
- data=None,
893
- post=None,
894
- **kwargs,
895
- ):
896
- if data is None:
897
- data = list(self.elems(emphasized=True))
898
- if len(data) == 0:
899
- channel(_("No elements selected"))
900
- return "elements", data
901
- if native:
902
- linearize = False
903
- else:
904
- linearize = True
905
- if interpolation is None:
906
- interpolation = 500
907
- if offset is None:
908
- offset = 0
909
- else:
910
- try:
911
- ll = Length(offset)
912
- # Invert for right behaviour
913
- offset = -1.0 * float(ll)
914
- except ValueError:
915
- offset = 0
916
- if radial is None:
917
- radial = False
918
- data_out = list()
919
- for node in data:
920
- if hasattr(node, "as_path"):
921
- p = abs(node.as_path())
922
- else:
923
- bb = node.bounds
924
- if bb is None:
925
- # Node has no bounds or space, therefore no offset outline.
926
- return "elements", data_out
927
- p = Geomstr.rect(
928
- x=bb[0], y=bb[1], width=bb[2] - bb[0], height=bb[3] - bb[1]
929
- ).as_path()
930
-
931
- node_path = path_offset(
932
- p,
933
- offset,
934
- radial_connector=radial,
935
- linearize=linearize,
936
- interpolation=interpolation,
937
- )
938
- if node_path is None or len(node_path) == 0:
939
- continue
940
- node_path.validate_connections()
941
- newnode = self.elem_branch.add(
942
- path=node_path, type="elem path", stroke=node.stroke
943
- )
944
- newnode.stroke_width = UNITS_PER_PIXEL
945
- newnode.linejoin = Linejoin.JOIN_ROUND
946
- newnode.label = f"Offset of {node.id if node.label is None else node.label}"
947
- data_out.append(newnode)
948
-
949
- # Newly created! Classification needed?
950
- if len(data_out) > 0:
951
- post.append(classify_new(data_out))
952
- self.signal("refresh_scene", "Scene")
953
- return "elements", data_out
954
-
955
- # --------------------------- END COMMANDS ------------------------------
1
+ """
2
+ This adds console commands that deal with the creation of an offset
3
+ """
4
+ from copy import copy
5
+ from math import atan2, tau
6
+
7
+ from meerk40t.core.node.node import Linejoin
8
+ from meerk40t.core.units import UNITS_PER_PIXEL, Length
9
+ from meerk40t.svgelements import (
10
+ Arc,
11
+ Close,
12
+ CubicBezier,
13
+ Line,
14
+ Move,
15
+ Path,
16
+ Point,
17
+ QuadraticBezier,
18
+ )
19
+ from meerk40t.tools.geomstr import Geomstr
20
+
21
+ """
22
+ The following routines deal with the offset of an SVG path at a given distance D.
23
+ An offset or parallel curve can easily be established:
24
+ - for a line segment by another line parallel and in distance D:
25
+ Establish the two normals with length D on the end points and
26
+ create the two new endpoints
27
+ - for an arc segment: elongate rx and ry by D
28
+ To establish an offset for a quadratic or cubic bezier by another cubic bezier
29
+ is not possible so this requires approximation.
30
+ An acceptable approximation is proposed by Tiller and Hanson:
31
+ P1 start point
32
+ P2 end point
33
+ C1 control point 1
34
+ C2 control point 2
35
+ You create the offset version of these 3 lines and look for their intersections:
36
+ - offset to (P1 C1) -> helper 1
37
+ - offset to (C1 C2) -> helper 2
38
+ - offset to (P2 C2) -> helper 3
39
+ we establish P1-new
40
+ the intersections between helper 1 and helper 2 is our new control point C1-new
41
+ the intersections between helper 2 and helper 3 is our new control point C2-new
42
+
43
+
44
+
45
+ A good visual representation can be seen here:
46
+ https://feirell.github.io/offset-bezier/
47
+
48
+ The algorithm deals with the challenge as follows:
49
+ a) It walks through the subpaths of a given path so that we have a continuous curve
50
+ b) It looks at the different segment typs and deals with them,
51
+ generating a new offseted segment
52
+ c) Finally it stitches those segments together, preparing for the simplification
53
+ """
54
+
55
+
56
+ def norm_vector(p1, p2, target_len):
57
+ line_vector = p2 - p1
58
+ # if line_vector.x == 0 and line_vector.y == 0:
59
+ # return Point(target_len, 0)
60
+ factor = target_len
61
+ normal_vector = Point(-1 * line_vector.y, line_vector.x)
62
+ normlen = abs(normal_vector)
63
+ if normlen != 0:
64
+ factor = target_len / normlen
65
+ normal_vector *= factor
66
+ return normal_vector
67
+
68
+
69
+ def is_clockwise(path, start=0):
70
+ def poly_clockwise(poly):
71
+ """
72
+ returns True if the polygon is clockwise ordered, false if not
73
+ """
74
+
75
+ total = (
76
+ poly[-1].x * poly[0].y - poly[0].x * poly[-1].y
77
+ ) # last point to first point
78
+ for i in range(len(poly) - 1):
79
+ total += poly[i].x * poly[i + 1].y - poly[i + 1].x * poly[i].y
80
+
81
+ if total <= 0:
82
+ return True
83
+ else:
84
+ return False
85
+
86
+ poly = []
87
+ idx = start
88
+ while idx < len(path._segments):
89
+ seg = path._segments[idx]
90
+ if isinstance(seg, (Arc, Line, QuadraticBezier, CubicBezier)):
91
+ if len(poly) == 0:
92
+ poly.append(seg.start)
93
+ poly.append(seg.end)
94
+ else:
95
+ if len(poly) > 0:
96
+ break
97
+ idx += 1
98
+ if len(poly) == 0:
99
+ res = True
100
+ else:
101
+ res = poly_clockwise(poly)
102
+ return res
103
+
104
+
105
+ def linearize_segment(segment, interpolation=500, reduce=True):
106
+ slope_tolerance = 0.001
107
+ s = []
108
+ delta = 1.0 / interpolation
109
+ lastpt = None
110
+ t = 0
111
+ last_slope = None
112
+ while t <= 1:
113
+ appendit = True
114
+ np = segment.point(t)
115
+ if lastpt is not None:
116
+ dx = lastpt.x - np.x
117
+ dy = lastpt.y - np.y
118
+ if abs(dx) < 1e-6 and abs(dy) < 1e-6:
119
+ appendit = False
120
+ # identical points!
121
+ else:
122
+ this_slope = atan2(dy, dx)
123
+ if last_slope is not None:
124
+ if abs(last_slope - this_slope) < slope_tolerance:
125
+ # Combine segments, i.e. get rid of mid point
126
+ this_slope = last_slope
127
+ appendit = False
128
+ last_slope = this_slope
129
+
130
+ if appendit or not reduce:
131
+ s.append(np)
132
+ else:
133
+ s[-1] = np
134
+ t += delta
135
+ lastpt = np
136
+ if s[-1] != segment.end:
137
+ np = Point(segment.end)
138
+ s.append(np)
139
+ # print (f"linearize: {type(segment).__name__}")
140
+ # print (f"Start: ({segment.start.x:.0f}, {segment.start.y:.0f}) - ({s[0].x:.0f}, {s[0].y:.0f})")
141
+ # print (f"End: ({segment.end.x:.0f}, {segment.end.y:.0f}) - ({s[-1].x:.0f}, {s[-1].y:.0f})")
142
+ return s
143
+
144
+
145
+ def offset_point_array(points, offset):
146
+ result = list()
147
+ p0 = None
148
+ for idx, p1 in enumerate(points):
149
+ if idx > 0:
150
+ nv = norm_vector(p0, p1, offset)
151
+ result.append(p0 + nv)
152
+ result.append(p1 + nv)
153
+ p0 = Point(p1)
154
+ for idx in range(3, len(result)):
155
+ w = result[idx - 3]
156
+ z = result[idx - 2]
157
+ x = result[idx - 1]
158
+ y = result[idx]
159
+ p_i, s, t = intersect_line_segments(w, z, x, y)
160
+ if p_i is None:
161
+ continue
162
+ result[idx - 2] = Point(p_i)
163
+ result[idx - 1] = Point(p_i)
164
+ return result
165
+
166
+
167
+ def offset_arc(segment, offset=0, linearize=False, interpolation=500):
168
+ if not isinstance(segment, Arc):
169
+ return None
170
+ newsegments = list()
171
+ if linearize:
172
+ s = linearize_segment(segment, interpolation=interpolation, reduce=True)
173
+ s = offset_point_array(s, offset)
174
+ for idx in range(1, len(s)):
175
+ seg = Line(
176
+ start=Point(s[idx - 1][0], s[idx - 1][1]),
177
+ end=Point(s[idx][0], s[idx][1]),
178
+ )
179
+ newsegments.append(seg)
180
+ else:
181
+ centerpt = Point(segment.center)
182
+ startpt = centerpt.polar_to(
183
+ angle=centerpt.angle_to(segment.start),
184
+ distance=centerpt.distance_to(segment.start) + offset,
185
+ )
186
+ endpt = centerpt.polar_to(
187
+ angle=centerpt.angle_to(segment.end),
188
+ distance=centerpt.distance_to(segment.end) + offset,
189
+ )
190
+ newseg = Arc(
191
+ startpt,
192
+ endpt,
193
+ centerpt,
194
+ # ccw=ccw,
195
+ )
196
+ newsegments.append(newseg)
197
+ return newsegments
198
+
199
+
200
+ def offset_line(segment, offset=0):
201
+ if not isinstance(segment, Line):
202
+ return None
203
+ newseg = copy(segment)
204
+ normal_vector = norm_vector(segment.start, segment.end, offset)
205
+ newseg.start += normal_vector
206
+ newseg.end += normal_vector
207
+ # print (f"Old= ({segment.start.x:.0f}, {segment.start.y:.0f})-({segment.end.x:.0f}, {segment.end.y:.0f})")
208
+ # print (f"New= ({newsegment.start.x:.0f}, {newsegment.start.y:.0f})-({newsegment.end.x:.0f}, {newsegment.end.y:.0f})")
209
+ return [newseg]
210
+
211
+
212
+ def offset_quad(segment, offset=0, linearize=False, interpolation=500):
213
+ if not isinstance(segment, QuadraticBezier):
214
+ return None
215
+ cubic = CubicBezier(
216
+ start=segment.start,
217
+ control1=segment.start + 2 / 3 * (segment.control - segment.start),
218
+ control2=segment.end + 2 / 3 * (segment.control - segment.end),
219
+ end=segment.end,
220
+ )
221
+ newsegments = offset_cubic(cubic, offset, linearize, interpolation)
222
+
223
+ return newsegments
224
+
225
+
226
+ def offset_cubic(segment, offset=0, linearize=False, interpolation=500):
227
+ """
228
+ To establish an offset for a quadratic or cubic bezier by another cubic bezier
229
+ is not possible so this requires approximation.
230
+ An acceptable approximation is proposed by Tiller and Hanson:
231
+ P1 start point
232
+ P2 end point
233
+ C1 control point 1
234
+ C2 control point 2
235
+ You create the offset version of these 3 lines and look for their intersections:
236
+ - offset to (P1 C1) -> helper 1
237
+ - offset to (C1 C2) -> helper 2
238
+ - offset to (P2 C2) -> helper 3
239
+ we establish P1-new
240
+ the intersections between helper 1 and helper 2 is our new control point C1-new
241
+ the intersections between helper 2 and helper 3 is our new control point C2-new
242
+
243
+ Beware, this has limitations! It's not dealing well with curves that have cusps
244
+ """
245
+
246
+ if not isinstance(segment, CubicBezier):
247
+ return None
248
+ newsegments = list()
249
+ if linearize:
250
+ s = linearize_segment(segment, interpolation=interpolation, reduce=True)
251
+ s = offset_point_array(s, offset)
252
+ for idx in range(1, len(s)):
253
+ seg = Line(
254
+ start=Point(s[idx - 1][0], s[idx - 1][1]),
255
+ end=Point(s[idx][0], s[idx][1]),
256
+ )
257
+ newsegments.append(seg)
258
+ else:
259
+ newseg = copy(segment)
260
+ if segment.control1 == segment.start:
261
+ p1 = segment.control2
262
+ else:
263
+ p1 = segment.control1
264
+ normal_vector1 = norm_vector(segment.start, p1, offset)
265
+ if segment.control2 == segment.end:
266
+ p1 = segment.control1
267
+ else:
268
+ p1 = segment.control2
269
+ normal_vector2 = norm_vector(p1, segment.end, offset)
270
+ normal_vector3 = norm_vector(segment.control1, segment.control2, offset)
271
+
272
+ newseg.start += normal_vector1
273
+ newseg.end += normal_vector2
274
+
275
+ v = segment.start + normal_vector1
276
+ w = segment.control1 + normal_vector1
277
+ x = segment.control1 + normal_vector3
278
+ y = segment.control2 + normal_vector3
279
+ intersect, s, t = intersect_line_segments(v, w, x, y)
280
+ if intersect is None:
281
+ # Fallback
282
+ intersect = segment.control1 + 0.5 * (normal_vector1 + normal_vector3)
283
+ newseg.control1 = intersect
284
+
285
+ x = segment.control2 + normal_vector2
286
+ y = segment.end + normal_vector2
287
+ v = segment.control1 + normal_vector3
288
+ w = segment.control2 + normal_vector3
289
+ intersect, s, t = intersect_line_segments(v, w, x, y)
290
+ if intersect is None:
291
+ # Fallback
292
+ intersect = segment.control2 + 0.5 * (normal_vector2 + normal_vector3)
293
+ newseg.control2 = intersect
294
+ # print (f"Old: start=({segment.start.x:.0f}, {segment.start.y:.0f}), c1=({segment.control1.x:.0f}, {segment.control1.y:.0f}), c2=({segment.control2.x:.0f}, {segment.control2.y:.0f}), end=({segment.end.x:.0f}, {segment.end.y:.0f})")
295
+ # print (f"New: start=({newsegment.start.x:.0f}, {newsegment.start.y:.0f}), c1=({newsegment.control1.x:.0f}, {newsegment.control1.y:.0f}), c2=({newsegment.control2.x:.0f}, {newsegment.control2.y:.0f}), end=({newsegment.end.x:.0f}, {newsegment.end.y:.0f})")
296
+ newsegments.append(newseg)
297
+ return newsegments
298
+
299
+
300
+ def intersect_line_segments(w, z, x, y):
301
+ """
302
+ We establish the intersection between two lines given by
303
+ line1 = (w, z), line2 = (x, y)
304
+ We define the first line by the equation w + s * (z - w)
305
+ We define the second line by the equation x + t * (y - x)
306
+ We give back the intersection and the values for s and t
307
+ out of these two equations at the intersection point.
308
+ Notabene: if the intersection is on the two line segments
309
+ then s and t need to be between 0 and 1.
310
+
311
+ Args:
312
+ w (Point): Start point of the first line segment
313
+ z (Point): End point of the second line segment
314
+ x (Point): Start point of the first line segment
315
+ y (Point): End point of the second line segment
316
+ Returns three values: P, s, t
317
+ P: Point of intersection, None if the two lines have no intersection
318
+ S: Value for s in P = w + s * (z - w)
319
+ T: Value for t in P = x + t * (y - x)
320
+
321
+ ( w1 ) ( z1 - w1 ) ( x1 ) ( y1 - x1 )
322
+ ( ) + t ( ) = ( ) + s ( )
323
+ ( w2 ) ( z2 - w2 ) ( y1 ) ( y2 - x2 )
324
+
325
+ ( w1 - x1 ) ( y1 - x1 ) ( z1 - w1 )
326
+ ( ) = s ( ) - t ( )
327
+ ( w2 - x2 ) ( y2 - x2 ) ( z2 - w2 )
328
+
329
+ ( w1 - x1 ) ( y1 - x1 -z1 + w1 ) ( s )
330
+ ( ) = ( ) ( )
331
+ ( w2 - x2 ) ( y2 - x2 -z2 + w2 ) ( t )
332
+
333
+ """
334
+ a = y.x - x.x
335
+ b = -z.x + w.x
336
+ c = y.y - x.y
337
+ d = -z.y + w.y
338
+ """
339
+ The inverse matrix of
340
+ (a b) 1 (d -b)
341
+ = -------- * ( )
342
+ (c d) ad - bc (-c a)
343
+ """
344
+ deter = a * d - b * c
345
+ if abs(deter) < 1.0e-8:
346
+ # They don't have an interference
347
+ return None, None, None
348
+
349
+ s = 1 / deter * (d * (w.x - x.x) + -b * (w.y - x.y))
350
+ t = 1 / deter * (-c * (w.x - x.x) + a * (w.y - x.y))
351
+ p1 = w + t * (z - w)
352
+ # p2 = x + s * (y - x)
353
+ # print (f"p1 = ({p1.x:.3f}, {p1.y:.3f})")
354
+ # print (f"p2 = ({p2.x:.3f}, {p2.y:.3f})")
355
+ p = p1
356
+ return p, s, t
357
+
358
+
359
+ def offset_path(self, path, offset_value=0):
360
+ # As this oveloading a regular method in a class
361
+ # it needs to have the very same definition (including the class
362
+ # reference self)
363
+ p = path_offset(
364
+ path,
365
+ offset_value=-offset_value,
366
+ radial_connector=True,
367
+ linearize=True,
368
+ interpolation=500,
369
+ )
370
+ if p is None:
371
+ return path
372
+ g = Geomstr.svg(p)
373
+ if g.index:
374
+ # We are already at device resolution, so we need to reduce tolerance a lot
375
+ # Standard is 25 tats, so about 1/3 of a mil
376
+ p = g.simplify(tolerance=0.1).as_path()
377
+ p.stroke = path.stroke
378
+ p.fill = path.fill
379
+ return p
380
+
381
+
382
+ def path_offset(
383
+ path, offset_value=0, radial_connector=False, linearize=True, interpolation=500
384
+ ):
385
+ MINIMAL_LEN = 5
386
+
387
+ def stitch_segments_at_index(
388
+ offset, stitchpath, seg1_end, orgintersect, radial=False, closed=False
389
+ ):
390
+ point_added = 0
391
+ left_end = seg1_end
392
+ lp = len(stitchpath)
393
+ right_start = left_end + 1
394
+ if right_start >= lp:
395
+ if not closed:
396
+ return point_added
397
+ # Look for the first segment
398
+ right_start = right_start % lp
399
+ while not isinstance(
400
+ stitchpath._segments[right_start],
401
+ (Arc, Line, QuadraticBezier, CubicBezier),
402
+ ):
403
+ right_start += 1
404
+ seg1 = stitchpath._segments[left_end]
405
+ seg2 = stitchpath._segments[right_start]
406
+
407
+ # print (f"Stitch {left_end}: {type(seg1).__name__}, {right_start}: {type(seg2).__name__} - max={len(stitchpath._segments)}")
408
+ if isinstance(seg1, Close):
409
+ # Close will be dealt with differently...
410
+ return point_added
411
+ if isinstance(seg1, Move):
412
+ seg1.end = Point(seg2.start)
413
+ return point_added
414
+
415
+ if isinstance(seg1, Line):
416
+ needs_connector = True
417
+ if isinstance(seg2, Line):
418
+ p, s, t = intersect_line_segments(
419
+ Point(seg1.start),
420
+ Point(seg1.end),
421
+ Point(seg2.start),
422
+ Point(seg2.end),
423
+ )
424
+ if p is not None:
425
+ # We have an intersection
426
+ if 0 <= abs(s) <= 1 and 0 <= abs(t) <= 1:
427
+ # We shorten the segments accordingly.
428
+ seg1.end = Point(p)
429
+ seg2.start = Point(p)
430
+ if right_start > 0 and isinstance(
431
+ stitchpath._segments[right_start - 1], Move
432
+ ):
433
+ stitchpath._segments[right_start - 1].end = Point(p)
434
+ needs_connector = False
435
+ # print ("Used internal intersect")
436
+ elif not radial:
437
+ # is the intersection too far away for our purposes?
438
+ odist = orgintersect.distance_to(p)
439
+ if odist > abs(offset):
440
+ angle = orgintersect.angle_to(p)
441
+ p = orgintersect.polar_to(angle, abs(offset))
442
+
443
+ newseg1 = Line(seg1.end, p)
444
+ newseg2 = Line(p, seg2.start)
445
+ stitchpath._segments.insert(left_end + 1, newseg2)
446
+ stitchpath._segments.insert(left_end + 1, newseg1)
447
+ point_added = 2
448
+ needs_connector = False
449
+ # print ("Used shortened external intersect")
450
+ else:
451
+ seg1.end = Point(p)
452
+ seg2.start = Point(p)
453
+ if right_start > 0 and isinstance(
454
+ stitchpath._segments[right_start - 1], Move
455
+ ):
456
+ stitchpath._segments[right_start - 1].end = Point(p)
457
+ needs_connector = False
458
+ # print ("Used external intersect")
459
+ elif isinstance(seg1, Move):
460
+ needs_connector = False
461
+ else: # Arc, Quad and Cubic Bezier
462
+ needs_connector = True
463
+ if isinstance(seg2, Line):
464
+ needs_connector = True
465
+ elif isinstance(seg2, Move):
466
+ needs_connector = False
467
+
468
+ if needs_connector and seg1.end != seg2.start:
469
+ """
470
+ There is a fundamental challenge to this naiive implementation:
471
+ if the offset gets bigger you will get intersections of previous segments
472
+ which will effectively defeat it. You will end up with connection lines
473
+ reaching back creating a loop. Right now there's no real good way
474
+ to deal with it:
475
+ a) if it would be just the effort to create an offset of your path you
476
+ can apply an intersection algorithm like Bentley-Ottman to identify
477
+ intersections and remove them (or even simpler just use the
478
+ Point.convex_hull method in svgelements).
479
+ *BUT*
480
+ b) this might defeat the initial purpose of the routine to get some kerf
481
+ compensation. So you are effectively eliminating cutlines from your design
482
+ which may not be what you want.
483
+
484
+ So we try to avoid that by just looking at two consecutive path segments
485
+ as these were by definition continuous.
486
+ """
487
+
488
+ if radial:
489
+ # print ("Inserted an arc")
490
+ # Let's check whether the distance of these points is smaller
491
+ # than the radius
492
+
493
+ angle = seg1.end.angle_to(seg1.start) - seg1.end.angle_to(seg2.start)
494
+ while angle < 0:
495
+ angle += tau
496
+ while angle > tau:
497
+ angle -= tau
498
+ # print (f"Angle: {angle:.2f} ({angle / tau * 360.0:.1f})")
499
+ startpt = Point(seg1.end)
500
+ endpt = Point(seg2.start)
501
+
502
+ if angle >= tau / 2:
503
+ ccw = True
504
+ else:
505
+ ccw = False
506
+ # print ("Generate connect-arc")
507
+ connect_seg = Arc(
508
+ start=startpt, end=endpt, center=Point(orgintersect), ccw=ccw
509
+ )
510
+ clen = connect_seg.length(error=1e-2)
511
+ # print (f"Ratio: {clen / abs(tau * offset):.2f}")
512
+ if clen > abs(tau * offset / 2):
513
+ # That seems strange...
514
+ connect_seg = Line(startpt, endpt)
515
+ else:
516
+ # print ("Inserted a Line")
517
+ connect_seg = Line(Point(seg1.end), Point(seg2.start))
518
+ stitchpath._segments.insert(left_end + 1, connect_seg)
519
+ point_added = 1
520
+ elif needs_connector:
521
+ # print ("Need connector but end points were identical")
522
+ pass
523
+ else:
524
+ # print ("No connector needed")
525
+ pass
526
+ return point_added
527
+
528
+ def close_subpath(radial, sub_path, firstidx, lastidx, offset, orgintersect):
529
+ # from time import perf_counter
530
+ seg1 = None
531
+ seg2 = None
532
+ very_first = None
533
+ very_last = None
534
+ # t_start = perf_counter()
535
+ idx = firstidx
536
+ while idx < len(sub_path._segments) and very_first is None:
537
+ seg = sub_path._segments[idx]
538
+ if seg.start is not None:
539
+ very_first = Point(seg.start)
540
+ seg1 = seg
541
+ break
542
+ idx += 1
543
+ idx = lastidx
544
+ while idx >= 0 and very_last is None:
545
+ seg = sub_path._segments[idx]
546
+ if seg.end is not None:
547
+ seg2 = seg
548
+ very_last = Point(seg.end)
549
+ break
550
+ idx -= 1
551
+ if very_first is None or very_last is None:
552
+ return
553
+ # print (f"{perf_counter()-t_start:.3f} Found first and last")
554
+ seglen = very_first.distance_to(very_last)
555
+ if seglen > MINIMAL_LEN:
556
+ p, s, t = intersect_line_segments(
557
+ Point(seg1.start),
558
+ Point(seg1.end),
559
+ Point(seg2.start),
560
+ Point(seg2.end),
561
+ )
562
+ if p is not None:
563
+ # We have an intersection and shorten the segments accordingly.
564
+ d = orgintersect.distance_to(p)
565
+ if 0 <= abs(s) <= 1 and 0 <= abs(t) <= 1:
566
+ seg1.start = Point(p)
567
+ seg2.end = Point(p)
568
+ # print (f"{perf_counter()-t_start:.3f} Close subpath by adjusting inner lines, d={d:.2f} vs. offs={offset:.2f}")
569
+ elif d >= abs(offset):
570
+ if radial:
571
+ # print (f"{perf_counter()-t_start:.3f} Insert an arc")
572
+ # Let's check whether the distance of these points is smaller
573
+ # than the radius
574
+
575
+ angle = seg1.end.angle_to(seg1.start) - seg1.end.angle_to(
576
+ seg2.start
577
+ )
578
+ while angle < 0:
579
+ angle += tau
580
+ while angle > tau:
581
+ angle -= tau
582
+ # print (f"{perf_counter()-t_start:.3f} Angle: {angle:.2f} ({angle / tau * 360.0:.1f})")
583
+ startpt = Point(seg2.end)
584
+ endpt = Point(seg1.start)
585
+
586
+ if angle >= tau / 2:
587
+ ccw = True
588
+ else:
589
+ ccw = False
590
+ # print (f"{perf_counter()-t_start:.3f} Generate connect-arc")
591
+ # print (f"{perf_counter()-t_start:.3f} s={startpt}, e={endpt}, c={orgintersect}, ccw={ccw}")
592
+ segment = Arc(
593
+ start=startpt,
594
+ end=endpt,
595
+ center=Point(orgintersect),
596
+ ccw=ccw,
597
+ )
598
+ # print (f"{perf_counter()-t_start:.3f} Now calculating length")
599
+ clen = segment.length(error=1e-2)
600
+ # print (f"{perf_counter()-t_start:.3f} Ratio: {clen / abs(tau * offset):.2f}")
601
+ if clen > abs(tau * offset / 2):
602
+ # That seems strange...
603
+ segment = Line(startpt, endpt)
604
+ # print(f"{perf_counter()-t_start:.3f} Inserting segment at {lastidx + 1}...")
605
+ sub_path._segments.insert(lastidx + 1, segment)
606
+ # print(f"{perf_counter()-t_start:.3f} Done.")
607
+
608
+ else:
609
+ p = orgintersect.polar_to(
610
+ angle=orgintersect.angle_to(p),
611
+ distance=abs(offset),
612
+ )
613
+ segment = Line(p, seg1.start)
614
+ sub_path._segments.insert(lastidx + 1, segment)
615
+ segment = Line(seg2.end, p)
616
+ sub_path._segments.insert(lastidx + 1, segment)
617
+ # sub_path._segments.insert(firstidx, segment)
618
+ # print (f"Close subpath with interim pt, d={d:.2f} vs. offs={offset:.2f}")
619
+ else:
620
+ seg1.start = Point(p)
621
+ seg2.end = Point(p)
622
+ # print (f"Close subpath by adjusting lines, d={d:.2f} vs. offs={offset:.2f}")
623
+ else:
624
+ segment = Line(very_last, very_first)
625
+ sub_path._segments.insert(lastidx + 1, segment)
626
+ # print ("Fallback case, just create line")
627
+
628
+ # def dis(pt):
629
+ # if pt is None:
630
+ # return "None"
631
+ # else:
632
+ # return f"({pt.x:.0f}, {pt.y:.0f})"
633
+
634
+ results = []
635
+ # This needs to be a continuous path
636
+ spct = 0
637
+ for subpath in path.as_subpaths():
638
+ spct += 1
639
+ # print (f"Subpath {spct}")
640
+ p = Path(subpath)
641
+ if not linearize:
642
+ # p.approximate_arcs_with_cubics()
643
+ pass
644
+ offset = offset_value
645
+ # # No offset bigger than half the path size, otherwise stuff will get crazy
646
+ # if offset > 0:
647
+ # bb = p.bbox()
648
+ # offset = min(offset, bb[2] - bb[0])
649
+ # offset = min(offset, bb[3] - bb[1])
650
+ is_closed = False
651
+ # Let's check the first and last valid point. If they are identical
652
+ # we consider this to be a closed path even if it has no closed indicator.
653
+ # firstp_start = None
654
+ # lastp = None
655
+ idx = 0
656
+ while (idx < len(p)) and not isinstance(
657
+ p._segments[idx], (Arc, Line, QuadraticBezier, CubicBezier)
658
+ ):
659
+ idx += 1
660
+ firstp_start = Point(p._segments[idx].start)
661
+ idx = len(p._segments) - 1
662
+ while idx >= 0 and not isinstance(
663
+ p._segments[idx], (Arc, Line, QuadraticBezier, CubicBezier)
664
+ ):
665
+ idx -= 1
666
+ lastp = Point(p._segments[idx].end)
667
+ if firstp_start.distance_to(lastp) < 1e-3:
668
+ is_closed = True
669
+ # print ("Seems to be closed!")
670
+ # We need to establish if this is a closed path and if the first segment goes counterclockwise
671
+ cw = False
672
+ if not is_closed:
673
+ for idx in range(len(p._segments) - 1, -1, -1):
674
+ if isinstance(p._segments[idx], Close):
675
+ is_closed = True
676
+ break
677
+ if is_closed:
678
+ cw = is_clockwise(p, 0)
679
+ if cw:
680
+ offset = -1 * offset_value
681
+ # print (f"Subpath: closed={is_closed}, clockwise={cw}")
682
+ # Remember the complete subshape (could be multiple segements due to linearization)
683
+ last_point = None
684
+ first_point = None
685
+ is_closed = False
686
+ helper1 = None
687
+ helper2 = None
688
+ for idx in range(len(p._segments) - 1, -1, -1):
689
+ segment = p._segments[idx]
690
+ # print (f"Deal with seg {idx}: {type(segment).__name__} - {first_point}, {last_point}, {is_closed}")
691
+ if isinstance(segment, Close):
692
+ # Let's add a line and replace the closed segment by this new segment
693
+ # Look for the last two valid segments
694
+ last_point = None
695
+ first_point = None
696
+ pt_last = None
697
+ pt_first = None
698
+ idx1 = idx - 1
699
+ while idx1 >= 0:
700
+ if isinstance(
701
+ p._segments[idx1], (Arc, Line, QuadraticBezier, CubicBezier)
702
+ ):
703
+ pt_last = Point(p._segments[idx1].end)
704
+ break
705
+ idx1 -= 1
706
+ idx1 -= 1
707
+ while idx1 >= 0:
708
+ if isinstance(
709
+ p._segments[idx1], (Arc, Line, QuadraticBezier, CubicBezier)
710
+ ):
711
+ pt_first = Point(p._segments[idx1].start)
712
+ else:
713
+ break
714
+ idx1 -= 1
715
+ if pt_last is not None and pt_first is not None:
716
+ segment = Line(pt_last, pt_first)
717
+ p._segments[idx] = segment
718
+ last_point = idx
719
+ is_closed = True
720
+ cw = is_clockwise(p, max(0, idx1))
721
+ if cw:
722
+ offset = -1 * offset_value
723
+ else:
724
+ # Invalid close?! Remove it
725
+ p._segments.pop(idx)
726
+ if last_point is not None:
727
+ last_point -= 1
728
+ continue
729
+ elif isinstance(segment, Move):
730
+ if last_point is not None and first_point is not None and is_closed:
731
+ seglen = p._segments[first_point].start.distance_to(
732
+ p._segments[last_point].end
733
+ )
734
+ if seglen > MINIMAL_LEN:
735
+ close_subpath(
736
+ radial_connector,
737
+ p,
738
+ first_point,
739
+ last_point,
740
+ offset,
741
+ helper2,
742
+ )
743
+ last_point = None
744
+ first_point = None
745
+ if segment.start is not None and segment.end is not None:
746
+ seglen = segment.start.distance_to(segment.end)
747
+ if seglen < MINIMAL_LEN:
748
+ # print (f"Skipped: {seglen}")
749
+ p._segments.pop(idx)
750
+ if last_point is not None:
751
+ last_point -= 1
752
+ continue
753
+ first_point = idx
754
+ if last_point is None:
755
+ last_point = idx
756
+ is_closed = False
757
+ offset = offset_value
758
+ # We need to establish if this is a closed path and if it goes counterclockwise
759
+ # Let establish the range and check whether this is closed
760
+ idx1 = last_point - 1
761
+ fpt = None
762
+ while idx1 >= 0:
763
+ seg = p._segments[idx1]
764
+ if isinstance(seg, (Line, Arc, QuadraticBezier, CubicBezier)):
765
+ fpt = seg.start
766
+ idx1 -= 1
767
+ if fpt is not None and segment.end.distance_to(fpt) < MINIMAL_LEN:
768
+ is_closed = True
769
+ cw = is_clockwise(p, max(0, idx1))
770
+ if cw:
771
+ offset = -1 * offset_value
772
+ # print ("Seems to be closed!")
773
+ # print (f"Regular point: {idx}, {type(segment).__name__}, {first_point}, {last_point}, {is_closed}")
774
+ helper1 = Point(p._segments[idx].end)
775
+ helper2 = Point(p._segments[idx].start)
776
+ left_end = idx
777
+ # print (f"Segment to deal with: {type(segment).__name__}")
778
+ if isinstance(segment, Arc):
779
+ arclinearize = linearize
780
+ # Arc is not working, so we always linearize
781
+ arclinearize = True
782
+ newsegment = offset_arc(segment, offset, arclinearize, interpolation)
783
+ if newsegment is None or len(newsegment) == 0:
784
+ continue
785
+ left_end = idx - 1 + len(newsegment)
786
+ last_point += len(newsegment) - 1
787
+ p._segments[idx] = newsegment[0]
788
+ for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
789
+ p._segments.insert(idx + 1, newsegment[nidx])
790
+ elif isinstance(segment, QuadraticBezier):
791
+ newsegment = offset_quad(segment, offset, linearize, interpolation)
792
+ if newsegment is None or len(newsegment) == 0:
793
+ continue
794
+ left_end = idx - 1 + len(newsegment)
795
+ last_point += len(newsegment) - 1
796
+ p._segments[idx] = newsegment[0]
797
+ for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
798
+ p._segments.insert(idx + 1, newsegment[nidx])
799
+ elif isinstance(segment, CubicBezier):
800
+ newsegment = offset_cubic(segment, offset, linearize, interpolation)
801
+ if newsegment is None or len(newsegment) == 0:
802
+ continue
803
+ left_end = idx - 1 + len(newsegment)
804
+ last_point += len(newsegment) - 1
805
+ p._segments[idx] = newsegment[0]
806
+ for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
807
+ p._segments.insert(idx + 1, newsegment[nidx])
808
+ elif isinstance(segment, Line):
809
+ newsegment = offset_line(segment, offset)
810
+ if newsegment is None or len(newsegment) == 0:
811
+ continue
812
+ left_end = idx - 1 + len(newsegment)
813
+ last_point += len(newsegment) - 1
814
+ p._segments[idx] = newsegment[0]
815
+ for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
816
+ p._segments.insert(idx + 1, newsegment[nidx])
817
+ stitched = stitch_segments_at_index(
818
+ offset, p, left_end, helper1, radial=radial_connector
819
+ )
820
+ if last_point is not None:
821
+ last_point += stitched
822
+ if last_point is not None and first_point is not None and is_closed:
823
+ seglen = p._segments[first_point].start.distance_to(
824
+ p._segments[last_point].end
825
+ )
826
+ if seglen > MINIMAL_LEN:
827
+ close_subpath(
828
+ radial_connector, p, first_point, last_point, offset, helper2
829
+ )
830
+
831
+ results.append(p)
832
+
833
+ if len(results) == 0:
834
+ # Strange, should never happen
835
+ return path
836
+ result = results[0]
837
+ for idx in range(1, len(results)):
838
+ result += results[idx]
839
+ # result.approximate_arcs_with_cubics()
840
+ return result
841
+
842
+
843
+ def plugin(kernel, lifecycle=None):
844
+ _ = kernel.translation
845
+ if lifecycle == "postboot":
846
+ init_commands(kernel)
847
+
848
+
849
+ def init_commands(kernel):
850
+ self = kernel.elements
851
+
852
+ _ = kernel.translation
853
+
854
+ classify_new = self.post_classify
855
+ # We are patching the class responsible for Cut nodes in general,
856
+ # so that any new instance of this class will be able to use the
857
+ # new functionality.
858
+ # Notabene: this may be overloaded by another routine (like from pyclipr)
859
+ # at a later time.
860
+ from meerk40t.core.node.op_cut import CutOpNode
861
+
862
+ CutOpNode.offset_routine = offset_path
863
+
864
+ @self.console_argument(
865
+ "offset",
866
+ type=str,
867
+ help=_(
868
+ "offset to line mm (positive values to left/outside, negative values to right/inside)"
869
+ ),
870
+ )
871
+ @self.console_option(
872
+ "radial", "r", action="store_true", type=bool, help=_("radial connector")
873
+ )
874
+ @self.console_option(
875
+ "native",
876
+ "n",
877
+ action="store_true",
878
+ type=bool,
879
+ help=_("native path offset (use at you own risk)"),
880
+ )
881
+ @self.console_option(
882
+ "interpolation", "i", type=int, help=_("interpolation points per segment")
883
+ )
884
+ @self.console_command(
885
+ ("offset2", "offset"),
886
+ help=_("create an offset path for any of the given elements, old algorithm"),
887
+ input_type=(None, "elements"),
888
+ output_type="elements",
889
+ )
890
+ def element_offset_path(
891
+ command,
892
+ channel,
893
+ _,
894
+ offset=None,
895
+ radial=None,
896
+ native=False,
897
+ interpolation=None,
898
+ data=None,
899
+ post=None,
900
+ **kwargs,
901
+ ):
902
+ if data is None:
903
+ data = list(self.elems(emphasized=True))
904
+ if len(data) == 0:
905
+ channel(_("No elements selected"))
906
+ return "elements", data
907
+ if native:
908
+ linearize = False
909
+ else:
910
+ linearize = True
911
+ if interpolation is None:
912
+ interpolation = 500
913
+ if offset is None:
914
+ offset = 0
915
+ else:
916
+ try:
917
+ ll = Length(offset)
918
+ # Invert for right behaviour
919
+ offset = -1.0 * float(ll)
920
+ except ValueError:
921
+ offset = 0
922
+ if radial is None:
923
+ radial = False
924
+ data_out = list()
925
+ for node in data:
926
+ if hasattr(node, "as_path"):
927
+ p = abs(node.as_path())
928
+ else:
929
+ bb = node.bounds
930
+ if bb is None:
931
+ # Node has no bounds or space, therefore no offset outline.
932
+ return "elements", data_out
933
+ p = Geomstr.rect(
934
+ x=bb[0], y=bb[1], width=bb[2] - bb[0], height=bb[3] - bb[1]
935
+ ).as_path()
936
+
937
+ node_path = path_offset(
938
+ p,
939
+ offset,
940
+ radial_connector=radial,
941
+ linearize=linearize,
942
+ interpolation=interpolation,
943
+ )
944
+ if node_path is None or len(node_path) == 0:
945
+ continue
946
+ node_path.validate_connections()
947
+ newnode = self.elem_branch.add(
948
+ path=node_path, type="elem path", stroke=node.stroke
949
+ )
950
+ newnode.stroke_width = UNITS_PER_PIXEL
951
+ newnode.linejoin = Linejoin.JOIN_ROUND
952
+ newnode.label = (
953
+ f"Offset of {node.id if node.label is None else node.display_label()}"
954
+ )
955
+ data_out.append(newnode)
956
+
957
+ # Newly created! Classification needed?
958
+ if len(data_out) > 0:
959
+ post.append(classify_new(data_out))
960
+ self.signal("refresh_scene", "Scene")
961
+ return "elements", data_out
962
+
963
+ # --------------------------- END COMMANDS ------------------------------