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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (445) hide show
  1. meerk40t/__init__.py +1 -1
  2. meerk40t/balormk/balor_params.py +167 -167
  3. meerk40t/balormk/clone_loader.py +457 -457
  4. meerk40t/balormk/controller.py +1566 -1512
  5. meerk40t/balormk/cylindermod.py +64 -0
  6. meerk40t/balormk/device.py +966 -1959
  7. meerk40t/balormk/driver.py +778 -591
  8. meerk40t/balormk/galvo_commands.py +1195 -0
  9. meerk40t/balormk/gui/balorconfig.py +237 -111
  10. meerk40t/balormk/gui/balorcontroller.py +191 -184
  11. meerk40t/balormk/gui/baloroperationproperties.py +116 -115
  12. meerk40t/balormk/gui/corscene.py +845 -0
  13. meerk40t/balormk/gui/gui.py +179 -147
  14. meerk40t/balormk/livelightjob.py +466 -382
  15. meerk40t/balormk/mock_connection.py +131 -109
  16. meerk40t/balormk/plugin.py +133 -135
  17. meerk40t/balormk/usb_connection.py +306 -301
  18. meerk40t/camera/__init__.py +1 -1
  19. meerk40t/camera/camera.py +514 -397
  20. meerk40t/camera/gui/camerapanel.py +1241 -1095
  21. meerk40t/camera/gui/gui.py +58 -58
  22. meerk40t/camera/plugin.py +441 -399
  23. meerk40t/ch341/__init__.py +27 -27
  24. meerk40t/ch341/ch341device.py +628 -628
  25. meerk40t/ch341/libusb.py +595 -589
  26. meerk40t/ch341/mock.py +171 -171
  27. meerk40t/ch341/windriver.py +157 -157
  28. meerk40t/constants.py +13 -0
  29. meerk40t/core/__init__.py +1 -1
  30. meerk40t/core/bindalias.py +550 -539
  31. meerk40t/core/core.py +47 -47
  32. meerk40t/core/cutcode/cubiccut.py +73 -73
  33. meerk40t/core/cutcode/cutcode.py +315 -312
  34. meerk40t/core/cutcode/cutgroup.py +141 -137
  35. meerk40t/core/cutcode/cutobject.py +192 -185
  36. meerk40t/core/cutcode/dwellcut.py +37 -37
  37. meerk40t/core/cutcode/gotocut.py +29 -29
  38. meerk40t/core/cutcode/homecut.py +29 -29
  39. meerk40t/core/cutcode/inputcut.py +34 -34
  40. meerk40t/core/cutcode/linecut.py +33 -33
  41. meerk40t/core/cutcode/outputcut.py +34 -34
  42. meerk40t/core/cutcode/plotcut.py +335 -335
  43. meerk40t/core/cutcode/quadcut.py +61 -61
  44. meerk40t/core/cutcode/rastercut.py +168 -148
  45. meerk40t/core/cutcode/waitcut.py +34 -34
  46. meerk40t/core/cutplan.py +1843 -1316
  47. meerk40t/core/drivers.py +330 -329
  48. meerk40t/core/elements/align.py +801 -669
  49. meerk40t/core/elements/branches.py +1844 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4561 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4329 -3617
  54. meerk40t/core/elements/files.py +117 -64
  55. meerk40t/core/elements/geometry.py +473 -224
  56. meerk40t/core/elements/grid.py +467 -316
  57. meerk40t/core/elements/materials.py +158 -94
  58. meerk40t/core/elements/notes.py +50 -38
  59. meerk40t/core/elements/offset_clpr.py +933 -912
  60. meerk40t/core/elements/offset_mk.py +963 -955
  61. meerk40t/core/elements/penbox.py +339 -267
  62. meerk40t/core/elements/placements.py +300 -83
  63. meerk40t/core/elements/render.py +785 -687
  64. meerk40t/core/elements/shapes.py +2618 -2092
  65. meerk40t/core/elements/trace.py +651 -563
  66. meerk40t/core/elements/tree_commands.py +415 -409
  67. meerk40t/core/elements/undo_redo.py +116 -58
  68. meerk40t/core/elements/wordlist.py +319 -200
  69. meerk40t/core/exceptions.py +9 -9
  70. meerk40t/core/laserjob.py +220 -220
  71. meerk40t/core/logging.py +63 -63
  72. meerk40t/core/node/blobnode.py +83 -86
  73. meerk40t/core/node/bootstrap.py +105 -103
  74. meerk40t/core/node/branch_elems.py +40 -31
  75. meerk40t/core/node/branch_ops.py +45 -38
  76. meerk40t/core/node/branch_regmark.py +48 -41
  77. meerk40t/core/node/cutnode.py +29 -32
  78. meerk40t/core/node/effect_hatch.py +375 -257
  79. meerk40t/core/node/effect_warp.py +398 -0
  80. meerk40t/core/node/effect_wobble.py +441 -309
  81. meerk40t/core/node/elem_ellipse.py +404 -309
  82. meerk40t/core/node/elem_image.py +1082 -801
  83. meerk40t/core/node/elem_line.py +358 -292
  84. meerk40t/core/node/elem_path.py +259 -201
  85. meerk40t/core/node/elem_point.py +129 -102
  86. meerk40t/core/node/elem_polyline.py +310 -246
  87. meerk40t/core/node/elem_rect.py +376 -286
  88. meerk40t/core/node/elem_text.py +445 -418
  89. meerk40t/core/node/filenode.py +59 -40
  90. meerk40t/core/node/groupnode.py +138 -74
  91. meerk40t/core/node/image_processed.py +777 -766
  92. meerk40t/core/node/image_raster.py +156 -113
  93. meerk40t/core/node/layernode.py +31 -31
  94. meerk40t/core/node/mixins.py +135 -107
  95. meerk40t/core/node/node.py +1427 -1304
  96. meerk40t/core/node/nutils.py +117 -114
  97. meerk40t/core/node/op_cut.py +462 -335
  98. meerk40t/core/node/op_dots.py +296 -251
  99. meerk40t/core/node/op_engrave.py +414 -311
  100. meerk40t/core/node/op_image.py +755 -369
  101. meerk40t/core/node/op_raster.py +787 -522
  102. meerk40t/core/node/place_current.py +37 -40
  103. meerk40t/core/node/place_point.py +329 -126
  104. meerk40t/core/node/refnode.py +58 -47
  105. meerk40t/core/node/rootnode.py +225 -219
  106. meerk40t/core/node/util_console.py +48 -48
  107. meerk40t/core/node/util_goto.py +84 -65
  108. meerk40t/core/node/util_home.py +61 -61
  109. meerk40t/core/node/util_input.py +102 -102
  110. meerk40t/core/node/util_output.py +102 -102
  111. meerk40t/core/node/util_wait.py +65 -65
  112. meerk40t/core/parameters.py +709 -707
  113. meerk40t/core/planner.py +875 -785
  114. meerk40t/core/plotplanner.py +656 -652
  115. meerk40t/core/space.py +120 -113
  116. meerk40t/core/spoolers.py +706 -705
  117. meerk40t/core/svg_io.py +1836 -1549
  118. meerk40t/core/treeop.py +534 -445
  119. meerk40t/core/undos.py +278 -124
  120. meerk40t/core/units.py +784 -680
  121. meerk40t/core/view.py +393 -322
  122. meerk40t/core/webhelp.py +62 -62
  123. meerk40t/core/wordlist.py +513 -504
  124. meerk40t/cylinder/cylinder.py +247 -0
  125. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  126. meerk40t/cylinder/gui/gui.py +24 -0
  127. meerk40t/device/__init__.py +1 -1
  128. meerk40t/device/basedevice.py +322 -123
  129. meerk40t/device/devicechoices.py +50 -0
  130. meerk40t/device/dummydevice.py +163 -128
  131. meerk40t/device/gui/defaultactions.py +618 -602
  132. meerk40t/device/gui/effectspanel.py +114 -0
  133. meerk40t/device/gui/formatterpanel.py +253 -290
  134. meerk40t/device/gui/warningpanel.py +337 -260
  135. meerk40t/device/mixins.py +13 -13
  136. meerk40t/dxf/__init__.py +1 -1
  137. meerk40t/dxf/dxf_io.py +766 -554
  138. meerk40t/dxf/plugin.py +47 -35
  139. meerk40t/external_plugins.py +79 -79
  140. meerk40t/external_plugins_build.py +28 -28
  141. meerk40t/extra/cag.py +112 -116
  142. meerk40t/extra/coolant.py +403 -0
  143. meerk40t/extra/encode_detect.py +198 -0
  144. meerk40t/extra/ezd.py +1165 -1165
  145. meerk40t/extra/hershey.py +835 -340
  146. meerk40t/extra/imageactions.py +322 -316
  147. meerk40t/extra/inkscape.py +630 -622
  148. meerk40t/extra/lbrn.py +424 -424
  149. meerk40t/extra/outerworld.py +284 -0
  150. meerk40t/extra/param_functions.py +1542 -1556
  151. meerk40t/extra/potrace.py +257 -253
  152. meerk40t/extra/serial_exchange.py +118 -0
  153. meerk40t/extra/updater.py +602 -453
  154. meerk40t/extra/vectrace.py +147 -146
  155. meerk40t/extra/winsleep.py +83 -83
  156. meerk40t/extra/xcs_reader.py +597 -0
  157. meerk40t/fill/fills.py +781 -335
  158. meerk40t/fill/patternfill.py +1061 -1061
  159. meerk40t/fill/patterns.py +614 -567
  160. meerk40t/grbl/control.py +87 -87
  161. meerk40t/grbl/controller.py +990 -903
  162. meerk40t/grbl/device.py +1081 -768
  163. meerk40t/grbl/driver.py +989 -771
  164. meerk40t/grbl/emulator.py +532 -497
  165. meerk40t/grbl/gcodejob.py +783 -767
  166. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  167. meerk40t/grbl/gui/grblcontroller.py +485 -271
  168. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  169. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  170. meerk40t/grbl/gui/gui.py +147 -116
  171. meerk40t/grbl/interpreter.py +44 -44
  172. meerk40t/grbl/loader.py +22 -22
  173. meerk40t/grbl/mock_connection.py +56 -56
  174. meerk40t/grbl/plugin.py +294 -264
  175. meerk40t/grbl/serial_connection.py +93 -88
  176. meerk40t/grbl/tcp_connection.py +81 -79
  177. meerk40t/grbl/ws_connection.py +112 -0
  178. meerk40t/gui/__init__.py +1 -1
  179. meerk40t/gui/about.py +2042 -296
  180. meerk40t/gui/alignment.py +1644 -1608
  181. meerk40t/gui/autoexec.py +199 -0
  182. meerk40t/gui/basicops.py +791 -670
  183. meerk40t/gui/bufferview.py +77 -71
  184. meerk40t/gui/busy.py +170 -133
  185. meerk40t/gui/choicepropertypanel.py +1673 -1469
  186. meerk40t/gui/consolepanel.py +706 -542
  187. meerk40t/gui/devicepanel.py +687 -581
  188. meerk40t/gui/dialogoptions.py +110 -107
  189. meerk40t/gui/executejob.py +316 -306
  190. meerk40t/gui/fonts.py +90 -90
  191. meerk40t/gui/functionwrapper.py +252 -0
  192. meerk40t/gui/gui_mixins.py +729 -0
  193. meerk40t/gui/guicolors.py +205 -182
  194. meerk40t/gui/help_assets/help_assets.py +218 -201
  195. meerk40t/gui/helper.py +154 -0
  196. meerk40t/gui/hersheymanager.py +1430 -846
  197. meerk40t/gui/icons.py +3422 -2747
  198. meerk40t/gui/imagesplitter.py +555 -508
  199. meerk40t/gui/keymap.py +354 -344
  200. meerk40t/gui/laserpanel.py +892 -806
  201. meerk40t/gui/laserrender.py +1470 -1232
  202. meerk40t/gui/lasertoolpanel.py +805 -793
  203. meerk40t/gui/magnetoptions.py +436 -0
  204. meerk40t/gui/materialmanager.py +2917 -0
  205. meerk40t/gui/materialtest.py +1722 -1694
  206. meerk40t/gui/mkdebug.py +646 -359
  207. meerk40t/gui/mwindow.py +163 -140
  208. meerk40t/gui/navigationpanels.py +2605 -2467
  209. meerk40t/gui/notes.py +143 -142
  210. meerk40t/gui/opassignment.py +414 -410
  211. meerk40t/gui/operation_info.py +310 -299
  212. meerk40t/gui/plugin.py +494 -328
  213. meerk40t/gui/position.py +714 -669
  214. meerk40t/gui/preferences.py +901 -650
  215. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  216. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  217. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  218. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  219. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  220. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  221. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  222. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  223. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  224. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  225. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  226. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  227. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  228. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  229. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  230. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  231. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  232. meerk40t/gui/propertypanels/textproperty.py +770 -755
  233. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  234. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  235. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  236. meerk40t/gui/ribbon.py +2468 -2210
  237. meerk40t/gui/scene/scene.py +1100 -1051
  238. meerk40t/gui/scene/sceneconst.py +22 -22
  239. meerk40t/gui/scene/scenepanel.py +439 -349
  240. meerk40t/gui/scene/scenespacewidget.py +365 -365
  241. meerk40t/gui/scene/widget.py +518 -505
  242. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  243. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  244. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  245. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  246. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  247. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  248. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  249. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  250. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  251. meerk40t/gui/scenewidgets/rectselectwidget.py +589 -346
  252. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  253. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  254. meerk40t/gui/scenewidgets/selectionwidget.py +2952 -2756
  255. meerk40t/gui/simpleui.py +357 -333
  256. meerk40t/gui/simulation.py +2431 -2094
  257. meerk40t/gui/snapoptions.py +208 -203
  258. meerk40t/gui/spoolerpanel.py +1227 -1180
  259. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  260. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  261. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  262. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  263. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  264. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  265. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  266. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  267. meerk40t/gui/themes.py +200 -78
  268. meerk40t/gui/tips.py +591 -0
  269. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  270. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  271. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  272. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  273. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  274. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  275. meerk40t/gui/toolwidgets/toolline.py +39 -144
  276. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  277. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  278. meerk40t/gui/toolwidgets/toolmeasure.py +160 -216
  279. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  280. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  281. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  282. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  283. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  284. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  285. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  286. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  287. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  288. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  289. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  290. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  291. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  292. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  293. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  294. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  295. meerk40t/gui/usbconnect.py +98 -91
  296. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  297. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  298. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  299. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  300. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  301. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  302. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  303. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  304. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  305. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  306. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  307. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  308. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  309. meerk40t/gui/wordlisteditor.py +985 -931
  310. meerk40t/gui/wxmeerk40t.py +1444 -1169
  311. meerk40t/gui/wxmmain.py +5578 -4112
  312. meerk40t/gui/wxmribbon.py +1591 -1076
  313. meerk40t/gui/wxmscene.py +1635 -1453
  314. meerk40t/gui/wxmtree.py +2410 -2089
  315. meerk40t/gui/wxutils.py +1769 -1099
  316. meerk40t/gui/zmatrix.py +102 -102
  317. meerk40t/image/__init__.py +1 -1
  318. meerk40t/image/dither.py +429 -0
  319. meerk40t/image/imagetools.py +2778 -2269
  320. meerk40t/internal_plugins.py +150 -130
  321. meerk40t/kernel/__init__.py +63 -12
  322. meerk40t/kernel/channel.py +259 -212
  323. meerk40t/kernel/context.py +538 -538
  324. meerk40t/kernel/exceptions.py +41 -41
  325. meerk40t/kernel/functions.py +463 -414
  326. meerk40t/kernel/jobs.py +100 -100
  327. meerk40t/kernel/kernel.py +3809 -3571
  328. meerk40t/kernel/lifecycles.py +71 -71
  329. meerk40t/kernel/module.py +49 -49
  330. meerk40t/kernel/service.py +147 -147
  331. meerk40t/kernel/settings.py +383 -343
  332. meerk40t/lihuiyu/controller.py +883 -876
  333. meerk40t/lihuiyu/device.py +1181 -1069
  334. meerk40t/lihuiyu/driver.py +1466 -1372
  335. meerk40t/lihuiyu/gui/gui.py +127 -106
  336. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  337. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  338. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  339. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  340. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  341. meerk40t/lihuiyu/interpreter.py +53 -53
  342. meerk40t/lihuiyu/laserspeed.py +450 -450
  343. meerk40t/lihuiyu/loader.py +90 -90
  344. meerk40t/lihuiyu/parser.py +404 -404
  345. meerk40t/lihuiyu/plugin.py +101 -102
  346. meerk40t/lihuiyu/tcp_connection.py +111 -109
  347. meerk40t/main.py +231 -165
  348. meerk40t/moshi/builder.py +788 -781
  349. meerk40t/moshi/controller.py +505 -499
  350. meerk40t/moshi/device.py +495 -442
  351. meerk40t/moshi/driver.py +862 -696
  352. meerk40t/moshi/gui/gui.py +78 -76
  353. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  354. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  355. meerk40t/moshi/plugin.py +43 -43
  356. meerk40t/network/console_server.py +102 -57
  357. meerk40t/network/kernelserver.py +10 -9
  358. meerk40t/network/tcp_server.py +142 -140
  359. meerk40t/network/udp_server.py +103 -77
  360. meerk40t/network/web_server.py +390 -0
  361. meerk40t/newly/controller.py +1158 -1144
  362. meerk40t/newly/device.py +874 -732
  363. meerk40t/newly/driver.py +540 -412
  364. meerk40t/newly/gui/gui.py +219 -188
  365. meerk40t/newly/gui/newlyconfig.py +116 -101
  366. meerk40t/newly/gui/newlycontroller.py +193 -186
  367. meerk40t/newly/gui/operationproperties.py +51 -51
  368. meerk40t/newly/mock_connection.py +82 -82
  369. meerk40t/newly/newly_params.py +56 -56
  370. meerk40t/newly/plugin.py +1214 -1246
  371. meerk40t/newly/usb_connection.py +322 -322
  372. meerk40t/rotary/gui/gui.py +52 -46
  373. meerk40t/rotary/gui/rotarysettings.py +240 -232
  374. meerk40t/rotary/rotary.py +202 -98
  375. meerk40t/ruida/control.py +291 -91
  376. meerk40t/ruida/controller.py +138 -1088
  377. meerk40t/ruida/device.py +672 -231
  378. meerk40t/ruida/driver.py +534 -472
  379. meerk40t/ruida/emulator.py +1494 -1491
  380. meerk40t/ruida/exceptions.py +4 -4
  381. meerk40t/ruida/gui/gui.py +71 -76
  382. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  383. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  384. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  385. meerk40t/ruida/loader.py +54 -52
  386. meerk40t/ruida/mock_connection.py +57 -109
  387. meerk40t/ruida/plugin.py +124 -87
  388. meerk40t/ruida/rdjob.py +2084 -945
  389. meerk40t/ruida/serial_connection.py +116 -0
  390. meerk40t/ruida/tcp_connection.py +146 -0
  391. meerk40t/ruida/udp_connection.py +73 -0
  392. meerk40t/svgelements.py +9671 -9669
  393. meerk40t/tools/driver_to_path.py +584 -579
  394. meerk40t/tools/geomstr.py +5583 -4680
  395. meerk40t/tools/jhfparser.py +357 -292
  396. meerk40t/tools/kerftest.py +904 -890
  397. meerk40t/tools/livinghinges.py +1168 -1033
  398. meerk40t/tools/pathtools.py +987 -949
  399. meerk40t/tools/pmatrix.py +234 -0
  400. meerk40t/tools/pointfinder.py +942 -942
  401. meerk40t/tools/polybool.py +940 -940
  402. meerk40t/tools/rasterplotter.py +1660 -547
  403. meerk40t/tools/shxparser.py +989 -901
  404. meerk40t/tools/ttfparser.py +726 -446
  405. meerk40t/tools/zinglplotter.py +595 -593
  406. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/LICENSE +21 -21
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/METADATA +150 -139
  408. meerk40t-0.9.7010.dist-info/RECORD +445 -0
  409. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/WHEEL +1 -1
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/top_level.txt +0 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/zip-safe +1 -1
  412. meerk40t/balormk/elementlightjob.py +0 -159
  413. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  414. test/bootstrap.py +0 -63
  415. test/test_cli.py +0 -12
  416. test/test_core_cutcode.py +0 -418
  417. test/test_core_elements.py +0 -144
  418. test/test_core_plotplanner.py +0 -397
  419. test/test_core_viewports.py +0 -312
  420. test/test_drivers_grbl.py +0 -108
  421. test/test_drivers_lihuiyu.py +0 -443
  422. test/test_drivers_newly.py +0 -113
  423. test/test_element_degenerate_points.py +0 -43
  424. test/test_elements_classify.py +0 -97
  425. test/test_elements_penbox.py +0 -22
  426. test/test_file_svg.py +0 -176
  427. test/test_fill.py +0 -155
  428. test/test_geomstr.py +0 -1523
  429. test/test_geomstr_nodes.py +0 -18
  430. test/test_imagetools_actualize.py +0 -306
  431. test/test_imagetools_wizard.py +0 -258
  432. test/test_kernel.py +0 -200
  433. test/test_laser_speeds.py +0 -3303
  434. test/test_length.py +0 -57
  435. test/test_lifecycle.py +0 -66
  436. test/test_operations.py +0 -251
  437. test/test_operations_hatch.py +0 -57
  438. test/test_ruida.py +0 -19
  439. test/test_spooler.py +0 -22
  440. test/test_tools_rasterplotter.py +0 -29
  441. test/test_wobble.py +0 -133
  442. test/test_zingl.py +0 -124
  443. {test → meerk40t/cylinder}/__init__.py +0 -0
  444. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  445. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/entry_points.txt +0 -0
@@ -1,1384 +1,2244 @@
1
- # import threading
2
- import wx
3
- from PIL import Image
4
-
5
- from meerk40t.core.node.elem_path import PathNode
6
- from meerk40t.core.units import UNITS_PER_INCH
7
-
8
- # from meerk40t.gui.icons import icons8_image
9
- # from meerk40t.gui.mwindow import MWindow
10
- from meerk40t.gui.propertypanels.attributes import (
11
- IdPanel,
12
- PositionSizePanel,
13
- PreventChangePanel,
14
- )
15
- from meerk40t.gui.wxutils import ScrolledPanel, StaticBoxSizer, TextCtrl, dip_size
16
- from meerk40t.svgelements import Matrix
17
-
18
- _ = wx.GetTranslation
19
-
20
- # The default value needs to be true, as the static method will be called before init happened...
21
- HAS_VECTOR_ENGINE = True
22
-
23
-
24
- class CropPanel(wx.Panel):
25
- name = _("Crop")
26
- priority = 5
27
-
28
- @staticmethod
29
- def accepts(node):
30
- if node.type != "elem image":
31
- return False
32
- for n in node.operations:
33
- if n.get("name") == "crop":
34
- return True
35
- return False
36
-
37
- def __init__(self, *args, context=None, node=None, **kwds):
38
- kwds["style"] = kwds.get("style", 0) | wx.TAB_TRAVERSAL
39
- wx.Panel.__init__(self, *args, **kwds)
40
- self.context = context
41
- self.node = node
42
- self._width = None
43
- self._height = None
44
- self._bounds = None
45
- self.op = None
46
- self._no_update = False
47
-
48
- self.check_enable_crop = wx.CheckBox(self, wx.ID_ANY, _("Enable"))
49
- self.button_reset = wx.Button(self, wx.ID_ANY, _("Reset"))
50
-
51
- self.label_info = wx.StaticText(self, wx.ID_ANY, "--")
52
-
53
- self.slider_left = wx.Slider(
54
- self, wx.ID_ANY, 0, -127, 127, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
55
- )
56
- self.slider_right = wx.Slider(
57
- self, wx.ID_ANY, 0, -127, 127, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
58
- )
59
- self.slider_top = wx.Slider(
60
- self, wx.ID_ANY, 0, -127, 127, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
61
- )
62
- self.slider_bottom = wx.Slider(
63
- self, wx.ID_ANY, 0, -127, 127, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
64
- )
65
- self.text_left = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_READONLY)
66
- self.text_right = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_READONLY)
67
- self.text_top = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_READONLY)
68
- self.text_bottom = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_READONLY)
69
-
70
- self.__set_properties()
71
- self.__do_layout()
72
-
73
- self.Bind(wx.EVT_BUTTON, self.on_button_reset, self.button_reset)
74
- self.Bind(wx.EVT_CHECKBOX, self.on_check_enable_crop, self.check_enable_crop)
75
- self.Bind(wx.EVT_SLIDER, self.on_slider_left, self.slider_left)
76
- self.Bind(wx.EVT_SLIDER, self.on_slider_right, self.slider_right)
77
- self.Bind(wx.EVT_SLIDER, self.on_slider_top, self.slider_top)
78
- self.Bind(wx.EVT_SLIDER, self.on_slider_bottom, self.slider_bottom)
79
-
80
- flag = False
81
- self.activate_controls(flag)
82
- self.set_widgets(node)
83
-
84
- def activate_controls(self, flag):
85
- self.button_reset.Enable(flag)
86
- self.slider_left.Enable(flag)
87
- self.slider_right.Enable(flag)
88
- self.slider_top.Enable(flag)
89
- self.slider_bottom.Enable(flag)
90
-
91
- def set_widgets(self, node):
92
- if self.node is None:
93
- self.label_info.SetLabel("")
94
- self.Hide()
95
- return
96
- else:
97
- self.Show()
98
- self._no_update = True
99
- self.node = node
100
- self.op = None
101
- self._width, self._height = self.node.image.size
102
- self._bounds = [0, 0, self._width, self._height]
103
- flag = False
104
- for n in node.operations:
105
- if n.get("name") == "crop":
106
- self.op = n
107
- break
108
- self._width, self._height = self.node.image.size
109
- self.label_info.SetLabel(f"{self._width} x {self._height} px")
110
- if self.op is not None:
111
- flag = self.op["enable"]
112
- self._bounds = self.op["bounds"]
113
- if self._bounds is None:
114
- self._bounds = [0, 0, self._width, self._height]
115
- self.op["bounds"] = self._bounds
116
-
117
- self.set_slider_limits("lrtb", False)
118
-
119
- self.check_enable_crop.SetValue(flag)
120
- self.activate_controls(flag)
121
-
122
- self.cropleft = self._bounds[0]
123
- self.cropright = self._bounds[2]
124
- self.croptop = self._bounds[1]
125
- self.cropbottom = self._bounds[3]
126
- self._no_update = False
127
-
128
- def __set_properties(self):
129
- self.slider_left.SetToolTip(
130
- _("How many pixels do you want to crop from the left?")
131
- )
132
- self.slider_right.SetToolTip(
133
- _("How many pixels do you want to crop from the right?")
134
- )
135
- self.slider_top.SetToolTip(
136
- _("How many pixels do you want to crop from the top?")
137
- )
138
- self.slider_bottom.SetToolTip(
139
- _("How many pixels do you want to crop from the bottom?")
140
- )
141
-
142
- def __do_layout(self):
143
- # begin wxGlade: ContrastPanel.__do_layout
144
- sizer_main = StaticBoxSizer(self, wx.ID_ANY, _("Image-Dimensions"), wx.VERTICAL)
145
- sizer_info = wx.BoxSizer(wx.HORIZONTAL)
146
- sizer_info.Add(self.check_enable_crop, 1, wx.ALIGN_CENTER_VERTICAL, 0)
147
- sizer_info.Add(self.button_reset, 0, wx.EXPAND, 0)
148
- sizer_info.Add(self.label_info, 0, wx.ALIGN_CENTER_VERTICAL, 0)
149
-
150
- sizer_left = wx.BoxSizer(wx.HORIZONTAL)
151
- sizer_right = wx.BoxSizer(wx.HORIZONTAL)
152
- sizer_top = wx.BoxSizer(wx.HORIZONTAL)
153
- sizer_bottom = wx.BoxSizer(wx.HORIZONTAL)
154
-
155
- lbl_left = wx.StaticText(self, wx.ID_ANY, _("Left"))
156
- lbl_left.SetMinSize(dip_size(self, 60, -1))
157
- lbl_right = wx.StaticText(self, wx.ID_ANY, _("Right"))
158
- lbl_right.SetMinSize(dip_size(self, 60, -1))
159
- lbl_bottom = wx.StaticText(self, wx.ID_ANY, _("Bottom"))
160
- lbl_bottom.SetMinSize(dip_size(self, 60, -1))
161
- lbl_top = wx.StaticText(self, wx.ID_ANY, _("Top"))
162
- lbl_top.SetMinSize(dip_size(self, 60, -1))
163
-
164
- self.text_left.SetMaxSize(dip_size(self, 60, -1))
165
- self.text_right.SetMaxSize(dip_size(self, 60, -1))
166
- self.text_top.SetMaxSize(dip_size(self, 60, -1))
167
- self.text_bottom.SetMaxSize(dip_size(self, 60, -1))
168
-
169
- sizer_left.Add(lbl_left, 0, wx.ALIGN_CENTER_VERTICAL)
170
- sizer_left.Add(self.slider_left, 4, wx.ALIGN_CENTER_VERTICAL)
171
- sizer_left.Add(self.text_left, 1, wx.ALIGN_CENTER_VERTICAL)
172
-
173
- sizer_right.Add(lbl_right, 0, wx.ALIGN_CENTER_VERTICAL)
174
- sizer_right.Add(self.slider_right, 4, wx.ALIGN_CENTER_VERTICAL)
175
- sizer_right.Add(self.text_right, 1, wx.ALIGN_CENTER_VERTICAL)
176
-
177
- sizer_top.Add(lbl_top, 0, wx.ALIGN_CENTER_VERTICAL)
178
- sizer_top.Add(self.slider_top, 4, wx.ALIGN_CENTER_VERTICAL)
179
- sizer_top.Add(self.text_top, 1, wx.ALIGN_CENTER_VERTICAL)
180
-
181
- sizer_bottom.Add(lbl_bottom, 0, wx.ALIGN_CENTER_VERTICAL)
182
- sizer_bottom.Add(self.slider_bottom, 4, wx.ALIGN_CENTER_VERTICAL)
183
- sizer_bottom.Add(self.text_bottom, 1, wx.ALIGN_CENTER_VERTICAL)
184
-
185
- sizer_main.Add(sizer_info, 0, wx.EXPAND, 0)
186
- sizer_main.Add(sizer_left, 0, wx.EXPAND, 0)
187
- sizer_main.Add(sizer_right, 0, wx.EXPAND, 0)
188
- sizer_main.Add(sizer_top, 0, wx.EXPAND, 0)
189
- sizer_main.Add(sizer_bottom, 0, wx.EXPAND, 0)
190
-
191
- self.SetSizer(sizer_main)
192
- sizer_main.Fit(self)
193
- self.Layout()
194
-
195
- def on_button_reset(self, event):
196
- if self.node is None:
197
- return
198
- w, h = self.node.image.size
199
- self._bounds = [0, 0, w, h]
200
- self.op["bounds"] = self._bounds
201
- self.set_slider_limits("lrtb")
202
- self.node.update(self.context)
203
-
204
- def on_check_enable_crop(self, event=None):
205
- flag = self.check_enable_crop.GetValue()
206
- if flag:
207
- if self.op is None:
208
- w, h = self.node.image.size
209
- self._width = w
210
- self._height = h
211
- self.op = {"name": "crop", "enable": True, "bounds": [0, 0, w, h]}
212
- self.node.operations.append(self.op)
213
- self.set_slider_limits("lrtb", False)
214
- last = self._no_update
215
- self._no_update = True
216
- self.cropleft = 0
217
- self.cropright = w
218
- self.croptop = 0
219
- self.cropbottom = h
220
- self._no_update = last
221
- else:
222
- self.op["enable"] = flag
223
- else:
224
- if self.op is not None:
225
- self.op["enable"] = flag
226
- if self.op is not None and not self._no_update:
227
- self.node.update(self.context)
228
- self.activate_controls(flag)
229
-
230
- def on_slider_left(self, event=None):
231
- self.cropleft = self.slider_left.GetValue()
232
-
233
- def on_slider_right(self, event=None):
234
- self.cropright = self.slider_right.GetValue()
235
-
236
- def on_slider_top(self, event=None):
237
- self.croptop = self.slider_top.GetValue()
238
-
239
- def on_slider_bottom(self, event=None):
240
- self.cropbottom = self.slider_bottom.GetValue()
241
-
242
- def set_slider_limits(self, pattern, constraint=True):
243
- if "l" in pattern:
244
- value = self._bounds[2]
245
- self.slider_left.SetMin(0)
246
- self.slider_left.SetMax(value - 1 if constraint else self._width)
247
- if self._bounds[0] != self.slider_left.GetValue():
248
- self.slider_left.SetValue(int(self._bounds[0]))
249
- dvalue = self._bounds[0]
250
- if dvalue == 0:
251
- self.text_left.SetValue("---")
252
- else:
253
- self.text_left.SetValue(f"> {dvalue} px")
254
- if "r" in pattern:
255
- value = self._bounds[0]
256
- self.slider_right.SetMin(value + 1 if constraint else 0)
257
- self.slider_right.SetMax(self._width)
258
- if self._bounds[2] != self.slider_right.GetValue():
259
- self.slider_right.SetValue(int(self._bounds[2]))
260
- dvalue = self._width - self._bounds[2]
261
- if dvalue == 0:
262
- self.text_right.SetValue("---")
263
- else:
264
- self.text_right.SetValue(f"{dvalue} px <")
265
- if "t" in pattern:
266
- value = self._bounds[3]
267
- self.slider_top.SetMin(0)
268
- self.slider_top.SetMax(value - 1 if constraint else self._height)
269
- if self._bounds[1] != self.slider_top.GetValue():
270
- self.slider_top.SetValue(int(self._bounds[1]))
271
- dvalue = self._bounds[1]
272
- if dvalue == 0:
273
- self.text_top.SetValue("---")
274
- else:
275
- self.text_top.SetValue(f"> {dvalue} px")
276
- if "b" in pattern:
277
- value = self._bounds[1]
278
- self.slider_bottom.SetMin(value + 1 if constraint else 0)
279
- self.slider_bottom.SetMax(self._height)
280
- if self._bounds[3] != self.slider_bottom.GetValue():
281
- self.slider_bottom.SetValue(int(self._bounds[3]))
282
- dvalue = self._height - self._bounds[3]
283
- if dvalue == 0:
284
- self.text_bottom.SetValue("---")
285
- else:
286
- self.text_bottom.SetValue(f"{dvalue} px <")
287
-
288
- @property
289
- def cropleft(self):
290
- if self._bounds is None:
291
- return None
292
- else:
293
- return self._bounds[0]
294
-
295
- @cropleft.setter
296
- def cropleft(self, value):
297
- # print(f"Set left to: {value}")
298
- self._bounds[0] = value
299
- if self.slider_left.GetValue() != value:
300
- self.slider_left.SetValue(int(value))
301
- if value == 0:
302
- self.text_left.SetValue("---")
303
- else:
304
- self.text_left.SetValue(f"> {value} px")
305
- # We need to adjust the boundaries of the right slider.
306
- self.set_slider_limits("r")
307
- if self.op is not None:
308
- self.op["bounds"][0] = value
309
- if not self._no_update:
310
- self.node.update(self.context)
311
-
312
- @property
313
- def cropright(self):
314
- if self._bounds is None:
315
- return None
316
- else:
317
- return self._bounds[2]
318
-
319
- @cropright.setter
320
- def cropright(self, value):
321
- # print(f"Set right to: {value}")
322
- self._bounds[2] = value
323
- if self.slider_right.GetValue() != value:
324
- self.slider_right.SetValue(int(value))
325
- dvalue = self._width - value
326
- if dvalue == 0:
327
- self.text_right.SetValue("---")
328
- else:
329
- self.text_right.SetValue(f"{dvalue} px <")
330
- # We need to adjust the boundaries of the left slider.
331
- self.set_slider_limits("l")
332
- if self.op is not None:
333
- self.op["bounds"][2] = value
334
- if not self._no_update:
335
- self.node.update(self.context)
336
-
337
- @property
338
- def croptop(self):
339
- if self._bounds is None:
340
- return None
341
- else:
342
- return self._bounds[1]
343
-
344
- @croptop.setter
345
- def croptop(self, value):
346
- # print(f"Set top to: {value}")
347
- self._bounds[1] = value
348
- if self.slider_top.GetValue() != value:
349
- self.slider_top.SetValue(int(value))
350
- if value == 0:
351
- self.text_top.SetValue("---")
352
- else:
353
- self.text_top.SetValue(f"> {value} px")
354
- # We need to adjust the boundaries of the bottom slider.
355
- self.set_slider_limits("b")
356
- if self.op is not None:
357
- self.op["bounds"][1] = value
358
- if not self._no_update:
359
- self.node.update(self.context)
360
-
361
- @property
362
- def cropbottom(self):
363
- if self._bounds is None:
364
- return None
365
- else:
366
- return self._bounds[3]
367
-
368
- @cropbottom.setter
369
- def cropbottom(self, value):
370
- self._bounds[3] = value
371
- if self.slider_bottom.GetValue() != value:
372
- self.slider_bottom.SetValue(int(value))
373
- # We need to adjust the boundaries of the top slider.
374
- self.set_slider_limits("t")
375
- if self.op is not None:
376
- self.op["bounds"][3] = value
377
- if not self._no_update:
378
- self.node.update(self.context)
379
-
380
-
381
- class ImageModificationPanel(ScrolledPanel):
382
- name = _("Modification")
383
- priority = 90
384
-
385
- def __init__(self, *args, context=None, node=None, **kwargs):
386
- # begin wxGlade: ConsolePanel.__init__
387
- kwargs["style"] = kwargs.get("style", 0) | wx.TAB_TRAVERSAL
388
- wx.Panel.__init__(self, *args, **kwargs)
389
- self.context = context
390
- self.node = node
391
- self.scripts = []
392
- choices = [_("Set to None")]
393
- for entry in list(self.context.match("raster_script/.*", suffix=True)):
394
- self.scripts.append(entry)
395
- choices.append(_("Apply {entry}").format(entry=entry))
396
- self.combo_scripts = wx.ComboBox(
397
- self, wx.ID_ANY, choices=choices, style=wx.CB_READONLY | wx.CB_DROPDOWN
398
- )
399
- self.combo_scripts.SetSelection(0)
400
- self.button_apply = wx.Button(self, wx.ID_ANY, _("Apply Script"))
401
- self.list_operations = wx.ListCtrl(
402
- self,
403
- wx.ID_ANY,
404
- style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES | wx.LC_SINGLE_SEL,
405
- )
406
-
407
- self._do_layout()
408
- self._do_logic()
409
- self.set_widgets(node)
410
-
411
- def _do_layout(self):
412
- self.list_operations.AppendColumn(_("#"), format=wx.LIST_FORMAT_LEFT, width=58)
413
- self.list_operations.AppendColumn(
414
- _("Action"), format=wx.LIST_FORMAT_LEFT, width=65
415
- )
416
- self.list_operations.AppendColumn(
417
- _("Active"), format=wx.LIST_FORMAT_LEFT, width=25
418
- )
419
- self.list_operations.AppendColumn(
420
- _("Parameters"), format=wx.LIST_FORMAT_LEFT, width=95
421
- )
422
-
423
- sizer_main = wx.BoxSizer(wx.VERTICAL)
424
- sizer_script = StaticBoxSizer(self, wx.ID_ANY, _("RasterWizard"), wx.HORIZONTAL)
425
-
426
- sizer_script.Add(self.combo_scripts, 1, wx.EXPAND, 0)
427
- sizer_script.Add(self.button_apply, 0, wx.EXPAND, 0)
428
- sizer_main.Add(sizer_script, 0, wx.EXPAND, 0)
429
- sizer_main.Add(self.list_operations, 1, wx.EXPAND, 0)
430
-
431
- self.SetSizer(sizer_main)
432
- self.Layout()
433
- self.Centre()
434
-
435
- def _do_logic(self):
436
- self.button_apply.Bind(wx.EVT_BUTTON, self.on_apply_replace)
437
- self.button_apply.Bind(wx.EVT_RIGHT_DOWN, self.on_apply_append)
438
- self.list_operations.Bind(wx.EVT_RIGHT_DOWN, self.on_list_menu)
439
-
440
- @staticmethod
441
- def accepts(node):
442
- if node.type == "elem image":
443
- return True
444
- return False
445
-
446
- def set_widgets(self, node=None):
447
- self.node = node
448
- if node is None:
449
- return
450
- self.fill_operations()
451
-
452
- def fill_operations(self):
453
- self.list_operations.DeleteAllItems()
454
- idx = 0
455
- for op in self.node.operations:
456
- idx += 1
457
- list_id = self.list_operations.InsertItem(
458
- self.list_operations.GetItemCount(), f"#{idx}"
459
- )
460
- self.list_operations.SetItem(list_id, 1, op["name"])
461
- self.list_operations.SetItem(list_id, 2, "x" if op["enable"] else "-")
462
- self.list_operations.SetItem(list_id, 3, str(op))
463
-
464
- def apply_script(self, index, addition):
465
- if index == 0:
466
- self.node.operations = []
467
- self.update_node()
468
- else:
469
- if index < 1 or index > len(self.scripts):
470
- return
471
- script = self.scripts[index - 1]
472
- raster_script = self.context.lookup(f"raster_script/{script}")
473
- if not addition:
474
- self.node.operations = []
475
- for entry in raster_script:
476
- self.node.operations.append(entry)
477
- self.update_node()
478
-
479
- def update_node(self):
480
- self.context.elements.emphasized()
481
- self.node.update(self.context)
482
- self.context.signal("element_property_update", self.node)
483
- self.context.signal("selected", self.node)
484
-
485
- def on_apply_replace(self, event):
486
- idx = self.combo_scripts.GetSelection()
487
- if idx >= 0:
488
- self.apply_script(idx, False)
489
-
490
- def on_apply_append(self, event):
491
- idx = self.combo_scripts.GetSelection()
492
- if idx >= 0:
493
- self.apply_script(idx, True)
494
-
495
- def on_list_menu(self, event):
496
- def on_delete(index):
497
- def check(event):
498
- self.node.operations.pop(index)
499
- self.update_node()
500
-
501
- return check
502
-
503
- def on_enable(index):
504
- def check(event):
505
- self.node.operations[index]["enable"] = not self.node.operations[index][
506
- "enable"
507
- ]
508
- self.update_node()
509
-
510
- return check
511
-
512
- def on_op_insert(index, op):
513
- def check(event):
514
- self.node.operations.insert(index, op)
515
- self.update_node()
516
-
517
- return check
518
-
519
- def on_op_append(index, op):
520
- def check(event):
521
- self.node.operations.append(op)
522
- self.update_node()
523
-
524
- return check
525
-
526
- index = self.list_operations.GetFirstSelected()
527
-
528
- possible_ops = [
529
- {"name": "crop", "enable": True, "bounds": None},
530
- {
531
- "name": "grayscale",
532
- "enable": True,
533
- "invert": False,
534
- "red": 1.0,
535
- "green": 1.0,
536
- "blue": 1.0,
537
- "lightness": 1.0,
538
- },
539
- {"name": "auto_contrast", "enable": True, "cutoff": 3},
540
- {"name": "contrast", "enable": True, "contrast": 25, "brightness": 25},
541
- {
542
- "name": "unsharp_mask",
543
- "enable": True,
544
- "percent": 500,
545
- "radius": 4,
546
- "threshold": 0,
547
- },
548
- {
549
- "name": "tone",
550
- "type": "spline",
551
- "enable": True,
552
- "values": [[0, 0], [100, 150], [255, 255]],
553
- },
554
- {"name": "gamma", "enable": True, "factor": 3.5},
555
- {"name": "edge_enhance", "enable": False},
556
- {
557
- "name": "halftone",
558
- "enable": True,
559
- "black": True,
560
- "sample": 10,
561
- "angle": 22,
562
- "oversample": 2,
563
- },
564
- {"name": "dither", "enable": True, "type": "Floyd-Steinberg"},
565
- ]
566
- devmode = self.context.root.setting(bool, "developer_mode", False)
567
- menu = wx.Menu()
568
- if index >= 0:
569
- # Edit-Part
570
- menuitem = menu.Append(
571
- wx.ID_ANY, _("Delete item"), _("Will delete the current entry")
572
- )
573
- self.Bind(wx.EVT_MENU, on_delete(index), id=menuitem.GetId())
574
-
575
- menuitem = menu.Append(
576
- wx.ID_ANY,
577
- _("Enable"),
578
- _("Toggles enable-status of operation"),
579
- kind=wx.ITEM_CHECK,
580
- )
581
- menuitem.Check(self.node.operations[index]["enable"])
582
- self.Bind(wx.EVT_MENU, on_enable(index), id=menuitem.GetId())
583
- if devmode:
584
- menu.AppendSeparator()
585
- for op in possible_ops:
586
- menuitem = menu.Append(
587
- wx.ID_ANY,
588
- _("Insert {op}").format(op=op["name"]),
589
- _("Will insert this operation before the current entry"),
590
- )
591
- self.Bind(wx.EVT_MENU, on_op_insert(index, op), id=menuitem.GetId())
592
- menu.AppendSeparator()
593
- if devmode:
594
- for op in possible_ops:
595
- menuitem = menu.Append(
596
- wx.ID_ANY,
597
- _("Append {op}").format(op=op["name"]),
598
- _("Will append this operation to the end of the list"),
599
- )
600
- self.Bind(wx.EVT_MENU, on_op_append(index, op), id=menuitem.GetId())
601
-
602
- if menu.MenuItemCount != 0:
603
- self.PopupMenu(menu)
604
- menu.Destroy()
605
-
606
- def pane_show(self):
607
- self.fill_operations()
608
-
609
- def pane_active(self):
610
- self.fill_operations()
611
-
612
-
613
- class ImageVectorisationPanel(ScrolledPanel):
614
- name = _("Vectorisation")
615
- priority = 95
616
-
617
- def __init__(self, *args, context=None, node=None, **kwargs):
618
- # begin wxGlade: ConsolePanel.__init__
619
- kwargs["style"] = kwargs.get("style", 0) | wx.TAB_TRAVERSAL
620
- wx.Panel.__init__(self, *args, **kwargs)
621
- self.context = context
622
- self.node = node
623
- main_sizer = wx.BoxSizer(wx.HORIZONTAL)
624
- # self.vector_lock = threading.Lock()
625
- # self.alive = True
626
- # Only display if we have a vector engine
627
- make_vector = self.context.kernel.lookup("render-op/make_vector")
628
- if make_vector:
629
- global HAS_VECTOR_ENGINE
630
- HAS_VECTOR_ENGINE = True
631
- if not make_vector:
632
- main_sizer.Add(
633
- wx.StaticText(
634
- self, wx.ID_ANY, "No vector engine installed, you need potrace"
635
- ),
636
- 1,
637
- wx.EXPAND,
638
- 0,
639
- )
640
- self.SetSizer(main_sizer)
641
- main_sizer.Fit(self)
642
- self.Layout()
643
- self.Centre()
644
- return
645
-
646
- sizer_options = StaticBoxSizer(self, wx.ID_ANY, _("Options"), wx.VERTICAL)
647
- main_sizer.Add(sizer_options, 1, wx.EXPAND, 0)
648
-
649
- sizer_turn = wx.BoxSizer(wx.HORIZONTAL)
650
- sizer_options.Add(sizer_turn, 0, wx.EXPAND, 0)
651
-
652
- label_turn = wx.StaticText(self, wx.ID_ANY, _("Turnpolicy"))
653
- label_turn.SetMinSize(dip_size(self, 70, -1))
654
- sizer_turn.Add(label_turn, 0, wx.ALIGN_CENTER_VERTICAL, 0)
655
- self.turn_choices = [
656
- "Black",
657
- "White",
658
- "Left",
659
- "Right",
660
- "Minority",
661
- "Majority",
662
- "Random",
663
- ]
664
- self.combo_turnpolicy = wx.ComboBox(
665
- self,
666
- wx.ID_ANY,
667
- choices=self.turn_choices,
668
- style=wx.CB_DROPDOWN | wx.CB_READONLY,
669
- )
670
- self.combo_turnpolicy.SetToolTip(
671
- _(
672
- "This parameter determines how to resolve ambiguities during decomposition of bitmaps into paths.\n\n"
673
- + "BLACK: prefers to connect black (foreground) components.\n"
674
- + "WHITE: prefers to connect white (background) components.\n"
675
- + "LEFT: always take a left turn.\n"
676
- + "RIGHT: always take a right turn.\n"
677
- + "MINORITY: prefers to connect the color (black or white) that occurs least frequently in a local neighborhood of the current position.\n"
678
- + "MAJORITY: prefers to connect the color (black or white) that occurs most frequently in a local neighborhood of the current position.\n"
679
- + "RANDOM: choose randomly."
680
- )
681
- )
682
- self.combo_turnpolicy.SetSelection(4)
683
- sizer_turn.Add(self.combo_turnpolicy, 1, wx.ALIGN_CENTER_VERTICAL, 0)
684
-
685
- sizer_turd = wx.BoxSizer(wx.HORIZONTAL)
686
- sizer_options.Add(sizer_turd, 0, wx.EXPAND, 0)
687
-
688
- label_turd = wx.StaticText(self, wx.ID_ANY, _("Despeckle"))
689
- label_turd.SetMinSize(dip_size(self, 70, -1))
690
- sizer_turd.Add(label_turd, 0, wx.ALIGN_CENTER_VERTICAL, 0)
691
-
692
- self.slider_turdsize = wx.Slider(self, wx.ID_ANY, 2, 0, 10)
693
- self.slider_turdsize.SetToolTip(
694
- _("Suppress speckles of up to this size (default 2 px)")
695
- )
696
- sizer_turd.Add(self.slider_turdsize, 1, wx.EXPAND, 0)
697
-
698
- sizer_alphamax = wx.BoxSizer(wx.HORIZONTAL)
699
- sizer_options.Add(sizer_alphamax, 0, wx.EXPAND, 0)
700
-
701
- label_alphamax = wx.StaticText(self, wx.ID_ANY, _("Corners"))
702
- label_alphamax.SetMinSize(dip_size(self, 70, -1))
703
- sizer_alphamax.Add(label_alphamax, 0, wx.ALIGN_CENTER_VERTICAL, 0)
704
-
705
- self.slider_alphamax = wx.Slider(self, wx.ID_ANY, 9, 0, 12)
706
- self.slider_alphamax.SetToolTip(
707
- _(
708
- "This parameter is a threshold for the detection of corners. It controls the smoothness of the traced curve."
709
- )
710
- )
711
- sizer_alphamax.Add(self.slider_alphamax, 1, wx.EXPAND, 0)
712
-
713
- sizer_opticurve = wx.BoxSizer(wx.HORIZONTAL)
714
- sizer_options.Add(sizer_opticurve, 0, wx.EXPAND, 0)
715
-
716
- label_opticurve = wx.StaticText(self, wx.ID_ANY, _("Simplify"))
717
- label_opticurve.SetMinSize(dip_size(self, 70, -1))
718
- sizer_opticurve.Add(label_opticurve, 0, wx.ALIGN_CENTER_VERTICAL, 0)
719
-
720
- self.check_opticurve = wx.CheckBox(self, wx.ID_ANY, "")
721
- self.check_opticurve.SetToolTip(
722
- _(
723
- "Try to 'simplify' the final curve by reducing the number of Bezier curve segments."
724
- )
725
- )
726
- self.check_opticurve.SetValue(1)
727
- sizer_opticurve.Add(self.check_opticurve, 0, wx.ALIGN_CENTER_VERTICAL, 0)
728
-
729
- sizer_opttolerance = wx.BoxSizer(wx.HORIZONTAL)
730
- sizer_options.Add(sizer_opttolerance, 0, wx.EXPAND, 0)
731
-
732
- label_opttolerance = wx.StaticText(self, wx.ID_ANY, _("Tolerance"))
733
- label_opttolerance.SetMinSize(dip_size(self, 70, -1))
734
- sizer_opttolerance.Add(label_opttolerance, 0, wx.ALIGN_CENTER_VERTICAL, 0)
735
-
736
- self.slider_tolerance = wx.Slider(self, wx.ID_ANY, 20, 0, 150)
737
- self.slider_tolerance.SetToolTip(
738
- _(
739
- "This defines the amount of error allowed in this simplification.\n"
740
- + "Larger values tend to decrease the number of segments, at the expense of less accuracy."
741
- )
742
- )
743
- sizer_opttolerance.Add(self.slider_tolerance, 1, wx.EXPAND, 0)
744
-
745
- sizer_blacklevel = wx.BoxSizer(wx.HORIZONTAL)
746
- sizer_options.Add(sizer_blacklevel, 0, wx.EXPAND, 0)
747
-
748
- label_blacklevel = wx.StaticText(self, wx.ID_ANY, _("Black-Level"))
749
- label_blacklevel.SetMinSize(dip_size(self, 70, -1))
750
- sizer_blacklevel.Add(label_blacklevel, 0, wx.ALIGN_CENTER_VERTICAL, 0)
751
-
752
- self.slider_blacklevel = wx.Slider(
753
- self, wx.ID_ANY, 50, 0, 100, style=wx.SL_HORIZONTAL | wx.SL_LABELS
754
- )
755
- self.slider_blacklevel.SetToolTip(_("Establish when 'black' starts"))
756
- sizer_blacklevel.Add(self.slider_blacklevel, 1, wx.EXPAND, 0)
757
-
758
- sizer_buttons = wx.BoxSizer(wx.HORIZONTAL)
759
- sizer_options.Add(sizer_buttons, 1, wx.EXPAND, 0)
760
-
761
- self.button_vector = wx.Button(self, wx.ID_ANY, _("Vectorize"))
762
- sizer_buttons.Add(self.button_vector, 0, 0, 0)
763
-
764
- label_spacer = wx.StaticText(self, wx.ID_ANY, " ")
765
- sizer_buttons.Add(label_spacer, 1, 0, 0)
766
-
767
- self.button_generate = wx.Button(self, wx.ID_ANY, _("Preview"))
768
- self.button_generate.SetToolTip(_("Generate a preview of the result"))
769
- sizer_buttons.Add(self.button_generate, 0, 0, 0)
770
-
771
- sizer_preview = StaticBoxSizer(self, wx.ID_ANY, _("Preview"), wx.VERTICAL)
772
- main_sizer.Add(sizer_preview, 2, wx.EXPAND, 0)
773
-
774
- self.bitmap_preview = wx.StaticBitmap(self, wx.ID_ANY, wx.NullBitmap)
775
- sizer_preview.Add(self.bitmap_preview, 1, wx.EXPAND, 0)
776
-
777
- self.vector_preview = wx.StaticBitmap(self, wx.ID_ANY, wx.NullBitmap)
778
- sizer_preview.Add(self.vector_preview, 1, wx.EXPAND, 0)
779
-
780
- self.SetSizer(main_sizer)
781
- main_sizer.Fit(self)
782
-
783
- # self._preview = True
784
- # self._need_updates = False
785
-
786
- # self.check_generate.SetValue(self._preview)
787
-
788
- self.wximage = wx.NullBitmap
789
- self.wxvector = wx.NullBitmap
790
- self._visible = False
791
-
792
- self.Layout()
793
- self.Centre()
794
- self.Bind(wx.EVT_BUTTON, self.on_button_create, self.button_vector)
795
- # self.Bind(wx.EVT_CHECKBOX, self.on_check_preview, self.check_generate)
796
- self.Bind(wx.EVT_BUTTON, self.on_changes, self.button_generate)
797
- self.Bind(wx.EVT_SIZE, self.on_size)
798
- # self.Bind(wx.EVT_SLIDER, self.on_changes, self.slider_alphamax)
799
- # self.Bind(wx.EVT_SLIDER, self.on_changes, self.slider_blacklevel)
800
- # self.Bind(wx.EVT_SLIDER, self.on_changes, self.slider_tolerance)
801
- # self.Bind(wx.EVT_SLIDER, self.on_changes, self.slider_turdsize)
802
- # self.Bind(wx.EVT_COMBOBOX, self.on_changes, self.combo_turnpolicy)
803
- # self.stop = None
804
- # self._update_thread = self.context.threaded(
805
- # self.generate_preview, result=self.stop, daemon=True
806
- # )
807
-
808
- self.set_widgets(node)
809
-
810
- # def on_check_preview(self, event):
811
- # self._preview = self.check_generate.GetValue()
812
-
813
- def on_size(self, event):
814
- self.set_images(True)
815
-
816
- def pane_active(self):
817
- self._visible = True
818
- self.set_images(True)
819
-
820
- def pane_deactive(self):
821
- self._visible = False
822
-
823
- def on_changes(self, event):
824
- # self._need_updates = True
825
- self.generate_preview()
826
-
827
- def on_button_create(self, event):
828
- ipolicy = self.combo_turnpolicy.GetSelection()
829
- turnpolicy = self.turn_choices[ipolicy].lower()
830
- # slider 0 .. 10 translate to 0 .. 10
831
- turdsize = self.slider_turdsize.GetValue()
832
- # slider 0 .. 100 translate to 0 .. 1
833
- blacklevel = (
834
- self.slider_blacklevel.GetValue() / self.slider_blacklevel.GetMax() * 1.0
835
- )
836
- # slider 0 .. 150 translate to 0 .. 1.5
837
- opttolerance = (
838
- self.slider_tolerance.GetValue() / self.slider_tolerance.GetMax() * 1.5
839
- )
840
- # slider 0 .. 12 translate to 0 .. 1.333
841
- alphamax = (
842
- self.slider_alphamax.GetValue() / self.slider_alphamax.GetMax() * 4.0 / 3.0
843
- )
844
- opticurve = self.check_opticurve.GetValue()
845
- cmd = f"vectorize -z {turnpolicy} -t {turdsize} -a {alphamax}{' -n' if opticurve else ''} -O {opttolerance} -k {blacklevel}\n"
846
- self.context(cmd)
847
-
848
- def set_images(self, refresh=False):
849
- def opaque(source):
850
- img = source
851
- if img is not None:
852
- if img.mode == "RGBA":
853
- r, g, b, a = img.split()
854
- background = Image.new("RGB", img.size, "white")
855
- background.paste(img, mask=a)
856
- img = background
857
- return img
858
-
859
- if not self._visible:
860
- return
861
- if self.node is None or self.node.image is None:
862
- self.wximage = wx.NullBitmap
863
- else:
864
- if refresh:
865
- source_image = self.node.active_image
866
- source_image = opaque(source_image)
867
- pw, ph = self.bitmap_preview.GetSize()
868
- iw, ih = source_image.size
869
- wfac = pw / iw
870
- hfac = ph / ih
871
- # The smaller of the two decide how to scale the picture
872
- if wfac < hfac:
873
- factor = wfac
874
- else:
875
- factor = hfac
876
- # print (f"Window: {pw} x {ph}, Image= {iw} x {ih}, factor={factor:.3f}")
877
- if factor < 1.0:
878
- image = source_image.resize((int(iw * factor), int(ih * factor)))
879
- else:
880
- image = source_image
881
- self.wximage = self.img_2_wx(image)
882
-
883
- self.bitmap_preview.SetBitmap(self.wximage)
884
-
885
- def generate_preview(self):
886
- # from time import sleep
887
- make_vector = self.context.kernel.lookup("render-op/make_vector")
888
- make_raster = self.context.kernel.lookup("render-op/make_raster")
889
- # while self.alive:
890
- if not self._visible:
891
- return
892
- self.wxvector = wx.NullBitmap
893
-
894
- if self.node is not None and self.node.image is not None:
895
- matrix = self.node.matrix
896
- image = self.node.opaque_image
897
- ipolicy = self.combo_turnpolicy.GetSelection()
898
- # turnpolicy = self.turn_choices[ipolicy].lower()
899
- # slider 0 .. 10 translate to 0 .. 10
900
- turdsize = self.slider_turdsize.GetValue()
901
- # slider 0 .. 100 translate to 0 .. 1
902
- blacklevel = (
903
- self.slider_blacklevel.GetValue()
904
- / self.slider_blacklevel.GetMax()
905
- * 1.0
906
- )
907
- # slider 0 .. 150 translate to 0 .. 1.5
908
- opttolerance = (
909
- self.slider_tolerance.GetValue() / self.slider_tolerance.GetMax() * 1.5
910
- )
911
- # slider 0 .. 12 translate to 0 .. 1.333
912
- alphamax = (
913
- self.slider_alphamax.GetValue()
914
- / self.slider_alphamax.GetMax()
915
- * 4.0
916
- / 3.0
917
- )
918
- opticurve = self.check_opticurve.GetValue()
919
- bounds = self.node.paint_bounds
920
- if bounds is None:
921
- bounds = self.node.bounds
922
- if bounds is None:
923
- return
924
- xmin, ymin, xmax, ymax = bounds
925
- width = xmax - xmin
926
- height = ymax - ymin
927
- dpi = 500
928
- dots_per_units = dpi / UNITS_PER_INCH
929
- new_width = width * dots_per_units
930
- new_height = height * dots_per_units
931
- new_height = max(new_height, 1)
932
- new_width = max(new_width, 1)
933
-
934
- try:
935
- image = make_raster(
936
- self.node,
937
- bounds=bounds,
938
- width=new_width,
939
- height=new_height,
940
- )
941
- path = make_vector(
942
- image=image,
943
- interpolationpolicy=ipolicy,
944
- turdsize=turdsize,
945
- alphamax=alphamax,
946
- opticurve=opticurve,
947
- opttolerance=opttolerance,
948
- blacklevel=blacklevel,
949
- )
950
- except:
951
- return
952
- path.transform *= Matrix(matrix)
953
- dummynode = PathNode(
954
- path=abs(path),
955
- stroke_width=0,
956
- stroke_scaled=False,
957
- fillrule=0, # Fillrule.FILLRULE_NONZERO
958
- )
959
- if dummynode is None:
960
- return
961
- bounds = dummynode.paint_bounds
962
- if bounds is None:
963
- bounds = dummynode.bounds
964
- if bounds is None:
965
- return
966
- pw, ph = self.vector_preview.GetSize()
967
- iw, ih = self.node.image.size
968
- wfac = pw / iw
969
- hfac = ph / ih
970
- # The smaller of the two decide how to scale the picture
971
- if wfac < hfac:
972
- factor = wfac
973
- else:
974
- factor = hfac
975
- image = make_raster(
976
- dummynode,
977
- bounds,
978
- width=pw,
979
- height=ph,
980
- keep_ratio=True,
981
- )
982
- rw, rh = image.size
983
- # print (f"Area={pw}x{ph}, Org={iw}x{ih}, Raster={rw}x{rh}")
984
- # if factor < 1.0:
985
- # image = image.resize((int(iw * factor), int(ih * factor)))
986
- self.wxvector = self.img_2_wx(image)
987
-
988
- self.vector_preview.SetBitmap(self.wxvector)
989
-
990
- @staticmethod
991
- def accepts(node):
992
- # Changing the staticmethod into a regular method will cause a crash
993
- # Not the nicest thing in the world, as we need to instantiate the class once to reset the status flag
994
- global HAS_VECTOR_ENGINE
995
- if node.type == "elem image" and HAS_VECTOR_ENGINE:
996
- return True
997
- return False
998
-
999
- def img_2_wx(self, image):
1000
- width, height = image.size
1001
- newimage = image.convert("RGB")
1002
- return wx.Bitmap.FromBuffer(width, height, newimage.tobytes())
1003
-
1004
- def set_widgets(self, node=None):
1005
- self.node = node
1006
- if node is not None:
1007
- self._need_updates = True
1008
- self.set_images()
1009
-
1010
-
1011
- class ImagePropertyPanel(ScrolledPanel):
1012
- def __init__(self, *args, context=None, node=None, **kwargs):
1013
- # begin wxGlade: ConsolePanel.__init__
1014
- kwargs["style"] = kwargs.get("style", 0) | wx.TAB_TRAVERSAL
1015
- wx.Panel.__init__(self, *args, **kwargs)
1016
- self.context = context
1017
- self.node = node
1018
- self.panel_id = IdPanel(
1019
- self, id=wx.ID_ANY, context=self.context, node=self.node
1020
- )
1021
-
1022
- self.text_dpi = TextCtrl(
1023
- self,
1024
- wx.ID_ANY,
1025
- "500",
1026
- style=wx.TE_PROCESS_ENTER,
1027
- check="float",
1028
- limited=True,
1029
- nonzero=True,
1030
- )
1031
- self.check_prevent_crop = wx.CheckBox(self, wx.ID_ANY, _("No final crop"))
1032
-
1033
- self.panel_lock = PreventChangePanel(
1034
- self, id=wx.ID_ANY, context=self.context, node=self.node
1035
- )
1036
- self.panel_xy = PositionSizePanel(
1037
- self, id=wx.ID_ANY, context=self.context, node=self.node
1038
- )
1039
-
1040
- self.panel_crop = CropPanel(
1041
- self, id=wx.ID_ANY, context=self.context, node=self.node
1042
- )
1043
- self.check_enable_dither = wx.CheckBox(self, wx.ID_ANY, _("Dither"))
1044
- self.choices = [
1045
- "Floyd-Steinberg",
1046
- "Atkinson",
1047
- "Jarvis-Judice-Ninke",
1048
- "Stucki",
1049
- "Burkes",
1050
- "Sierra3",
1051
- "Sierra2",
1052
- "Sierra-2-4a",
1053
- ]
1054
- self.combo_dither = wx.ComboBox(
1055
- self,
1056
- wx.ID_ANY,
1057
- choices=self.choices,
1058
- style=wx.CB_DROPDOWN,
1059
- )
1060
- # self.op_choices = []
1061
- # self.image_ops = []
1062
- # self.op_choices.append(_("Choose a script to apply"))
1063
- # self.op_choices.append(_("Set to None"))
1064
- # for op in list(self.context.elements.match("raster_script", suffix=True)):
1065
- # self.op_choices.append(_("Apply: {script}").format(script=op))
1066
- # self.image_ops.append(op)
1067
-
1068
- # self.combo_operations = wx.ComboBox(
1069
- # self,
1070
- # wx.ID_ANY,
1071
- # choices=self.op_choices,
1072
- # style=wx.CB_DROPDOWN,
1073
- # )
1074
-
1075
- self.check_invert_grayscale = wx.CheckBox(self, wx.ID_ANY, _("Invert"))
1076
- self.slider_grayscale_red = wx.Slider(
1077
- self, wx.ID_ANY, 0, -1000, 1000, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
1078
- )
1079
- self.text_grayscale_red = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_READONLY)
1080
- self.slider_grayscale_green = wx.Slider(
1081
- self, wx.ID_ANY, 0, -1000, 1000, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
1082
- )
1083
- self.text_grayscale_green = wx.TextCtrl(
1084
- self, wx.ID_ANY, "", style=wx.TE_READONLY
1085
- )
1086
- self.slider_grayscale_blue = wx.Slider(
1087
- self, wx.ID_ANY, 0, -1000, 1000, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
1088
- )
1089
- self.text_grayscale_blue = wx.TextCtrl(
1090
- self, wx.ID_ANY, "", style=wx.TE_READONLY
1091
- )
1092
- self.slider_grayscale_lightness = wx.Slider(
1093
- self, wx.ID_ANY, 500, 0, 1000, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
1094
- )
1095
- self.text_grayscale_lightness = wx.TextCtrl(
1096
- self, wx.ID_ANY, "", style=wx.TE_READONLY
1097
- )
1098
-
1099
- self.__set_properties()
1100
- self.__do_layout()
1101
-
1102
- self.Bind(wx.EVT_CHECKBOX, self.on_dither, self.check_enable_dither)
1103
- self.Bind(wx.EVT_COMBOBOX, self.on_dither, self.combo_dither)
1104
- # self.Bind(wx.EVT_COMBOBOX, self.on_combo_operation, self.combo_operations)
1105
-
1106
- self.Bind(wx.EVT_TEXT_ENTER, self.on_dither, self.combo_dither)
1107
-
1108
- self.text_dpi.SetActionRoutine(self.on_text_dpi)
1109
-
1110
- self.Bind(
1111
- wx.EVT_CHECKBOX, self.on_check_invert_grayscale, self.check_invert_grayscale
1112
- )
1113
- self.Bind(
1114
- wx.EVT_SLIDER,
1115
- self.on_slider_grayscale_component,
1116
- self.slider_grayscale_lightness,
1117
- )
1118
- self.Bind(
1119
- wx.EVT_SLIDER, self.on_slider_grayscale_component, self.slider_grayscale_red
1120
- )
1121
- self.Bind(
1122
- wx.EVT_SLIDER,
1123
- self.on_slider_grayscale_component,
1124
- self.slider_grayscale_green,
1125
- )
1126
- self.Bind(
1127
- wx.EVT_SLIDER,
1128
- self.on_slider_grayscale_component,
1129
- self.slider_grayscale_blue,
1130
- )
1131
- self.check_prevent_crop.Bind(wx.EVT_CHECKBOX, self.on_crop_option)
1132
-
1133
- # self.check_enable_grayscale.SetValue(op["enable"])
1134
- if node.invert is None:
1135
- node.invert = False
1136
- self.check_invert_grayscale.SetValue(node.invert)
1137
-
1138
- self.slider_grayscale_red.SetValue(int(node.red * 500.0))
1139
- self.text_grayscale_red.SetValue(str(node.red))
1140
-
1141
- self.slider_grayscale_green.SetValue(int(node.green * 500.0))
1142
- self.text_grayscale_green.SetValue(str(node.green))
1143
-
1144
- self.slider_grayscale_blue.SetValue(int(node.blue * 500.0))
1145
- self.text_grayscale_blue.SetValue(str(node.blue))
1146
-
1147
- self.slider_grayscale_lightness.SetValue(int(node.lightness * 500.0))
1148
- self.text_grayscale_lightness.SetValue(str(node.lightness))
1149
- self.set_widgets()
1150
-
1151
- @staticmethod
1152
- def accepts(node):
1153
- if node.type == "elem image":
1154
- return True
1155
- return False
1156
-
1157
- def set_widgets(self, node=None):
1158
- if node is None:
1159
- node = self.node
1160
- self.panel_id.set_widgets(node)
1161
- self.panel_xy.set_widgets(node)
1162
- self.panel_lock.set_widgets(node)
1163
- self.panel_crop.set_widgets(node)
1164
- self.node = node
1165
- if node is None:
1166
- return
1167
-
1168
- self.text_dpi.SetValue(str(node.dpi))
1169
- self.check_enable_dither.SetValue(node.dither)
1170
- self.combo_dither.SetValue(node.dither_type)
1171
- self.check_prevent_crop.SetValue(node.prevent_crop)
1172
-
1173
- def __set_properties(self):
1174
- self.check_prevent_crop.SetToolTip(_("Prevent final crop after all operations"))
1175
- self.check_enable_dither.SetToolTip(_("Enable Dither"))
1176
- self.check_enable_dither.SetValue(1)
1177
- self.combo_dither.SetToolTip(_("Select dither algorithm to use"))
1178
- self.combo_dither.SetSelection(0)
1179
- # self.combo_operations.SetToolTip(_("Select image enhancement script to apply"))
1180
- # self.combo_operations.SetSelection(0)
1181
- self.check_invert_grayscale.SetToolTip(_("Invert Grayscale"))
1182
- self.slider_grayscale_red.SetToolTip(_("Red component amount"))
1183
- self.text_grayscale_red.SetToolTip(_("Red Factor"))
1184
- self.slider_grayscale_green.SetToolTip(_("Green component control"))
1185
- self.text_grayscale_green.SetToolTip(_("Green Factor"))
1186
- self.slider_grayscale_blue.SetToolTip(_("Blue component control"))
1187
- self.text_grayscale_blue.SetToolTip(_("Blue Factor"))
1188
- self.slider_grayscale_lightness.SetToolTip(_("Lightness control"))
1189
- self.text_grayscale_lightness.SetToolTip(_("Lightness"))
1190
- # end wxGlade
1191
-
1192
- def __do_layout(self):
1193
- # begin wxGlade: ImageProperty.__do_layout
1194
- sizer_main = wx.BoxSizer(wx.VERTICAL)
1195
- sizer_dim = wx.BoxSizer(wx.HORIZONTAL)
1196
- sizer_xy = wx.BoxSizer(wx.HORIZONTAL)
1197
- sizer_main.Add(self.panel_id, 0, wx.EXPAND, 0)
1198
- sizer_main.Add(self.panel_crop, 0, wx.EXPAND, 0)
1199
-
1200
- sizer_dpi_dither = wx.BoxSizer(wx.HORIZONTAL)
1201
- sizer_dpi = StaticBoxSizer(self, wx.ID_ANY, _("DPI:"), wx.HORIZONTAL)
1202
- self.text_dpi.SetToolTip(_("Dots Per Inch"))
1203
- sizer_dpi.Add(self.text_dpi, 1, wx.EXPAND, 0)
1204
-
1205
- sizer_dpi_dither.Add(sizer_dpi, 1, wx.EXPAND, 0)
1206
-
1207
- sizer_crop = StaticBoxSizer(self, wx.ID_ANY, _("Auto-Crop:"), wx.HORIZONTAL)
1208
- sizer_crop.Add(self.check_prevent_crop, 1, wx.ALIGN_CENTER_VERTICAL, 0)
1209
-
1210
- sizer_dpi_dither.Add(sizer_crop, 1, wx.EXPAND, 0)
1211
-
1212
- sizer_dither = StaticBoxSizer(self, wx.ID_ANY, _("Dither"), wx.HORIZONTAL)
1213
- sizer_dither.Add(self.check_enable_dither, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1214
- sizer_dither.Add(self.combo_dither, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1215
-
1216
- sizer_dpi_dither.Add(sizer_dither, 2, wx.EXPAND, 0)
1217
-
1218
- sizer_main.Add(sizer_dpi_dither, 0, wx.EXPAND, 0)
1219
-
1220
- sizer_rg = wx.BoxSizer(wx.HORIZONTAL)
1221
- sizer_bl = wx.BoxSizer(wx.HORIZONTAL)
1222
- sizer_grayscale = StaticBoxSizer(self, wx.ID_ANY, _("Grayscale"), wx.VERTICAL)
1223
- sizer_grayscale_lightness = StaticBoxSizer(
1224
- self, wx.ID_ANY, _("Lightness"), wx.HORIZONTAL
1225
- )
1226
- sizer_grayscale_blue = StaticBoxSizer(self, wx.ID_ANY, _("Blue"), wx.HORIZONTAL)
1227
- sizer_grayscale_green = StaticBoxSizer(
1228
- self, wx.ID_ANY, _("Green"), wx.HORIZONTAL
1229
- )
1230
- sizer_grayscale_red = StaticBoxSizer(self, wx.ID_ANY, _("Red"), wx.HORIZONTAL)
1231
- sizer_grayscale.Add(self.check_invert_grayscale, 0, 0, 0)
1232
- sizer_grayscale_red.Add(self.slider_grayscale_red, 1, wx.EXPAND, 0)
1233
- sizer_grayscale_red.Add(self.text_grayscale_red, 1, 0, 0)
1234
- sizer_rg.Add(sizer_grayscale_red, 1, wx.EXPAND, 0)
1235
- sizer_grayscale_green.Add(self.slider_grayscale_green, 1, wx.EXPAND, 0)
1236
- sizer_grayscale_green.Add(self.text_grayscale_green, 1, 0, 0)
1237
- sizer_rg.Add(sizer_grayscale_green, 1, wx.EXPAND, 0)
1238
- sizer_grayscale_blue.Add(self.slider_grayscale_blue, 1, wx.EXPAND, 0)
1239
- sizer_grayscale_blue.Add(self.text_grayscale_blue, 1, 0, 0)
1240
- sizer_bl.Add(sizer_grayscale_blue, 1, wx.EXPAND, 0)
1241
- sizer_grayscale_lightness.Add(self.slider_grayscale_lightness, 1, wx.EXPAND, 0)
1242
- sizer_grayscale_lightness.Add(self.text_grayscale_lightness, 1, 0, 0)
1243
- sizer_bl.Add(sizer_grayscale_lightness, 1, wx.EXPAND, 0)
1244
- sizer_grayscale.Add(sizer_rg, 5, wx.EXPAND, 0)
1245
- sizer_grayscale.Add(sizer_bl, 5, wx.EXPAND, 0)
1246
-
1247
- self.text_grayscale_red.SetMaxSize(dip_size(self, 70, -1))
1248
- self.text_grayscale_green.SetMaxSize(dip_size(self, 70, -1))
1249
- self.text_grayscale_blue.SetMaxSize(dip_size(self, 70, -1))
1250
- self.text_grayscale_lightness.SetMaxSize(dip_size(self, 70, -1))
1251
-
1252
- sizer_main.Add(sizer_grayscale, 0, wx.EXPAND, 0)
1253
-
1254
- sizer_main.Add(self.panel_lock, 0, wx.EXPAND, 0)
1255
- sizer_main.Add(self.panel_xy, 0, wx.EXPAND, 0)
1256
-
1257
- self.SetSizer(sizer_main)
1258
- self.Layout()
1259
- self.Centre()
1260
- # end wxGlade
1261
-
1262
- def node_update(self):
1263
- self.node.set_dirty_bounds()
1264
- self.node.update(self.context)
1265
- self.context.elements.emphasized()
1266
- self.context.signal("element_property_update", self.node)
1267
-
1268
- def on_text_dpi(self):
1269
- new_step = float(self.text_dpi.GetValue())
1270
- self.node.dpi = new_step
1271
- self.node_update()
1272
-
1273
- def on_crop_option(self, event):
1274
- self.node.prevent_crop = self.check_prevent_crop.GetValue()
1275
- self.node_update()
1276
-
1277
- def on_dither(self, event=None):
1278
- # Dither can be set by two different means:
1279
- # a. directly
1280
- # b. via a script
1281
- dither_op = None
1282
- for op in self.node.operations:
1283
- if op["name"] == "dither":
1284
- dither_op = op
1285
- break
1286
- dither_flag = self.check_enable_dither.GetValue()
1287
- dither_type = self.choices[self.combo_dither.GetSelection()]
1288
- if dither_op is not None:
1289
- dither_op["enable"] = dither_flag
1290
- dither_op["type"] = dither_type
1291
- self.node.dither = dither_flag
1292
- self.node.dither_type = dither_type
1293
- self.node_update()
1294
-
1295
- def on_check_invert_grayscale(
1296
- self, event=None
1297
- ): # wxGlade: RasterWizard.<event_handler>
1298
- self.node.invert = self.check_invert_grayscale.GetValue()
1299
- self.node_update()
1300
-
1301
- def on_slider_grayscale_component(
1302
- self, event=None
1303
- ): # wxGlade: GrayscalePanel.<event_handler>
1304
- self.node.red = float(int(self.slider_grayscale_red.GetValue()) / 500.0)
1305
- self.text_grayscale_red.SetValue(str(self.node.red))
1306
-
1307
- self.node.green = float(int(self.slider_grayscale_green.GetValue()) / 500.0)
1308
- self.text_grayscale_green.SetValue(str(self.node.green))
1309
-
1310
- self.node.blue = float(int(self.slider_grayscale_blue.GetValue()) / 500.0)
1311
- self.text_grayscale_blue.SetValue(str(self.node.blue))
1312
-
1313
- self.node.lightness = float(
1314
- int(self.slider_grayscale_lightness.GetValue()) / 500.0
1315
- )
1316
- self.text_grayscale_lightness.SetValue(str(self.node.lightness))
1317
- self.node_update()
1318
-
1319
- # def on_combo_operation(self, event):
1320
- # idx = self.combo_operations.GetSelection()
1321
- # if idx <= 0:
1322
- # return
1323
- # elif idx == 1:
1324
- # self.node.operations = []
1325
- # else:
1326
- # script = self.image_ops[idx - 2]
1327
- # raster_script = self.context.lookup(f"raster_script/{script}")
1328
- # if raster_script is None:
1329
- # return
1330
- # self.node.operations = raster_script
1331
- # self.node.update(self.context)
1332
- # self.context.signal("element_property_reload", self.node)
1333
- # self.context.signal("propupdate", self.node)
1334
-
1335
-
1336
- # class ImageProperty(MWindow):
1337
- # def __init__(self, *args, node=None, **kwds):
1338
- # super().__init__(276, 218, *args, **kwds)
1339
- # self.panels = []
1340
- # main_sizer = wx.BoxSizer(wx.VERTICAL)
1341
- # panel_main = ImagePropertyPanel(
1342
- # self, wx.ID_ANY, context=self.context, node=node
1343
- # )
1344
- # panel_modify = ImageModificationPanel(
1345
- # self, wx.ID_ANY, context=self.context, node=node
1346
- # )
1347
- # panel_vector = ImageVectorisationPanel(
1348
- # self, wx.ID_ANY, context=self.context, node=node
1349
- # )
1350
- # notebook_main = wx.aui.AuiNotebook(
1351
- # self,
1352
- # -1,
1353
- # style=wx.aui.AUI_NB_TAB_EXTERNAL_MOVE
1354
- # | wx.aui.AUI_NB_SCROLL_BUTTONS
1355
- # | wx.aui.AUI_NB_TAB_SPLIT
1356
- # | wx.aui.AUI_NB_TAB_MOVE,
1357
- # )
1358
- # notebook_main.AddPage(panel_main, _("Properties"))
1359
- # notebook_main.AddPage(panel_modify, _("Modification"))
1360
- # notebook_main.AddPage(panel_vector, _("Vectorisation"))
1361
-
1362
- # self.panels.append(panel_main)
1363
- # self.panels.append(panel_modify)
1364
- # self.panels.append(panel_vector)
1365
- # for panel in self.panels:
1366
- # self.add_module_delegate(panel)
1367
- # # begin wxGlade: ImageProperty.__set_properties
1368
- # _icon = wx.NullIcon
1369
- # _icon.CopyFromBitmap(icons8_image.GetBitmap())
1370
- # self.SetIcon(_icon)
1371
- # self.SetTitle(_("Image Properties"))
1372
- # main_sizer.Add(notebook_main, 1, wx.EXPAND, 0)
1373
- # self.SetSizer(main_sizer)
1374
- # self.Layout()
1375
-
1376
- # def restore(self, *args, node=None, **kwds):
1377
- # for panel in self.panels:
1378
- # panel.set_widgets(node)
1379
-
1380
- # def window_preserve(self):
1381
- # return False
1382
-
1383
- # def window_menu(self):
1384
- # return False
1
+ # import threading
2
+ from copy import copy
3
+ from PIL import Image, ImageOps, ImageEnhance
4
+ import numpy as np
5
+ import wx
6
+ from meerk40t.kernel.kernel import Job
7
+ from meerk40t.core.node.node import Node
8
+ from meerk40t.core.node.elem_image import ImageNode
9
+ from meerk40t.core.node.elem_path import PathNode
10
+ from meerk40t.core.units import UNITS_PER_INCH
11
+
12
+ # from meerk40t.gui.icons import icon_ignore
13
+ # from meerk40t.gui.mwindow import MWindow
14
+ from meerk40t.gui.propertypanels.attributes import (
15
+ IdPanel,
16
+ PositionSizePanel,
17
+ PreventChangePanel,
18
+ )
19
+ from meerk40t.gui.wxutils import (
20
+ ScrolledPanel,
21
+ StaticBoxSizer,
22
+ TextCtrl,
23
+ dip_size,
24
+ wxButton,
25
+ wxCheckBox,
26
+ wxComboBox,
27
+ wxListCtrl,
28
+ wxRadioBox,
29
+ wxStaticBitmap,
30
+ wxStaticText,
31
+ )
32
+ from meerk40t.image.imagetools import img_to_polygons, img_to_rectangles
33
+ from meerk40t.svgelements import Matrix, Color
34
+
35
+ _ = wx.GetTranslation
36
+
37
+ # The default value needs to be true, as the static method will be called before init happened...
38
+ HAS_VECTOR_ENGINE = True
39
+
40
+
41
+ class ContourPanel(wx.Panel):
42
+ name = _("Contour recognition")
43
+ priority = 96
44
+
45
+ @staticmethod
46
+ def accepts(node):
47
+ return hasattr(node, "as_image")
48
+
49
+ def __init__(self, *args, context=None, node=None, simplified=False, direct_mode=False, **kwds):
50
+ # begin wxGlade: LayerSettingPanel.__init__
51
+ kwds["style"] = kwds.get("style", 0)
52
+ wx.Panel.__init__(self, *args, **kwds)
53
+ self.context = context
54
+ self.context.themes.set_window_colors(self)
55
+ self.node = node
56
+ self.direct_mode = direct_mode
57
+ self.check_enable_contrast = wxCheckBox(self, wx.ID_ANY, _("Enable"))
58
+ self.button_reset_contrast = wxButton(self, wx.ID_ANY, _("Reset"))
59
+ self.slider_contrast_contrast = wx.Slider(
60
+ self, wx.ID_ANY, 0, -127, 127, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
61
+ )
62
+ self.text_contrast_contrast = TextCtrl(
63
+ self, wx.ID_ANY, "", style=wx.TE_READONLY
64
+ )
65
+ self.slider_contrast_brightness = wx.Slider(
66
+ self, wx.ID_ANY, 0, -127, 127, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
67
+ )
68
+ self.text_contrast_brightness = TextCtrl(
69
+ self, wx.ID_ANY, "", style=wx.TE_READONLY
70
+ )
71
+ self.check_invert = wxCheckBox(self, wx.ID_ANY, _("Invert"))
72
+ self.check_original = wxCheckBox(self, wx.ID_ANY, _("Original picture"))
73
+
74
+ if simplified:
75
+ self.check_original.Hide()
76
+
77
+ self.text_minimum = TextCtrl(
78
+ self, wx.ID_ANY, "", limited=True, check="float"
79
+ )
80
+ self.text_maximum = TextCtrl(
81
+ self, wx.ID_ANY, "", limited=True, check="float"
82
+ )
83
+ self.check_inner = wxCheckBox(self, wx.ID_ANY, _("Ignore inner"))
84
+ self.radio_simplify = wxRadioBox(
85
+ self, wx.ID_ANY,
86
+ label=_("Contour simplification"),
87
+ choices = (_("None"), _("Visvalingam"), _("Douglas-Peucker"))
88
+ )
89
+ self.radio_method = wxRadioBox(
90
+ self, wx.ID_ANY,
91
+ label=_("Detection Method"),
92
+ choices = (_("Polygons"), _("Bounding rectangles"))
93
+ )
94
+
95
+ self.check_auto = wxCheckBox(self, wx.ID_ANY, _("Automatic update"))
96
+ self.button_update = wxButton(self, wx.ID_ANY, _("Update"))
97
+ self.button_create = wxButton(self, wx.ID_ANY, _("Generate contours"))
98
+ self.button_create_placement = wxButton(self, wx.ID_ANY, _("Generate placements"))
99
+ placement_choices = (
100
+ _("Edge (prefer short side)"),
101
+ _("Edge (prefer long side)"),
102
+ _("Center (unrotated)"),
103
+ _("Center (rotated)"),
104
+ )
105
+ self.combo_placement = wxComboBox(self, wx.ID_ANY, choices=placement_choices, style=wx.CB_DROPDOWN | wx.CB_READONLY)
106
+
107
+ self.bitmap_preview = wxStaticBitmap(self, wx.ID_ANY)
108
+ self.label_info = wxStaticText(self, wx.ID_ANY)
109
+ self.list_contours = wxListCtrl(
110
+ self, wx.ID_ANY,
111
+ style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES | wx.LC_SINGLE_SEL,
112
+ context=self.context, list_name="list_contours",
113
+ )
114
+ self.update_job = Job(
115
+ process=self.refresh_preview_job,
116
+ job_name="imageprop_contour",
117
+ interval=0.5,
118
+ times=1,
119
+ run_main=True,
120
+ )
121
+
122
+ self.image = None
123
+ self.matrix = None
124
+ self.contours = []
125
+ self.auto_update = self.context.setting(bool, "contour_autoupdate", True)
126
+ self._changed = True
127
+ self.make_raster = self.context.lookup("render-op/make_raster")
128
+ self.parameters = {}
129
+ self._pane_is_active = False
130
+ self.__set_properties()
131
+ self.__do_layout()
132
+ self.__do_logic()
133
+ self.set_widgets(self.node)
134
+
135
+ def __do_logic(self):
136
+ self.check_auto.Bind(wx.EVT_CHECKBOX, self.on_auto_check)
137
+ self.check_original.Bind(wx.EVT_CHECKBOX, self.on_control_update)
138
+ self.button_update.Bind(wx.EVT_BUTTON, self.on_refresh)
139
+ self.button_create.Bind(wx.EVT_BUTTON, self.on_creation)
140
+ self.button_create_placement.Bind(wx.EVT_BUTTON, self.on_creation_placement)
141
+ self.button_reset_contrast.Bind(wx.EVT_BUTTON, self.on_button_reset_contrast)
142
+ self.check_invert.Bind(wx.EVT_CHECKBOX, self.on_control_update)
143
+ self.check_inner.Bind(wx.EVT_CHECKBOX, self.on_control_update)
144
+ self.check_enable_contrast.Bind(wx.EVT_CHECKBOX, self.on_control_update)
145
+ self.slider_contrast_brightness.Bind(wx.EVT_SLIDER, self.on_slider_contrast_brightness)
146
+ self.slider_contrast_contrast.Bind(wx.EVT_SLIDER, self.on_slider_contrast_contrast)
147
+ self.radio_method.Bind(wx.EVT_RADIOBOX, self.on_control_update)
148
+ self.radio_simplify.Bind(wx.EVT_RADIOBOX, self.on_control_update)
149
+ self.text_minimum.SetActionRoutine(self.on_control_update)
150
+ self.text_maximum.SetActionRoutine(self.on_control_update)
151
+ self.Bind(wx.EVT_SIZE, self.on_resize)
152
+ self.list_contours.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_list_selection)
153
+ self.list_contours.Bind(wx.EVT_LIST_COL_CLICK, self.on_list_selection)
154
+ self.list_contours.Bind(wx.EVT_RIGHT_DOWN, self.on_right_click)
155
+
156
+ def __do_layout(self):
157
+ # begin wxGlade: PositionPanel.__do_layout
158
+ sizer_main = wx.BoxSizer(wx.HORIZONTAL)
159
+ sizer_left = wx.BoxSizer(wx.VERTICAL)
160
+ sizer_right = wx.BoxSizer(wx.VERTICAL)
161
+ sizer_main.Add(sizer_left, 0, wx.EXPAND, 0)
162
+ sizer_main.Add(sizer_right, 2, wx.EXPAND, 0)
163
+
164
+ sizer_right.Add(self.bitmap_preview, 4, wx.EXPAND, 0)
165
+ sizer_right.Add(self.list_contours, 1, wx.EXPAND, 0)
166
+
167
+ sizer_param_picture = StaticBoxSizer(
168
+ self, wx.ID_ANY, _("Image:"), wx.VERTICAL
169
+ )
170
+ sizer_contrast = StaticBoxSizer(self, wx.ID_ANY, _("Contrast"), wx.VERTICAL)
171
+
172
+ sizer_contrast_main = wx.BoxSizer(wx.HORIZONTAL)
173
+ sizer_contrast_main.Add(self.check_enable_contrast, 0, 0, 0)
174
+ sizer_contrast_main.Add(self.button_reset_contrast, 0, 0, 0)
175
+
176
+ sizer_contrast_contrast = StaticBoxSizer(
177
+ self, wx.ID_ANY, _("Contrast Amount"), wx.HORIZONTAL
178
+ )
179
+ sizer_contrast_contrast.Add(self.slider_contrast_contrast, 5, wx.EXPAND, 0)
180
+ sizer_contrast_contrast.Add(self.text_contrast_contrast, 1, 0, 0)
181
+
182
+ sizer_contrast_brightness = StaticBoxSizer(
183
+ self, wx.ID_ANY, _("Brightness Amount"), wx.HORIZONTAL
184
+ )
185
+ sizer_contrast_brightness.Add(self.slider_contrast_brightness, 5, wx.EXPAND, 0)
186
+ sizer_contrast_brightness.Add(self.text_contrast_brightness, 1, 0, 0)
187
+
188
+ sizer_contrast.Add(sizer_contrast_main, 0, wx.EXPAND, 0)
189
+ sizer_contrast.Add(sizer_contrast_contrast, 0, wx.EXPAND, 0)
190
+ sizer_contrast.Add(sizer_contrast_brightness, 0, wx.EXPAND, 0)
191
+
192
+ sizer_param_picture.Add(sizer_contrast, 0, wx.EXPAND, 0)
193
+ option_sizer = wx.BoxSizer(wx.HORIZONTAL)
194
+ option_sizer.Add(self.check_invert, 0, wx.ALIGN_CENTER_VERTICAL, 0)
195
+ option_sizer.Add(self.check_original, 0, wx.ALIGN_CENTER_VERTICAL, 0)
196
+ sizer_param_picture.Add(option_sizer, 0, wx.EXPAND, 0)
197
+
198
+ sizer_param_contour = StaticBoxSizer(
199
+ self, wx.ID_ANY, _("Parameters:"), wx.VERTICAL
200
+ )
201
+
202
+ min_sizer = StaticBoxSizer(self, wx.ID_ANY, _("Minimal size:"), wx.HORIZONTAL)
203
+ label1 = wxStaticText(self, wx.ID_ANY, "%")
204
+ min_sizer.Add(self.text_minimum, 1, wx.EXPAND, 0)
205
+ min_sizer.Add(label1, 0, wx.ALIGN_CENTER_VERTICAL, 0)
206
+
207
+ max_sizer = StaticBoxSizer(self, wx.ID_ANY, _("Maximal size:"), wx.HORIZONTAL)
208
+ label2 = wxStaticText(self, wx.ID_ANY, "%")
209
+ max_sizer.Add(self.text_maximum, 1, wx.EXPAND, 0)
210
+ max_sizer.Add(label2, 0, wx.ALIGN_CENTER_VERTICAL, 0)
211
+
212
+ minmax_sizer = wx.BoxSizer(wx.HORIZONTAL)
213
+ minmax_sizer.Add(min_sizer, 1, wx.EXPAND, 0)
214
+ minmax_sizer.Add(max_sizer, 1, wx.EXPAND, 0)
215
+ sizer_param_contour.Add(minmax_sizer, 0, wx.EXPAND, 0)
216
+
217
+ sizer_param_contour.Add(self.check_inner, 0, wx.EXPAND, 0)
218
+ sizer_param_contour.Add(self.radio_method, 0, wx.EXPAND, 0)
219
+ sizer_param_contour.Add(self.radio_simplify, 0, wx.EXPAND, 0)
220
+
221
+ sizer_param_update = StaticBoxSizer(
222
+ self, wx.ID_ANY, _("Update:"), wx.HORIZONTAL
223
+ )
224
+ sizer_param_update.Add(self.check_auto, 1, wx.ALIGN_CENTER_VERTICAL, 0)
225
+ sizer_param_update.Add(self.button_update, 0, wx.ALIGN_CENTER_VERTICAL, 0)
226
+
227
+ sizer_generation = wx.BoxSizer(wx.HORIZONTAL)
228
+ sizer_generation.Add(self.button_create, 0, wx.EXPAND, 0)
229
+ sizer_generation.Add(self.button_create_placement, 0, wx.EXPAND, 0)
230
+ sizer_generation.Add(self.combo_placement, 0, wx.ALIGN_CENTER_VERTICAL, 0)
231
+
232
+ sizer_left.Add(sizer_param_picture, 0, wx.EXPAND, 0)
233
+ sizer_left.Add(sizer_param_contour, 0, wx.EXPAND, 0)
234
+ sizer_left.Add(sizer_param_update, 0, wx.EXPAND, 0)
235
+ sizer_left.Add(sizer_generation, 0, wx.EXPAND, 0)
236
+ sizer_left.Add(self.label_info, 0, wx.ALIGN_CENTER_HORIZONTAL, 0)
237
+
238
+ self.__do_defaults()
239
+ self.SetSizer(sizer_main)
240
+ sizer_main.Fit(self)
241
+ self.Layout()
242
+
243
+ def __do_defaults(self):
244
+ self.check_auto.SetValue(self.auto_update)
245
+ self.button_update.Enable(not self.auto_update)
246
+ last_val = self.context.setting(int, "contour_placement", 1) # Long side is default preference
247
+ if last_val < 0 or last_val >= self.combo_placement.GetCount():
248
+ last_val = 0
249
+ self.combo_placement.SetSelection(last_val)
250
+
251
+ def __set_properties(self):
252
+ self.button_create.SetToolTip(_("Creates the recognized contour elements / placements"))
253
+ self.check_enable_contrast.SetToolTip(_("Enable Contrast"))
254
+ self.check_enable_contrast.SetValue(False)
255
+ self.button_reset_contrast.SetToolTip(_("Reset Contrast"))
256
+ self.slider_contrast_contrast.SetToolTip(_("Contrast amount"))
257
+ self.text_contrast_contrast.SetToolTip(_("Contrast the lights and darks by how much?"))
258
+ self.slider_contrast_brightness.SetToolTip(_("Brightness amount"))
259
+ self.text_contrast_brightness.SetToolTip(_("Make the image how much more bright?"))
260
+ self.text_minimum.SetToolTip(_("What is the minimal size of objects (as percentage of the overall area)?"))
261
+ self.text_maximum.SetToolTip(_("What is the maximal size of objects (as percentage of the overall area)?"))
262
+ self.check_inner.SetToolTip(_("Do you want to recognize objects inside of another object?"))
263
+ self.radio_method.SetToolTip(_("Do you want to create the contour itself or the minimal rectangle enclosing it?"))
264
+ self.radio_simplify.SetToolTip(_("Shall we try to reduce the number of points for the created contour?"))
265
+
266
+ self.slider_contrast_brightness.SetMaxSize(wx.Size(200, -1))
267
+ self.slider_contrast_contrast.SetMaxSize(wx.Size(200, -1))
268
+ self.text_contrast_brightness.SetMaxSize(wx.Size(50, -1))
269
+ self.text_contrast_contrast.SetMaxSize(wx.Size(50, -1))
270
+
271
+ self.list_contours.AppendColumn(_("#"), format=wx.LIST_FORMAT_LEFT, width=55)
272
+ self.list_contours.AppendColumn(
273
+ _("Area"), format=wx.LIST_FORMAT_LEFT, width=100
274
+ )
275
+ self.list_contours.resize_columns()
276
+
277
+ self.text_minimum.SetValue("2")
278
+ self.text_maximum.SetValue("95")
279
+ self.radio_method.SetSelection(0)
280
+ self.radio_simplify.SetSelection(0)
281
+ self.reset_contrast()
282
+
283
+ def pane_hide(self):
284
+ self.list_contours.save_column_widths()
285
+ try:
286
+ self.context.contour_placement = self.combo_placement.GetSelection()
287
+ except RuntimeError:
288
+ # Might have already been deleted...
289
+ pass
290
+
291
+ def pane_deactive(self):
292
+ self._pane_is_active = False
293
+
294
+ def pane_active(self):
295
+ self._pane_is_active = True
296
+ if self.auto_update and self._pane_is_active:
297
+ self.refresh_preview()
298
+
299
+ def pane_show(self):
300
+ if self.auto_update and self._pane_is_active:
301
+ self.refresh_preview()
302
+
303
+ def _set_widgets_hidden(self):
304
+ self.Hide()
305
+
306
+ def set_widgets(self, node):
307
+ self.node = node
308
+ if self.node is None:
309
+ return
310
+ self.refresh_preview()
311
+
312
+ def reset_contrast(self):
313
+ contrast = 0
314
+ brightness = 0
315
+ self.slider_contrast_contrast.SetValue(contrast)
316
+ self.text_contrast_contrast.SetValue(str(contrast))
317
+ self.slider_contrast_brightness.SetValue(brightness)
318
+ self.text_contrast_brightness.SetValue(str(brightness))
319
+
320
+ def on_button_reset_contrast(self, event=None):
321
+ self.reset_contrast()
322
+ self.on_control_update(None)
323
+
324
+ def on_slider_contrast_contrast(
325
+ self, event=None
326
+ ):
327
+ contrast = int(self.slider_contrast_contrast.GetValue())
328
+ self.text_contrast_contrast.SetValue(str(contrast))
329
+ if event and (
330
+ not self.context.process_while_sliding
331
+ and wx.GetMouseState().LeftIsDown()
332
+ ):
333
+ event.Skip()
334
+ return
335
+
336
+ self.on_control_update(None)
337
+
338
+ def on_slider_contrast_brightness(self, event=None):
339
+ brightness = int(self.slider_contrast_brightness.GetValue())
340
+ self.text_contrast_brightness.SetValue(str(brightness))
341
+ if event and (
342
+ not self.context.process_while_sliding
343
+ and wx.GetMouseState().LeftIsDown()
344
+ ):
345
+ event.Skip()
346
+ return
347
+ self.on_control_update(None)
348
+
349
+ def on_auto_check(self, event):
350
+ self.auto_update = self.check_auto.GetValue()
351
+ self.context.contour_autoupdate = self.auto_update
352
+
353
+ self.button_update.Enable(not self.auto_update)
354
+ if self.auto_update:
355
+ self.refresh_preview()
356
+
357
+ def on_creation(self, event):
358
+ for idx, (geom, area) in enumerate(self.contours):
359
+ node = PathNode(
360
+ geometry = geom,
361
+ stroke=Color("blue"),
362
+ label=f"Contour {self.node.display_label()} #{idx+1}",
363
+ )
364
+ self.context.elements.elem_branch.add_node(node)
365
+ # print (f"Having added: {node.display_label()}")
366
+ self.context.elements.signal("refresh_scene", "Scene")
367
+
368
+ def on_creation_placement(self, event):
369
+
370
+ def get_place_parameters(geom, method):
371
+ points = list(geom.as_points())
372
+ nx = None
373
+ mx = None
374
+ ny = None
375
+ my = None
376
+ for pt in points:
377
+ if nx is None:
378
+ nx = pt.real
379
+ mx = pt.real
380
+ ny = pt.imag
381
+ my = pt.imag
382
+ else:
383
+ nx = min(nx, pt.real)
384
+ mx = max(mx, pt.real)
385
+ ny = min(ny, pt.imag)
386
+ my = max(my, pt.imag)
387
+
388
+ # The geometry points are order in such a way,
389
+ # that the point with the smallest X-value comes first and then follows the rectangle clockwise
390
+ side1 = geom.distance(points[0], points[1])
391
+ side2 = geom.distance(points[1], points[2])
392
+ if method in (0, 1):
393
+ if method == 0:
394
+ # If the preference is for a long side then our reference point is pt0 if side1 is long and pt1 if side1 is short
395
+ refidx = 0 if side1 < side2 else 1
396
+ else:
397
+ # If the preference is for a short side then our reference point is pt1 if side1 is long and pt0 if side1 is short
398
+ refidx = 1 if side1 < side2 else 0
399
+ ref_x = points[refidx].real
400
+ ref_y = points[refidx].imag
401
+ rotation_angle = geom.angle(points[refidx], points[refidx+1])
402
+ place_corner = 0
403
+ elif method == 2: # center - unrotated
404
+ ref_x = (nx + mx) / 2
405
+ ref_y = (ny + my) / 2
406
+ rotation_angle = 0
407
+ place_corner = 4
408
+ elif method == 3: # center - rotated
409
+ ref_x = (nx + mx) / 2
410
+ ref_y = (ny + my) / 2
411
+ rotation_angle = geom.angle(points[0], points[1])
412
+ place_corner = 4
413
+ return ref_x, ref_y, rotation_angle, place_corner
414
+
415
+ method = self.combo_placement.GetSelection()
416
+ if method < 0:
417
+ return
418
+ for idx, (geom, area) in enumerate(self.contours):
419
+ ref_x, ref_y, rotation_angle, place_corner = get_place_parameters(geom, method)
420
+ self.context.elements.op_branch.add(
421
+ type="place point",
422
+ label=f"Contour #{idx+1}",
423
+ x=ref_x,
424
+ y=ref_y,
425
+ rotation=rotation_angle,
426
+ corner=place_corner
427
+ )
428
+
429
+ self.context.elements.signal("refresh_scene")
430
+
431
+ def on_refresh(self, event):
432
+ self._changed = True
433
+ self.refresh_preview()
434
+
435
+ def on_control_update(self, event=None):
436
+ self._changed = True
437
+ # The following controls will only be used
438
+ # if we have surrounding rectangles selected
439
+ flag = self.radio_method.GetSelection() == 0
440
+ for ctrl in (self.radio_simplify,):
441
+ ctrl.Enable(flag)
442
+ for ctrl in (self.button_create_placement, self.combo_placement):
443
+ ctrl.Enable(not flag)
444
+ if self.auto_update and self._pane_is_active:
445
+ self.refresh_preview()
446
+
447
+ def refresh_preview_job(self):
448
+ if self.make_raster is None or not self._changed:
449
+ return
450
+ # That job may come too late, when the panel has already be destroyed,
451
+ # so just a simple check:
452
+ try:
453
+ _dummy = self.check_invert.GetValue()
454
+ except RuntimeError:
455
+ return
456
+ self.gather_parameters()
457
+ self.update_image()
458
+ self.calculate_contours()
459
+ self.populate_list()
460
+ self.display_contours()
461
+ self._changed = False
462
+
463
+ def refresh_preview(self):
464
+ if self._pane_is_active:
465
+ if self.direct_mode:
466
+ self.update_job()
467
+ else:
468
+ self.context.schedule(self.update_job)
469
+
470
+ def gather_parameters(self):
471
+ self.parameters["img_invert"] = self.check_invert.GetValue()
472
+ self.parameters["img_original"] = self.check_original.GetValue()
473
+ self.parameters["img_usecontrast"] = self.check_enable_contrast.GetValue()
474
+ self.parameters["img_contrast"] = self.slider_contrast_contrast.GetValue()
475
+ self.parameters["img_brightness"] = self.slider_contrast_brightness.GetValue()
476
+ self.parameters["cnt_method"] = self.radio_method.GetSelection()
477
+ self.parameters["cnt_ignoreinner"] = self.check_inner.GetValue()
478
+ try:
479
+ self.parameters["cnt_minimum"] = float(self.text_minimum.GetValue())
480
+ except ValueError:
481
+ self.parameters["cnt_minimum"] = 2
482
+ try:
483
+ self.parameters["cnt_maximum"] = float(self.text_maximum.GetValue())
484
+ except ValueError:
485
+ self.parameters["cnt_maximum"] = 95
486
+ self.parameters["cnt_simplify"] = self.radio_simplify.GetSelection()
487
+ # We are on pixel level
488
+ self.parameters["cnt_threshold"] = 0.25
489
+
490
+ def update_image(self):
491
+ if self.parameters["img_original"]:
492
+ if self.image.mode == "I":
493
+ image = self.image._as_convert_image_to_grayscale()
494
+ else:
495
+ image = self.node.image.convert("L")
496
+ self.matrix = self.node.matrix
497
+ else:
498
+ reapply = False
499
+ remembered_dither = self.node.dither
500
+ if remembered_dither:
501
+ reapply = True
502
+ self.node.dither = False
503
+ self.node.update(None)
504
+
505
+ image = self.node.active_image
506
+ if image is None:
507
+ if reapply:
508
+ self.node.dither = remembered_dither
509
+ self.image = None
510
+ return
511
+ self.matrix = self.node.active_matrix
512
+ if reapply:
513
+ self.node.dither = remembered_dither
514
+ self.node.update(None)
515
+
516
+ if self.parameters["img_invert"]:
517
+ image = ImageOps.invert(image)
518
+ if self.parameters["img_usecontrast"]:
519
+ try:
520
+ contrast = ImageEnhance.Contrast(image)
521
+ c = (self.parameters["img_contrast"] + 128.0) / 128.0
522
+ image = contrast.enhance(c)
523
+ except ValueError:
524
+ # Not available for this type of image
525
+ pass
526
+
527
+ try:
528
+ brightness = ImageEnhance.Brightness(image)
529
+ b = (self.parameters["img_brightness"] + 128.0) / 128.0
530
+ image = brightness.enhance(b)
531
+ except ValueError:
532
+ # Not available for this type of image
533
+ pass
534
+ self.image = image
535
+
536
+ def calculate_contours(self):
537
+ import time
538
+ self.contours.clear()
539
+ if self.image is None:
540
+ return
541
+ t_a = time.perf_counter()
542
+
543
+ method = self.parameters["cnt_method"]
544
+ if method == 0:
545
+ self.contours = img_to_polygons(
546
+ self.image,
547
+ minimal=self.parameters["cnt_minimum"],
548
+ maximal=self.parameters["cnt_maximum"],
549
+ ignoreinner=self.parameters["cnt_ignoreinner"],
550
+ needs_invert=True,
551
+ )
552
+ else:
553
+ self.contours = img_to_rectangles(
554
+ self.image,
555
+ minimal=self.parameters["cnt_minimum"],
556
+ maximal=self.parameters["cnt_maximum"],
557
+ ignoreinner=self.parameters["cnt_ignoreinner"],
558
+ needs_invert=True,
559
+ )
560
+ for idx, (geom, area) in enumerate(self.contours):
561
+ if method == 0:
562
+ simple = self.parameters["cnt_simplify"]
563
+ # We are on pixel level
564
+ threshold = self.parameters["cnt_threshold"]
565
+ if simple==1:
566
+ # Let's try Visvalingam line simplification
567
+ geom = geom.simplify_geometry(threshold=threshold)
568
+ elif simple==2:
569
+ # Use Douglas-Peucker instead
570
+ geom = geom.simplify(threshold)
571
+ geom.transform(self.matrix)
572
+ self.contours[idx] = (geom, area)
573
+
574
+ t_b = time.perf_counter()
575
+
576
+ self.label_info.SetLabel (
577
+ _("Contours generated: {count} in {duration}").format(
578
+ count=len(self.contours),
579
+ duration=f"{t_b-t_a:.2f} sec"
580
+ )
581
+ )
582
+
583
+ def on_resize(self, event):
584
+ event.Skip()
585
+ if self.auto_update and self._pane_is_active:
586
+ idx = self.list_contours.GetFirstSelected()
587
+ self.display_contours(highlight_index=idx)
588
+
589
+ def on_right_click(self, event):
590
+
591
+ def compare_contour(operation, index, this_area, idx, area):
592
+ if operation == 'this':
593
+ return idx == index
594
+ elif operation == 'others':
595
+ return idx != index
596
+ elif operation == 'smaller':
597
+ return area < this_area
598
+ elif operation == 'bigger':
599
+ return area > this_area
600
+ return False
601
+
602
+ def delete_contours(operation, index):
603
+ def handler(event):
604
+ this_area = self.contours[index][1]
605
+ to_be_deleted = [
606
+ idx for idx, (_, area) in enumerate(self.contours)
607
+ if compare_contour(operation, index, this_area, idx, area)
608
+ ]
609
+ for idx in reversed(to_be_deleted):
610
+ self.contours.pop(idx)
611
+ self.populate_list()
612
+ self.display_contours()
613
+ return handler
614
+
615
+ index = self.list_contours.GetFirstSelected()
616
+ if index < 0:
617
+ return
618
+ menu = wx.Menu()
619
+ operations = [
620
+ ('this', _("Delete this contour")),
621
+ ('others', _("Delete all others")),
622
+ ('bigger', _("Delete all bigger")),
623
+ ('smaller', _("Delete all smaller"))
624
+ ]
625
+
626
+ for op, label in operations:
627
+ item = menu.Append(wx.ID_ANY, label)
628
+ self.Bind(wx.EVT_MENU, delete_contours(op, index), item)
629
+
630
+ self.PopupMenu(menu)
631
+ menu.Destroy()
632
+
633
+ def populate_list(self):
634
+ self.list_contours.DeleteAllItems()
635
+ for idx, (geom, area) in enumerate(self.contours):
636
+ list_id = self.list_contours.InsertItem(
637
+ self.list_contours.GetItemCount(), f"#{idx + 1}"
638
+ )
639
+ self.list_contours.SetItem(list_id, 1, f"{area:.2f}%")
640
+
641
+ def display_contours(self, highlight_index = -1):
642
+ if self.make_raster is None:
643
+ return
644
+ if self.image is None:
645
+ self.bitmap_preview.SetBitmap(wx.NullBitmap)
646
+ return
647
+ copynode = ImageNode(image=self.image, matrix=self.matrix, dither=False, prevent_crop=True)
648
+ data = [copynode]
649
+ for idx, (geom, area) in enumerate(self.contours):
650
+ node = PathNode(
651
+ geometry = geom,
652
+ stroke=Color("red") if highlight_index==idx else Color("blue"),
653
+ fill=Color("yellow") if highlight_index==idx else None,
654
+ label=f"Contour {self.node.display_label()} #{idx+1} [{area:.2f}%]",
655
+ )
656
+ data.append(node)
657
+ bounds = Node.union_bounds(data, attr="bounds")
658
+ width, height = self.bitmap_preview.GetClientSize()
659
+ while width > 1024 or height > 1024:
660
+ width = width // 2
661
+ height = height // 2
662
+ width = max(1, width)
663
+ height = max(1, height)
664
+ try:
665
+ bit_map = self.make_raster(
666
+ data,
667
+ bounds,
668
+ width=width,
669
+ height=height,
670
+ bitmap=True,
671
+ keep_ratio=True,
672
+ )
673
+ except Exception:
674
+ return
675
+ self.bitmap_preview.SetBitmap(bit_map)
676
+
677
+ def on_list_selection(self, event):
678
+ idx = self.list_contours.GetFirstSelected()
679
+ self.display_contours(highlight_index=idx)
680
+
681
+ class KeyholePanel(wx.Panel):
682
+ name = _("Keyhole")
683
+ priority = 5
684
+
685
+ @staticmethod
686
+ def accepts(node):
687
+ return hasattr(node, "as_image")
688
+
689
+ def __init__(self, *args, context=None, node=None, **kwds):
690
+ # begin wxGlade: LayerSettingPanel.__init__
691
+ kwds["style"] = kwds.get("style", 0)
692
+ wx.Panel.__init__(self, *args, **kwds)
693
+ self.context = context
694
+ self.context.themes.set_window_colors(self)
695
+ self.node = node
696
+ self.button_release = wxButton(self, wx.ID_ANY, _("Remove keyhole"))
697
+ self.__set_properties()
698
+ self.__do_layout()
699
+ self.button_release.Bind(wx.EVT_BUTTON, self.on_release)
700
+ self.set_widgets(self.node)
701
+
702
+ def __do_layout(self):
703
+ # begin wxGlade: PositionPanel.__do_layout
704
+ sizer_main = wx.BoxSizer(wx.VERTICAL)
705
+ sizer_release = StaticBoxSizer(
706
+ self, wx.ID_ANY, _("Keyhole:"), wx.HORIZONTAL
707
+ )
708
+ sizer_release.Add(self.button_release, 1, wx.ALIGN_CENTER_VERTICAL, 0)
709
+
710
+ sizer_main.Add(sizer_release, 0, wx.EXPAND, 0)
711
+
712
+ self.SetSizer(sizer_main)
713
+ sizer_main.Fit(self)
714
+ self.Layout()
715
+
716
+ def __set_properties(self):
717
+ self.button_release.SetToolTip(
718
+ _(
719
+ "Remove the keyhole and show the complete image"
720
+ )
721
+ )
722
+
723
+ def pane_hide(self):
724
+ pass
725
+
726
+ def pane_show(self):
727
+ pass
728
+
729
+ def _set_widgets_hidden(self):
730
+ self.Hide()
731
+
732
+ def set_widgets(self, node):
733
+ self.node = node
734
+ if self.node.keyhole_reference is None:
735
+ self.button_release.Enable(False)
736
+ else:
737
+ self.button_release.Enable(True)
738
+ self.Show()
739
+
740
+ def on_release(self, event):
741
+ elements = self.context.elements
742
+ rid = self.node.keyhole_reference
743
+ elements.deregister_keyhole(rid, self.node)
744
+ self.set_widgets(self.node)
745
+ elements.process_keyhole_updates(self.context)
746
+
747
+
748
+ class CropPanel(wx.Panel):
749
+ name = _("Crop")
750
+ priority = 5
751
+
752
+ @staticmethod
753
+ def accepts(node):
754
+ if not hasattr(node, "as_image"):
755
+ return False
756
+ return any(n.get("name") == "crop" for n in node.operations)
757
+
758
+ def __init__(self, *args, context=None, node=None, **kwds):
759
+ kwds["style"] = kwds.get("style", 0) | wx.TAB_TRAVERSAL
760
+ wx.Panel.__init__(self, *args, **kwds)
761
+ self.context = context
762
+ self.context.themes.set_window_colors(self)
763
+ self.node = node
764
+ self._width = None
765
+ self._height = None
766
+ self._bounds = None
767
+ self._cropleft = 0
768
+ self._cropright = 0
769
+ self._cropbottom = 0
770
+ self._croptop = 0
771
+ self.op = None
772
+ self._no_update = False
773
+
774
+ self.check_enable_crop = wxCheckBox(self, wx.ID_ANY, _("Enable"))
775
+ self.button_reset = wxButton(self, wx.ID_ANY, _("Reset"))
776
+
777
+ self.label_info = wxStaticText(self, wx.ID_ANY, "--")
778
+
779
+ self.slider_left = wx.Slider(
780
+ self, wx.ID_ANY, 0, -127, 127, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
781
+ )
782
+ self.slider_right = wx.Slider(
783
+ self, wx.ID_ANY, 0, -127, 127, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
784
+ )
785
+ self.slider_top = wx.Slider(
786
+ self, wx.ID_ANY, 0, -127, 127, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
787
+ )
788
+ self.slider_bottom = wx.Slider(
789
+ self, wx.ID_ANY, 0, -127, 127, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
790
+ )
791
+ self.text_left = TextCtrl(self, wx.ID_ANY, style=wx.TE_READONLY)
792
+ self.text_right = TextCtrl(self, wx.ID_ANY, style=wx.TE_READONLY)
793
+ self.text_top = TextCtrl(self, wx.ID_ANY, style=wx.TE_READONLY)
794
+ self.text_bottom = TextCtrl(self, wx.ID_ANY, style=wx.TE_READONLY)
795
+
796
+ self.__set_properties()
797
+ self.__do_layout()
798
+
799
+ self.Bind(wx.EVT_BUTTON, self.on_button_reset, self.button_reset)
800
+ self.Bind(wx.EVT_CHECKBOX, self.on_check_enable_crop, self.check_enable_crop)
801
+ self.Bind(wx.EVT_SLIDER, self.on_slider_left, self.slider_left)
802
+ self.Bind(wx.EVT_SLIDER, self.on_slider_right, self.slider_right)
803
+ self.Bind(wx.EVT_SLIDER, self.on_slider_top, self.slider_top)
804
+ self.Bind(wx.EVT_SLIDER, self.on_slider_bottom, self.slider_bottom)
805
+
806
+ flag = False
807
+ self.activate_controls(flag)
808
+ self.set_widgets(node)
809
+
810
+ def activate_controls(self, flag):
811
+ self.button_reset.Enable(flag)
812
+ self.slider_left.Enable(flag)
813
+ self.slider_right.Enable(flag)
814
+ self.slider_top.Enable(flag)
815
+ self.slider_bottom.Enable(flag)
816
+
817
+ def set_widgets(self, node):
818
+ if self.node is None:
819
+ self.label_info.SetLabel("")
820
+ self.Hide()
821
+ return
822
+ else:
823
+ self.Show()
824
+ self._no_update = True
825
+ self.node = node
826
+ self.op = None
827
+ self._width, self._height = self.node.image.size
828
+ for ctl in (self.slider_left, self.slider_right):
829
+ ctl.SetMin(0)
830
+ ctl.SetMax(self._width)
831
+ for ctl in (self.slider_top, self.slider_bottom):
832
+ ctl.SetMin(0)
833
+ ctl.SetMax(self._height)
834
+
835
+ self._bounds = [0, 0, self._width, self._height]
836
+ flag = False
837
+ for n in node.operations:
838
+ if n.get("name") == "crop":
839
+ self.op = n
840
+ break
841
+ self._width, self._height = self.node.image.size
842
+ self.label_info.SetLabel(f"{self._width} x {self._height} px")
843
+ if self.op is not None:
844
+ flag = self.op["enable"]
845
+ self._bounds = self.op["bounds"]
846
+ if self._bounds is None:
847
+ self._bounds = [0, 0, self._width, self._height]
848
+ self.op["bounds"] = self._bounds
849
+
850
+ self.check_enable_crop.SetValue(flag)
851
+ self.activate_controls(flag)
852
+ # We need to set the internal variables, as otherwise recalc will take place
853
+ self._cropleft = self._bounds[0]
854
+ self._cropright = self._width - self._bounds[2]
855
+
856
+ self._croptop = self._bounds[1]
857
+ self._cropbottom = self._height - self._bounds[3]
858
+ # print (f"From {self._bounds} to l={self.cropleft}, r={self.cropright}, t={self.croptop}, b={self.cropbottom}")
859
+
860
+ self.set_slider_limits("lrtb", False)
861
+
862
+ self._no_update = False
863
+
864
+ def __set_properties(self):
865
+ self.slider_left.SetToolTip(
866
+ _("How many pixels do you want to crop from the left?")
867
+ )
868
+ self.slider_right.SetToolTip(
869
+ _("How many pixels do you want to crop from the right?")
870
+ )
871
+ self.slider_top.SetToolTip(
872
+ _("How many pixels do you want to crop from the top?")
873
+ )
874
+ self.slider_bottom.SetToolTip(
875
+ _("How many pixels do you want to crop from the bottom?")
876
+ )
877
+
878
+ def __do_layout(self):
879
+ # begin wxGlade: ContrastPanel.__do_layout
880
+ sizer_main = StaticBoxSizer(self, wx.ID_ANY, _("Image-Dimensions"), wx.VERTICAL)
881
+ sizer_info = wx.BoxSizer(wx.HORIZONTAL)
882
+ sizer_info.Add(self.check_enable_crop, 1, wx.ALIGN_CENTER_VERTICAL, 0)
883
+ sizer_info.Add(self.button_reset, 0, wx.EXPAND, 0)
884
+ sizer_info.Add(self.label_info, 0, wx.ALIGN_CENTER_VERTICAL, 0)
885
+
886
+ sizer_left = wx.BoxSizer(wx.HORIZONTAL)
887
+ sizer_right = wx.BoxSizer(wx.HORIZONTAL)
888
+ sizer_top = wx.BoxSizer(wx.HORIZONTAL)
889
+ sizer_bottom = wx.BoxSizer(wx.HORIZONTAL)
890
+
891
+ lbl_left = wxStaticText(self, wx.ID_ANY, _("Left"))
892
+ lbl_left.SetMinSize(dip_size(self, 60, -1))
893
+ lbl_right = wxStaticText(self, wx.ID_ANY, _("Right"))
894
+ lbl_right.SetMinSize(dip_size(self, 60, -1))
895
+ lbl_bottom = wxStaticText(self, wx.ID_ANY, _("Bottom"))
896
+ lbl_bottom.SetMinSize(dip_size(self, 60, -1))
897
+ lbl_top = wxStaticText(self, wx.ID_ANY, _("Top"))
898
+ lbl_top.SetMinSize(dip_size(self, 60, -1))
899
+
900
+ self.text_left.SetMaxSize(dip_size(self, 60, -1))
901
+ self.text_right.SetMaxSize(dip_size(self, 60, -1))
902
+ self.text_top.SetMaxSize(dip_size(self, 60, -1))
903
+ self.text_bottom.SetMaxSize(dip_size(self, 60, -1))
904
+
905
+ sizer_left.Add(lbl_left, 0, wx.ALIGN_CENTER_VERTICAL)
906
+ sizer_left.Add(self.slider_left, 4, wx.ALIGN_CENTER_VERTICAL)
907
+ sizer_left.Add(self.text_left, 1, wx.ALIGN_CENTER_VERTICAL)
908
+
909
+ sizer_right.Add(lbl_right, 0, wx.ALIGN_CENTER_VERTICAL)
910
+ sizer_right.Add(self.slider_right, 4, wx.ALIGN_CENTER_VERTICAL)
911
+ sizer_right.Add(self.text_right, 1, wx.ALIGN_CENTER_VERTICAL)
912
+
913
+ sizer_top.Add(lbl_top, 0, wx.ALIGN_CENTER_VERTICAL)
914
+ sizer_top.Add(self.slider_top, 4, wx.ALIGN_CENTER_VERTICAL)
915
+ sizer_top.Add(self.text_top, 1, wx.ALIGN_CENTER_VERTICAL)
916
+
917
+ sizer_bottom.Add(lbl_bottom, 0, wx.ALIGN_CENTER_VERTICAL)
918
+ sizer_bottom.Add(self.slider_bottom, 4, wx.ALIGN_CENTER_VERTICAL)
919
+ sizer_bottom.Add(self.text_bottom, 1, wx.ALIGN_CENTER_VERTICAL)
920
+
921
+ sizer_main.Add(sizer_info, 0, wx.EXPAND, 0)
922
+ sizer_main.Add(sizer_left, 0, wx.EXPAND, 0)
923
+ sizer_main.Add(sizer_right, 0, wx.EXPAND, 0)
924
+ sizer_main.Add(sizer_top, 0, wx.EXPAND, 0)
925
+ sizer_main.Add(sizer_bottom, 0, wx.EXPAND, 0)
926
+
927
+ self.SetSizer(sizer_main)
928
+ sizer_main.Fit(self)
929
+ self.Layout()
930
+
931
+ def on_button_reset(self, event):
932
+ if self.node is None:
933
+ return
934
+ w, h = self.node.image.size
935
+ self._bounds = [0, 0, w, h]
936
+ self._cropleft = 0
937
+ self._cropright = 0
938
+ self._croptop = 0
939
+ self._cropbottom = 0
940
+ self.op["bounds"] = self._bounds
941
+ self.set_slider_limits("lrtb")
942
+ self.context.elements.do_image_update(self.node, self.context)
943
+
944
+
945
+ def on_check_enable_crop(self, event=None):
946
+ flag = self.check_enable_crop.GetValue()
947
+ if flag:
948
+ if self.op is None:
949
+ w, h = self.node.image.size
950
+ self._width = w
951
+ self._height = h
952
+ self.op = {"name": "crop", "enable": True, "bounds": [0, 0, w, h]}
953
+ self.node.operations.append(self.op)
954
+ self.set_slider_limits("lrtb", False)
955
+ last = self._no_update
956
+ self._no_update = True
957
+ self.cropleft = 0
958
+ self.cropright = 0
959
+ self.croptop = 0
960
+ self.cropbottom = 0
961
+ self._no_update = last
962
+ else:
963
+ self.op["enable"] = flag
964
+ else:
965
+ if self.op is not None:
966
+ self.op["enable"] = flag
967
+ if self.op is not None and not self._no_update:
968
+ self.context.elements.do_image_update(self.node, self.context)
969
+
970
+ self.activate_controls(flag)
971
+
972
+ def on_slider_left(self, event=None):
973
+ if event and (
974
+ not self.context.process_while_sliding
975
+ and wx.GetMouseState().LeftIsDown()
976
+ ):
977
+ event.Skip()
978
+ return
979
+ self.cropleft = self.slider_left.GetValue()
980
+
981
+ def on_slider_right(self, event=None):
982
+ if event and (
983
+ not self.context.process_while_sliding
984
+ and wx.GetMouseState().LeftIsDown()
985
+ ):
986
+ event.Skip()
987
+ return
988
+ self.cropright = self.slider_right.GetValue()
989
+
990
+ def on_slider_top(self, event=None):
991
+ # Wait until the user has stopped to move the slider
992
+ if event and (
993
+ not self.context.process_while_sliding
994
+ and wx.GetMouseState().LeftIsDown()
995
+ ):
996
+ event.Skip()
997
+ return
998
+ self.croptop = self.slider_top.GetValue()
999
+
1000
+ def on_slider_bottom(self, event=None):
1001
+ if event and (
1002
+ not self.context.process_while_sliding
1003
+ and wx.GetMouseState().LeftIsDown()
1004
+ ):
1005
+ event.Skip()
1006
+ return
1007
+ self.cropbottom = self.slider_bottom.GetValue()
1008
+
1009
+ def set_slider_limits(self, pattern, constraint=True):
1010
+ if "l" in pattern:
1011
+ value = self._width - self.cropright
1012
+ self.slider_left.SetMin(0)
1013
+ self.slider_left.SetMax(value - 1 if constraint else self._width)
1014
+ if self.cropleft != self.slider_left.GetValue():
1015
+ self.slider_left.SetValue(int(self.cropleft))
1016
+ dvalue = self.cropleft
1017
+ if dvalue == 0:
1018
+ self.text_left.SetValue("---")
1019
+ else:
1020
+ self.text_left.SetValue(f"> {dvalue} px")
1021
+ if "r" in pattern:
1022
+ value = self._width - self.cropleft
1023
+ self.slider_right.SetMin(0)
1024
+ self.slider_right.SetMax(value - 1 if constraint else self._width)
1025
+ if self.cropright != self.slider_right.GetValue():
1026
+ self.slider_right.SetValue(int(self.cropright))
1027
+ dvalue = self.cropright
1028
+ if dvalue == 0:
1029
+ self.text_right.SetValue("---")
1030
+ else:
1031
+ self.text_right.SetValue(f"> {dvalue} px")
1032
+ if "t" in pattern:
1033
+ value = self._height - self.cropbottom
1034
+ self.slider_top.SetMin(0)
1035
+ self.slider_top.SetMax(value - 1 if constraint else self._height)
1036
+ if self.croptop != self.slider_top.GetValue():
1037
+ self.slider_top.SetValue(int(self.croptop))
1038
+ dvalue = self.croptop
1039
+ if dvalue == 0:
1040
+ self.text_top.SetValue("---")
1041
+ else:
1042
+ self.text_top.SetValue(f"> {dvalue} px")
1043
+ if "b" in pattern:
1044
+ value = self._height - self.croptop
1045
+ self.slider_bottom.SetMin(0)
1046
+ self.slider_bottom.SetMax(value - 1 if constraint else self._height)
1047
+ if self.cropbottom != self.slider_bottom.GetValue():
1048
+ self.slider_bottom.SetValue(int(self.cropbottom))
1049
+ dvalue = self.cropbottom
1050
+ if dvalue == 0:
1051
+ self.text_bottom.SetValue("---")
1052
+ else:
1053
+ self.text_bottom.SetValue(f"> {dvalue} px")
1054
+
1055
+
1056
+ def _setbounds(self):
1057
+ if self.op is None:
1058
+ return
1059
+
1060
+ self.op["bounds"][0] = self.cropleft
1061
+ self.op["bounds"][2] = self._width - self.cropright
1062
+ self.op["bounds"][1] = self.croptop
1063
+ self.op["bounds"][3] = self._height - self.cropbottom
1064
+ self._bounds = self.op["bounds"]
1065
+ # print (f"width: {self._width} from left: {self.cropleft}, from right {self.cropright}: {self.op['bounds'][0]} - {self.op['bounds'][2]}")
1066
+ # print (f"height: {self._height} from top: {self.croptop}, from bottom {self.cropbottom}: {self.op['bounds'][1]} - {self.op['bounds'][3]}")
1067
+ if not self._no_update:
1068
+ self.context.elements.do_image_update(self.node, self.context)
1069
+
1070
+ @property
1071
+ def cropleft(self):
1072
+ return self._cropleft
1073
+
1074
+ @cropleft.setter
1075
+ def cropleft(self, value):
1076
+ self._cropleft = value
1077
+ if self.slider_left.GetValue() != value:
1078
+ self.slider_left.SetValue(int(value))
1079
+ if value == 0:
1080
+ self.text_left.SetValue("---")
1081
+ else:
1082
+ self.text_left.SetValue(f"> {value} px")
1083
+ # We need to adjust the boundaries of the right slider.
1084
+ self.set_slider_limits("r")
1085
+ self._setbounds()
1086
+
1087
+ @property
1088
+ def cropright(self):
1089
+ return self._cropright
1090
+
1091
+ @cropright.setter
1092
+ def cropright(self, value):
1093
+ self._cropright = value
1094
+ if self.slider_right.GetValue() != value:
1095
+ self.slider_right.SetValue(int(value))
1096
+ if value == 0:
1097
+ self.text_right.SetValue("---")
1098
+ else:
1099
+ self.text_right.SetValue(f"{value} px <")
1100
+ # We need to adjust the boundaries of the left slider.
1101
+ self.set_slider_limits("l")
1102
+ self._setbounds()
1103
+
1104
+ @property
1105
+ def croptop(self):
1106
+ return self._croptop
1107
+
1108
+ @croptop.setter
1109
+ def croptop(self, value):
1110
+ # print(f"Set top to: {value}")
1111
+ self._croptop = value
1112
+ if self.slider_top.GetValue() != value:
1113
+ self.slider_top.SetValue(int(value))
1114
+ if value == 0:
1115
+ self.text_top.SetValue("---")
1116
+ else:
1117
+ self.text_top.SetValue(f"> {value} px")
1118
+ # We need to adjust the boundaries of the bottom slider.
1119
+ self.set_slider_limits("b")
1120
+ self._setbounds()
1121
+
1122
+ @property
1123
+ def cropbottom(self):
1124
+ return self._cropbottom
1125
+
1126
+ @cropbottom.setter
1127
+ def cropbottom(self, value):
1128
+ # print(f"Set top to: {value}")
1129
+ self._cropbottom = value
1130
+ if self.slider_bottom.GetValue() != value:
1131
+ self.slider_bottom.SetValue(int(value))
1132
+ if value == 0:
1133
+ self.text_bottom.SetValue("---")
1134
+ else:
1135
+ self.text_bottom.SetValue(f"{value} px <")
1136
+ # We need to adjust the boundaries of the top slider.
1137
+ self.set_slider_limits("t")
1138
+ self._setbounds()
1139
+
1140
+ class ImageModificationPanel(ScrolledPanel):
1141
+ name = _("Modification")
1142
+ priority = 90
1143
+
1144
+ def __init__(self, *args, context=None, node=None, **kwargs):
1145
+ # begin wxGlade: ConsolePanel.__init__
1146
+ kwargs["style"] = kwargs.get("style", 0) | wx.TAB_TRAVERSAL
1147
+ wx.Panel.__init__(self, *args, **kwargs)
1148
+ self.context = context
1149
+ self.context.themes.set_window_colors(self)
1150
+ self.node = node
1151
+ self.scripts = []
1152
+ choices = []
1153
+ for entry in list(self.context.match("raster_script/.*", suffix=True)):
1154
+ self.scripts.append(entry)
1155
+ choices.append(_("Apply {entry}").format(entry=entry))
1156
+ self.combo_scripts = wxComboBox(
1157
+ self, wx.ID_ANY, choices=choices, style=wx.CB_READONLY | wx.CB_DROPDOWN
1158
+ )
1159
+ self.combo_scripts.SetSelection(0)
1160
+ self.button_apply = wxButton(self, wx.ID_ANY, _("Apply Script"))
1161
+ self.button_apply.SetToolTip(
1162
+ _("Apply image modification script\nRight click: append to existing script")
1163
+ )
1164
+ self.button_clear = wxButton(self, wx.ID_ANY, _("Clear"))
1165
+ self.button_clear.SetToolTip(_("Remove all image operations"))
1166
+ self.list_operations = wxListCtrl(
1167
+ self,
1168
+ wx.ID_ANY,
1169
+ style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES | wx.LC_SINGLE_SEL,
1170
+ context=self.context, list_name="list_imageoperations",
1171
+ )
1172
+
1173
+ self._do_layout()
1174
+ self._do_logic()
1175
+ self.set_widgets(node)
1176
+
1177
+ def _do_layout(self):
1178
+ self.list_operations.AppendColumn(_("#"), format=wx.LIST_FORMAT_LEFT, width=58)
1179
+ self.list_operations.AppendColumn(
1180
+ _("Action"), format=wx.LIST_FORMAT_LEFT, width=65
1181
+ )
1182
+ self.list_operations.AppendColumn(
1183
+ _("Active"), format=wx.LIST_FORMAT_LEFT, width=25
1184
+ )
1185
+ self.list_operations.AppendColumn(
1186
+ _("Parameters"), format=wx.LIST_FORMAT_LEFT, width=95
1187
+ )
1188
+ self.list_operations.resize_columns()
1189
+ sizer_main = wx.BoxSizer(wx.VERTICAL)
1190
+ sizer_script = StaticBoxSizer(self, wx.ID_ANY, _("Raster-Wizard"), wx.HORIZONTAL)
1191
+
1192
+ sizer_script.Add(self.combo_scripts, 1, wx.EXPAND, 0)
1193
+ sizer_script.Add(self.button_apply, 0, wx.EXPAND, 0)
1194
+ sizer_script.Add(self.button_clear, 0, wx.EXPAND, 0)
1195
+ sizer_main.Add(sizer_script, 0, wx.EXPAND, 0)
1196
+ sizer_main.Add(self.list_operations, 1, wx.EXPAND, 0)
1197
+
1198
+ self.SetSizer(sizer_main)
1199
+ self.Layout()
1200
+ self.Centre()
1201
+
1202
+ def _do_logic(self):
1203
+ self.button_apply.Bind(wx.EVT_BUTTON, self.on_apply_replace)
1204
+ self.button_apply.Bind(wx.EVT_RIGHT_DOWN, self.on_apply_append)
1205
+ self.button_clear.Bind(wx.EVT_BUTTON, self.on_clear)
1206
+ self.list_operations.Bind(wx.EVT_RIGHT_DOWN, self.on_list_menu)
1207
+
1208
+ @staticmethod
1209
+ def accepts(node):
1210
+ return hasattr(node, "as_image")
1211
+
1212
+ def set_widgets(self, node=None):
1213
+ self.node = node
1214
+ if node is None:
1215
+ return
1216
+ self.fill_operations()
1217
+
1218
+ def fill_operations(self):
1219
+ self.list_operations.DeleteAllItems()
1220
+ idx = 0
1221
+ for op in self.node.operations:
1222
+ idx += 1
1223
+ list_id = self.list_operations.InsertItem(
1224
+ self.list_operations.GetItemCount(), f"#{idx}"
1225
+ )
1226
+ self.list_operations.SetItem(list_id, 1, op["name"])
1227
+ self.list_operations.SetItem(list_id, 2, "x" if op["enable"] else "-")
1228
+ self.list_operations.SetItem(list_id, 3, str(op))
1229
+
1230
+ def on_clear(self, event):
1231
+ self.node.operations = []
1232
+ self.update_node()
1233
+
1234
+ def apply_script(self, index, addition):
1235
+ if index < 0 or index >= len(self.scripts):
1236
+ return
1237
+ script = self.scripts[index]
1238
+ raster_script = self.context.lookup(f"raster_script/{script}")
1239
+ if not addition:
1240
+ self.node.operations = []
1241
+ for entry in raster_script:
1242
+ self.node.operations.append(entry)
1243
+ self.update_node()
1244
+
1245
+ def update_node(self):
1246
+ self.context.elements.emphasized()
1247
+ self.context.elements.do_image_update(self.node, self.context)
1248
+ self.context.signal("element_property_force", self.node)
1249
+ # self.context.signal("selected", self.node)
1250
+ self.fill_operations()
1251
+
1252
+ def on_apply_replace(self, event):
1253
+ idx = self.combo_scripts.GetSelection()
1254
+ if idx >= 0:
1255
+ self.apply_script(idx, False)
1256
+
1257
+ def on_apply_append(self, event):
1258
+ idx = self.combo_scripts.GetSelection()
1259
+ if idx >= 0:
1260
+ self.apply_script(idx, True)
1261
+
1262
+ def on_list_menu(self, event):
1263
+ def on_delete(index):
1264
+ def check(event):
1265
+ self.node.operations.pop(index)
1266
+ self.update_node()
1267
+
1268
+ return check
1269
+
1270
+ def on_enable(index):
1271
+ def check(event):
1272
+ self.node.operations[index]["enable"] = not self.node.operations[index][
1273
+ "enable"
1274
+ ]
1275
+ self.update_node()
1276
+
1277
+ return check
1278
+
1279
+ def on_op_insert(index, op):
1280
+ def check(event):
1281
+ self.node.operations.insert(index, op)
1282
+ self.update_node()
1283
+
1284
+ return check
1285
+
1286
+ def on_op_append(index, op):
1287
+ def check(event):
1288
+ self.node.operations.append(op)
1289
+ self.update_node()
1290
+
1291
+ return check
1292
+
1293
+ selected = self.list_operations.GetFirstSelected()
1294
+
1295
+ possible_ops = [
1296
+ {"name": "crop", "enable": True, "bounds": None},
1297
+ {
1298
+ "name": "grayscale",
1299
+ "enable": True,
1300
+ "invert": False,
1301
+ "red": 1.0,
1302
+ "green": 1.0,
1303
+ "blue": 1.0,
1304
+ "lightness": 1.0,
1305
+ },
1306
+ {"name": "auto_contrast", "enable": True, "cutoff": 3},
1307
+ {"name": "contrast", "enable": True, "contrast": 25, "brightness": 25},
1308
+ {
1309
+ "name": "unsharp_mask",
1310
+ "enable": True,
1311
+ "percent": 500,
1312
+ "radius": 4,
1313
+ "threshold": 0,
1314
+ },
1315
+ {
1316
+ "name": "tone",
1317
+ "type": "spline",
1318
+ "enable": True,
1319
+ "values": [[0, 0], [100, 150], [255, 255]],
1320
+ },
1321
+ {"name": "gamma", "enable": True, "factor": 3.5},
1322
+ {"name": "edge_enhance", "enable": False},
1323
+ {
1324
+ "name": "halftone",
1325
+ "enable": True,
1326
+ "black": True,
1327
+ "sample": 10,
1328
+ "angle": 22,
1329
+ "oversample": 2,
1330
+ },
1331
+ {"name": "dither", "enable": True, "type": "Floyd-Steinberg"},
1332
+ ]
1333
+ devmode = self.context.root.setting(bool, "developer_mode", False)
1334
+ menu = wx.Menu()
1335
+ if selected >= 0:
1336
+ # Edit-Part
1337
+ menuitem = menu.Append(
1338
+ wx.ID_ANY, _("Delete item"), _("Will delete the current entry")
1339
+ )
1340
+ self.Bind(wx.EVT_MENU, on_delete(selected), id=menuitem.GetId())
1341
+
1342
+ menuitem = menu.Append(
1343
+ wx.ID_ANY,
1344
+ _("Enable"),
1345
+ _("Toggles enable-status of operation"),
1346
+ kind=wx.ITEM_CHECK,
1347
+ )
1348
+ menuitem.Check(self.node.operations[selected]["enable"])
1349
+ self.Bind(wx.EVT_MENU, on_enable(selected), id=menuitem.GetId())
1350
+ if devmode:
1351
+ menu.AppendSeparator()
1352
+ for op in possible_ops:
1353
+ menuitem = menu.Append(
1354
+ wx.ID_ANY,
1355
+ _("Insert {op}").format(op=op["name"]),
1356
+ _("Will insert this operation before the current entry"),
1357
+ )
1358
+ self.Bind(
1359
+ wx.EVT_MENU, on_op_insert(selected, op), id=menuitem.GetId()
1360
+ )
1361
+ menu.AppendSeparator()
1362
+ if devmode:
1363
+ for op in possible_ops:
1364
+ menuitem = menu.Append(
1365
+ wx.ID_ANY,
1366
+ _("Append {op}").format(op=op["name"]),
1367
+ _("Will append this operation to the end of the list"),
1368
+ )
1369
+ self.Bind(wx.EVT_MENU, on_op_append(selected, op), id=menuitem.GetId())
1370
+
1371
+ if menu.MenuItemCount != 0:
1372
+ self.PopupMenu(menu)
1373
+ menu.Destroy()
1374
+
1375
+ def pane_show(self):
1376
+ self.list_operations.load_column_widths()
1377
+ self.fill_operations()
1378
+
1379
+ def pane_active(self):
1380
+ self.fill_operations()
1381
+
1382
+ def pane_hide(self):
1383
+ self.list_operations.save_column_widths()
1384
+
1385
+ def signal(self, signalstr, myargs):
1386
+ return
1387
+
1388
+ class ImageVectorisationPanel(ScrolledPanel):
1389
+ name = _("Vectorisation")
1390
+ priority = 95
1391
+
1392
+ def __init__(self, *args, context=None, node=None, **kwargs):
1393
+ # begin wxGlade: ConsolePanel.__init__
1394
+ kwargs["style"] = kwargs.get("style", 0) | wx.TAB_TRAVERSAL
1395
+ wx.Panel.__init__(self, *args, **kwargs)
1396
+ self.context = context
1397
+ self.context.themes.set_window_colors(self)
1398
+ self.node = node
1399
+ main_sizer = wx.BoxSizer(wx.HORIZONTAL)
1400
+ # self.vector_lock = threading.Lock()
1401
+ # self.alive = True
1402
+ # Only display if we have a vector engine
1403
+ self._pane_is_active = False
1404
+ make_vector = self.context.kernel.lookup("render-op/make_vector")
1405
+ if make_vector:
1406
+ global HAS_VECTOR_ENGINE
1407
+ HAS_VECTOR_ENGINE = True
1408
+ if not make_vector:
1409
+ main_sizer.Add(
1410
+ wxStaticText(
1411
+ self, wx.ID_ANY, "No vector engine installed, you need potrace"
1412
+ ),
1413
+ 1,
1414
+ wx.EXPAND,
1415
+ 0,
1416
+ )
1417
+ self.SetSizer(main_sizer)
1418
+ main_sizer.Fit(self)
1419
+ self.Layout()
1420
+ self.Centre()
1421
+ return
1422
+
1423
+ sizer_options = StaticBoxSizer(self, wx.ID_ANY, _("Options"), wx.VERTICAL)
1424
+ main_sizer.Add(sizer_options, 1, wx.EXPAND, 0)
1425
+
1426
+ sizer_turn = wx.BoxSizer(wx.HORIZONTAL)
1427
+ sizer_options.Add(sizer_turn, 0, wx.EXPAND, 0)
1428
+
1429
+ label_turn = wxStaticText(self, wx.ID_ANY, _("Turnpolicy"))
1430
+ label_turn.SetMinSize(dip_size(self, 70, -1))
1431
+ sizer_turn.Add(label_turn, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1432
+ self.turn_choices = [
1433
+ "Black",
1434
+ "White",
1435
+ "Left",
1436
+ "Right",
1437
+ "Minority",
1438
+ "Majority",
1439
+ "Random",
1440
+ ]
1441
+ self.combo_turnpolicy = wxComboBox(
1442
+ self,
1443
+ wx.ID_ANY,
1444
+ choices=self.turn_choices,
1445
+ style=wx.CB_DROPDOWN | wx.CB_READONLY,
1446
+ )
1447
+ self.combo_turnpolicy.SetToolTip(
1448
+ _(
1449
+ "This parameter determines how to resolve ambiguities during decomposition of bitmaps into paths.\n\n"
1450
+ + "BLACK: prefers to connect black (foreground) components.\n"
1451
+ + "WHITE: prefers to connect white (background) components.\n"
1452
+ + "LEFT: always take a left turn.\n"
1453
+ + "RIGHT: always take a right turn.\n"
1454
+ + "MINORITY: prefers to connect the color (black or white) that occurs least frequently in a local neighborhood of the current position.\n"
1455
+ + "MAJORITY: prefers to connect the color (black or white) that occurs most frequently in a local neighborhood of the current position.\n"
1456
+ + "RANDOM: choose randomly."
1457
+ )
1458
+ )
1459
+ self.combo_turnpolicy.SetSelection(4)
1460
+ sizer_turn.Add(self.combo_turnpolicy, 1, wx.ALIGN_CENTER_VERTICAL, 0)
1461
+
1462
+ sizer_turd = wx.BoxSizer(wx.HORIZONTAL)
1463
+ sizer_options.Add(sizer_turd, 0, wx.EXPAND, 0)
1464
+
1465
+ label_turd = wxStaticText(self, wx.ID_ANY, _("Despeckle"))
1466
+ label_turd.SetMinSize(dip_size(self, 70, -1))
1467
+ sizer_turd.Add(label_turd, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1468
+
1469
+ self.slider_turdsize = wx.Slider(self, wx.ID_ANY, 2, 0, 10)
1470
+ self.slider_turdsize.SetToolTip(
1471
+ _("Suppress speckles of up to this size (default 2 px)")
1472
+ )
1473
+ sizer_turd.Add(self.slider_turdsize, 1, wx.EXPAND, 0)
1474
+
1475
+ sizer_alphamax = wx.BoxSizer(wx.HORIZONTAL)
1476
+ sizer_options.Add(sizer_alphamax, 0, wx.EXPAND, 0)
1477
+
1478
+ label_alphamax = wxStaticText(self, wx.ID_ANY, _("Corners"))
1479
+ label_alphamax.SetMinSize(dip_size(self, 70, -1))
1480
+ sizer_alphamax.Add(label_alphamax, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1481
+
1482
+ self.slider_alphamax = wx.Slider(self, wx.ID_ANY, 9, 0, 12)
1483
+ self.slider_alphamax.SetToolTip(
1484
+ _(
1485
+ "This parameter is a threshold for the detection of corners. It controls the smoothness of the traced curve."
1486
+ )
1487
+ )
1488
+ sizer_alphamax.Add(self.slider_alphamax, 1, wx.EXPAND, 0)
1489
+
1490
+ sizer_opticurve = wx.BoxSizer(wx.HORIZONTAL)
1491
+ sizer_options.Add(sizer_opticurve, 0, wx.EXPAND, 0)
1492
+
1493
+ label_opticurve = wxStaticText(self, wx.ID_ANY, _("Simplify"))
1494
+ label_opticurve.SetMinSize(dip_size(self, 70, -1))
1495
+ sizer_opticurve.Add(label_opticurve, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1496
+
1497
+ self.check_opticurve = wxCheckBox(self, wx.ID_ANY, "")
1498
+ self.check_opticurve.SetToolTip(
1499
+ _(
1500
+ "Try to 'simplify' the final curve by reducing the number of Bezier curve segments."
1501
+ )
1502
+ )
1503
+ self.check_opticurve.SetValue(1)
1504
+ sizer_opticurve.Add(self.check_opticurve, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1505
+
1506
+ sizer_opttolerance = wx.BoxSizer(wx.HORIZONTAL)
1507
+ sizer_options.Add(sizer_opttolerance, 0, wx.EXPAND, 0)
1508
+
1509
+ label_opttolerance = wxStaticText(self, wx.ID_ANY, _("Tolerance"))
1510
+ label_opttolerance.SetMinSize(dip_size(self, 70, -1))
1511
+ sizer_opttolerance.Add(label_opttolerance, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1512
+
1513
+ self.slider_tolerance = wx.Slider(self, wx.ID_ANY, 20, 0, 150)
1514
+ self.slider_tolerance.SetToolTip(
1515
+ _(
1516
+ "This defines the amount of error allowed in this simplification.\n"
1517
+ + "Larger values tend to decrease the number of segments, at the expense of less accuracy."
1518
+ )
1519
+ )
1520
+ sizer_opttolerance.Add(self.slider_tolerance, 1, wx.EXPAND, 0)
1521
+
1522
+ sizer_blacklevel = wx.BoxSizer(wx.HORIZONTAL)
1523
+ sizer_options.Add(sizer_blacklevel, 0, wx.EXPAND, 0)
1524
+
1525
+ label_blacklevel = wxStaticText(self, wx.ID_ANY, _("Black-Level"))
1526
+ label_blacklevel.SetMinSize(dip_size(self, 70, -1))
1527
+ sizer_blacklevel.Add(label_blacklevel, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1528
+
1529
+ self.slider_blacklevel = wx.Slider(
1530
+ self, wx.ID_ANY, 50, 0, 100, style=wx.SL_HORIZONTAL | wx.SL_LABELS
1531
+ )
1532
+ self.slider_blacklevel.SetToolTip(_("Establish when 'black' starts"))
1533
+ sizer_blacklevel.Add(self.slider_blacklevel, 1, wx.EXPAND, 0)
1534
+
1535
+ sizer_buttons = wx.BoxSizer(wx.HORIZONTAL)
1536
+ sizer_options.Add(sizer_buttons, 1, wx.EXPAND, 0)
1537
+
1538
+ self.button_vector = wxButton(self, wx.ID_ANY, _("Vectorize"))
1539
+ sizer_buttons.Add(self.button_vector, 0, 0, 0)
1540
+
1541
+ label_spacer = wxStaticText(self, wx.ID_ANY, " ")
1542
+ sizer_buttons.Add(label_spacer, 1, 0, 0)
1543
+
1544
+ self.button_generate = wxButton(self, wx.ID_ANY, _("Preview"))
1545
+ self.button_generate.SetToolTip(_("Generate a preview of the result"))
1546
+ sizer_buttons.Add(self.button_generate, 0, 0, 0)
1547
+
1548
+ sizer_preview = StaticBoxSizer(self, wx.ID_ANY, _("Preview"), wx.VERTICAL)
1549
+ main_sizer.Add(sizer_preview, 2, wx.EXPAND, 0)
1550
+
1551
+ self.bitmap_preview = wxStaticBitmap(self, wx.ID_ANY, wx.NullBitmap)
1552
+ sizer_preview.Add(self.bitmap_preview, 1, wx.EXPAND, 0)
1553
+
1554
+ self.vector_preview = wxStaticBitmap(self, wx.ID_ANY, wx.NullBitmap)
1555
+ sizer_preview.Add(self.vector_preview, 1, wx.EXPAND, 0)
1556
+
1557
+ self.SetSizer(main_sizer)
1558
+ main_sizer.Fit(self)
1559
+
1560
+ # self._preview = True
1561
+ # self._need_updates = False
1562
+
1563
+ # self.check_generate.SetValue(self._preview)
1564
+
1565
+ self.wximage = wx.NullBitmap
1566
+ self.wxvector = wx.NullBitmap
1567
+
1568
+ self.Layout()
1569
+ self.Centre()
1570
+ self.Bind(wx.EVT_BUTTON, self.on_button_create, self.button_vector)
1571
+ # self.Bind(wx.EVT_CHECKBOX, self.on_check_preview, self.check_generate)
1572
+ self.Bind(wx.EVT_BUTTON, self.on_changes, self.button_generate)
1573
+ self.Bind(wx.EVT_SIZE, self.on_size)
1574
+ # self.Bind(wx.EVT_SLIDER, self.on_changes, self.slider_alphamax)
1575
+ # self.Bind(wx.EVT_SLIDER, self.on_changes, self.slider_blacklevel)
1576
+ # self.Bind(wx.EVT_SLIDER, self.on_changes, self.slider_tolerance)
1577
+ # self.Bind(wx.EVT_SLIDER, self.on_changes, self.slider_turdsize)
1578
+ # self.Bind(wx.EVT_COMBOBOX, self.on_changes, self.combo_turnpolicy)
1579
+ # self.stop = None
1580
+ # self._update_thread = self.context.threaded(
1581
+ # self.generate_preview, result=self.stop, daemon=True
1582
+ # )
1583
+
1584
+ self.set_widgets(node)
1585
+
1586
+ # def on_check_preview(self, event):
1587
+ # self._preview = self.check_generate.GetValue()
1588
+
1589
+ def on_size(self, event):
1590
+ self.set_images(True)
1591
+
1592
+ def pane_active(self):
1593
+ self._pane_is_active = True
1594
+ self.set_images(True)
1595
+
1596
+ def pane_deactive(self):
1597
+ self._pane_is_active = False
1598
+
1599
+ def on_changes(self, event):
1600
+ # self._need_updates = True
1601
+ self.generate_preview()
1602
+
1603
+ def on_button_create(self, event):
1604
+ ipolicy = self.combo_turnpolicy.GetSelection()
1605
+ turnpolicy = self.turn_choices[ipolicy].lower()
1606
+ # slider 0 .. 10 translate to 0 .. 10
1607
+ turdsize = self.slider_turdsize.GetValue()
1608
+ # slider 0 .. 100 translate to 0 .. 1
1609
+ blacklevel = (
1610
+ self.slider_blacklevel.GetValue() / self.slider_blacklevel.GetMax() * 1.0
1611
+ )
1612
+ # slider 0 .. 150 translate to 0 .. 1.5
1613
+ opttolerance = (
1614
+ self.slider_tolerance.GetValue() / self.slider_tolerance.GetMax() * 1.5
1615
+ )
1616
+ # slider 0 .. 12 translate to 0 .. 1.333
1617
+ alphamax = (
1618
+ self.slider_alphamax.GetValue() / self.slider_alphamax.GetMax() * 4.0 / 3.0
1619
+ )
1620
+ opticurve = self.check_opticurve.GetValue()
1621
+ cmd = f"vectorize -z {turnpolicy} -t {turdsize} -a {alphamax}{' -n' if opticurve else ''} -O {opttolerance} -k {blacklevel}\n"
1622
+ self.context(cmd)
1623
+
1624
+ def set_images(self, refresh=False):
1625
+ def opaque(source):
1626
+ img = source
1627
+ if img is not None and img.mode == "RGBA":
1628
+ r, g, b, a = img.split()
1629
+ background = Image.new("RGB", img.size, "white")
1630
+ background.paste(img, mask=a)
1631
+ img = background
1632
+ return img
1633
+
1634
+ if not self._pane_is_active:
1635
+ return
1636
+ if self.node is None or self.node.image is None:
1637
+ self.wximage = wx.NullBitmap
1638
+ else:
1639
+ if refresh:
1640
+ source_image = self.node.active_image
1641
+ source_image = opaque(source_image)
1642
+ pw, ph = self.bitmap_preview.GetSize()
1643
+ iw, ih = source_image.size
1644
+ wfac = pw / iw
1645
+ hfac = ph / ih
1646
+ # The smaller of the two decide how to scale the picture
1647
+ if wfac < hfac:
1648
+ factor = wfac
1649
+ else:
1650
+ factor = hfac
1651
+ # print (f"Window: {pw} x {ph}, Image= {iw} x {ih}, factor={factor:.3f}")
1652
+ if factor < 1.0:
1653
+ image = source_image.resize((int(iw * factor), int(ih * factor)))
1654
+ else:
1655
+ image = source_image
1656
+ self.wximage = self.img_2_wx(image)
1657
+
1658
+ self.bitmap_preview.SetBitmap(self.wximage)
1659
+
1660
+ def generate_preview(self):
1661
+ # from time import sleep
1662
+ make_vector = self.context.kernel.lookup("render-op/make_vector")
1663
+ make_raster = self.context.kernel.lookup("render-op/make_raster")
1664
+ # while self.alive:
1665
+ if not self._pane_is_active:
1666
+ return
1667
+ self.wxvector = wx.NullBitmap
1668
+
1669
+ if self.node is not None and self.node.image is not None:
1670
+ matrix = self.node.matrix
1671
+ # image = self.node.opaque_image
1672
+ ipolicy = self.combo_turnpolicy.GetSelection()
1673
+ # turnpolicy = self.turn_choices[ipolicy].lower()
1674
+ # slider 0 .. 10 translate to 0 .. 10
1675
+ turdsize = self.slider_turdsize.GetValue()
1676
+ # slider 0 .. 100 translate to 0 .. 1
1677
+ blacklevel = (
1678
+ self.slider_blacklevel.GetValue()
1679
+ / self.slider_blacklevel.GetMax()
1680
+ * 1.0
1681
+ )
1682
+ # slider 0 .. 150 translate to 0 .. 1.5
1683
+ opttolerance = (
1684
+ self.slider_tolerance.GetValue() / self.slider_tolerance.GetMax() * 1.5
1685
+ )
1686
+ # slider 0 .. 12 translate to 0 .. 1.333
1687
+ alphamax = (
1688
+ self.slider_alphamax.GetValue()
1689
+ / self.slider_alphamax.GetMax()
1690
+ * 4.0
1691
+ / 3.0
1692
+ )
1693
+ opticurve = self.check_opticurve.GetValue()
1694
+ bounds = self.node.paint_bounds
1695
+ if bounds is None:
1696
+ bounds = self.node.bounds
1697
+ if bounds is None:
1698
+ return
1699
+ xmin, ymin, xmax, ymax = bounds
1700
+ width = xmax - xmin
1701
+ height = ymax - ymin
1702
+ dpi = 250
1703
+ dots_per_units = dpi / UNITS_PER_INCH
1704
+ new_width = width * dots_per_units
1705
+ new_height = height * dots_per_units
1706
+ new_height = max(new_height, 1)
1707
+ new_width = max(new_width, 1)
1708
+ self.context.kernel.busyinfo.start(msg=_("Generating..."))
1709
+ try:
1710
+ image = make_raster(
1711
+ self.node,
1712
+ bounds=bounds,
1713
+ width=new_width,
1714
+ height=new_height,
1715
+ )
1716
+ path = make_vector(
1717
+ image=image,
1718
+ interpolationpolicy=ipolicy,
1719
+ turdsize=turdsize,
1720
+ alphamax=alphamax,
1721
+ opticurve=opticurve,
1722
+ opttolerance=opttolerance,
1723
+ blacklevel=blacklevel,
1724
+ )
1725
+ except:
1726
+ self.context.kernel.busyinfo.end()
1727
+ return
1728
+ self.context.kernel.busyinfo.end()
1729
+ path.transform *= Matrix(matrix)
1730
+ dummynode = PathNode(
1731
+ path=abs(path),
1732
+ stroke_width=500,
1733
+ stroke_scaled=False,
1734
+ fillrule=0, # Fillrule.FILLRULE_NONZERO
1735
+ )
1736
+ if dummynode is None:
1737
+ return
1738
+ bounds = dummynode.paint_bounds
1739
+ if bounds is None:
1740
+ bounds = dummynode.bounds
1741
+ if bounds is None:
1742
+ return
1743
+ pw, ph = self.vector_preview.GetSize()
1744
+ # iw, ih = self.node.image.size
1745
+ # wfac = pw / iw
1746
+ # hfac = ph / ih
1747
+ # The smaller of the two decide how to scale the picture
1748
+ # if wfac < hfac:
1749
+ # factor = wfac
1750
+ # else:
1751
+ # factor = hfac
1752
+ image = make_raster(
1753
+ dummynode,
1754
+ bounds,
1755
+ width=pw,
1756
+ height=ph,
1757
+ keep_ratio=True,
1758
+ )
1759
+ # rw, rh = image.size
1760
+ # print (f"Area={pw}x{ph}, Org={iw}x{ih}, Raster={rw}x{rh}")
1761
+ # if factor < 1.0:
1762
+ # image = image.resize((int(iw * factor), int(ih * factor)))
1763
+ self.wxvector = self.img_2_wx(image)
1764
+
1765
+ self.vector_preview.SetBitmap(self.wxvector)
1766
+
1767
+ @staticmethod
1768
+ def accepts(node):
1769
+ # Changing the staticmethod into a regular method will cause a crash
1770
+ # Not the nicest thing in the world, as we need to instantiate the class once to reset the status flag
1771
+ global HAS_VECTOR_ENGINE
1772
+ return hasattr(node, "as_image") and HAS_VECTOR_ENGINE
1773
+
1774
+ def img_2_wx(self, image):
1775
+ width, height = image.size
1776
+ newimage = image.convert("RGB")
1777
+ return wx.Bitmap.FromBuffer(width, height, newimage.tobytes())
1778
+
1779
+ def set_widgets(self, node=None):
1780
+ self.node = node
1781
+ if node is not None:
1782
+ self._need_updates = True
1783
+ self.set_images()
1784
+
1785
+ def signal(self, signalstr, myargs):
1786
+ return
1787
+
1788
+ class ImagePropertyPanel(ScrolledPanel):
1789
+ def __init__(self, *args, context=None, node=None, **kwargs):
1790
+ # begin wxGlade: ConsolePanel.__init__
1791
+ kwargs["style"] = kwargs.get("style", 0) | wx.TAB_TRAVERSAL
1792
+ wx.Panel.__init__(self, *args, **kwargs)
1793
+ self.subpanels = list()
1794
+ self.context = context
1795
+ self.context.themes.set_window_colors(self)
1796
+ self.node = node
1797
+ self.panel_id = IdPanel(
1798
+ self, id=wx.ID_ANY, context=self.context, node=self.node
1799
+ )
1800
+ self.SetHelpText("imageproperty")
1801
+ self.subpanels.append(self.panel_id)
1802
+ self.text_dpi = TextCtrl(
1803
+ self,
1804
+ wx.ID_ANY,
1805
+ "500",
1806
+ style=wx.TE_PROCESS_ENTER,
1807
+ check="float",
1808
+ limited=True,
1809
+ nonzero=True,
1810
+ )
1811
+ self.text_dpi.set_default_values(
1812
+ [
1813
+ (str(dpi), _("Set DPI to {value}").format(value=str(dpi)))
1814
+ for dpi in self.context.device.view.get_sensible_dpi_values()
1815
+ ]
1816
+ )
1817
+ self.check_keep_size = wxCheckBox(self, wx.ID_ANY, _("Keep size on change"))
1818
+ self.check_keep_size.SetValue(True)
1819
+ self.check_prevent_crop = wxCheckBox(self, wx.ID_ANY, _("No final crop"))
1820
+
1821
+ self.panel_lock = PreventChangePanel(
1822
+ self, id=wx.ID_ANY, context=self.context, node=self.node
1823
+ )
1824
+ self.subpanels.append(self.panel_lock)
1825
+
1826
+ self.panel_keyhole = KeyholePanel(
1827
+ self, id=wx.ID_ANY, context=self.context, node=self.node
1828
+ )
1829
+ self.subpanels.append(self.panel_keyhole)
1830
+
1831
+ self.panel_xy = PositionSizePanel(
1832
+ self, id=wx.ID_ANY, context=self.context, node=self.node
1833
+ )
1834
+ self.subpanels.append(self.panel_xy)
1835
+
1836
+ self.panel_crop = CropPanel(
1837
+ self, id=wx.ID_ANY, context=self.context, node=self.node
1838
+ )
1839
+ self.subpanels.append(self.panel_crop)
1840
+ self.check_enable_dither = wxCheckBox(self, wx.ID_ANY, _("Dither"))
1841
+ self.choices = [
1842
+ "Floyd-Steinberg",
1843
+ "Legacy-Floyd-Steinberg",
1844
+ "Atkinson",
1845
+ "Jarvis-Judice-Ninke",
1846
+ "Stucki",
1847
+ "Burkes",
1848
+ "Sierra3",
1849
+ "Sierra2",
1850
+ "Sierra-2-4a",
1851
+ "Shiau-Fan",
1852
+ "Shiau-Fan-2",
1853
+ "Bayer",
1854
+ "Bayer-Blue",
1855
+ ]
1856
+ self.combo_dither = wxComboBox(
1857
+ self,
1858
+ wx.ID_ANY,
1859
+ choices=self.choices,
1860
+ style=wx.CB_READONLY | wx.CB_DROPDOWN,
1861
+ )
1862
+ self.check_enable_depthmap = wxCheckBox(self, wx.ID_ANY, _("Depthmap"))
1863
+ resolutions = list((f"{2**p} - {p}bit" for p in range(8, 1, -1)))
1864
+ self.combo_depthmap = wxComboBox(
1865
+ self,
1866
+ wx.ID_ANY,
1867
+ choices=resolutions,
1868
+ style=wx.CB_DROPDOWN | wx.CB_READONLY,
1869
+ )
1870
+
1871
+ # self.op_choices = []
1872
+ # self.image_ops = []
1873
+ # self.op_choices.append(_("Choose a script to apply"))
1874
+ # self.op_choices.append(_("Set to None"))
1875
+ # for op in list(self.context.elements.match("raster_script", suffix=True)):
1876
+ # self.op_choices.append(_("Apply: {script}").format(script=op))
1877
+ # self.image_ops.append(op)
1878
+
1879
+ # self.combo_operations = wxComboBox(
1880
+ # self,
1881
+ # wx.ID_ANY,
1882
+ # choices=self.op_choices,
1883
+ # style=wx.CB_DROPDOWN,
1884
+ # )
1885
+
1886
+ self.check_invert_grayscale = wxCheckBox(self, wx.ID_ANY, _("Invert"))
1887
+ self.btn_reset_grayscale = wxButton(self, wx.ID_ANY, _("Reset"))
1888
+
1889
+ self.slider_grayscale_red = wx.Slider(
1890
+ self, wx.ID_ANY, 0, -1000, 1000, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
1891
+ )
1892
+ self.text_grayscale_red = TextCtrl(self, wx.ID_ANY, "", style=wx.TE_READONLY)
1893
+ self.slider_grayscale_green = wx.Slider(
1894
+ self, wx.ID_ANY, 0, -1000, 1000, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
1895
+ )
1896
+ self.text_grayscale_green = TextCtrl(
1897
+ self, wx.ID_ANY, "", style=wx.TE_READONLY
1898
+ )
1899
+ self.slider_grayscale_blue = wx.Slider(
1900
+ self, wx.ID_ANY, 0, -1000, 1000, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
1901
+ )
1902
+ self.text_grayscale_blue = TextCtrl(
1903
+ self, wx.ID_ANY, "", style=wx.TE_READONLY
1904
+ )
1905
+ self.slider_grayscale_lightness = wx.Slider(
1906
+ self, wx.ID_ANY, 500, 0, 1000, style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL
1907
+ )
1908
+ self.text_grayscale_lightness = TextCtrl(
1909
+ self, wx.ID_ANY, "", style=wx.TE_READONLY
1910
+ )
1911
+
1912
+ self.__set_properties()
1913
+ self.__do_layout()
1914
+
1915
+ self.Bind(wx.EVT_CHECKBOX, self.on_dither, self.check_enable_dither)
1916
+ self.Bind(wx.EVT_COMBOBOX, self.on_dither, self.combo_dither)
1917
+
1918
+ self.Bind(wx.EVT_CHECKBOX, self.on_depthmap, self.check_enable_depthmap)
1919
+ self.Bind(wx.EVT_COMBOBOX, self.on_depthmap, self.combo_depthmap)
1920
+ # self.Bind(wx.EVT_COMBOBOX, self.on_combo_operation, self.combo_operations)
1921
+
1922
+ self.Bind(wx.EVT_TEXT_ENTER, self.on_dither, self.combo_dither)
1923
+
1924
+ self.text_dpi.SetActionRoutine(self.on_text_dpi)
1925
+
1926
+ self.Bind(
1927
+ wx.EVT_CHECKBOX, self.on_check_invert_grayscale, self.check_invert_grayscale
1928
+ )
1929
+ self.Bind(wx.EVT_BUTTON, self.on_reset_grayscale, self.btn_reset_grayscale)
1930
+ self.Bind(
1931
+ wx.EVT_SLIDER,
1932
+ self.on_slider_grayscale_component,
1933
+ self.slider_grayscale_lightness,
1934
+ )
1935
+ self.Bind(
1936
+ wx.EVT_SLIDER, self.on_slider_grayscale_component, self.slider_grayscale_red
1937
+ )
1938
+ self.Bind(
1939
+ wx.EVT_SLIDER,
1940
+ self.on_slider_grayscale_component,
1941
+ self.slider_grayscale_green,
1942
+ )
1943
+ self.Bind(
1944
+ wx.EVT_SLIDER,
1945
+ self.on_slider_grayscale_component,
1946
+ self.slider_grayscale_blue,
1947
+ )
1948
+ self.check_prevent_crop.Bind(wx.EVT_CHECKBOX, self.on_crop_option)
1949
+
1950
+ # self.check_enable_grayscale.SetValue(op["enable"])
1951
+ if node.invert is None:
1952
+ node.invert = False
1953
+ self.set_grayscale_values()
1954
+ self.set_widgets()
1955
+
1956
+ @staticmethod
1957
+ def accepts(node):
1958
+ return hasattr(node, "as_image")
1959
+
1960
+ def set_grayscale_values(self):
1961
+ self.check_invert_grayscale.SetValue(self.node.invert)
1962
+
1963
+ self.slider_grayscale_red.SetValue(int(self.node.red * 500.0))
1964
+ self.text_grayscale_red.SetValue(str(self.node.red))
1965
+
1966
+ self.slider_grayscale_green.SetValue(int(self.node.green * 500.0))
1967
+ self.text_grayscale_green.SetValue(str(self.node.green))
1968
+
1969
+ self.slider_grayscale_blue.SetValue(int(self.node.blue * 500.0))
1970
+ self.text_grayscale_blue.SetValue(str(self.node.blue))
1971
+
1972
+ self.slider_grayscale_lightness.SetValue(int(self.node.lightness * 500.0))
1973
+ self.text_grayscale_lightness.SetValue(str(self.node.lightness))
1974
+
1975
+ def set_widgets(self, node=None):
1976
+ if node is None:
1977
+ node = self.node
1978
+ for p in self.subpanels:
1979
+ p.set_widgets(node)
1980
+ self.node = node
1981
+ if node is None:
1982
+ return
1983
+ if self.node.type == "elem image":
1984
+ self.check_keep_size.Show(True)
1985
+ else:
1986
+ self.check_keep_size.Show(True)
1987
+ self.text_dpi.SetValue(str(node.dpi))
1988
+ self.check_enable_dither.SetValue(node.dither)
1989
+ self.combo_dither.SetValue(node.dither_type)
1990
+ self.combo_dither.Enable(bool(node.dither))
1991
+ self.check_enable_depthmap.SetValue(node.is_depthmap)
1992
+ resolutions =list((2**p for p in range(8, 1, -1)))
1993
+ try:
1994
+ idx = resolutions.index(node.depth_resolution)
1995
+ except (IndexError, AttributeError, ValueError) as e:
1996
+ # print(f"Caught error {e} for value {node.depth_resolution}")
1997
+ idx = 0
1998
+ self.combo_depthmap.SetSelection(idx)
1999
+ self.combo_depthmap.Enable(bool(node.is_depthmap))
2000
+
2001
+ self.check_prevent_crop.SetValue(node.prevent_crop)
2002
+
2003
+ def __set_properties(self):
2004
+ self.check_keep_size.SetToolTip(
2005
+ _("Enabled: Keep size and amend internal resolution") + "\n" +
2006
+ _("Disabled: Keep internal resolution and change size")
2007
+ )
2008
+ self.check_prevent_crop.SetToolTip(_("Prevent final crop after all operations"))
2009
+ self.check_enable_dither.SetToolTip(_("Enable Dither"))
2010
+ self.check_enable_dither.SetValue(True)
2011
+ self.combo_dither.Enable(True)
2012
+ self.combo_dither.SetToolTip(_("Select dither algorithm to use"))
2013
+ self.combo_dither.SetSelection(0)
2014
+ # self.combo_operations.SetToolTip(_("Select image enhancement script to apply"))
2015
+ # self.combo_operations.SetSelection(0)
2016
+ self.check_invert_grayscale.SetToolTip(_("Invert Grayscale"))
2017
+ self.slider_grayscale_red.SetToolTip(_("Red component amount"))
2018
+ self.text_grayscale_red.SetToolTip(_("Red Factor"))
2019
+ self.slider_grayscale_green.SetToolTip(_("Green component control"))
2020
+ self.text_grayscale_green.SetToolTip(_("Green Factor"))
2021
+ self.slider_grayscale_blue.SetToolTip(_("Blue component control"))
2022
+ self.text_grayscale_blue.SetToolTip(_("Blue Factor"))
2023
+ self.slider_grayscale_lightness.SetToolTip(_("Lightness control"))
2024
+ self.text_grayscale_lightness.SetToolTip(_("Lightness"))
2025
+ self.btn_reset_grayscale.SetToolTip(_("Reset the grayscale modifiers to standard values"))
2026
+
2027
+ DEPTH_FLAG_TOOLTIP = _("Do you want to treat this bitmap as depthmap where every greyscal-level corresponds to the amount of times this pixel will be burnt")
2028
+ self.check_enable_depthmap.SetToolTip(DEPTH_FLAG_TOOLTIP)
2029
+ self.check_enable_depthmap.SetValue(False)
2030
+ DEPTH_RES_TOOLTIP =(
2031
+ _("How many grayscales do you want to distinguish?") + "\n" +
2032
+ _(
2033
+ "This operation will step through the image and process it per defined grayscale resolution."
2034
+ ) + "\n" +
2035
+ _(
2036
+ "So for full resolution every grayscale level would be processed individually: a black line (or a white line if inverted) would be processed 255 times, a line with grayscale value 128 would be processed 128 times."
2037
+ ) + "\n" +
2038
+ _(
2039
+ "You can define a coarser resolution e.g. 64: then very faint lines (grayscale 1-4) would be burned just once, very strong lines (level 252-255) would be burned 64 times."
2040
+ )
2041
+ )
2042
+ self.combo_depthmap.SetToolTip(DEPTH_RES_TOOLTIP)
2043
+ self.combo_depthmap.SetSelection(0)
2044
+ self.combo_depthmap.Enable(False)
2045
+ # end wxGlade
2046
+
2047
+ def __do_layout(self):
2048
+ # begin wxGlade: ImageProperty.__do_layout
2049
+ sizer_main = wx.BoxSizer(wx.VERTICAL)
2050
+ sizer_main.Add(self.panel_id, 0, wx.EXPAND, 0)
2051
+ sizer_main.Add(self.panel_crop, 0, wx.EXPAND, 0)
2052
+
2053
+ sizer_dpi_crop = wx.BoxSizer(wx.HORIZONTAL)
2054
+ sizer_dpi = StaticBoxSizer(self, wx.ID_ANY, _("DPI:"), wx.HORIZONTAL)
2055
+ self.text_dpi.SetToolTip(_("Dots Per Inch"))
2056
+ sizer_dpi.Add(self.text_dpi, 1, wx.EXPAND, 0)
2057
+ sizer_dpi.Add(self.check_keep_size, 0, wx.EXPAND, 0)
2058
+
2059
+ sizer_dpi_crop.Add(sizer_dpi, 1, wx.EXPAND, 0)
2060
+
2061
+ sizer_crop = StaticBoxSizer(self, wx.ID_ANY, _("Auto-Crop:"), wx.HORIZONTAL)
2062
+ sizer_crop.Add(self.check_prevent_crop, 1, wx.ALIGN_CENTER_VERTICAL, 0)
2063
+
2064
+ sizer_dpi_crop.Add(sizer_crop, 1, wx.EXPAND, 0)
2065
+
2066
+ sizer_dither_depth = wx.BoxSizer(wx.HORIZONTAL)
2067
+ sizer_dither = StaticBoxSizer(self, wx.ID_ANY, _("Dither"), wx.HORIZONTAL)
2068
+ sizer_dither.Add(self.check_enable_dither, 0, wx.ALIGN_CENTER_VERTICAL, 0)
2069
+ sizer_dither.Add(self.combo_dither, 0, wx.ALIGN_CENTER_VERTICAL, 0)
2070
+
2071
+ sizer_dither_depth.Add(sizer_dither, 1, wx.EXPAND, 0)
2072
+ sizer_depth = StaticBoxSizer(self, wx.ID_ANY, _("3D-Treatment"), wx.HORIZONTAL)
2073
+ sizer_depth.Add(self.check_enable_depthmap, 0, wx.ALIGN_CENTER_VERTICAL, 0)
2074
+ sizer_depth.Add(self.combo_depthmap, 0, wx.ALIGN_CENTER_VERTICAL, 0)
2075
+ sizer_dither_depth.Add(sizer_depth, 1, wx.EXPAND, 0)
2076
+
2077
+ sizer_main.Add(sizer_dpi_crop, 0, wx.EXPAND, 0)
2078
+ sizer_main.Add(sizer_dither_depth, 0, wx.EXPAND, 0)
2079
+
2080
+ sizer_rg = wx.BoxSizer(wx.HORIZONTAL)
2081
+ sizer_bl = wx.BoxSizer(wx.HORIZONTAL)
2082
+ sizer_grayscale = StaticBoxSizer(self, wx.ID_ANY, _("Grayscale"), wx.VERTICAL)
2083
+ sizer_inversion_reset = wx.BoxSizer(wx.HORIZONTAL)
2084
+ sizer_inversion_reset.Add(
2085
+ self.check_invert_grayscale, 0, wx.ALIGN_CENTER_VERTICAL, 0
2086
+ )
2087
+ sizer_inversion_reset.AddStretchSpacer(1)
2088
+ sizer_inversion_reset.Add(
2089
+ self.btn_reset_grayscale, 0, wx.ALIGN_CENTER_VERTICAL, 0
2090
+ )
2091
+
2092
+ sizer_grayscale_lightness = StaticBoxSizer(
2093
+ self, wx.ID_ANY, _("Lightness"), wx.HORIZONTAL
2094
+ )
2095
+ sizer_grayscale_blue = StaticBoxSizer(self, wx.ID_ANY, _("Blue"), wx.HORIZONTAL)
2096
+ sizer_grayscale_green = StaticBoxSizer(
2097
+ self, wx.ID_ANY, _("Green"), wx.HORIZONTAL
2098
+ )
2099
+ sizer_grayscale_red = StaticBoxSizer(self, wx.ID_ANY, _("Red"), wx.HORIZONTAL)
2100
+ sizer_grayscale.Add(sizer_inversion_reset, 0, wx.EXPAND, 0)
2101
+ sizer_grayscale_red.Add(self.slider_grayscale_red, 1, wx.EXPAND, 0)
2102
+ sizer_grayscale_red.Add(self.text_grayscale_red, 1, 0, 0)
2103
+ sizer_rg.Add(sizer_grayscale_red, 1, wx.EXPAND, 0)
2104
+ sizer_grayscale_green.Add(self.slider_grayscale_green, 1, wx.EXPAND, 0)
2105
+ sizer_grayscale_green.Add(self.text_grayscale_green, 1, 0, 0)
2106
+ sizer_rg.Add(sizer_grayscale_green, 1, wx.EXPAND, 0)
2107
+ sizer_grayscale_blue.Add(self.slider_grayscale_blue, 1, wx.EXPAND, 0)
2108
+ sizer_grayscale_blue.Add(self.text_grayscale_blue, 1, 0, 0)
2109
+ sizer_bl.Add(sizer_grayscale_blue, 1, wx.EXPAND, 0)
2110
+ sizer_grayscale_lightness.Add(self.slider_grayscale_lightness, 1, wx.EXPAND, 0)
2111
+ sizer_grayscale_lightness.Add(self.text_grayscale_lightness, 1, 0, 0)
2112
+ sizer_bl.Add(sizer_grayscale_lightness, 1, wx.EXPAND, 0)
2113
+ sizer_grayscale.Add(sizer_rg, 5, wx.EXPAND, 0)
2114
+ sizer_grayscale.Add(sizer_bl, 5, wx.EXPAND, 0)
2115
+
2116
+ self.text_grayscale_red.SetMaxSize(dip_size(self, 70, -1))
2117
+ self.text_grayscale_green.SetMaxSize(dip_size(self, 70, -1))
2118
+ self.text_grayscale_blue.SetMaxSize(dip_size(self, 70, -1))
2119
+ self.text_grayscale_lightness.SetMaxSize(dip_size(self, 70, -1))
2120
+
2121
+ sizer_main.Add(sizer_grayscale, 0, wx.EXPAND, 0)
2122
+
2123
+ hor_sizer = wx.BoxSizer(wx.HORIZONTAL)
2124
+ hor_sizer.Add(self.panel_lock, 1, wx.EXPAND, 0)
2125
+ hor_sizer.Add(self.panel_keyhole, 1, wx.EXPAND, 0)
2126
+
2127
+ sizer_main.Add(hor_sizer, 0, wx.EXPAND, 0)
2128
+ sizer_main.Add(self.panel_xy, 0, wx.EXPAND, 0)
2129
+
2130
+ self.SetSizer(sizer_main)
2131
+ self.Layout()
2132
+ self.Centre()
2133
+ # end wxGlade
2134
+
2135
+ def node_update(self):
2136
+ self.node.set_dirty_bounds()
2137
+ self.context.elements.do_image_update(self.node, self.context)
2138
+ self.context.elements.emphasized()
2139
+ self.context.signal("element_property_update", self.node)
2140
+
2141
+ def on_text_dpi(self):
2142
+ old_step = self.node.dpi
2143
+ new_step = float(self.text_dpi.GetValue())
2144
+ if old_step == new_step:
2145
+ return
2146
+ if self.node.type == "elem image":
2147
+ keep_size = self.check_keep_size.GetValue()
2148
+ if not keep_size:
2149
+ # We need to rescale the image
2150
+ img_scale = old_step / new_step
2151
+ bb = self.node.bounds
2152
+ self.node.matrix.post_scale(img_scale, img_scale, bb[0], bb[1])
2153
+ self.node.dpi = new_step
2154
+ self.node_update()
2155
+
2156
+ def on_crop_option(self, event):
2157
+ self.node.prevent_crop = self.check_prevent_crop.GetValue()
2158
+ self.node_update()
2159
+
2160
+ def on_dither(self, event=None):
2161
+ # Dither can be set by two different means:
2162
+ # a. directly
2163
+ # b. via a script
2164
+ dither_op = None
2165
+ for op in self.node.operations:
2166
+ if op["name"] == "dither":
2167
+ dither_op = op
2168
+ break
2169
+ dither_flag = self.check_enable_dither.GetValue()
2170
+ self.combo_dither.Enable(dither_flag)
2171
+ dither_type = self.choices[self.combo_dither.GetSelection()]
2172
+ if dither_op is not None:
2173
+ dither_op["enable"] = dither_flag
2174
+ dither_op["type"] = dither_type
2175
+ self.node.dither = dither_flag
2176
+ self.node.dither_type = dither_type
2177
+ if dither_flag:
2178
+ self.node.is_depthmap = False
2179
+ self.check_enable_depthmap.SetValue(False)
2180
+ self.combo_depthmap.Enable(False)
2181
+ self.node_update()
2182
+ self.context.signal("nodetype")
2183
+
2184
+ def on_depthmap(self, event=None):
2185
+ depth_flag = self.check_enable_depthmap.GetValue()
2186
+ self.combo_depthmap.Enable(depth_flag)
2187
+ resolutions = (256, 128, 64, 32, 16, 8, 4)
2188
+ idx = self.combo_depthmap.GetSelection()
2189
+ if idx < 1:
2190
+ idx = 0
2191
+ depth_res = resolutions[idx]
2192
+ self.node.is_depthmap = depth_flag
2193
+ self.node.depth_resolution = depth_res
2194
+ if depth_flag:
2195
+ self.node.dither = False
2196
+ self.check_enable_dither.SetValue(False)
2197
+ self.combo_dither.Enable(False)
2198
+ self.node_update()
2199
+ self.context.signal("nodetype")
2200
+
2201
+ def on_reset_grayscale(self, event):
2202
+ self.node.invert = False
2203
+ self.node.red = 1.0
2204
+ self.node.green = 1.0
2205
+ self.node.blue = 1.0
2206
+ self.node.lightness = 1.0
2207
+ self.node_update()
2208
+ self.set_grayscale_values()
2209
+
2210
+ def on_check_invert_grayscale(
2211
+ self, event=None
2212
+ ): # wxGlade: RasterWizard.<event_handler>
2213
+ self.node.invert = self.check_invert_grayscale.GetValue()
2214
+ self.node_update()
2215
+
2216
+ def on_slider_grayscale_component(
2217
+ self, event=None
2218
+ ): # wxGlade: GrayscalePanel.<event_handler>
2219
+ if event and (
2220
+ not self.context.process_while_sliding
2221
+ and wx.GetMouseState().LeftIsDown()
2222
+ ):
2223
+ event.Skip()
2224
+ return
2225
+
2226
+ self.node.red = float(int(self.slider_grayscale_red.GetValue()) / 500.0)
2227
+ self.text_grayscale_red.SetValue(str(self.node.red))
2228
+
2229
+ self.node.green = float(int(self.slider_grayscale_green.GetValue()) / 500.0)
2230
+ self.text_grayscale_green.SetValue(str(self.node.green))
2231
+
2232
+ self.node.blue = float(int(self.slider_grayscale_blue.GetValue()) / 500.0)
2233
+ self.text_grayscale_blue.SetValue(str(self.node.blue))
2234
+
2235
+ self.node.lightness = float(
2236
+ int(self.slider_grayscale_lightness.GetValue()) / 500.0
2237
+ )
2238
+ self.text_grayscale_lightness.SetValue(str(self.node.lightness))
2239
+ self.node_update()
2240
+
2241
+ def signal(self, signalstr, myargs):
2242
+ for p in self.subpanels:
2243
+ if hasattr(p, "signal"):
2244
+ p.signal(signalstr, myargs)