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