meerk40t 0.9.7030__py2.py3-none-any.whl → 0.9.7040__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 (79) hide show
  1. meerk40t/balormk/clone_loader.py +3 -2
  2. meerk40t/balormk/controller.py +28 -11
  3. meerk40t/balormk/cylindermod.py +1 -0
  4. meerk40t/balormk/device.py +13 -9
  5. meerk40t/balormk/driver.py +9 -2
  6. meerk40t/balormk/galvo_commands.py +3 -1
  7. meerk40t/balormk/gui/gui.py +6 -0
  8. meerk40t/balormk/livelightjob.py +338 -321
  9. meerk40t/balormk/mock_connection.py +4 -3
  10. meerk40t/balormk/usb_connection.py +11 -2
  11. meerk40t/camera/camera.py +19 -14
  12. meerk40t/camera/gui/camerapanel.py +6 -0
  13. meerk40t/core/cutplan.py +109 -51
  14. meerk40t/core/elements/element_treeops.py +435 -140
  15. meerk40t/core/elements/elements.py +100 -9
  16. meerk40t/core/elements/shapes.py +259 -39
  17. meerk40t/core/elements/tree_commands.py +10 -5
  18. meerk40t/core/node/elem_ellipse.py +18 -8
  19. meerk40t/core/node/elem_image.py +51 -19
  20. meerk40t/core/node/elem_line.py +18 -8
  21. meerk40t/core/node/elem_path.py +18 -8
  22. meerk40t/core/node/elem_point.py +10 -4
  23. meerk40t/core/node/elem_polyline.py +19 -11
  24. meerk40t/core/node/elem_rect.py +18 -8
  25. meerk40t/core/node/elem_text.py +11 -5
  26. meerk40t/core/node/filenode.py +2 -8
  27. meerk40t/core/node/groupnode.py +11 -11
  28. meerk40t/core/node/image_processed.py +11 -5
  29. meerk40t/core/node/image_raster.py +11 -5
  30. meerk40t/core/node/node.py +64 -16
  31. meerk40t/core/node/refnode.py +2 -1
  32. meerk40t/core/svg_io.py +91 -34
  33. meerk40t/device/dummydevice.py +7 -1
  34. meerk40t/extra/vtracer.py +222 -0
  35. meerk40t/grbl/device.py +81 -8
  36. meerk40t/gui/about.py +20 -0
  37. meerk40t/gui/devicepanel.py +20 -16
  38. meerk40t/gui/gui_mixins.py +4 -0
  39. meerk40t/gui/icons.py +330 -253
  40. meerk40t/gui/laserpanel.py +8 -3
  41. meerk40t/gui/laserrender.py +41 -21
  42. meerk40t/gui/magnetoptions.py +158 -65
  43. meerk40t/gui/materialtest.py +229 -39
  44. meerk40t/gui/navigationpanels.py +229 -24
  45. meerk40t/gui/propertypanels/hatchproperty.py +2 -0
  46. meerk40t/gui/propertypanels/imageproperty.py +160 -106
  47. meerk40t/gui/ribbon.py +6 -1
  48. meerk40t/gui/scenewidgets/gridwidget.py +29 -32
  49. meerk40t/gui/scenewidgets/rectselectwidget.py +190 -192
  50. meerk40t/gui/simulation.py +75 -77
  51. meerk40t/gui/statusbarwidgets/defaultoperations.py +84 -48
  52. meerk40t/gui/statusbarwidgets/infowidget.py +2 -2
  53. meerk40t/gui/tips.py +15 -1
  54. meerk40t/gui/toolwidgets/toolpointmove.py +3 -1
  55. meerk40t/gui/wxmmain.py +242 -114
  56. meerk40t/gui/wxmscene.py +107 -24
  57. meerk40t/gui/wxmtree.py +4 -2
  58. meerk40t/gui/wxutils.py +60 -15
  59. meerk40t/image/imagetools.py +129 -65
  60. meerk40t/internal_plugins.py +4 -0
  61. meerk40t/kernel/kernel.py +39 -18
  62. meerk40t/kernel/settings.py +28 -9
  63. meerk40t/lihuiyu/device.py +24 -12
  64. meerk40t/main.py +1 -1
  65. meerk40t/moshi/device.py +20 -6
  66. meerk40t/network/console_server.py +22 -6
  67. meerk40t/newly/device.py +10 -3
  68. meerk40t/newly/gui/gui.py +10 -0
  69. meerk40t/ruida/device.py +22 -2
  70. meerk40t/ruida/loader.py +6 -3
  71. meerk40t/tools/geomstr.py +193 -125
  72. meerk40t/tools/rasterplotter.py +179 -93
  73. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/METADATA +1 -1
  74. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/RECORD +79 -78
  75. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/LICENSE +0 -0
  76. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/WHEEL +0 -0
  77. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/entry_points.txt +0 -0
  78. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/top_level.txt +0 -0
  79. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/zip-safe +0 -0
@@ -0,0 +1,222 @@
1
+ """
2
+ Vtracer https://github.com/visioncortex/vtracer
3
+ visioncortex VTracer is an open source software to convert raster images (like jpg & png)
4
+ into vector graphics (svg). It can vectorize graphics and photographs and trace the curves
5
+ to output compact vector files.
6
+ Comparing to [Potrace](http://potrace.sourceforge.net/) which only accept binarized
7
+ inputs (Black & White pixmap), VTracer has an image processing pipeline which
8
+ can handle colored high resolution scans.
9
+ Comparing to Adobe Illustrator's [Image Trace](https://helpx.adobe.com/illustrator/using/image-trace.html),
10
+ VTracer's output is much more compact (less shapes) as we adopt a stacking strategy
11
+ and avoid producing shapes with holes.
12
+ A technical description of the algorithm is on [visioncortex.org/vtracer-docs](//www.visioncortex.org/vtracer-docs).
13
+ To use it we need to have the python interface in place: pip install vtracer
14
+ """
15
+
16
+ """
17
+ Not needed left here for reference purposes
18
+
19
+ def simplified_load(source : str, bbox : tuple) -> list:
20
+ from meerk40t.tools.geomstr import Geomstr
21
+ from meerk40t.svgelements import Matrix, Color
22
+ from meerk40t.core.node.elem_path import PathNode
23
+ from time import perf_counter
24
+ t0 = perf_counter()
25
+ time_geom = 0
26
+ time_trans = 0
27
+ time_read = 0
28
+ time_bbox = 0
29
+ result_list = []
30
+ p_pattern = '<path d="'
31
+ t_pattern = 'transform="'
32
+ f_pattern = 'fill="'
33
+ black = Color("black")
34
+ content = []
35
+ min_x = float("inf")
36
+ min_y = float("inf")
37
+ max_x = -float("inf")
38
+ max_y = -float("inf")
39
+ with open(source, "r") as svg_file:
40
+ tt = perf_counter()
41
+ all_lines = svg_file.readlines()
42
+ time_read = perf_counter() - tt
43
+ for line in all_lines:
44
+ idx_start = line.find(p_pattern, 0)
45
+ if idx_start < 0:
46
+ continue
47
+ idx_end = line.find('"', idx_start + len(p_pattern))
48
+ d_str = line[idx_start + len(p_pattern):idx_end]
49
+ # print (d_str)
50
+ tt = perf_counter()
51
+ geom = Geomstr.svg(d_str)
52
+ time_geom += perf_counter() - tt
53
+ if geom.index == 0:
54
+ # print (f"Strange, empty from '{d_str}' ({line})")
55
+ continue
56
+ fill_value = None
57
+ mat_start = line.find(t_pattern, idx_end)
58
+ if mat_start >= 0:
59
+ tt = perf_counter()
60
+ mat_end = line.find('"', mat_start + len(t_pattern))
61
+ mat_str = line[mat_start + len(t_pattern):mat_end]
62
+ matrix = Matrix(mat_str)
63
+ geom.transform(matrix)
64
+ time_trans += perf_counter() - tt
65
+ fill_start = line.find(f_pattern, idx_end)
66
+ if fill_start >= 0:
67
+ fill_end = line.find('"', fill_start + len(f_pattern))
68
+ fill_str = line[fill_start + len(f_pattern):fill_end]
69
+ # if fill_str == "#ffffff":
70
+ # continue
71
+
72
+ else:
73
+ fill_str = ""
74
+ content.append((geom, fill_str))
75
+ tt = perf_counter()
76
+ g_bb = geom.bbox()
77
+ min_x = min(min_x, g_bb[0])
78
+ min_y = min(min_y, g_bb[1])
79
+ max_x = max(max_x, g_bb[2])
80
+ max_y = max(max_y, g_bb[3])
81
+ time_bbox += perf_counter() - tt
82
+ t1 = perf_counter()
83
+ if content:
84
+ sx = (bbox[2] - bbox[0]) / (max_x - min_x)
85
+ sy = (bbox[3] - bbox[1]) / (max_y - min_y)
86
+ tx = bbox[0] - min_x
87
+ ty = bbox[1] - min_y
88
+ components = (sx, 0, 0, sy, tx, ty)
89
+ matrix = Matrix(components)
90
+ else:
91
+ matrix = None
92
+ for geom, fill_str in content:
93
+ if matrix:
94
+ tt = perf_counter()
95
+ geom.transform(matrix)
96
+ time_trans += perf_counter() - tt
97
+ node = PathNode(geometry = geom, stroke=black, stroke_width = 500)
98
+ if fill_str:
99
+ fill_value = Color(fill_str)
100
+ node.fill = fill_value
101
+ result_list.append(node)
102
+ t2 = perf_counter()
103
+ # print (f"Loading and geometry creation: {t1-t0:.2f}sec, node creation: {t2-t1:.2f} sec, total: {t2-t0:.2f}sec")
104
+ # print (f"Pure creation: {time_geom:.2f}sec, transform {time_trans:.2f}sec, reading {time_read:.2f}sec, bbox: {time_bbox:.2f}sec")
105
+ return result_list
106
+ """
107
+
108
+
109
+ def plugin(kernel, lifecycle=None):
110
+ if lifecycle == "invalidate":
111
+ try:
112
+ import vtracer
113
+ except ImportError:
114
+ # print("vtracer plugin could not load because vtracer is not installed.")
115
+ return True
116
+
117
+ if lifecycle == "register":
118
+ _ = kernel.translation
119
+
120
+ @kernel.console_command(
121
+ "vtracer",
122
+ help=_("return paths around image"),
123
+ input_type=("image", "elements", None),
124
+ output_type="elements",
125
+ )
126
+ def do_vtracer(
127
+ channel,
128
+ data=None,
129
+ **kwargs,
130
+ ):
131
+ try:
132
+ import os
133
+ from time import perf_counter
134
+
135
+ from vtracer import convert_image_to_svg_py
136
+
137
+ from meerk40t.core.units import Length
138
+ from meerk40t.kernel import get_safe_path
139
+ except ImportError:
140
+ channel("vtracer isn't installed, use 'pip install vtracer'")
141
+ return None
142
+ elements = kernel.root.elements
143
+ if data is None:
144
+ data = list(elements.elems(emphasized=True))
145
+ if not data:
146
+ channel(_("Nothing selected"))
147
+ return
148
+ images = [node for node in data if hasattr(node, "image")]
149
+ if not images:
150
+ channel(_("No images selected"))
151
+ return
152
+
153
+ safe_dir = os.path.realpath(get_safe_path(kernel.name))
154
+ input_file = os.path.join(safe_dir, "_vtrace_input.png")
155
+ output_file = os.path.join(safe_dir, "_vtrace_output.svg")
156
+ t_start = perf_counter()
157
+ t_convert = t_load = 0
158
+ # _("Vectorizing image")
159
+ with elements.undoscope("Vectorizing image"):
160
+ for node in images:
161
+ # kernel.root.signal("freeze_tree", True)
162
+ _start = perf_counter()
163
+ bb = node.bounds
164
+ im_wd = bb[2] - bb[0]
165
+ im_ht = bb[3] - bb[1]
166
+ im_x = bb[0]
167
+ im_y = bb[1]
168
+ flag = node.prevent_crop
169
+ if not flag:
170
+ node.prevent_crop = True
171
+ node.update(None)
172
+ bb2 = node.bounds
173
+ dx = bb2[0] - bb[0]
174
+ dy = bb2[1] - bb[1]
175
+ bb = (bb[0] - dx, bb[1] - dy, bb[2] - dx, bb[3] - dy)
176
+ node.prevent_crop = flag
177
+ node.update(None)
178
+ image = node.image
179
+
180
+ image.save(input_file)
181
+ convert_image_to_svg_py(
182
+ image_path=input_file, out_path=output_file, colormode="binary"
183
+ )
184
+ t_convert += perf_counter() - _start
185
+
186
+ _start = perf_counter()
187
+ # print (f"Vectorization took {t1-t0:.1f}sec, now loading file, executing {cmd}")
188
+ elements.suppress_updates = True
189
+ cmd = (
190
+ f'xload "{output_file}"'
191
+ + f" {Length(im_x).length_mm}"
192
+ + f" {Length(im_y).length_mm}"
193
+ f" {Length(im_wd).length_mm}" + f" {Length(im_ht).length_mm}"
194
+ )
195
+ kernel.root(f"{cmd}\n")
196
+ # elements.suppress_updates = True
197
+ # nodes = simplified_load(output_file, bb)
198
+ # elem = kernel.elements.elem_branch.add(type="group", label=f"VTrace ({node.display_label()})")
199
+ # for e in nodes:
200
+ # elem.add_node(e)
201
+ # elements.suppress_updates = False
202
+ t_load += perf_counter() - _start
203
+ try:
204
+ os.remove(input_file)
205
+ os.remove(output_file)
206
+ except (PermissionError, OSError):
207
+ pass
208
+ except Exception as e:
209
+ channel(f"Could not remove temporary files: {e}")
210
+ # kernel.root.signal("freeze_tree", False)
211
+ t_end = perf_counter()
212
+ channel(
213
+ _(
214
+ "Time needed for vectorisation: {time_total}sec (analysis: {time_convert}sec, loading: {time_load}sec)"
215
+ ).format(
216
+ time_total=round(t_end - t_start, 1),
217
+ time_convert=round(t_convert, 1),
218
+ time_load=round(t_load, 1),
219
+ )
220
+ )
221
+ kernel.root.signal("refresh_scene", "Scene")
222
+ return "elements", None
meerk40t/grbl/device.py CHANGED
@@ -7,6 +7,7 @@ Registers relevant commands and options.
7
7
 
8
8
  from time import sleep
9
9
 
10
+ from meerk40t.device.devicechoices import get_effect_choices
10
11
  from meerk40t.kernel import CommandSyntaxError, Service, signal_listener
11
12
 
12
13
  from ..core.laserjob import LaserJob
@@ -16,7 +17,6 @@ from ..core.view import View
16
17
  from ..device.mixins import Status
17
18
  from .controller import GrblController
18
19
  from .driver import GRBLDriver
19
- from meerk40t.device.devicechoices import get_effect_choices
20
20
 
21
21
 
22
22
  class GRBLDevice(Service, Status):
@@ -181,6 +181,30 @@ class GRBLDevice(Service, Status):
181
181
  "tip": _("Override native home location"),
182
182
  "subsection": "_60_Home position",
183
183
  },
184
+ {
185
+ "attr": "supports_z_axis",
186
+ "object": self,
187
+ "default": False,
188
+ "type": bool,
189
+ "label": _("Supports Z-axis"),
190
+ "tip": _("Does this device have a Z-axis?"),
191
+ "subsection": "_70_Z-Axis support",
192
+ },
193
+ {
194
+ "attr": "z_home_command",
195
+ "object": self,
196
+ "default": "$HZ",
197
+ "type": str,
198
+ "style": "combosmall",
199
+ "choices": [
200
+ "$HZ",
201
+ "G28 Z",
202
+ ],
203
+ "exclusive": False,
204
+ "label": _("Z-Homing"),
205
+ "tip": _("Which command triggers the z-homing sequence"),
206
+ "subsection": "_70_Z-Axis support",
207
+ },
184
208
  {
185
209
  "attr": "signal_updates",
186
210
  "object": self,
@@ -258,6 +282,7 @@ class GRBLDevice(Service, Status):
258
282
  choice_dict["display"] = ["pyserial-not-installed"]
259
283
 
260
284
  from platform import system
285
+
261
286
  is_linux = system() == "Linux"
262
287
  choices = [
263
288
  {
@@ -271,7 +296,7 @@ class GRBLDevice(Service, Status):
271
296
  "section": "_10_Serial Interface",
272
297
  "subsection": "_00_",
273
298
  "dynamic": update,
274
- "exclusive": not is_linux,
299
+ "exclusive": not is_linux,
275
300
  },
276
301
  {
277
302
  "attr": "baud_rate",
@@ -635,6 +660,56 @@ class GRBLDevice(Service, Status):
635
660
  if self.permit_serial:
636
661
  self._register_console_serial()
637
662
 
663
+ @self.console_command(
664
+ "z_home",
665
+ help=_("Homes the z-Axis"),
666
+ input_type=None,
667
+ )
668
+ def command_zhome(command, channel, _, data=None, remainder=None, **kwgs):
669
+ if not self.supports_z_axis:
670
+ channel(_("This device does not support a z-axis."))
671
+ return
672
+ zhome = self.z_home_command
673
+ if not zhome:
674
+ channel(_("There is no homing sequence defined."))
675
+ return
676
+ channel(_("Z-Homing..."))
677
+ self.driver(zhome + self.driver.line_end)
678
+
679
+ @self.console_argument("step", type=Length, help=_("Amount to move the z-axis"))
680
+ @self.console_command(
681
+ "z_move",
682
+ help=_("Moves the z-Axis by the given amount"),
683
+ input_type=None,
684
+ )
685
+ def command_zmove_rel(command, channel, _, data=None, step=None, **kwgs):
686
+ if not self.supports_z_axis:
687
+ channel(_("This device does not support a z-axis."))
688
+ return
689
+ if step is None:
690
+ channel(_("No z-movement defined"))
691
+ return
692
+ # relative movement in mm
693
+ gcode = f"G91 G21 Z{step.mm:.3f}"
694
+ self.driver(gcode + self.driver.line_end)
695
+
696
+ @self.console_argument("step", type=Length, help=_("New z-axis position"))
697
+ @self.console_command(
698
+ "z_move_to",
699
+ help=_("Moves the z-Axis to the given position"),
700
+ input_type=None,
701
+ )
702
+ def command_zmove_abs(command, channel, _, data=None, step=None, **kwgs):
703
+ if not self.supports_z_axis:
704
+ channel(_("This device does not support a z-axis."))
705
+ return
706
+ if step is None:
707
+ channel(_("No z-movement defined"))
708
+ return
709
+ # absolute movement in mm
710
+ gcode = f"G91 G20 Z{step.mm:.3f}"
711
+ self.driver(gcode + self.driver.line_end)
712
+
638
713
  @self.console_command(
639
714
  ("gcode", "grbl"),
640
715
  help=_("Send raw gcode to the device"),
@@ -901,9 +976,7 @@ class GRBLDevice(Service, Status):
901
976
  channel(_("Interpreter cannot be attached to any device."))
902
977
  return
903
978
 
904
- @self.console_argument(
905
- "index", type=int, help=_("macro to run (1-5).")
906
- )
979
+ @self.console_argument("index", type=int, help=_("macro to run (1-5)."))
907
980
  @self.console_command(
908
981
  "macro",
909
982
  help=_("Send a predefined macro to the device."),
@@ -916,11 +989,11 @@ class GRBLDevice(Service, Status):
916
989
  macrotext = self.setting(str, f"macro_{idx}", "")
917
990
  channel(f"Content of macro {idx + 1}:")
918
991
  for no, line in enumerate(macrotext.splitlines()):
919
- channel (f"{no:2d}: {line}")
992
+ channel(f"{no:2d}: {line}")
920
993
  return
921
994
  err = True
922
995
  try:
923
- macro_index = int(index) -1
996
+ macro_index = int(index) - 1
924
997
  if 0 <= macro_index <= 4:
925
998
  err = False
926
999
  except ValueError:
@@ -1081,4 +1154,4 @@ class GRBLDevice(Service, Status):
1081
1154
  def get_raster_instructions(self):
1082
1155
  return {
1083
1156
  "gantry": True,
1084
- }
1157
+ }
meerk40t/gui/about.py CHANGED
@@ -1790,6 +1790,25 @@ class ComponentPanel(ScrolledPanel):
1790
1790
  entry[2] = status
1791
1791
  self.content.append(entry)
1792
1792
 
1793
+ def get_vtrace():
1794
+ entry = ["vtracer", "", "", "https://pypi.org/project/vtracer/"]
1795
+ try:
1796
+ import vtracer
1797
+
1798
+ # for e in vars(vtracer):
1799
+ # print (f"var {e} - {getattr(vtracer, e)}")
1800
+ try:
1801
+ info = vtracer.__version__
1802
+ except AttributeError:
1803
+ info = "??"
1804
+ status = _("Present")
1805
+ except ImportError:
1806
+ info = "??"
1807
+ status = _("Missing")
1808
+ entry[1] = info
1809
+ entry[2] = status
1810
+ self.content.append(entry)
1811
+
1793
1812
  def get_ezdxf():
1794
1813
  entry = ["ezdxf", "", "", "https://ezdxf.readthedocs.io/en/stable/"]
1795
1814
  try:
@@ -1959,6 +1978,7 @@ class ComponentPanel(ScrolledPanel):
1959
1978
  get_numpy()
1960
1979
  get_pillow()
1961
1980
  get_potrace()
1981
+ get_vtrace()
1962
1982
  get_ezdxf()
1963
1983
  get_pyusb()
1964
1984
  get_pyserial()
@@ -5,12 +5,12 @@ from meerk40t.gui.icons import icons8_manager
5
5
  from meerk40t.gui.mwindow import MWindow
6
6
  from meerk40t.gui.wxutils import (
7
7
  StaticBoxSizer,
8
+ TextCtrl,
8
9
  dip_size,
9
10
  wxButton,
10
11
  wxListCtrl,
11
12
  wxStaticText,
12
13
  wxTreeCtrl,
13
- TextCtrl,
14
14
  )
15
15
  from meerk40t.kernel import lookup_listener, signal_listener
16
16
 
@@ -71,15 +71,15 @@ class SelectDevice(wx.Dialog):
71
71
  )
72
72
  # Used for proper sorting in the device add menu.
73
73
  self.sort_family_name = {
74
- _("K-Series CO2-Laser"): 99,
75
- _("Ortur Diode-Laser"): 98,
76
- _("Longer Diode-Laser"): 97,
77
- _("Newly CO2-Laser"): 96,
78
- _("Generic UV-Laser"): 95,
79
- _("Generic CO2-Laser"): 94,
80
- _("Generic Fibre-Laser"): 93,
81
- _("Generic Diode-Laser"): 92,
82
- _("Generic"): 91,
74
+ _("K-Series CO2-Laser"): 99,
75
+ _("Ortur Diode-Laser"): 98,
76
+ _("Longer Diode-Laser"): 97,
77
+ _("Newly CO2-Laser"): 96,
78
+ _("Generic UV-Laser"): 95,
79
+ _("Generic CO2-Laser"): 94,
80
+ _("Generic Fibre-Laser"): 93,
81
+ _("Generic Diode-Laser"): 92,
82
+ _("Generic"): 91,
83
83
  }
84
84
  sizer_main.Add(self.tree_devices, 3, wx.EXPAND, 0)
85
85
  self.no_msg = (
@@ -125,7 +125,6 @@ class SelectDevice(wx.Dialog):
125
125
  self.Layout()
126
126
  self.populate_tree()
127
127
 
128
-
129
128
  def populate_tree(self):
130
129
  tree = self.tree_devices
131
130
  tree.DeleteAllItems()
@@ -223,18 +222,20 @@ class DevicePanel(wx.Panel):
223
222
  | wx.LC_SINGLE_SEL
224
223
  | wx.LC_SORT_ASCENDING,
225
224
  context=self.context,
226
- list_name="list_devices"
225
+ list_name="list_devices",
227
226
  )
228
227
  self.list_columns = {
229
228
  "device": 0,
230
229
  "driver": 1,
231
230
  "family": 2,
232
231
  "status": 3,
232
+ "location": 4,
233
233
  }
234
234
  self.devices_list.InsertColumn(self.list_columns["device"], _("Device"))
235
235
  self.devices_list.InsertColumn(self.list_columns["driver"], _("Driver"))
236
236
  self.devices_list.InsertColumn(self.list_columns["family"], _("Type"))
237
237
  self.devices_list.InsertColumn(self.list_columns["status"], _("Status"))
238
+ self.devices_list.InsertColumn(self.list_columns["location"], _("Interface"))
238
239
  self.devices_list.resize_columns()
239
240
  sizer_1.Add(self.devices_list, 7, wx.EXPAND, 0)
240
241
 
@@ -287,9 +288,7 @@ class DevicePanel(wx.Panel):
287
288
  self.Bind(
288
289
  wx.EVT_BUTTON, self.on_button_create_device, self.button_create_device
289
290
  )
290
- self.Bind(
291
- wx.EVT_BUTTON, self.on_button_copy_device, self.button_copy_device
292
- )
291
+ self.Bind(wx.EVT_BUTTON, self.on_button_copy_device, self.button_copy_device)
293
292
  self.Bind(
294
293
  wx.EVT_BUTTON, self.on_button_remove_device, self.button_remove_device
295
294
  )
@@ -317,7 +316,6 @@ class DevicePanel(wx.Panel):
317
316
  def pane_hide(self, *args):
318
317
  pass
319
318
 
320
-
321
319
  def on_start_edit(self, event):
322
320
  event.Allow()
323
321
 
@@ -410,11 +408,17 @@ class DevicePanel(wx.Panel):
410
408
  except AttributeError:
411
409
  pass
412
410
 
411
+ try:
412
+ loc_info = device.location()
413
+ except AttributeError:
414
+ loc_info = "undefined"
415
+
413
416
  self.devices_list.SetItem(index, self.list_columns["driver"], type_info)
414
417
  self.devices_list.SetItem(index, self.list_columns["family"], family_info)
415
418
  self.devices_list.SetItem(
416
419
  index, self.list_columns["status"], _(active_status)
417
420
  )
421
+ self.devices_list.SetItem(index, self.list_columns["location"], loc_info)
418
422
  self.devices_list.SetItemData(index, dev_index)
419
423
  if self.context.device is device:
420
424
  self.devices_list.SetItemTextColour(index, wx.RED)
@@ -63,6 +63,8 @@ class FormatPainter:
63
63
  ("linecap", True),
64
64
  ("linejoin", True),
65
65
  ("fillrule", True),
66
+ ("stroke_dash", True),
67
+ ("mktablength", False),
66
68
  # Image attributes
67
69
  ("dpi", False),
68
70
  ("operations", False),
@@ -193,6 +195,8 @@ class FormatPainter:
193
195
  flag_changed = True
194
196
 
195
197
  if flag_changed:
198
+ if hasattr(node, "empty_cache"):
199
+ node.empty_cache()
196
200
  nodes_changed.append(node)
197
201
  if node.type == "elem image":
198
202
  nodes_images.append(node)