meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7020__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. meerk40t/__init__.py +1 -1
  2. meerk40t/balormk/balor_params.py +167 -167
  3. meerk40t/balormk/clone_loader.py +457 -457
  4. meerk40t/balormk/controller.py +1566 -1512
  5. meerk40t/balormk/cylindermod.py +64 -0
  6. meerk40t/balormk/device.py +966 -1959
  7. meerk40t/balormk/driver.py +778 -591
  8. meerk40t/balormk/galvo_commands.py +1194 -0
  9. meerk40t/balormk/gui/balorconfig.py +237 -111
  10. meerk40t/balormk/gui/balorcontroller.py +191 -184
  11. meerk40t/balormk/gui/baloroperationproperties.py +116 -115
  12. meerk40t/balormk/gui/corscene.py +845 -0
  13. meerk40t/balormk/gui/gui.py +179 -147
  14. meerk40t/balormk/livelightjob.py +466 -382
  15. meerk40t/balormk/mock_connection.py +131 -109
  16. meerk40t/balormk/plugin.py +133 -135
  17. meerk40t/balormk/usb_connection.py +306 -301
  18. meerk40t/camera/__init__.py +1 -1
  19. meerk40t/camera/camera.py +514 -397
  20. meerk40t/camera/gui/camerapanel.py +1241 -1095
  21. meerk40t/camera/gui/gui.py +58 -58
  22. meerk40t/camera/plugin.py +441 -399
  23. meerk40t/ch341/__init__.py +27 -27
  24. meerk40t/ch341/ch341device.py +628 -628
  25. meerk40t/ch341/libusb.py +595 -589
  26. meerk40t/ch341/mock.py +171 -171
  27. meerk40t/ch341/windriver.py +157 -157
  28. meerk40t/constants.py +13 -0
  29. meerk40t/core/__init__.py +1 -1
  30. meerk40t/core/bindalias.py +550 -539
  31. meerk40t/core/core.py +47 -47
  32. meerk40t/core/cutcode/cubiccut.py +73 -73
  33. meerk40t/core/cutcode/cutcode.py +315 -312
  34. meerk40t/core/cutcode/cutgroup.py +141 -137
  35. meerk40t/core/cutcode/cutobject.py +192 -185
  36. meerk40t/core/cutcode/dwellcut.py +37 -37
  37. meerk40t/core/cutcode/gotocut.py +29 -29
  38. meerk40t/core/cutcode/homecut.py +29 -29
  39. meerk40t/core/cutcode/inputcut.py +34 -34
  40. meerk40t/core/cutcode/linecut.py +33 -33
  41. meerk40t/core/cutcode/outputcut.py +34 -34
  42. meerk40t/core/cutcode/plotcut.py +335 -335
  43. meerk40t/core/cutcode/quadcut.py +61 -61
  44. meerk40t/core/cutcode/rastercut.py +168 -148
  45. meerk40t/core/cutcode/waitcut.py +34 -34
  46. meerk40t/core/cutplan.py +1843 -1316
  47. meerk40t/core/drivers.py +330 -329
  48. meerk40t/core/elements/align.py +801 -669
  49. meerk40t/core/elements/branches.py +1858 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4595 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4315 -3617
  54. meerk40t/core/elements/files.py +117 -64
  55. meerk40t/core/elements/geometry.py +473 -224
  56. meerk40t/core/elements/grid.py +467 -316
  57. meerk40t/core/elements/materials.py +158 -94
  58. meerk40t/core/elements/notes.py +50 -38
  59. meerk40t/core/elements/offset_clpr.py +934 -912
  60. meerk40t/core/elements/offset_mk.py +963 -955
  61. meerk40t/core/elements/penbox.py +339 -267
  62. meerk40t/core/elements/placements.py +300 -83
  63. meerk40t/core/elements/render.py +785 -687
  64. meerk40t/core/elements/shapes.py +2618 -2092
  65. meerk40t/core/elements/testcases.py +105 -0
  66. meerk40t/core/elements/trace.py +651 -563
  67. meerk40t/core/elements/tree_commands.py +415 -409
  68. meerk40t/core/elements/undo_redo.py +116 -58
  69. meerk40t/core/elements/wordlist.py +319 -200
  70. meerk40t/core/exceptions.py +9 -9
  71. meerk40t/core/laserjob.py +220 -220
  72. meerk40t/core/logging.py +63 -63
  73. meerk40t/core/node/blobnode.py +83 -86
  74. meerk40t/core/node/bootstrap.py +105 -103
  75. meerk40t/core/node/branch_elems.py +40 -31
  76. meerk40t/core/node/branch_ops.py +45 -38
  77. meerk40t/core/node/branch_regmark.py +48 -41
  78. meerk40t/core/node/cutnode.py +29 -32
  79. meerk40t/core/node/effect_hatch.py +375 -257
  80. meerk40t/core/node/effect_warp.py +398 -0
  81. meerk40t/core/node/effect_wobble.py +441 -309
  82. meerk40t/core/node/elem_ellipse.py +404 -309
  83. meerk40t/core/node/elem_image.py +1082 -801
  84. meerk40t/core/node/elem_line.py +358 -292
  85. meerk40t/core/node/elem_path.py +259 -201
  86. meerk40t/core/node/elem_point.py +129 -102
  87. meerk40t/core/node/elem_polyline.py +310 -246
  88. meerk40t/core/node/elem_rect.py +376 -286
  89. meerk40t/core/node/elem_text.py +445 -418
  90. meerk40t/core/node/filenode.py +59 -40
  91. meerk40t/core/node/groupnode.py +138 -74
  92. meerk40t/core/node/image_processed.py +777 -766
  93. meerk40t/core/node/image_raster.py +156 -113
  94. meerk40t/core/node/layernode.py +31 -31
  95. meerk40t/core/node/mixins.py +135 -107
  96. meerk40t/core/node/node.py +1427 -1304
  97. meerk40t/core/node/nutils.py +117 -114
  98. meerk40t/core/node/op_cut.py +463 -335
  99. meerk40t/core/node/op_dots.py +296 -251
  100. meerk40t/core/node/op_engrave.py +414 -311
  101. meerk40t/core/node/op_image.py +755 -369
  102. meerk40t/core/node/op_raster.py +787 -522
  103. meerk40t/core/node/place_current.py +37 -40
  104. meerk40t/core/node/place_point.py +329 -126
  105. meerk40t/core/node/refnode.py +58 -47
  106. meerk40t/core/node/rootnode.py +225 -219
  107. meerk40t/core/node/util_console.py +48 -48
  108. meerk40t/core/node/util_goto.py +84 -65
  109. meerk40t/core/node/util_home.py +61 -61
  110. meerk40t/core/node/util_input.py +102 -102
  111. meerk40t/core/node/util_output.py +102 -102
  112. meerk40t/core/node/util_wait.py +65 -65
  113. meerk40t/core/parameters.py +709 -707
  114. meerk40t/core/planner.py +875 -785
  115. meerk40t/core/plotplanner.py +656 -652
  116. meerk40t/core/space.py +120 -113
  117. meerk40t/core/spoolers.py +706 -705
  118. meerk40t/core/svg_io.py +1836 -1549
  119. meerk40t/core/treeop.py +534 -445
  120. meerk40t/core/undos.py +278 -124
  121. meerk40t/core/units.py +784 -680
  122. meerk40t/core/view.py +393 -322
  123. meerk40t/core/webhelp.py +62 -62
  124. meerk40t/core/wordlist.py +513 -504
  125. meerk40t/cylinder/cylinder.py +247 -0
  126. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  127. meerk40t/cylinder/gui/gui.py +24 -0
  128. meerk40t/device/__init__.py +1 -1
  129. meerk40t/device/basedevice.py +322 -123
  130. meerk40t/device/devicechoices.py +50 -0
  131. meerk40t/device/dummydevice.py +163 -128
  132. meerk40t/device/gui/defaultactions.py +618 -602
  133. meerk40t/device/gui/effectspanel.py +114 -0
  134. meerk40t/device/gui/formatterpanel.py +253 -290
  135. meerk40t/device/gui/warningpanel.py +337 -260
  136. meerk40t/device/mixins.py +13 -13
  137. meerk40t/dxf/__init__.py +1 -1
  138. meerk40t/dxf/dxf_io.py +766 -554
  139. meerk40t/dxf/plugin.py +47 -35
  140. meerk40t/external_plugins.py +79 -79
  141. meerk40t/external_plugins_build.py +28 -28
  142. meerk40t/extra/cag.py +112 -116
  143. meerk40t/extra/coolant.py +403 -0
  144. meerk40t/extra/encode_detect.py +204 -0
  145. meerk40t/extra/ezd.py +1165 -1165
  146. meerk40t/extra/hershey.py +834 -340
  147. meerk40t/extra/imageactions.py +322 -316
  148. meerk40t/extra/inkscape.py +628 -622
  149. meerk40t/extra/lbrn.py +424 -424
  150. meerk40t/extra/outerworld.py +283 -0
  151. meerk40t/extra/param_functions.py +1542 -1556
  152. meerk40t/extra/potrace.py +257 -253
  153. meerk40t/extra/serial_exchange.py +118 -0
  154. meerk40t/extra/updater.py +602 -453
  155. meerk40t/extra/vectrace.py +147 -146
  156. meerk40t/extra/winsleep.py +83 -83
  157. meerk40t/extra/xcs_reader.py +597 -0
  158. meerk40t/fill/fills.py +781 -335
  159. meerk40t/fill/patternfill.py +1061 -1061
  160. meerk40t/fill/patterns.py +614 -567
  161. meerk40t/grbl/control.py +87 -87
  162. meerk40t/grbl/controller.py +990 -903
  163. meerk40t/grbl/device.py +1084 -768
  164. meerk40t/grbl/driver.py +989 -771
  165. meerk40t/grbl/emulator.py +532 -497
  166. meerk40t/grbl/gcodejob.py +783 -767
  167. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  168. meerk40t/grbl/gui/grblcontroller.py +485 -271
  169. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  170. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  171. meerk40t/grbl/gui/gui.py +147 -116
  172. meerk40t/grbl/interpreter.py +44 -44
  173. meerk40t/grbl/loader.py +22 -22
  174. meerk40t/grbl/mock_connection.py +56 -56
  175. meerk40t/grbl/plugin.py +294 -264
  176. meerk40t/grbl/serial_connection.py +93 -88
  177. meerk40t/grbl/tcp_connection.py +81 -79
  178. meerk40t/grbl/ws_connection.py +112 -0
  179. meerk40t/gui/__init__.py +1 -1
  180. meerk40t/gui/about.py +2042 -296
  181. meerk40t/gui/alignment.py +1644 -1608
  182. meerk40t/gui/autoexec.py +199 -0
  183. meerk40t/gui/basicops.py +791 -670
  184. meerk40t/gui/bufferview.py +77 -71
  185. meerk40t/gui/busy.py +232 -133
  186. meerk40t/gui/choicepropertypanel.py +1662 -1469
  187. meerk40t/gui/consolepanel.py +706 -542
  188. meerk40t/gui/devicepanel.py +687 -581
  189. meerk40t/gui/dialogoptions.py +110 -107
  190. meerk40t/gui/executejob.py +316 -306
  191. meerk40t/gui/fonts.py +90 -90
  192. meerk40t/gui/functionwrapper.py +252 -0
  193. meerk40t/gui/gui_mixins.py +729 -0
  194. meerk40t/gui/guicolors.py +205 -182
  195. meerk40t/gui/help_assets/help_assets.py +218 -201
  196. meerk40t/gui/helper.py +154 -0
  197. meerk40t/gui/hersheymanager.py +1440 -846
  198. meerk40t/gui/icons.py +3422 -2747
  199. meerk40t/gui/imagesplitter.py +555 -508
  200. meerk40t/gui/keymap.py +354 -344
  201. meerk40t/gui/laserpanel.py +897 -806
  202. meerk40t/gui/laserrender.py +1470 -1232
  203. meerk40t/gui/lasertoolpanel.py +805 -793
  204. meerk40t/gui/magnetoptions.py +436 -0
  205. meerk40t/gui/materialmanager.py +2944 -0
  206. meerk40t/gui/materialtest.py +1722 -1694
  207. meerk40t/gui/mkdebug.py +646 -359
  208. meerk40t/gui/mwindow.py +163 -140
  209. meerk40t/gui/navigationpanels.py +2605 -2467
  210. meerk40t/gui/notes.py +143 -142
  211. meerk40t/gui/opassignment.py +414 -410
  212. meerk40t/gui/operation_info.py +310 -299
  213. meerk40t/gui/plugin.py +500 -328
  214. meerk40t/gui/position.py +714 -669
  215. meerk40t/gui/preferences.py +901 -650
  216. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  217. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  218. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  219. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  220. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  221. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  222. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  223. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  224. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  225. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  226. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  227. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  228. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  229. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  230. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  231. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  232. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  233. meerk40t/gui/propertypanels/textproperty.py +770 -755
  234. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  235. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  236. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  237. meerk40t/gui/ribbon.py +2471 -2210
  238. meerk40t/gui/scene/scene.py +1100 -1051
  239. meerk40t/gui/scene/sceneconst.py +22 -22
  240. meerk40t/gui/scene/scenepanel.py +439 -349
  241. meerk40t/gui/scene/scenespacewidget.py +365 -365
  242. meerk40t/gui/scene/widget.py +518 -505
  243. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  244. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  245. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  246. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  247. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  248. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  249. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  250. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  251. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  252. meerk40t/gui/scenewidgets/rectselectwidget.py +592 -346
  253. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  254. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  255. meerk40t/gui/scenewidgets/selectionwidget.py +2958 -2756
  256. meerk40t/gui/simpleui.py +362 -333
  257. meerk40t/gui/simulation.py +2451 -2094
  258. meerk40t/gui/snapoptions.py +208 -203
  259. meerk40t/gui/spoolerpanel.py +1227 -1180
  260. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  261. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  262. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  263. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  264. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  265. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  266. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  267. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  268. meerk40t/gui/themes.py +200 -78
  269. meerk40t/gui/tips.py +590 -0
  270. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  271. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  272. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  273. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  274. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  275. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  276. meerk40t/gui/toolwidgets/toolline.py +39 -144
  277. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  278. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  279. meerk40t/gui/toolwidgets/toolmeasure.py +163 -216
  280. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  281. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  282. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  283. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  284. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  285. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  286. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  287. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  288. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  289. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  290. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  291. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  292. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  293. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  294. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  295. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  296. meerk40t/gui/usbconnect.py +98 -91
  297. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  298. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  299. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  300. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  301. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  302. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  303. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  304. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  305. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  306. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  307. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  308. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  309. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  310. meerk40t/gui/wordlisteditor.py +985 -931
  311. meerk40t/gui/wxmeerk40t.py +1447 -1169
  312. meerk40t/gui/wxmmain.py +5644 -4112
  313. meerk40t/gui/wxmribbon.py +1591 -1076
  314. meerk40t/gui/wxmscene.py +1631 -1453
  315. meerk40t/gui/wxmtree.py +2416 -2089
  316. meerk40t/gui/wxutils.py +1769 -1099
  317. meerk40t/gui/zmatrix.py +102 -102
  318. meerk40t/image/__init__.py +1 -1
  319. meerk40t/image/dither.py +429 -0
  320. meerk40t/image/imagetools.py +2793 -2269
  321. meerk40t/internal_plugins.py +150 -130
  322. meerk40t/kernel/__init__.py +63 -12
  323. meerk40t/kernel/channel.py +259 -212
  324. meerk40t/kernel/context.py +538 -538
  325. meerk40t/kernel/exceptions.py +41 -41
  326. meerk40t/kernel/functions.py +463 -414
  327. meerk40t/kernel/jobs.py +100 -100
  328. meerk40t/kernel/kernel.py +3828 -3571
  329. meerk40t/kernel/lifecycles.py +71 -71
  330. meerk40t/kernel/module.py +49 -49
  331. meerk40t/kernel/service.py +147 -147
  332. meerk40t/kernel/settings.py +383 -343
  333. meerk40t/lihuiyu/controller.py +883 -876
  334. meerk40t/lihuiyu/device.py +1181 -1069
  335. meerk40t/lihuiyu/driver.py +1466 -1372
  336. meerk40t/lihuiyu/gui/gui.py +127 -106
  337. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  338. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  339. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  340. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  341. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  342. meerk40t/lihuiyu/interpreter.py +53 -53
  343. meerk40t/lihuiyu/laserspeed.py +450 -450
  344. meerk40t/lihuiyu/loader.py +90 -90
  345. meerk40t/lihuiyu/parser.py +404 -404
  346. meerk40t/lihuiyu/plugin.py +101 -102
  347. meerk40t/lihuiyu/tcp_connection.py +111 -109
  348. meerk40t/main.py +231 -165
  349. meerk40t/moshi/builder.py +788 -781
  350. meerk40t/moshi/controller.py +505 -499
  351. meerk40t/moshi/device.py +495 -442
  352. meerk40t/moshi/driver.py +862 -696
  353. meerk40t/moshi/gui/gui.py +78 -76
  354. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  355. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  356. meerk40t/moshi/plugin.py +43 -43
  357. meerk40t/network/console_server.py +140 -57
  358. meerk40t/network/kernelserver.py +10 -9
  359. meerk40t/network/tcp_server.py +142 -140
  360. meerk40t/network/udp_server.py +103 -77
  361. meerk40t/network/web_server.py +404 -0
  362. meerk40t/newly/controller.py +1158 -1144
  363. meerk40t/newly/device.py +874 -732
  364. meerk40t/newly/driver.py +540 -412
  365. meerk40t/newly/gui/gui.py +219 -188
  366. meerk40t/newly/gui/newlyconfig.py +116 -101
  367. meerk40t/newly/gui/newlycontroller.py +193 -186
  368. meerk40t/newly/gui/operationproperties.py +51 -51
  369. meerk40t/newly/mock_connection.py +82 -82
  370. meerk40t/newly/newly_params.py +56 -56
  371. meerk40t/newly/plugin.py +1214 -1246
  372. meerk40t/newly/usb_connection.py +322 -322
  373. meerk40t/rotary/gui/gui.py +52 -46
  374. meerk40t/rotary/gui/rotarysettings.py +240 -232
  375. meerk40t/rotary/rotary.py +202 -98
  376. meerk40t/ruida/control.py +291 -91
  377. meerk40t/ruida/controller.py +138 -1088
  378. meerk40t/ruida/device.py +676 -231
  379. meerk40t/ruida/driver.py +534 -472
  380. meerk40t/ruida/emulator.py +1494 -1491
  381. meerk40t/ruida/exceptions.py +4 -4
  382. meerk40t/ruida/gui/gui.py +71 -76
  383. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  384. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  385. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  386. meerk40t/ruida/loader.py +54 -52
  387. meerk40t/ruida/mock_connection.py +57 -109
  388. meerk40t/ruida/plugin.py +124 -87
  389. meerk40t/ruida/rdjob.py +2084 -945
  390. meerk40t/ruida/serial_connection.py +116 -0
  391. meerk40t/ruida/tcp_connection.py +146 -0
  392. meerk40t/ruida/udp_connection.py +73 -0
  393. meerk40t/svgelements.py +9671 -9669
  394. meerk40t/tools/driver_to_path.py +584 -579
  395. meerk40t/tools/geomstr.py +5583 -4680
  396. meerk40t/tools/jhfparser.py +357 -292
  397. meerk40t/tools/kerftest.py +904 -890
  398. meerk40t/tools/livinghinges.py +1168 -1033
  399. meerk40t/tools/pathtools.py +987 -949
  400. meerk40t/tools/pmatrix.py +234 -0
  401. meerk40t/tools/pointfinder.py +942 -942
  402. meerk40t/tools/polybool.py +941 -940
  403. meerk40t/tools/rasterplotter.py +1660 -547
  404. meerk40t/tools/shxparser.py +1047 -901
  405. meerk40t/tools/ttfparser.py +726 -446
  406. meerk40t/tools/zinglplotter.py +595 -593
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +21 -21
  408. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +150 -139
  409. meerk40t-0.9.7020.dist-info/RECORD +446 -0
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +1 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -1
  412. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +1 -1
  413. meerk40t/balormk/elementlightjob.py +0 -159
  414. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  415. test/bootstrap.py +0 -63
  416. test/test_cli.py +0 -12
  417. test/test_core_cutcode.py +0 -418
  418. test/test_core_elements.py +0 -144
  419. test/test_core_plotplanner.py +0 -397
  420. test/test_core_viewports.py +0 -312
  421. test/test_drivers_grbl.py +0 -108
  422. test/test_drivers_lihuiyu.py +0 -443
  423. test/test_drivers_newly.py +0 -113
  424. test/test_element_degenerate_points.py +0 -43
  425. test/test_elements_classify.py +0 -97
  426. test/test_elements_penbox.py +0 -22
  427. test/test_file_svg.py +0 -176
  428. test/test_fill.py +0 -155
  429. test/test_geomstr.py +0 -1523
  430. test/test_geomstr_nodes.py +0 -18
  431. test/test_imagetools_actualize.py +0 -306
  432. test/test_imagetools_wizard.py +0 -258
  433. test/test_kernel.py +0 -200
  434. test/test_laser_speeds.py +0 -3303
  435. test/test_length.py +0 -57
  436. test/test_lifecycle.py +0 -66
  437. test/test_operations.py +0 -251
  438. test/test_operations_hatch.py +0 -57
  439. test/test_ruida.py +0 -19
  440. test/test_spooler.py +0 -22
  441. test/test_tools_rasterplotter.py +0 -29
  442. test/test_wobble.py +0 -133
  443. test/test_zingl.py +0 -124
  444. {test → meerk40t/cylinder}/__init__.py +0 -0
  445. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  446. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/entry_points.txt +0 -0
@@ -1,2092 +1,2618 @@
1
- """
2
- This is a giant list of console commands that deal with and often implement the elements system in the program.
3
- """
4
-
5
- from math import sqrt
6
-
7
- from meerk40t.core.node.node import Fillrule, Linecap, Linejoin, Node
8
- from meerk40t.core.units import UNITS_PER_MM, UNITS_PER_PIXEL, UNITS_PER_POINT, Length
9
- from meerk40t.kernel import CommandSyntaxError
10
- from meerk40t.svgelements import (
11
- SVG_RULE_EVENODD,
12
- SVG_RULE_NONZERO,
13
- Angle,
14
- Color,
15
- Matrix,
16
- Path,
17
- Polygon,
18
- Polyline,
19
- )
20
- from meerk40t.tools.geomstr import Geomstr
21
-
22
-
23
- def plugin(kernel, lifecycle=None):
24
- _ = kernel.translation
25
- if lifecycle == "postboot":
26
- init_commands(kernel)
27
-
28
-
29
- def init_commands(kernel):
30
- self = kernel.elements
31
-
32
- _ = kernel.translation
33
-
34
- classify_new = self.post_classify
35
-
36
- # ==========
37
- # ELEMENT/SHAPE COMMANDS
38
- # ==========
39
- @self.console_argument("x_pos", type=Length)
40
- @self.console_argument("y_pos", type=Length)
41
- @self.console_argument("r_pos", type=Length)
42
- @self.console_command(
43
- "circle",
44
- help=_("circle <x> <y> <r>"),
45
- input_type=("elements", None),
46
- output_type="elements",
47
- all_arguments_required=True,
48
- )
49
- def element_circle(channel, _, x_pos, y_pos, r_pos, data=None, post=None, **kwargs):
50
- node = self.elem_branch.add(
51
- cx=float(x_pos),
52
- cy=float(y_pos),
53
- rx=float(r_pos),
54
- ry=float(r_pos),
55
- stroke=self.default_stroke,
56
- stroke_width=self.default_strokewidth,
57
- fill=self.default_fill,
58
- type="elem ellipse",
59
- )
60
- self.set_emphasis([node])
61
- node.focus()
62
- if data is None:
63
- data = list()
64
- data.append(node)
65
- # Newly created! Classification needed?
66
- post.append(classify_new(data))
67
- return "elements", data
68
-
69
- @self.console_argument("r_pos", type=Length)
70
- @self.console_command(
71
- "circle_r",
72
- help=_("circle_r <r>"),
73
- input_type=("elements", None),
74
- output_type="elements",
75
- all_arguments_required=True,
76
- )
77
- def element_circle_r(channel, _, r_pos, data=None, post=None, **kwargs):
78
- node = self.elem_branch.add(
79
- cx=0,
80
- cy=0,
81
- rx=float(r_pos),
82
- ry=float(r_pos),
83
- stroke=self.default_stroke,
84
- stroke_width=self.default_strokewidth,
85
- fill=self.default_fill,
86
- type="elem ellipse",
87
- )
88
- node.altered()
89
- self.set_emphasis([node])
90
- node.focus()
91
- if data is None:
92
- data = list()
93
- data.append(node)
94
- # Newly created! Classification needed?
95
- post.append(classify_new(data))
96
- return "elements", data
97
-
98
- @self.console_argument("x_pos", type=Length)
99
- @self.console_argument("y_pos", type=Length)
100
- @self.console_argument("rx_pos", type=Length)
101
- @self.console_argument("ry_pos", type=Length)
102
- @self.console_command(
103
- "ellipse",
104
- help=_("ellipse <cx> <cy> <rx> <ry>"),
105
- input_type=("elements", None),
106
- output_type="elements",
107
- all_arguments_required=True,
108
- )
109
- def element_ellipse(
110
- channel, _, x_pos, y_pos, rx_pos, ry_pos, data=None, post=None, **kwargs
111
- ):
112
- node = self.elem_branch.add(
113
- cx=float(x_pos),
114
- cy=float(y_pos),
115
- rx=float(rx_pos),
116
- ry=float(ry_pos),
117
- stroke=self.default_stroke,
118
- stroke_width=self.default_strokewidth,
119
- fill=self.default_fill,
120
- type="elem ellipse",
121
- )
122
- node.altered()
123
- self.set_emphasis([node])
124
- node.focus()
125
- if data is None:
126
- data = list()
127
- data.append(node)
128
- # Newly created! Classification needed?
129
- post.append(classify_new(data))
130
- return "elements", data
131
-
132
- @self.console_argument(
133
- "x_pos",
134
- type=self.length_x,
135
- help=_("x position for top left corner of rectangle."),
136
- )
137
- @self.console_argument(
138
- "y_pos",
139
- type=self.length_y,
140
- help=_("y position for top left corner of rectangle."),
141
- )
142
- @self.console_argument(
143
- "width", type=self.length_x, help=_("width of the rectangle.")
144
- )
145
- @self.console_argument(
146
- "height", type=self.length_y, help=_("height of the rectangle.")
147
- )
148
- @self.console_option(
149
- "rx", "x", type=self.length_x, help=_("rounded rx corner value.")
150
- )
151
- @self.console_option(
152
- "ry", "y", type=self.length_y, help=_("rounded ry corner value.")
153
- )
154
- @self.console_command(
155
- "rect",
156
- help=_("adds rectangle to scene"),
157
- input_type=("elements", None),
158
- output_type="elements",
159
- all_arguments_required=True,
160
- )
161
- def element_rect(
162
- channel,
163
- _,
164
- x_pos,
165
- y_pos,
166
- width,
167
- height,
168
- rx=None,
169
- ry=None,
170
- data=None,
171
- post=None,
172
- **kwargs,
173
- ):
174
- """
175
- Draws a svg rectangle with optional rounded corners.
176
- """
177
- node = self.elem_branch.add(
178
- x=x_pos,
179
- y=y_pos,
180
- width=width,
181
- height=height,
182
- rx=rx,
183
- ry=ry,
184
- stroke=self.default_stroke,
185
- stroke_width=self.default_strokewidth,
186
- fill=self.default_fill,
187
- type="elem rect",
188
- )
189
- self.set_emphasis([node])
190
- node.focus()
191
- if data is None:
192
- data = list()
193
- data.append(node)
194
- # Newly created! Classification needed?
195
- post.append(classify_new(data))
196
- return "elements", data
197
-
198
- @self.console_argument("x0", type=self.length_x, help=_("start x position"))
199
- @self.console_argument("y0", type=self.length_y, help=_("start y position"))
200
- @self.console_argument("x1", type=self.length_x, help=_("end x position"))
201
- @self.console_argument("y1", type=self.length_y, help=_("end y position"))
202
- @self.console_command(
203
- "line",
204
- help=_("adds line to scene"),
205
- input_type=("elements", None),
206
- output_type="elements",
207
- all_arguments_required=True,
208
- )
209
- def element_line(command, x0, y0, x1, y1, data=None, post=None, **kwargs):
210
- """
211
- Draws a svg line in the scene.
212
- """
213
- node = self.elem_branch.add(
214
- x1=x0,
215
- y1=y0,
216
- x2=x1,
217
- y2=y1,
218
- stroke=self.default_stroke,
219
- stroke_width=self.default_strokewidth,
220
- type="elem line",
221
- )
222
- node.altered()
223
- self.set_emphasis([node])
224
- node.focus()
225
- if data is None:
226
- data = list()
227
- data.append(node)
228
- # Newly created! Classification needed?
229
- post.append(classify_new(data))
230
- return "elements", data
231
-
232
- @self.console_option("distance", "d", type=Length, default="1mm")
233
- @self.console_option("angle", "a", type=Angle.parse, default="0deg")
234
- @self.console_option("angle_delta", "a", type=Angle.parse, default="0deg")
235
- @self.console_command(
236
- "effect-hatch",
237
- help=_("adds hatch-effect to scene"),
238
- input_type=(None, "elements"),
239
- )
240
- def effect_hatch(
241
- command,
242
- data=None,
243
- angle=None,
244
- angle_delta=None,
245
- distance=None,
246
- post=None,
247
- **kwargs,
248
- ):
249
- """
250
- Add an effect hatch object
251
- """
252
- if data is None:
253
- data = list(self.elems(emphasized=True))
254
- if len(data) == 0:
255
- return
256
- first_node = data[0]
257
-
258
- node = first_node.parent.add(
259
- type="effect hatch",
260
- label="Hatch Effect",
261
- hatch_angle=angle.as_radians,
262
- hatch_angle_delta=angle_delta.as_radians,
263
- hatch_distance=distance,
264
- )
265
- for n in data:
266
- node.append_child(n)
267
-
268
- # Newly created! Classification needed?
269
- post.append(classify_new([node]))
270
-
271
- self.set_emphasis([node])
272
- node.focus()
273
-
274
- @self.console_option(
275
- "size", "s", type=float, default=16, help=_("font size to for object")
276
- )
277
- @self.console_argument("text", type=str, help=_("quoted string of text"))
278
- @self.console_command(
279
- "text",
280
- help=_("text <text>"),
281
- input_type=(None, "elements"),
282
- output_type="elements",
283
- )
284
- def element_text(
285
- command, channel, _, data=None, text=None, size=None, post=None, **kwargs
286
- ):
287
- if text is None:
288
- channel(_("No text specified"))
289
- return
290
- node = self.elem_branch.add(
291
- text=text, matrix=Matrix(f"scale({UNITS_PER_PIXEL})"), type="elem text"
292
- )
293
- node.font_size = size
294
- node.stroke = self.default_stroke
295
- node.stroke_width = self.default_strokewidth
296
- node.fill = self.default_fill
297
- node.altered()
298
- self.set_emphasis([node])
299
- node.focus()
300
- if data is None:
301
- data = list()
302
- data.append(node)
303
- # Newly created! Classification needed?
304
- post.append(classify_new(data))
305
- return "elements", data
306
-
307
- @self.console_argument(
308
- "anchor", type=str, default="start", help=_("set text anchor")
309
- )
310
- @self.console_command(
311
- "text-anchor",
312
- help=_("set text object text-anchor; start, middle, end"),
313
- input_type=(
314
- None,
315
- "elements",
316
- ),
317
- output_type="elements",
318
- )
319
- def element_text_anchor(command, channel, _, data, anchor=None, **kwargs):
320
- if anchor not in ("start", "middle", "end"):
321
- raise CommandSyntaxError(
322
- _("Only 'start', 'middle', and 'end' are valid anchors.")
323
- )
324
- if data is None:
325
- data = list(self.elems(emphasized=True))
326
- if len(data) == 0:
327
- channel(_("No selected elements."))
328
- return
329
- for e in data:
330
- if hasattr(e, "can_modify") and not e.can_modify:
331
- channel(_("Can't modify a locked element: {name}").format(name=str(e)))
332
- continue
333
- if e.type == "elem text":
334
- old_anchor = e.anchor
335
- e.anchor = anchor
336
- channel(f"Node {e} anchor changed from {old_anchor} to {anchor}")
337
-
338
- e.altered()
339
- return "elements", data
340
-
341
- @self.console_argument("new_text", type=str, help=_("set new text contents"))
342
- @self.console_command(
343
- "text-edit",
344
- help=_("set text object text to new text"),
345
- input_type=(
346
- None,
347
- "elements",
348
- ),
349
- output_type="elements",
350
- all_arguments_required=True,
351
- )
352
- def element_text_edit(command, channel, _, data, new_text=None, **kwargs):
353
- if data is None:
354
- data = list(self.elems(emphasized=True))
355
- if len(data) == 0:
356
- channel(_("No selected elements."))
357
- return
358
- for e in data:
359
- if hasattr(e, "can_modify") and not e.can_modify:
360
- channel(_("Can't modify a locked element: {name}").format(name=str(e)))
361
- continue
362
- if e.type == "elem text":
363
- old_text = e.text
364
- e.text = new_text
365
- elif hasattr(e, "mktext"):
366
- old_text = e.mktext
367
- e.mktext = new_text
368
- for property_op in self.kernel.lookup_all("path_updater/.*"):
369
- property_op(self.kernel.root, e)
370
- else:
371
- continue
372
- channel(f"Node {e} anchor changed from {old_text} to {new_text}")
373
- e.altered()
374
-
375
- return "elements", data
376
-
377
- def calculate_text_bounds(data):
378
- # A render operation will use the LaserRender class
379
- # and will re-calculate the element bounds
380
- make_raster = self.lookup("render-op/make_raster")
381
- if not make_raster:
382
- # No renderer is registered to perform render.
383
- return
384
- for e in data:
385
- e.set_dirty_bounds()
386
- # arbitrary bounds...
387
- bounds = (0, 0, float(Length("5cm")), float(Length("5cm")))
388
- image = make_raster(
389
- data,
390
- bounds=bounds,
391
- width=500,
392
- height=500,
393
- )
394
-
395
- @self.console_argument("prop", type=str, help=_("property to set"))
396
- @self.console_argument("new_value", type=str, help=_("new property value"))
397
- @self.console_command(
398
- "property-set",
399
- help=_("set property to new value"),
400
- input_type=(
401
- None,
402
- "elements",
403
- ),
404
- output_type="elements",
405
- all_arguments_required=True,
406
- )
407
- def element_property_set(
408
- command, channel, _, data, post=None, prop=None, new_value=None, **kwargs
409
- ):
410
- """
411
- Generic node manipulation routine, use with care
412
- """
413
- if data is None:
414
- data = list(self.elems(emphasized=True))
415
- if len(data) == 0:
416
- channel(_("No selected elements."))
417
- return
418
- if prop is None:
419
- channel(_("You need to provide the property to set."))
420
- return
421
- classify_required = False
422
- prop = prop.lower()
423
- if len(new_value) == 0:
424
- new_value = None
425
- if prop in ("fill", "stroke") and self.classify_on_color:
426
- classify_required = True
427
- # Let's distinguish a couple of special cases...
428
- prevalidated = False
429
- if prop in ("fill", "stroke", "color"):
430
- if new_value is not None:
431
- if new_value.lower() == "none":
432
- # The text...
433
- new_value = None
434
- try:
435
- new_value = Color(new_value)
436
- prevalidated = True
437
- except ValueError:
438
- channel(_("Invalid color value: {value}").format(value=new_value))
439
- return
440
- elif prop in ("x", "y", "width", "height", "stroke_width"):
441
- if new_value is None:
442
- channel(_("Invalid length: {value}").format(value=new_value))
443
- return
444
- else:
445
- try:
446
- new_value = float(Length(new_value))
447
- prevalidated = True
448
- except ValueError:
449
- channel(_("Invalid length: {value}").format(value=new_value))
450
- return
451
-
452
- changed = []
453
- text_elems = []
454
-
455
- if prop == "lock":
456
- if new_value.lower() in ("1", "true"):
457
- setval = True
458
- elif new_value.lower() in ("0", "false"):
459
- setval = False
460
- else:
461
- try:
462
- setval = bool(new_value)
463
- except ValueError:
464
- channel(
465
- _("Can't set '{val}' for {field}.").format(
466
- val=new_value, field=prop
467
- )
468
- )
469
- return
470
- # print (f"Will set lock to {setval} ({new_value})")
471
- for e in data:
472
- if hasattr(e, "lock"):
473
- e.lock = setval
474
- changed.append(e)
475
- else:
476
- for e in data:
477
- if prop in ("x", "y"):
478
- if not e.can_move(self.lock_allows_move):
479
- channel(
480
- _("Element can not be moved: {name}").format(name=str(e))
481
- )
482
- continue
483
- # We need to adjust the matrix
484
- if hasattr(e, "matrix"):
485
- dx = 0
486
- dy = 0
487
- otx = e.matrix.value_trans_x()
488
- oty = e.matrix.value_trans_y()
489
- if prop == "x":
490
- dx = new_value - otx
491
- else:
492
- dy = new_value - oty
493
- e.matrix.post_translate(dx, dy)
494
- else:
495
- channel(
496
- _("Element has no matrix to modify: {name}").format(
497
- name=str(e)
498
- )
499
- )
500
- continue
501
- elif prop in ("width", "height"):
502
- if new_value == 0:
503
- channel(_("Can't set {field} to zero").format(field=prop))
504
- continue
505
- if hasattr(e, "can_scale") and not e.can_scale:
506
- channel(
507
- _("Element can not be scaled: {name}").format(name=str(e))
508
- )
509
- continue
510
- if hasattr(e, "matrix") and hasattr(e, "bounds"):
511
- bb = e.bounds
512
- sx = 1.0
513
- sy = 1.0
514
- wd = bb[2] - bb[0]
515
- ht = bb[3] - bb[1]
516
- if prop == "width":
517
- sx = new_value / wd
518
- else:
519
- sy = new_value / ht
520
- e.matrix.post_scale(sx, sy)
521
- else:
522
- channel(
523
- _("Element has no matrix to modify: {name}").format(
524
- name=str(e)
525
- )
526
- )
527
- continue
528
- elif hasattr(e, prop):
529
- if hasattr(e, "can_modify") and not e.can_modify:
530
- channel(
531
- _("Can't modify a locked element: {name}").format(
532
- name=str(e)
533
- )
534
- )
535
- continue
536
- try:
537
- oldval = getattr(e, prop)
538
- if prevalidated:
539
- setval = new_value
540
- else:
541
- if oldval is not None:
542
- proptype = type(oldval)
543
- setval = proptype(new_value)
544
- if isinstance(oldval, bool):
545
- if new_value.lower() in ("1", "true"):
546
- setval = True
547
- elif new_value.lower() in ("0", "false"):
548
- setval = False
549
- else:
550
- setval = new_value
551
- setattr(e, prop, setval)
552
- except TypeError:
553
- channel(
554
- _(
555
- "Can't set '{val}' for {field} (invalid type, old={oldval})."
556
- ).format(val=new_value, field=prop, oldval=oldval)
557
- )
558
- except ValueError:
559
- channel(
560
- _(
561
- "Can't set '{val}' for {field} (invalid value, old={oldval})."
562
- ).format(val=new_value, field=prop, oldval=oldval)
563
- )
564
-
565
- if "font" in prop:
566
- # We need to force a recalculation of the underlying wxfont property
567
- if hasattr(e, "wxfont"):
568
- delattr(e, "wxfont")
569
- text_elems.append(e)
570
- if prop in ("mktext", "mkfont"):
571
- for property_op in self.kernel.lookup_all("path_updater/.*"):
572
- property_op(self.kernel.root, e)
573
- else:
574
- channel(
575
- _("Element {name} has no property {field}").format(
576
- name=str(e), field=prop
577
- )
578
- )
579
- continue
580
- e.altered()
581
- changed.append(e)
582
- if len(changed) > 0:
583
- if len(text_elems) > 0:
584
- # Recalculate bounds
585
- calculate_text_bounds(text_elems)
586
- self.signal("refresh_scene", "Scene")
587
- self.signal("element_property_update", changed)
588
- self.validate_selected_area()
589
- if classify_required:
590
- post.append(classify_new(changed))
591
-
592
- return "elements", data
593
-
594
- @self.console_command(
595
- "recalc", input_type=("elements", None), output_type="elements"
596
- )
597
- def recalc(command, channel, _, data=None, post=None, **kwargs):
598
- if data is None:
599
- data = list(self.elems(emphasized=True))
600
- if len(data) == 0:
601
- return
602
- for e in data:
603
- e.set_dirty_bounds()
604
- self.signal("refresh_scene", "Scene")
605
- self.validate_selected_area()
606
-
607
- @self.console_command(
608
- "simplify", input_type=("elements", None), output_type="elements"
609
- )
610
- def simplify_path(command, channel, _, data=None, post=None, **kwargs):
611
- if data is None:
612
- data = list(self.elems(emphasized=True))
613
- data_changed = list()
614
- if len(data) == 0:
615
- channel("Requires a selected polygon")
616
- return None
617
- for node in data:
618
- try:
619
- sub_before = len(list(node.as_geometry().as_subpaths()))
620
- except AttributeError:
621
- sub_before = 0
622
-
623
- changed, before, after = self.simplify_node(node)
624
- if changed:
625
- node.altered()
626
- try:
627
- sub_after = len(list(node.as_geometry().as_subpaths()))
628
- except AttributeError:
629
- sub_after = 0
630
- channel(
631
- f"Simplified {node.type} ({node.label}): from {before} to {after}"
632
- )
633
- channel(f"Subpaths before: {sub_before} to {sub_after}")
634
- data_changed.append(node)
635
- else:
636
- channel(f"Could not simplify {node.type} ({node.label})")
637
- if len(data_changed) > 0:
638
- self.signal("element_property_update", data_changed)
639
- self.signal("refresh_scene", "Scene")
640
- return "elements", data
641
-
642
- @self.console_command(
643
- "polycut", input_type=("elements", None), output_type="elements"
644
- )
645
- def create_pattern(command, channel, _, data=None, post=None, **kwargs):
646
- if data is None:
647
- data = list(self.elems(emphasized=True))
648
- if len(data) <= 1:
649
- channel("Requires a selected cutter polygon")
650
- return None
651
- data.sort(key=lambda n: n.emphasized_time)
652
- try:
653
- outer_path = data[0].as_path()
654
- inner_path = data[1].as_path()
655
- except AttributeError:
656
- # elem text does not have an as_path() object
657
- return "elements", data
658
- data[1].remove_node()
659
-
660
- from meerk40t.tools.pathtools import VectorMontonizer
661
-
662
- vm = VectorMontonizer()
663
- outer_path = Polygon(
664
- [outer_path.point(i / 1000.0, error=1e4) for i in range(1001)]
665
- )
666
- vm.add_polyline(outer_path)
667
- path = Path()
668
- for sub_inner in inner_path.as_subpaths():
669
- sub_inner = Path(sub_inner)
670
- pts_sub = [sub_inner.point(i / 1000.0, error=1e4) for i in range(1001)]
671
- for i in range(len(pts_sub) - 1, -1, -1):
672
- pt = pts_sub[i]
673
- if not vm.is_point_inside(pt[0], pt[1]):
674
- del pts_sub[i]
675
- path += Path(Polyline(pts_sub))
676
- node = self.elem_branch.add(path=path, type="elem path")
677
- data.append(node)
678
- node.stroke = self.default_stroke
679
- node.stroke_width = self.default_strokewidth
680
- node.fill = self.default_fill
681
- node.altered()
682
- node.focus()
683
- post.append(classify_new(data))
684
- return "elements", data
685
-
686
- @self.console_argument("mlist", type=Length, help=_("list of positions"), nargs="*")
687
- @self.console_command(
688
- ("polygon", "polyline"),
689
- help=_("poly(gon|line) (Length Length)*"),
690
- input_type=("elements", None),
691
- output_type="elements",
692
- all_arguments_required=True,
693
- )
694
- def element_poly(command, channel, _, mlist, data=None, post=None, **kwargs):
695
- try:
696
- pts = [float(Length(p)) for p in mlist]
697
- if command == "polygon":
698
- shape = Polygon(pts)
699
- else:
700
- shape = Polyline(pts)
701
- except ValueError:
702
- raise CommandSyntaxError(
703
- _("Must be a list of spaced delimited length pairs.")
704
- )
705
- if shape.is_degenerate():
706
- channel(_("Shape is degenerate."))
707
- return "elements", data
708
- node = self.elem_branch.add(shape=shape, type="elem polyline")
709
- node.stroke = self.default_stroke
710
- node.stroke_width = self.default_strokewidth
711
- node.fill = self.default_fill
712
- node.altered()
713
- self.set_emphasis([node])
714
- node.focus()
715
- if data is None:
716
- data = list()
717
- data.append(node)
718
- # Newly created! Classification needed?
719
- post.append(classify_new(data))
720
- return "elements", data
721
-
722
- @self.console_option(
723
- "real",
724
- "r",
725
- action="store_true",
726
- type=bool,
727
- help="Display non-transformed path",
728
- )
729
- @self.console_command(
730
- "path_d_info",
731
- help=_("List the path_d of any recognized paths"),
732
- input_type="elements",
733
- )
734
- def element_pathd_info(command, channel, _, data, real=True, **kwargs):
735
- for node in data:
736
- try:
737
- g = node.as_geometry()
738
- path = g.as_path()
739
- ident = " (Identity)" if node.matrix.is_identity() else ""
740
- channel(f"{str(node)}{ident}: {path.d(transformed=not real)}")
741
- except AttributeError:
742
- channel(f"{str(node)}: Invalid")
743
-
744
- @self.console_argument(
745
- "path_d", type=str, help=_("svg path syntax command (quoted).")
746
- )
747
- @self.console_command(
748
- "path",
749
- help=_("path <svg path>"),
750
- output_type="elements",
751
- )
752
- def element_path(path_d, data, post=None, **kwargs):
753
- if path_d is None:
754
- raise CommandSyntaxError(_("Not a valid path_d string"))
755
- try:
756
- path = Path(path_d)
757
- path *= f"Scale({UNITS_PER_PIXEL})"
758
- except ValueError:
759
- raise CommandSyntaxError(_("Not a valid path_d string (try quotes)"))
760
-
761
- node = self.elem_branch.add(path=path, type="elem path")
762
- node.stroke = self.default_stroke
763
- node.stroke_width = self.default_strokewidth
764
- node.fill = self.default_fill
765
- node.altered()
766
- self.set_emphasis([node])
767
- node.focus()
768
- if data is None:
769
- data = list()
770
- data.append(node)
771
- # Newly created! Classification needed?
772
- post.append(classify_new(data))
773
- return "elements", data
774
-
775
- @self.console_argument(
776
- "stroke_width",
777
- type=self.length,
778
- help=_("Stroke-width for the given stroke"),
779
- )
780
- @self.console_command(
781
- "stroke-width",
782
- help=_("stroke-width <length>"),
783
- input_type=(
784
- None,
785
- "elements",
786
- ),
787
- output_type="elements",
788
- )
789
- def element_stroke_width(command, channel, _, stroke_width, data=None, **kwargs):
790
- def width_string(value):
791
- if value is None:
792
- return "-"
793
- res = ""
794
- display_units = (
795
- (1, ""),
796
- (UNITS_PER_PIXEL, "px"),
797
- (UNITS_PER_POINT, "pt"),
798
- (UNITS_PER_MM, "mm"),
799
- )
800
- for unit in display_units:
801
- unit_value = value / unit[0]
802
- if res != "":
803
- res += ", "
804
- res += f"{unit_value:.3f}{unit[1]}"
805
- return res
806
-
807
- if data is None:
808
- data = list(self.elems(emphasized=True))
809
- if stroke_width is None:
810
- # Display data about stroke widths.
811
- channel("----------")
812
- channel(_("Stroke-Width Values:"))
813
- for i, e in enumerate(self.elems()):
814
- name = str(e)
815
- if len(name) > 50:
816
- name = name[:50] + "…"
817
- try:
818
- stroke_width = e.stroke_width
819
- except AttributeError:
820
- # Has no stroke width.
821
- continue
822
- if not hasattr(e, "stroke_scaled"):
823
- # Can't have a scaled stroke.
824
- channel(
825
- _(
826
- "{index}: {name} - {typename}\n stroke-width = {stroke_width}\n scaled-width = {scaled_stroke_width}"
827
- ).format(
828
- index=i,
829
- typename="scaled-stroke",
830
- stroke_width=width_string(stroke_width),
831
- scaled_stroke_width=width_string(None),
832
- name=name,
833
- )
834
- )
835
- continue
836
- factor = 1.0
837
- if e.stroke_scaled:
838
- typename = "scaled-stroke"
839
- try:
840
- factor = e.stroke_factor
841
- except AttributeError:
842
- pass
843
- else:
844
- typename = "non-scaling-stroke"
845
- implied_value = factor * stroke_width
846
- channel(
847
- _(
848
- "{index}: {name} - {typename}\n stroke-width = {stroke_width}\n scaled-width = {scaled_stroke_width}"
849
- ).format(
850
- index=i,
851
- typename=typename,
852
- stroke_width=width_string(stroke_width),
853
- scaled_stroke_width=width_string(implied_value),
854
- name=name,
855
- )
856
- )
857
- channel("----------")
858
- return
859
-
860
- if len(data) == 0:
861
- channel(_("No selected elements."))
862
- return
863
- for e in data:
864
- if hasattr(e, "lock") and e.lock:
865
- channel(_("Can't modify a locked element: {name}").format(name=str(e)))
866
- continue
867
- e.stroke_width = stroke_width
868
- try:
869
- e.stroke_width_zero()
870
- except AttributeError:
871
- pass
872
- # No full modified required, we are effectively only adjusting
873
- # the painted_bounds
874
- e.translated(0, 0)
875
- self.signal("element_property_update", data)
876
- self.signal("refresh_scene", "Scene")
877
- return "elements", data
878
-
879
- @self.console_command(
880
- ("enable_stroke_scale", "disable_stroke_scale"),
881
- help=_("stroke-width <length>"),
882
- input_type=(
883
- None,
884
- "elements",
885
- ),
886
- hidden=True,
887
- output_type="elements",
888
- )
889
- def element_stroke_scale_enable(command, channel, _, data=None, **kwargs):
890
- if data is None:
891
- data = list(self.elems(emphasized=True))
892
- if len(data) == 0:
893
- channel(_("No selected elements."))
894
- return
895
- for e in data:
896
- if hasattr(e, "lock") and e.lock:
897
- channel(_("Can't modify a locked element: {name}").format(name=str(e)))
898
- continue
899
- e.stroke_scaled = command == "enable_stroke_scale"
900
- e.altered()
901
- self.signal("element_property_update", data)
902
- self.signal("refresh_scene", "Scene")
903
- return "elements", data
904
-
905
- @self.console_option("filter", "f", type=str, help="Filter indexes")
906
- @self.console_argument(
907
- "cap",
908
- type=str,
909
- help=_("Linecap to apply to the path (one of butt, round, square)"),
910
- )
911
- @self.console_command(
912
- "linecap",
913
- help=_("linecap <cap>"),
914
- input_type=(
915
- None,
916
- "elements",
917
- ),
918
- output_type="elements",
919
- )
920
- def element_cap(command, channel, _, cap=None, data=None, filter=None, **kwargs):
921
- if data is None:
922
- data = list(self.elems(emphasized=True))
923
- apply = data
924
- if filter is not None:
925
- apply = list()
926
- for value in filter.split(","):
927
- try:
928
- value = int(value)
929
- except ValueError:
930
- continue
931
- try:
932
- apply.append(data[value])
933
- except IndexError:
934
- channel(_("index {index} out of range").format(index=value))
935
- if cap is None:
936
- channel("----------")
937
- channel(_("Linecaps:"))
938
- i = 0
939
- for e in self.elems():
940
- name = str(e)
941
- if len(name) > 50:
942
- name = name[:50] + "…"
943
- if hasattr(e, "linecap"):
944
- if e.linecap == Linecap.CAP_SQUARE:
945
- capname = "square"
946
- elif e.linecap == Linecap.CAP_BUTT:
947
- capname = "butt"
948
- else:
949
- capname = "round"
950
- channel(
951
- _("{index}: linecap = {linecap} - {name}").format(
952
- index=i, linecap=capname, name=name
953
- )
954
- )
955
- i += 1
956
- channel("----------")
957
- return
958
- else:
959
- capvalue = None
960
- if cap.lower() == "butt":
961
- capvalue = Linecap.CAP_BUTT
962
- elif cap.lower() == "round":
963
- capvalue = Linecap.CAP_ROUND
964
- elif cap.lower() == "square":
965
- capvalue = Linecap.CAP_SQUARE
966
- if capvalue is not None:
967
- for e in apply:
968
- if hasattr(e, "linecap"):
969
- if hasattr(e, "lock") and e.lock:
970
- channel(
971
- _("Can't modify a locked element: {name}").format(
972
- name=str(e)
973
- )
974
- )
975
- continue
976
- e.linecap = capvalue
977
- e.altered()
978
- return "elements", data
979
-
980
- @self.console_option("filter", "f", type=str, help="Filter indexes")
981
- @self.console_argument(
982
- "join",
983
- type=str,
984
- help=_(
985
- "jointype to apply to the path (one of arcs, bevel, miter, miter-clip, round)"
986
- ),
987
- )
988
- @self.console_command(
989
- "linejoin",
990
- help=_("linejoin <join>"),
991
- input_type=(
992
- None,
993
- "elements",
994
- ),
995
- output_type="elements",
996
- )
997
- def element_join(command, channel, _, join=None, data=None, filter=None, **kwargs):
998
- if data is None:
999
- data = list(self.elems(emphasized=True))
1000
- apply = data
1001
- if filter is not None:
1002
- apply = list()
1003
- for value in filter.split(","):
1004
- try:
1005
- value = int(value)
1006
- except ValueError:
1007
- continue
1008
- try:
1009
- apply.append(data[value])
1010
- except IndexError:
1011
- channel(_("index {index} out of range").format(index=value))
1012
- if join is None:
1013
- channel("----------")
1014
- channel(_("Linejoins:"))
1015
- i = 0
1016
- for e in self.elems():
1017
- name = str(e)
1018
- if len(name) > 50:
1019
- name = name[:50] + "…"
1020
- if hasattr(e, "linejoin"):
1021
- if e.linejoin == Linejoin.JOIN_ARCS:
1022
- joinname = "arcs"
1023
- elif e.linejoin == Linejoin.JOIN_BEVEL:
1024
- joinname = "bevel"
1025
- elif e.linejoin == Linejoin.JOIN_MITER_CLIP:
1026
- joinname = "miter-clip"
1027
- elif e.linejoin == Linejoin.JOIN_MITER:
1028
- joinname = "miter"
1029
- elif e.linejoin == Linejoin.JOIN_ROUND:
1030
- joinname = "round"
1031
- channel(
1032
- _("{index}: linejoin = {linejoin} - {name}").format(
1033
- index=i, linejoin=joinname, name=name
1034
- )
1035
- )
1036
- i += 1
1037
- channel("----------")
1038
- return
1039
- else:
1040
- joinvalue = None
1041
- if join.lower() == "arcs":
1042
- joinvalue = Linejoin.JOIN_ARCS
1043
- elif join.lower() == "bevel":
1044
- joinvalue = Linejoin.JOIN_BEVEL
1045
- elif join.lower() == "miter":
1046
- joinvalue = Linejoin.JOIN_MITER
1047
- elif join.lower() == "miter-clip":
1048
- joinvalue = Linejoin.JOIN_MITER_CLIP
1049
- elif join.lower() == "round":
1050
- joinvalue = Linejoin.JOIN_ROUND
1051
- if joinvalue is not None:
1052
- for e in apply:
1053
- if hasattr(e, "linejoin"):
1054
- if hasattr(e, "lock") and e.lock:
1055
- channel(
1056
- _("Can't modify a locked element: {name}").format(
1057
- name=str(e)
1058
- )
1059
- )
1060
- continue
1061
- e.linejoin = joinvalue
1062
- e.altered()
1063
- return "elements", data
1064
-
1065
- @self.console_option("filter", "f", type=str, help="Filter indexes")
1066
- @self.console_argument(
1067
- "rule",
1068
- type=str,
1069
- help=_("rule to apply to fill the path (one of {nonzero}, {evenodd})").format(
1070
- nonzero=SVG_RULE_NONZERO, evenodd=SVG_RULE_EVENODD
1071
- ),
1072
- )
1073
- @self.console_command(
1074
- "fillrule",
1075
- help=_("fillrule <rule>"),
1076
- input_type=(
1077
- None,
1078
- "elements",
1079
- ),
1080
- output_type="elements",
1081
- )
1082
- def element_rule(command, channel, _, rule=None, data=None, filter=None, **kwargs):
1083
- if data is None:
1084
- data = list(self.elems(emphasized=True))
1085
- apply = data
1086
- if filter is not None:
1087
- apply = list()
1088
- for value in filter.split(","):
1089
- try:
1090
- value = int(value)
1091
- except ValueError:
1092
- continue
1093
- try:
1094
- apply.append(data[value])
1095
- except IndexError:
1096
- channel(_("index {index} out of range").format(index=value))
1097
- if rule is None:
1098
- channel("----------")
1099
- channel(_("fillrules:"))
1100
- i = 0
1101
- for e in self.elems():
1102
- name = str(e)
1103
- if len(name) > 50:
1104
- name = name[:50] + ""
1105
- if hasattr(e, "fillrule"):
1106
- if e.fillrule == Fillrule.FILLRULE_EVENODD:
1107
- rulename = SVG_RULE_EVENODD
1108
- elif e.fillrule == Fillrule.FILLRULE_NONZERO:
1109
- rulename = SVG_RULE_NONZERO
1110
- channel(
1111
- _("{index}: fillrule = {fillrule} - {name}").format(
1112
- index=i, fillrule=rulename, name=name
1113
- )
1114
- )
1115
- i += 1
1116
- channel("----------")
1117
- return
1118
- else:
1119
- rulevalue = None
1120
- if rule.lower() == SVG_RULE_EVENODD:
1121
- rulevalue = Fillrule.FILLRULE_EVENODD
1122
- elif rule.lower() == SVG_RULE_NONZERO:
1123
- rulevalue = Fillrule.FILLRULE_NONZERO
1124
- if rulevalue is not None:
1125
- for e in apply:
1126
- if hasattr(e, "fillrule"):
1127
- if hasattr(e, "lock") and e.lock:
1128
- channel(
1129
- _("Can't modify a locked element: {name}").format(
1130
- name=str(e)
1131
- )
1132
- )
1133
- continue
1134
- e.fillrule = rulevalue
1135
- e.altered()
1136
- return "elements", data
1137
-
1138
- @self.console_option(
1139
- "classify", "c", type=bool, action="store_true", help="Reclassify element"
1140
- )
1141
- @self.console_option("filter", "f", type=str, help="Filter indexes")
1142
- @self.console_argument(
1143
- "color", type=Color, help=_("Color to color the given stroke")
1144
- )
1145
- @self.console_command(
1146
- "stroke",
1147
- help=_("stroke <svg color>"),
1148
- input_type=(
1149
- None,
1150
- "elements",
1151
- ),
1152
- output_type="elements",
1153
- )
1154
- def element_stroke(
1155
- command, channel, _, color, data=None, classify=None, filter=None, **kwargs
1156
- ):
1157
- if data is None:
1158
- data = list(self.elems(emphasized=True))
1159
- was_emphasized = True
1160
- old_first = self.first_emphasized
1161
- else:
1162
- was_emphasized = False
1163
- old_first = None
1164
- apply = data
1165
- if filter is not None:
1166
- apply = list()
1167
- for value in filter.split(","):
1168
- try:
1169
- value = int(value)
1170
- except ValueError:
1171
- continue
1172
- try:
1173
- apply.append(data[value])
1174
- except IndexError:
1175
- channel(_("index {index} out of range").format(index=value))
1176
- if color is None:
1177
- channel("----------")
1178
- channel(_("Stroke Values:"))
1179
- i = 0
1180
- for e in self.elems():
1181
- name = str(e)
1182
- if len(name) > 50:
1183
- name = name[:50] + ""
1184
- if not hasattr(e, "stroke"):
1185
- pass
1186
- elif hasattr(e, "stroke") and e.stroke is None or e.stroke == "none":
1187
- channel(f"{i}: stroke = none - {name}")
1188
- else:
1189
- channel(f"{i}: stroke = {e.stroke.hex} - {name}")
1190
- i += 1
1191
- channel("----------")
1192
- return
1193
- self.set_start_time("full_load")
1194
- if color == "none":
1195
- self.set_start_time("stroke")
1196
- for e in apply:
1197
- if hasattr(e, "lock") and e.lock:
1198
- channel(
1199
- _("Can't modify a locked element: {name}").format(name=str(e))
1200
- )
1201
- continue
1202
- e.stroke = None
1203
- e.translated(0, 0)
1204
- # e.altered()
1205
- self.set_end_time("stroke")
1206
- else:
1207
- self.set_start_time("stroke")
1208
- for e in apply:
1209
- if hasattr(e, "lock") and e.lock:
1210
- channel(
1211
- _("Can't modify a locked element: {name}").format(name=str(e))
1212
- )
1213
- continue
1214
- e.stroke = Color(color)
1215
- e.translated(0, 0)
1216
- # e.altered()
1217
- self.set_end_time("stroke")
1218
- if classify is None:
1219
- classify = False
1220
- if classify:
1221
- self.set_start_time("classify")
1222
- self.remove_elements_from_operations(apply)
1223
- self.classify(apply)
1224
- if was_emphasized:
1225
- for e in apply:
1226
- e.emphasized = True
1227
- if len(apply) == 1:
1228
- apply[0].focus()
1229
- if old_first is not None and old_first in apply:
1230
- self.first_emphasized = old_first
1231
- else:
1232
- self.first_emphasized = None
1233
- self.set_end_time("classify")
1234
- # self.signal("rebuild_tree")
1235
- self.signal("refresh_tree", apply)
1236
- else:
1237
- self.signal("element_property_update", apply)
1238
- self.signal("refresh_scene", "Scene")
1239
- return "elements", data
1240
-
1241
- @self.console_option(
1242
- "classify", "c", type=bool, action="store_true", help="Reclassify element"
1243
- )
1244
- @self.console_option("filter", "f", type=str, help="Filter indexes")
1245
- @self.console_argument("color", type=Color, help=_("Color to set the fill to"))
1246
- @self.console_command(
1247
- "fill",
1248
- help=_("fill <svg color>"),
1249
- input_type=(
1250
- None,
1251
- "elements",
1252
- ),
1253
- output_type="elements",
1254
- )
1255
- def element_fill(
1256
- command, channel, _, color, data=None, classify=None, filter=None, **kwargs
1257
- ):
1258
- if data is None:
1259
- data = list(self.elems(emphasized=True))
1260
- was_emphasized = True
1261
- old_first = self.first_emphasized
1262
- else:
1263
- was_emphasized = False
1264
- old_first = None
1265
- apply = data
1266
- if filter is not None:
1267
- apply = list()
1268
- for value in filter.split(","):
1269
- try:
1270
- value = int(value)
1271
- except ValueError:
1272
- continue
1273
- try:
1274
- apply.append(data[value])
1275
- except IndexError:
1276
- channel(_("index {index} out of range").format(index=value))
1277
- if color is None:
1278
- channel("----------")
1279
- channel(_("Fill Values:"))
1280
- i = 0
1281
- for e in self.elems():
1282
- name = str(e)
1283
- if len(name) > 50:
1284
- name = name[:50] + "…"
1285
- if not hasattr(e, "fill"):
1286
- pass
1287
- elif e.fill is None or e.fill == "none":
1288
- channel(
1289
- _("{index}: fill = none - {name}").format(index=i, name=name)
1290
- )
1291
- else:
1292
- channel(
1293
- _("{index}: fill = {fill} - {name}").format(
1294
- index=i, fill=e.fill.hex, name=name
1295
- )
1296
- )
1297
- i += 1
1298
- channel("----------")
1299
- return "elements", data
1300
- elif color == "none":
1301
- self.set_start_time("fill")
1302
- for e in apply:
1303
- if hasattr(e, "lock") and e.lock:
1304
- channel(
1305
- _("Can't modify a locked element: {name}").format(name=str(e))
1306
- )
1307
- continue
1308
- e.fill = None
1309
- e.translated(0, 0)
1310
- # e.altered()
1311
- self.set_end_time("fill")
1312
- else:
1313
- self.set_start_time("fill")
1314
- for e in apply:
1315
- if hasattr(e, "lock") and e.lock:
1316
- channel(
1317
- _("Can't modify a locked element: {name}").format(name=str(e))
1318
- )
1319
- continue
1320
- e.fill = Color(color)
1321
- e.translated(0, 0)
1322
- # e.altered()
1323
- self.set_end_time("fill")
1324
- if classify is None:
1325
- classify = False
1326
- if classify:
1327
- self.set_start_time("classify")
1328
- self.remove_elements_from_operations(apply)
1329
- self.classify(apply)
1330
- if was_emphasized:
1331
- for e in apply:
1332
- e.emphasized = True
1333
- if len(apply) == 1:
1334
- apply[0].focus()
1335
- if old_first is not None and old_first in apply:
1336
- self.first_emphasized = old_first
1337
- else:
1338
- self.first_emphasized = None
1339
- self.signal("refresh_tree", apply)
1340
- # self.signal("rebuild_tree")
1341
- self.set_end_time("classify")
1342
- else:
1343
- self.signal("element_property_update", apply)
1344
- self.signal("refresh_scene", "Scene")
1345
- return "elements", data
1346
-
1347
- @self.console_argument(
1348
- "x_offset", type=self.length_x, help=_("x offset."), default="0"
1349
- )
1350
- @self.console_argument(
1351
- "y_offset", type=self.length_y, help=_("y offset"), default="0"
1352
- )
1353
- @self.console_command(
1354
- "frame",
1355
- help=_("Draws a frame the current selected elements"),
1356
- input_type=(
1357
- None,
1358
- "elements",
1359
- ),
1360
- output_type="elements",
1361
- )
1362
- def element_frame(
1363
- command,
1364
- channel,
1365
- _,
1366
- x_offset=None,
1367
- y_offset=None,
1368
- data=None,
1369
- post=None,
1370
- **kwargs,
1371
- ):
1372
- """
1373
- Draws an outline of the current shape.
1374
- """
1375
- bounds = self.selected_area()
1376
- if bounds is None:
1377
- channel(_("Nothing Selected"))
1378
- return
1379
- x_pos = bounds[0]
1380
- y_pos = bounds[1]
1381
- width = bounds[2] - bounds[0]
1382
- height = bounds[3] - bounds[1]
1383
- x_pos -= x_offset
1384
- y_pos -= y_offset
1385
- width += x_offset * 2
1386
- height += y_offset * 2
1387
- node = self.elem_branch.add(
1388
- x=x_pos,
1389
- y=y_pos,
1390
- width=width,
1391
- height=height,
1392
- stroke=Color("red"),
1393
- type="elem rect",
1394
- )
1395
- self.set_emphasis([node])
1396
- node.focus()
1397
- if data is None:
1398
- data = list()
1399
- data.append(node)
1400
- # Newly created! Classification needed?
1401
- post.append(classify_new(data))
1402
- return "elements", data
1403
-
1404
- @self.console_argument("angle", type=Angle.parse, help=_("angle to rotate by"))
1405
- @self.console_option("cx", "x", type=self.length_x, help=_("center x"))
1406
- @self.console_option("cy", "y", type=self.length_y, help=_("center y"))
1407
- @self.console_option(
1408
- "absolute",
1409
- "a",
1410
- type=bool,
1411
- action="store_true",
1412
- help=_("angle_to absolute angle"),
1413
- )
1414
- @self.console_command(
1415
- "rotate",
1416
- help=_("rotate <angle>"),
1417
- input_type=(
1418
- None,
1419
- "elements",
1420
- ),
1421
- output_type="elements",
1422
- )
1423
- def element_rotate(
1424
- command,
1425
- channel,
1426
- _,
1427
- angle,
1428
- cx=None,
1429
- cy=None,
1430
- absolute=False,
1431
- data=None,
1432
- **kwargs,
1433
- ):
1434
- if angle is None:
1435
- channel("----------")
1436
- channel(_("Rotate Values:"))
1437
- i = 0
1438
- for node in self.elems():
1439
- name = str(node)
1440
- if len(name) > 50:
1441
- name = name[:50] + "…"
1442
- channel(
1443
- _("{index}: rotate({angle}turn) - {name}").format(
1444
- index=i, angle=node.matrix.rotation.as_turns, name=name
1445
- )
1446
- )
1447
- i += 1
1448
- channel("----------")
1449
- return
1450
- if data is None:
1451
- data = list(self.elems(emphasized=True))
1452
- if len(data) == 0:
1453
- channel(_("No selected elements."))
1454
- return
1455
- self.validate_selected_area()
1456
- bounds = self.selected_area()
1457
- if bounds is None:
1458
- channel(_("No selected elements."))
1459
- return
1460
-
1461
- if cx is None:
1462
- cx = (bounds[2] + bounds[0]) / 2.0
1463
- if cy is None:
1464
- cy = (bounds[3] + bounds[1]) / 2.0
1465
- images = []
1466
- try:
1467
- if not absolute:
1468
- for node in data:
1469
- if hasattr(node, "lock") and node.lock:
1470
- continue
1471
- node.matrix.post_rotate(angle, cx, cy)
1472
- node.modified()
1473
- if hasattr(node, "update"):
1474
- images.append(node)
1475
- else:
1476
- for node in data:
1477
- if hasattr(node, "lock") and node.lock:
1478
- continue
1479
- start_angle = node.matrix.rotation
1480
- node.matrix.post_rotate(angle - start_angle, cx, cy)
1481
- node.modified()
1482
- if hasattr(node, "update"):
1483
- images.append(node)
1484
- except ValueError:
1485
- raise CommandSyntaxError
1486
- for node in images:
1487
- node.update(None)
1488
- self.signal("refresh_scene", "Scene")
1489
- return "elements", data
1490
-
1491
- @self.console_argument("scale_x", type=str, help=_("scale_x value"))
1492
- @self.console_argument("scale_y", type=str, help=_("scale_y value"))
1493
- @self.console_option("px", "x", type=self.length_x, help=_("scale x origin point"))
1494
- @self.console_option("py", "y", type=self.length_y, help=_("scale y origin point"))
1495
- @self.console_option(
1496
- "absolute",
1497
- "a",
1498
- type=bool,
1499
- action="store_true",
1500
- help=_("scale to absolute size"),
1501
- )
1502
- @self.console_command(
1503
- "scale",
1504
- help=_("scale <scale> [<scale-y>]?"),
1505
- input_type=(None, "elements"),
1506
- output_type="elements",
1507
- )
1508
- def element_scale(
1509
- command,
1510
- channel,
1511
- _,
1512
- scale_x=None,
1513
- scale_y=None,
1514
- px=None,
1515
- py=None,
1516
- absolute=False,
1517
- data=None,
1518
- **kwargs,
1519
- ):
1520
- if scale_x is None:
1521
- channel("----------")
1522
- channel(_("Scale Values:"))
1523
- i = 0
1524
- for node in self.elems():
1525
- name = str(node)
1526
- if len(name) > 50:
1527
- name = name[:50] + "…"
1528
- channel(
1529
- f"{i}: scale({node.matrix.value_scale_x()}, {node.matrix.value_scale_y()}) - {name}"
1530
- )
1531
- i += 1
1532
- channel("----------")
1533
- return
1534
- if data is None:
1535
- data = list(self.elems(emphasized=True))
1536
- if len(data) == 0:
1537
- channel(_("No selected elements."))
1538
- return
1539
- # print (f"Start: {scale_x} ({type(scale_x).__name__}), {scale_y} ({type(scale_y).__name__})")
1540
- factor = 1
1541
- if scale_x.endswith("%"):
1542
- factor = 0.01
1543
- scale_x = scale_x[:-1]
1544
- try:
1545
- scale_x = factor * float(scale_x)
1546
- except ValueError:
1547
- scale_x = 1
1548
- if scale_y is None:
1549
- scale_y = scale_x
1550
- else:
1551
- factor = 1
1552
- if scale_y.endswith("%"):
1553
- factor = 0.01
1554
- scale_y = scale_y[:-1]
1555
- try:
1556
- scale_y = factor * float(scale_y)
1557
- except ValueError:
1558
- scale_y = 1
1559
- # print (f"End: {scale_x} ({type(scale_x).__name__}), {scale_y} ({type(scale_y).__name__})")
1560
-
1561
- bounds = Node.union_bounds(data)
1562
- if px is None:
1563
- px = (bounds[2] + bounds[0]) / 2.0
1564
- if py is None:
1565
- py = (bounds[3] + bounds[1]) / 2.0
1566
- if scale_x == 0 or scale_y == 0:
1567
- channel(_("Scaling by Zero Error"))
1568
- return
1569
- matrix = Matrix(f"scale({scale_x},{scale_y},{px},{py})")
1570
- images = []
1571
- try:
1572
- if not absolute:
1573
- for node in data:
1574
- if hasattr(node, "lock") and node.lock:
1575
- continue
1576
- node.matrix *= matrix
1577
- node.scaled(sx=scale_x, sy=scale_y, ox=px, oy=py)
1578
- if hasattr(node, "update"):
1579
- images.append(node)
1580
- else:
1581
- for node in data:
1582
- if hasattr(node, "lock") and node.lock:
1583
- continue
1584
- osx = node.matrix.value_scale_x()
1585
- osy = node.matrix.value_scale_y()
1586
- nsx = scale_x / osx
1587
- nsy = scale_y / osy
1588
- matrix = Matrix(f"scale({nsx},{nsy},{px},{px})")
1589
- node.matrix *= matrix
1590
- node.scaled(sx=nsx, sy=nsy, ox=px, oy=py)
1591
- if hasattr(node, "update"):
1592
- images.append(node)
1593
- except ValueError:
1594
- raise CommandSyntaxError
1595
- for node in images:
1596
- node.update(None)
1597
- self.signal("refresh_scene", "Scene")
1598
- return "elements", data
1599
-
1600
- @self.console_option(
1601
- "new_area", "n", type=self.area, help=_("provide a new area to cover")
1602
- )
1603
- @self.console_option(
1604
- "density", "d", type=int, help=_("Defines the interpolation density")
1605
- )
1606
- @self.console_command(
1607
- "area",
1608
- help=_("provides information about/changes the area of a selected element"),
1609
- input_type=(None, "elements"),
1610
- output_type="elements",
1611
- )
1612
- def element_area(
1613
- command,
1614
- channel,
1615
- _,
1616
- new_area=None,
1617
- density=None,
1618
- data=None,
1619
- **kwargs,
1620
- ):
1621
- if density is None:
1622
- density = 200
1623
- if new_area is None:
1624
- display_only = True
1625
- else:
1626
- if new_area == 0:
1627
- channel(_("You shouldn't collapse a shape to a zero-sized thing"))
1628
- return
1629
- display_only = False
1630
- if data is None:
1631
- data = list(self.elems(emphasized=True))
1632
- if len(data) == 0:
1633
- channel(_("No selected elements."))
1634
- return
1635
- total_area = 0
1636
- if display_only:
1637
- channel("----------")
1638
- channel(_("Area values (Density={density})").format(density=density))
1639
-
1640
- units = ("mm", "cm", "in")
1641
- square_unit = [0] * len(units)
1642
- for idx, u in enumerate(units):
1643
- value = float(Length(f"1{u}"))
1644
- square_unit[idx] = value * value
1645
-
1646
- i = 0
1647
- for elem in data:
1648
- try:
1649
- geometry = elem.as_geometry()
1650
- except AttributeError:
1651
- continue
1652
- # this_length = geometry.length()
1653
- this_area = geometry.area(density=density)
1654
-
1655
- if display_only:
1656
- name = str(elem)
1657
- if len(name) > 50:
1658
- name = name[:50] + "…"
1659
- channel(f"{i}: {name}")
1660
- for idx, u in enumerate(units):
1661
- this_area_local = this_area / square_unit[idx]
1662
- channel(
1663
- _(" Area= {area:.3f} {unit}²").format(
1664
- area=this_area_local, unit=u
1665
- )
1666
- )
1667
- i += 1
1668
- total_area += this_area
1669
- if display_only:
1670
- channel("----------")
1671
- else:
1672
- if total_area == 0:
1673
- channel(_("You can't reshape a zero-sized shape"))
1674
- return
1675
-
1676
- ratio = sqrt(new_area / total_area)
1677
- self(f"scale {ratio}\n")
1678
-
1679
- return "elements", data
1680
- # Do we have a new value to set? If yes scale by sqrt(of the fraction)
1681
-
1682
- @self.console_argument("tx", type=self.length_x, help=_("translate x value"))
1683
- @self.console_argument("ty", type=self.length_y, help=_("translate y value"))
1684
- @self.console_option(
1685
- "absolute",
1686
- "a",
1687
- type=bool,
1688
- action="store_true",
1689
- help=_("translate to absolute position"),
1690
- )
1691
- @self.console_command(
1692
- "translate",
1693
- help=_("translate <tx> <ty>"),
1694
- input_type=(None, "elements"),
1695
- output_type="elements",
1696
- )
1697
- def element_translate(
1698
- command, channel, _, tx, ty, absolute=False, data=None, **kwargs
1699
- ):
1700
- if tx is None:
1701
- channel("----------")
1702
- channel(_("Translate Values:"))
1703
- i = 0
1704
- for node in self.elems():
1705
- name = str(node)
1706
- if len(name) > 50:
1707
- name = name[:50] + "…"
1708
- channel(
1709
- f"{i}: translate({node.matrix.value_trans_x():.1f}, {node.matrix.value_trans_y():.1f}) - {name}"
1710
- )
1711
- i += 1
1712
- channel("----------")
1713
- return
1714
- if data is None:
1715
- data = list(self.elems(emphasized=True))
1716
- if len(data) == 0:
1717
- channel(_("No selected elements."))
1718
- return
1719
- if tx is None:
1720
- tx = 0
1721
- if ty is None:
1722
- ty = 0
1723
- changes = False
1724
- matrix = Matrix.translate(tx, ty)
1725
- try:
1726
- if not absolute:
1727
- for node in data:
1728
- if not node.can_move(self.lock_allows_move):
1729
- continue
1730
-
1731
- node.matrix *= matrix
1732
- node.translated(tx, ty)
1733
- changes = True
1734
- else:
1735
- for node in data:
1736
- if not node.can_move(self.lock_allows_move):
1737
- continue
1738
- otx = node.matrix.value_trans_x()
1739
- oty = node.matrix.value_trans_y()
1740
- ntx = tx - otx
1741
- nty = ty - oty
1742
- matrix = Matrix.translate(ntx, nty)
1743
- node.matrix *= matrix
1744
- node.translated(ntx, nty)
1745
- changes = True
1746
- except ValueError:
1747
- raise CommandSyntaxError
1748
- if changes:
1749
- self.signal("refresh_scene", "Scene")
1750
- return "elements", data
1751
-
1752
- @self.console_argument("tx", type=self.length_x, help=_("New x value"))
1753
- @self.console_argument("ty", type=self.length_y, help=_("New y value"))
1754
- @self.console_command(
1755
- "position",
1756
- help=_("position <tx> <ty>"),
1757
- input_type=(None, "elements"),
1758
- output_type="elements",
1759
- )
1760
- def element_position(
1761
- command, channel, _, tx, ty, absolute=False, data=None, **kwargs
1762
- ):
1763
- if data is None:
1764
- data = list(self.elems(emphasized=True))
1765
- if len(data) == 0:
1766
- channel(_("No selected elements."))
1767
- return
1768
- if tx is None or ty is None:
1769
- channel(_("You need to provide a new position."))
1770
- return
1771
- changes = False
1772
- dbounds = Node.union_bounds(data)
1773
- for node in data:
1774
- if not node.can_move(self.lock_allows_move):
1775
- continue
1776
- nbounds = node.bounds
1777
- dx = tx - dbounds[0]
1778
- dy = ty - dbounds[1]
1779
- if dx != 0 or dy != 0:
1780
- node.matrix.post_translate(dx, dy)
1781
- # node.modified()
1782
- node.translated(dx, dy)
1783
- changes = True
1784
- if changes:
1785
- self.signal("refresh_scene", "Scene")
1786
- return "elements", data
1787
-
1788
- @self.console_command(
1789
- "move_to_laser",
1790
- help=_("translates the selected element to the laser head"),
1791
- input_type=(None, "elements"),
1792
- output_type="elements",
1793
- )
1794
- def element_move_to_laser(command, channel, _, data=None, **kwargs):
1795
- if data is None:
1796
- data = list(self.elems(emphasized=True))
1797
- if len(data) == 0:
1798
- channel(_("No selected elements."))
1799
- return
1800
- tx, ty = self.device.current
1801
- try:
1802
- bounds = Node.union_bounds(data)
1803
- otx = bounds[0]
1804
- oty = bounds[1]
1805
- ntx = tx - otx
1806
- nty = ty - oty
1807
- for node in data:
1808
- if not node.can_move(self.lock_allows_move):
1809
- continue
1810
- node.matrix.post_translate(ntx, nty)
1811
- # node.modified()
1812
- node.translated(ntx, nty)
1813
- except ValueError:
1814
- raise CommandSyntaxError
1815
- return "elements", data
1816
-
1817
- @self.console_argument(
1818
- "x_pos", type=self.length_x, help=_("x position for top left corner")
1819
- )
1820
- @self.console_argument(
1821
- "y_pos", type=self.length_y, help=_("y position for top left corner")
1822
- )
1823
- @self.console_argument("width", type=self.length_x, help=_("new width of selected"))
1824
- @self.console_argument(
1825
- "height", type=self.length_y, help=_("new height of selected")
1826
- )
1827
- @self.console_command(
1828
- "resize",
1829
- help=_("resize <x-pos> <y-pos> <width> <height>"),
1830
- input_type=(None, "elements"),
1831
- output_type="elements",
1832
- )
1833
- def element_resize(
1834
- command, channel, _, x_pos, y_pos, width, height, data=None, **kwargs
1835
- ):
1836
- if height is None:
1837
- raise CommandSyntaxError
1838
- try:
1839
- area = self.selected_area()
1840
- if area is None:
1841
- channel(_("resize: nothing selected"))
1842
- return
1843
- x, y, x1, y1 = area
1844
- w, h = x1 - x, y1 - y
1845
- if w == 0 or h == 0: # dot
1846
- channel(_("resize: cannot resize a dot"))
1847
- return
1848
- sx = width / w
1849
- sy = height / h
1850
- # Don't do anything if scale is 1
1851
- if sx == 1.0 and sy == 1.0:
1852
- scale_str = ""
1853
- else:
1854
- scale_str = f"scale({sx},{sy})"
1855
- if x_pos == x and y_pos == y and scale_str == "":
1856
- return
1857
- # trans1_str = ""
1858
- # trans2_str = ""
1859
- # else:
1860
- trans1_str = f"translate({round(x_pos, 7)},{round(y_pos, 7)})"
1861
- trans2_str = f"translate({round(-x, 7)},{round(-y, 7)})"
1862
- matrixstr = f"{trans1_str} {scale_str} {trans2_str}".strip()
1863
- # channel(f"{matrixstr}")
1864
- matrix = Matrix(matrixstr)
1865
- if data is None:
1866
- data = list(self.elems(emphasized=True))
1867
- images = []
1868
- for node in data:
1869
- if hasattr(node, "lock") and node.lock:
1870
- channel(_("resize: cannot resize a locked element"))
1871
- continue
1872
- node.matrix *= matrix
1873
- node.modified()
1874
- if hasattr(node, "update"):
1875
- images.append(node)
1876
- for node in images:
1877
- node.update(None)
1878
- self.signal("refresh_scene", "Scene")
1879
- return "elements", data
1880
- except (ValueError, ZeroDivisionError, TypeError):
1881
- raise CommandSyntaxError
1882
-
1883
- @self.console_argument("sx", type=float, help=_("scale_x value"))
1884
- @self.console_argument("kx", type=float, help=_("skew_x value"))
1885
- @self.console_argument("ky", type=float, help=_("skew_y value"))
1886
- @self.console_argument("sy", type=float, help=_("scale_y value"))
1887
- @self.console_argument("tx", type=self.length_x, help=_("translate_x value"))
1888
- @self.console_argument("ty", type=self.length_y, help=_("translate_y value"))
1889
- @self.console_command(
1890
- "matrix",
1891
- help=_("matrix <sx> <kx> <ky> <sy> <tx> <ty>"),
1892
- input_type=(None, "elements"),
1893
- output_type="elements",
1894
- )
1895
- def element_matrix(
1896
- command, channel, _, sx, kx, ky, sy, tx, ty, data=None, **kwargs
1897
- ):
1898
- if data is None:
1899
- data = list(self.elems(emphasized=True))
1900
- if ty is None:
1901
- channel("----------")
1902
- channel(_("Matrix Values:"))
1903
- i = 0
1904
- for node in data:
1905
- name = str(node)
1906
- if len(name) > 50:
1907
- name = name[:50] + ""
1908
- channel(f"{i}: {str(node.matrix)} - {name}")
1909
- i += 1
1910
- channel("----------")
1911
- return
1912
- if len(data) == 0:
1913
- channel(_("No selected elements."))
1914
- return
1915
- images = []
1916
- try:
1917
- # SVG 7.15.3 defines the matrix form as:
1918
- # [a c e]
1919
- # [b d f]
1920
- m = Matrix(
1921
- sx,
1922
- kx,
1923
- ky,
1924
- sy,
1925
- tx,
1926
- ty,
1927
- )
1928
- for node in data:
1929
- if hasattr(node, "lock") and node.lock:
1930
- continue
1931
- node.matrix = Matrix(m)
1932
- node.modified()
1933
- if hasattr(node, "update"):
1934
- images.append(node)
1935
- except ValueError:
1936
- raise CommandSyntaxError
1937
- for node in images:
1938
- node.update(None)
1939
- self.signal("refresh_scene", "Scene")
1940
- return
1941
-
1942
- @self.console_command(
1943
- "reset",
1944
- help=_("reset affine transformations"),
1945
- input_type=(None, "elements"),
1946
- output_type="elements",
1947
- )
1948
- def reset(command, channel, _, data=None, **kwargs):
1949
- if data is None:
1950
- data = list(self.elems(emphasized=True))
1951
- images = []
1952
- for e in data:
1953
- if hasattr(e, "lock") and e.lock:
1954
- continue
1955
- name = str(e)
1956
- if len(name) > 50:
1957
- name = name[:50] + "…"
1958
- channel(_("reset - {name}").format(name=name))
1959
- e.matrix.reset()
1960
- e.modified()
1961
- if hasattr(e, "update"):
1962
- images.append(e)
1963
- for e in images:
1964
- e.update(None)
1965
- self.signal("refresh_scene", "Scene")
1966
- return "elements", data
1967
-
1968
- @self.console_command(
1969
- "reify",
1970
- help=_("reify affine transformations"),
1971
- input_type=(None, "elements"),
1972
- output_type="elements",
1973
- )
1974
- def element_reify(command, channel, _, data=None, **kwargs):
1975
- if data is None:
1976
- data = list(self.elems(emphasized=True))
1977
- for e in data:
1978
- try:
1979
- if e.lock:
1980
- continue
1981
- except AttributeError:
1982
- pass
1983
-
1984
- name = str(e)
1985
- if len(name) > 50:
1986
- name = name[:50] + "…"
1987
- try:
1988
- e.stroke_reify()
1989
- except AttributeError:
1990
- pass
1991
-
1992
- try:
1993
- e.shape.reify()
1994
- except AttributeError as err:
1995
- try:
1996
- e.path.reify()
1997
- except AttributeError:
1998
- channel(_("Couldn't reify - %s - %s") % (name, err))
1999
- return "elements", data
2000
- try:
2001
- e.stroke_width_zero()
2002
- except AttributeError:
2003
- pass
2004
- e.altered()
2005
- channel(_("reified - %s") % name)
2006
- return "elements", data
2007
-
2008
- @self.console_command(
2009
- "circle_arc_path",
2010
- help=_("Convert paths to use circular arcs."),
2011
- input_type=(None, "elements"),
2012
- output_type="elements",
2013
- )
2014
- def element_circ_arc_path(command, channel, _, data=None, **kwargs):
2015
- if data is None:
2016
- data = list(self.elems(emphasized=True))
2017
- for e in data:
2018
- try:
2019
- if e.lock:
2020
- continue
2021
- except AttributeError:
2022
- pass
2023
- if e.type == "elem path":
2024
- g = e.geometry
2025
- path = g.as_path()
2026
- path.approximate_bezier_with_circular_arcs()
2027
- e.geometry = Geomstr.svg(path)
2028
- e.altered()
2029
-
2030
- return "elements", data
2031
-
2032
- @self.console_command(
2033
- "classify",
2034
- help=_("classify elements into operations"),
2035
- input_type=(None, "elements"),
2036
- output_type="elements",
2037
- )
2038
- def element_classify(command, channel, _, data=None, **kwargs):
2039
- if data is None:
2040
- data = list(self.elems(emphasized=True))
2041
- was_emphasized = True
2042
- old_first = self.first_emphasized
2043
- else:
2044
- was_emphasized = False
2045
- old_first = None
2046
- if len(data) == 0:
2047
- channel(_("No selected elements."))
2048
- return
2049
- self.classify(data)
2050
- if was_emphasized:
2051
- for e in data:
2052
- e.emphasized = True
2053
- if len(data) == 1:
2054
- data[0].focus()
2055
- if old_first is not None and old_first in data:
2056
- self.first_emphasized = old_first
2057
- else:
2058
- self.first_emphasized = None
2059
-
2060
- return "elements", data
2061
-
2062
- @self.console_command(
2063
- "declassify",
2064
- help=_("declassify selected elements"),
2065
- input_type=(None, "elements"),
2066
- output_type="elements",
2067
- )
2068
- def declassify(command, channel, _, data=None, **kwargs):
2069
- if data is None:
2070
- data = list(self.elems(emphasized=True))
2071
- was_emphasized = True
2072
- old_first = self.first_emphasized
2073
- else:
2074
- was_emphasized = False
2075
- old_first = None
2076
- if len(data) == 0:
2077
- channel(_("No selected elements."))
2078
- return
2079
- self.remove_elements_from_operations(data)
2080
- # restore emphasized flag as it is relevant for subsequent operations
2081
- if was_emphasized:
2082
- for e in data:
2083
- e.emphasized = True
2084
- if len(data) == 1:
2085
- data[0].focus()
2086
- if old_first is not None and old_first in data:
2087
- self.first_emphasized = old_first
2088
- else:
2089
- self.first_emphasized = None
2090
- return "elements", data
2091
-
2092
- # --------------------------- END COMMANDS ------------------------------
1
+ """
2
+ This module contains a collection of console commands that manage and implement the elements system within the application.
3
+ It provides functionalities for creating, modifying, and classifying various geometric shapes and elements.
4
+
5
+ Functions:
6
+ - plugin: Initializes the console commands for the elements system.
7
+ - init_commands: Sets up the console commands related to shapes and elements.
8
+ - element_circle: Creates a circle element at specified coordinates with a given radius.
9
+ - element_circle_r: Creates a circle element at the origin with a specified radius.
10
+ - element_ellipse: Creates an ellipse element with specified center and radii.
11
+ - element_rect: Draws a rectangle with optional rounded corners.
12
+ - element_line: Draws a line between two specified points.
13
+ - effect_remove: Removes effects from selected elements.
14
+ - effect_hatch: Adds a hatch effect to selected elements.
15
+ - effect_wobble: Adds a wobble effect to selected elements.
16
+ - element_text: Creates a text element with specified content and font size.
17
+ - element_text_anchor: Sets the text anchor for a text element.
18
+ - element_text_edit: Edits the text content of a text element.
19
+ - element_property_set: Sets a specified property to a new value for selected elements.
20
+ - recalc: Recalculates the bounds of selected elements.
21
+ - simplify_path: Simplifies the geometry of selected paths.
22
+ - create_pattern: Creates a pattern from selected elements.
23
+ - element_poly: Creates a polygon or polyline from specified points.
24
+ - element_pathd_info: Lists the path data of recognized paths.
25
+ - element_path: Creates a path element from SVG path syntax.
26
+ - element_stroke_width: Adjusts the stroke width of selected elements.
27
+ - element_cap: Sets the line cap style for selected paths.
28
+ - element_join: Sets the line join style for selected paths.
29
+ - element_rule: Sets the fill rule for selected paths.
30
+ - element_stroke: Sets the stroke color for selected elements.
31
+ - element_fill: Sets the fill color for selected elements.
32
+ - element_frame: Draws a frame around the currently selected elements.
33
+ - element_rotate: Rotates selected elements by a specified angle.
34
+ - element_scale: Scales selected elements by specified factors.
35
+ - element_area: Provides information about or changes the area of selected elements.
36
+ - element_translate: Translates selected elements by specified offsets.
37
+ - element_position: Sets the position of selected elements to specified coordinates.
38
+ - element_move_to_laser: Moves selected elements to the current position of the laser head.
39
+ - element_resize: Resizes selected elements to specified dimensions.
40
+ - element_matrix: Sets the transformation matrix for selected elements.
41
+ - reset: Resets affine transformations for selected elements.
42
+ - element_reify: Reifies affine transformations for selected elements.
43
+ - element_circ_arc_path: Converts paths to use circular arcs.
44
+ - element_classify: Classifies selected elements into operations.
45
+ - declassify: Declassifies selected elements.
46
+
47
+ """
48
+
49
+ from math import sqrt
50
+
51
+ from meerk40t.core.node.node import Fillrule, Linecap, Linejoin, Node
52
+ from meerk40t.core.units import (
53
+ UNITS_PER_MM,
54
+ UNITS_PER_PIXEL,
55
+ UNITS_PER_POINT,
56
+ Angle,
57
+ Length,
58
+ )
59
+ from meerk40t.kernel import CommandSyntaxError
60
+ from meerk40t.svgelements import (
61
+ SVG_RULE_EVENODD,
62
+ SVG_RULE_NONZERO,
63
+ Color,
64
+ Matrix,
65
+ Path,
66
+ Polygon,
67
+ Polyline,
68
+ )
69
+ from meerk40t.tools.geomstr import Geomstr
70
+
71
+
72
+ def plugin(kernel, lifecycle=None):
73
+ _ = kernel.translation
74
+ if lifecycle == "postboot":
75
+ init_commands(kernel)
76
+
77
+
78
+ def init_commands(kernel):
79
+ self = kernel.elements
80
+
81
+ _ = kernel.translation
82
+
83
+ classify_new = self.post_classify
84
+
85
+ # ==========
86
+ # ELEMENT/SHAPE COMMANDS
87
+ # ==========
88
+ @self.console_argument("x_pos", type=Length)
89
+ @self.console_argument("y_pos", type=Length)
90
+ @self.console_argument("r_pos", type=Length)
91
+ @self.console_command(
92
+ "circle",
93
+ help=_("circle <x> <y> <r>"),
94
+ input_type=("elements", None),
95
+ output_type="elements",
96
+ all_arguments_required=True,
97
+ )
98
+ def element_circle(channel, _, x_pos, y_pos, r_pos, data=None, post=None, **kwargs):
99
+ node = self.elem_branch.add(
100
+ cx=float(x_pos),
101
+ cy=float(y_pos),
102
+ rx=float(r_pos),
103
+ ry=float(r_pos),
104
+ stroke=self.default_stroke,
105
+ stroke_width=self.default_strokewidth,
106
+ fill=self.default_fill,
107
+ type="elem ellipse",
108
+ )
109
+ self.set_emphasis([node])
110
+ node.focus()
111
+ if data is None:
112
+ data = list()
113
+ data.append(node)
114
+ # Newly created! Classification needed?
115
+ post.append(classify_new(data))
116
+ return "elements", data
117
+
118
+ @self.console_argument("r_pos", type=Length)
119
+ @self.console_command(
120
+ "circle_r",
121
+ help=_("circle_r <r>"),
122
+ input_type=("elements", None),
123
+ output_type="elements",
124
+ all_arguments_required=True,
125
+ )
126
+ def element_circle_r(channel, _, r_pos, data=None, post=None, **kwargs):
127
+ node = self.elem_branch.add(
128
+ cx=0,
129
+ cy=0,
130
+ rx=float(r_pos),
131
+ ry=float(r_pos),
132
+ stroke=self.default_stroke,
133
+ stroke_width=self.default_strokewidth,
134
+ fill=self.default_fill,
135
+ type="elem ellipse",
136
+ )
137
+ node.altered()
138
+ self.set_emphasis([node])
139
+ node.focus()
140
+ if data is None:
141
+ data = list()
142
+ data.append(node)
143
+ # Newly created! Classification needed?
144
+ post.append(classify_new(data))
145
+ return "elements", data
146
+
147
+ @self.console_argument("x_pos", type=Length)
148
+ @self.console_argument("y_pos", type=Length)
149
+ @self.console_argument("rx", type=Length)
150
+ @self.console_argument("ry", type=Length)
151
+ @self.console_command(
152
+ "ellipse",
153
+ help=_("ellipse <cx> <cy> <rx> <ry>"),
154
+ input_type=("elements", None),
155
+ output_type="elements",
156
+ all_arguments_required=True,
157
+ )
158
+ def element_ellipse(
159
+ channel, _, x_pos, y_pos, rx, ry, data=None, post=None, **kwargs
160
+ ):
161
+ node = self.elem_branch.add(
162
+ cx=float(x_pos),
163
+ cy=float(y_pos),
164
+ rx=float(rx),
165
+ ry=float(ry),
166
+ stroke=self.default_stroke,
167
+ stroke_width=self.default_strokewidth,
168
+ fill=self.default_fill,
169
+ type="elem ellipse",
170
+ )
171
+ node.altered()
172
+ self.set_emphasis([node])
173
+ node.focus()
174
+ if data is None:
175
+ data = list()
176
+ data.append(node)
177
+ # Newly created! Classification needed?
178
+ post.append(classify_new(data))
179
+ return "elements", data
180
+
181
+ @self.console_argument("x_pos", type=Length, help=_("X-coordinate of center"))
182
+ @self.console_argument("y_pos", type=Length, help=_("Y-coordinate of center"))
183
+ @self.console_argument("rx", type=Length, help=_("Primary radius of ellipse"))
184
+ @self.console_argument("ry", type=Length, help=_("Secondary radius of ellipse (default equal to primary radius=circle)"))
185
+ @self.console_argument("start_angle", type=Angle, help=_("Start angle of arc (default 0°)"))
186
+ @self.console_argument("end_angle", type=Angle, help=_("End angle of arc (default 360°)"))
187
+ @self.console_option(
188
+ "rotation", "r", type=Angle, help=_("Rotation of arc")
189
+ )
190
+ @self.console_command(
191
+ "arc",
192
+ help=_("arc <cx> <cy> <rx> <ry> <start> <end>"),
193
+ input_type=("elements", None),
194
+ output_type="elements",
195
+ all_arguments_required=True,
196
+ )
197
+ def element_arc(
198
+ channel, _, x_pos, y_pos, rx, ry=None, start_angle=None, end_angle=None, rotation=None, data=None, post=None, **kwargs
199
+ ):
200
+ if start_angle is None:
201
+ start_angle = Angle("0deg")
202
+ if end_angle is None:
203
+ end_angle = Angle("360deg")
204
+ if rotation is None:
205
+ rotation = Angle("0deg")
206
+ if ry is None:
207
+ ry = rx
208
+ rx_val = float(rx)
209
+ ry_val = float(ry)
210
+ cx = float(x_pos)
211
+ cy = float(y_pos)
212
+ geom = Geomstr()
213
+ geom.arc_as_cubics(
214
+ start_t=start_angle.radians,
215
+ end_t=end_angle.radians,
216
+ rx=rx_val,
217
+ ry=ry_val,
218
+ cx=cx,
219
+ cy=cy,
220
+ rotation=rotation.radians,
221
+ )
222
+ node = self.elem_branch.add(
223
+ label="Arc",
224
+ geometry=geom,
225
+ stroke=self.default_stroke,
226
+ stroke_width=self.default_strokewidth,
227
+ fill=self.default_fill,
228
+ type="elem path",
229
+ )
230
+ node.altered()
231
+ self.set_emphasis([node])
232
+ node.focus()
233
+ if data is None:
234
+ data = list()
235
+ data.append(node)
236
+
237
+ # Newly created! Classification needed?
238
+ post.append(classify_new(data))
239
+ return "elements", data
240
+
241
+ @self.console_argument(
242
+ "x_pos",
243
+ type=self.length_x,
244
+ help=_("x position for top left corner of rectangle."),
245
+ )
246
+ @self.console_argument(
247
+ "y_pos",
248
+ type=self.length_y,
249
+ help=_("y position for top left corner of rectangle."),
250
+ )
251
+ @self.console_argument(
252
+ "width", type=self.length_x, help=_("width of the rectangle.")
253
+ )
254
+ @self.console_argument(
255
+ "height", type=self.length_y, help=_("height of the rectangle.")
256
+ )
257
+ @self.console_option(
258
+ "rx", "x", type=self.length_x, help=_("rounded rx corner value.")
259
+ )
260
+ @self.console_option(
261
+ "ry", "y", type=self.length_y, help=_("rounded ry corner value.")
262
+ )
263
+ @self.console_command(
264
+ "rect",
265
+ help=_("adds rectangle to scene"),
266
+ input_type=("elements", None),
267
+ output_type="elements",
268
+ all_arguments_required=True,
269
+ )
270
+ def element_rect(
271
+ channel,
272
+ _,
273
+ x_pos,
274
+ y_pos,
275
+ width,
276
+ height,
277
+ rx=None,
278
+ ry=None,
279
+ data=None,
280
+ post=None,
281
+ **kwargs,
282
+ ):
283
+ """
284
+ Draws a svg rectangle with optional rounded corners.
285
+ """
286
+ node = self.elem_branch.add(
287
+ x=x_pos,
288
+ y=y_pos,
289
+ width=width,
290
+ height=height,
291
+ rx=rx,
292
+ ry=ry,
293
+ stroke=self.default_stroke,
294
+ stroke_width=self.default_strokewidth,
295
+ fill=self.default_fill,
296
+ type="elem rect",
297
+ )
298
+ self.set_emphasis([node])
299
+ node.focus()
300
+ if data is None:
301
+ data = list()
302
+ data.append(node)
303
+ # Newly created! Classification needed?
304
+ post.append(classify_new(data))
305
+ return "elements", data
306
+
307
+ @self.console_argument("x0", type=self.length_x, help=_("start x position"))
308
+ @self.console_argument("y0", type=self.length_y, help=_("start y position"))
309
+ @self.console_argument("x1", type=self.length_x, help=_("end x position"))
310
+ @self.console_argument("y1", type=self.length_y, help=_("end y position"))
311
+ @self.console_command(
312
+ "line",
313
+ help=_("adds line to scene"),
314
+ input_type=("elements", None),
315
+ output_type="elements",
316
+ all_arguments_required=True,
317
+ )
318
+ def element_line(command, x0, y0, x1, y1, data=None, post=None, **kwargs):
319
+ """
320
+ Draws a svg line in the scene.
321
+ """
322
+ node = self.elem_branch.add(
323
+ x1=x0,
324
+ y1=y0,
325
+ x2=x1,
326
+ y2=y1,
327
+ stroke=self.default_stroke,
328
+ stroke_width=self.default_strokewidth,
329
+ type="elem line",
330
+ )
331
+ node.altered()
332
+ self.set_emphasis([node])
333
+ node.focus()
334
+ if data is None:
335
+ data = list()
336
+ data.append(node)
337
+ # Newly created! Classification needed?
338
+ post.append(classify_new(data))
339
+ return "elements", data
340
+
341
+ @self.console_command(
342
+ "effect-remove",
343
+ help=_("remove effects from element"),
344
+ input_type=(None, "elements"),
345
+ )
346
+ def effect_remove(
347
+ command,
348
+ channel,
349
+ _,
350
+ data=None,
351
+ post=None,
352
+ **kwargs,
353
+ ):
354
+ if data is None:
355
+ data = list(self.elems(emphasized=True))
356
+
357
+ if len(data) == 0:
358
+ return
359
+ for node in data:
360
+ eparent = node.parent
361
+ nparent = eparent
362
+ while True:
363
+ if nparent.type.startswith("effect"):
364
+ break
365
+ if nparent.parent is None:
366
+ nparent = None
367
+ break
368
+ if nparent.parent is self.elem_branch:
369
+ nparent = None
370
+ break
371
+ nparent = nparent.parent
372
+ if nparent is None:
373
+ continue
374
+ was_emphasized = node.emphasized
375
+ node._parent = None # Otherwise add_node will fail below
376
+ try:
377
+ idx = eparent._children.index(node)
378
+ if idx >= 0:
379
+ eparent._children.pop(idx)
380
+ except IndexError:
381
+ pass
382
+ nparent.parent.add_node(node)
383
+ if len(nparent.children) == 0:
384
+ nparent.remove_node()
385
+ else:
386
+ nparent.altered()
387
+ node.emphasized = was_emphasized
388
+ self.signal("refresh_scene", "Scene")
389
+
390
+ @self.console_option("etype", "e", type=str, default="scanline")
391
+ @self.console_option("distance", "d", type=Length, default=None)
392
+ @self.console_option("angle", "a", type=Angle, default=None)
393
+ @self.console_option("angle_delta", "b", type=Angle, default=None)
394
+ @self.console_command(
395
+ "effect-hatch",
396
+ help=_("adds hatch-effect to scene"),
397
+ input_type=(None, "elements"),
398
+ )
399
+ def effect_hatch(
400
+ command,
401
+ channel,
402
+ _,
403
+ data=None,
404
+ etype=None,
405
+ angle=None,
406
+ angle_delta=None,
407
+ distance=None,
408
+ post=None,
409
+ **kwargs,
410
+ ):
411
+ """
412
+ Add an effect hatch object
413
+ """
414
+
415
+ if data is None:
416
+ data = list(self.elems(emphasized=True))
417
+ if len(data) == 0:
418
+ channel(_("No selected elements."))
419
+ return
420
+
421
+ if distance is None and hasattr(self.device, "effect_hatch_default_distance"):
422
+ distance = getattr(self.device, "effect_hatch_default_distance")
423
+ elif distance is None:
424
+ distance = "1mm"
425
+
426
+ if angle is None and hasattr(self.device, "effect_hatch_default_angle"):
427
+ angle = Angle(getattr(self.device, "effect_hatch_default_angle"))
428
+ elif angle is None:
429
+ angle = Angle("0deg")
430
+
431
+ if angle_delta is None and hasattr(
432
+ self.device, "effect_hatch_default_angle_delta"
433
+ ):
434
+ angle_delta = Angle(
435
+ getattr(self.device, "effect_hatch_default_angle_delta")
436
+ )
437
+ elif angle_delta is None:
438
+ angle_delta = Angle("0deg")
439
+
440
+ if etype is None:
441
+ etype = "scanline"
442
+ first_node = data[0]
443
+
444
+ node = first_node.parent.add(
445
+ type="effect hatch",
446
+ label="Hatch Effect",
447
+ hatch_type=etype,
448
+ hatch_angle=angle.radians,
449
+ hatch_angle_delta=angle_delta.radians,
450
+ hatch_distance=distance,
451
+ )
452
+ for n in data:
453
+ node.append_child(n)
454
+
455
+ # Newly created! Classification needed?
456
+ post.append(classify_new([node]))
457
+
458
+ self.set_emphasis([node])
459
+ node.focus()
460
+
461
+ @self.console_option("wtype", "w", type=str, default="circle")
462
+ @self.console_option("radius", "r", type=Length, default=None)
463
+ @self.console_option("interval", "i", type=Length, default=None)
464
+ @self.console_command(
465
+ "effect-wobble",
466
+ help=_("adds wobble-effect to selected elements"),
467
+ input_type=(None, "elements"),
468
+ )
469
+ def effect_wobble(
470
+ command,
471
+ channel,
472
+ _,
473
+ data=None,
474
+ wtype=None,
475
+ radius=None,
476
+ interval=None,
477
+ post=None,
478
+ **kwargs,
479
+ ):
480
+ """
481
+ Add an effect hatch object
482
+ """
483
+ if data is None:
484
+ data = list(self.elems(emphasized=True))
485
+ if len(data) == 0:
486
+ return
487
+ if wtype is None:
488
+ wtype = "circle"
489
+
490
+ if radius is None and hasattr(self.device, "effect_wobble_default_radius"):
491
+ radius = getattr(self.device, "effect_wobble_default_radius")
492
+ elif radius is None:
493
+ radius = "0.5mm"
494
+
495
+ if interval is None and hasattr(self.device, "effect_wobble_default_interval"):
496
+ interval = getattr(self.device, "effect_wobble_default_interval")
497
+ elif interval is None:
498
+ interval = "0.5mm"
499
+
500
+ wtype = wtype.lower()
501
+ allowed = list(self.kernel.root.match("wobble", suffix=True))
502
+ if wtype not in allowed:
503
+ channel(f"Invalid wobble type, allowed: {','.join(allowed)}")
504
+ return
505
+ try:
506
+ rlen = Length(radius)
507
+ except ValueError:
508
+ channel("Invalid value for radius")
509
+ return
510
+ try:
511
+ ilen = Length(interval)
512
+ except ValueError:
513
+ channel("Invalid value for interval")
514
+ return
515
+ first_node = data[0]
516
+ node = first_node.parent.add(
517
+ type="effect wobble",
518
+ label="Wobble Effect",
519
+ wobble_type=wtype,
520
+ wobble_radius=rlen.length_mm,
521
+ wobble_interval=ilen.length_mm,
522
+ )
523
+ for n in data:
524
+ node.append_child(n)
525
+
526
+ # Newly created! Classification needed?
527
+ post.append(classify_new([node]))
528
+
529
+ self.set_emphasis([node])
530
+ node.focus()
531
+
532
+ @self.console_option(
533
+ "size", "s", type=float, default=16, help=_("font size to for object")
534
+ )
535
+ @self.console_argument("text", type=str, help=_("quoted string of text"))
536
+ @self.console_command(
537
+ "text",
538
+ help=_("text <text>"),
539
+ input_type=(None, "elements"),
540
+ output_type="elements",
541
+ )
542
+ def element_text(
543
+ command, channel, _, data=None, text=None, size=None, post=None, **kwargs
544
+ ):
545
+ if text is None:
546
+ channel(_("No text specified"))
547
+ return
548
+ node = self.elem_branch.add(
549
+ text=text, matrix=Matrix(f"scale({UNITS_PER_PIXEL})"), type="elem text"
550
+ )
551
+ node.font_size = size
552
+ node.stroke = self.default_stroke
553
+ node.stroke_width = self.default_strokewidth
554
+ node.fill = self.default_fill
555
+ node.altered()
556
+ self.set_emphasis([node])
557
+ node.focus()
558
+ if data is None:
559
+ data = list()
560
+ data.append(node)
561
+ # Newly created! Classification needed?
562
+ post.append(classify_new(data))
563
+ return "elements", data
564
+
565
+ @self.console_argument(
566
+ "anchor", type=str, default="start", help=_("set text anchor")
567
+ )
568
+ @self.console_command(
569
+ "text-anchor",
570
+ help=_("set text object text-anchor; start, middle, end"),
571
+ input_type=(
572
+ None,
573
+ "elements",
574
+ ),
575
+ output_type="elements",
576
+ )
577
+ def element_text_anchor(command, channel, _, data, anchor=None, **kwargs):
578
+ if anchor not in ("start", "middle", "end"):
579
+ raise CommandSyntaxError(
580
+ _("Only 'start', 'middle', and 'end' are valid anchors.")
581
+ )
582
+ if data is None:
583
+ data = list(self.elems(emphasized=True))
584
+ if len(data) == 0:
585
+ channel(_("No selected elements."))
586
+ return
587
+ for e in data:
588
+ if hasattr(e, "can_modify") and not e.can_modify:
589
+ channel(_("Can't modify a locked element: {name}").format(name=str(e)))
590
+ continue
591
+ if e.type == "elem text":
592
+ old_anchor = e.anchor
593
+ e.anchor = anchor
594
+ channel(f"Node {e} anchor changed from {old_anchor} to {anchor}")
595
+
596
+ e.altered()
597
+ return "elements", data
598
+
599
+ @self.console_argument("new_text", type=str, help=_("set new text contents"))
600
+ @self.console_command(
601
+ "text-edit",
602
+ help=_("set text object text to new text"),
603
+ input_type=(
604
+ None,
605
+ "elements",
606
+ ),
607
+ output_type="elements",
608
+ all_arguments_required=True,
609
+ )
610
+ def element_text_edit(command, channel, _, data, new_text=None, **kwargs):
611
+ if data is None:
612
+ data = list(self.elems(emphasized=True))
613
+ if len(data) == 0:
614
+ channel(_("No selected elements."))
615
+ return
616
+ for e in data:
617
+ if hasattr(e, "can_modify") and not e.can_modify:
618
+ channel(_("Can't modify a locked element: {name}").format(name=str(e)))
619
+ continue
620
+ if e.type == "elem text":
621
+ old_text = e.text
622
+ e.text = new_text
623
+ elif hasattr(e, "mktext"):
624
+ old_text = e.mktext
625
+ e.mktext = new_text
626
+ for property_op in self.kernel.lookup_all("path_updater/.*"):
627
+ property_op(self.kernel.root, e)
628
+ else:
629
+ continue
630
+ channel(f"Node {e} anchor changed from {old_text} to {new_text}")
631
+ e.altered()
632
+
633
+ return "elements", data
634
+
635
+ def calculate_text_bounds(data):
636
+ """
637
+ A render operation will use the LaserRender class
638
+ and will re-calculate the element bounds
639
+ @param data:
640
+ @return:
641
+ """
642
+ make_raster = self.lookup("render-op/make_raster")
643
+ if not make_raster:
644
+ # No renderer is registered to perform render.
645
+ return
646
+ for e in data:
647
+ e.set_dirty_bounds()
648
+ # arbitrary bounds...
649
+ bounds = (0, 0, float(Length("5cm")), float(Length("5cm")))
650
+ try:
651
+ image = make_raster(
652
+ data,
653
+ bounds=bounds,
654
+ width=500,
655
+ height=500,
656
+ )
657
+ except Exception:
658
+ pass # Not relevant...
659
+
660
+ @self.console_argument("prop", type=str, help=_("property to set"))
661
+ @self.console_argument("new_value", type=str, help=_("new property value"))
662
+ @self.console_command(
663
+ "property-set",
664
+ help=_("set property to new value"),
665
+ input_type=(
666
+ None,
667
+ "elements",
668
+ ),
669
+ output_type="elements",
670
+ all_arguments_required=True,
671
+ )
672
+ def element_property_set(
673
+ command, channel, _, data, post=None, prop=None, new_value=None, **kwargs
674
+ ):
675
+ """
676
+ Generic node manipulation routine, use with care
677
+ """
678
+ if data is None:
679
+ data = list(self.elems(emphasized=True))
680
+ if len(data) == 0:
681
+ channel(_("No selected elements."))
682
+ return
683
+ if prop is None or (prop == "?" and new_value=="?"):
684
+ channel(_("You need to provide the property to set."))
685
+ if prop == "?":
686
+ identified = []
687
+ for op in data:
688
+ if op.type in identified:
689
+ continue
690
+ identified.append(op.type)
691
+ prop_str = f"{op.type} has the following properties: "
692
+ for d in op.__dict__:
693
+ if d.startswith("_"):
694
+ continue
695
+ prop_str = f"{prop_str}, {d}"
696
+ channel(prop_str)
697
+ channel ("Be careful what you do - this is a failsafe method to crash MeerK40t, burn down your house or whatever...")
698
+ return
699
+ classify_required = False
700
+ prop = prop.lower()
701
+ if len(new_value) == 0:
702
+ new_value = None
703
+ if prop in ("fill", "stroke") and self.classify_on_color:
704
+ classify_required = True
705
+ # Let's distinguish a couple of special cases...
706
+ prevalidated = False
707
+ if prop in ("fill", "stroke", "color"):
708
+ if new_value is not None:
709
+ if new_value.lower() == "none":
710
+ # The text...
711
+ new_value = None
712
+ try:
713
+ new_value = Color(new_value)
714
+ prevalidated = True
715
+ except ValueError:
716
+ channel(_("Invalid color value: {value}").format(value=new_value))
717
+ return
718
+ elif prop in ("x", "y", "width", "height", "stroke_width"):
719
+ if new_value is None:
720
+ channel(_("Invalid length: {value}").format(value=new_value))
721
+ return
722
+ else:
723
+ try:
724
+ new_value = float(Length(new_value))
725
+ prevalidated = True
726
+ except ValueError:
727
+ channel(_("Invalid length: {value}").format(value=new_value))
728
+ return
729
+
730
+ changed = []
731
+ text_elems = []
732
+
733
+ if prop == "lock":
734
+ if new_value.lower() in ("1", "true"):
735
+ setval = True
736
+ elif new_value.lower() in ("0", "false"):
737
+ setval = False
738
+ else:
739
+ try:
740
+ setval = bool(new_value)
741
+ except ValueError:
742
+ channel(
743
+ _("Can't set '{val}' for {field}.").format(
744
+ val=new_value, field=prop
745
+ )
746
+ )
747
+ return
748
+ # print (f"Will set lock to {setval} ({new_value})")
749
+ for e in data:
750
+ if hasattr(e, "lock"):
751
+ e.lock = setval
752
+ changed.append(e)
753
+ else:
754
+ for e in data:
755
+ # dbg = ""
756
+ # if hasattr(e, "bounds"):
757
+ # bb = e.bounds
758
+ # dbg += (
759
+ # f"x:{Length(bb[0], digits=2).length_mm}, "
760
+ # + f"y:{Length(bb[1], digits=2).length_mm}, "
761
+ # + f"w:{Length(bb[2]-bb[0], digits=2).length_mm}, "
762
+ # + f"h:{Length(bb[3]-bb[1], digits=2).length_mm}, "
763
+ # )
764
+ # dbg += f"{prop}:{str(getattr(e, prop)) if hasattr(e, prop) else '--'}"
765
+ # print (f"Before: {dbg}")
766
+ if prop in ("x", "y"):
767
+ if not e.can_move(self.lock_allows_move):
768
+ channel(
769
+ _("Element can not be moved: {name}").format(name=str(e))
770
+ )
771
+ continue
772
+ # We need to adjust the matrix
773
+ if hasattr(e, "bounds") and hasattr(e, "matrix"):
774
+ dx = 0
775
+ dy = 0
776
+ bb = e.bounds
777
+ if prop == "x":
778
+ dx = new_value - bb[0]
779
+ else:
780
+ dy = new_value - bb[1]
781
+ e.matrix.post_translate(dx, dy)
782
+ else:
783
+ channel(
784
+ _("Element has no matrix to modify: {name}").format(
785
+ name=str(e)
786
+ )
787
+ )
788
+ continue
789
+ elif prop in ("width", "height"):
790
+ if new_value == 0:
791
+ channel(_("Can't set {field} to zero").format(field=prop))
792
+ continue
793
+ if hasattr(e, "can_scale") and not e.can_scale:
794
+ channel(
795
+ _("Element can not be scaled: {name}").format(name=str(e))
796
+ )
797
+ continue
798
+ if hasattr(e, "matrix") and hasattr(e, "bounds"):
799
+ bb = e.bounds
800
+ sx = 1.0
801
+ sy = 1.0
802
+ wd = bb[2] - bb[0]
803
+ ht = bb[3] - bb[1]
804
+ if prop == "width":
805
+ sx = new_value / wd
806
+ else:
807
+ sy = new_value / ht
808
+ e.matrix.post_scale(sx, sy)
809
+ else:
810
+ channel(
811
+ _("Element has no matrix to modify: {name}").format(
812
+ name=str(e)
813
+ )
814
+ )
815
+ continue
816
+ elif hasattr(e, prop):
817
+ if hasattr(e, "can_modify") and not e.can_modify:
818
+ channel(
819
+ _("Can't modify a locked element: {name}").format(
820
+ name=str(e)
821
+ )
822
+ )
823
+ continue
824
+ try:
825
+ oldval = getattr(e, prop)
826
+ if prevalidated:
827
+ setval = new_value
828
+ else:
829
+ if oldval is not None:
830
+ proptype = type(oldval)
831
+ setval = proptype(new_value)
832
+ if isinstance(oldval, bool):
833
+ if new_value.lower() in ("1", "true"):
834
+ setval = True
835
+ elif new_value.lower() in ("0", "false"):
836
+ setval = False
837
+ else:
838
+ setval = new_value
839
+ setattr(e, prop, setval)
840
+ except TypeError:
841
+ channel(
842
+ _(
843
+ "Can't set '{val}' for {field} (invalid type, old={oldval})."
844
+ ).format(val=new_value, field=prop, oldval=oldval)
845
+ )
846
+ except ValueError:
847
+ channel(
848
+ _(
849
+ "Can't set '{val}' for {field} (invalid value, old={oldval})."
850
+ ).format(val=new_value, field=prop, oldval=oldval)
851
+ )
852
+ except AttributeError:
853
+ channel(
854
+ _(
855
+ "Can't set '{val}' for {field} (incompatible attribute, old={oldval})."
856
+ ).format(val=new_value, field=prop, oldval=oldval)
857
+ )
858
+
859
+ if "font" in prop:
860
+ # We need to force a recalculation of the underlying wxfont property
861
+ if hasattr(e, "wxfont"):
862
+ delattr(e, "wxfont")
863
+ text_elems.append(e)
864
+ if prop in ("mktext", "mkfont"):
865
+ for property_op in self.kernel.lookup_all("path_updater/.*"):
866
+ property_op(self.kernel.root, e)
867
+ if prop in (
868
+ "dpi",
869
+ "dither",
870
+ "dither_type",
871
+ "invert",
872
+ "red",
873
+ "green",
874
+ "blue",
875
+ "lightness",
876
+ ):
877
+ # Images require some recalculation too
878
+ self.do_image_update(e)
879
+
880
+ else:
881
+ channel(
882
+ _("Element {name} has no property {field}").format(
883
+ name=str(e), field=prop
884
+ )
885
+ )
886
+ continue
887
+ e.altered()
888
+ # dbg = ""
889
+ # if hasattr(e, "bounds"):
890
+ # bb = e.bounds
891
+ # dbg += (
892
+ # f"x:{Length(bb[0], digits=2).length_mm}, "
893
+ # + f"y:{Length(bb[1], digits=2).length_mm}, "
894
+ # + f"w:{Length(bb[2]-bb[0], digits=2).length_mm}, "
895
+ # + f"h:{Length(bb[3]-bb[1], digits=2).length_mm}, "
896
+ # )
897
+ # dbg += f"{prop}:{str(getattr(e, prop)) if hasattr(e, prop) else '--'}"
898
+ # print (f"After: {dbg}")
899
+ changed.append(e)
900
+ if len(changed) > 0:
901
+ if len(text_elems) > 0:
902
+ # Recalculate bounds
903
+ calculate_text_bounds(text_elems)
904
+ self.signal("refresh_scene", "Scene")
905
+ self.signal("element_property_update", changed)
906
+ self.validate_selected_area()
907
+ if classify_required:
908
+ post.append(classify_new(changed))
909
+
910
+ return "elements", data
911
+
912
+ @self.console_argument("prop", type=str, help=_("property to set"))
913
+ @self.console_argument("new_value", type=str, help=_("new property value"))
914
+ @self.console_command(
915
+ "op-property-set",
916
+ help=_("set operation property to new value"),
917
+ input_type=(
918
+ None,
919
+ "ops",
920
+ ),
921
+ output_type="ops",
922
+ all_arguments_required=True,
923
+ )
924
+ def operation_property_set(
925
+ command, channel, _, data, post=None, prop=None, new_value=None, **kwargs
926
+ ):
927
+ """
928
+ Generic node manipulation routine, use with care
929
+ """
930
+ if data is None:
931
+ data = list(self.ops(selected=True))
932
+ if not data:
933
+ channel(_("No selected operations."))
934
+ return
935
+ if prop is None or (prop == "?" and new_value=="?"):
936
+ channel(_("You need to provide the property to set."))
937
+ if prop == "?":
938
+ identified = []
939
+ for op in data:
940
+ if op.type in identified:
941
+ continue
942
+ identified.append(op.type)
943
+ prop_str = f"{op.type} has the following properties: "
944
+ for d in op.__dict__:
945
+ if d.startswith("_"):
946
+ continue
947
+ prop_str = f"{prop_str}, {d}"
948
+ channel(prop_str)
949
+ channel ("Be careful what you do - this is a failsafe method to crash MeerK40t, burn down your house or whatever...")
950
+ return
951
+ prop = prop.lower()
952
+ if len(new_value) == 0:
953
+ new_value = None
954
+ # Let's distinguish a couple of special cases...
955
+ prevalidated = False
956
+ if prop == "color":
957
+ if new_value is not None:
958
+ if new_value.lower() == "none":
959
+ # The text...
960
+ new_value = None
961
+ try:
962
+ new_value = Color(new_value)
963
+ prevalidated = True
964
+ except ValueError:
965
+ channel(_("Invalid color value: {value}").format(value=new_value))
966
+ return
967
+ if prop in ("power", "speed", "dpi"):
968
+ try:
969
+ testval = float(new_value)
970
+ except ValueError:
971
+ channel(f"Invalid value: {new_value}")
972
+ return
973
+ if testval < 0:
974
+ channel(f"Invalid value: {new_value}")
975
+ return
976
+ if prop == "power" and testval > 1000:
977
+ channel(f"Invalid value: {new_value}")
978
+ return
979
+ new_value = testval
980
+ prevalidated = True
981
+
982
+
983
+ changed = []
984
+
985
+ for e in data:
986
+ if hasattr(e, prop):
987
+ if hasattr(e, "can_modify") and not e.can_modify:
988
+ channel(
989
+ _("Can't modify a locked element: {name}").format(
990
+ name=str(e)
991
+ )
992
+ )
993
+ continue
994
+ try:
995
+ oldval = getattr(e, prop)
996
+ if prevalidated:
997
+ setval = new_value
998
+ else:
999
+ if oldval is not None:
1000
+ proptype = type(oldval)
1001
+ setval = proptype(new_value)
1002
+ if isinstance(oldval, bool):
1003
+ if new_value.lower() in ("1", "true"):
1004
+ setval = True
1005
+ elif new_value.lower() in ("0", "false"):
1006
+ setval = False
1007
+ else:
1008
+ setval = new_value
1009
+ setattr(e, prop, setval)
1010
+ except TypeError:
1011
+ channel(
1012
+ _(
1013
+ "Can't set '{val}' for {field} (invalid type, old={oldval})."
1014
+ ).format(val=new_value, field=prop, oldval=oldval)
1015
+ )
1016
+ except ValueError:
1017
+ channel(
1018
+ _(
1019
+ "Can't set '{val}' for {field} (invalid value, old={oldval})."
1020
+ ).format(val=new_value, field=prop, oldval=oldval)
1021
+ )
1022
+ except AttributeError:
1023
+ channel(
1024
+ _(
1025
+ "Can't set '{val}' for {field} (incompatible attribute, old={oldval})."
1026
+ ).format(val=new_value, field=prop, oldval=oldval)
1027
+ )
1028
+
1029
+
1030
+ else:
1031
+ channel(
1032
+ _("Operation {name} has no property {field}").format(
1033
+ name=str(e), field=prop
1034
+ )
1035
+ )
1036
+ continue
1037
+ e.altered()
1038
+ changed.append(e)
1039
+ if len(changed) > 0:
1040
+ self.signal("refresh_scene", "Scene")
1041
+ self.signal("element_property_update", changed)
1042
+
1043
+ return "ops", data
1044
+
1045
+ @self.console_command(
1046
+ "recalc", input_type=("elements", None), output_type="elements"
1047
+ )
1048
+ def recalc(command, channel, _, data=None, post=None, **kwargs):
1049
+ if data is None:
1050
+ data = list(self.elems(emphasized=True))
1051
+ if len(data) == 0:
1052
+ channel(_("No selected elements."))
1053
+ return
1054
+ for e in data:
1055
+ e.set_dirty_bounds()
1056
+ self.signal("refresh_scene", "Scene")
1057
+ self.validate_selected_area()
1058
+
1059
+ @self.console_option("douglas", "d", type=bool, action="store_true", default=False)
1060
+ @self.console_option(
1061
+ "visvalingam", "v", type=bool, action="store_true", default=False
1062
+ )
1063
+ @self.console_option(
1064
+ "tolerance",
1065
+ "t",
1066
+ type=float,
1067
+ help=_("simplification tolerance"),
1068
+ )
1069
+ @self.console_command(
1070
+ "simplify", input_type=("elements", None), output_type="elements"
1071
+ )
1072
+ def simplify_path(
1073
+ command,
1074
+ channel,
1075
+ _,
1076
+ data=None,
1077
+ tolerance=None,
1078
+ douglas=None,
1079
+ visvalingam=None,
1080
+ post=None,
1081
+ **kwargs,
1082
+ ):
1083
+ if data is None:
1084
+ data = list(self.elems(emphasized=True))
1085
+ data_changed = list()
1086
+ if len(data) == 0:
1087
+ channel("Requires a selected polygon")
1088
+ return None
1089
+ method = "douglaspeucker"
1090
+ if douglas:
1091
+ method = "douglaspeucker"
1092
+ if visvalingam:
1093
+ method = "visvalingam"
1094
+ if tolerance is None:
1095
+ tolerance = 25 # About 1/1000 mil
1096
+ for node in data:
1097
+ try:
1098
+ sub_before = len(list(node.as_geometry().as_subpaths()))
1099
+ except AttributeError:
1100
+ sub_before = 0
1101
+ if hasattr(node, "geometry"):
1102
+ geom = node.geometry
1103
+ seg_before = node.geometry.index
1104
+ if method == "douglaspeucker":
1105
+ node.geometry = geom.simplify(tolerance)
1106
+ else:
1107
+ # Let's try Visvalingam line simplification
1108
+ node.geometry = geom.simplify_geometry(threshold=tolerance)
1109
+ node.altered()
1110
+ seg_after = node.geometry.index
1111
+ try:
1112
+ sub_after = len(list(node.as_geometry().as_subpaths()))
1113
+ except AttributeError:
1114
+ sub_after = 0
1115
+ channel(
1116
+ f"Simplified {node.type} ({node.display_label()}), tolerance: {tolerance}={Length(tolerance, digits=4).length_mm})"
1117
+ )
1118
+ if seg_before:
1119
+ saving = f"({(seg_before - seg_after)/seg_before*100:.1f}%)"
1120
+ else:
1121
+ saving = ""
1122
+ channel(f"Subpaths before: {sub_before} to {sub_after}")
1123
+ channel(f"Segments before: {seg_before} to {seg_after} {saving}")
1124
+ data_changed.append(node)
1125
+ else:
1126
+ channel(
1127
+ f"Invalid node for simplify {node.type} ({node.display_label()})"
1128
+ )
1129
+ if len(data_changed) > 0:
1130
+ self.signal("element_property_update", data_changed)
1131
+ self.signal("refresh_scene", "Scene")
1132
+ return "elements", data
1133
+
1134
+ @self.console_command(
1135
+ "polycut", input_type=("elements", None), output_type="elements"
1136
+ )
1137
+ def create_pattern(command, channel, _, data=None, post=None, **kwargs):
1138
+ if data is None:
1139
+ data = list(self.elems(emphasized=True))
1140
+ if len(data) <= 1:
1141
+ channel("Requires a selected cutter polygon")
1142
+ return None
1143
+ data.sort(key=lambda n: n.emphasized_time)
1144
+ try:
1145
+ outer_path = data[0].as_path()
1146
+ inner_path = data[1].as_path()
1147
+ except AttributeError:
1148
+ # elem text does not have an as_path() object
1149
+ return "elements", data
1150
+ data[1].remove_node()
1151
+
1152
+ from meerk40t.tools.pathtools import VectorMontonizer
1153
+
1154
+ vm = VectorMontonizer()
1155
+ outer_path = Polygon(
1156
+ [outer_path.point(i / 1000.0, error=1e4) for i in range(1001)]
1157
+ )
1158
+ vm.add_polyline(outer_path)
1159
+ path = Path()
1160
+ for sub_inner in inner_path.as_subpaths():
1161
+ sub_inner = Path(sub_inner)
1162
+ pts_sub = [sub_inner.point(i / 1000.0, error=1e4) for i in range(1001)]
1163
+ for i in range(len(pts_sub) - 1, -1, -1):
1164
+ pt = pts_sub[i]
1165
+ if not vm.is_point_inside(pt[0], pt[1]):
1166
+ del pts_sub[i]
1167
+ path += Path(Polyline(pts_sub))
1168
+ node = self.elem_branch.add(path=path, type="elem path")
1169
+ data.append(node)
1170
+ node.stroke = self.default_stroke
1171
+ node.stroke_width = self.default_strokewidth
1172
+ node.fill = self.default_fill
1173
+ node.altered()
1174
+ self.set_emphasis([node])
1175
+ node.focus()
1176
+ post.append(classify_new(data))
1177
+ return "elements", data
1178
+
1179
+ @self.console_argument("mlist", type=Length, help=_("list of positions"), nargs="*")
1180
+ @self.console_command(
1181
+ ("polygon", "polyline"),
1182
+ help=_("poly(gon|line) (Length Length)*"),
1183
+ input_type=("elements", None),
1184
+ output_type="elements",
1185
+ all_arguments_required=True,
1186
+ )
1187
+ def element_poly(command, channel, _, mlist, data=None, post=None, **kwargs):
1188
+ try:
1189
+ pts = [float(Length(p)) for p in mlist]
1190
+ if command == "polygon":
1191
+ shape = Polygon(pts)
1192
+ else:
1193
+ shape = Polyline(pts)
1194
+ except ValueError:
1195
+ raise CommandSyntaxError(
1196
+ _("Must be a list of spaced delimited length pairs.")
1197
+ )
1198
+ if shape.is_degenerate():
1199
+ channel(_("Shape is degenerate."))
1200
+ return "elements", data
1201
+ node = self.elem_branch.add(shape=shape, type="elem polyline")
1202
+ node.stroke = self.default_stroke
1203
+ node.stroke_width = self.default_strokewidth
1204
+ node.fill = self.default_fill
1205
+ node.altered()
1206
+ self.set_emphasis([node])
1207
+ node.focus()
1208
+ if data is None:
1209
+ data = list()
1210
+ data.append(node)
1211
+ # Newly created! Classification needed?
1212
+ post.append(classify_new(data))
1213
+ return "elements", data
1214
+
1215
+ @self.console_option(
1216
+ "real",
1217
+ "r",
1218
+ action="store_true",
1219
+ type=bool,
1220
+ help="Display non-transformed path",
1221
+ )
1222
+ @self.console_command(
1223
+ "path_d_info",
1224
+ help=_("List the path_d of any recognized paths"),
1225
+ input_type="elements",
1226
+ )
1227
+ def element_pathd_info(command, channel, _, data, real=True, **kwargs):
1228
+ for node in data:
1229
+ try:
1230
+ g = node.as_geometry()
1231
+ path = g.as_path()
1232
+ ident = " (Identity)" if node.matrix.is_identity() else ""
1233
+ channel(f"{str(node)}{ident}: {path.d(transformed=not real)}")
1234
+ except AttributeError:
1235
+ channel(f"{str(node)}: Invalid")
1236
+
1237
+ @self.console_argument(
1238
+ "path_d", type=str, help=_("svg path syntax command (quoted).")
1239
+ )
1240
+ @self.console_command(
1241
+ "path",
1242
+ help=_("path <svg path>"),
1243
+ output_type="elements",
1244
+ )
1245
+ def element_path(path_d, data, post=None, **kwargs):
1246
+ if path_d is None:
1247
+ raise CommandSyntaxError(_("Not a valid path_d string"))
1248
+ try:
1249
+ path = Path(path_d)
1250
+ path *= f"Scale({UNITS_PER_PIXEL})"
1251
+ except (ValueError, AttributeError):
1252
+ raise CommandSyntaxError(_("Not a valid path_d string (try quotes)"))
1253
+
1254
+ node = self.elem_branch.add(path=path, type="elem path")
1255
+ node.stroke = self.default_stroke
1256
+ node.stroke_width = self.default_strokewidth
1257
+ node.fill = self.default_fill
1258
+ node.altered()
1259
+ self.set_emphasis([node])
1260
+ node.focus()
1261
+ if data is None:
1262
+ data = list()
1263
+ data.append(node)
1264
+ # Newly created! Classification needed?
1265
+ post.append(classify_new(data))
1266
+ return "elements", data
1267
+
1268
+ @self.console_argument(
1269
+ "stroke_width",
1270
+ type=self.length,
1271
+ help=_("Stroke-width for the given stroke"),
1272
+ )
1273
+ @self.console_command(
1274
+ "stroke-width",
1275
+ help=_("stroke-width <length>"),
1276
+ input_type=(
1277
+ None,
1278
+ "elements",
1279
+ ),
1280
+ output_type="elements",
1281
+ )
1282
+ def element_stroke_width(command, channel, _, stroke_width, data=None, **kwargs):
1283
+ def width_string(value):
1284
+ if value is None:
1285
+ return "-"
1286
+ res = ""
1287
+ display_units = (
1288
+ (1, ""),
1289
+ (UNITS_PER_PIXEL, "px"),
1290
+ (UNITS_PER_POINT, "pt"),
1291
+ (UNITS_PER_MM, "mm"),
1292
+ )
1293
+ for unit in display_units:
1294
+ unit_value = value / unit[0]
1295
+ if res != "":
1296
+ res += ", "
1297
+ res += f"{unit_value:.3f}{unit[1]}"
1298
+ return res
1299
+
1300
+ if data is None:
1301
+ data = list(self.elems(emphasized=True))
1302
+ if stroke_width is None:
1303
+ # Display data about stroke widths.
1304
+ channel("----------")
1305
+ channel(_("Stroke-Width Values:"))
1306
+ for i, e in enumerate(self.elems()):
1307
+ name = str(e)
1308
+ if len(name) > 50:
1309
+ name = name[:50] + "…"
1310
+ try:
1311
+ stroke_width = e.stroke_width
1312
+ except AttributeError:
1313
+ # Has no stroke width.
1314
+ continue
1315
+ if not hasattr(e, "stroke_scaled"):
1316
+ # Can't have a scaled stroke.
1317
+ channel(
1318
+ _(
1319
+ "{index}: {name} - {typename}\n stroke-width = {stroke_width}\n scaled-width = {scaled_stroke_width}"
1320
+ ).format(
1321
+ index=i,
1322
+ typename="scaled-stroke",
1323
+ stroke_width=width_string(stroke_width),
1324
+ scaled_stroke_width=width_string(None),
1325
+ name=name,
1326
+ )
1327
+ )
1328
+ continue
1329
+ factor = 1.0
1330
+ if e.stroke_scaled:
1331
+ typename = "scaled-stroke"
1332
+ try:
1333
+ factor = e.stroke_factor
1334
+ except AttributeError:
1335
+ pass
1336
+ else:
1337
+ typename = "non-scaling-stroke"
1338
+ implied_value = factor * stroke_width
1339
+ channel(
1340
+ _(
1341
+ "{index}: {name} - {typename}\n stroke-width = {stroke_width}\n scaled-width = {scaled_stroke_width}"
1342
+ ).format(
1343
+ index=i,
1344
+ typename=typename,
1345
+ stroke_width=width_string(stroke_width),
1346
+ scaled_stroke_width=width_string(implied_value),
1347
+ name=name,
1348
+ )
1349
+ )
1350
+ channel("----------")
1351
+ return
1352
+
1353
+ if len(data) == 0:
1354
+ channel(_("No selected elements."))
1355
+ return
1356
+ for e in data:
1357
+ if hasattr(e, "lock") and e.lock:
1358
+ channel(_("Can't modify a locked element: {name}").format(name=str(e)))
1359
+ continue
1360
+ e.stroke_width = stroke_width
1361
+ try:
1362
+ e.stroke_width_zero()
1363
+ except AttributeError:
1364
+ pass
1365
+ # No full modified required, we are effectively only adjusting
1366
+ # the painted_bounds
1367
+ e.translated(0, 0)
1368
+ self.signal("element_property_update", data)
1369
+ self.signal("refresh_scene", "Scene")
1370
+ return "elements", data
1371
+
1372
+ @self.console_command(
1373
+ ("enable_stroke_scale", "disable_stroke_scale"),
1374
+ help=_("stroke-width <length>"),
1375
+ input_type=(
1376
+ None,
1377
+ "elements",
1378
+ ),
1379
+ hidden=True,
1380
+ output_type="elements",
1381
+ )
1382
+ def element_stroke_scale_enable(command, channel, _, data=None, **kwargs):
1383
+ if data is None:
1384
+ data = list(self.elems(emphasized=True))
1385
+ if len(data) == 0:
1386
+ channel(_("No selected elements."))
1387
+ return
1388
+ for e in data:
1389
+ if hasattr(e, "lock") and e.lock:
1390
+ channel(_("Can't modify a locked element: {name}").format(name=str(e)))
1391
+ continue
1392
+ e.stroke_scaled = command == "enable_stroke_scale"
1393
+ e.altered()
1394
+ self.signal("element_property_update", data)
1395
+ self.signal("refresh_scene", "Scene")
1396
+ return "elements", data
1397
+
1398
+ @self.console_option("filter", "f", type=str, help="Filter indexes")
1399
+ @self.console_argument(
1400
+ "cap",
1401
+ type=str,
1402
+ help=_("Linecap to apply to the path (one of butt, round, square)"),
1403
+ )
1404
+ @self.console_command(
1405
+ "linecap",
1406
+ help=_("linecap <cap>"),
1407
+ input_type=(
1408
+ None,
1409
+ "elements",
1410
+ ),
1411
+ output_type="elements",
1412
+ )
1413
+ def element_cap(command, channel, _, cap=None, data=None, filter=None, **kwargs):
1414
+ if data is None:
1415
+ data = list(self.elems(emphasized=True))
1416
+ apply = data
1417
+ if filter is not None:
1418
+ apply = list()
1419
+ for value in filter.split(","):
1420
+ try:
1421
+ value = int(value)
1422
+ except ValueError:
1423
+ continue
1424
+ try:
1425
+ apply.append(data[value])
1426
+ except IndexError:
1427
+ channel(_("index {index} out of range").format(index=value))
1428
+ if cap is None:
1429
+ channel("----------")
1430
+ channel(_("Linecaps:"))
1431
+ i = 0
1432
+ for e in self.elems():
1433
+ name = str(e)
1434
+ if len(name) > 50:
1435
+ name = name[:50] + ""
1436
+ if hasattr(e, "linecap"):
1437
+ if e.linecap == Linecap.CAP_SQUARE:
1438
+ capname = "square"
1439
+ elif e.linecap == Linecap.CAP_BUTT:
1440
+ capname = "butt"
1441
+ else:
1442
+ capname = "round"
1443
+ channel(
1444
+ _("{index}: linecap = {linecap} - {name}").format(
1445
+ index=i, linecap=capname, name=name
1446
+ )
1447
+ )
1448
+ i += 1
1449
+ channel("----------")
1450
+ return
1451
+ else:
1452
+ capvalue = None
1453
+ if cap.lower() == "butt":
1454
+ capvalue = Linecap.CAP_BUTT
1455
+ elif cap.lower() == "round":
1456
+ capvalue = Linecap.CAP_ROUND
1457
+ elif cap.lower() == "square":
1458
+ capvalue = Linecap.CAP_SQUARE
1459
+ if capvalue is not None:
1460
+ for e in apply:
1461
+ if hasattr(e, "linecap"):
1462
+ if hasattr(e, "lock") and e.lock:
1463
+ channel(
1464
+ _("Can't modify a locked element: {name}").format(
1465
+ name=str(e)
1466
+ )
1467
+ )
1468
+ continue
1469
+ e.linecap = capvalue
1470
+ e.altered()
1471
+ return "elements", data
1472
+
1473
+ @self.console_option("filter", "f", type=str, help="Filter indexes")
1474
+ @self.console_argument(
1475
+ "join",
1476
+ type=str,
1477
+ help=_(
1478
+ "jointype to apply to the path (one of arcs, bevel, miter, miter-clip, round)"
1479
+ ),
1480
+ )
1481
+ @self.console_command(
1482
+ "linejoin",
1483
+ help=_("linejoin <join>"),
1484
+ input_type=(
1485
+ None,
1486
+ "elements",
1487
+ ),
1488
+ output_type="elements",
1489
+ )
1490
+ def element_join(command, channel, _, join=None, data=None, filter=None, **kwargs):
1491
+ if data is None:
1492
+ data = list(self.elems(emphasized=True))
1493
+ apply = data
1494
+ if filter is not None:
1495
+ apply = list()
1496
+ for value in filter.split(","):
1497
+ try:
1498
+ value = int(value)
1499
+ except ValueError:
1500
+ continue
1501
+ try:
1502
+ apply.append(data[value])
1503
+ except IndexError:
1504
+ channel(_("index {index} out of range").format(index=value))
1505
+ if join is None:
1506
+ channel("----------")
1507
+ channel(_("Linejoins:"))
1508
+ i = 0
1509
+ for e in self.elems():
1510
+ name = str(e)
1511
+ if len(name) > 50:
1512
+ name = name[:50] + "…"
1513
+ if hasattr(e, "linejoin"):
1514
+ if e.linejoin == Linejoin.JOIN_ARCS:
1515
+ joinname = "arcs"
1516
+ elif e.linejoin == Linejoin.JOIN_BEVEL:
1517
+ joinname = "bevel"
1518
+ elif e.linejoin == Linejoin.JOIN_MITER_CLIP:
1519
+ joinname = "miter-clip"
1520
+ elif e.linejoin == Linejoin.JOIN_MITER:
1521
+ joinname = "miter"
1522
+ elif e.linejoin == Linejoin.JOIN_ROUND:
1523
+ joinname = "round"
1524
+ channel(
1525
+ _("{index}: linejoin = {linejoin} - {name}").format(
1526
+ index=i, linejoin=joinname, name=name
1527
+ )
1528
+ )
1529
+ i += 1
1530
+ channel("----------")
1531
+ return
1532
+ else:
1533
+ joinvalue = None
1534
+ if join.lower() == "arcs":
1535
+ joinvalue = Linejoin.JOIN_ARCS
1536
+ elif join.lower() == "bevel":
1537
+ joinvalue = Linejoin.JOIN_BEVEL
1538
+ elif join.lower() == "miter":
1539
+ joinvalue = Linejoin.JOIN_MITER
1540
+ elif join.lower() == "miter-clip":
1541
+ joinvalue = Linejoin.JOIN_MITER_CLIP
1542
+ elif join.lower() == "round":
1543
+ joinvalue = Linejoin.JOIN_ROUND
1544
+ if joinvalue is not None:
1545
+ for e in apply:
1546
+ if hasattr(e, "linejoin"):
1547
+ if hasattr(e, "lock") and e.lock:
1548
+ channel(
1549
+ _("Can't modify a locked element: {name}").format(
1550
+ name=str(e)
1551
+ )
1552
+ )
1553
+ continue
1554
+ e.linejoin = joinvalue
1555
+ e.altered()
1556
+ return "elements", data
1557
+
1558
+ @self.console_option("filter", "f", type=str, help="Filter indexes")
1559
+ @self.console_argument(
1560
+ "rule",
1561
+ type=str,
1562
+ help=_("rule to apply to fill the path (one of {nonzero}, {evenodd})").format(
1563
+ nonzero=SVG_RULE_NONZERO, evenodd=SVG_RULE_EVENODD
1564
+ ),
1565
+ )
1566
+ @self.console_command(
1567
+ "fillrule",
1568
+ help=_("fillrule <rule>"),
1569
+ input_type=(
1570
+ None,
1571
+ "elements",
1572
+ ),
1573
+ output_type="elements",
1574
+ )
1575
+ def element_rule(command, channel, _, rule=None, data=None, filter=None, **kwargs):
1576
+ if data is None:
1577
+ data = list(self.elems(emphasized=True))
1578
+ apply = data
1579
+ if filter is not None:
1580
+ apply = list()
1581
+ for value in filter.split(","):
1582
+ try:
1583
+ value = int(value)
1584
+ except ValueError:
1585
+ continue
1586
+ try:
1587
+ apply.append(data[value])
1588
+ except IndexError:
1589
+ channel(_("index {index} out of range").format(index=value))
1590
+ if rule is None:
1591
+ channel("----------")
1592
+ channel(_("fillrules:"))
1593
+ i = 0
1594
+ for e in self.elems():
1595
+ name = str(e)
1596
+ if len(name) > 50:
1597
+ name = name[:50] + ""
1598
+ if hasattr(e, "fillrule"):
1599
+ if e.fillrule == Fillrule.FILLRULE_EVENODD:
1600
+ rulename = SVG_RULE_EVENODD
1601
+ elif e.fillrule == Fillrule.FILLRULE_NONZERO:
1602
+ rulename = SVG_RULE_NONZERO
1603
+ channel(
1604
+ _("{index}: fillrule = {fillrule} - {name}").format(
1605
+ index=i, fillrule=rulename, name=name
1606
+ )
1607
+ )
1608
+ i += 1
1609
+ channel("----------")
1610
+ return
1611
+ else:
1612
+ rulevalue = None
1613
+ if rule.lower() == SVG_RULE_EVENODD:
1614
+ rulevalue = Fillrule.FILLRULE_EVENODD
1615
+ elif rule.lower() == SVG_RULE_NONZERO:
1616
+ rulevalue = Fillrule.FILLRULE_NONZERO
1617
+ if rulevalue is not None:
1618
+ for e in apply:
1619
+ if hasattr(e, "fillrule"):
1620
+ if hasattr(e, "lock") and e.lock:
1621
+ channel(
1622
+ _("Can't modify a locked element: {name}").format(
1623
+ name=str(e)
1624
+ )
1625
+ )
1626
+ continue
1627
+ e.fillrule = rulevalue
1628
+ e.altered()
1629
+ return "elements", data
1630
+
1631
+ @self.console_option(
1632
+ "classify", "c", type=bool, action="store_true", help="Reclassify element"
1633
+ )
1634
+ @self.console_option("filter", "f", type=str, help="Filter indexes")
1635
+ @self.console_argument(
1636
+ "color", type=Color, help=_("Color to color the given stroke")
1637
+ )
1638
+ @self.console_command(
1639
+ "stroke",
1640
+ help=_("stroke <svg color>"),
1641
+ input_type=(
1642
+ None,
1643
+ "elements",
1644
+ ),
1645
+ output_type="elements",
1646
+ )
1647
+ def element_stroke(
1648
+ command, channel, _, color, data=None, classify=None, filter=None, **kwargs
1649
+ ):
1650
+ if data is None:
1651
+ data = list(self.elems(emphasized=True))
1652
+ was_emphasized = True
1653
+ old_first = self.first_emphasized
1654
+ else:
1655
+ was_emphasized = False
1656
+ old_first = None
1657
+ apply = data
1658
+ if filter is not None:
1659
+ apply = list()
1660
+ for value in filter.split(","):
1661
+ try:
1662
+ value = int(value)
1663
+ except ValueError:
1664
+ continue
1665
+ try:
1666
+ apply.append(data[value])
1667
+ except IndexError:
1668
+ channel(_("index {index} out of range").format(index=value))
1669
+ if color is None:
1670
+ channel("----------")
1671
+ channel(_("Stroke Values:"))
1672
+ i = 0
1673
+ for e in self.elems():
1674
+ name = str(e)
1675
+ if len(name) > 50:
1676
+ name = name[:50] + "…"
1677
+ if not hasattr(e, "stroke"):
1678
+ pass
1679
+ elif hasattr(e, "stroke") and e.stroke is None or e.stroke == "none":
1680
+ channel(f"{i}: stroke = none - {name}")
1681
+ else:
1682
+ channel(f"{i}: stroke = {e.stroke.hex} - {name}")
1683
+ i += 1
1684
+ channel("----------")
1685
+ return
1686
+ self.set_start_time("full_load")
1687
+ # _("Set stroke")
1688
+ with self.undoscope("Set stroke"):
1689
+ if color == "none":
1690
+ self.set_start_time("stroke")
1691
+ for e in apply:
1692
+ if hasattr(e, "lock") and e.lock:
1693
+ channel(
1694
+ _("Can't modify a locked element: {name}").format(name=str(e))
1695
+ )
1696
+ continue
1697
+ e.stroke = None
1698
+ e.translated(0, 0)
1699
+ # e.altered()
1700
+ self.set_end_time("stroke")
1701
+ else:
1702
+ self.set_start_time("stroke")
1703
+ for e in apply:
1704
+ if hasattr(e, "lock") and e.lock:
1705
+ channel(
1706
+ _("Can't modify a locked element: {name}").format(name=str(e))
1707
+ )
1708
+ continue
1709
+ e.stroke = Color(color)
1710
+ e.translated(0, 0)
1711
+ # e.altered()
1712
+ self.set_end_time("stroke")
1713
+ if classify is None:
1714
+ classify = False
1715
+ if classify:
1716
+ self.set_start_time("classify")
1717
+ self.remove_elements_from_operations(apply)
1718
+ self.classify(apply)
1719
+ if was_emphasized:
1720
+ for e in apply:
1721
+ e.emphasized = True
1722
+ if len(apply) == 1:
1723
+ apply[0].focus()
1724
+ if old_first is not None and old_first in apply:
1725
+ self.first_emphasized = old_first
1726
+ else:
1727
+ self.first_emphasized = None
1728
+ self.set_end_time("classify")
1729
+ # self.signal("rebuild_tree")
1730
+ self.signal("refresh_tree", apply)
1731
+ else:
1732
+ self.signal("element_property_reload", apply)
1733
+ self.signal("refresh_scene", "Scene")
1734
+ return "elements", data
1735
+
1736
+ @self.console_option(
1737
+ "classify", "c", type=bool, action="store_true", help="Reclassify element"
1738
+ )
1739
+ @self.console_option("filter", "f", type=str, help="Filter indexes")
1740
+ @self.console_argument("color", type=Color, help=_("Color to set the fill to"))
1741
+ @self.console_command(
1742
+ "fill",
1743
+ help=_("fill <svg color>"),
1744
+ input_type=(
1745
+ None,
1746
+ "elements",
1747
+ ),
1748
+ output_type="elements",
1749
+ )
1750
+ def element_fill(
1751
+ command, channel, _, color, data=None, classify=None, filter=None, **kwargs
1752
+ ):
1753
+ if data is None:
1754
+ data = list(self.elems(emphasized=True))
1755
+ was_emphasized = True
1756
+ old_first = self.first_emphasized
1757
+ else:
1758
+ was_emphasized = False
1759
+ old_first = None
1760
+ apply = data
1761
+ if filter is not None:
1762
+ apply = list()
1763
+ for value in filter.split(","):
1764
+ try:
1765
+ value = int(value)
1766
+ except ValueError:
1767
+ continue
1768
+ try:
1769
+ apply.append(data[value])
1770
+ except IndexError:
1771
+ channel(_("index {index} out of range").format(index=value))
1772
+ if color is None:
1773
+ channel("----------")
1774
+ channel(_("Fill Values:"))
1775
+ i = 0
1776
+ for e in self.elems():
1777
+ name = str(e)
1778
+ if len(name) > 50:
1779
+ name = name[:50] + "…"
1780
+ if not hasattr(e, "fill"):
1781
+ pass
1782
+ elif e.fill is None or e.fill == "none":
1783
+ channel(
1784
+ _("{index}: fill = none - {name}").format(index=i, name=name)
1785
+ )
1786
+ else:
1787
+ channel(
1788
+ _("{index}: fill = {fill} - {name}").format(
1789
+ index=i, fill=e.fill.hex, name=name
1790
+ )
1791
+ )
1792
+ i += 1
1793
+ channel("----------")
1794
+ return "elements", data
1795
+ # _("Set fill")
1796
+ with self.undoscope("Set fill"):
1797
+
1798
+ if color == "none":
1799
+ self.set_start_time("fill")
1800
+ for e in apply:
1801
+ if hasattr(e, "lock") and e.lock:
1802
+ channel(
1803
+ _("Can't modify a locked element: {name}").format(name=str(e))
1804
+ )
1805
+ continue
1806
+ e.fill = None
1807
+ e.translated(0, 0)
1808
+ # e.altered()
1809
+ self.set_end_time("fill")
1810
+ else:
1811
+ self.set_start_time("fill")
1812
+ for e in apply:
1813
+ if hasattr(e, "lock") and e.lock:
1814
+ channel(
1815
+ _("Can't modify a locked element: {name}").format(name=str(e))
1816
+ )
1817
+ continue
1818
+ e.fill = Color(color)
1819
+ e.translated(0, 0)
1820
+ # e.altered()
1821
+ self.set_end_time("fill")
1822
+ if classify is None:
1823
+ classify = False
1824
+ if classify:
1825
+ self.set_start_time("classify")
1826
+ self.remove_elements_from_operations(apply)
1827
+ self.classify(apply)
1828
+ if was_emphasized:
1829
+ for e in apply:
1830
+ e.emphasized = True
1831
+ if len(apply) == 1:
1832
+ apply[0].focus()
1833
+ if old_first is not None and old_first in apply:
1834
+ self.first_emphasized = old_first
1835
+ else:
1836
+ self.first_emphasized = None
1837
+ self.signal("refresh_tree", apply)
1838
+ # self.signal("rebuild_tree")
1839
+ self.set_end_time("classify")
1840
+ else:
1841
+ self.signal("element_property_update", apply)
1842
+ self.signal("refresh_scene", "Scene")
1843
+ return "elements", data
1844
+
1845
+ @self.console_argument(
1846
+ "x_offset", type=self.length_x, help=_("x offset."), default="0"
1847
+ )
1848
+ @self.console_argument(
1849
+ "y_offset", type=self.length_y, help=_("y offset"), default="0"
1850
+ )
1851
+ @self.console_command(
1852
+ "frame",
1853
+ help=_("Draws a frame the current selected elements"),
1854
+ input_type=(
1855
+ None,
1856
+ "elements",
1857
+ ),
1858
+ output_type="elements",
1859
+ )
1860
+ def element_frame(
1861
+ command,
1862
+ channel,
1863
+ _,
1864
+ x_offset=None,
1865
+ y_offset=None,
1866
+ data=None,
1867
+ post=None,
1868
+ **kwargs,
1869
+ ):
1870
+ """
1871
+ Draws an outline of the current shape.
1872
+ """
1873
+ bounds = self.selected_area()
1874
+ if bounds is None:
1875
+ channel(_("Nothing Selected"))
1876
+ return
1877
+ x_pos = bounds[0]
1878
+ y_pos = bounds[1]
1879
+ width = bounds[2] - bounds[0]
1880
+ height = bounds[3] - bounds[1]
1881
+ x_pos -= x_offset
1882
+ y_pos -= y_offset
1883
+ width += x_offset * 2
1884
+ height += y_offset * 2
1885
+ node = self.elem_branch.add(
1886
+ x=x_pos,
1887
+ y=y_pos,
1888
+ width=width,
1889
+ height=height,
1890
+ stroke=Color("red"),
1891
+ type="elem rect",
1892
+ )
1893
+ self.set_emphasis([node])
1894
+ node.focus()
1895
+ if data is None:
1896
+ data = list()
1897
+ data.append(node)
1898
+ # Newly created! Classification needed?
1899
+ post.append(classify_new(data))
1900
+ return "elements", data
1901
+
1902
+ @self.console_argument("angle", type=Angle, help=_("angle to rotate by"))
1903
+ @self.console_option("cx", "x", type=self.length_x, help=_("center x"))
1904
+ @self.console_option("cy", "y", type=self.length_y, help=_("center y"))
1905
+ @self.console_option(
1906
+ "absolute",
1907
+ "a",
1908
+ type=bool,
1909
+ action="store_true",
1910
+ help=_("angle_to absolute angle"),
1911
+ )
1912
+ @self.console_command(
1913
+ "rotate",
1914
+ help=_("rotate <angle>"),
1915
+ input_type=(
1916
+ None,
1917
+ "elements",
1918
+ ),
1919
+ output_type="elements",
1920
+ )
1921
+ def element_rotate(
1922
+ command,
1923
+ channel,
1924
+ _,
1925
+ angle,
1926
+ cx=None,
1927
+ cy=None,
1928
+ absolute=False,
1929
+ data=None,
1930
+ **kwargs,
1931
+ ):
1932
+ if angle is None:
1933
+ channel("----------")
1934
+ channel(_("Rotate Values:"))
1935
+ i = 0
1936
+ for node in self.elems():
1937
+ name = str(node)
1938
+ if len(name) > 50:
1939
+ name = name[:50] + ""
1940
+ channel(
1941
+ _("{index}: rotate({angle}turn) - {name}").format(
1942
+ index=i,
1943
+ angle=Angle(node.matrix.rotation).angle_turns[:-4],
1944
+ name=name,
1945
+ )
1946
+ )
1947
+ i += 1
1948
+ channel("----------")
1949
+ return
1950
+ if data is None:
1951
+ data = list(self.elems(emphasized=True))
1952
+ if len(data) == 0:
1953
+ channel(_("No selected elements."))
1954
+ return
1955
+ self.validate_selected_area()
1956
+ bounds = self.selected_area()
1957
+ if bounds is None:
1958
+ channel(_("No selected elements."))
1959
+ return
1960
+
1961
+ if cx is None:
1962
+ cx = (bounds[2] + bounds[0]) / 2.0
1963
+ if cy is None:
1964
+ cy = (bounds[3] + bounds[1]) / 2.0
1965
+ images = []
1966
+ try:
1967
+ if not absolute:
1968
+ for node in data:
1969
+ if hasattr(node, "lock") and node.lock:
1970
+ continue
1971
+ node.matrix.post_rotate(angle, cx, cy)
1972
+ node.modified()
1973
+ if hasattr(node, "update"):
1974
+ images.append(node)
1975
+ else:
1976
+ for node in data:
1977
+ if hasattr(node, "lock") and node.lock:
1978
+ continue
1979
+ start_angle = node.matrix.rotation
1980
+ node.matrix.post_rotate(angle - start_angle, cx, cy)
1981
+ node.modified()
1982
+ if hasattr(node, "update"):
1983
+ images.append(node)
1984
+ except ValueError:
1985
+ raise CommandSyntaxError
1986
+ for node in images:
1987
+ self.do_image_update(node)
1988
+
1989
+ self.signal("refresh_scene", "Scene")
1990
+ return "elements", data
1991
+
1992
+ @self.console_argument("scale_x", type=str, help=_("scale_x value"))
1993
+ @self.console_argument("scale_y", type=str, help=_("scale_y value"))
1994
+ @self.console_option("px", "x", type=self.length_x, help=_("scale x origin point"))
1995
+ @self.console_option("py", "y", type=self.length_y, help=_("scale y origin point"))
1996
+ @self.console_option(
1997
+ "absolute",
1998
+ "a",
1999
+ type=bool,
2000
+ action="store_true",
2001
+ help=_("scale to absolute size"),
2002
+ )
2003
+ @self.console_command(
2004
+ "scale",
2005
+ help=_("scale <scale> [<scale-y>]?"),
2006
+ input_type=(None, "elements"),
2007
+ output_type="elements",
2008
+ )
2009
+ def element_scale(
2010
+ command,
2011
+ channel,
2012
+ _,
2013
+ scale_x=None,
2014
+ scale_y=None,
2015
+ px=None,
2016
+ py=None,
2017
+ absolute=False,
2018
+ data=None,
2019
+ **kwargs,
2020
+ ):
2021
+ if scale_x is None:
2022
+ channel("----------")
2023
+ channel(_("Scale Values:"))
2024
+ i = 0
2025
+ for node in self.elems():
2026
+ name = str(node)
2027
+ if len(name) > 50:
2028
+ name = name[:50] + "…"
2029
+ channel(
2030
+ f"{i}: scale({node.matrix.value_scale_x()}, {node.matrix.value_scale_y()}) - {name}"
2031
+ )
2032
+ i += 1
2033
+ channel("----------")
2034
+ return
2035
+ if data is None:
2036
+ data = list(self.elems(emphasized=True))
2037
+ if len(data) == 0:
2038
+ channel(_("No selected elements."))
2039
+ return
2040
+ # print (f"Start: {scale_x} ({type(scale_x).__name__}), {scale_y} ({type(scale_y).__name__})")
2041
+ factor = 1
2042
+ if scale_x.endswith("%"):
2043
+ factor = 0.01
2044
+ scale_x = scale_x[:-1]
2045
+ try:
2046
+ scale_x = factor * float(scale_x)
2047
+ except ValueError:
2048
+ scale_x = 1
2049
+ if scale_y is None:
2050
+ scale_y = scale_x
2051
+ else:
2052
+ factor = 1
2053
+ if scale_y.endswith("%"):
2054
+ factor = 0.01
2055
+ scale_y = scale_y[:-1]
2056
+ try:
2057
+ scale_y = factor * float(scale_y)
2058
+ except ValueError:
2059
+ scale_y = 1
2060
+ # print (f"End: {scale_x} ({type(scale_x).__name__}), {scale_y} ({type(scale_y).__name__})")
2061
+
2062
+ bounds = Node.union_bounds(data)
2063
+ if px is None:
2064
+ px = (bounds[2] + bounds[0]) / 2.0
2065
+ if py is None:
2066
+ py = (bounds[3] + bounds[1]) / 2.0
2067
+ if scale_x == 0 or scale_y == 0:
2068
+ channel(_("Scaling by Zero Error"))
2069
+ return
2070
+ matrix = Matrix(f"scale({scale_x},{scale_y},{px},{py})")
2071
+ images = []
2072
+ with self.undoscope("Scale"):
2073
+ try:
2074
+ if not absolute:
2075
+ for node in data:
2076
+ if hasattr(node, "lock") and node.lock:
2077
+ continue
2078
+ node.matrix *= matrix
2079
+ node.scaled(sx=scale_x, sy=scale_y, ox=px, oy=py)
2080
+ if hasattr(node, "update"):
2081
+ images.append(node)
2082
+ else:
2083
+ for node in data:
2084
+ if hasattr(node, "lock") and node.lock:
2085
+ continue
2086
+ osx = node.matrix.value_scale_x()
2087
+ osy = node.matrix.value_scale_y()
2088
+ nsx = scale_x / osx
2089
+ nsy = scale_y / osy
2090
+ matrix = Matrix(f"scale({nsx},{nsy},{px},{px})")
2091
+ node.matrix *= matrix
2092
+ node.scaled(sx=nsx, sy=nsy, ox=px, oy=py)
2093
+ if hasattr(node, "update"):
2094
+ images.append(node)
2095
+ except ValueError:
2096
+ raise CommandSyntaxError
2097
+ for node in images:
2098
+ self.do_image_update(node)
2099
+ self.process_keyhole_updates(None)
2100
+ self.signal("refresh_scene", "Scene")
2101
+ self.signal("modified_by_tool")
2102
+ return "elements", data
2103
+
2104
+ @self.console_option(
2105
+ "new_area", "n", type=self.area, help=_("provide a new area to cover")
2106
+ )
2107
+ @self.console_option(
2108
+ "density", "d", type=int, help=_("Defines the interpolation density")
2109
+ )
2110
+ @self.console_command(
2111
+ "area",
2112
+ help=_("provides information about/changes the area of a selected element"),
2113
+ input_type=(None, "elements"),
2114
+ output_type="elements",
2115
+ )
2116
+ def element_area(
2117
+ command,
2118
+ channel,
2119
+ _,
2120
+ new_area=None,
2121
+ density=None,
2122
+ data=None,
2123
+ **kwargs,
2124
+ ):
2125
+ if density is None:
2126
+ density = 200
2127
+ if new_area is None:
2128
+ display_only = True
2129
+ else:
2130
+ if new_area == 0:
2131
+ channel(_("You shouldn't collapse a shape to a zero-sized thing"))
2132
+ return
2133
+ display_only = False
2134
+ if data is None:
2135
+ data = list(self.elems(emphasized=True))
2136
+ if len(data) == 0:
2137
+ channel(_("No selected elements."))
2138
+ return
2139
+ total_area = 0
2140
+ if display_only:
2141
+ channel("----------")
2142
+ channel(_("Area values (Density={density})").format(density=density))
2143
+
2144
+ units = ("mm", "cm", "in")
2145
+ square_unit = [0] * len(units)
2146
+ for idx, u in enumerate(units):
2147
+ value = float(Length(f"1{u}"))
2148
+ square_unit[idx] = value * value
2149
+
2150
+ i = 0
2151
+ for elem in data:
2152
+ try:
2153
+ geometry = elem.as_geometry()
2154
+ except AttributeError:
2155
+ continue
2156
+ # this_length = geometry.length()
2157
+ this_area = geometry.area(density=density)
2158
+
2159
+ if display_only:
2160
+ name = str(elem)
2161
+ if len(name) > 50:
2162
+ name = name[:50] + "…"
2163
+ channel(f"{i}: {name}")
2164
+ for idx, u in enumerate(units):
2165
+ this_area_local = this_area / square_unit[idx]
2166
+ channel(
2167
+ _(" Area= {area:.3f} {unit}²").format(
2168
+ area=this_area_local, unit=u
2169
+ )
2170
+ )
2171
+ i += 1
2172
+ total_area += this_area
2173
+ if display_only:
2174
+ channel("----------")
2175
+ else:
2176
+ if total_area == 0:
2177
+ channel(_("You can't reshape a zero-sized shape"))
2178
+ return
2179
+
2180
+ ratio = sqrt(new_area / total_area)
2181
+ self(f"scale {ratio}\n")
2182
+
2183
+ return "elements", data
2184
+ # Do we have a new value to set? If yes scale by sqrt(of the fraction)
2185
+
2186
+ @self.console_argument("tx", type=self.length_x, help=_("translate x value"))
2187
+ @self.console_argument("ty", type=self.length_y, help=_("translate y value"))
2188
+ @self.console_option(
2189
+ "absolute",
2190
+ "a",
2191
+ type=bool,
2192
+ action="store_true",
2193
+ help=_("translate to absolute position"),
2194
+ )
2195
+ @self.console_command(
2196
+ "translate",
2197
+ help=_("translate <tx> <ty>"),
2198
+ input_type=(None, "elements"),
2199
+ output_type="elements",
2200
+ )
2201
+ def element_translate(
2202
+ command, channel, _, tx, ty, absolute=False, data=None, **kwargs
2203
+ ):
2204
+ if tx is None:
2205
+ channel("----------")
2206
+ channel(_("Translate Values:"))
2207
+ i = 0
2208
+ for node in self.elems():
2209
+ name = str(node)
2210
+ if len(name) > 50:
2211
+ name = name[:50] + "…"
2212
+ channel(
2213
+ f"{i}: translate({node.matrix.value_trans_x():.1f}, {node.matrix.value_trans_y():.1f}) - {name}"
2214
+ )
2215
+ i += 1
2216
+ channel("----------")
2217
+ return
2218
+ if data is None:
2219
+ data = list(self.elems(emphasized=True))
2220
+ if len(data) == 0:
2221
+ channel(_("No selected elements."))
2222
+ return
2223
+ if tx is None:
2224
+ tx = 0
2225
+ if ty is None:
2226
+ ty = 0
2227
+ changes = False
2228
+ matrix = Matrix.translate(tx, ty)
2229
+ with self.undoscope("Translate"):
2230
+ try:
2231
+ if not absolute:
2232
+ for node in data:
2233
+ if not node.can_move(self.lock_allows_move):
2234
+ continue
2235
+
2236
+ node.matrix *= matrix
2237
+ node.translated(tx, ty)
2238
+ changes = True
2239
+ else:
2240
+ for node in data:
2241
+ if not node.can_move(self.lock_allows_move):
2242
+ continue
2243
+ otx = node.matrix.value_trans_x()
2244
+ oty = node.matrix.value_trans_y()
2245
+ ntx = tx - otx
2246
+ nty = ty - oty
2247
+ matrix = Matrix.translate(ntx, nty)
2248
+ node.matrix *= matrix
2249
+ node.translated(ntx, nty)
2250
+ changes = True
2251
+ except ValueError:
2252
+ raise CommandSyntaxError
2253
+ if changes:
2254
+ self.signal("refresh_scene", "Scene")
2255
+ self.signal("modified_by_tool")
2256
+ return "elements", data
2257
+
2258
+ @self.console_argument("tx", type=self.length_x, help=_("New x value"))
2259
+ @self.console_argument("ty", type=self.length_y, help=_("New y value"))
2260
+ @self.console_command(
2261
+ "position",
2262
+ help=_("position <tx> <ty>"),
2263
+ input_type=(None, "elements"),
2264
+ output_type="elements",
2265
+ )
2266
+ def element_position(
2267
+ command, channel, _, tx, ty, absolute=False, data=None, **kwargs
2268
+ ):
2269
+ if data is None:
2270
+ data = list(self.elems(emphasized=True))
2271
+ if len(data) == 0:
2272
+ channel(_("No selected elements."))
2273
+ return
2274
+ if tx is None or ty is None:
2275
+ channel(_("You need to provide a new position."))
2276
+ return
2277
+ with self.undoscope("Position"):
2278
+ changes = False
2279
+ dbounds = Node.union_bounds(data)
2280
+ for node in data:
2281
+ if not node.can_move(self.lock_allows_move):
2282
+ continue
2283
+ nbounds = node.bounds
2284
+ dx = tx - dbounds[0]
2285
+ dy = ty - dbounds[1]
2286
+ if dx != 0 or dy != 0:
2287
+ node.matrix.post_translate(dx, dy)
2288
+ # node.modified()
2289
+ node.translated(dx, dy)
2290
+ changes = True
2291
+ if changes:
2292
+ self.process_keyhole_updates(None)
2293
+ self.signal("refresh_scene", "Scene")
2294
+ self.signal("modified_by_tool")
2295
+ return "elements", data
2296
+
2297
+ @self.console_command(
2298
+ "move_to_laser",
2299
+ help=_("translates the selected element to the laser head"),
2300
+ input_type=(None, "elements"),
2301
+ output_type="elements",
2302
+ )
2303
+ def element_move_to_laser(command, channel, _, data=None, **kwargs):
2304
+ if data is None:
2305
+ data = list(self.elems(emphasized=True))
2306
+ if len(data) == 0:
2307
+ channel(_("No selected elements."))
2308
+ return
2309
+ tx, ty = self.device.current
2310
+ with self.undoscope("Translate to laser"):
2311
+ try:
2312
+ bounds = Node.union_bounds(data)
2313
+ otx = bounds[0]
2314
+ oty = bounds[1]
2315
+ ntx = tx - otx
2316
+ nty = ty - oty
2317
+ for node in data:
2318
+ if not node.can_move(self.lock_allows_move):
2319
+ continue
2320
+ node.matrix.post_translate(ntx, nty)
2321
+ # node.modified()
2322
+ node.translated(ntx, nty)
2323
+ except ValueError:
2324
+ raise CommandSyntaxError
2325
+ return "elements", data
2326
+
2327
+ @self.console_argument(
2328
+ "x_pos", type=self.length_x, help=_("x position for top left corner")
2329
+ )
2330
+ @self.console_argument(
2331
+ "y_pos", type=self.length_y, help=_("y position for top left corner")
2332
+ )
2333
+ @self.console_argument("width", type=self.length_x, help=_("new width of selected"))
2334
+ @self.console_argument(
2335
+ "height", type=self.length_y, help=_("new height of selected")
2336
+ )
2337
+ @self.console_command(
2338
+ "resize",
2339
+ help=_("resize <x-pos> <y-pos> <width> <height>"),
2340
+ input_type=(None, "elements"),
2341
+ output_type="elements",
2342
+ )
2343
+ def element_resize(
2344
+ command, channel, _, x_pos, y_pos, width, height, data=None, **kwargs
2345
+ ):
2346
+ if height is None:
2347
+ raise CommandSyntaxError
2348
+ if data is None:
2349
+ data = list(self.elems(emphasized=True))
2350
+ if len(data) == 0:
2351
+ channel(_("No selected elements."))
2352
+ return
2353
+ area = Node.union_bounds(data)
2354
+ if area is None:
2355
+ channel(_("resize: nothing selected"))
2356
+ return
2357
+ x, y, x1, y1 = area
2358
+ w, h = x1 - x, y1 - y
2359
+ if w == 0 or h == 0: # dot
2360
+ channel(_("resize: cannot resize a dot"))
2361
+ return
2362
+ sx = width / w
2363
+ sy = height / h
2364
+ if sx == 0 or sy == 0:
2365
+ channel(_("Invalid width/height"))
2366
+ return
2367
+ px = area[0]
2368
+ py = area[2]
2369
+ matrix = Matrix(f"scale({sx},{sy},{px},{py})")
2370
+ with self.undoscope("Resize"):
2371
+ images = []
2372
+ if sx != 1.0 or sy != 1.0:
2373
+ # Don't do anything if scale is 1
2374
+ for node in data:
2375
+ if not hasattr(node, "matrix"):
2376
+ continue
2377
+ if hasattr(node, "lock") and node.lock:
2378
+ continue
2379
+ node.matrix *= matrix
2380
+ node.modified()
2381
+ if hasattr(node, "update") and node not in images:
2382
+ images.append(node)
2383
+
2384
+ # Calculate again
2385
+ area = Node.union_bounds(data)
2386
+ dx = x_pos - area[0]
2387
+ dy = y_pos - area[1]
2388
+ if dx != 0.0 or dy != 0.0:
2389
+ # Don't do anything if scale is 1
2390
+ for node in data:
2391
+ if not hasattr(node, "matrix"):
2392
+ continue
2393
+ node.matrix.post_translate(dx, dy)
2394
+ node.modified()
2395
+ if hasattr(node, "update") and node not in images:
2396
+ images.append(node)
2397
+
2398
+ for node in images:
2399
+ self.do_image_update(node)
2400
+ self.signal("refresh_scene", "Scene")
2401
+ return "elements", data
2402
+
2403
+ @self.console_argument("sx", type=float, help=_("scale_x value"))
2404
+ @self.console_argument("kx", type=float, help=_("skew_x value"))
2405
+ @self.console_argument("ky", type=float, help=_("skew_y value"))
2406
+ @self.console_argument("sy", type=float, help=_("scale_y value"))
2407
+ @self.console_argument("tx", type=self.length_x, help=_("translate_x value"))
2408
+ @self.console_argument("ty", type=self.length_y, help=_("translate_y value"))
2409
+ @self.console_command(
2410
+ "matrix",
2411
+ help=_("matrix <sx> <kx> <ky> <sy> <tx> <ty>"),
2412
+ input_type=(None, "elements"),
2413
+ output_type="elements",
2414
+ )
2415
+ def element_matrix(
2416
+ command, channel, _, sx, kx, ky, sy, tx, ty, data=None, **kwargs
2417
+ ):
2418
+ if data is None:
2419
+ data = list(self.elems(emphasized=True))
2420
+ if ty is None:
2421
+ channel("----------")
2422
+ channel(_("Matrix Values:"))
2423
+ i = 0
2424
+ for node in data:
2425
+ name = str(node)
2426
+ if len(name) > 50:
2427
+ name = name[:50] + "…"
2428
+ channel(f"{i}: {str(node.matrix)} - {name}")
2429
+ i += 1
2430
+ channel("----------")
2431
+ return
2432
+ if len(data) == 0:
2433
+ channel(_("No selected elements."))
2434
+ return
2435
+ with self.undoscope("Matrix"):
2436
+ images = []
2437
+ try:
2438
+ # SVG 7.15.3 defines the matrix form as:
2439
+ # [a c e]
2440
+ # [b d f]
2441
+ m = Matrix(
2442
+ sx,
2443
+ kx,
2444
+ ky,
2445
+ sy,
2446
+ tx,
2447
+ ty,
2448
+ )
2449
+ for node in data:
2450
+ if hasattr(node, "lock") and node.lock:
2451
+ continue
2452
+ node.matrix = Matrix(m)
2453
+ node.modified()
2454
+ if hasattr(node, "update"):
2455
+ images.append(node)
2456
+ except ValueError:
2457
+ raise CommandSyntaxError
2458
+ for node in images:
2459
+ self.do_image_update(node)
2460
+ self.signal("refresh_scene", "Scene")
2461
+ return
2462
+
2463
+ @self.console_command(
2464
+ "reset",
2465
+ help=_("reset affine transformations"),
2466
+ input_type=(None, "elements"),
2467
+ output_type="elements",
2468
+ )
2469
+ def reset(command, channel, _, data=None, **kwargs):
2470
+ if data is None:
2471
+ data = list(self.elems(emphasized=True))
2472
+ with self.undoscope("Reset"):
2473
+ images = []
2474
+ for e in data:
2475
+ if hasattr(e, "lock") and e.lock:
2476
+ continue
2477
+ name = str(e)
2478
+ if len(name) > 50:
2479
+ name = name[:50] + "…"
2480
+ channel(_("reset - {name}").format(name=name))
2481
+ e.matrix.reset()
2482
+ e.modified()
2483
+ if hasattr(e, "update"):
2484
+ images.append(e)
2485
+ for e in images:
2486
+ self.do_image_update(e)
2487
+ self.signal("refresh_scene", "Scene")
2488
+ return "elements", data
2489
+
2490
+ @self.console_command(
2491
+ "reify",
2492
+ help=_("reify affine transformations"),
2493
+ input_type=(None, "elements"),
2494
+ output_type="elements",
2495
+ )
2496
+ def element_reify(command, channel, _, data=None, **kwargs):
2497
+ if data is None:
2498
+ data = list(self.elems(emphasized=True))
2499
+ with self.undoscope("Reify"):
2500
+ for e in data:
2501
+ try:
2502
+ if e.lock:
2503
+ continue
2504
+ except AttributeError:
2505
+ pass
2506
+
2507
+ name = str(e)
2508
+ if len(name) > 50:
2509
+ name = name[:50] + "…"
2510
+ try:
2511
+ e.stroke_reify()
2512
+ except AttributeError:
2513
+ pass
2514
+
2515
+ try:
2516
+ e.shape.reify()
2517
+ except AttributeError as err:
2518
+ try:
2519
+ e.path.reify()
2520
+ except AttributeError:
2521
+ channel(_("Couldn't reify - %s - %s") % (name, err))
2522
+ return "elements", data
2523
+ try:
2524
+ e.stroke_width_zero()
2525
+ except AttributeError:
2526
+ pass
2527
+ e.altered()
2528
+ channel(_("reified - %s") % name)
2529
+ return "elements", data
2530
+
2531
+ @self.console_command(
2532
+ "circle_arc_path",
2533
+ help=_("Convert paths to use circular arcs."),
2534
+ input_type=(None, "elements"),
2535
+ output_type="elements",
2536
+ )
2537
+ def element_circ_arc_path(command, channel, _, data=None, **kwargs):
2538
+ if data is None:
2539
+ data = list(self.elems(emphasized=True))
2540
+ # _("Convert paths")
2541
+ with self.undoscope("Convert paths"):
2542
+ for e in data:
2543
+ try:
2544
+ if e.lock:
2545
+ continue
2546
+ except AttributeError:
2547
+ pass
2548
+ if e.type == "elem path":
2549
+ g = e.geometry
2550
+ path = g.as_path()
2551
+ path.approximate_bezier_with_circular_arcs()
2552
+ e.geometry = Geomstr.svg(path)
2553
+ e.altered()
2554
+
2555
+ return "elements", data
2556
+
2557
+ @self.console_command(
2558
+ "classify",
2559
+ help=_("classify elements into operations"),
2560
+ input_type=(None, "elements"),
2561
+ output_type="elements",
2562
+ )
2563
+ def element_classify(command, channel, _, data=None, **kwargs):
2564
+ if data is None:
2565
+ data = list(self.elems(emphasized=True))
2566
+ was_emphasized = True
2567
+ old_first = self.first_emphasized
2568
+ else:
2569
+ was_emphasized = False
2570
+ old_first = None
2571
+ if len(data) == 0:
2572
+ channel(_("No selected elements."))
2573
+ return
2574
+ with self.undoscope("Classify"):
2575
+ self.classify(data)
2576
+ if was_emphasized:
2577
+ for e in data:
2578
+ e.emphasized = True
2579
+ if len(data) == 1:
2580
+ data[0].focus()
2581
+ if old_first is not None and old_first in data:
2582
+ self.first_emphasized = old_first
2583
+ else:
2584
+ self.first_emphasized = None
2585
+
2586
+ return "elements", data
2587
+
2588
+ @self.console_command(
2589
+ "declassify",
2590
+ help=_("declassify selected elements"),
2591
+ input_type=(None, "elements"),
2592
+ output_type="elements",
2593
+ )
2594
+ def declassify(command, channel, _, data=None, **kwargs):
2595
+ if data is None:
2596
+ data = list(self.elems(emphasized=True))
2597
+ was_emphasized = True
2598
+ old_first = self.first_emphasized
2599
+ else:
2600
+ was_emphasized = False
2601
+ old_first = None
2602
+ if len(data) == 0:
2603
+ channel(_("No selected elements."))
2604
+ return
2605
+ self.remove_elements_from_operations(data)
2606
+ # restore emphasized flag as it is relevant for subsequent operations
2607
+ if was_emphasized:
2608
+ for e in data:
2609
+ e.emphasized = True
2610
+ if len(data) == 1:
2611
+ data[0].focus()
2612
+ if old_first is not None and old_first in data:
2613
+ self.first_emphasized = old_first
2614
+ else:
2615
+ self.first_emphasized = None
2616
+ return "elements", data
2617
+
2618
+ # --------------------------- END COMMANDS ------------------------------