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,2074 +1,2088 @@
1
- import math
2
- from copy import copy
3
-
4
- import wx
5
-
6
- from meerk40t.gui.icons import (
7
- STD_ICON_SIZE,
8
- icon_node_add,
9
- icon_node_append,
10
- icon_node_break,
11
- icon_node_close,
12
- icon_node_curve,
13
- icon_node_delete,
14
- icon_node_join,
15
- icon_node_line,
16
- icon_node_line_all,
17
- icon_node_smooth,
18
- icon_node_smooth_all,
19
- icon_node_symmetric,
20
- )
21
- from meerk40t.gui.laserrender import swizzlecolor
22
- from meerk40t.gui.mwindow import MWindow
23
- from meerk40t.gui.scene.sceneconst import (
24
- RESPONSE_CHAIN,
25
- RESPONSE_CONSUME,
26
- RESPONSE_DROP,
27
- )
28
- from meerk40t.gui.toolwidgets.toolwidget import ToolWidget
29
- from meerk40t.kernel import signal_listener
30
- from meerk40t.svgelements import (
31
- Arc,
32
- Close,
33
- CubicBezier,
34
- Line,
35
- Move,
36
- Path,
37
- Point,
38
- Polygon,
39
- Polyline,
40
- QuadraticBezier,
41
- )
42
- from meerk40t.tools.geomstr import Geomstr
43
-
44
- _ = wx.GetTranslation
45
-
46
-
47
- class EditTool(ToolWidget):
48
- """
49
- Edit tool allows you to view and edit the nodes within a
50
- selected element in the scene. It can currently handle
51
- polylines / polygons and paths.
52
- """
53
-
54
- def __init__(self, scene):
55
- ToolWidget.__init__(self, scene)
56
- self._listener_active = False
57
- self.nodes = []
58
- self.shape = None
59
- self.path = None
60
- self.element = None
61
- self.selected_index = None
62
-
63
- self.move_type = "node"
64
- self.node_type = "path"
65
- self.p1 = None
66
- self.p2 = None
67
- self.pen = wx.Pen()
68
- self.pen.SetColour(wx.BLUE)
69
- # wx.Colour(swizzlecolor(self.scene.context.elements.default_stroke))
70
- self.pen_ctrl = wx.Pen()
71
- self.pen_ctrl.SetColour(wx.CYAN)
72
- self.pen_ctrl_semi = wx.Pen()
73
- self.pen_ctrl_semi.SetColour(wx.GREEN)
74
- self.pen_highlight = wx.Pen()
75
- self.pen_highlight.SetColour(wx.RED)
76
- self.pen_highlight_line = wx.Pen()
77
- self.pen_highlight_line.SetColour(wx.Colour(255, 0, 0, 80))
78
- self.pen_selection = wx.Pen()
79
- self.pen_selection.SetColour(self.scene.colors.color_selection3)
80
- self.pen_selection.SetStyle(wx.PENSTYLE_SHORT_DASH)
81
- self.brush_highlight = wx.Brush(wx.RED_BRUSH)
82
- self.brush_normal = wx.Brush(wx.TRANSPARENT_BRUSH)
83
- # want to have sharp edges
84
- self.pen_selection.SetJoin(wx.JOIN_MITER)
85
- # "key": (routine, info, available for poly, available for path)
86
- self.commands = {
87
- "d": (self.delete_nodes, _("Delete"), True, True),
88
- "l": (self.convert_to_line, _("Line"), False, True),
89
- "c": (self.convert_to_curve, _("Curve"), False, True),
90
- "s": (self.cubic_symmetrical, _("Symmetric"), False, True),
91
- "i": (self.insert_midpoint, _("Insert"), True, True),
92
- "a": (self.append_line, _("Append"), True, True),
93
- "b": (self.break_path, _("Break"), False, True),
94
- "j": (self.join_path, _("Join"), False, True),
95
- "o": (self.smoothen, _("Smooth"), False, True),
96
- "z": (self.toggle_close, _("Close path"), True, True),
97
- "v": (self.smoothen_all, _("Smooth all"), False, True),
98
- "w": (self.linear_all, _("Line all"), False, True),
99
- "p": (self.convert_to_path, _("To path"), True, False),
100
- }
101
- self.define_buttons()
102
- self.message = ""
103
-
104
- def define_buttons(self):
105
- def becomes_enabled(needs_selection, active_for_path, active_for_poly):
106
- def routine(*args):
107
- # print(
108
- # f"Was asked to perform with {my_selection}, {my_active_poly}, {my_active_path} while {self.anyselected} + {self.node_type}"
109
- # )
110
- if self.element is None:
111
- return False
112
- flag_sel = True
113
- flag_poly = False
114
- flag_path = False
115
- if my_selection and not self.anyselected:
116
- flag_sel = False
117
- if my_active_poly and self.node_type == "polyline":
118
- flag_poly = True
119
- if my_active_path and self.node_type == "path":
120
- flag_path = True
121
- flag = flag_sel and (flag_path or flag_poly)
122
- return flag
123
-
124
- my_selection = needs_selection
125
- my_active_poly = active_for_poly
126
- my_active_path = active_for_path
127
- return routine
128
-
129
- def becomes_visible(active_for_path, active_for_poly):
130
- def routine(*args):
131
- # print(
132
- # f"Was asked to perform with {my_active_poly}, {my_active_path} while {self.anyselected} + {self.node_type}"
133
- # )
134
- flag_poly = False
135
- flag_path = False
136
- if my_active_poly and self.node_type == "polyline":
137
- flag_poly = True
138
- if my_active_path and self.node_type == "path":
139
- flag_path = True
140
- flag = flag_path or flag_poly
141
- return flag
142
-
143
- my_active_path = active_for_path
144
- my_active_poly = active_for_poly
145
- return routine
146
-
147
- def do_action(code):
148
- def routine(*args):
149
- self.perform_action(mycode)
150
-
151
- mycode = code
152
- return routine
153
-
154
- cmd_icons = {
155
- # "command": [
156
- # image, requires_selection,
157
- # active_for_path, active_for_poly,
158
- # "tooltiptext", button],
159
- "i": [
160
- icon_node_add,
161
- True,
162
- True,
163
- True,
164
- _("Insert point before"),
165
- _("Insert"),
166
- ],
167
- "a": [
168
- icon_node_append,
169
- False,
170
- True,
171
- True,
172
- _("Append point at end"),
173
- _("Append"),
174
- ],
175
- "d": [
176
- icon_node_delete,
177
- True,
178
- True,
179
- True,
180
- _("Delete point"),
181
- _("Delete"),
182
- ],
183
- "l": [
184
- icon_node_line,
185
- True,
186
- True,
187
- False,
188
- _("Make segment a line"),
189
- _("> Line"),
190
- ],
191
- "c": [
192
- icon_node_curve,
193
- True,
194
- True,
195
- False,
196
- _("Make segment a curve"),
197
- _("> Curve"),
198
- ],
199
- "s": [
200
- icon_node_symmetric,
201
- True,
202
- True,
203
- False,
204
- _("Make segment symmetrical"),
205
- _("Symmetric"),
206
- ],
207
- "j": [
208
- icon_node_join,
209
- True,
210
- True,
211
- False,
212
- _("Join two segments"),
213
- _("Join"),
214
- ],
215
- "b": [
216
- icon_node_break,
217
- True,
218
- True,
219
- False,
220
- _("Break segment apart"),
221
- _("Break"),
222
- ],
223
- "o": [
224
- icon_node_smooth,
225
- True,
226
- True,
227
- False,
228
- _("Smooth transit to adjacent segments"),
229
- _("Smooth"),
230
- ],
231
- "v": [
232
- icon_node_smooth_all,
233
- False,
234
- True,
235
- False,
236
- _("Convert all lines into smooth curves"),
237
- _("Very smooth"),
238
- ],
239
- "w": [
240
- icon_node_line_all,
241
- False,
242
- True,
243
- False,
244
- _("Convert all segments into lines"),
245
- _("Line all"),
246
- ],
247
- "z": [
248
- icon_node_close,
249
- False,
250
- True,
251
- True,
252
- _("Toggle closed status"),
253
- _("Close"),
254
- ],
255
- "p": [
256
- icon_node_smooth_all,
257
- False,
258
- False,
259
- True,
260
- _("Convert polyline to a path element"),
261
- _("To Path"),
262
- ],
263
- }
264
- icon_size = STD_ICON_SIZE
265
- for command, entry in cmd_icons.items():
266
- # print(command, f"button/secondarytool_edit/tool_{command}")
267
- self.scene.context.kernel.register(
268
- f"button/secondarytool_edit/tool_{command}",
269
- {
270
- "label": entry[5],
271
- "icon": entry[0],
272
- "tip": entry[4],
273
- "action": do_action(command),
274
- "size": icon_size,
275
- "rule_enabled": becomes_enabled(entry[1], entry[2], entry[3]),
276
- "rule_visible": becomes_visible(entry[2], entry[3]),
277
- },
278
- )
279
-
280
- def enable_rules(self):
281
- toolbar = self.scene.context.lookup("ribbonbar/tools")
282
- if toolbar is not None:
283
- toolbar.apply_enable_rules()
284
-
285
- def final(self, context):
286
- """
287
- Shutdown routine for widget that unregisters the listener routines
288
- and closes the toolbar window.
289
- This could be called more than once which, if not dealt with, will
290
- cause a console warning message
291
- """
292
- if self._listener_active:
293
- self.scene.context.unlisten("emphasized", self.on_emphasized_changed)
294
- self.scene.context.unlisten("nodeedit", self.on_signal_nodeedit)
295
- self._listener_active = False
296
- self.scene.request_refresh()
297
-
298
- def init(self, context):
299
- """
300
- Startup routine for widget that establishes the listener routines
301
- and opens the toolbar window
302
- """
303
- self.scene.context.listen("emphasized", self.on_emphasized_changed)
304
- self.scene.context.listen("nodeedit", self.on_signal_nodeedit)
305
- self._listener_active = True
306
-
307
- def on_emphasized_changed(self, origin, *args):
308
- """
309
- Receiver routine for scene selection signal
310
- """
311
- selected_node = self.scene.context.elements.first_element(emphasized=True)
312
- if selected_node is not self.element:
313
- self.calculate_points(selected_node)
314
- self.scene.request_refresh()
315
- self.enable_rules()
316
-
317
- def set_pen_widths(self):
318
- """
319
- Calculate the pen widths according to the current scene zoom levels,
320
- so that they always appear 1 pixel wide - except for the
321
- pen associated to the path segment outline that gets a 2 pixel wide 'halo'
322
- """
323
-
324
- def set_width_pen(pen, width):
325
- try:
326
- try:
327
- pen.SetWidth(width)
328
- except TypeError:
329
- pen.SetWidth(int(width))
330
- except OverflowError:
331
- pass # Exceeds 32 bit signed integer.
332
-
333
- matrix = self.scene.widget_root.scene_widget.matrix
334
- linewidth = 1.0 / matrix.value_scale_x()
335
- if linewidth < 1:
336
- linewidth = 1
337
- set_width_pen(self.pen, linewidth)
338
- set_width_pen(self.pen_highlight, linewidth)
339
- set_width_pen(self.pen_ctrl, linewidth)
340
- set_width_pen(self.pen_ctrl_semi, linewidth)
341
- set_width_pen(self.pen_selection, linewidth)
342
- value = linewidth
343
- if self.element is not None and hasattr(self.element, "stroke_width"):
344
- if self.element.stroke_width is not None:
345
- value = self.element.stroke_width
346
- value += 4 * linewidth
347
- set_width_pen(self.pen_highlight_line, value)
348
-
349
- def calculate_points(self, selected_node):
350
- """
351
- Parse the element and create a list of dictionaries with relevant information required for display and logic
352
- """
353
- self.message = ""
354
-
355
- self.element = selected_node
356
- self.selected_index = None
357
- self.nodes = []
358
- # print ("After load:")
359
- # self.debug_path()
360
- if selected_node is None:
361
- return
362
- self.shape = None
363
- self.path = None
364
- if selected_node.type == "elem polyline":
365
- self.node_type = "polyline"
366
- try:
367
- self.shape = selected_node.shape
368
- except AttributeError:
369
- return
370
- start = 0
371
- for idx, pt in enumerate(self.shape.points):
372
- self.nodes.append(
373
- {
374
- "prev": None,
375
- "next": None,
376
- "point": pt,
377
- "segment": None,
378
- "path": self.shape,
379
- "type": "point",
380
- "connector": -1,
381
- "selected": False,
382
- "segtype": "L",
383
- "start": start,
384
- }
385
- )
386
- else:
387
- self.node_type = "path"
388
- # self.path = selected_node.geometry.as_path()
389
- if hasattr(selected_node, "path"):
390
- self.path = selected_node.path
391
- elif hasattr(selected_node, "geometry"):
392
- self.path = selected_node.geometry.as_path()
393
- elif hasattr(selected_node, "as_geometry"):
394
- self.path = selected_node.as_geometry().as_path()
395
- elif hasattr(selected_node, "as_path"):
396
- self.path = selected_node.as_path()
397
- else:
398
- return
399
- # print(self.path.d(), self.path)
400
- if self.path is None:
401
- return
402
- self.path.approximate_arcs_with_cubics()
403
- # print(self.path.d(), self.path)
404
- # try:
405
- # except AttributeError:
406
- # return
407
- # print (f"Path: {str(path)}")
408
- prev_seg = None
409
- start = 0
410
- # Idx of last point
411
- l_idx = 0
412
- for idx, segment in enumerate(self.path):
413
- # print(
414
- # f"{idx}# {type(segment).__name__} - S={segment.start} - E={segment.end}"
415
- # )
416
- if idx < len(self.path) - 1:
417
- next_seg = self.path[idx + 1]
418
- else:
419
- next_seg = None
420
- if isinstance(segment, Move):
421
- if idx != start:
422
- start = idx
423
-
424
- if isinstance(segment, Close):
425
- # We don't do anything with a Close - it's drawn anyway
426
- pass
427
- elif isinstance(segment, Line):
428
- self.nodes.append(
429
- {
430
- "prev": prev_seg,
431
- "next": next_seg,
432
- "point": segment.end,
433
- "segment": segment,
434
- "path": self.path,
435
- "type": "point",
436
- "connector": -1,
437
- "selected": False,
438
- "segtype": "Z" if isinstance(segment, Close) else "L",
439
- "start": start,
440
- "pathindex": idx,
441
- }
442
- )
443
- nidx = len(self.nodes) - 1
444
- elif isinstance(segment, Move):
445
- self.nodes.append(
446
- {
447
- "prev": prev_seg,
448
- "next": next_seg,
449
- "point": segment.end,
450
- "segment": segment,
451
- "path": self.path,
452
- "type": "point",
453
- "connector": -1,
454
- "selected": False,
455
- "segtype": "M",
456
- "start": start,
457
- "pathindex": idx,
458
- }
459
- )
460
- nidx = len(self.nodes) - 1
461
- elif isinstance(segment, QuadraticBezier):
462
- self.nodes.append(
463
- {
464
- "prev": prev_seg,
465
- "next": next_seg,
466
- "point": segment.end,
467
- "segment": segment,
468
- "path": self.path,
469
- "type": "point",
470
- "connector": -1,
471
- "selected": False,
472
- "segtype": "Q",
473
- "start": start,
474
- "pathindex": idx,
475
- }
476
- )
477
- nidx = len(self.nodes) - 1
478
- self.nodes.append(
479
- {
480
- "prev": None,
481
- "next": None,
482
- "point": segment.control,
483
- "segment": segment,
484
- "path": self.path,
485
- "type": "control",
486
- "connector": nidx,
487
- "selected": False,
488
- "segtype": "",
489
- "start": start,
490
- "pathindex": idx,
491
- }
492
- )
493
- elif isinstance(segment, CubicBezier):
494
- self.nodes.append(
495
- {
496
- "prev": prev_seg,
497
- "next": next_seg,
498
- "point": segment.end,
499
- "segment": segment,
500
- "path": self.path,
501
- "type": "point",
502
- "connector": -1,
503
- "selected": False,
504
- "segtype": "C",
505
- "start": start,
506
- "pathindex": idx,
507
- }
508
- )
509
- nidx = len(self.nodes) - 1
510
- self.nodes.append(
511
- {
512
- "prev": None,
513
- "next": None,
514
- "point": segment.control1,
515
- "segment": segment,
516
- "path": self.path,
517
- "type": "control",
518
- "connector": l_idx,
519
- "selected": False,
520
- "segtype": "",
521
- "start": start,
522
- "pathindex": idx,
523
- }
524
- )
525
- self.nodes.append(
526
- {
527
- "prev": None,
528
- "next": None,
529
- "point": segment.control2,
530
- "segment": segment,
531
- "path": self.path,
532
- "type": "control",
533
- "connector": nidx,
534
- "selected": False,
535
- "segtype": "",
536
- "start": start,
537
- "pathindex": idx,
538
- }
539
- )
540
- # midp = segment.point(0.5)
541
- midp = self.get_bezier_point(segment, 0.5)
542
- self.nodes.append(
543
- {
544
- "prev": None,
545
- "next": None,
546
- "point": midp,
547
- "segment": segment,
548
- "path": self.path,
549
- "type": "midpoint",
550
- "connector": -1,
551
- "selected": False,
552
- "segtype": "",
553
- "start": start,
554
- "pathindex": idx,
555
- }
556
- )
557
- elif isinstance(segment, Arc):
558
- self.nodes.append(
559
- {
560
- "prev": prev_seg,
561
- "next": next_seg,
562
- "point": segment.end,
563
- "segment": segment,
564
- "path": self.path,
565
- "type": "point",
566
- "connector": -1,
567
- "selected": False,
568
- "segtype": "A",
569
- "start": start,
570
- "pathindex": idx,
571
- }
572
- )
573
- nidx = len(self.nodes) - 1
574
- self.nodes.append(
575
- {
576
- "prev": None,
577
- "next": None,
578
- "point": segment.center,
579
- "segment": segment,
580
- "path": self.path,
581
- "type": "control",
582
- "connector": nidx,
583
- "selected": False,
584
- "segtype": "",
585
- "start": start,
586
- "pathindex": idx,
587
- }
588
- )
589
- prev_seg = segment
590
- l_idx = nidx
591
- for cmd in self.commands:
592
- action = self.commands[cmd]
593
- if self.node_type == "path" and action[3]:
594
- if self.message:
595
- self.message += ", "
596
- self.message += f"{cmd}: {action[1]}"
597
- if self.node_type == "polyline" and action[2]:
598
- if self.message:
599
- self.message += ", "
600
- self.message += f"{cmd}: {action[1]}"
601
-
602
- self.enable_rules()
603
-
604
- def calc_and_draw(self, gc):
605
- """
606
- Takes a svgelements.Path and converts it to a GraphicsContext.Graphics Path
607
- """
608
-
609
- def deal_with_segment(seg, init):
610
- if isinstance(seg, Line):
611
- if not init:
612
- init = True
613
- ptx, pty = node.matrix.point_in_matrix_space(seg.start)
614
- p.MoveToPoint(ptx, pty)
615
- ptx, pty = node.matrix.point_in_matrix_space(seg.end)
616
- p.AddLineToPoint(ptx, pty)
617
- elif isinstance(seg, Close):
618
- if not init:
619
- init = True
620
- ptx, pty = node.matrix.point_in_matrix_space(seg.start)
621
- p.MoveToPoint(ptx, pty)
622
- p.CloseSubpath()
623
- elif isinstance(seg, QuadraticBezier):
624
- if not init:
625
- init = True
626
- ptx, pty = node.matrix.point_in_matrix_space(seg.start)
627
- p.MoveToPoint(ptx, pty)
628
- ptx, pty = node.matrix.point_in_matrix_space(seg.end)
629
- c1x, c1y = node.matrix.point_in_matrix_space(seg.control)
630
- p.AddQuadCurveToPoint(c1x, c1y, ptx, pty)
631
- elif isinstance(seg, CubicBezier):
632
- if not init:
633
- init = True
634
- ptx, pty = node.matrix.point_in_matrix_space(seg.start)
635
- p.MoveToPoint(ptx, pty)
636
- ptx, pty = node.matrix.point_in_matrix_space(seg.end)
637
- c1x, c1y = node.matrix.point_in_matrix_space(seg.control1)
638
- c2x, c2y = node.matrix.point_in_matrix_space(seg.control2)
639
- p.AddCurveToPoint(c1x, c1y, c2x, c2y, ptx, pty)
640
- elif isinstance(seg, Arc):
641
- if not init:
642
- init = True
643
- ptx, pty = node.matrix.point_in_matrix_space(seg.start)
644
- p.MoveToPoint(ptx, pty)
645
- for curve in seg.as_cubic_curves():
646
- ptx, pty = node.matrix.point_in_matrix_space(curve.end)
647
- c1x, c1y = node.matrix.point_in_matrix_space(curve.control1)
648
- c2x, c2y = node.matrix.point_in_matrix_space(curve.control2)
649
- p.AddCurveToPoint(c1x, c1y, c2x, c2y, ptx, pty)
650
- return init
651
-
652
- node = self.element
653
- p = gc.CreatePath()
654
- if self.node_type == "polyline":
655
- for idx, entry in enumerate(self.nodes):
656
- ptx, pty = node.matrix.point_in_matrix_space(entry["point"])
657
- # print (f"Idx={idx}, selected={entry['selected']}, prev={'-' if idx == 0 else self.nodes[idx-1]['selected']}")
658
- if idx == 1 and (
659
- self.nodes[0]["selected"] or self.nodes[1]["selected"]
660
- ):
661
- p.AddLineToPoint(ptx, pty)
662
- elif idx == 0 or not entry["selected"]:
663
- p.MoveToPoint(ptx, pty)
664
- else:
665
- p.AddLineToPoint(ptx, pty)
666
- else:
667
- path = self.path
668
- init = False
669
- for idx, entry in enumerate(self.nodes):
670
- if not entry["type"] == "point":
671
- continue
672
- # treatment = ""
673
- e = entry["segment"]
674
- if isinstance(e, Move):
675
- if entry["selected"]:
676
- # The next segment needs to be highlighted...
677
- ptx, pty = node.matrix.point_in_matrix_space(e.end)
678
- p.MoveToPoint(ptx, pty)
679
- e = entry["next"]
680
- init = deal_with_segment(e, init)
681
- # treatment = "move+next"
682
- else:
683
- ptx, pty = node.matrix.point_in_matrix_space(e.end)
684
- p.MoveToPoint(ptx, pty)
685
- init = True
686
- # treatment = "move"
687
- elif not entry["selected"]:
688
- ptx, pty = node.matrix.point_in_matrix_space(e.end)
689
- p.MoveToPoint(ptx, pty)
690
- init = True
691
- # treatment = "nonselected"
692
- else:
693
- init = deal_with_segment(e, init)
694
- # treatment = "selected"
695
- # print (f"#{idx} {entry['type']} got treatment: {treatment}")
696
-
697
- gc.SetPen(self.pen_highlight_line)
698
- gc.DrawPath(p)
699
-
700
- def process_draw(self, gc: wx.GraphicsContext):
701
- """
702
- Widget-Routine to draw the different elements on the provided GraphicContext
703
- """
704
-
705
- def draw_selection_rectangle():
706
- x0 = min(self.p1.real, self.p2.real)
707
- y0 = min(self.p1.imag, self.p2.imag)
708
- x1 = max(self.p1.real, self.p2.real)
709
- y1 = max(self.p1.imag, self.p2.imag)
710
- gc.SetPen(self.pen_selection)
711
- gc.SetBrush(wx.TRANSPARENT_BRUSH)
712
- gc.DrawRectangle(x0, y0, x1 - x0, y1 - y0)
713
-
714
- if not self.nodes:
715
- return
716
- self.set_pen_widths()
717
- if self.p1 is not None and self.p2 is not None:
718
- # Selection mode!
719
- draw_selection_rectangle()
720
- return
721
- offset = 5
722
- s = math.sqrt(abs(self.scene.widget_root.scene_widget.matrix.determinant))
723
- offset /= s
724
- gc.SetBrush(wx.TRANSPARENT_BRUSH)
725
- idx = -1
726
- node = self.element
727
- self.calc_and_draw(gc)
728
- for entry in self.nodes:
729
- idx += 1
730
- ptx, pty = node.matrix.point_in_matrix_space(entry["point"])
731
- if entry["type"] == "point":
732
- if idx == self.selected_index or entry["selected"]:
733
- gc.SetPen(self.pen_highlight)
734
- gc.SetBrush(self.brush_highlight)
735
- factor = 1.25
736
- else:
737
- gc.SetPen(self.pen)
738
- gc.SetBrush(self.brush_normal)
739
- factor = 1
740
- gc.DrawEllipse(
741
- ptx - factor * offset,
742
- pty - factor * offset,
743
- offset * 2 * factor,
744
- offset * 2 * factor,
745
- )
746
- elif entry["type"] == "control":
747
- if idx == self.selected_index or entry["selected"]:
748
- factor = 1.25
749
- gc.SetPen(self.pen_highlight)
750
- else:
751
- factor = 1
752
- gc.SetPen(self.pen_ctrl)
753
- # Do we have a second controlpoint at the same segment?
754
- if isinstance(entry["segment"], CubicBezier):
755
- orgnode = None
756
- if idx > 0 and self.nodes[idx - 1]["type"] == "point":
757
- orgnode = self.nodes[idx - 1]
758
- elif idx > 1 and self.nodes[idx - 2]["type"] == "point":
759
- orgnode = self.nodes[idx - 2]
760
- if orgnode is not None and orgnode["selected"]:
761
- gc.SetPen(self.pen_ctrl_semi)
762
- pattern = [
763
- (ptx - factor * offset, pty),
764
- (ptx, pty + factor * offset),
765
- (ptx + factor * offset, pty),
766
- (ptx, pty - factor * offset),
767
- (ptx - factor * offset, pty),
768
- ]
769
- gc.DrawLines(pattern)
770
- if 0 <= entry["connector"] < len(self.nodes):
771
- orgnode = self.nodes[entry["connector"]]
772
- org_pt = orgnode["point"]
773
- org_ptx, org_pty = node.matrix.point_in_matrix_space(org_pt)
774
- pattern = [(ptx, pty), (org_ptx, org_pty)]
775
- gc.DrawLines(pattern)
776
- elif entry["type"] == "midpoint":
777
- if idx == self.selected_index or entry["selected"]:
778
- factor = 1.25
779
- gc.SetPen(self.pen_highlight)
780
- else:
781
- factor = 1
782
- gc.SetPen(self.pen_ctrl)
783
- pattern = [
784
- (ptx - factor * offset, pty),
785
- (ptx, pty + factor * offset),
786
- (ptx + factor * offset, pty),
787
- (ptx, pty - factor * offset),
788
- (ptx - factor * offset, pty),
789
- ]
790
- gc.DrawLines(pattern)
791
-
792
- def done(self):
793
- """
794
- We are done with node editing, so shutdown stuff
795
- """
796
- self.scene.pane.tool_active = False
797
- self.scene.pane.modif_active = False
798
- self.p1 = None
799
- self.p2 = None
800
- self.move_type = "node"
801
- self.scene.context("tool none\n")
802
- self.scene.context.signal("statusmsg", "")
803
- self.scene.context.elements.validate_selected_area()
804
- self.scene.request_refresh()
805
-
806
- def modify_element(self, reload=True):
807
- """
808
- Central routine that tells the system that the node was
809
- changed, if 'reload' is set to True then it requires
810
- reload/recalculation of the properties (e.g. after the
811
- segment structure of a path was changed)
812
- """
813
- if self.element is None:
814
- return
815
- if self.shape is not None:
816
- self.element.geometry = Geomstr.svg(Path(self.shape))
817
- elif self.path is not None:
818
- self.element.geometry = Geomstr.svg(self.path)
819
- self.element.altered()
820
- try:
821
- __ = self.element.bbox()
822
- except AttributeError:
823
- pass
824
- self.scene.context.elements.validate_selected_area()
825
- self.scene.request_refresh()
826
- self.scene.context.signal("element_property_reload", [self.element])
827
- if reload:
828
- self.calculate_points(self.element)
829
- self.scene.request_refresh()
830
- self.enable_rules()
831
-
832
- def clear_selection(self):
833
- """
834
- Clears the selection
835
- """
836
- if self.nodes is not None:
837
- for entry in self.nodes:
838
- entry["selected"] = False
839
- self.enable_rules()
840
-
841
- def first_segment_in_subpath(self, index):
842
- """
843
- Provides the first non-move/close segment in the subpath
844
- to which the segment at location index belongs to
845
- """
846
- result = None
847
- if not self.element is None and hasattr(self.element, "path"):
848
- for idx in range(index, -1, -1):
849
- seg = self.path[idx]
850
- if isinstance(seg, (Move, Close)):
851
- break
852
- result = seg
853
- return result
854
-
855
- def last_segment_in_subpath(self, index):
856
- """
857
- Provides the last non-move/close segment in the subpath
858
- to which the segment at location index belongs to
859
- """
860
- result = None
861
- if not self.element is None and hasattr(self.element, "path"):
862
- for idx in range(index, len(self.path)):
863
- seg = self.path[idx]
864
- if isinstance(seg, (Move, Close)):
865
- break
866
- result = seg
867
- return result
868
-
869
- def is_closed_subpath(self, index):
870
- """
871
- Provides the last segment in the subpath
872
- to which the segment at location index belongs to
873
- """
874
- result = False
875
- if not self.element is None and hasattr(self.element, "path"):
876
- for idx in range(index, len(self.path)):
877
- seg = self.path[idx]
878
- if isinstance(seg, Move):
879
- break
880
- if isinstance(seg, Close):
881
- result = True
882
- break
883
- return result
884
-
885
- def convert_to_path(self):
886
- """
887
- Converts a polyline element to a path and reloads the scene
888
- """
889
- if self.element is None or hasattr(self.element, "path"):
890
- return
891
- node = self.element
892
- oldstuff = []
893
- for attrib in ("stroke", "fill", "stroke_width", "stroke_scaled"):
894
- if hasattr(node, attrib):
895
- oldval = getattr(node, attrib, None)
896
- oldstuff.append([attrib, oldval])
897
- try:
898
- path = node.as_path()
899
- # There are some challenges around the treatment
900
- # of arcs within svgelements, so let's circumvent
901
- # them for the time being (until resolved)
902
- # by replacing arc segments with cubic Béziers
903
- if node.type in ("elem path", "elem ellipse"):
904
- path.approximate_arcs_with_cubics()
905
- except AttributeError:
906
- return
907
- newnode = node.replace_node(path=path, type="elem path")
908
- for item in oldstuff:
909
- setattr(newnode, item[0], item[1])
910
- newnode.altered()
911
- self.element = newnode
912
- self.shape = None
913
- self.path = path
914
- self.modify_element(reload=True)
915
-
916
- def toggle_close(self):
917
- """
918
- Toggle the closed status for a polyline or path element
919
- """
920
- if self.element is None or self.nodes is None:
921
- return
922
- modified = False
923
- if self.node_type == "polyline":
924
- dist = (self.shape.points[0].x - self.shape.points[-1].x) ** 2 + (
925
- self.shape.points[0].y - self.shape.points[-1].y
926
- ) ** 2
927
- if dist < 1: # Closed
928
- newshape = Polyline(self.shape)
929
- if len(newshape.points) > 2:
930
- newshape.points.pop(-1)
931
- else:
932
- newshape = Polygon(self.shape)
933
- self.shape = newshape
934
- modified = True
935
- else:
936
- dealt_with = []
937
- if not self.anyselected:
938
- # Let's select the last point, so the last segment will be closed/opened
939
- for idx in range(len(self.nodes) - 1, -1, -1):
940
- entry = self.nodes[idx]
941
- if entry["type"] == "point":
942
- entry["selected"] = True
943
- break
944
-
945
- for idx in range(len(self.nodes) - 1, -1, -1):
946
- entry = self.nodes[idx]
947
- if entry["selected"] and entry["type"] == "point":
948
- # What's the index of the last selected element
949
- # Have we dealt with that before? ie not multiple toggles..
950
- segstart = entry["start"]
951
- if segstart in dealt_with:
952
- continue
953
- dealt_with.append(segstart)
954
- # Let's establish the last segment in the path
955
- prevseg = None
956
- is_closed = False
957
- firstseg = None
958
- for sidx in range(segstart, len(self.path), 1):
959
- seg = self.path[sidx]
960
- if isinstance(seg, Move) and prevseg is None:
961
- # Not the one at the very beginning!
962
- continue
963
- if isinstance(seg, Move):
964
- # Ready
965
- break
966
- if isinstance(seg, Close):
967
- # Ready
968
- is_closed = True
969
- break
970
- if firstseg is None:
971
- firstseg = seg
972
- lastidx = sidx
973
- prevseg = seg
974
- if firstseg is not None and not is_closed:
975
- dist = firstseg.start.distance_to(prevseg.end)
976
- if dist < 1:
977
- lastidx -= 1
978
- is_closed = True
979
- else:
980
- dist = 1e6
981
- if is_closed:
982
- # it's enough just to delete it...
983
- del self.path[lastidx + 1]
984
- modified = True
985
- else:
986
- # Need to insert a Close segment
987
- # print(f"Inserting a close, dist={dist:.2f}")
988
- # print(
989
- # f"First seg, idx={segstart}, type={type(firstseg).__name__}"
990
- # )
991
- # print(f"Last seg, idx={lastidx}, type={type(prevseg).__name__}")
992
- newseg = Close(
993
- start=Point(prevseg.end.x, prevseg.end.y),
994
- end=Point(prevseg.end.x, prevseg.end.y),
995
- )
996
- self.path.insert(lastidx + 1, newseg)
997
- modified = True
998
-
999
- if modified:
1000
- self.modify_element(True)
1001
-
1002
- @staticmethod
1003
- def get_bezier_point(segment, t):
1004
- """
1005
- Provide a point on the cubic bezier curve for t (0 <= t <= 1)
1006
- Args:
1007
- segment (PathSegment): a cubic bezier
1008
- t (float): (0 <= t <= 1)
1009
- Computation: b(t) = (1-t)^3 * P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3 * P3
1010
- """
1011
- p0 = segment.start
1012
- p1 = segment.control1
1013
- p2 = segment.control2
1014
- p3 = segment.end
1015
- result = (
1016
- (1 - t) ** 3 * p0
1017
- + 3 * (1 - t) ** 2 * t * p1
1018
- + 3 * (1 - t) * t**2 * p2
1019
- + t**3 * p3
1020
- )
1021
- return result
1022
-
1023
- @staticmethod
1024
- def revise_bezier_to_point(segment, midpoint, change_2nd_control=False):
1025
- """
1026
- Adjust the two control points for a cubic Bézier segment,
1027
- so that the given point will lie on the cubic Bézier curve for t=0.5
1028
- Args:
1029
- segment (PathSegment): a cubic bezier segment to be amended
1030
- midpoint (Point): the new point
1031
- change_2nd_control: modify the 2nd control point, rather than the first
1032
- Computation: b(t) = (1-t)^3 * P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3 * P3
1033
- """
1034
- t = 0.5
1035
- p0 = segment.start
1036
- p1 = segment.control1
1037
- p2 = segment.control2
1038
- p3 = segment.end
1039
- if change_2nd_control:
1040
- factor = 1 / (3 * (1 - t) * t**2)
1041
- result = (
1042
- midpoint - (1 - t) ** 3 * p0 - 3 * (1 - t) ** 2 * t * p1 - t**3 * p3
1043
- ) * factor
1044
- segment.control2 = result
1045
- else:
1046
- factor = 1 / (3 * (1 - t) ** 2 * t)
1047
- result = (
1048
- midpoint - (1 - t) ** 3 * p0 - 3 * (1 - t) * t**2 * p2 - t**3 * p3
1049
- ) * factor
1050
- segment.control1 = result
1051
-
1052
- def adjust_midpoint(self, index):
1053
- """
1054
- Computes and sets the midpoint of a cubic bezier segment
1055
- """
1056
- for j in range(3):
1057
- k = index + 1 + j
1058
- if k < len(self.nodes) and self.nodes[k]["type"] == "midpoint":
1059
- self.nodes[k]["point"] = self.get_bezier_point(
1060
- self.nodes[index]["segment"], 0.5
1061
- )
1062
- break
1063
-
1064
- def smoothen(self):
1065
- """
1066
- Smoothen a circular bezier segment to adjacent segments, ie adjust
1067
- the control points so that they are an extension of the previous/next segment
1068
- """
1069
- if self.element is None or self.nodes is None:
1070
- return
1071
- modified = False
1072
- if self.node_type == "polyline":
1073
- # Not valid for a polyline Could make a path now but that might be more than the user expected...
1074
- return
1075
- for entry in self.nodes:
1076
- if entry["selected"] and entry["segtype"] == "C": # Cubic Bezier only
1077
- segment = entry["segment"]
1078
- pt_start = segment.start
1079
- pt_end = segment.end
1080
- other_segment = entry["prev"]
1081
- if other_segment is not None:
1082
- if isinstance(other_segment, Line):
1083
- other_pt_x = other_segment.start.x
1084
- other_pt_y = other_segment.start.y
1085
- dx = pt_start.x - other_pt_x
1086
- dy = pt_start.y - other_pt_y
1087
- segment.control1.x = pt_start.x + 0.25 * dx
1088
- segment.control1.y = pt_start.y + 0.25 * dy
1089
- modified = True
1090
- elif isinstance(other_segment, CubicBezier):
1091
- other_pt_x = other_segment.control2.x
1092
- other_pt_y = other_segment.control2.y
1093
- dx = pt_start.x - other_pt_x
1094
- dy = pt_start.y - other_pt_y
1095
- segment.control1.x = pt_start.x + dx
1096
- segment.control1.y = pt_start.y + dy
1097
- modified = True
1098
- elif isinstance(other_segment, QuadraticBezier):
1099
- other_pt_x = other_segment.control.x
1100
- other_pt_y = other_segment.control.y
1101
- dx = pt_start.x - other_pt_x
1102
- dy = pt_start.y - other_pt_y
1103
- segment.control1.x = pt_start.x + dx
1104
- segment.control1.y = pt_start.y + dy
1105
- modified = True
1106
- elif isinstance(other_segment, Arc):
1107
- # We need the tangent in the end-point,
1108
- other_pt_x = other_segment.end.x
1109
- other_pt_y = other_segment.end.y
1110
- dx = pt_start.x - other_pt_x
1111
- dy = pt_start.y - other_pt_y
1112
- segment.control1.x = pt_start.x + dx
1113
- segment.control1.y = pt_start.y + dy
1114
- modified = True
1115
- other_segment = entry["next"]
1116
- if other_segment is not None:
1117
- if isinstance(other_segment, Line):
1118
- other_pt_x = other_segment.end.x
1119
- other_pt_y = other_segment.end.y
1120
- dx = pt_end.x - other_pt_x
1121
- dy = pt_end.y - other_pt_y
1122
- segment.control2.x = pt_end.x + 0.25 * dx
1123
- segment.control2.y = pt_end.y + 0.25 * dy
1124
- modified = True
1125
- elif isinstance(other_segment, CubicBezier):
1126
- other_pt_x = other_segment.control1.x
1127
- other_pt_y = other_segment.control1.y
1128
- dx = pt_end.x - other_pt_x
1129
- dy = pt_end.y - other_pt_y
1130
- segment.control2.x = pt_end.x + dx
1131
- segment.control2.y = pt_end.y + dy
1132
- modified = True
1133
- elif isinstance(other_segment, QuadraticBezier):
1134
- other_pt_x = other_segment.control.x
1135
- other_pt_y = other_segment.control.y
1136
- dx = pt_end.x - other_pt_x
1137
- dy = pt_end.y - other_pt_y
1138
- segment.control2.x = pt_end.x + dx
1139
- segment.control2.y = pt_end.y + dy
1140
- modified = True
1141
- elif isinstance(other_segment, Arc):
1142
- # We need the tangent in the end-point,
1143
- other_pt_x = other_segment.start.x
1144
- other_pt_y = other_segment.start.y
1145
- dx = pt_end.x - other_pt_x
1146
- dy = pt_end.y - other_pt_y
1147
- segment.control2.x = pt_end.x + dx
1148
- segment.control2.y = pt_end.y + dy
1149
- modified = True
1150
- if modified:
1151
- self.modify_element(True)
1152
-
1153
- def smoothen_all(self):
1154
- """
1155
- Convert all segments of the path that are not cubic Béziers into
1156
- such segments and apply the same smoothen logic as in smoothen(),
1157
- ie adjust the control points of two neighbouring segments
1158
- so that the three points
1159
- 'prev control2' - 'prev/end=next start' - 'next control1'
1160
- are collinear
1161
- """
1162
- if self.element is None or self.nodes is None:
1163
- return
1164
- modified = False
1165
- if self.node_type == "polyline":
1166
- # Not valid for a polyline Could make a path now but that might be more than the user expected...
1167
- return
1168
- # Pass 1 - make all lines a cubic bezier
1169
- for idx, segment in enumerate(self.path):
1170
- if isinstance(segment, Line):
1171
- startpt = copy(segment.start)
1172
- endpt = copy(segment.end)
1173
- ctrl1pt = Point(
1174
- startpt.x + 0.25 * (endpt.x - startpt.x),
1175
- startpt.y + 0.25 * (endpt.y - startpt.y),
1176
- )
1177
- ctrl2pt = Point(
1178
- startpt.x + 0.75 * (endpt.x - startpt.x),
1179
- startpt.y + 0.75 * (endpt.y - startpt.y),
1180
- )
1181
- newsegment = CubicBezier(
1182
- start=startpt, end=endpt, control1=ctrl1pt, control2=ctrl2pt
1183
- )
1184
- self.path[idx] = newsegment
1185
- modified = True
1186
- elif isinstance(segment, QuadraticBezier):
1187
- # The cubic control - points lie on 2/3 of the way of the
1188
- # line-segments from the endpoint to the quadratic control-point
1189
- startpt = copy(segment.start)
1190
- endpt = copy(segment.end)
1191
- dx = segment.control.x - startpt.x
1192
- dy = segment.control.y - startpt.y
1193
- ctrl1pt = Point(startpt.x + 2 / 3 * dx, startpt.y + 2 / 3 * dy)
1194
- dx = segment.control.x - endpt.x
1195
- dy = segment.control.y - endpt.y
1196
- ctrl2pt = Point(endpt.x + 2 / 3 * dx, endpt.y + 2 / 3 * dy)
1197
- newsegment = CubicBezier(
1198
- start=startpt, end=endpt, control1=ctrl1pt, control2=ctrl2pt
1199
- )
1200
- self.path[idx] = newsegment
1201
- modified = True
1202
- elif isinstance(segment, Arc):
1203
- for newsegment in list(segment.as_cubic_curves(1)):
1204
- self.path[idx] = newsegment
1205
- break
1206
- modified = True
1207
- # Pass 2 - make all control lines align
1208
- prevseg = None
1209
- lastidx = len(self.path) - 1
1210
- for idx, segment in enumerate(self.path):
1211
- nextseg = None
1212
- if idx < lastidx:
1213
- nextseg = self.path[idx + 1]
1214
- if isinstance(nextseg, (Move, Close)):
1215
- nextseg = None
1216
- if isinstance(segment, CubicBezier):
1217
- if prevseg is None:
1218
- if self.is_closed_subpath(idx):
1219
- otherseg = self.last_segment_in_subpath(idx)
1220
- prevseg = Line(
1221
- start=Point(otherseg.end.x, otherseg.end.y),
1222
- end=Point(segment.start.x, segment.start.y),
1223
- )
1224
- if prevseg is not None:
1225
- angle1 = Point.angle(prevseg.end, prevseg.start)
1226
- angle2 = Point.angle(segment.start, segment.end)
1227
- d_angle = math.tau / 2 - (angle1 - angle2)
1228
- while d_angle >= math.tau:
1229
- d_angle -= math.tau
1230
- while d_angle < -math.tau:
1231
- d_angle += math.tau
1232
-
1233
- # print (f"to prev: Angle 1 = {angle1/math.tau*360:.1f}°, Angle 2 = {angle2/math.tau*360:.1f}°, Delta = {d_angle/math.tau*360:.1f}°")
1234
- dist = segment.start.distance_to(segment.control1)
1235
- candidate1 = Point.polar(segment.start, angle2 - d_angle / 2, dist)
1236
- candidate2 = Point.polar(segment.start, angle1 + d_angle / 2, dist)
1237
- if segment.end.distance_to(candidate1) < segment.end.distance_to(
1238
- candidate2
1239
- ):
1240
- segment.control1 = candidate1
1241
- else:
1242
- segment.control1 = candidate2
1243
- modified = True
1244
- if nextseg is None:
1245
- if self.is_closed_subpath(idx):
1246
- otherseg = self.first_segment_in_subpath(idx)
1247
- nextseg = Line(
1248
- start=Point(segment.end.x, segment.end.y),
1249
- end=Point(otherseg.start.x, otherseg.start.y),
1250
- )
1251
- if nextseg is not None:
1252
- angle1 = Point.angle(segment.end, segment.start)
1253
- angle2 = Point.angle(nextseg.start, nextseg.end)
1254
- d_angle = math.tau / 2 - (angle1 - angle2)
1255
- while d_angle >= math.tau:
1256
- d_angle -= math.tau
1257
- while d_angle < -math.tau:
1258
- d_angle += math.tau
1259
-
1260
- # print (f"to next: Angle 1 = {angle1/math.tau*360:.1f}°, Angle 2 = {angle2/math.tau*360:.1f}°, Delta = {d_angle/math.tau*360:.1f}°")
1261
- dist = segment.end.distance_to(segment.control2)
1262
- candidate1 = Point.polar(segment.end, angle2 - d_angle / 2, dist)
1263
- candidate2 = Point.polar(segment.end, angle1 + d_angle / 2, dist)
1264
- if segment.start.distance_to(
1265
- candidate1
1266
- ) < segment.start.distance_to(candidate2):
1267
- segment.control2 = candidate1
1268
- else:
1269
- segment.control2 = candidate2
1270
- modified = True
1271
- if isinstance(segment, (Move, Close)):
1272
- prevseg = None
1273
- else:
1274
- prevseg = segment
1275
- if modified:
1276
- self.modify_element(True)
1277
-
1278
- def cubic_symmetrical(self):
1279
- """
1280
- Adjust the two control points control1 and control2 of a cubic segment
1281
- so that they are symmetrical to the perpendicular bisector on start - end
1282
- """
1283
- if self.element is None or self.nodes is None:
1284
- return
1285
- modified = False
1286
- if self.node_type == "polyline":
1287
- # Not valid for a polyline Could make a path now but that might be more than the user expected...
1288
- return
1289
- for entry in self.nodes:
1290
- if entry["selected"] and entry["segtype"] == "C": # Cubic Bezier only
1291
- segment = entry["segment"]
1292
- pt_start = segment.start
1293
- pt_end = segment.end
1294
- midpoint = Point(
1295
- (pt_end.x + pt_start.x) / 2, (pt_end.y + pt_start.y) / 2
1296
- )
1297
- angle_to_end = midpoint.angle_to(pt_end)
1298
- angle_to_start = midpoint.angle_to(pt_start)
1299
- angle_to_control2 = midpoint.angle_to(segment.control2)
1300
- distance = midpoint.distance_to(segment.control2)
1301
- # The new point
1302
- angle_to_control1 = angle_to_start + (angle_to_end - angle_to_control2)
1303
- # newx = midpoint.x + distance * math.cos(angle_to_control1)
1304
- # newy = midpoint.y + distance * math.sin(angle_to_control1)
1305
- # segment.control1 = Point(newx, newy)
1306
- segment.control1 = Point.polar(
1307
- (midpoint.x, midpoint.y), angle_to_control1, distance
1308
- )
1309
- modified = True
1310
- if modified:
1311
- self.modify_element(True)
1312
-
1313
- def delete_nodes(self):
1314
- """
1315
- Delete all selected (point) nodes
1316
- """
1317
- if self.element is None or self.nodes is None:
1318
- return
1319
- modified = False
1320
- for idx in range(len(self.nodes) - 1, -1, -1):
1321
- entry = self.nodes[idx]
1322
- if entry["selected"] and entry["type"] == "point":
1323
- if self.node_type == "polyline":
1324
- if len(self.shape.points) > 2:
1325
- modified = True
1326
- self.shape.points.pop(idx)
1327
- else:
1328
- break
1329
- else:
1330
- idx = entry["pathindex"]
1331
- prevseg = None
1332
- nextseg = None
1333
- seg = self.path[idx]
1334
- if idx > 0:
1335
- prevseg = self.path[idx - 1]
1336
- if idx < len(self.path) - 1:
1337
- nextseg = self.path[idx + 1]
1338
- if nextseg is None:
1339
- # Last point of the path
1340
- # Can just be deleted, provided we have something
1341
- # in front...
1342
- if prevseg is None or isinstance(prevseg, (Move, Close)):
1343
- continue
1344
- del self.path[idx]
1345
- modified = True
1346
- elif isinstance(nextseg, (Move, Close)):
1347
- # last point of the subsegment...
1348
- # We need to have another full segment in the front
1349
- # otherwise we would end up with a single point...
1350
- if prevseg is None or isinstance(prevseg, (Move, Close)):
1351
- continue
1352
- nextseg.start.x = seg.start.x
1353
- nextseg.start.y = seg.start.y
1354
- if isinstance(nextseg, Close):
1355
- nextseg.end.x = seg.start.x
1356
- nextseg.end.y = seg.start.y
1357
-
1358
- del self.path[idx]
1359
- modified = True
1360
- else:
1361
- # Could be the first point...
1362
- if prevseg is None and (
1363
- nextseg is None or isinstance(nextseg, (Move, Close))
1364
- ):
1365
- continue
1366
- if prevseg is None: # # Move
1367
- seg.end = Point(nextseg.end.x, nextseg.end.y)
1368
- del self.path[idx + 1]
1369
- modified = True
1370
- elif isinstance(seg, Move): # # Move
1371
- seg.end = Point(nextseg.end.x, nextseg.end.y)
1372
- del self.path[idx + 1]
1373
- modified = True
1374
- else:
1375
- nextseg.start.x = prevseg.end.x
1376
- nextseg.start.y = prevseg.end.y
1377
- del self.path[idx]
1378
- modified = True
1379
-
1380
- if modified:
1381
- self.modify_element(True)
1382
-
1383
- def convert_to_line(self):
1384
- """
1385
- Convert all selected segments to a line
1386
- """
1387
- if self.element is None or self.nodes is None:
1388
- return
1389
- modified = False
1390
- if self.node_type == "polyline":
1391
- # Not valid for a polyline Could make a path now but that might be more than the user expected...
1392
- return
1393
- for entry in self.nodes:
1394
- if entry["selected"] and entry["type"] == "point":
1395
- idx = entry["pathindex"]
1396
- if entry["segment"] is None or entry["segment"].start is None:
1397
- continue
1398
- startpt = Point(entry["segment"].start.x, entry["segment"].start.y)
1399
- endpt = Point(entry["segment"].end.x, entry["segment"].end.y)
1400
- if entry["segtype"] not in ("C", "Q", "A"):
1401
- continue
1402
- newsegment = Line(start=startpt, end=endpt)
1403
- self.path[idx] = newsegment
1404
- modified = True
1405
- if modified:
1406
- self.modify_element(True)
1407
-
1408
- def linear_all(self):
1409
- """
1410
- Convert all segments of the path to a line
1411
- """
1412
- if self.element is None or self.nodes is None:
1413
- return
1414
- modified = False
1415
- if self.node_type == "polyline":
1416
- # Not valid for a polyline Could make a path now but that might be more than the user expected...
1417
- return
1418
- for idx, segment in enumerate(self.path):
1419
- if isinstance(segment, (Close, Move, Line)):
1420
- continue
1421
- startpt = Point(segment.start.x, segment.start.y)
1422
- endpt = Point(segment.end.x, segment.end.y)
1423
- newsegment = Line(start=startpt, end=endpt)
1424
- self.path[idx] = newsegment
1425
- modified = True
1426
-
1427
- if modified:
1428
- self.modify_element(True)
1429
-
1430
- def convert_to_curve(self):
1431
- """
1432
- Convert all selected segments to a circular bezier
1433
- """
1434
- if self.element is None or self.nodes is None:
1435
- return
1436
- modified = False
1437
- if self.node_type == "polyline":
1438
- # Not valid for a polyline Could make a path now but that might be more than the user expected...
1439
- return
1440
- for entry in self.nodes:
1441
- if entry["selected"] and entry["type"] == "point":
1442
- idx = entry["pathindex"]
1443
- if entry["segment"] is None or entry["segment"].start is None:
1444
- continue
1445
- startpt = Point(entry["segment"].start.x, entry["segment"].start.y)
1446
- endpt = Point(entry["segment"].end.x, entry["segment"].end.y)
1447
- if entry["segtype"] == "L":
1448
- ctrl1pt = Point(
1449
- startpt.x + 0.25 * (endpt.x - startpt.x),
1450
- startpt.y + 0.25 * (endpt.y - startpt.y),
1451
- )
1452
- ctrl2pt = Point(
1453
- startpt.x + 0.75 * (endpt.x - startpt.x),
1454
- startpt.y + 0.75 * (endpt.y - startpt.y),
1455
- )
1456
- elif entry["segtype"] == "Q":
1457
- ctrl1pt = Point(
1458
- entry["segment"].control.x, entry["segment"].control.y
1459
- )
1460
- ctrl2pt = Point(endpt.x, endpt.y)
1461
- elif entry["segtype"] == "A":
1462
- ctrl1pt = Point(
1463
- startpt.x + 0.25 * (endpt.x - startpt.x),
1464
- startpt.y + 0.25 * (endpt.y - startpt.y),
1465
- )
1466
- ctrl2pt = Point(
1467
- startpt.x + 0.75 * (endpt.x - startpt.x),
1468
- startpt.y + 0.75 * (endpt.y - startpt.y),
1469
- )
1470
- else:
1471
- continue
1472
-
1473
- newsegment = CubicBezier(
1474
- start=startpt, end=endpt, control1=ctrl1pt, control2=ctrl2pt
1475
- )
1476
- self.path[idx] = newsegment
1477
- modified = True
1478
- if modified:
1479
- self.modify_element(True)
1480
-
1481
- def break_path(self):
1482
- """
1483
- Break a path at the selected (point) nodes
1484
- """
1485
- if self.element is None or self.nodes is None:
1486
- return
1487
- # Stub for breaking the path
1488
- modified = False
1489
- if self.node_type == "polyline":
1490
- # Not valid for a polyline Could make a path now but that might be more than the user expected...
1491
- return
1492
- for idx in range(len(self.nodes) - 1, -1, -1):
1493
- entry = self.nodes[idx]
1494
- if entry["selected"] and entry["type"] == "point":
1495
- idx = entry["pathindex"]
1496
- seg = entry["segment"]
1497
- if isinstance(seg, (Move, Close)):
1498
- continue
1499
- # Is this the last point? Then no use to break the path
1500
- nextseg = None
1501
- if idx in (0, len(self.path) - 1):
1502
- # Don't break at the first or last point
1503
- continue
1504
- nextseg = self.path[idx + 1]
1505
- if isinstance(nextseg, (Move, Close)):
1506
- # Not at end of subpath
1507
- continue
1508
- prevseg = self.path[idx - 1]
1509
- if isinstance(prevseg, (Move, Close)):
1510
- # We could still be at the end point of the first segment...
1511
- if entry["point"] == seg.start:
1512
- # Not at start of subpath
1513
- continue
1514
- newseg = Move(
1515
- start=Point(seg.end.x, seg.end.y),
1516
- end=Point(nextseg.start.x, nextseg.start.y),
1517
- )
1518
- self.path.insert(idx + 1, newseg)
1519
- # Now let's validate whether the 'right' path still has a
1520
- # close segment at its end. That will be removed as this would
1521
- # create an unwanted behaviour
1522
- prevseg = None
1523
- is_closed = False
1524
- for sidx in range(idx + 1, len(self.path), 1):
1525
- seg = self.path[sidx]
1526
- if isinstance(seg, Move) and prevseg is None:
1527
- # Not the one at the very beginning!
1528
- continue
1529
- if isinstance(seg, Move):
1530
- # Ready
1531
- break
1532
- if isinstance(seg, Close):
1533
- # Ready
1534
- is_closed = True
1535
- break
1536
- lastidx = sidx
1537
- prevseg = seg
1538
- if is_closed:
1539
- # it's enough just to delete it...
1540
- del self.path[lastidx + 1]
1541
-
1542
- modified = True
1543
- if modified:
1544
- self.modify_element(True)
1545
-
1546
- def join_path(self):
1547
- """
1548
- Join two selected (point) nodes if they are on different subpath
1549
- """
1550
- if self.element is None or self.nodes is None:
1551
- return
1552
- modified = False
1553
- if self.node_type == "polyline":
1554
- # Not valid for a polyline
1555
- return
1556
- for idx in range(len(self.nodes) - 1, -1, -1):
1557
- entry = self.nodes[idx]
1558
- if entry["selected"] and entry["type"] == "point":
1559
- idx = entry["pathindex"]
1560
- seg = entry["segment"]
1561
- prevseg = None
1562
- nextseg = None
1563
- if idx > 0:
1564
- prevseg = self.path[idx - 1]
1565
- if idx < len(self.path) - 1:
1566
- nextseg = self.path[idx + 1]
1567
- if isinstance(seg, (Move, Close)):
1568
- # Beginning of path
1569
- if prevseg is None:
1570
- # Very beginning?! Ignore...
1571
- continue
1572
- if nextseg is None:
1573
- continue
1574
- if isinstance(nextseg, (Move, Close)):
1575
- # Two consecutive moves? Ignore....
1576
- continue
1577
- nextseg.start.x = seg.start.x
1578
- nextseg.start.y = seg.start.y
1579
- del self.path[idx]
1580
- modified = True
1581
- else:
1582
- # Let's look at the next segment
1583
- if nextseg is None:
1584
- continue
1585
- if not isinstance(nextseg, Move):
1586
- continue
1587
- seg.end.x = nextseg.end.x
1588
- seg.end.y = nextseg.end.y
1589
- del self.path[idx + 1]
1590
- modified = True
1591
-
1592
- if modified:
1593
- self.modify_element(True)
1594
-
1595
- def insert_midpoint(self):
1596
- """
1597
- Insert a point in the middle of a selected segment
1598
- """
1599
- if self.element is None or self.nodes is None:
1600
- return
1601
- modified = False
1602
- # Move backwards as len will change
1603
- for idx in range(len(self.nodes) - 1, -1, -1):
1604
- entry = self.nodes[idx]
1605
- if entry["selected"] and entry["type"] == "point":
1606
- if self.node_type == "polyline":
1607
- pt1 = self.shape.points[idx]
1608
- if idx == 0:
1609
- # Very first point? Mirror first segment and take midpoint
1610
- pt2 = Point(
1611
- self.shape.points[idx + 1].x,
1612
- self.shape.points[idx + 1].y,
1613
- )
1614
- pt2.x = pt1.x - (pt2.x - pt1.x)
1615
- pt2.y = pt1.y - (pt2.y - pt1.y)
1616
- pt2.x = (pt1.x + pt2.x) / 2
1617
- pt2.y = (pt1.y + pt2.y) / 2
1618
- self.shape.points.insert(0, pt2)
1619
- else:
1620
- pt2 = Point(
1621
- self.shape.points[idx - 1].x,
1622
- self.shape.points[idx - 1].y,
1623
- )
1624
- pt2.x = (pt1.x + pt2.x) / 2
1625
- pt2.y = (pt1.y + pt2.y) / 2
1626
- # Mid point
1627
- self.shape.points.insert(idx, pt2)
1628
- modified = True
1629
- else:
1630
- # Path
1631
- idx = entry["pathindex"]
1632
- if entry["segment"] is None:
1633
- continue
1634
- segment = entry["segment"]
1635
-
1636
- def pt_info(pt):
1637
- return f"({pt.x:.0f}, {pt.y:.0f})"
1638
-
1639
- if entry["segtype"] == "L":
1640
- # Line
1641
- mid_x = (segment.start.x + segment.end.x) / 2
1642
- mid_y = (segment.start.y + segment.end.y) / 2
1643
- newsegment = Line(
1644
- start=Point(mid_x, mid_y),
1645
- end=Point(segment.end.x, segment.end.y),
1646
- )
1647
- self.path.insert(idx + 1, newsegment)
1648
- # path.insert may change the start and end point
1649
- # of the segement to make sure it maintains a
1650
- # contiguous path, so we need to set it again...
1651
- newsegment.start.x = mid_x
1652
- newsegment.start.y = mid_y
1653
- segment.end.x = mid_x
1654
- segment.end.y = mid_y
1655
- modified = True
1656
- elif entry["segtype"] == "C":
1657
- midpoint = segment.point(0.5)
1658
- mid_x = midpoint.x
1659
- mid_y = midpoint.y
1660
- newsegment = CubicBezier(
1661
- start=Point(mid_x, mid_y),
1662
- end=Point(segment.end.x, segment.end.y),
1663
- control1=Point(mid_x, mid_y),
1664
- control2=Point(segment.control2.x, segment.control2.y),
1665
- )
1666
- self.path.insert(idx + 1, newsegment)
1667
- segment.end.x = mid_x
1668
- segment.end.y = mid_y
1669
- segment.control2.x = mid_x
1670
- segment.control2.y = mid_y
1671
- newsegment.start.x = mid_x
1672
- newsegment.start.y = mid_y
1673
- modified = True
1674
- elif entry["segtype"] == "A":
1675
- midpoint = segment.point(0.5)
1676
- mid_x = midpoint.x
1677
- mid_y = midpoint.y
1678
- # newsegment = Arc(
1679
- # start=Point(mid_x, mid_y),
1680
- # end=Point(segment.end.x, segment.end.y),
1681
- # control=Point(segment.center.x, segment.center.y),
1682
- # )
1683
- newsegment = copy(segment)
1684
- newsegment.start.x = mid_x
1685
- newsegment.start.y = mid_y
1686
- self.path.insert(idx + 1, newsegment)
1687
- segment.end.x = mid_x
1688
- segment.end.y = mid_y
1689
- newsegment.start.x = mid_x
1690
- newsegment.start.y = mid_y
1691
- modified = True
1692
- elif entry["segtype"] == "Q":
1693
- midpoint = segment.point(0.5)
1694
- mid_x = midpoint.x
1695
- mid_y = midpoint.y
1696
- newsegment = QuadraticBezier(
1697
- start=Point(mid_x, mid_y),
1698
- end=Point(segment.end.x, segment.end.y),
1699
- control=Point(segment.control.x, segment.control.y),
1700
- )
1701
- self.path.insert(idx + 1, newsegment)
1702
- segment.end.x = mid_x
1703
- segment.end.y = mid_y
1704
- segment.control.x = mid_x
1705
- segment.control.y = mid_y
1706
- newsegment.start.x = mid_x
1707
- newsegment.start.y = mid_y
1708
- modified = True
1709
- elif entry["segtype"] == "M":
1710
- # Very first point? Mirror first segment and take midpoint
1711
- nextseg = entry["next"]
1712
- if nextseg is None:
1713
- continue
1714
- p1x = nextseg.start.x
1715
- p1y = nextseg.start.y
1716
- p2x = nextseg.end.x
1717
- p2y = nextseg.end.y
1718
- p2x = p1x - (p2x - p1x)
1719
- p2y = p1y - (p2y - p1y)
1720
- pt1 = Point((p1x + p2x) / 2, (p1y + p2y) / 2)
1721
- pt2 = copy(nextseg.start)
1722
- newsegment = Line(start=pt1, end=pt2)
1723
- self.path.insert(idx + 1, newsegment)
1724
- segment.end = pt1
1725
- newsegment.start.x = pt1.x
1726
- newsegment.start.y = pt1.y
1727
- # We need to step forward to assess whether there is a close segment
1728
- for idx2 in range(idx + 1, len(self.path)):
1729
- if isinstance(self.path[idx2], Move):
1730
- break
1731
- if isinstance(self.path[idx2], Close):
1732
- # Adjust the close segment to that it points again
1733
- # to the first move end
1734
- self.path[idx2].end = Point(pt1.x, pt1.y)
1735
- break
1736
-
1737
- modified = True
1738
-
1739
- if modified:
1740
- self.modify_element(True)
1741
-
1742
- def append_line(self):
1743
- """
1744
- Append a point to the selected element, works all the time and does not require a valid selection
1745
- """
1746
- if self.element is None or self.nodes is None:
1747
- return
1748
- modified = False
1749
- if self.node_type == "polyline":
1750
- idx = len(self.shape.points) - 1
1751
- pt1 = self.shape.points[idx - 1]
1752
- pt2 = self.shape.points[idx]
1753
- newpt = Point(pt2.x + (pt2.x - pt1.x), pt2.y + (pt2.y - pt1.y))
1754
- self.shape.points.append(newpt)
1755
- modified = True
1756
- else:
1757
- # path
1758
- try:
1759
- valididx = len(self.path) - 1
1760
- except AttributeError:
1761
- # Shape
1762
- return
1763
- while valididx >= 0 and isinstance(self.path[valididx], (Close, Move)):
1764
- valididx -= 1
1765
- if valididx >= 0:
1766
- seg = self.path[valididx]
1767
- pt1 = seg.start
1768
- pt2 = seg.end
1769
- newpt = Point(pt2.x + (pt2.x - pt1.x), pt2.y + (pt2.y - pt1.y))
1770
- newsegment = Line(start=Point(seg.end.x, seg.end.y), end=newpt)
1771
- if valididx < len(self.path) - 1:
1772
- if self.path[valididx + 1].end == self.path[valididx + 1].start:
1773
- self.path[valididx + 1].end.x = newpt.x
1774
- self.path[valididx + 1].end.y = newpt.y
1775
- self.path[valididx + 1].start.x = newpt.x
1776
- self.path[valididx + 1].start.y = newpt.y
1777
-
1778
- self.path.insert(valididx + 1, newsegment)
1779
- newsegment.start.x = seg.end.x
1780
- newsegment.start.y = seg.end.y
1781
- modified = True
1782
-
1783
- if modified:
1784
- self.modify_element(True)
1785
-
1786
- @property
1787
- def anyselected(self):
1788
- if self.nodes:
1789
- for entry in self.nodes:
1790
- if entry["selected"]:
1791
- return True
1792
- return False
1793
-
1794
- def event(
1795
- self,
1796
- window_pos=None,
1797
- space_pos=None,
1798
- event_type=None,
1799
- nearest_snap=None,
1800
- modifiers=None,
1801
- keycode=None,
1802
- **kwargs,
1803
- ):
1804
- """
1805
- The routine dealing with propagated scene events
1806
-
1807
- Args:
1808
- window_pos (tuple): The coordinates of the mouse position in window coordinates
1809
- space_pos (tuple): The coordinates of the mouse position in scene coordinates
1810
- event_type (string): [description]. Defaults to None.
1811
- nearest_snap (tuple, optional): If set the coordinates of the nearest snap point in scene coordinates.
1812
- modifiers (string): If available provides a list of modifier keys that were pressed (shift, alt, ctrl).
1813
- keycode (string): if available the keycode that was pressed
1814
-
1815
- Returns:
1816
- Indicator how to proceed with this event after its execution (consume, chain etc)
1817
- """
1818
- if self.scene.pane.active_tool != "edit":
1819
- return RESPONSE_CHAIN
1820
- # print (f"event: {event_type}, modifiers: '{modifiers}', keycode: '{keycode}'")
1821
- offset = 5
1822
- s = math.sqrt(abs(self.scene.widget_root.scene_widget.matrix.determinant))
1823
- offset /= s
1824
- elements = self.scene.context.elements
1825
- if event_type in ("leftdown", "leftclick"):
1826
- self.pen = wx.Pen()
1827
- self.pen.SetColour(wx.Colour(swizzlecolor(elements.default_stroke)))
1828
- self.pen.SetWidth(25)
1829
- self.scene.pane.tool_active = True
1830
- self.scene.pane.modif_active = True
1831
-
1832
- self.scene.context.signal("statusmsg", self.message)
1833
- self.move_type = "node"
1834
-
1835
- xp = space_pos[0]
1836
- yp = space_pos[1]
1837
- if self.nodes:
1838
- w = offset * 4
1839
- h = offset * 4
1840
- node = self.element
1841
- for i, entry in enumerate(self.nodes):
1842
- pt = entry["point"]
1843
- ptx, pty = node.matrix.point_in_matrix_space(pt)
1844
- x = ptx - 2 * offset
1845
- y = pty - 2 * offset
1846
- if x <= xp <= x + w and y <= yp <= y + h:
1847
- self.selected_index = i
1848
- if entry["type"] == "control":
1849
- # We select the corresponding segment
1850
- for entry2 in self.nodes:
1851
- entry2["selected"] = False
1852
- orgnode = None
1853
- for j in range(0, 3):
1854
- k = i - j - 1
1855
- if k >= 0 and self.nodes[k]["type"] == "point":
1856
- orgnode = self.nodes[k]
1857
- break
1858
- if orgnode is not None:
1859
- orgnode["selected"] = True
1860
- entry["selected"] = True
1861
- else:
1862
- # Shift-Key Pressed?
1863
- if "shift" not in modifiers:
1864
- self.clear_selection()
1865
- entry["selected"] = True
1866
- else:
1867
- entry["selected"] = not entry["selected"]
1868
- break
1869
- else: # For-else == icky
1870
- self.selected_index = None
1871
- self.enable_rules()
1872
- if self.selected_index is None:
1873
- if event_type == "leftclick":
1874
- # Have we clicked outside the bbox? Then we call it a day...
1875
- outside = False
1876
- if not self.element:
1877
- # Element is required.
1878
- return RESPONSE_CONSUME
1879
- bb = self.element.bbox()
1880
- if bb is None:
1881
- return RESPONSE_CONSUME
1882
- if space_pos[0] < bb[0] or space_pos[0] > bb[2]:
1883
- outside = True
1884
- if space_pos[1] < bb[1] or space_pos[1] > bb[3]:
1885
- outside = True
1886
- if outside:
1887
- self.done()
1888
- return RESPONSE_CONSUME
1889
- else:
1890
- # Clear selection
1891
- self.clear_selection()
1892
- self.scene.request_refresh()
1893
- else:
1894
- # Fine we start a selection rectangle to select multiple nodes
1895
- self.move_type = "selection"
1896
- self.p1 = complex(space_pos[0], space_pos[1])
1897
- else:
1898
- self.scene.request_refresh()
1899
- return RESPONSE_CONSUME
1900
- elif event_type == "rightdown":
1901
- # We stop
1902
- self.done()
1903
- return RESPONSE_CONSUME
1904
- elif event_type == "move":
1905
- if self.move_type == "selection":
1906
- if self.p1 is not None:
1907
- self.p2 = complex(space_pos[0], space_pos[1])
1908
- self.scene.request_refresh()
1909
- else:
1910
- if self.selected_index is None or self.selected_index < 0:
1911
- self.scene.request_refresh()
1912
- return RESPONSE_CONSUME
1913
- current = self.nodes[self.selected_index]
1914
- pt = current["point"]
1915
-
1916
- m = self.element.matrix.point_in_inverse_space(space_pos[:2])
1917
- # Special treatment for the virtual midpoint:
1918
- if current["type"] == "midpoint" and self.node_type == "path":
1919
- self.scene.context.signal(
1920
- "statusmsg",
1921
- _(
1922
- "Drag to change the curve shape (ctrl to affect the other side)"
1923
- ),
1924
- )
1925
- idx = self.selected_index
1926
- newpt = Point(m[0], m[1])
1927
- change2nd = bool("ctrl" in modifiers)
1928
- self.revise_bezier_to_point(
1929
- current["segment"], newpt, change_2nd_control=change2nd
1930
- )
1931
- self.modify_element(False)
1932
- self.calculate_points(self.element)
1933
- self.selected_index = idx
1934
- self.nodes[idx]["selected"] = True
1935
- orgnode = None
1936
- for j in range(0, 3):
1937
- k = idx - j - 1
1938
- if k >= 0 and self.nodes[k]["type"] == "point":
1939
- orgnode = self.nodes[k]
1940
- break
1941
- if orgnode is not None:
1942
- orgnode["selected"] = True
1943
- self.scene.request_refresh()
1944
- return RESPONSE_CONSUME
1945
- pt.x = m[0]
1946
- pt.y = m[1]
1947
- if self.node_type == "path":
1948
- current["point"] = pt
1949
- # We need to adjust the start-point of the next segment
1950
- # unless it's a closed path then we need to adjust the
1951
- # very first - need to be mindful of closed subpaths
1952
- if current["segtype"] == "M":
1953
- # We changed the end, let's check whether the last segment in
1954
- # the subpath is a Close then we need to change this .end as well
1955
- for nidx in range(self.selected_index + 1, len(self.path), 1):
1956
- nextseg = self.path[nidx]
1957
- if isinstance(nextseg, Move):
1958
- break
1959
- if isinstance(nextseg, Close):
1960
- nextseg.end.x = m[0]
1961
- nextseg.end.y = m[1]
1962
- break
1963
- nextseg = current["next"]
1964
- if nextseg is not None and nextseg.start is not None:
1965
- nextseg.start.x = m[0]
1966
- nextseg.start.y = m[1]
1967
-
1968
- if isinstance(current["segment"], CubicBezier):
1969
- self.adjust_midpoint(self.selected_index)
1970
- elif isinstance(current["segment"], Move):
1971
- if nextseg is not None and isinstance(nextseg, CubicBezier):
1972
- self.adjust_midpoint(self.selected_index + 1)
1973
-
1974
- # self.debug_path()
1975
- self.modify_element(False)
1976
- return RESPONSE_CONSUME
1977
- elif event_type == "key_down":
1978
- if not self.scene.pane.tool_active:
1979
- return RESPONSE_CHAIN
1980
- # print (f"event: {event_type}, modifiers: '{modifiers}', keycode: '{keycode}'")
1981
- return RESPONSE_CONSUME
1982
- elif event_type == "key_up":
1983
- if not self.scene.pane.tool_active:
1984
- return RESPONSE_CHAIN
1985
- # print (f"event: {event_type}, modifiers: '{modifiers}', keycode: '{keycode}'")
1986
- if modifiers == "escape":
1987
- self.done()
1988
- return RESPONSE_CONSUME
1989
- # print(f"Key: '{keycode}'")
1990
- if not self.selected_index is None:
1991
- entry = self.nodes[self.selected_index]
1992
- else:
1993
- entry = None
1994
- self.perform_action(keycode)
1995
-
1996
- return RESPONSE_CONSUME
1997
-
1998
- elif event_type == "lost":
1999
- if self.scene.pane.tool_active:
2000
- self.done()
2001
- return RESPONSE_CONSUME
2002
- else:
2003
- return RESPONSE_CHAIN
2004
- elif event_type == "leftup":
2005
- if (
2006
- self.move_type == "selection"
2007
- and self.p1 is not None
2008
- and self.p2 is not None
2009
- ):
2010
- if "shift" not in modifiers:
2011
- self.clear_selection()
2012
- x0 = min(self.p1.real, self.p2.real)
2013
- y0 = min(self.p1.imag, self.p2.imag)
2014
- x1 = max(self.p1.real, self.p2.real)
2015
- y1 = max(self.p1.imag, self.p2.imag)
2016
- dx = self.p1.real - self.p2.real
2017
- dy = self.p1.imag - self.p2.imag
2018
- if abs(dx) < 1e-10 or abs(dy) < 1e-10:
2019
- return RESPONSE_CONSUME
2020
- # We select all points (not controls) inside
2021
- if self.element:
2022
- for entry in self.nodes:
2023
- pt = entry["point"]
2024
- if (
2025
- entry["type"] == "point"
2026
- and x0 <= pt.x <= x1
2027
- and y0 <= pt.y <= y1
2028
- ):
2029
- entry["selected"] = True
2030
- self.scene.request_refresh()
2031
- self.enable_rules()
2032
- self.p1 = None
2033
- self.p2 = None
2034
- return RESPONSE_CONSUME
2035
- return RESPONSE_DROP
2036
-
2037
- def perform_action(self, code):
2038
- """
2039
- Translates a keycode into a command to execute
2040
- """
2041
- # print(f"Perform action called with {code}")
2042
- if self.element is None or self.nodes is None:
2043
- return
2044
- if code in self.commands:
2045
- action = self.commands[code]
2046
- # print(f"Execute {action[1]}")
2047
- action[0]()
2048
-
2049
- def signal(self, signal, *args, **kwargs):
2050
- """
2051
- Signal routine for stuff that's passed along within a scene,
2052
- does not receive global signals
2053
- """
2054
- # print(f"Signal: {signal}")
2055
- if signal == "tool_changed":
2056
- if len(args) > 0 and len(args[0]) > 1 and args[0][1] == "edit":
2057
- selected_node = self.scene.context.elements.first_element(
2058
- emphasized=True
2059
- )
2060
- if selected_node is not None:
2061
- self.calculate_points(selected_node)
2062
- self.scene.request_refresh()
2063
- self.enable_rules()
2064
- return
2065
- elif signal == "rebuild_tree":
2066
- selected_node = self.scene.context.elements.first_element(emphasized=True)
2067
- if selected_node is None:
2068
- self.done()
2069
- else:
2070
- self.calculate_points(selected_node)
2071
- self.enable_rules()
2072
- self.scene.request_refresh()
2073
- if self.element is None:
2074
- return
1
+ import math
2
+ from copy import copy
3
+
4
+ import wx
5
+
6
+ from meerk40t.gui.icons import (
7
+ STD_ICON_SIZE,
8
+ icon_node_add,
9
+ icon_node_append,
10
+ icon_node_break,
11
+ icon_node_close,
12
+ icon_node_curve,
13
+ icon_node_delete,
14
+ icon_node_join,
15
+ icon_node_line,
16
+ icon_node_line_all,
17
+ icon_node_smooth,
18
+ icon_node_smooth_all,
19
+ icon_node_symmetric,
20
+ )
21
+ from meerk40t.gui.laserrender import swizzlecolor
22
+ from meerk40t.gui.scene.sceneconst import (
23
+ RESPONSE_CHAIN,
24
+ RESPONSE_CONSUME,
25
+ RESPONSE_DROP,
26
+ )
27
+ from meerk40t.gui.toolwidgets.toolwidget import ToolWidget
28
+ from meerk40t.gui.wxutils import get_matrix_scale
29
+ from meerk40t.svgelements import (
30
+ Arc,
31
+ Close,
32
+ CubicBezier,
33
+ Line,
34
+ Move,
35
+ Path,
36
+ Point,
37
+ Polygon,
38
+ Polyline,
39
+ QuadraticBezier,
40
+ )
41
+ from meerk40t.tools.geomstr import Geomstr
42
+
43
+ _ = wx.GetTranslation
44
+
45
+
46
+ class EditTool(ToolWidget):
47
+ """
48
+ Edit tool allows you to view and edit the nodes within a
49
+ selected element in the scene. It can currently handle
50
+ polylines / polygons and paths.
51
+ """
52
+
53
+ def __init__(self, scene, mode=None):
54
+ ToolWidget.__init__(self, scene)
55
+ self._listener_active = False
56
+ self.nodes = []
57
+ self.shape = None
58
+ self.path = None
59
+ self.element = None
60
+ self.selected_index = None
61
+
62
+ self.move_type = "node"
63
+ self.node_type = "path"
64
+ self.p1 = None
65
+ self.p2 = None
66
+ self.pen = wx.Pen()
67
+ self.pen.SetColour(wx.BLUE)
68
+ # wx.Colour(swizzlecolor(self.scene.context.elements.default_stroke))
69
+ self.pen_ctrl = wx.Pen()
70
+ self.pen_ctrl.SetColour(wx.CYAN)
71
+ self.pen_ctrl_semi = wx.Pen()
72
+ self.pen_ctrl_semi.SetColour(wx.GREEN)
73
+ self.pen_highlight = wx.Pen()
74
+ self.pen_highlight.SetColour(wx.RED)
75
+ self.pen_highlight_line = wx.Pen()
76
+ self.pen_highlight_line.SetColour(wx.Colour(255, 0, 0, 80))
77
+ self.pen_selection = wx.Pen()
78
+ self.pen_selection.SetColour(self.scene.colors.color_selection3)
79
+ self.pen_selection.SetStyle(wx.PENSTYLE_SHORT_DASH)
80
+ self.brush_highlight = wx.Brush(wx.RED_BRUSH)
81
+ self.brush_normal = wx.Brush(wx.TRANSPARENT_BRUSH)
82
+ # want to have sharp edges
83
+ self.pen_selection.SetJoin(wx.JOIN_MITER)
84
+ # "key": (routine, info, available for poly, available for path)
85
+ self.commands = {
86
+ "d": (self.delete_nodes, _("Delete"), True, True),
87
+ "delete": (self.delete_nodes, _("Delete"), True, True),
88
+ "l": (self.convert_to_line, _("Line"), False, True),
89
+ "c": (self.convert_to_curve, _("Curve"), False, True),
90
+ "s": (self.cubic_symmetrical, _("Symmetric"), False, True),
91
+ "i": (self.insert_midpoint, _("Insert"), True, True),
92
+ "insert": (self.insert_midpoint, _("Insert"), True, True),
93
+ "a": (self.append_line, _("Append"), True, True),
94
+ "b": (self.break_path, _("Break"), False, True),
95
+ "j": (self.join_path, _("Join"), False, True),
96
+ "o": (self.smoothen, _("Smooth"), False, True),
97
+ "z": (self.toggle_close, _("Close path"), True, True),
98
+ "v": (self.smoothen_all, _("Smooth all"), False, True),
99
+ "w": (self.linear_all, _("Line all"), False, True),
100
+ "p": (self.convert_to_path, _("To path"), True, False),
101
+ }
102
+ self.define_buttons()
103
+ self.message = ""
104
+
105
+ def define_buttons(self):
106
+ def becomes_enabled(needs_selection, active_for_path, active_for_poly):
107
+ def routine(*args):
108
+ # print(
109
+ # f"Was asked to perform with {my_selection}, {my_active_poly}, {my_active_path} while {self.anyselected} + {self.node_type}"
110
+ # )
111
+ if self.element is None:
112
+ return False
113
+ flag_sel = True
114
+ flag_poly = False
115
+ flag_path = False
116
+ if my_selection and not self.anyselected:
117
+ flag_sel = False
118
+ if my_active_poly and self.node_type == "polyline":
119
+ flag_poly = True
120
+ if my_active_path and self.node_type == "path":
121
+ flag_path = True
122
+ flag = flag_sel and (flag_path or flag_poly)
123
+ return flag
124
+
125
+ my_selection = needs_selection
126
+ my_active_poly = active_for_poly
127
+ my_active_path = active_for_path
128
+ return routine
129
+
130
+ def becomes_visible(active_for_path, active_for_poly):
131
+ def routine(*args):
132
+ # print(
133
+ # f"Was asked to perform with {my_active_poly}, {my_active_path} while {self.anyselected} + {self.node_type}"
134
+ # )
135
+ flag_poly = False
136
+ flag_path = False
137
+ if my_active_poly and self.node_type == "polyline":
138
+ flag_poly = True
139
+ if my_active_path and self.node_type == "path":
140
+ flag_path = True
141
+ flag = flag_path or flag_poly
142
+ return flag
143
+
144
+ my_active_path = active_for_path
145
+ my_active_poly = active_for_poly
146
+ return routine
147
+
148
+ def do_action(code):
149
+ def routine(*args):
150
+ self.perform_action(mycode)
151
+
152
+ mycode = code
153
+ return routine
154
+
155
+ cmd_icons = {
156
+ # "command": [
157
+ # image, requires_selection,
158
+ # active_for_path, active_for_poly,
159
+ # "tooltiptext", button],
160
+ "i": [
161
+ icon_node_add,
162
+ True,
163
+ True,
164
+ True,
165
+ _("Insert point before"),
166
+ _("Insert"),
167
+ ],
168
+ "a": [
169
+ icon_node_append,
170
+ False,
171
+ True,
172
+ True,
173
+ _("Append point at end"),
174
+ _("Append"),
175
+ ],
176
+ "d": [
177
+ icon_node_delete,
178
+ True,
179
+ True,
180
+ True,
181
+ _("Delete point"),
182
+ _("Delete"),
183
+ ],
184
+ "l": [
185
+ icon_node_line,
186
+ True,
187
+ True,
188
+ False,
189
+ _("Make segment a line"),
190
+ _("> Line"),
191
+ ],
192
+ "c": [
193
+ icon_node_curve,
194
+ True,
195
+ True,
196
+ False,
197
+ _("Make segment a curve"),
198
+ _("> Curve"),
199
+ ],
200
+ "s": [
201
+ icon_node_symmetric,
202
+ True,
203
+ True,
204
+ False,
205
+ _("Make segment symmetrical"),
206
+ _("Symmetric"),
207
+ ],
208
+ "j": [
209
+ icon_node_join,
210
+ True,
211
+ True,
212
+ False,
213
+ _("Join two segments"),
214
+ _("Join"),
215
+ ],
216
+ "b": [
217
+ icon_node_break,
218
+ True,
219
+ True,
220
+ False,
221
+ _("Break segment apart"),
222
+ _("Break"),
223
+ ],
224
+ "o": [
225
+ icon_node_smooth,
226
+ True,
227
+ True,
228
+ False,
229
+ _("Smooth transit to adjacent segments"),
230
+ _("Smooth"),
231
+ ],
232
+ "v": [
233
+ icon_node_smooth_all,
234
+ False,
235
+ True,
236
+ False,
237
+ _("Convert all lines into smooth curves"),
238
+ _("Very smooth"),
239
+ ],
240
+ "w": [
241
+ icon_node_line_all,
242
+ False,
243
+ True,
244
+ False,
245
+ _("Convert all segments into lines"),
246
+ _("Line all"),
247
+ ],
248
+ "z": [
249
+ icon_node_close,
250
+ False,
251
+ True,
252
+ True,
253
+ _("Toggle closed status"),
254
+ _("Close"),
255
+ ],
256
+ "p": [
257
+ icon_node_smooth_all,
258
+ False,
259
+ False,
260
+ True,
261
+ _("Convert polyline to a path element"),
262
+ _("To Path"),
263
+ ],
264
+ }
265
+ icon_size = STD_ICON_SIZE
266
+ for command, entry in cmd_icons.items():
267
+ # print(command, f"button/secondarytool_edit/tool_{command}")
268
+ self.scene.context.kernel.register(
269
+ f"button/secondarytool_edit/tool_{command}",
270
+ {
271
+ "label": entry[5],
272
+ "icon": entry[0],
273
+ "tip": entry[4],
274
+ "help": "nodeedit",
275
+ "action": do_action(command),
276
+ "size": icon_size,
277
+ "rule_enabled": becomes_enabled(entry[1], entry[2], entry[3]),
278
+ "rule_visible": becomes_visible(entry[2], entry[3]),
279
+ },
280
+ )
281
+
282
+ def enable_rules(self):
283
+ toolbar = self.scene.context.lookup("ribbonbar/tools")
284
+ if toolbar is not None:
285
+ toolbar.apply_enable_rules()
286
+
287
+ def final(self, context):
288
+ """
289
+ Shutdown routine for widget that unregisters the listener routines
290
+ and closes the toolbar window.
291
+ This could be called more than once which, if not dealt with, will
292
+ cause a console warning message
293
+ """
294
+ if self._listener_active:
295
+ self.scene.context.unlisten("emphasized", self.on_emphasized_changed)
296
+ self.scene.context.unlisten("nodeedit", self.on_signal_nodeedit)
297
+ self._listener_active = False
298
+ self.scene.request_refresh()
299
+
300
+ def init(self, context):
301
+ """
302
+ Startup routine for widget that establishes the listener routines
303
+ and opens the toolbar window
304
+ """
305
+ self.scene.context.listen("emphasized", self.on_emphasized_changed)
306
+ self.scene.context.listen("nodeedit", self.on_signal_nodeedit)
307
+ self._listener_active = True
308
+
309
+ def on_emphasized_changed(self, origin, *args):
310
+ """
311
+ Receiver routine for scene selection signal
312
+ """
313
+ selected_node = self.scene.context.elements.first_element(emphasized=True)
314
+ if selected_node is not self.element:
315
+ self.calculate_points(selected_node)
316
+ self.scene.request_refresh()
317
+ self.enable_rules()
318
+
319
+ def set_pen_widths(self):
320
+ """
321
+ Calculate the pen widths according to the current scene zoom levels,
322
+ so that they always appear 1 pixel wide - except for the
323
+ pen associated to the path segment outline that gets a 2 pixel wide 'halo'
324
+ """
325
+
326
+ def set_width_pen(pen, width):
327
+ try:
328
+ try:
329
+ pen.SetWidth(width)
330
+ except TypeError:
331
+ pen.SetWidth(int(width))
332
+ except OverflowError:
333
+ pass # Exceeds 32 bit signed integer.
334
+
335
+ matrix = self.scene.widget_root.scene_widget.matrix
336
+ linewidth = 1.0 / get_matrix_scale(matrix)
337
+ if linewidth < 1:
338
+ linewidth = 1
339
+ set_width_pen(self.pen, linewidth)
340
+ set_width_pen(self.pen_highlight, linewidth)
341
+ set_width_pen(self.pen_ctrl, linewidth)
342
+ set_width_pen(self.pen_ctrl_semi, linewidth)
343
+ set_width_pen(self.pen_selection, linewidth)
344
+ value = linewidth
345
+ if self.element is not None and hasattr(self.element, "stroke_width"):
346
+ if self.element.stroke_width is not None:
347
+ value = self.element.stroke_width
348
+ value += 4 * linewidth
349
+ set_width_pen(self.pen_highlight_line, value)
350
+
351
+ def calculate_points(self, selected_node):
352
+ """
353
+ Parse the element and create a list of dictionaries with relevant information required for display and logic
354
+ """
355
+ self.message = ""
356
+
357
+ self.element = selected_node
358
+ self.selected_index = None
359
+ self.nodes = []
360
+ # print ("After load:")
361
+ # self.debug_path()
362
+ if selected_node is None:
363
+ return
364
+ self.shape = None
365
+ self.path = None
366
+ if selected_node.type == "elem polyline":
367
+ self.node_type = "polyline"
368
+ try:
369
+ self.shape = selected_node.shape
370
+ except AttributeError:
371
+ return
372
+ start = 0
373
+ for idx, pt in enumerate(self.shape.points):
374
+ self.nodes.append(
375
+ {
376
+ "prev": None,
377
+ "next": None,
378
+ "point": pt,
379
+ "segment": None,
380
+ "path": self.shape,
381
+ "type": "point",
382
+ "connector": -1,
383
+ "selected": False,
384
+ "segtype": "L",
385
+ "start": start,
386
+ }
387
+ )
388
+ else:
389
+ self.node_type = "path"
390
+ # self.path = selected_node.geometry.as_path()
391
+ if hasattr(selected_node, "path"):
392
+ self.path = selected_node.path
393
+ elif hasattr(selected_node, "geometry"):
394
+ self.path = selected_node.geometry.as_path()
395
+ elif hasattr(selected_node, "as_geometry"):
396
+ self.path = selected_node.as_geometry().as_path()
397
+ elif hasattr(selected_node, "as_path"):
398
+ self.path = selected_node.as_path()
399
+ else:
400
+ return
401
+ # print(self.path.d(), self.path)
402
+ if self.path is None:
403
+ return
404
+ self.path.approximate_arcs_with_cubics()
405
+ # print(self.path.d(), self.path)
406
+ # try:
407
+ # except AttributeError:
408
+ # return
409
+ # print (f"Path: {str(path)}")
410
+ prev_seg = None
411
+ start = 0
412
+ # Idx of last point
413
+ l_idx = 0
414
+ for idx, segment in enumerate(self.path):
415
+ # print(
416
+ # f"{idx}# {type(segment).__name__} - S={segment.start} - E={segment.end}"
417
+ # )
418
+ if idx < len(self.path) - 1:
419
+ next_seg = self.path[idx + 1]
420
+ else:
421
+ next_seg = None
422
+ if isinstance(segment, Move):
423
+ if idx != start:
424
+ start = idx
425
+
426
+ if isinstance(segment, Close):
427
+ # We don't do anything with a Close - it's drawn anyway
428
+ pass
429
+ elif isinstance(segment, Line):
430
+ self.nodes.append(
431
+ {
432
+ "prev": prev_seg,
433
+ "next": next_seg,
434
+ "point": segment.end,
435
+ "segment": segment,
436
+ "path": self.path,
437
+ "type": "point",
438
+ "connector": -1,
439
+ "selected": False,
440
+ "segtype": "Z" if isinstance(segment, Close) else "L",
441
+ "start": start,
442
+ "pathindex": idx,
443
+ }
444
+ )
445
+ nidx = len(self.nodes) - 1
446
+ elif isinstance(segment, Move):
447
+ self.nodes.append(
448
+ {
449
+ "prev": prev_seg,
450
+ "next": next_seg,
451
+ "point": segment.end,
452
+ "segment": segment,
453
+ "path": self.path,
454
+ "type": "point",
455
+ "connector": -1,
456
+ "selected": False,
457
+ "segtype": "M",
458
+ "start": start,
459
+ "pathindex": idx,
460
+ }
461
+ )
462
+ nidx = len(self.nodes) - 1
463
+ elif isinstance(segment, QuadraticBezier):
464
+ self.nodes.append(
465
+ {
466
+ "prev": prev_seg,
467
+ "next": next_seg,
468
+ "point": segment.end,
469
+ "segment": segment,
470
+ "path": self.path,
471
+ "type": "point",
472
+ "connector": -1,
473
+ "selected": False,
474
+ "segtype": "Q",
475
+ "start": start,
476
+ "pathindex": idx,
477
+ }
478
+ )
479
+ nidx = len(self.nodes) - 1
480
+ self.nodes.append(
481
+ {
482
+ "prev": None,
483
+ "next": None,
484
+ "point": segment.control,
485
+ "segment": segment,
486
+ "path": self.path,
487
+ "type": "control",
488
+ "connector": nidx,
489
+ "selected": False,
490
+ "segtype": "",
491
+ "start": start,
492
+ "pathindex": idx,
493
+ }
494
+ )
495
+ elif isinstance(segment, CubicBezier):
496
+ self.nodes.append(
497
+ {
498
+ "prev": prev_seg,
499
+ "next": next_seg,
500
+ "point": segment.end,
501
+ "segment": segment,
502
+ "path": self.path,
503
+ "type": "point",
504
+ "connector": -1,
505
+ "selected": False,
506
+ "segtype": "C",
507
+ "start": start,
508
+ "pathindex": idx,
509
+ }
510
+ )
511
+ nidx = len(self.nodes) - 1
512
+ self.nodes.append(
513
+ {
514
+ "prev": None,
515
+ "next": None,
516
+ "point": segment.control1,
517
+ "segment": segment,
518
+ "path": self.path,
519
+ "type": "control",
520
+ "connector": l_idx,
521
+ "selected": False,
522
+ "segtype": "",
523
+ "start": start,
524
+ "pathindex": idx,
525
+ }
526
+ )
527
+ self.nodes.append(
528
+ {
529
+ "prev": None,
530
+ "next": None,
531
+ "point": segment.control2,
532
+ "segment": segment,
533
+ "path": self.path,
534
+ "type": "control",
535
+ "connector": nidx,
536
+ "selected": False,
537
+ "segtype": "",
538
+ "start": start,
539
+ "pathindex": idx,
540
+ }
541
+ )
542
+ # midp = segment.point(0.5)
543
+ midp = self.get_bezier_point(segment, 0.5)
544
+ self.nodes.append(
545
+ {
546
+ "prev": None,
547
+ "next": None,
548
+ "point": midp,
549
+ "segment": segment,
550
+ "path": self.path,
551
+ "type": "midpoint",
552
+ "connector": -1,
553
+ "selected": False,
554
+ "segtype": "",
555
+ "start": start,
556
+ "pathindex": idx,
557
+ }
558
+ )
559
+ elif isinstance(segment, Arc):
560
+ self.nodes.append(
561
+ {
562
+ "prev": prev_seg,
563
+ "next": next_seg,
564
+ "point": segment.end,
565
+ "segment": segment,
566
+ "path": self.path,
567
+ "type": "point",
568
+ "connector": -1,
569
+ "selected": False,
570
+ "segtype": "A",
571
+ "start": start,
572
+ "pathindex": idx,
573
+ }
574
+ )
575
+ nidx = len(self.nodes) - 1
576
+ self.nodes.append(
577
+ {
578
+ "prev": None,
579
+ "next": None,
580
+ "point": segment.center,
581
+ "segment": segment,
582
+ "path": self.path,
583
+ "type": "control",
584
+ "connector": nidx,
585
+ "selected": False,
586
+ "segtype": "",
587
+ "start": start,
588
+ "pathindex": idx,
589
+ }
590
+ )
591
+ prev_seg = segment
592
+ l_idx = nidx
593
+ for cmd in self.commands:
594
+ action = self.commands[cmd]
595
+ if self.node_type == "path" and action[3]:
596
+ if self.message:
597
+ self.message += ", "
598
+ self.message += f"{cmd}: {action[1]}"
599
+ if self.node_type == "polyline" and action[2]:
600
+ if self.message:
601
+ self.message += ", "
602
+ self.message += f"{cmd}: {action[1]}"
603
+
604
+ self.enable_rules()
605
+
606
+ def calc_and_draw(self, gc):
607
+ """
608
+ Takes a svgelements.Path and converts it to a GraphicsContext.Graphics Path
609
+ """
610
+
611
+ def deal_with_segment(seg, init):
612
+ if isinstance(seg, Line):
613
+ if not init:
614
+ init = True
615
+ ptx, pty = node.matrix.point_in_matrix_space(seg.start)
616
+ p.MoveToPoint(ptx, pty)
617
+ ptx, pty = node.matrix.point_in_matrix_space(seg.end)
618
+ p.AddLineToPoint(ptx, pty)
619
+ elif isinstance(seg, Close):
620
+ if not init:
621
+ init = True
622
+ ptx, pty = node.matrix.point_in_matrix_space(seg.start)
623
+ p.MoveToPoint(ptx, pty)
624
+ p.CloseSubpath()
625
+ elif isinstance(seg, QuadraticBezier):
626
+ if not init:
627
+ init = True
628
+ ptx, pty = node.matrix.point_in_matrix_space(seg.start)
629
+ p.MoveToPoint(ptx, pty)
630
+ ptx, pty = node.matrix.point_in_matrix_space(seg.end)
631
+ c1x, c1y = node.matrix.point_in_matrix_space(seg.control)
632
+ p.AddQuadCurveToPoint(c1x, c1y, ptx, pty)
633
+ elif isinstance(seg, CubicBezier):
634
+ if not init:
635
+ init = True
636
+ ptx, pty = node.matrix.point_in_matrix_space(seg.start)
637
+ p.MoveToPoint(ptx, pty)
638
+ ptx, pty = node.matrix.point_in_matrix_space(seg.end)
639
+ c1x, c1y = node.matrix.point_in_matrix_space(seg.control1)
640
+ c2x, c2y = node.matrix.point_in_matrix_space(seg.control2)
641
+ p.AddCurveToPoint(c1x, c1y, c2x, c2y, ptx, pty)
642
+ elif isinstance(seg, Arc):
643
+ if not init:
644
+ init = True
645
+ ptx, pty = node.matrix.point_in_matrix_space(seg.start)
646
+ p.MoveToPoint(ptx, pty)
647
+ for curve in seg.as_cubic_curves():
648
+ ptx, pty = node.matrix.point_in_matrix_space(curve.end)
649
+ c1x, c1y = node.matrix.point_in_matrix_space(curve.control1)
650
+ c2x, c2y = node.matrix.point_in_matrix_space(curve.control2)
651
+ p.AddCurveToPoint(c1x, c1y, c2x, c2y, ptx, pty)
652
+ return init
653
+
654
+ node = self.element
655
+ p = gc.CreatePath()
656
+ if self.node_type == "polyline":
657
+ for idx, entry in enumerate(self.nodes):
658
+ ptx, pty = node.matrix.point_in_matrix_space(entry["point"])
659
+ # print (f"Idx={idx}, selected={entry['selected']}, prev={'-' if idx == 0 else self.nodes[idx-1]['selected']}")
660
+ if idx == 1 and (
661
+ self.nodes[0]["selected"] or self.nodes[1]["selected"]
662
+ ):
663
+ p.AddLineToPoint(ptx, pty)
664
+ elif idx == 0 or not entry["selected"]:
665
+ p.MoveToPoint(ptx, pty)
666
+ else:
667
+ p.AddLineToPoint(ptx, pty)
668
+ else:
669
+ # path = self.path
670
+ init = False
671
+ for idx, entry in enumerate(self.nodes):
672
+ if not entry["type"] == "point":
673
+ continue
674
+ # treatment = ""
675
+ e = entry["segment"]
676
+ if isinstance(e, Move):
677
+ if entry["selected"]:
678
+ # The next segment needs to be highlighted...
679
+ ptx, pty = node.matrix.point_in_matrix_space(e.end)
680
+ p.MoveToPoint(ptx, pty)
681
+ e = entry["next"]
682
+ init = deal_with_segment(e, init)
683
+ # treatment = "move+next"
684
+ else:
685
+ ptx, pty = node.matrix.point_in_matrix_space(e.end)
686
+ p.MoveToPoint(ptx, pty)
687
+ init = True
688
+ # treatment = "move"
689
+ elif not entry["selected"]:
690
+ ptx, pty = node.matrix.point_in_matrix_space(e.end)
691
+ p.MoveToPoint(ptx, pty)
692
+ init = True
693
+ # treatment = "nonselected"
694
+ else:
695
+ init = deal_with_segment(e, init)
696
+ # treatment = "selected"
697
+ # print (f"#{idx} {entry['type']} got treatment: {treatment}")
698
+
699
+ gc.SetPen(self.pen_highlight_line)
700
+ gc.DrawPath(p)
701
+
702
+ def process_draw(self, gc: wx.GraphicsContext):
703
+ """
704
+ Widget-Routine to draw the different elements on the provided GraphicContext
705
+ """
706
+
707
+ def draw_selection_rectangle():
708
+ x0 = min(self.p1.real, self.p2.real)
709
+ y0 = min(self.p1.imag, self.p2.imag)
710
+ x1 = max(self.p1.real, self.p2.real)
711
+ y1 = max(self.p1.imag, self.p2.imag)
712
+ gc.SetPen(self.pen_selection)
713
+ gc.SetBrush(wx.TRANSPARENT_BRUSH)
714
+ gc.DrawRectangle(x0, y0, x1 - x0, y1 - y0)
715
+
716
+ if not self.nodes:
717
+ return
718
+ self.set_pen_widths()
719
+ if self.p1 is not None and self.p2 is not None:
720
+ # Selection mode!
721
+ draw_selection_rectangle()
722
+ return
723
+ offset = 5
724
+ s = math.sqrt(abs(self.scene.widget_root.scene_widget.matrix.determinant))
725
+ offset /= s
726
+ gc.SetBrush(wx.TRANSPARENT_BRUSH)
727
+ idx = -1
728
+ node = self.element
729
+ self.calc_and_draw(gc)
730
+ for entry in self.nodes:
731
+ idx += 1
732
+ ptx, pty = node.matrix.point_in_matrix_space(entry["point"])
733
+ if entry["type"] == "point":
734
+ if idx == self.selected_index or entry["selected"]:
735
+ gc.SetPen(self.pen_highlight)
736
+ gc.SetBrush(self.brush_highlight)
737
+ factor = 1.25
738
+ else:
739
+ gc.SetPen(self.pen)
740
+ gc.SetBrush(self.brush_normal)
741
+ factor = 1
742
+ gc.DrawEllipse(
743
+ ptx - factor * offset,
744
+ pty - factor * offset,
745
+ offset * 2 * factor,
746
+ offset * 2 * factor,
747
+ )
748
+ elif entry["type"] == "control":
749
+ if idx == self.selected_index or entry["selected"]:
750
+ factor = 1.25
751
+ gc.SetPen(self.pen_highlight)
752
+ else:
753
+ factor = 1
754
+ gc.SetPen(self.pen_ctrl)
755
+ # Do we have a second controlpoint at the same segment?
756
+ if isinstance(entry["segment"], CubicBezier):
757
+ orgnode = None
758
+ if idx > 0 and self.nodes[idx - 1]["type"] == "point":
759
+ orgnode = self.nodes[idx - 1]
760
+ elif idx > 1 and self.nodes[idx - 2]["type"] == "point":
761
+ orgnode = self.nodes[idx - 2]
762
+ if orgnode is not None and orgnode["selected"]:
763
+ gc.SetPen(self.pen_ctrl_semi)
764
+ pattern = [
765
+ (ptx - factor * offset, pty),
766
+ (ptx, pty + factor * offset),
767
+ (ptx + factor * offset, pty),
768
+ (ptx, pty - factor * offset),
769
+ (ptx - factor * offset, pty),
770
+ ]
771
+ gc.DrawLines(pattern)
772
+ if 0 <= entry["connector"] < len(self.nodes):
773
+ orgnode = self.nodes[entry["connector"]]
774
+ org_pt = orgnode["point"]
775
+ org_ptx, org_pty = node.matrix.point_in_matrix_space(org_pt)
776
+ pattern = [(ptx, pty), (org_ptx, org_pty)]
777
+ gc.DrawLines(pattern)
778
+ elif entry["type"] == "midpoint":
779
+ if idx == self.selected_index or entry["selected"]:
780
+ factor = 1.25
781
+ gc.SetPen(self.pen_highlight)
782
+ else:
783
+ factor = 1
784
+ gc.SetPen(self.pen_ctrl)
785
+ pattern = [
786
+ (ptx - factor * offset, pty),
787
+ (ptx, pty + factor * offset),
788
+ (ptx + factor * offset, pty),
789
+ (ptx, pty - factor * offset),
790
+ (ptx - factor * offset, pty),
791
+ ]
792
+ gc.DrawLines(pattern)
793
+
794
+ def done(self):
795
+ """
796
+ We are done with node editing, so shutdown stuff
797
+ """
798
+ self.scene.pane.tool_active = False
799
+ self.scene.pane.modif_active = False
800
+ self.scene.pane.suppress_selection = False
801
+ self.p1 = None
802
+ self.p2 = None
803
+ self.move_type = "node"
804
+ self.scene.context("tool none\n")
805
+ self.scene.context.signal("statusmsg", "")
806
+ self.scene.context.elements.validate_selected_area()
807
+ self.scene.request_refresh()
808
+
809
+ def modify_element(self, reload=True):
810
+ """
811
+ Central routine that tells the system that the node was
812
+ changed, if 'reload' is set to True then it requires
813
+ reload/recalculation of the properties (e.g. after the
814
+ segment structure of a path was changed)
815
+ """
816
+ if self.element is None:
817
+ return
818
+ if self.shape is not None:
819
+ self.element.geometry = Geomstr.svg(Path(self.shape))
820
+ elif self.path is not None:
821
+ self.element.geometry = Geomstr.svg(self.path)
822
+ self.element.altered()
823
+ try:
824
+ __ = self.element.bbox()
825
+ except AttributeError:
826
+ pass
827
+ self.scene.context.elements.validate_selected_area()
828
+ self.scene.request_refresh()
829
+ self.scene.context.signal("element_property_reload", [self.element])
830
+ if reload:
831
+ self.calculate_points(self.element)
832
+ self.scene.request_refresh()
833
+ self.enable_rules()
834
+
835
+ def clear_selection(self):
836
+ """
837
+ Clears the selection
838
+ """
839
+ if self.nodes is not None:
840
+ for entry in self.nodes:
841
+ entry["selected"] = False
842
+ self.enable_rules()
843
+
844
+ def first_segment_in_subpath(self, index):
845
+ """
846
+ Provides the first non-move/close segment in the subpath
847
+ to which the segment at location index belongs to
848
+ """
849
+ result = None
850
+ if not self.element is None and hasattr(self.element, "path"):
851
+ for idx in range(index, -1, -1):
852
+ seg = self.path[idx]
853
+ if isinstance(seg, (Move, Close)):
854
+ break
855
+ result = seg
856
+ return result
857
+
858
+ def last_segment_in_subpath(self, index):
859
+ """
860
+ Provides the last non-move/close segment in the subpath
861
+ to which the segment at location index belongs to
862
+ """
863
+ result = None
864
+ if not self.element is None and hasattr(self.element, "path"):
865
+ for idx in range(index, len(self.path)):
866
+ seg = self.path[idx]
867
+ if isinstance(seg, (Move, Close)):
868
+ break
869
+ result = seg
870
+ return result
871
+
872
+ def is_closed_subpath(self, index):
873
+ """
874
+ Provides the last segment in the subpath
875
+ to which the segment at location index belongs to
876
+ """
877
+ result = False
878
+ if not self.element is None and hasattr(self.element, "path"):
879
+ for idx in range(index, len(self.path)):
880
+ seg = self.path[idx]
881
+ if isinstance(seg, Move):
882
+ break
883
+ if isinstance(seg, Close):
884
+ result = True
885
+ break
886
+ return result
887
+
888
+ def convert_to_path(self):
889
+ """
890
+ Converts a polyline element to a path and reloads the scene
891
+ """
892
+ if self.element is None or hasattr(self.element, "path"):
893
+ return
894
+ node = self.element
895
+ oldstuff = []
896
+ for attrib in ("stroke", "fill", "stroke_width", "stroke_scaled"):
897
+ if hasattr(node, attrib):
898
+ oldval = getattr(node, attrib, None)
899
+ oldstuff.append([attrib, oldval])
900
+ try:
901
+ path = node.as_path()
902
+ # There are some challenges around the treatment
903
+ # of arcs within svgelements, so let's circumvent
904
+ # them for the time being (until resolved)
905
+ # by replacing arc segments with cubic Béziers
906
+ if node.type in ("elem path", "elem ellipse"):
907
+ path.approximate_arcs_with_cubics()
908
+ except AttributeError:
909
+ return
910
+ newnode = node.replace_node(path=path, type="elem path")
911
+ for item in oldstuff:
912
+ setattr(newnode, item[0], item[1])
913
+ newnode.altered()
914
+ self.element = newnode
915
+ self.shape = None
916
+ self.path = path
917
+ self.modify_element(reload=True)
918
+
919
+ def toggle_close(self):
920
+ """
921
+ Toggle the closed status for a polyline or path element
922
+ """
923
+ if self.element is None or self.nodes is None:
924
+ return
925
+ modified = False
926
+ if self.node_type == "polyline":
927
+ dist = (self.shape.points[0].x - self.shape.points[-1].x) ** 2 + (
928
+ self.shape.points[0].y - self.shape.points[-1].y
929
+ ) ** 2
930
+ if dist < 1: # Closed
931
+ newshape = Polyline(self.shape)
932
+ if len(newshape.points) > 2:
933
+ newshape.points.pop(-1)
934
+ else:
935
+ newshape = Polygon(self.shape)
936
+ self.shape = newshape
937
+ modified = True
938
+ else:
939
+ dealt_with = []
940
+ if not self.anyselected:
941
+ # Let's select the last point, so the last segment will be closed/opened
942
+ for idx in range(len(self.nodes) - 1, -1, -1):
943
+ entry = self.nodes[idx]
944
+ if entry["type"] == "point":
945
+ entry["selected"] = True
946
+ break
947
+
948
+ for idx in range(len(self.nodes) - 1, -1, -1):
949
+ entry = self.nodes[idx]
950
+ if entry["selected"] and entry["type"] == "point":
951
+ # What's the index of the last selected element
952
+ # Have we dealt with that before? i.e. not multiple toggles.
953
+ segstart = entry["start"]
954
+ if segstart in dealt_with:
955
+ continue
956
+ dealt_with.append(segstart)
957
+ # Let's establish the last segment in the path
958
+ prevseg = None
959
+ is_closed = False
960
+ firstseg = None
961
+ for sidx in range(segstart, len(self.path), 1):
962
+ seg = self.path[sidx]
963
+ if isinstance(seg, Move) and prevseg is None:
964
+ # Not the one at the very beginning!
965
+ continue
966
+ if isinstance(seg, Move):
967
+ # Ready
968
+ break
969
+ if isinstance(seg, Close):
970
+ # Ready
971
+ is_closed = True
972
+ break
973
+ if firstseg is None:
974
+ firstseg = seg
975
+ lastidx = sidx
976
+ prevseg = seg
977
+ if firstseg is not None and not is_closed:
978
+ dist = firstseg.start.distance_to(prevseg.end)
979
+ if dist < 1:
980
+ lastidx -= 1
981
+ is_closed = True
982
+ # else:
983
+ # dist = 1e6
984
+ if is_closed:
985
+ # it's enough just to delete it...
986
+ del self.path[lastidx + 1]
987
+ modified = True
988
+ else:
989
+ # Need to insert a Close segment
990
+ # print(f"Inserting a close, dist={dist:.2f}")
991
+ # print(
992
+ # f"First seg, idx={segstart}, type={type(firstseg).__name__}"
993
+ # )
994
+ # print(f"Last seg, idx={lastidx}, type={type(prevseg).__name__}")
995
+ newseg = Close(
996
+ start=Point(prevseg.end.x, prevseg.end.y),
997
+ end=Point(prevseg.end.x, prevseg.end.y),
998
+ )
999
+ self.path.insert(lastidx + 1, newseg)
1000
+ modified = True
1001
+
1002
+ if modified:
1003
+ self.modify_element(True)
1004
+
1005
+ @staticmethod
1006
+ def get_bezier_point(segment, t):
1007
+ """
1008
+ Provide a point on the cubic Bézier curve for t (0 <= t <= 1)
1009
+ Args:
1010
+ segment (PathSegment): a cubic bezier
1011
+ t (float): (0 <= t <= 1)
1012
+ Computation: b(t) = (1-t)^3 * P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3 * P3
1013
+ """
1014
+ p0 = segment.start
1015
+ p1 = segment.control1
1016
+ p2 = segment.control2
1017
+ p3 = segment.end
1018
+ result = (
1019
+ (1 - t) ** 3 * p0
1020
+ + 3 * (1 - t) ** 2 * t * p1
1021
+ + 3 * (1 - t) * t**2 * p2
1022
+ + t**3 * p3
1023
+ )
1024
+ return result
1025
+
1026
+ @staticmethod
1027
+ def revise_bezier_to_point(segment, midpoint, change_2nd_control=False):
1028
+ """
1029
+ Adjust the two control points for a cubic Bézier segment,
1030
+ so that the given point will lie on the cubic Bézier curve for t=0.5
1031
+ Args:
1032
+ segment (PathSegment): a cubic bezier segment to be amended
1033
+ midpoint (Point): the new point
1034
+ change_2nd_control: modify the 2nd control point, rather than the first
1035
+ Computation: b(t) = (1-t)^3 * P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3 * P3
1036
+ """
1037
+ t = 0.5
1038
+ p0 = segment.start
1039
+ p1 = segment.control1
1040
+ p2 = segment.control2
1041
+ p3 = segment.end
1042
+ if change_2nd_control:
1043
+ factor = 1 / (3 * (1 - t) * t**2)
1044
+ result = (
1045
+ midpoint - (1 - t) ** 3 * p0 - 3 * (1 - t) ** 2 * t * p1 - t**3 * p3
1046
+ ) * factor
1047
+ segment.control2 = result
1048
+ else:
1049
+ factor = 1 / (3 * (1 - t) ** 2 * t)
1050
+ result = (
1051
+ midpoint - (1 - t) ** 3 * p0 - 3 * (1 - t) * t**2 * p2 - t**3 * p3
1052
+ ) * factor
1053
+ segment.control1 = result
1054
+
1055
+ def adjust_midpoint(self, index):
1056
+ """
1057
+ Computes and sets the midpoint of a cubic bezier segment
1058
+ """
1059
+ for j in range(3):
1060
+ k = index + 1 + j
1061
+ if k < len(self.nodes) and self.nodes[k]["type"] == "midpoint":
1062
+ self.nodes[k]["point"] = self.get_bezier_point(
1063
+ self.nodes[index]["segment"], 0.5
1064
+ )
1065
+ break
1066
+
1067
+ def smoothen(self):
1068
+ """
1069
+ Smoothen a circular bezier segment to adjacent segments, i.e. adjust
1070
+ the control points so that they are an extension of the previous/next segment
1071
+ """
1072
+ if self.element is None or self.nodes is None:
1073
+ return
1074
+ modified = False
1075
+ if self.node_type == "polyline":
1076
+ # Not valid for a polyline Could make a path now but that might be more than the user expected...
1077
+ return
1078
+ for entry in self.nodes:
1079
+ if entry["selected"] and entry["segtype"] == "C": # Cubic Bezier only
1080
+ segment = entry["segment"]
1081
+ pt_start = segment.start
1082
+ pt_end = segment.end
1083
+ other_segment = entry["prev"]
1084
+ if other_segment is not None:
1085
+ if isinstance(other_segment, Line):
1086
+ other_pt_x = other_segment.start.x
1087
+ other_pt_y = other_segment.start.y
1088
+ dx = pt_start.x - other_pt_x
1089
+ dy = pt_start.y - other_pt_y
1090
+ segment.control1.x = pt_start.x + 0.25 * dx
1091
+ segment.control1.y = pt_start.y + 0.25 * dy
1092
+ modified = True
1093
+ elif isinstance(other_segment, CubicBezier):
1094
+ other_pt_x = other_segment.control2.x
1095
+ other_pt_y = other_segment.control2.y
1096
+ dx = pt_start.x - other_pt_x
1097
+ dy = pt_start.y - other_pt_y
1098
+ segment.control1.x = pt_start.x + dx
1099
+ segment.control1.y = pt_start.y + dy
1100
+ modified = True
1101
+ elif isinstance(other_segment, QuadraticBezier):
1102
+ other_pt_x = other_segment.control.x
1103
+ other_pt_y = other_segment.control.y
1104
+ dx = pt_start.x - other_pt_x
1105
+ dy = pt_start.y - other_pt_y
1106
+ segment.control1.x = pt_start.x + dx
1107
+ segment.control1.y = pt_start.y + dy
1108
+ modified = True
1109
+ elif isinstance(other_segment, Arc):
1110
+ # We need the tangent in the end-point,
1111
+ other_pt_x = other_segment.end.x
1112
+ other_pt_y = other_segment.end.y
1113
+ dx = pt_start.x - other_pt_x
1114
+ dy = pt_start.y - other_pt_y
1115
+ segment.control1.x = pt_start.x + dx
1116
+ segment.control1.y = pt_start.y + dy
1117
+ modified = True
1118
+ other_segment = entry["next"]
1119
+ if other_segment is not None:
1120
+ if isinstance(other_segment, Line):
1121
+ other_pt_x = other_segment.end.x
1122
+ other_pt_y = other_segment.end.y
1123
+ dx = pt_end.x - other_pt_x
1124
+ dy = pt_end.y - other_pt_y
1125
+ segment.control2.x = pt_end.x + 0.25 * dx
1126
+ segment.control2.y = pt_end.y + 0.25 * dy
1127
+ modified = True
1128
+ elif isinstance(other_segment, CubicBezier):
1129
+ other_pt_x = other_segment.control1.x
1130
+ other_pt_y = other_segment.control1.y
1131
+ dx = pt_end.x - other_pt_x
1132
+ dy = pt_end.y - other_pt_y
1133
+ segment.control2.x = pt_end.x + dx
1134
+ segment.control2.y = pt_end.y + dy
1135
+ modified = True
1136
+ elif isinstance(other_segment, QuadraticBezier):
1137
+ other_pt_x = other_segment.control.x
1138
+ other_pt_y = other_segment.control.y
1139
+ dx = pt_end.x - other_pt_x
1140
+ dy = pt_end.y - other_pt_y
1141
+ segment.control2.x = pt_end.x + dx
1142
+ segment.control2.y = pt_end.y + dy
1143
+ modified = True
1144
+ elif isinstance(other_segment, Arc):
1145
+ # We need the tangent in the end-point,
1146
+ other_pt_x = other_segment.start.x
1147
+ other_pt_y = other_segment.start.y
1148
+ dx = pt_end.x - other_pt_x
1149
+ dy = pt_end.y - other_pt_y
1150
+ segment.control2.x = pt_end.x + dx
1151
+ segment.control2.y = pt_end.y + dy
1152
+ modified = True
1153
+ if modified:
1154
+ self.modify_element(True)
1155
+
1156
+ def smoothen_all(self):
1157
+ """
1158
+ Convert all segments of the path that are not cubic Béziers into
1159
+ such segments and apply the same smoothen logic as in smoothen(),
1160
+ i.e. adjust the control points of two neighbouring segments
1161
+ so that the three points
1162
+ 'prev control2' - 'prev/end=next start' - 'next control1'
1163
+ are collinear
1164
+ """
1165
+ if self.element is None or self.nodes is None:
1166
+ return
1167
+ modified = False
1168
+ if self.node_type == "polyline":
1169
+ # Not valid for a polyline Could make a path now but that might be more than the user expected...
1170
+ return
1171
+ # Pass 1 - make all lines a cubic bezier
1172
+ for idx, segment in enumerate(self.path):
1173
+ if isinstance(segment, Line):
1174
+ startpt = copy(segment.start)
1175
+ endpt = copy(segment.end)
1176
+ ctrl1pt = Point(
1177
+ startpt.x + 0.25 * (endpt.x - startpt.x),
1178
+ startpt.y + 0.25 * (endpt.y - startpt.y),
1179
+ )
1180
+ ctrl2pt = Point(
1181
+ startpt.x + 0.75 * (endpt.x - startpt.x),
1182
+ startpt.y + 0.75 * (endpt.y - startpt.y),
1183
+ )
1184
+ newsegment = CubicBezier(
1185
+ start=startpt, end=endpt, control1=ctrl1pt, control2=ctrl2pt
1186
+ )
1187
+ self.path[idx] = newsegment
1188
+ modified = True
1189
+ elif isinstance(segment, QuadraticBezier):
1190
+ # The cubic control - points lie on 2/3 of the way of the
1191
+ # line-segments from the endpoint to the quadratic control-point
1192
+ startpt = copy(segment.start)
1193
+ endpt = copy(segment.end)
1194
+ dx = segment.control.x - startpt.x
1195
+ dy = segment.control.y - startpt.y
1196
+ ctrl1pt = Point(startpt.x + 2 / 3 * dx, startpt.y + 2 / 3 * dy)
1197
+ dx = segment.control.x - endpt.x
1198
+ dy = segment.control.y - endpt.y
1199
+ ctrl2pt = Point(endpt.x + 2 / 3 * dx, endpt.y + 2 / 3 * dy)
1200
+ newsegment = CubicBezier(
1201
+ start=startpt, end=endpt, control1=ctrl1pt, control2=ctrl2pt
1202
+ )
1203
+ self.path[idx] = newsegment
1204
+ modified = True
1205
+ elif isinstance(segment, Arc):
1206
+ for newsegment in list(segment.as_cubic_curves(1)):
1207
+ self.path[idx] = newsegment
1208
+ break
1209
+ modified = True
1210
+ # Pass 2 - make all control lines align
1211
+ prevseg = None
1212
+ lastidx = len(self.path) - 1
1213
+ for idx, segment in enumerate(self.path):
1214
+ nextseg = None
1215
+ if idx < lastidx:
1216
+ nextseg = self.path[idx + 1]
1217
+ if isinstance(nextseg, (Move, Close)):
1218
+ nextseg = None
1219
+ if isinstance(segment, CubicBezier):
1220
+ if prevseg is None:
1221
+ if self.is_closed_subpath(idx):
1222
+ otherseg = self.last_segment_in_subpath(idx)
1223
+ prevseg = Line(
1224
+ start=Point(otherseg.end.x, otherseg.end.y),
1225
+ end=Point(segment.start.x, segment.start.y),
1226
+ )
1227
+ if prevseg is not None:
1228
+ angle1 = Point.angle(prevseg.end, prevseg.start)
1229
+ angle2 = Point.angle(segment.start, segment.end)
1230
+ d_angle = math.tau / 2 - (angle1 - angle2)
1231
+ while d_angle >= math.tau:
1232
+ d_angle -= math.tau
1233
+ while d_angle < -math.tau:
1234
+ d_angle += math.tau
1235
+
1236
+ # print (f"to prev: Angle 1 = {angle1/math.tau*360:.1f}°, Angle 2 = {angle2/math.tau*360:.1f}°, Delta = {d_angle/math.tau*360:.1f}°")
1237
+ dist = segment.start.distance_to(segment.control1)
1238
+ candidate1 = Point.polar(segment.start, angle2 - d_angle / 2, dist)
1239
+ candidate2 = Point.polar(segment.start, angle1 + d_angle / 2, dist)
1240
+ if segment.end.distance_to(candidate1) < segment.end.distance_to(
1241
+ candidate2
1242
+ ):
1243
+ segment.control1 = candidate1
1244
+ else:
1245
+ segment.control1 = candidate2
1246
+ modified = True
1247
+ if nextseg is None:
1248
+ if self.is_closed_subpath(idx):
1249
+ otherseg = self.first_segment_in_subpath(idx)
1250
+ nextseg = Line(
1251
+ start=Point(segment.end.x, segment.end.y),
1252
+ end=Point(otherseg.start.x, otherseg.start.y),
1253
+ )
1254
+ if nextseg is not None:
1255
+ angle1 = Point.angle(segment.end, segment.start)
1256
+ angle2 = Point.angle(nextseg.start, nextseg.end)
1257
+ d_angle = math.tau / 2 - (angle1 - angle2)
1258
+ while d_angle >= math.tau:
1259
+ d_angle -= math.tau
1260
+ while d_angle < -math.tau:
1261
+ d_angle += math.tau
1262
+
1263
+ # print (f"to next: Angle 1 = {angle1/math.tau*360:.1f}°, Angle 2 = {angle2/math.tau*360:.1f}°, Delta = {d_angle/math.tau*360:.1f}°")
1264
+ dist = segment.end.distance_to(segment.control2)
1265
+ candidate1 = Point.polar(segment.end, angle2 - d_angle / 2, dist)
1266
+ candidate2 = Point.polar(segment.end, angle1 + d_angle / 2, dist)
1267
+ if segment.start.distance_to(
1268
+ candidate1
1269
+ ) < segment.start.distance_to(candidate2):
1270
+ segment.control2 = candidate1
1271
+ else:
1272
+ segment.control2 = candidate2
1273
+ modified = True
1274
+ if isinstance(segment, (Move, Close)):
1275
+ prevseg = None
1276
+ else:
1277
+ prevseg = segment
1278
+ if modified:
1279
+ self.modify_element(True)
1280
+
1281
+ def cubic_symmetrical(self):
1282
+ """
1283
+ Adjust the two control points control1 and control2 of a cubic segment
1284
+ so that they are symmetrical to the perpendicular bisector on start - end
1285
+ """
1286
+ if self.element is None or self.nodes is None:
1287
+ return
1288
+ modified = False
1289
+ if self.node_type == "polyline":
1290
+ # Not valid for a polyline Could make a path now but that might be more than the user expected...
1291
+ return
1292
+ for entry in self.nodes:
1293
+ if entry["selected"] and entry["segtype"] == "C": # Cubic Bezier only
1294
+ segment = entry["segment"]
1295
+ pt_start = segment.start
1296
+ pt_end = segment.end
1297
+ midpoint = Point(
1298
+ (pt_end.x + pt_start.x) / 2, (pt_end.y + pt_start.y) / 2
1299
+ )
1300
+ angle_to_end = midpoint.angle_to(pt_end)
1301
+ angle_to_start = midpoint.angle_to(pt_start)
1302
+ angle_to_control2 = midpoint.angle_to(segment.control2)
1303
+ distance = midpoint.distance_to(segment.control2)
1304
+ # The new point
1305
+ angle_to_control1 = angle_to_start + (angle_to_end - angle_to_control2)
1306
+ # newx = midpoint.x + distance * math.cos(angle_to_control1)
1307
+ # newy = midpoint.y + distance * math.sin(angle_to_control1)
1308
+ # segment.control1 = Point(newx, newy)
1309
+ segment.control1 = Point.polar(
1310
+ (midpoint.x, midpoint.y), angle_to_control1, distance
1311
+ )
1312
+ modified = True
1313
+ if modified:
1314
+ self.modify_element(True)
1315
+
1316
+ def delete_nodes(self):
1317
+ """
1318
+ Delete all selected (point) nodes
1319
+ """
1320
+ if self.element is None or self.nodes is None:
1321
+ return
1322
+ modified = False
1323
+ for idx in range(len(self.nodes) - 1, -1, -1):
1324
+ entry = self.nodes[idx]
1325
+ if entry["selected"] and entry["type"] == "point":
1326
+ if self.node_type == "polyline":
1327
+ if len(self.shape.points) > 2:
1328
+ modified = True
1329
+ self.shape.points.pop(idx)
1330
+ else:
1331
+ break
1332
+ else:
1333
+ idx = entry["pathindex"]
1334
+ prevseg = None
1335
+ nextseg = None
1336
+ seg = self.path[idx]
1337
+ if idx > 0:
1338
+ prevseg = self.path[idx - 1]
1339
+ if idx < len(self.path) - 1:
1340
+ nextseg = self.path[idx + 1]
1341
+ if nextseg is None:
1342
+ # Last point of the path
1343
+ # Can just be deleted, provided we have something
1344
+ # in front...
1345
+ if prevseg is None or isinstance(prevseg, (Move, Close)):
1346
+ continue
1347
+ del self.path[idx]
1348
+ modified = True
1349
+ elif isinstance(nextseg, (Move, Close)):
1350
+ # last point of the subsegment...
1351
+ # We need to have another full segment in the front
1352
+ # otherwise we would end up with a single point...
1353
+ if prevseg is None or isinstance(prevseg, (Move, Close)):
1354
+ continue
1355
+ nextseg.start.x = seg.start.x
1356
+ nextseg.start.y = seg.start.y
1357
+ if isinstance(nextseg, Close):
1358
+ nextseg.end.x = seg.start.x
1359
+ nextseg.end.y = seg.start.y
1360
+
1361
+ del self.path[idx]
1362
+ modified = True
1363
+ else:
1364
+ # Could be the first point...
1365
+ if prevseg is None and (
1366
+ nextseg is None or isinstance(nextseg, (Move, Close))
1367
+ ):
1368
+ continue
1369
+ if prevseg is None: # # Move
1370
+ seg.end = Point(nextseg.end.x, nextseg.end.y)
1371
+ del self.path[idx + 1]
1372
+ modified = True
1373
+ elif isinstance(seg, Move): # # Move
1374
+ seg.end = Point(nextseg.end.x, nextseg.end.y)
1375
+ del self.path[idx + 1]
1376
+ modified = True
1377
+ else:
1378
+ nextseg.start.x = prevseg.end.x
1379
+ nextseg.start.y = prevseg.end.y
1380
+ del self.path[idx]
1381
+ modified = True
1382
+
1383
+ if modified:
1384
+ self.modify_element(True)
1385
+
1386
+ def convert_to_line(self):
1387
+ """
1388
+ Convert all selected segments to a line
1389
+ """
1390
+ if self.element is None or self.nodes is None:
1391
+ return
1392
+ modified = False
1393
+ if self.node_type == "polyline":
1394
+ # Not valid for a polyline Could make a path now but that might be more than the user expected...
1395
+ return
1396
+ for entry in self.nodes:
1397
+ if entry["selected"] and entry["type"] == "point":
1398
+ idx = entry["pathindex"]
1399
+ if entry["segment"] is None or entry["segment"].start is None:
1400
+ continue
1401
+ startpt = Point(entry["segment"].start.x, entry["segment"].start.y)
1402
+ endpt = Point(entry["segment"].end.x, entry["segment"].end.y)
1403
+ if entry["segtype"] not in ("C", "Q", "A"):
1404
+ continue
1405
+ newsegment = Line(start=startpt, end=endpt)
1406
+ self.path[idx] = newsegment
1407
+ modified = True
1408
+ if modified:
1409
+ self.modify_element(True)
1410
+
1411
+ def linear_all(self):
1412
+ """
1413
+ Convert all segments of the path to a line
1414
+ """
1415
+ if self.element is None or self.nodes is None:
1416
+ return
1417
+ modified = False
1418
+ if self.node_type == "polyline":
1419
+ # Not valid for a polyline Could make a path now but that might be more than the user expected...
1420
+ return
1421
+ for idx, segment in enumerate(self.path):
1422
+ if isinstance(segment, (Close, Move, Line)):
1423
+ continue
1424
+ startpt = Point(segment.start.x, segment.start.y)
1425
+ endpt = Point(segment.end.x, segment.end.y)
1426
+ newsegment = Line(start=startpt, end=endpt)
1427
+ self.path[idx] = newsegment
1428
+ modified = True
1429
+
1430
+ if modified:
1431
+ self.modify_element(True)
1432
+
1433
+ def convert_to_curve(self):
1434
+ """
1435
+ Convert all selected segments to a circular bezier
1436
+ """
1437
+ if self.element is None or self.nodes is None:
1438
+ return
1439
+ modified = False
1440
+ if self.node_type == "polyline":
1441
+ # Not valid for a polyline Could make a path now but that might be more than the user expected...
1442
+ return
1443
+ for entry in self.nodes:
1444
+ if entry["selected"] and entry["type"] == "point":
1445
+ idx = entry["pathindex"]
1446
+ if entry["segment"] is None or entry["segment"].start is None:
1447
+ continue
1448
+ startpt = Point(entry["segment"].start.x, entry["segment"].start.y)
1449
+ endpt = Point(entry["segment"].end.x, entry["segment"].end.y)
1450
+ if entry["segtype"] == "L":
1451
+ ctrl1pt = Point(
1452
+ startpt.x + 0.25 * (endpt.x - startpt.x),
1453
+ startpt.y + 0.25 * (endpt.y - startpt.y),
1454
+ )
1455
+ ctrl2pt = Point(
1456
+ startpt.x + 0.75 * (endpt.x - startpt.x),
1457
+ startpt.y + 0.75 * (endpt.y - startpt.y),
1458
+ )
1459
+ elif entry["segtype"] == "Q":
1460
+ ctrl1pt = Point(
1461
+ entry["segment"].control.x, entry["segment"].control.y
1462
+ )
1463
+ ctrl2pt = Point(endpt.x, endpt.y)
1464
+ elif entry["segtype"] == "A":
1465
+ ctrl1pt = Point(
1466
+ startpt.x + 0.25 * (endpt.x - startpt.x),
1467
+ startpt.y + 0.25 * (endpt.y - startpt.y),
1468
+ )
1469
+ ctrl2pt = Point(
1470
+ startpt.x + 0.75 * (endpt.x - startpt.x),
1471
+ startpt.y + 0.75 * (endpt.y - startpt.y),
1472
+ )
1473
+ else:
1474
+ continue
1475
+
1476
+ newsegment = CubicBezier(
1477
+ start=startpt, end=endpt, control1=ctrl1pt, control2=ctrl2pt
1478
+ )
1479
+ self.path[idx] = newsegment
1480
+ modified = True
1481
+ if modified:
1482
+ self.modify_element(True)
1483
+
1484
+ def break_path(self):
1485
+ """
1486
+ Break a path at the selected (point) nodes
1487
+ """
1488
+ if self.element is None or self.nodes is None:
1489
+ return
1490
+ # Stub for breaking the path
1491
+ modified = False
1492
+ if self.node_type == "polyline":
1493
+ # Not valid for a polyline Could make a path now but that might be more than the user expected...
1494
+ return
1495
+ for idx in range(len(self.nodes) - 1, -1, -1):
1496
+ entry = self.nodes[idx]
1497
+ if entry["selected"] and entry["type"] == "point":
1498
+ idx = entry["pathindex"]
1499
+ seg = entry["segment"]
1500
+ if isinstance(seg, (Move, Close)):
1501
+ continue
1502
+ # Is this the last point? Then no use to break the path
1503
+ nextseg = None
1504
+ if idx in (0, len(self.path) - 1):
1505
+ # Don't break at the first or last point
1506
+ continue
1507
+ nextseg = self.path[idx + 1]
1508
+ if isinstance(nextseg, (Move, Close)):
1509
+ # Not at end of subpath
1510
+ continue
1511
+ prevseg = self.path[idx - 1]
1512
+ if isinstance(prevseg, (Move, Close)):
1513
+ # We could still be at the end point of the first segment...
1514
+ if entry["point"] == seg.start:
1515
+ # Not at start of subpath
1516
+ continue
1517
+ newseg = Move(
1518
+ start=Point(seg.end.x, seg.end.y),
1519
+ end=Point(nextseg.start.x, nextseg.start.y),
1520
+ )
1521
+ self.path.insert(idx + 1, newseg)
1522
+ # Now let's validate whether the 'right' path still has a
1523
+ # close segment at its end. That will be removed as this would
1524
+ # create an unwanted behaviour
1525
+ prevseg = None
1526
+ is_closed = False
1527
+ for sidx in range(idx + 1, len(self.path), 1):
1528
+ seg = self.path[sidx]
1529
+ if isinstance(seg, Move) and prevseg is None:
1530
+ # Not the one at the very beginning!
1531
+ continue
1532
+ if isinstance(seg, Move):
1533
+ # Ready
1534
+ break
1535
+ if isinstance(seg, Close):
1536
+ # Ready
1537
+ is_closed = True
1538
+ break
1539
+ lastidx = sidx
1540
+ prevseg = seg
1541
+ if is_closed:
1542
+ # it's enough just to delete it...
1543
+ del self.path[lastidx + 1]
1544
+
1545
+ modified = True
1546
+ if modified:
1547
+ self.modify_element(True)
1548
+
1549
+ def join_path(self):
1550
+ """
1551
+ Join two selected (point) nodes if they are on different subpath
1552
+ """
1553
+ if self.element is None or self.nodes is None:
1554
+ return
1555
+ modified = False
1556
+ if self.node_type == "polyline":
1557
+ # Not valid for a polyline
1558
+ return
1559
+ for idx in range(len(self.nodes) - 1, -1, -1):
1560
+ entry = self.nodes[idx]
1561
+ if entry["selected"] and entry["type"] == "point":
1562
+ idx = entry["pathindex"]
1563
+ seg = entry["segment"]
1564
+ prevseg = None
1565
+ nextseg = None
1566
+ if idx > 0:
1567
+ prevseg = self.path[idx - 1]
1568
+ if idx < len(self.path) - 1:
1569
+ nextseg = self.path[idx + 1]
1570
+ if isinstance(seg, (Move, Close)):
1571
+ # Beginning of path
1572
+ if prevseg is None:
1573
+ # Very beginning?! Ignore...
1574
+ continue
1575
+ if nextseg is None:
1576
+ continue
1577
+ if isinstance(nextseg, (Move, Close)):
1578
+ # Two consecutive moves? Ignore....
1579
+ continue
1580
+ nextseg.start.x = seg.start.x
1581
+ nextseg.start.y = seg.start.y
1582
+ del self.path[idx]
1583
+ modified = True
1584
+ else:
1585
+ # Let's look at the next segment
1586
+ if nextseg is None:
1587
+ continue
1588
+ if not isinstance(nextseg, Move):
1589
+ continue
1590
+ seg.end.x = nextseg.end.x
1591
+ seg.end.y = nextseg.end.y
1592
+ del self.path[idx + 1]
1593
+ modified = True
1594
+
1595
+ if modified:
1596
+ self.modify_element(True)
1597
+
1598
+ def insert_midpoint(self):
1599
+ """
1600
+ Insert a point in the middle of a selected segment
1601
+ """
1602
+ if self.element is None or self.nodes is None:
1603
+ return
1604
+ modified = False
1605
+ # Move backwards as len will change
1606
+ for idx in range(len(self.nodes) - 1, -1, -1):
1607
+ entry = self.nodes[idx]
1608
+ if entry["selected"] and entry["type"] == "point":
1609
+ if self.node_type == "polyline":
1610
+ pt1 = self.shape.points[idx]
1611
+ if idx == 0:
1612
+ # Very first point? Mirror first segment and take midpoint
1613
+ pt2 = Point(
1614
+ self.shape.points[idx + 1].x,
1615
+ self.shape.points[idx + 1].y,
1616
+ )
1617
+ pt2.x = pt1.x - (pt2.x - pt1.x)
1618
+ pt2.y = pt1.y - (pt2.y - pt1.y)
1619
+ pt2.x = (pt1.x + pt2.x) / 2
1620
+ pt2.y = (pt1.y + pt2.y) / 2
1621
+ self.shape.points.insert(0, pt2)
1622
+ else:
1623
+ pt2 = Point(
1624
+ self.shape.points[idx - 1].x,
1625
+ self.shape.points[idx - 1].y,
1626
+ )
1627
+ pt2.x = (pt1.x + pt2.x) / 2
1628
+ pt2.y = (pt1.y + pt2.y) / 2
1629
+ # Mid point
1630
+ self.shape.points.insert(idx, pt2)
1631
+ modified = True
1632
+ else:
1633
+ # Path
1634
+ idx = entry["pathindex"]
1635
+ if entry["segment"] is None:
1636
+ continue
1637
+ segment = entry["segment"]
1638
+
1639
+ # def pt_info(pt):
1640
+ # return f"({pt.x:.0f}, {pt.y:.0f})"
1641
+
1642
+ if entry["segtype"] == "L":
1643
+ # Line
1644
+ mid_x = (segment.start.x + segment.end.x) / 2
1645
+ mid_y = (segment.start.y + segment.end.y) / 2
1646
+ newsegment = Line(
1647
+ start=Point(mid_x, mid_y),
1648
+ end=Point(segment.end.x, segment.end.y),
1649
+ )
1650
+ self.path.insert(idx + 1, newsegment)
1651
+ # path.insert may change the start and end point
1652
+ # of the segement to make sure it maintains a
1653
+ # contiguous path, so we need to set it again...
1654
+ newsegment.start.x = mid_x
1655
+ newsegment.start.y = mid_y
1656
+ segment.end.x = mid_x
1657
+ segment.end.y = mid_y
1658
+ modified = True
1659
+ elif entry["segtype"] == "C":
1660
+ midpoint = segment.point(0.5)
1661
+ mid_x = midpoint.x
1662
+ mid_y = midpoint.y
1663
+ newsegment = CubicBezier(
1664
+ start=Point(mid_x, mid_y),
1665
+ end=Point(segment.end.x, segment.end.y),
1666
+ control1=Point(mid_x, mid_y),
1667
+ control2=Point(segment.control2.x, segment.control2.y),
1668
+ )
1669
+ self.path.insert(idx + 1, newsegment)
1670
+ segment.end.x = mid_x
1671
+ segment.end.y = mid_y
1672
+ segment.control2.x = mid_x
1673
+ segment.control2.y = mid_y
1674
+ newsegment.start.x = mid_x
1675
+ newsegment.start.y = mid_y
1676
+ modified = True
1677
+ elif entry["segtype"] == "A":
1678
+ midpoint = segment.point(0.5)
1679
+ mid_x = midpoint.x
1680
+ mid_y = midpoint.y
1681
+ # newsegment = Arc(
1682
+ # start=Point(mid_x, mid_y),
1683
+ # end=Point(segment.end.x, segment.end.y),
1684
+ # control=Point(segment.center.x, segment.center.y),
1685
+ # )
1686
+ newsegment = copy(segment)
1687
+ newsegment.start.x = mid_x
1688
+ newsegment.start.y = mid_y
1689
+ self.path.insert(idx + 1, newsegment)
1690
+ segment.end.x = mid_x
1691
+ segment.end.y = mid_y
1692
+ newsegment.start.x = mid_x
1693
+ newsegment.start.y = mid_y
1694
+ modified = True
1695
+ elif entry["segtype"] == "Q":
1696
+ midpoint = segment.point(0.5)
1697
+ mid_x = midpoint.x
1698
+ mid_y = midpoint.y
1699
+ newsegment = QuadraticBezier(
1700
+ start=Point(mid_x, mid_y),
1701
+ end=Point(segment.end.x, segment.end.y),
1702
+ control=Point(segment.control.x, segment.control.y),
1703
+ )
1704
+ self.path.insert(idx + 1, newsegment)
1705
+ segment.end.x = mid_x
1706
+ segment.end.y = mid_y
1707
+ segment.control.x = mid_x
1708
+ segment.control.y = mid_y
1709
+ newsegment.start.x = mid_x
1710
+ newsegment.start.y = mid_y
1711
+ modified = True
1712
+ elif entry["segtype"] == "M":
1713
+ # Very first point? Mirror first segment and take midpoint
1714
+ nextseg = entry["next"]
1715
+ if nextseg is None:
1716
+ continue
1717
+ p1x = nextseg.start.x
1718
+ p1y = nextseg.start.y
1719
+ p2x = nextseg.end.x
1720
+ p2y = nextseg.end.y
1721
+ p2x = p1x - (p2x - p1x)
1722
+ p2y = p1y - (p2y - p1y)
1723
+ pt1 = Point((p1x + p2x) / 2, (p1y + p2y) / 2)
1724
+ pt2 = copy(nextseg.start)
1725
+ newsegment = Line(start=pt1, end=pt2)
1726
+ self.path.insert(idx + 1, newsegment)
1727
+ segment.end = pt1
1728
+ newsegment.start.x = pt1.x
1729
+ newsegment.start.y = pt1.y
1730
+ # We need to step forward to assess whether there is a close segment
1731
+ for idx2 in range(idx + 1, len(self.path)):
1732
+ if isinstance(self.path[idx2], Move):
1733
+ break
1734
+ if isinstance(self.path[idx2], Close):
1735
+ # Adjust the close segment to that it points again
1736
+ # to the first move end
1737
+ self.path[idx2].end = Point(pt1.x, pt1.y)
1738
+ break
1739
+
1740
+ modified = True
1741
+
1742
+ if modified:
1743
+ self.modify_element(True)
1744
+
1745
+ def append_line(self):
1746
+ """
1747
+ Append a point to the selected element, works all the time and does not require a valid selection
1748
+ """
1749
+ if self.element is None or self.nodes is None:
1750
+ return
1751
+ modified = False
1752
+ if self.node_type == "polyline":
1753
+ idx = len(self.shape.points) - 1
1754
+ pt1 = self.shape.points[idx - 1]
1755
+ pt2 = self.shape.points[idx]
1756
+ newpt = Point(pt2.x + (pt2.x - pt1.x), pt2.y + (pt2.y - pt1.y))
1757
+ self.shape.points.append(newpt)
1758
+ modified = True
1759
+ else:
1760
+ # path
1761
+ try:
1762
+ valididx = len(self.path) - 1
1763
+ except AttributeError:
1764
+ # Shape
1765
+ return
1766
+ while valididx >= 0 and isinstance(self.path[valididx], (Close, Move)):
1767
+ valididx -= 1
1768
+ if valididx >= 0:
1769
+ seg = self.path[valididx]
1770
+ pt1 = seg.start
1771
+ pt2 = seg.end
1772
+ newpt = Point(pt2.x + (pt2.x - pt1.x), pt2.y + (pt2.y - pt1.y))
1773
+ newsegment = Line(start=Point(seg.end.x, seg.end.y), end=newpt)
1774
+ if valididx < len(self.path) - 1:
1775
+ if self.path[valididx + 1].end == self.path[valididx + 1].start:
1776
+ self.path[valididx + 1].end.x = newpt.x
1777
+ self.path[valididx + 1].end.y = newpt.y
1778
+ self.path[valididx + 1].start.x = newpt.x
1779
+ self.path[valididx + 1].start.y = newpt.y
1780
+
1781
+ self.path.insert(valididx + 1, newsegment)
1782
+ newsegment.start.x = seg.end.x
1783
+ newsegment.start.y = seg.end.y
1784
+ modified = True
1785
+
1786
+ if modified:
1787
+ self.modify_element(True)
1788
+
1789
+ @property
1790
+ def anyselected(self):
1791
+ if self.nodes:
1792
+ for entry in self.nodes:
1793
+ if entry["selected"]:
1794
+ return True
1795
+ return False
1796
+
1797
+ def event(
1798
+ self,
1799
+ window_pos=None,
1800
+ space_pos=None,
1801
+ event_type=None,
1802
+ nearest_snap=None,
1803
+ modifiers=None,
1804
+ keycode=None,
1805
+ **kwargs,
1806
+ ):
1807
+ """
1808
+ The routine dealing with propagated scene events
1809
+
1810
+ Args:
1811
+ window_pos (tuple): The coordinates of the mouse position in window coordinates
1812
+ space_pos (tuple): The coordinates of the mouse position in scene coordinates
1813
+ event_type (string): [description]. Defaults to None.
1814
+ nearest_snap (tuple, optional): If set the coordinates of the nearest snap point in scene coordinates.
1815
+ modifiers (string): If available provides a list of modifier keys that were pressed (shift, alt, ctrl).
1816
+ keycode (string): if available the keycode that was pressed
1817
+
1818
+ Returns:
1819
+ Indicator how to proceed with this event after its execution (consume, chain etc.)
1820
+ """
1821
+ if self.scene.pane.active_tool != "edit":
1822
+ return RESPONSE_CHAIN
1823
+ # print (f"event: {event_type}, modifiers: '{modifiers}', keycode: '{keycode}'")
1824
+ offset = 5
1825
+ s = math.sqrt(abs(self.scene.widget_root.scene_widget.matrix.determinant))
1826
+ offset /= s
1827
+ elements = self.scene.context.elements
1828
+ if event_type in ("leftdown", "leftclick"):
1829
+ self.pen = wx.Pen()
1830
+ self.pen.SetColour(wx.Colour(swizzlecolor(elements.default_stroke)))
1831
+ self.pen.SetWidth(25)
1832
+ self.scene.pane.tool_active = True
1833
+ self.scene.pane.modif_active = True
1834
+
1835
+ self.scene.context.signal("statusmsg", self.message)
1836
+ self.move_type = "node"
1837
+
1838
+ xp = space_pos[0]
1839
+ yp = space_pos[1]
1840
+ if self.nodes:
1841
+ w = offset * 4
1842
+ h = offset * 4
1843
+ node = self.element
1844
+ for i, entry in enumerate(self.nodes):
1845
+ pt = entry["point"]
1846
+ ptx, pty = node.matrix.point_in_matrix_space(pt)
1847
+ x = ptx - 2 * offset
1848
+ y = pty - 2 * offset
1849
+ if x <= xp <= x + w and y <= yp <= y + h:
1850
+ self.selected_index = i
1851
+ if entry["type"] == "control":
1852
+ # We select the corresponding segment
1853
+ for entry2 in self.nodes:
1854
+ entry2["selected"] = False
1855
+ orgnode = None
1856
+ for j in range(0, 3):
1857
+ k = i - j - 1
1858
+ if k >= 0 and self.nodes[k]["type"] == "point":
1859
+ orgnode = self.nodes[k]
1860
+ break
1861
+ if orgnode is not None:
1862
+ orgnode["selected"] = True
1863
+ entry["selected"] = True
1864
+ else:
1865
+ # Shift-Key Pressed?
1866
+ if "shift" not in modifiers:
1867
+ self.clear_selection()
1868
+ entry["selected"] = True
1869
+ else:
1870
+ entry["selected"] = not entry["selected"]
1871
+ break
1872
+ else: # For-else == icky
1873
+ self.selected_index = None
1874
+ self.enable_rules()
1875
+ if self.selected_index is None:
1876
+ if event_type == "leftclick":
1877
+ # Have we clicked outside the bbox? Then we call it a day...
1878
+ outside = False
1879
+ if not self.element:
1880
+ # Element is required.
1881
+ return RESPONSE_CONSUME
1882
+ bb = self.element.bbox()
1883
+ if bb is None:
1884
+ return RESPONSE_CONSUME
1885
+ if space_pos[0] < bb[0] or space_pos[0] > bb[2]:
1886
+ outside = True
1887
+ if space_pos[1] < bb[1] or space_pos[1] > bb[3]:
1888
+ outside = True
1889
+ if outside:
1890
+ self.done()
1891
+ return RESPONSE_CONSUME
1892
+ else:
1893
+ # Clear selection
1894
+ self.clear_selection()
1895
+ self.scene.request_refresh()
1896
+ else:
1897
+ # Fine we start a selection rectangle to select multiple nodes
1898
+ self.move_type = "selection"
1899
+ self.p1 = complex(space_pos[0], space_pos[1])
1900
+ else:
1901
+ self.scene.request_refresh()
1902
+ return RESPONSE_CONSUME
1903
+ elif event_type == "rightdown":
1904
+ # We stop
1905
+ self.done()
1906
+ return RESPONSE_CONSUME
1907
+ elif event_type == "move":
1908
+ if self.move_type == "selection":
1909
+ if self.p1 is not None:
1910
+ self.p2 = complex(space_pos[0], space_pos[1])
1911
+ self.scene.request_refresh()
1912
+ else:
1913
+ if self.selected_index is None or self.selected_index < 0:
1914
+ self.scene.request_refresh()
1915
+ return RESPONSE_CONSUME
1916
+ current = self.nodes[self.selected_index]
1917
+ pt = current["point"]
1918
+ if nearest_snap is None:
1919
+ spt = Point(space_pos[0], space_pos[1])
1920
+ else:
1921
+ spt = Point(nearest_snap[0], nearest_snap[1])
1922
+
1923
+ m = self.element.matrix.point_in_inverse_space(spt)
1924
+ # Special treatment for the virtual midpoint:
1925
+ if current["type"] == "midpoint" and self.node_type == "path":
1926
+ self.scene.context.signal(
1927
+ "statusmsg",
1928
+ _(
1929
+ "Drag to change the curve shape (ctrl to affect the other side)"
1930
+ ),
1931
+ )
1932
+ idx = self.selected_index
1933
+ newpt = Point(m[0], m[1])
1934
+ change2nd = bool("ctrl" in modifiers)
1935
+ self.revise_bezier_to_point(
1936
+ current["segment"], newpt, change_2nd_control=change2nd
1937
+ )
1938
+ self.modify_element(False)
1939
+ self.calculate_points(self.element)
1940
+ self.selected_index = idx
1941
+ self.nodes[idx]["selected"] = True
1942
+ orgnode = None
1943
+ for j in range(0, 3):
1944
+ k = idx - j - 1
1945
+ if k >= 0 and self.nodes[k]["type"] == "point":
1946
+ orgnode = self.nodes[k]
1947
+ break
1948
+ if orgnode is not None:
1949
+ orgnode["selected"] = True
1950
+ self.scene.request_refresh()
1951
+ return RESPONSE_CONSUME
1952
+ pt.x = m[0]
1953
+ pt.y = m[1]
1954
+ if self.node_type == "path":
1955
+ current["point"] = pt
1956
+ # We need to adjust the start-point of the next segment
1957
+ # unless it's a closed path then we need to adjust the
1958
+ # very first - need to be mindful of closed subpaths
1959
+ if current["segtype"] == "M":
1960
+ # We changed the end, let's check whether the last segment in
1961
+ # the subpath is a Close then we need to change this .end as well
1962
+ for nidx in range(self.selected_index + 1, len(self.path), 1):
1963
+ nextseg = self.path[nidx]
1964
+ if isinstance(nextseg, Move):
1965
+ break
1966
+ if isinstance(nextseg, Close):
1967
+ nextseg.end.x = m[0]
1968
+ nextseg.end.y = m[1]
1969
+ break
1970
+ nextseg = current["next"]
1971
+ if nextseg is not None and nextseg.start is not None:
1972
+ nextseg.start.x = m[0]
1973
+ nextseg.start.y = m[1]
1974
+
1975
+ if isinstance(current["segment"], CubicBezier):
1976
+ self.adjust_midpoint(self.selected_index)
1977
+ elif isinstance(current["segment"], Move):
1978
+ if nextseg is not None and isinstance(nextseg, CubicBezier):
1979
+ self.adjust_midpoint(self.selected_index + 1)
1980
+
1981
+ # self.debug_path()
1982
+ self.modify_element(False)
1983
+ return RESPONSE_CONSUME
1984
+ elif event_type == "key_down":
1985
+ if not self.scene.pane.tool_active:
1986
+ return RESPONSE_CHAIN
1987
+ # print (f"event: {event_type}, modifiers: '{modifiers}', keycode: '{keycode}'")
1988
+ return RESPONSE_CONSUME
1989
+ elif event_type == "key_up":
1990
+ if not self.scene.pane.tool_active:
1991
+ return RESPONSE_CHAIN
1992
+ # print (f"event: {event_type}, modifiers: '{modifiers}', keycode: '{keycode}'")
1993
+ if modifiers == "escape":
1994
+ self.done()
1995
+ return RESPONSE_CONSUME
1996
+ # print(f"Key: '{keycode}'")
1997
+ # if self.selected_index is not None:
1998
+ # entry = self.nodes[self.selected_index]
1999
+ # else:
2000
+ # entry = None
2001
+ self.perform_action(modifiers)
2002
+
2003
+ return RESPONSE_CONSUME
2004
+
2005
+ elif event_type == "lost":
2006
+ if self.scene.pane.tool_active:
2007
+ self.done()
2008
+ return RESPONSE_CONSUME
2009
+ else:
2010
+ return RESPONSE_CHAIN
2011
+ elif event_type == "leftup":
2012
+ if (
2013
+ self.move_type == "selection"
2014
+ and self.p1 is not None
2015
+ and self.p2 is not None
2016
+ ):
2017
+ if "shift" not in modifiers:
2018
+ self.clear_selection()
2019
+ x0 = min(self.p1.real, self.p2.real)
2020
+ y0 = min(self.p1.imag, self.p2.imag)
2021
+ x1 = max(self.p1.real, self.p2.real)
2022
+ y1 = max(self.p1.imag, self.p2.imag)
2023
+ dx = self.p1.real - self.p2.real
2024
+ dy = self.p1.imag - self.p2.imag
2025
+ if abs(dx) < 1e-10 or abs(dy) < 1e-10:
2026
+ return RESPONSE_CONSUME
2027
+ # We select all points (not controls) inside
2028
+ if self.element:
2029
+ for entry in self.nodes:
2030
+ pt = entry["point"]
2031
+ if (
2032
+ entry["type"] == "point"
2033
+ and x0 <= pt.x <= x1
2034
+ and y0 <= pt.y <= y1
2035
+ ):
2036
+ entry["selected"] = True
2037
+ self.scene.request_refresh()
2038
+ self.enable_rules()
2039
+ self.p1 = None
2040
+ self.p2 = None
2041
+ return RESPONSE_CONSUME
2042
+ return RESPONSE_DROP
2043
+
2044
+ def perform_action(self, code):
2045
+ """
2046
+ Translates a keycode into a command to execute
2047
+ """
2048
+ # print(f"Perform action called with {code}")
2049
+ if self.element is None or self.nodes is None:
2050
+ return
2051
+ if code in self.commands:
2052
+ action = self.commands[code]
2053
+ # print(f"Execute {action[1]}")
2054
+ action[0]()
2055
+ # else:
2056
+ # print (f"Did not find {code}")
2057
+
2058
+ def _tool_change(self):
2059
+ selected_node = None
2060
+ elements = self.scene.context.elements.elem_branch
2061
+ for node in elements.flat(emphasized=True):
2062
+ if node.type in ("elem path", "elem polyline"):
2063
+ selected_node = node
2064
+ break
2065
+ self.scene.pane.suppress_selection = selected_node is not None
2066
+ if selected_node is None:
2067
+ self.done()
2068
+ else:
2069
+ self.calculate_points(selected_node)
2070
+ self.enable_rules()
2071
+ self.scene.request_refresh()
2072
+
2073
+ def signal(self, signal, *args, **kwargs):
2074
+ """
2075
+ Signal routine for stuff that's passed along within a scene,
2076
+ does not receive global signals
2077
+ """
2078
+ # print(f"Signal: {signal}")
2079
+ if signal == "tool_changed":
2080
+ if len(args) > 0 and len(args[0]) > 1 and args[0][1] == "edit":
2081
+ self._tool_change()
2082
+ return
2083
+ elif signal == "rebuild_tree":
2084
+ self._tool_change()
2085
+ elif signal == "emphasized":
2086
+ self._tool_change()
2087
+ if self.element is None:
2088
+ return