From 5a98bfe8655b4cb25422cc17f6afd27d353a67bf Mon Sep 17 00:00:00 2001 From: neofelis2X Date: Thu, 8 Jun 2023 23:28:23 +0200 Subject: [PATCH 1/6] Projectlayer: load only sliced svg files --- printrun/projectlayer.py | 144 +++++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 66 deletions(-) diff --git a/printrun/projectlayer.py b/printrun/projectlayer.py index 03da567f7..aefb5dd16 100644 --- a/printrun/projectlayer.py +++ b/printrun/projectlayer.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Printrun. If not, see . -import xml.etree.ElementTree +import xml.etree.ElementTree as ET import wx import os import time @@ -62,16 +62,13 @@ def __init__(self, parent, title, res = (1024, 768), printer = None, scale = 1.0 self.layer_red = False def clear_layer(self): - try: - dc = wx.MemoryDC() - dc.SelectObject(self.bitmap) - dc.SetBackground(wx.Brush("black")) - dc.Clear() - self.pic.SetBitmap(self.bitmap) - self.pic.Show() - self.Refresh() - except: - raise + dc = wx.MemoryDC() + dc.SelectObject(self.bitmap) + dc.SetBackground(wx.Brush("black")) + dc.Clear() + self.pic.SetBitmap(self.bitmap) + self.pic.Show() + self.Refresh() def resize(self, res = (1024, 768)): self.bitmap = wx.Bitmap(*res) @@ -83,56 +80,53 @@ def resize(self, res = (1024, 768)): dc.SelectObject(wx.NullBitmap) def draw_layer(self, image): - try: - dc = wx.MemoryDC() - dc.SelectObject(self.bitmap) - dc.SetBackground(wx.Brush("black")) - dc.Clear() - - if self.slicer in ['Slic3r', 'Skeinforge']: + dc = wx.MemoryDC() + dc.SelectObject(self.bitmap) + dc.SetBackground(wx.Brush("black")) + dc.Clear() - if self.scale != 1.0: - image = copy.deepcopy(image) - height = float(image.get('height').replace('m', '')) - width = float(image.get('width').replace('m', '')) + if self.slicer in ['Slic3r', 'Skeinforge']: - image.set('height', str(height * self.scale) + 'mm') - image.set('width', str(width * self.scale) + 'mm') - image.set('viewBox', '0 0 ' + str(width * self.scale) + ' ' + str(height * self.scale)) + if self.scale != 1.0: + image = copy.deepcopy(image) + height = float(image.get('height').replace('m', '')) + width = float(image.get('width').replace('m', '')) - g = image.find("{http://www.w3.org/2000/svg}g") - g.set('transform', 'scale(' + str(self.scale) + ')') + image.set('height', str(height * self.scale) + 'mm') + image.set('width', str(width * self.scale) + 'mm') + image.set('viewBox', '0 0 ' + str(width * self.scale) + ' ' + str(height * self.scale)) - pngbytes = PNGSurface.convert(dpi = self.dpi, bytestring = xml.etree.ElementTree.tostring(image)) - pngImage = wx.Image(io.BytesIO(pngbytes)) + g = image.find("{http://www.w3.org/2000/svg}g") + g.set('transform', 'scale(' + str(self.scale) + ')') - self.Parent.statusbar.SetLabel(f"Width: {pngImage.Width}, dpi: {self.dpi:.2f} -> Width: {((pngImage.Width / self.dpi) * 25.4):.3f} mm") + pngbytes = PNGSurface.convert(dpi = self.dpi, bytestring = ET.tostring(image)) + pngImage = wx.Image(io.BytesIO(pngbytes)) - if self.layer_red: - pngImage = pngImage.AdjustChannels(1, 0, 0, 1) + self.Parent.statusbar.SetLabel(f"Width: {pngImage.Width}, dpi: {self.dpi:.2f} -> Width: {((pngImage.Width / self.dpi) * 25.4):.3f} mm") - # AGE2022-07-31 Python 3.10 and DrawBitmap expects offset - # as integer value. Convert float values to int - dc.DrawBitmap(wx.Bitmap(pngImage), int(self.offset[0]), int(self.offset[1]), True) + if self.layer_red: + pngImage = pngImage.AdjustChannels(1, 0, 0, 1) - elif self.slicer == 'bitmap': - if isinstance(image, str): - image = wx.Image(image) - if self.layer_red: - image = image.AdjustChannels(1, 0, 0, 1) - # AGE2023-04-19 Python 3.10 and DrawBitmap expects offset - # as integer value. Convert float values to int - bitmap = wx.Bitmap(image.Scale(int(image.Width * self.scale), int(image.Height * self.scale))) - dc.DrawBitmap(bitmap, int(self.offset[0]), int(-self.offset[1]), True) - else: - raise Exception(self.slicer + _(" is an unknown method.")) + # AGE2022-07-31 Python 3.10 and DrawBitmap expects offset + # as integer value. Convert float values to int + dc.DrawBitmap(wx.Bitmap(pngImage), int(self.offset[0]), int(self.offset[1]), True) - self.pic.SetBitmap(self.bitmap) - self.pic.Show() - self.Refresh() + elif self.slicer == 'bitmap': + if isinstance(image, str): + image = wx.Image(image) + if self.layer_red: + image = image.AdjustChannels(1, 0, 0, 1) + # AGE2023-04-19 Python 3.10 and DrawBitmap expects offset + # as integer value. Convert float values to int + bitmap = wx.Bitmap(image.Scale(int(image.Width * self.scale), int(image.Height * self.scale))) + dc.DrawBitmap(bitmap, int(self.offset[0]), int(-self.offset[1]), True) + else: + self.Parent.statusbar.SetLabel(_("No valid file loaded.")) + return - except: - raise + self.pic.SetBitmap(self.bitmap) + self.pic.Show() + self.Refresh() def show_img_delay(self, image): self.Parent.statusbar.SetLabel(_("Showing, Timestamp %s s") % str(time.perf_counter())) @@ -544,10 +538,11 @@ def set_estimated_time(self): self.estimated_time.SetLabel(time.strftime("%H:%M:%S", time.gmtime(estimated_time))) def parse_svg(self, name): - et = xml.etree.ElementTree.ElementTree(file = name) - # xml.etree.ElementTree.dump(et) + et = ET.ElementTree(file = name) - slicer = 'Slic3r' if et.getroot().find('{http://www.w3.org/2000/svg}metadata') is None else 'Skeinforge' + namespaces = dict(node for (_, node) in ET.iterparse(name, events=['start-ns'])) + slicer = 'Slic3r' if 'slic3r' in namespaces.keys() else \ + 'Skeinforge' if et.getroot().find('{http://www.w3.org/2000/svg}metadata') else 'None' zlast = 0 zdiff = 0 ol = [] @@ -560,7 +555,7 @@ def parse_svg(self, name): zdiff = z - zlast zlast = z - svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg') + svgSnippet = ET.Element('{http://www.w3.org/2000/svg}svg') svgSnippet.set('height', height + 'mm') svgSnippet.set('width', width + 'mm') @@ -569,8 +564,8 @@ def parse_svg(self, name): svgSnippet.append(i) ol += [svgSnippet] - else: + elif slicer == 'Skeinforge': slice_layers = et.findall("{http://www.w3.org/2000/svg}metadata")[0].findall("{http://www.reprap.org/slice}layers")[0] minX = slice_layers.get('minX') maxX = slice_layers.get('maxX') @@ -596,7 +591,7 @@ def parse_svg(self, name): zdiff = z - zlast zlast = z - svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg') + svgSnippet = ET.Element('{http://www.w3.org/2000/svg}svg') svgSnippet.set('height', height + 'mm') svgSnippet.set('width', width + 'mm') @@ -609,7 +604,8 @@ def parse_svg(self, name): def parse_3DLP_zip(self, name): if not zipfile.is_zipfile(name): - raise Exception(name + _(" is not a zip file!")) + self.statusbar.SetLabel(_(f"{os.path.split(name)[1]} is not a zip file.")) + return 0, -1, "None" accepted_image_types = ['gif', 'tiff', 'jpg', 'jpeg', 'bmp', 'png'] with zipfile.ZipFile(name, 'r') as zipFile: self.image_dir = tempfile.mkdtemp() @@ -642,14 +638,27 @@ def load_file(self, event): if not os.path.exists(name): self.status.SetStatusText(_("File not found!")) return - if name.endswith(".3dlp.zip"): - layers = self.parse_3DLP_zip(name) - layerHeight = float(self.thickness.GetValue()) - else: + if name.endswith(('.svg', '.SVG')): layers = self.parse_svg(name) + if layers[2] == 'None': + self.statusbar.SetLabel(_(f"{os.path.split(name)[1]} is not a sliced svg-file.")) + self.display_filename("") + self.set_total_layers("") + self.set_current_layer(0) + self.estimated_time.SetLabel("") + return layerHeight = round(layers[1], 3) self.thickness.SetValue(str(layerHeight)) self.statusbar.SetLabel(_("Layer thickness detected: {0} mm").format(layerHeight)) + else: + layers = self.parse_3DLP_zip(name) + if layers[2] == 'None': + self.display_filename("") + self.set_total_layers("") + self.set_current_layer(0) + self.estimated_time.SetLabel("") + return + layerHeight = float(self.thickness.GetValue()) self.statusbar.SetLabel(_("{0} layers found, total height {1:.2f} mm").format(len(layers[0]), layerHeight * len(layers[0]))) self.layers = layers self.set_total_layers(len(layers[0])) @@ -943,10 +952,13 @@ def get_btn_label(self, value): def reset_all(self, event): # Ask confirmation for deleting - reset_dialog = wx.MessageDialog(self, - message = _("Are you sure you want to reset all the settings to the defaults?") + "\n" + - _("Be aware that the defaults are not guaranteed to work well with your machine."), - caption = _("Reset ProjectLayer Settings"), style = wx.YES_NO | wx.ICON_EXCLAMATION) + reset_dialog = wx.MessageDialog( + self, + message = _("Are you sure you want to reset all the settings " + "to the defaults?\nBe aware that the defaults are " + "not guaranteed to work well with your machine."), + caption = _("Reset ProjectLayer Settings"), + style = wx.YES_NO | wx.ICON_EXCLAMATION) if reset_dialog.ShowModal() == wx.ID_YES: # Reset all settings From 467210c8912fa50cc2583845613ea81f4dcf2069 Mon Sep 17 00:00:00 2001 From: neofelis2X Date: Sat, 10 Jun 2023 22:17:02 +0200 Subject: [PATCH 2/6] Projectlayer: add support to load Prusa SL1 files --- printrun/projectlayer.py | 327 ++++++++++++++++++++++++++++----------- 1 file changed, 234 insertions(+), 93 deletions(-) diff --git a/printrun/projectlayer.py b/printrun/projectlayer.py index aefb5dd16..f8c4ae69e 100644 --- a/printrun/projectlayer.py +++ b/printrun/projectlayer.py @@ -19,7 +19,6 @@ import time import zipfile import tempfile -import shutil from cairosvg.surface import PNGSurface import io import imghdr @@ -29,8 +28,9 @@ import math from printrun.gui.widgets import get_space from .utils import install_locale -install_locale('pronterface') + # Set up Internationalization using gettext +install_locale('pronterface') class DisplayFrame(wx.Frame): def __init__(self, parent, title, res = (1024, 768), printer = None, scale = 1.0, offset = (0, 0)): @@ -40,7 +40,7 @@ def __init__(self, parent, title, res = (1024, 768), printer = None, scale = 1.0 self.pic = wx.StaticBitmap(self) self.bitmap = wx.Bitmap(*res) self.bbitmap = wx.Bitmap(*res) - self.slicer = 'bitmap' + self.slicer = 'Bitmap' self.dpi = 96 dc = wx.MemoryDC() dc.SelectObject(self.bbitmap) @@ -102,7 +102,8 @@ def draw_layer(self, image): pngbytes = PNGSurface.convert(dpi = self.dpi, bytestring = ET.tostring(image)) pngImage = wx.Image(io.BytesIO(pngbytes)) - self.Parent.statusbar.SetLabel(f"Width: {pngImage.Width}, dpi: {self.dpi:.2f} -> Width: {((pngImage.Width / self.dpi) * 25.4):.3f} mm") + self.Parent.statusbar.SetLabel(f"Width: {pngImage.Width}, dpi: {self.dpi:.2f} -> " + f"Width: {((pngImage.Width / self.dpi) * 25.4):.3f} mm") if self.layer_red: pngImage = pngImage.AdjustChannels(1, 0, 0, 1) @@ -111,7 +112,7 @@ def draw_layer(self, image): # as integer value. Convert float values to int dc.DrawBitmap(wx.Bitmap(pngImage), int(self.offset[0]), int(self.offset[1]), True) - elif self.slicer == 'bitmap': + elif self.slicer in ('Bitmap', 'PrusaSlicer'): if isinstance(image, str): image = wx.Image(image) if self.layer_red: @@ -186,6 +187,7 @@ def next_img(self): wx.CallAfter(self.show_img_delay, self.layers[self.index]) self.index += 1 else: + self.Parent.stop_present(wx.wxEVT_NULL) self.Parent.statusbar.SetLabel(_("End")) wx.CallAfter(self.pic.Hide) wx.CallAfter(self.Refresh) @@ -240,10 +242,11 @@ def _get_setting(self, name, val): return val def __init__(self, parent, printer = None): - wx.Dialog.__init__(self, parent, title = _("ProjectLayer Control"), + wx.Dialog.__init__(self, parent, title = _("Layer Projector Control"), style = wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT) self.pronterface = parent - self.display_frame = DisplayFrame(self, title = _("ProjectLayer Display"), printer = printer) + self.image_dir = '' + self.display_frame = DisplayFrame(self, title = _("Layer Projector Display"), printer = printer) self.panel = wx.Panel(self) @@ -257,10 +260,11 @@ def fit(ev): buttonGroup = wx.StaticBox(self.panel, label = _("Controls")) buttonbox = wx.StaticBoxSizer(buttonGroup, wx.HORIZONTAL) - load_button = wx.Button(buttonGroup, -1, _("Load")) - load_button.Bind(wx.EVT_BUTTON, self.load_file) - load_button.SetToolTip(_("Choose an SVG file created from Slic3r or Skeinforge, or a zip file of bitmap images (Extension: .3dlp.zip).")) - buttonbox.Add(load_button, 1, + self.load_button = wx.Button(buttonGroup, -1, _("Load")) + self.load_button.Bind(wx.EVT_BUTTON, self.load_file) + self.load_button.SetToolTip(_("Choose a SVG file created from Slic3r or Skeinforge, a PrusaSlicer SL1-file " + "or a zip file of bitmap images (Extension: .3dlp.zip).")) + buttonbox.Add(self.load_button, 1, flag = wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border = get_space('mini')) self.present_button = wx.Button(buttonGroup, -1, _("Start")) @@ -272,8 +276,8 @@ def fit(ev): self.pause_button = wx.Button(buttonGroup, -1, self.get_btn_label('pause')) self.pause_button.Bind(wx.EVT_BUTTON, self.pause_present) - self.pause_button.SetToolTip(_("Pauses the presentation. Can be resumed afterwards by clicking this button,") + - _(" or restarted by clicking start again.")) + self.pause_button.SetToolTip(_("Pauses the presentation. Can be resumed afterwards by " + "clicking this button, or restarted by clicking start again.")) buttonbox.Add(self.pause_button, 1, flag = wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border = get_space('mini')) self.pause_button.Disable() @@ -290,34 +294,40 @@ def fit(ev): fieldsizer = wx.GridBagSizer(vgap = get_space('minor'), hgap = get_space('minor')) # Left Column - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Layer (mm):")), pos = (0, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Layer (mm):")), pos = (0, 0), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) self.thickness = wx.TextCtrl(settingsGroup, -1, str(self._get_setting("project_layer", "0.1")), size = (125, -1)) self.thickness.Bind(wx.EVT_TEXT, self.update_thickness) - self.thickness.SetToolTip(_("The thickness of each slice. Should match the value used to slice the model.") + - _(" SVG files update this value automatically, 3dlp.zip files have to be manually entered.")) + self.thickness.SetToolTip(_("The thickness of each slice. Should match the value used to slice the model. " + "SVG files update this value automatically, 3dlp.zip files have to be manually entered.")) fieldsizer.Add(self.thickness, pos = (0, 1)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Exposure (s):")), pos = (1, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Exposure (s):")), pos = (1, 0), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) self.interval = wx.TextCtrl(settingsGroup, -1, str(self._get_setting("project_interval", "0.5")), size = (125, -1)) self.interval.Bind(wx.EVT_TEXT, self.update_interval) self.interval.SetToolTip(_("How long each slice should be displayed.")) fieldsizer.Add(self.interval, pos = (1, 1)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Blank (s):")), pos = (2, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Blank (s):")), pos = (2, 0), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) self.pause = wx.TextCtrl(settingsGroup, -1, str(self._get_setting("project_pause", "0.5")), size = (125, -1)) self.pause.Bind(wx.EVT_TEXT, self.update_pause) - self.pause.SetToolTip(_("The pause length between slices. This should take into account any movement of the Z axis,") + - _(" plus time to prepare the resin surface (sliding, tilting, sweeping, etc).")) + self.pause.SetToolTip(_("The pause length between slices. This should take into account any movement of the Z axis, " + "plus time to prepare the resin surface (sliding, tilting, sweeping, etc).")) fieldsizer.Add(self.pause, pos = (2, 1)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Scale:")), pos = (3, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.scale = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting('project_scale', 1.0), inc = 0.1, size = (125, -1)) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Scale:")), pos = (3, 0), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + self.scale = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting('project_scale', 1.0), + inc = 0.1, size = (125, -1)) self.scale.SetDigits(3) self.scale.Bind(wx.EVT_SPINCTRLDOUBLE, self.update_scale) self.scale.SetToolTip(_("The additional scaling of each slice.")) fieldsizer.Add(self.scale, pos = (3, 1)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Direction:")), pos = (4, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Direction:")), pos = (4, 0), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) self.direction = wx.Choice(settingsGroup, -1, choices = [_('Top Down'), _('Bottom Up')], size = (125, -1)) saved_direction = self._get_setting('project_direction', 0) try: # This setting used to be a string, older values need to be replaced with an index @@ -330,29 +340,38 @@ def fit(ev): self._set_setting('project_direction', saved_direction) self.direction.SetSelection(int(saved_direction)) self.direction.Bind(wx.EVT_CHOICE, self.update_direction) - self.direction.SetToolTip(_("The direction the Z axis should move. Top Down is where the projector is above") + - _(" the model, Bottom up is where the projector is below the model.")) + self.direction.SetToolTip(_("The direction the Z axis should move. Top Down is where the projector is above " + "the model, Bottom up is where the projector is below the model.")) fieldsizer.Add(self.direction, pos = (4, 1), flag = wx.ALIGN_CENTER_VERTICAL) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Overshoot (mm):")), pos = (5, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.overshoot = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting('project_overshoot', 3.0), inc = 0.1, min = 0, size = (125, -1)) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Overshoot (mm):")), pos = (5, 0), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + self.overshoot = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting('project_overshoot', 3.0), + inc = 0.1, min = 0, size = (125, -1)) self.overshoot.SetDigits(1) self.overshoot.Bind(wx.EVT_SPINCTRLDOUBLE, self.update_overshoot) - self.overshoot.SetToolTip(_("How far the axis should move beyond the next slice position for each slice. For Top Down printers this would dunk") + - _(" the model under the resi and then return. For Bottom Up printers this would raise the base away from the vat and then return.")) + self.overshoot.SetToolTip(_("How far the axis should move beyond the next slice position for each slice. " + "For Top Down printers this would dunk the model under the resi and then return. " + "For Bottom Up printers this would raise the base away from the vat and then return.")) fieldsizer.Add(self.overshoot, pos = (5, 1)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Pre-lift Gcode:")), pos = (6, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.prelift_gcode = wx.TextCtrl(settingsGroup, -1, str(self._get_setting("project_prelift_gcode", "").replace("\\n", '\n')), size = (-1, 35), style = wx.TE_MULTILINE) - self.prelift_gcode.SetToolTip(_("Additional gcode to run before raising the Z-axis.") + - _(" Be sure to take into account any additional time needed in the pause value, and be careful what gcode is added!")) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Pre-lift Gcode:")), pos = (6, 0), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + self.prelift_gcode = wx.TextCtrl(settingsGroup, -1, str(self._get_setting("project_prelift_gcode", "").replace("\\n", '\n')), + size = (-1, 35), style = wx.TE_MULTILINE) + self.prelift_gcode.SetToolTip(_("Additional gcode to run before raising the Z-axis. " + "Be sure to take into account any additional time needed " + "in the pause value, and be careful what gcode is added!")) self.prelift_gcode.Bind(wx.EVT_TEXT, self.update_prelift_gcode) fieldsizer.Add(self.prelift_gcode, pos = (6, 1), span = (2, 1), flag = wx.EXPAND) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Post-lift Gcode:")), pos = (6, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.postlift_gcode = wx.TextCtrl(settingsGroup, -1, str(self._get_setting("project_postlift_gcode", "").replace("\\n", '\n')), size = (-1, 35), style = wx.TE_MULTILINE) - self.postlift_gcode.SetToolTip(_("Additional gcode to run after raising the Z-axis.") + - _(" Be sure to take into account any additional time needed in the pause value, and be careful what gcode is added!")) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Post-lift Gcode:")), pos = (6, 2), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + self.postlift_gcode = wx.TextCtrl(settingsGroup, -1, str(self._get_setting("project_postlift_gcode", "").replace("\\n", '\n')), + size = (-1, 35), style = wx.TE_MULTILINE) + self.postlift_gcode.SetToolTip(_("Additional gcode to run after raising the Z-axis. Be sure to take " + "into account any additional time needed in the pause value, " + "and be careful what gcode is added!")) self.postlift_gcode.Bind(wx.EVT_TEXT, self.update_postlift_gcode) fieldsizer.Add(self.postlift_gcode, pos = (6, 3), span = (2, 1), flag = wx.EXPAND) @@ -371,32 +390,42 @@ def fit(ev): self.Y.SetToolTip(_("The projector resolution in the Y axis.")) fieldsizer.Add(self.Y, pos = (1, 3)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Offset X (mm):")), pos = (2, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.offset_X = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_offset_x", 0.0), inc = 1, size = (125, -1)) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Offset X (mm):")), pos = (2, 2), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + self.offset_X = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_offset_x", 0.0), + inc = 1, size = (125, -1)) self.offset_X.SetDigits(1) self.offset_X.Bind(wx.EVT_SPINCTRLDOUBLE, self.update_offset) self.offset_X.SetToolTip(_("How far the slice should be offset from the edge in the X axis.")) fieldsizer.Add(self.offset_X, pos = (2, 3)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Offset Y (mm):")), pos = (3, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.offset_Y = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_offset_y", 0.0), inc = 1, size = (125, -1)) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Offset Y (mm):")), pos = (3, 2), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + self.offset_Y = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_offset_y", 0.0), + inc = 1, size = (125, -1)) self.offset_Y.SetDigits(1) self.offset_Y.Bind(wx.EVT_SPINCTRLDOUBLE, self.update_offset) self.offset_Y.SetToolTip(_("How far the slice should be offset from the edge in the Y axis.")) fieldsizer.Add(self.offset_Y, pos = (3, 3)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Projected X (mm):")), pos = (4, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.projected_X_mm = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_projected_x", 505.0), inc = 1, size = (125, -1)) - self.projected_X_mm.SetDigits(1) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Projected X (mm):")), pos = (4, 2), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + self.projected_X_mm = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_projected_x", 100.0), + inc = 0.5, size = (125, -1), min = 1.0, max = 999.9, style = wx.SP_ARROW_KEYS) + self.projected_X_mm.SetDigits(2) self.projected_X_mm.Bind(wx.EVT_SPINCTRLDOUBLE, self.update_projected_Xmm) - self.projected_X_mm.SetToolTip(_("The actual width of the entire projected image. Use the Calibrate grid to show the full size of the projected image,") + - _(" and measure the width at the same level where the slice will be projected onto the resin.")) + self.projected_X_mm.SetToolTip(_("The actual width of the entire projected image. Use the Calibrate " + "grid to show the full size of the projected image, and measure " + "the width at the same level where the slice will be projected onto the resin.")) fieldsizer.Add(self.projected_X_mm, pos = (4, 3)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Z-Axis Speed (mm/min):")), pos = (5, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.z_axis_rate = wx.SpinCtrl(settingsGroup, -1, str(self._get_setting("project_z_axis_rate", 200)), max = 9999, size = (125, -1)) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Z-Axis Speed (mm/min):")), pos = (5, 2), + flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + self.z_axis_rate = wx.SpinCtrl(settingsGroup, -1, str(self._get_setting("project_z_axis_rate", 200)), + max = 9999, size = (125, -1)) self.z_axis_rate.Bind(wx.EVT_SPINCTRL, self.update_z_axis_rate) - self.z_axis_rate.SetToolTip(_("Speed of the Z axis in mm/minute. Take into account that slower rates may require a longer pause value.")) + self.z_axis_rate.SetToolTip(_("Speed of the Z axis in mm/minute. Take into account that " + "slower rates may require a longer pause value.")) fieldsizer.Add(self.z_axis_rate, pos = (5, 3)) fieldboxsizer.Add(fieldsizer) @@ -415,15 +444,18 @@ def fit(ev): self.calibrate = wx.CheckBox(displayGroup, -1, _("Calibrate")) self.calibrate.Bind(wx.EVT_CHECKBOX, self.show_calibrate) self.calibrate.SetToolTip(_("Toggles the calibration grid. Each grid should be 10mmx10mm in size.") + - _(" Use the grid to ensure the projected size is correct. See also the help for the ProjectedX field.")) + _(" Use the grid to ensure the projected size is correct. " + "See also the help for the ProjectedX field.")) displaysizer.Add(self.calibrate, 0, wx.ALIGN_CENTER_VERTICAL) displaysizer.AddStretchSpacer(1) first_layer_boxer = wx.BoxSizer(wx.HORIZONTAL) self.first_layer = wx.CheckBox(displayGroup, -1, _("1st Layer")) self.first_layer.Bind(wx.EVT_CHECKBOX, self.show_first_layer) - self.first_layer.SetToolTip(_("Displays the first layer of the model. Use this to project the first layer for longer so it holds to the base.") + - _(" Note: this value does not affect the first layer when the \"Start\" run is started, it should be used manually.")) + self.first_layer.SetToolTip(_("Displays the first layer of the model. Use this to project " + "the first layer for longer so it holds to the base. Note: " + "this value does not affect the first layer when the 'Start' " + "run is started, it should be used manually.")) first_layer_boxer.Add(self.first_layer, flag = wx.ALIGN_CENTER_VERTICAL) @@ -437,7 +469,8 @@ def fit(ev): self.layer_red = wx.CheckBox(displayGroup, -1, _("Red")) self.layer_red.Bind(wx.EVT_CHECKBOX, self.show_layer_red) - self.layer_red.SetToolTip(_("Toggles whether the image should be red. Useful for positioning whilst resin is in the printer as it should not cause a reaction.")) + self.layer_red.SetToolTip(_("Toggles whether the image should be red. Useful for positioning " + "whilst resin is in the printer as it should not cause a reaction.")) displaysizer.Add(self.layer_red, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, get_space('staticbox')) displayboxsizer.Add(displaysizer, 1, wx.EXPAND) @@ -511,11 +544,14 @@ def fit(ev): self.Show() def __del__(self): - if hasattr(self, 'image_dir') and self.image_dir != '': - shutil.rmtree(self.image_dir) + self.cleanup_temp() if self.display_frame: self.display_frame.Destroy() + def cleanup_temp(self): + if isinstance(self.image_dir, tempfile.TemporaryDirectory): + self.image_dir.cleanup() + def set_total_layers(self, total): self.total_layers.SetLabel(str(total)) self.set_estimated_time() @@ -539,8 +575,8 @@ def set_estimated_time(self): def parse_svg(self, name): et = ET.ElementTree(file = name) - namespaces = dict(node for (_, node) in ET.iterparse(name, events=['start-ns'])) + slicer = 'Slic3r' if 'slic3r' in namespaces.keys() else \ 'Skeinforge' if et.getroot().find('{http://www.w3.org/2000/svg}metadata') else 'None' zlast = 0 @@ -604,62 +640,159 @@ def parse_svg(self, name): def parse_3DLP_zip(self, name): if not zipfile.is_zipfile(name): - self.statusbar.SetLabel(_(f"{os.path.split(name)[1]} is not a zip file.")) - return 0, -1, "None" + self.statusbar.SetLabel(_("{0} is not a zip file.").format(os.path.split(name)[1])) + return -1, -1, "None" + accepted_image_types = ['gif', 'tiff', 'jpg', 'jpeg', 'bmp', 'png'] with zipfile.ZipFile(name, 'r') as zipFile: - self.image_dir = tempfile.mkdtemp() - zipFile.extractall(self.image_dir) + # Make sure to clean up an exisiting temp dir before creating a new one + if isinstance(self.image_dir, tempfile.TemporaryDirectory): + self.image_dir.cleanup() + self.image_dir = tempfile.TemporaryDirectory() + zipFile.extractall(self.image_dir.name) ol = [] # Note: the following funky code extracts any numbers from the filenames, matches # them with the original then sorts them. It allows for filenames of the # format: abc_1.png, which would be followed by abc_10.png alphabetically. - os.chdir(self.image_dir) + os.chdir(self.image_dir.name) vals = [f for f in os.listdir('.') if os.path.isfile(f)] - keys = (int(re.search('\d+', p).group()) for p in vals) + keys = (int(re.search(r'\d+', p).group()) for p in vals) imagefilesDict = dict(zip(keys, vals)) imagefilesOrderedDict = OrderedDict(sorted(imagefilesDict.items(), key = lambda t: t[0])) for f in imagefilesOrderedDict.values(): - path = os.path.join(self.image_dir, f) + path = os.path.join(self.image_dir.name, f) if os.path.isfile(path) and imghdr.what(path) in accepted_image_types: ol.append(path) - return ol, -1, "bitmap" + return ol, -1, 'Bitmap' + + def parse_sl1(self, name): + if not zipfile.is_zipfile(name): + self.statusbar.SetLabel(_("{0} is not a zip file.").format(os.path.split(name)[1])) + return -1, -1, 'None' + + accepted_image_types = ('gif', 'tiff', 'jpg', 'jpeg', 'bmp', 'png') + + with zipfile.ZipFile(name, 'r') as zippy: + settings = self.load_sl1_config(zippy) + # Make sure to clean up an exisiting temp dir before creating a new one + if isinstance(self.image_dir, tempfile.TemporaryDirectory): + self.image_dir.cleanup() + self.image_dir = tempfile.TemporaryDirectory() + for f in zippy.namelist(): + if f.lower().endswith(accepted_image_types) and 'thumbnail' not in f: + zippy.extract(f, self.image_dir.name) + ol = [] + for f in sorted(os.listdir(self.image_dir.name)): + path = os.path.join(self.image_dir.name, f) + if os.path.isfile(path) and imghdr.what(path) in accepted_image_types: + ol.append(path) + + return ol, -1, 'PrusaSlicer', settings + + def load_sl1_config(self, zip_object: zipfile.ZipFile): + + files = zip_object.namelist() + settings = {} + if 'prusaslicer.ini' in files: + relevant_keys = ['display_height', 'display_width', 'display_orientation', + 'display_pixels_x', 'display_pixels_y', + 'display_mirror_x', 'display_mirror_y', + 'exposure_time', 'initial_exposure_time', + 'layer_height', 'printer_model', 'printer_technology', + 'material_colour'] + with zip_object.open('prusaslicer.ini', 'r') as lines: + for line in lines: + element = line.decode('UTF-8').rstrip().split(' = ') + if element[0] in relevant_keys: + settings[element[0]] = element[1] + return settings + + if 'config.ini' in files: + relevant_keys = ['expTime', 'expTimeFirst', 'layerHeight', 'printerModel'] + key_names = ['exposure_time', 'initial_exposure_time', 'layer_height', 'printer_model'] + with zip_object.open('config.ini', 'r') as lines: + for line in lines: + element = line.decode('UTF-8').rstrip().split(' = ') + if element[0] in relevant_keys: + index = relevant_keys.index(element[0]) + settings[key_names[index]] = element[1] + return settings + + def apply_sl1_settings(self, layers: list): + thickness = layers[3].get('layer_height') + if thickness is not None: + self.thickness.SetValue(thickness) + self.update_thickness(wx.wxEVT_NULL) + else: + self.statusbar.SetLabel(_("Could not load .sl1 config.")) + return False + interval = layers[3].get('exposure_time') + if interval is not None: + self.interval.SetValue(interval) + self.update_interval(wx.wxEVT_NULL) + init_exp = layers[3].get('initial_exposure_time') + if init_exp is not None: + self.show_first_layer_timer.SetValue(init_exp) + x_res = layers[3].get('display_pixels_x') + if x_res is not None: + self.X.SetValue(x_res) + self.update_resolution(wx.wxEVT_NULL) + y_res = layers[3].get('display_pixels_y') + if y_res is not None: + self.Y.SetValue(y_res) + self.update_resolution(wx.wxEVT_NULL) + real_width = layers[3].get('display_width') + real_height = layers[3].get('display_height') + if real_width and real_height is not None: + if float(real_width) > float(real_height): + self.projected_X_mm.SetValue(real_width.replace('.', ',')) + else: + self.projected_X_mm.SetValue(real_height.replace('.', ',')) + self.update_projected_Xmm(wx.wxEVT_NULL) + return True def load_file(self, event): + self.reset_loaded_file() dlg = wx.FileDialog(self, _("Open file to print"), style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) # On macOS, the wildcard for *.3dlp.zip is not recognised, so it is just *.zip. - load_wildcard = _("Slic3r or Skeinforge SVG files") + " (*.svg)|*.svg|" + _("3DLP Zip files") + " (*.3dlp.zip)|*.zip" - dlg.SetWildcard(load_wildcard) + dlg.SetWildcard(_("Slic3r or Skeinforge SVG files") + " (*.svg)|*.svg|" + + _("3DLP Zip files") + " (*.3dlp.zip)|*.zip|" + + _("Prusa SL1 files") + " (*.sl1;*.sl1s)|*.sl1;*.sl1s") if dlg.ShowModal() == wx.ID_OK: name = dlg.GetPath() if not os.path.exists(name): - self.status.SetStatusText(_("File not found!")) + self.statusbar.SetLabel(_("File not found!")) return - if name.endswith(('.svg', '.SVG')): + + if name.lower().endswith('.svg'): layers = self.parse_svg(name) - if layers[2] == 'None': - self.statusbar.SetLabel(_(f"{os.path.split(name)[1]} is not a sliced svg-file.")) - self.display_filename("") - self.set_total_layers("") - self.set_current_layer(0) - self.estimated_time.SetLabel("") - return + elif name.lower().endswith(('.sl1', '.sl1s')): + layers = self.parse_sl1(name) + elif name.lower().endswith('.3dlp.zip'): + layers = self.parse_3DLP_zip(name) + else: + self.statusbar.SetLabel(_("{0} is not a sliced svg-file or zip-file.").format(os.path.split(name)[1])) + return + + if layers[2] in ('Slic3r', 'Skeinforge'): layerHeight = round(layers[1], 3) self.thickness.SetValue(str(layerHeight)) - self.statusbar.SetLabel(_("Layer thickness detected: {0} mm").format(layerHeight)) - else: - layers = self.parse_3DLP_zip(name) - if layers[2] == 'None': - self.display_filename("") - self.set_total_layers("") - self.set_current_layer(0) - self.estimated_time.SetLabel("") - return + elif layers[2] == 'PrusaSlicer': + if self.apply_sl1_settings(layers): + layerHeight = float(layers[3]['layer_height']) + else: + layerHeight = float(self.thickness.GetValue()) + elif layers[2] == 'Bitmap': layerHeight = float(self.thickness.GetValue()) - self.statusbar.SetLabel(_("{0} layers found, total height {1:.2f} mm").format(len(layers[0]), layerHeight * len(layers[0]))) + else: + self.statusbar.SetLabel(_(f"{os.path.split(name)[1]} is not a sliced svg-file or zip-file.")) + return + + self.statusbar.SetLabel(_("{0} layers found, total height {1:.2f} mm").format(len(layers[0]), + layerHeight * len(layers[0]))) self.layers = layers self.set_total_layers(len(layers[0])) self.set_current_layer(0) @@ -670,6 +803,14 @@ def load_file(self, event): self.present_button.Enable() dlg.Destroy() + def reset_loaded_file(self): + if hasattr(self, 'layers'): + delattr(self, 'layers') + self.display_filename("") + self.set_total_layers("") + self.set_current_layer(0) + self.estimated_time.SetLabel("") + def show_calibrate(self, event): if self.calibrate.IsChecked(): self.present_calibrate(event) @@ -729,7 +870,7 @@ def present_calibrate(self, event): dc.DrawLine(int(x * (pixelsXPerMM * 10)), 0, int(x * (pixelsXPerMM * 10)), resolution_y_pixels) self.first_layer.SetValue(False) - self.display_frame.slicer = 'bitmap' + self.display_frame.slicer = 'Bitmap' self.display_frame.draw_layer(gridBitmap.ConvertToImage()) self.Raise() @@ -911,6 +1052,7 @@ def start_present(self, event): offset = (float(self.offset_X.GetValue()), float(self.offset_Y.GetValue())), layer_red = self.layer_red.IsChecked()) self.present_button.Disable() + self.load_button.Disable() self.Raise() def stop_present(self, event): @@ -919,6 +1061,7 @@ def stop_present(self, event): self.pause_button.SetLabel(self.get_btn_label('pause')) self.set_current_layer(0) self.present_button.Enable() + self.load_button.Enable() self.pause_button.Disable() self.stop_button.Disable() self.statusbar.SetLabel(_("Stop")) @@ -936,6 +1079,7 @@ def pause_present(self, event): def on_close(self, event): self.stop_present(event) + self.cleanup_temp() if self.display_frame: self.display_frame.Destroy() self.Destroy() @@ -957,7 +1101,7 @@ def reset_all(self, event): message = _("Are you sure you want to reset all the settings " "to the defaults?\nBe aware that the defaults are " "not guaranteed to work well with your machine."), - caption = _("Reset ProjectLayer Settings"), + caption = _("Reset Layer Projector Settings"), style = wx.YES_NO | wx.ICON_EXCLAMATION) if reset_dialog.ShowModal() == wx.ID_YES: @@ -981,7 +1125,7 @@ def reset_all(self, event): [self.fullscreen, False, self.update_fullscreen], [self.calibrate, False, self.show_calibrate], [self.first_layer, False, self.show_first_layer], - [self.show_first_layer_timer, -1.0, self.show_first_layer_timer], + [self.show_first_layer_timer, -1.0, self.show_first_layer], [self.layer_red, False, self.show_layer_red] ] @@ -994,11 +1138,8 @@ def reset_all(self, event): self.direction.SetSelection(0) self.update_direction(event) - self.filename.SetLabel("") - self.total_layers.SetLabel("") - self.current_layer.SetLabel("0") - self.estimated_time.SetLabel("") - self.statusbar.SetLabel(_("ProjectLayer Settings reset")) + self.reset_loaded_file() + self.statusbar.SetLabel(_("Layer Projector settings reset")) def reset_setting(self, event, name, value, update_function): # First check if the user actually changed the setting From 610dc0481a79fa2f06c740e46186bd8c9801b5ba Mon Sep 17 00:00:00 2001 From: neofelis2X Date: Sun, 11 Jun 2023 16:35:46 +0200 Subject: [PATCH 3/6] Projectlayer: replace cairosvg with wx.svg --- printrun/projectlayer.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/printrun/projectlayer.py b/printrun/projectlayer.py index f8c4ae69e..370127dca 100644 --- a/printrun/projectlayer.py +++ b/printrun/projectlayer.py @@ -15,12 +15,11 @@ import xml.etree.ElementTree as ET import wx +import wx.svg import os import time import zipfile import tempfile -from cairosvg.surface import PNGSurface -import io import imghdr import copy import re @@ -89,28 +88,34 @@ def draw_layer(self, image): if self.scale != 1.0: image = copy.deepcopy(image) - height = float(image.get('height').replace('m', '')) width = float(image.get('width').replace('m', '')) + height = float(image.get('height').replace('m', '')) + + new_width = str(round(width * self.scale, 3)) + new_height = str(round(height * self.scale, 3)) - image.set('height', str(height * self.scale) + 'mm') - image.set('width', str(width * self.scale) + 'mm') - image.set('viewBox', '0 0 ' + str(width * self.scale) + ' ' + str(height * self.scale)) + image.set('width', new_width + 'mm') + image.set('height', new_height + 'mm') + image.set('viewBox', '0 0 ' + new_width + ' ' + new_height) g = image.find("{http://www.w3.org/2000/svg}g") g.set('transform', 'scale(' + str(self.scale) + ')') - pngbytes = PNGSurface.convert(dpi = self.dpi, bytestring = ET.tostring(image)) - pngImage = wx.Image(io.BytesIO(pngbytes)) + if self.layer_red: + image.set('style', 'background-color: black; fill: red;') + for element in image.findall('{http://www.w3.org/2000/svg}g')[0]: + if element.get('fill') == 'white': + element.set('fill', 'red') + if element.get('style') == 'fill: white': + element.set('style', 'fill: red') - self.Parent.statusbar.SetLabel(f"Width: {pngImage.Width}, dpi: {self.dpi:.2f} -> " - f"Width: {((pngImage.Width / self.dpi) * 25.4):.3f} mm") + svg_image = wx.svg.SVGimage.CreateFromBytes(ET.tostring(image), units = 'px', dpi = self.dpi) - if self.layer_red: - pngImage = pngImage.AdjustChannels(1, 0, 0, 1) + self.Parent.statusbar.SetLabel(f"Width: {round(svg_image.width, 2)}, dpi: {self.dpi:.2f} -> " + f"Width: {((round(svg_image.width, 2) / self.dpi) * 25.4):.3f} mm") - # AGE2022-07-31 Python 3.10 and DrawBitmap expects offset - # as integer value. Convert float values to int - dc.DrawBitmap(wx.Bitmap(pngImage), int(self.offset[0]), int(self.offset[1]), True) + ctx = wx.GraphicsContext.Create(dc) + svg_image.RenderToGC(ctx) elif self.slicer in ('Bitmap', 'PrusaSlicer'): if isinstance(image, str): @@ -583,6 +588,7 @@ def parse_svg(self, name): zdiff = 0 ol = [] if slicer == 'Slic3r': + ET.register_namespace("", "http://www.w3.org/2000/svg") height = et.getroot().get('height').replace('m', '') width = et.getroot().get('width').replace('m', '') From db113a985ee851795f36f157e0fe288b44e53886 Mon Sep 17 00:00:00 2001 From: neofelis2X Date: Fri, 25 Aug 2023 19:18:26 +0200 Subject: [PATCH 4/6] Closing DisplayFrame also closes SettingsFrame --- printrun/projectlayer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/printrun/projectlayer.py b/printrun/projectlayer.py index 370127dca..df440d521 100644 --- a/printrun/projectlayer.py +++ b/printrun/projectlayer.py @@ -53,6 +53,9 @@ def __init__(self, parent, title, res = (1024, 768), printer = None, scale = 1.0 self.CentreOnParent() self.Show() + # Closing the DisplayFrame calls the close method of Settingsframe + self.Bind(wx.EVT_CLOSE, parent.on_close) + self.scale = scale self.index = 0 self.size = res From 0f3bcd42f9ecda22fa7abf190129152c57503c97 Mon Sep 17 00:00:00 2001 From: neofelis2X Date: Mon, 30 Oct 2023 20:38:02 +0100 Subject: [PATCH 5/6] Projectlayer: Various fixes of drawing and scaling --- printrun/projectlayer.py | 425 +++++++++++++++++++++------------------ 1 file changed, 226 insertions(+), 199 deletions(-) diff --git a/printrun/projectlayer.py b/printrun/projectlayer.py index df440d521..4b02eef0c 100644 --- a/printrun/projectlayer.py +++ b/printrun/projectlayer.py @@ -32,8 +32,8 @@ install_locale('pronterface') class DisplayFrame(wx.Frame): - def __init__(self, parent, title, res = (1024, 768), printer = None, scale = 1.0, offset = (0, 0)): - wx.Frame.__init__(self, parent = parent, title = title, size = res) + def __init__(self, parent, statusbar, title, res = (1024, 768), printer = None, scale = 1.0, offset = (0, 0)): + super().__init__(parent = parent, title = title, size = res) self.printer = printer self.control_frame = parent self.pic = wx.StaticBitmap(self) @@ -41,6 +41,8 @@ def __init__(self, parent, title, res = (1024, 768), printer = None, scale = 1.0 self.bbitmap = wx.Bitmap(*res) self.slicer = 'Bitmap' self.dpi = 96 + self.status = statusbar + dc = wx.MemoryDC() dc.SelectObject(self.bbitmap) dc.SetBackground(wx.Brush("black")) @@ -54,7 +56,7 @@ def __init__(self, parent, title, res = (1024, 768), printer = None, scale = 1.0 self.Show() # Closing the DisplayFrame calls the close method of Settingsframe - self.Bind(wx.EVT_CLOSE, parent.on_close) + self.Bind(wx.EVT_CLOSE, self.control_frame.on_close) self.scale = scale self.index = 0 @@ -81,29 +83,20 @@ def resize(self, res = (1024, 768)): dc.Clear() dc.SelectObject(wx.NullBitmap) - def draw_layer(self, image): + def convert_mm_to_px(self, mm_value) -> float: + resolution_x_px = self.control_frame.X.GetValue() + projected_x_mm = self.control_frame.projected_X_mm.GetValue() + return resolution_x_px / projected_x_mm * mm_value + + def draw_layer(self, image = None): dc = wx.MemoryDC() dc.SelectObject(self.bitmap) dc.SetBackground(wx.Brush("black")) dc.Clear() + gc = wx.GraphicsContext.Create(dc) if self.slicer in ['Slic3r', 'Skeinforge']: - if self.scale != 1.0: - image = copy.deepcopy(image) - width = float(image.get('width').replace('m', '')) - height = float(image.get('height').replace('m', '')) - - new_width = str(round(width * self.scale, 3)) - new_height = str(round(height * self.scale, 3)) - - image.set('width', new_width + 'mm') - image.set('height', new_height + 'mm') - image.set('viewBox', '0 0 ' + new_width + ' ' + new_height) - - g = image.find("{http://www.w3.org/2000/svg}g") - g.set('transform', 'scale(' + str(self.scale) + ')') - if self.layer_red: image.set('style', 'background-color: black; fill: red;') for element in image.findall('{http://www.w3.org/2000/svg}g')[0]: @@ -114,11 +107,11 @@ def draw_layer(self, image): svg_image = wx.svg.SVGimage.CreateFromBytes(ET.tostring(image), units = 'px', dpi = self.dpi) - self.Parent.statusbar.SetLabel(f"Width: {round(svg_image.width, 2)}, dpi: {self.dpi:.2f} -> " - f"Width: {((round(svg_image.width, 2) / self.dpi) * 25.4):.3f} mm") + self.status(f"Scaled width: {svg_image.width / self.dpi * 25.4 * self.scale:.2f} mm @ {round(svg_image.width * self.scale)} px") - ctx = wx.GraphicsContext.Create(dc) - svg_image.RenderToGC(ctx) + gc.Translate(self.convert_mm_to_px(self.offset[0]), self.convert_mm_to_px(self.offset[1])) + gc.Scale(self.scale, self.scale) + svg_image.RenderToGC(gc) elif self.slicer in ('Bitmap', 'PrusaSlicer'): if isinstance(image, str): @@ -127,18 +120,75 @@ def draw_layer(self, image): image = image.AdjustChannels(1, 0, 0, 1) # AGE2023-04-19 Python 3.10 and DrawBitmap expects offset # as integer value. Convert float values to int - bitmap = wx.Bitmap(image.Scale(int(image.Width * self.scale), int(image.Height * self.scale))) - dc.DrawBitmap(bitmap, int(self.offset[0]), int(-self.offset[1]), True) + width, height = image.GetSize() + if width < height: + image = image.Rotate90(clockwise = False) + real_width = max(width, height) + bitmap = gc.CreateBitmapFromImage(image) + + self.status(f"Scaled width: {real_width / self.dpi * 25.4 * self.scale:.2f} mm @ {round(real_width * self.scale)} px") + + gc.Translate(self.convert_mm_to_px(self.offset[0]), self.convert_mm_to_px(self.offset[1])) + gc.Scale(self.scale, self.scale) + gc.DrawBitmap(bitmap, 0, 0, image.Width, image.Height) + + elif 'Calibrate' in self.slicer: + #gc.Translate(self.convert_mm_to_px(self.offset[0]), self.convert_mm_to_px(self.offset[1])) + #gc.Scale(self.scale, self.scale) + self.draw_grid(gc) + else: - self.Parent.statusbar.SetLabel(_("No valid file loaded.")) + self.status(_("No valid file loaded.")) return self.pic.SetBitmap(self.bitmap) self.pic.Show() self.Refresh() + def draw_grid(self, graphics_context): + gc = graphics_context + + x_res_px = self.control_frame.X.GetValue() + y_res_px = self.control_frame.Y.GetValue() + + # Draw outline + path = gc.CreatePath() + path.AddRectangle(0, 0, x_res_px, y_res_px) + path.AddCircle(0, 0, 5.0) + path.AddCircle(0, 0, 14.0) + + solid_pen = gc.CreatePen(wx.GraphicsPenInfo(wx.RED).Width(5.0).Style(wx.PENSTYLE_SOLID)) + gc.SetPen(solid_pen) + gc.StrokePath(path) + + # Calculate gridlines + aspectRatio = x_res_px / y_res_px + + projected_x_mm = self.control_frame.projected_X_mm.GetValue() + projected_y_mm = round(projected_x_mm / aspectRatio, 2) + + px_per_mm = x_res_px / projected_x_mm + + grid_count_x = int(projected_x_mm / 10) + grid_count_y = int(projected_y_mm / 10) + + # Draw gridlines + path = gc.CreatePath() + for y in range(1, grid_count_y + 1): + for x in range(1, grid_count_x + 1): + # horizontal line + path.MoveToPoint(0, int(y * (px_per_mm * 10))) + path.AddLineToPoint(x_res_px, int(y * (px_per_mm * 10))) + # vertical line + path.MoveToPoint(int(x * (px_per_mm * 10)), 0) + path.AddLineToPoint(int(x * (px_per_mm * 10)), y_res_px) + + thin_pen = gc.CreatePen(wx.GraphicsPenInfo(wx.RED).Width(2.0).Style(wx.PENSTYLE_DOT)) + gc.SetPen(thin_pen) + gc.StrokePath(path) + def show_img_delay(self, image): - self.Parent.statusbar.SetLabel(_("Showing, Timestamp %s s") % str(time.perf_counter())) + self.status(_("Showing, Timestamp {:.3f} s").format(time.perf_counter())) self.control_frame.set_current_layer(self.index) self.draw_layer(image) # AGe 2022-07-31 Python 3.10 and CallLater expects delay in milliseconds as @@ -147,9 +197,9 @@ def show_img_delay(self, image): def rise(self): if self.direction == 0: # 0: Top Down - self.Parent.statusbar.SetLabel(_("Lowering, Timestamp %s s") % str(time.perf_counter())) + self.status(_("Lowering, Timestamp {:.3f} s").format(time.perf_counter())) else: # self.direction == 1, 1: Bottom Up - self.Parent.statusbar.SetLabel(_("Rising, Timestamp %s s") % str(time.perf_counter())) + self.status(_("Rising, Timestamp {:.3f} s").format(time.perf_counter())) if self.printer is not None and self.printer.online: self.printer.send_now("G91") @@ -180,7 +230,7 @@ def rise(self): wx.CallLater(int(1000 * self.pause), self.next_img) def hide_pic(self): - self.Parent.statusbar.SetLabel(_("Hiding, Timestamp %s s") % str(time.perf_counter())) + self.status(_("Hiding, Timestamp {:.3f} s").format(time.perf_counter())) self.pic.Hide() def hide_pic_and_rise(self): @@ -191,12 +241,12 @@ def next_img(self): if not self.running: return if self.index < len(self.layers): - self.Parent.statusbar.SetLabel(str(self.index)) + self.status(str(self.index)) wx.CallAfter(self.show_img_delay, self.layers[self.index]) self.index += 1 else: - self.Parent.stop_present(wx.wxEVT_NULL) - self.Parent.statusbar.SetLabel(_("End")) + self.control_frame.stop_present(wx.wxEVT_NULL) + self.status(_("End")) wx.CallAfter(self.pic.Hide) wx.CallAfter(self.Refresh) @@ -250,13 +300,12 @@ def _get_setting(self, name, val): return val def __init__(self, parent, printer = None): - wx.Dialog.__init__(self, parent, title = _("Layer Projector Control"), - style = wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT) + super().__init__(parent, title = _("Layer Projector Control"), + style = wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT) self.pronterface = parent self.image_dir = '' - self.display_frame = DisplayFrame(self, title = _("Layer Projector Display"), printer = printer) - - self.panel = wx.Panel(self) + self.current_filename = '' + self.slicer = '' # In wxPython 4.1.0 gtk3 (phoenix) wxWidgets 3.1.4 # Layout() breaks before Show(), invoke once after Show() @@ -265,13 +314,15 @@ def fit(ev): self.Unbind(wx.EVT_ACTIVATE, handler=fit) self.Bind(wx.EVT_ACTIVATE, fit) + self.panel = wx.Panel(self) + buttonGroup = wx.StaticBox(self.panel, label = _("Controls")) buttonbox = wx.StaticBoxSizer(buttonGroup, wx.HORIZONTAL) self.load_button = wx.Button(buttonGroup, -1, _("Load")) self.load_button.Bind(wx.EVT_BUTTON, self.load_file) self.load_button.SetToolTip(_("Choose a SVG file created from Slic3r or Skeinforge, a PrusaSlicer SL1-file " - "or a zip file of bitmap images (Extension: .3dlp.zip).")) + "or a zip file of bitmap images (.3dlp.zip).")) buttonbox.Add(self.load_button, 1, flag = wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border = get_space('mini')) @@ -302,25 +353,31 @@ def fit(ev): fieldsizer = wx.GridBagSizer(vgap = get_space('minor'), hgap = get_space('minor')) # Left Column - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Layer (mm):")), pos = (0, 0), + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Layerheight (mm):")), pos = (0, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.thickness = wx.TextCtrl(settingsGroup, -1, str(self._get_setting("project_layer", "0.1")), size = (125, -1)) - self.thickness.Bind(wx.EVT_TEXT, self.update_thickness) + self.thickness = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_layer", 0.1), + inc = 0.01, size = (125, -1)) + self.thickness.SetDigits(3) + self.thickness.Bind(wx.EVT_SPINCTRLDOUBLE, self.update_thickness) self.thickness.SetToolTip(_("The thickness of each slice. Should match the value used to slice the model. " "SVG files update this value automatically, 3dlp.zip files have to be manually entered.")) fieldsizer.Add(self.thickness, pos = (0, 1)) fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Exposure (s):")), pos = (1, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.interval = wx.TextCtrl(settingsGroup, -1, str(self._get_setting("project_interval", "0.5")), size = (125, -1)) - self.interval.Bind(wx.EVT_TEXT, self.update_interval) + self.interval = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_interval", 0.5), + inc = 0.1, size = (125, -1)) + self.interval.SetDigits(2) + self.interval.Bind(wx.EVT_SPINCTRLDOUBLE, self.update_interval) self.interval.SetToolTip(_("How long each slice should be displayed.")) fieldsizer.Add(self.interval, pos = (1, 1)) fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Blank (s):")), pos = (2, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - self.pause = wx.TextCtrl(settingsGroup, -1, str(self._get_setting("project_pause", "0.5")), size = (125, -1)) - self.pause.Bind(wx.EVT_TEXT, self.update_pause) + self.pause = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_pause", 0.5), + inc = 0.1, size = (125, -1)) + self.pause.SetDigits(2) + self.pause.Bind(wx.EVT_SPINCTRLDOUBLE, self.update_pause) self.pause.SetToolTip(_("The pause length between slices. This should take into account any movement of the Z axis, " "plus time to prepare the resin surface (sliding, tilting, sweeping, etc).")) fieldsizer.Add(self.pause, pos = (2, 1)) @@ -328,7 +385,7 @@ def fit(ev): fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Scale:")), pos = (3, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) self.scale = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting('project_scale', 1.0), - inc = 0.1, size = (125, -1)) + inc = 0.1, min = 0.05, size = (125, -1)) self.scale.SetDigits(3) self.scale.Bind(wx.EVT_SPINCTRLDOUBLE, self.update_scale) self.scale.SetToolTip(_("The additional scaling of each slice.")) @@ -337,15 +394,14 @@ def fit(ev): fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Direction:")), pos = (4, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) self.direction = wx.Choice(settingsGroup, -1, choices = [_('Top Down'), _('Bottom Up')], size = (125, -1)) + saved_direction = self._get_setting('project_direction', 0) try: # This setting used to be a string, older values need to be replaced with an index int(saved_direction) except ValueError: - if saved_direction == "Bottom Up": - saved_direction = 1 - else: - saved_direction = 0 + saved_direction = 1 if saved_direction == "Bottom Up" else 0 self._set_setting('project_direction', saved_direction) + self.direction.SetSelection(int(saved_direction)) self.direction.Bind(wx.EVT_CHOICE, self.update_direction) self.direction.SetToolTip(_("The direction the Z axis should move. Top Down is where the projector is above " @@ -384,21 +440,21 @@ def fit(ev): fieldsizer.Add(self.postlift_gcode, pos = (6, 3), span = (2, 1), flag = wx.EXPAND) # Right Column - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("X (px):")), pos = (0, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("X Resolution (px):")), pos = (0, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) projectX = int(math.floor(float(self._get_setting("project_x", 1920)))) self.X = wx.SpinCtrl(settingsGroup, -1, str(projectX), max = 999999, size = (125, -1)) self.X.Bind(wx.EVT_SPINCTRL, self.update_resolution) self.X.SetToolTip(_("The projector resolution in the X axis.")) fieldsizer.Add(self.X, pos = (0, 3)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Y (px):")), pos = (1, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Y Resolution (px):")), pos = (1, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) projectY = int(math.floor(float(self._get_setting("project_y", 1200)))) self.Y = wx.SpinCtrl(settingsGroup, -1, str(projectY), max = 999999, size = (125, -1)) self.Y.Bind(wx.EVT_SPINCTRL, self.update_resolution) self.Y.SetToolTip(_("The projector resolution in the Y axis.")) fieldsizer.Add(self.Y, pos = (1, 3)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Offset X (mm):")), pos = (2, 2), + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Offset in X (mm):")), pos = (2, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) self.offset_X = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_offset_x", 0.0), inc = 1, size = (125, -1)) @@ -407,7 +463,7 @@ def fit(ev): self.offset_X.SetToolTip(_("How far the slice should be offset from the edge in the X axis.")) fieldsizer.Add(self.offset_X, pos = (2, 3)) - fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Offset Y (mm):")), pos = (3, 2), + fieldsizer.Add(wx.StaticText(settingsGroup, -1, _("Offset in Y (mm):")), pos = (3, 2), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) self.offset_Y = wx.SpinCtrlDouble(settingsGroup, -1, initial = self._get_setting("project_offset_y", 0.0), inc = 1, size = (125, -1)) @@ -549,6 +605,14 @@ def fit(ev): self.SetSizerAndFit(topsizer) self.Fit() self.CentreOnParent() + + # res = (self.X.GetValue(), self.Y.GetValue()) + self.display_frame = DisplayFrame(self, statusbar = self.status, + title = _("Layer Projector Display"), + res = (1024, 768), + printer = printer) + self.display_frame.Centre() + self.Raise() self.Show() def __del__(self): @@ -556,6 +620,9 @@ def __del__(self): if self.display_frame: self.display_frame.Destroy() + def status(self, message: str) -> None: + return self.statusbar.SetLabel(message) + def cleanup_temp(self): if isinstance(self.image_dir, tempfile.TemporaryDirectory): self.image_dir.cleanup() @@ -578,7 +645,7 @@ def set_estimated_time(self): current_layer = int(self.current_layer.GetLabel()) remaining_layers = len(self.layers[0]) - current_layer # 0.5 for delay between hide and rise - estimated_time = remaining_layers * (float(self.interval.GetValue()) + float(self.pause.GetValue()) + 0.5) + estimated_time = remaining_layers * (self.interval.GetValue() + self.pause.GetValue() + 0.5) self.estimated_time.SetLabel(time.strftime("%H:%M:%S", time.gmtime(estimated_time))) def parse_svg(self, name): @@ -595,6 +662,9 @@ def parse_svg(self, name): height = et.getroot().get('height').replace('m', '') width = et.getroot().get('width').replace('m', '') + self.projected_X_mm.SetValue(float(width)) + self.update_projected_Xmm(wx.wxEVT_NULL) + for i in et.findall("{http://www.w3.org/2000/svg}g"): z = float(i.get('{http://slic3r.org/namespaces/slic3r}z')) zdiff = z - zlast @@ -620,6 +690,9 @@ def parse_svg(self, name): height = str(abs(float(minY)) + abs(float(maxY))) width = str(abs(float(minX)) + abs(float(maxX))) + self.projected_X_mm.SetValue(float(width)) + self.update_projected_Xmm(wx.wxEVT_NULL) + for g in et.findall("{http://www.w3.org/2000/svg}g")[0].findall("{http://www.w3.org/2000/svg}g"): g.set('transform', '') @@ -649,7 +722,7 @@ def parse_svg(self, name): def parse_3DLP_zip(self, name): if not zipfile.is_zipfile(name): - self.statusbar.SetLabel(_("{0} is not a zip file.").format(os.path.split(name)[1])) + self.status(_("{} is not a zip file.").format(os.path.split(name)[1])) return -1, -1, "None" accepted_image_types = ['gif', 'tiff', 'jpg', 'jpeg', 'bmp', 'png'] @@ -679,7 +752,7 @@ def parse_3DLP_zip(self, name): def parse_sl1(self, name): if not zipfile.is_zipfile(name): - self.statusbar.SetLabel(_("{0} is not a zip file.").format(os.path.split(name)[1])) + self.status(_("{} is not a zip file.").format(os.path.split(name)[1])) return -1, -1, 'None' accepted_image_types = ('gif', 'tiff', 'jpg', 'jpeg', 'bmp', 'png') @@ -716,6 +789,7 @@ def load_sl1_config(self, zip_object: zipfile.ZipFile): for line in lines: element = line.decode('UTF-8').rstrip().split(' = ') if element[0] in relevant_keys: + element[1] = self.cast_type(element[1]) settings[element[0]] = element[1] return settings @@ -727,16 +801,26 @@ def load_sl1_config(self, zip_object: zipfile.ZipFile): element = line.decode('UTF-8').rstrip().split(' = ') if element[0] in relevant_keys: index = relevant_keys.index(element[0]) + element[1] = self.cast_type(element[1]) settings[key_names[index]] = element[1] return settings + def cast_type(self, var): + '''Automaticly cast int or float from str''' + for caster in (int, float): + try: + return caster(var) + except ValueError: + pass + return var + def apply_sl1_settings(self, layers: list): thickness = layers[3].get('layer_height') if thickness is not None: self.thickness.SetValue(thickness) self.update_thickness(wx.wxEVT_NULL) else: - self.statusbar.SetLabel(_("Could not load .sl1 config.")) + self.status(_("Could not load .sl1 config.")) return False interval = layers[3].get('exposure_time') if interval is not None: @@ -754,12 +838,8 @@ def apply_sl1_settings(self, layers: list): self.Y.SetValue(y_res) self.update_resolution(wx.wxEVT_NULL) real_width = layers[3].get('display_width') - real_height = layers[3].get('display_height') - if real_width and real_height is not None: - if float(real_width) > float(real_height): - self.projected_X_mm.SetValue(real_width.replace('.', ',')) - else: - self.projected_X_mm.SetValue(real_height.replace('.', ',')) + if real_width: + self.projected_X_mm.SetValue(real_width) self.update_projected_Xmm(wx.wxEVT_NULL) return True @@ -773,7 +853,7 @@ def load_file(self, event): if dlg.ShowModal() == wx.ID_OK: name = dlg.GetPath() if not os.path.exists(name): - self.statusbar.SetLabel(_("File not found!")) + self.status(_("File not found!")) return if name.lower().endswith('.svg'): @@ -783,25 +863,25 @@ def load_file(self, event): elif name.lower().endswith('.3dlp.zip'): layers = self.parse_3DLP_zip(name) else: - self.statusbar.SetLabel(_("{0} is not a sliced svg-file or zip-file.").format(os.path.split(name)[1])) + self.status(_("{} is not a sliced svg-file or zip-file.").format(os.path.split(name)[1])) return if layers[2] in ('Slic3r', 'Skeinforge'): - layerHeight = round(layers[1], 3) - self.thickness.SetValue(str(layerHeight)) + layer_height = round(layers[1], 3) + self.thickness.SetValue(layer_height) elif layers[2] == 'PrusaSlicer': if self.apply_sl1_settings(layers): - layerHeight = float(layers[3]['layer_height']) + layer_height = float(layers[3]['layer_height']) else: - layerHeight = float(self.thickness.GetValue()) + layer_height = self.thickness.GetValue() elif layers[2] == 'Bitmap': - layerHeight = float(self.thickness.GetValue()) + layer_height = self.thickness.GetValue() else: - self.statusbar.SetLabel(_(f"{os.path.split(name)[1]} is not a sliced svg-file or zip-file.")) + self.status(_("{} is not a sliced svg-file or zip-file.").format(os.path.split(name)[1])) return - self.statusbar.SetLabel(_("{0} layers found, total height {1:.2f} mm").format(len(layers[0]), - layerHeight * len(layers[0]))) + self.status(_("{} layers found, total height {:.2f} mm").format(len(layers[0]), + layer_height * len(layers[0]))) self.layers = layers self.set_total_layers(len(layers[0])) self.set_current_layer(0) @@ -810,8 +890,12 @@ def load_file(self, event): self.slicer = layers[2] self.display_frame.slicer = self.slicer self.present_button.Enable() + dlg.Destroy() + self.display_frame.Raise() + self.Raise() + def reset_loaded_file(self): if hasattr(self, 'layers'): delattr(self, 'layers') @@ -826,7 +910,7 @@ def show_calibrate(self, event): else: if hasattr(self, 'layers'): self.display_frame.slicer = self.layers[2] - self.display_frame.scale = float(self.scale.GetValue()) + self.display_frame.scale = self.scale.GetValue() self.display_frame.clear_layer() def show_first_layer(self, event): @@ -835,7 +919,7 @@ def show_first_layer(self, event): else: if hasattr(self, 'layers'): self.display_frame.slicer = self.layers[2] - self.display_frame.scale = float(self.scale.GetValue()) + self.display_frame.scale = self.scale.GetValue() self.display_frame.clear_layer() def show_layer_red(self, event): @@ -843,72 +927,44 @@ def show_layer_red(self, event): def present_calibrate(self, event): if self.calibrate.IsChecked(): - self.display_frame.Raise() - self.display_frame.offset = (float(self.offset_X.GetValue()), -float(self.offset_Y.GetValue())) - self.display_frame.scale = 1.0 - resolution_x_pixels = int(self.X.GetValue()) - resolution_y_pixels = int(self.Y.GetValue()) - - gridBitmap = wx.Bitmap(resolution_x_pixels, resolution_y_pixels) - dc = wx.MemoryDC() - dc.SelectObject(gridBitmap) - dc.SetBackground(wx.Brush("black")) - dc.Clear() - - dc.SetPen(wx.Pen("red", 7)) - dc.DrawLine(0, 0, resolution_x_pixels, 0) - dc.DrawLine(0, 0, 0, resolution_y_pixels) - dc.DrawLine(resolution_x_pixels, 0, resolution_x_pixels, resolution_y_pixels) - dc.DrawLine(0, resolution_y_pixels, resolution_x_pixels, resolution_y_pixels) - - dc.SetPen(wx.Pen("red", 2)) - aspectRatio = float(resolution_x_pixels) / float(resolution_y_pixels) - - projectedXmm = float(self.projected_X_mm.GetValue()) - projectedYmm = round(projectedXmm / aspectRatio) + self.first_layer.SetValue(False) - pixelsXPerMM = resolution_x_pixels / projectedXmm - pixelsYPerMM = resolution_y_pixels / projectedYmm + previous_slicer = self.display_frame.slicer + self.display_frame.slicer = 'Calibrate' - gridCountX = int(projectedXmm / 10) - gridCountY = int(projectedYmm / 10) + self.display_frame.draw_layer() - for y in range(0, gridCountY + 1): - for x in range(0, gridCountX + 1): - dc.DrawLine(0, int(y * (pixelsYPerMM * 10)), resolution_x_pixels, int(y * (pixelsYPerMM * 10))) - dc.DrawLine(int(x * (pixelsXPerMM * 10)), 0, int(x * (pixelsXPerMM * 10)), resolution_y_pixels) + self.display_frame.slicer = previous_slicer - self.first_layer.SetValue(False) - self.display_frame.slicer = 'Bitmap' - self.display_frame.draw_layer(gridBitmap.ConvertToImage()) + self.display_frame.Raise() self.Raise() def present_first_layer(self, event): if self.first_layer.GetValue(): if not hasattr(self, "layers"): - self.statusbar.SetLabel(_("No model loaded!")) + self.status(_("No model loaded!")) self.first_layer.SetValue(False) return - self.display_frame.offset = (float(self.offset_X.GetValue()), float(self.offset_Y.GetValue())) - self.display_frame.scale = float(self.scale.GetValue()) + self.display_frame.offset = (self.offset_X.GetValue(), self.offset_Y.GetValue()) + self.display_frame.scale = self.scale.GetValue() self.display_frame.slicer = self.layers[2] self.display_frame.dpi = self.get_dpi() self.display_frame.draw_layer(copy.deepcopy(self.layers[0][0])) self.calibrate.SetValue(False) self.display_frame.Refresh() - if self.show_first_layer_timer != -1.0: + sfl_timer = self.show_first_layer_timer.GetValue() + if sfl_timer > 0: def unpresent_first_layer(): self.display_frame.clear_layer() self.first_layer.SetValue(False) # AGE2023-04-19 Python 3.10 expects delay in milliseconds as # integer value instead of float. Convert float value to int - wx.CallLater(int(self.show_first_layer_timer.GetValue() * 1000), unpresent_first_layer) + wx.CallLater(int(sfl_timer * 1000), unpresent_first_layer) def update_offset(self, event): - - offset_x = float(self.offset_X.GetValue()) - offset_y = float(self.offset_Y.GetValue()) + offset_x = self.offset_X.GetValue() + offset_y = self.offset_Y.GetValue() self.display_frame.offset = (offset_x, offset_y) self._set_setting('project_offset_x', offset_x) @@ -921,20 +977,7 @@ def refresh_display(self, event): self.present_first_layer(event) def update_thickness(self, event): - layer = self.thickness.GetValue() - if ',' in layer: - # Decimal point cannot be a comma - layer = layer.replace(',', '.') - self.thickness.SetValue(layer) - self.thickness.SetInsertionPointEnd() - try: - float(layer) - except ValueError: - self.statusbar.SetLabel(_("Unrecognized number in 'Layer': %s") % layer) - return - - self.statusbar.SetLabel("") - self._set_setting('project_layer', float(layer)) + self._set_setting('project_layer', self.thickness.GetValue()) self.refresh_display(event) def update_projected_Xmm(self, event): @@ -942,73 +985,54 @@ def update_projected_Xmm(self, event): self.refresh_display(event) def update_scale(self, event): - scale = float(self.scale.GetValue()) + scale = self.scale.GetValue() self.display_frame.scale = scale self._set_setting('project_scale', scale) self.refresh_display(event) def update_interval(self, event): interval = self.interval.GetValue() - if ',' in interval: - # Decimal point cannot be a comma - interval = interval.replace(',', '.') - self.interval.SetValue(interval) - self.interval.SetInsertionPointEnd() - try: - float(interval) - except ValueError: - self.statusbar.SetLabel(_("Unrecognized number in 'Exposure': %s") % interval) - return - - self.statusbar.SetLabel("") - self.display_frame.interval = float(interval) - self._set_setting('project_interval', float(self.interval.GetValue())) + self.display_frame.interval = interval + self._set_setting('project_interval', interval) self.set_estimated_time() self.refresh_display(event) def update_pause(self, event): pause = self.pause.GetValue() - if ',' in pause: - # Decimal point cannot be a comma - pause = pause.replace(',', '.') - self.pause.SetValue(pause) - self.pause.SetInsertionPointEnd() - try: - float(pause) - except ValueError: - self.statusbar.SetLabel(_("Unrecognized number in 'Blank': %s") % pause) - return - - self.statusbar.SetLabel("") - self.display_frame.pause = float(pause) - self._set_setting('project_pause', float(pause)) + self.display_frame.pause = pause + self._set_setting('project_pause', pause) self.set_estimated_time() self.refresh_display(event) def update_overshoot(self, event): - overshoot = float(self.overshoot.GetValue()) - self.display_frame.pause = overshoot + overshoot = self.overshoot.GetValue() + self.display_frame.overshoot = overshoot self._set_setting('project_overshoot', overshoot) + self.refresh_display(event) def update_prelift_gcode(self, event): prelift_gcode = self.prelift_gcode.GetValue().replace('\n', "\\n") self.display_frame.prelift_gcode = prelift_gcode self._set_setting('project_prelift_gcode', prelift_gcode) + self.refresh_display(event) def update_postlift_gcode(self, event): postlift_gcode = self.postlift_gcode.GetValue().replace('\n', "\\n") self.display_frame.postlift_gcode = postlift_gcode self._set_setting('project_postlift_gcode', postlift_gcode) + self.refresh_display(event) def update_z_axis_rate(self, event): - z_axis_rate = int(self.z_axis_rate.GetValue()) + z_axis_rate = self.z_axis_rate.GetValue() self.display_frame.z_axis_rate = z_axis_rate self._set_setting('project_z_axis_rate', z_axis_rate) + self.refresh_display(event) def update_direction(self, event): direction = self.direction.GetSelection() self.display_frame.direction = direction self._set_setting('project_direction', direction) + self.refresh_display(event) def update_fullscreen(self, event): if self.fullscreen.GetValue(): @@ -1019,25 +1043,26 @@ def update_fullscreen(self, event): self.Raise() def update_resolution(self, event): - x = int(self.X.GetValue()) - y = int(self.Y.GetValue()) + x = self.X.GetValue() + y = self.Y.GetValue() self.display_frame.resize((x, y)) self._set_setting('project_x', x) self._set_setting('project_y', y) self.refresh_display(event) def get_dpi(self): - resolution_x_pixels = int(self.X.GetValue()) - projected_x_mm = float(self.projected_X_mm.GetValue()) + '''Cacluate dots per inch from resolution and projection''' + resolution_x_pixels = self.X.GetValue() + projected_x_mm = self.projected_X_mm.GetValue() projected_x_inches = projected_x_mm / 25.4 return resolution_x_pixels / projected_x_inches def start_present(self, event): if not hasattr(self, "layers"): - self.statusbar.SetLabel(_("No model loaded!")) + self.status(_("No model loaded!")) return - self.statusbar.SetLabel(_("Starting...")) + self.status(_("Starting...")) self.pause_button.SetLabel(self.get_btn_label('pause')) self.pause_button.Enable() self.stop_button.Enable() @@ -1048,24 +1073,24 @@ def start_present(self, event): self.display_frame.slicer = self.layers[2] self.display_frame.dpi = self.get_dpi() self.display_frame.present(self.layers[0][:], - thickness = float(self.thickness.GetValue()), - interval = float(self.interval.GetValue()), - scale = float(self.scale.GetValue()), - pause = float(self.pause.GetValue()), - overshoot = float(self.overshoot.GetValue()), - z_axis_rate = int(self.z_axis_rate.GetValue()), + thickness = self.thickness.GetValue(), + interval = self.interval.GetValue(), + scale = self.scale.GetValue(), + pause = self.pause.GetValue(), + overshoot = self.overshoot.GetValue(), + z_axis_rate = self.z_axis_rate.GetValue(), prelift_gcode = self.prelift_gcode.GetValue(), postlift_gcode = self.postlift_gcode.GetValue(), direction = self.direction.GetSelection(), size = (float(self.X.GetValue()), float(self.Y.GetValue())), - offset = (float(self.offset_X.GetValue()), float(self.offset_Y.GetValue())), + offset = (self.offset_X.GetValue(), self.offset_Y.GetValue()), layer_red = self.layer_red.IsChecked()) self.present_button.Disable() self.load_button.Disable() self.Raise() def stop_present(self, event): - self.statusbar.SetLabel(_("Stopping...")) + self.status(_("Stopping...")) self.display_frame.running = False self.pause_button.SetLabel(self.get_btn_label('pause')) self.set_current_layer(0) @@ -1073,15 +1098,15 @@ def stop_present(self, event): self.load_button.Enable() self.pause_button.Disable() self.stop_button.Disable() - self.statusbar.SetLabel(_("Stop")) + self.status(_("Stop")) def pause_present(self, event): if self.pause_button.GetLabel() == self.get_btn_label('pause'): - self.statusbar.SetLabel(self.get_btn_label('pause')) + self.status(self.get_btn_label('pause')) self.pause_button.SetLabel(self.get_btn_label('continue')) self.display_frame.running = False else: - self.statusbar.SetLabel(self.get_btn_label('continue')) + self.status(self.get_btn_label('continue')) self.pause_button.SetLabel(self.get_btn_label('pause')) self.display_frame.running = True self.display_frame.next_img() @@ -1090,8 +1115,8 @@ def on_close(self, event): self.stop_present(event) self.cleanup_temp() if self.display_frame: - self.display_frame.Destroy() - self.Destroy() + self.display_frame.DestroyLater() + self.DestroyLater() def get_btn_label(self, value): # This method simplifies translation of the button label @@ -1116,10 +1141,11 @@ def reset_all(self, event): if reset_dialog.ShowModal() == wx.ID_YES: # Reset all settings std_settings = [ - [self.thickness, "0.1", self.update_thickness], - [self.interval, "2.0", self.update_interval], - [self.pause, "2.5", self.update_pause], + [self.thickness, 0.1, self.update_thickness], + [self.interval, 2.0, self.update_interval], + [self.pause, 2.5, self.update_pause], [self.scale, 1.0, self.update_scale], + [self.direction, 0, self.update_direction], [self.overshoot, 3.0, self.update_overshoot], [self.prelift_gcode, "", self.update_prelift_gcode], @@ -1141,21 +1167,22 @@ def reset_all(self, event): for setting in std_settings: self.reset_setting(event, setting[0], setting[1], setting[2]) - # Direction is not in the std_settings list because it can't be set - # with SetValue but SetSelection instead - if not 0 == self.direction.GetSelection(): - self.direction.SetSelection(0) - self.update_direction(event) - self.reset_loaded_file() - self.statusbar.SetLabel(_("Layer Projector settings reset")) + self.status(_("Layer Projector settings reset")) def reset_setting(self, event, name, value, update_function): - # First check if the user actually changed the setting - if not value == name.GetValue(): - # If so, set it back and invoke the update_function to save the value - name.SetValue(value) - update_function(event) + try: + # First check if the user actually changed the setting + if not value == name.GetValue(): + # If so, set it back and invoke the update_function to save the value + name.SetValue(value) + update_function(event) + + except AttributeError: + if not value == name.GetSelection(): + name.SetSelection(value) + update_function(event) + if __name__ == "__main__": From fbdd02fe2e1b927bb3549d9627d2f75ada31f7d3 Mon Sep 17 00:00:00 2001 From: neofelis2X Date: Sat, 4 Nov 2023 00:20:59 +0100 Subject: [PATCH 6/6] Projectlayer: update constructor of DisplayFrame --- printrun/projectlayer.py | 220 +++++++++++++++++++++------------------ 1 file changed, 121 insertions(+), 99 deletions(-) diff --git a/printrun/projectlayer.py b/printrun/projectlayer.py index 4b02eef0c..360578624 100644 --- a/printrun/projectlayer.py +++ b/printrun/projectlayer.py @@ -31,57 +31,63 @@ # Set up Internationalization using gettext install_locale('pronterface') + class DisplayFrame(wx.Frame): def __init__(self, parent, statusbar, title, res = (1024, 768), printer = None, scale = 1.0, offset = (0, 0)): super().__init__(parent = parent, title = title, size = res) + self.printer = printer self.control_frame = parent - self.pic = wx.StaticBitmap(self) - self.bitmap = wx.Bitmap(*res) - self.bbitmap = wx.Bitmap(*res) self.slicer = 'Bitmap' self.dpi = 96 self.status = statusbar + self.scale = scale + self.index = 0 + self.offset = offset + self.running = False + self.layer_red = False + self.startime = 0 - dc = wx.MemoryDC() - dc.SelectObject(self.bbitmap) - dc.SetBackground(wx.Brush("black")) + # Closing the DisplayFrame calls the close method of Settingsframe + self.Bind(wx.EVT_CLOSE, self.control_frame.on_close) + + x_res = self.control_frame.X.GetValue() + y_res = self.control_frame.Y.GetValue() + self.size = (x_res, y_res) + self.bitmap = wx.Bitmap(*self.size) + dc = wx.MemoryDC(self.bitmap) + dc.SetBackground(wx.BLACK_BRUSH) dc.Clear() dc.SelectObject(wx.NullBitmap) - self.SetBackgroundColour("black") - self.pic.Hide() + panel = wx.Panel(self) + panel.SetBackgroundColour(wx.BLACK) + self.bitmap_widget = wx.GenericStaticBitmap(panel, -1, self.bitmap) + self.bitmap_widget.SetScaleMode(0) + self.bitmap_widget.Hide() + sizer = wx.BoxSizer() + sizer.Add(self.bitmap_widget, wx.ALIGN_LEFT | wx.ALIGN_TOP) + panel.SetSizer(sizer) + self.SetDoubleBuffered(True) self.CentreOnParent() - self.Show() - - # Closing the DisplayFrame calls the close method of Settingsframe - self.Bind(wx.EVT_CLOSE, self.control_frame.on_close) - - self.scale = scale - self.index = 0 - self.size = res - self.offset = offset - self.running = False - self.layer_red = False def clear_layer(self): - dc = wx.MemoryDC() - dc.SelectObject(self.bitmap) - dc.SetBackground(wx.Brush("black")) + dc = wx.MemoryDC(self.bitmap) + dc.SetBackground(wx.BLACK_BRUSH) dc.Clear() - self.pic.SetBitmap(self.bitmap) - self.pic.Show() + dc.SelectObject(wx.NullBitmap) + self.bitmap_widget.SetBitmap(self.bitmap) + self.bitmap_widget.Show() self.Refresh() def resize(self, res = (1024, 768)): self.bitmap = wx.Bitmap(*res) - self.bbitmap = wx.Bitmap(*res) - dc = wx.MemoryDC() - dc.SelectObject(self.bbitmap) - dc.SetBackground(wx.Brush("black")) + dc = wx.MemoryDC(self.bitmap) + dc.SetBackground(wx.BLACK_BRUSH) dc.Clear() dc.SelectObject(wx.NullBitmap) + self.Refresh() def convert_mm_to_px(self, mm_value) -> float: resolution_x_px = self.control_frame.X.GetValue() @@ -89,9 +95,8 @@ def convert_mm_to_px(self, mm_value) -> float: return resolution_x_px / projected_x_mm * mm_value def draw_layer(self, image = None): - dc = wx.MemoryDC() - dc.SelectObject(self.bitmap) - dc.SetBackground(wx.Brush("black")) + dc = wx.MemoryDC(self.bitmap) + dc.SetBackground(wx.BLACK_BRUSH) dc.Clear() gc = wx.GraphicsContext.Create(dc) @@ -118,8 +123,6 @@ def draw_layer(self, image = None): image = wx.Image(image) if self.layer_red: image = image.AdjustChannels(1, 0, 0, 1) - # AGE2023-04-19 Python 3.10 and DrawBitmap expects offset - # as integer value. Convert float values to int width, height = image.GetSize() if width < height: image = image.Rotate90(clockwise = False) @@ -141,68 +144,70 @@ def draw_layer(self, image = None): self.status(_("No valid file loaded.")) return - self.pic.SetBitmap(self.bitmap) - self.pic.Show() + dc.SelectObject(wx.NullBitmap) + self.bitmap_widget.SetBitmap(self.bitmap) + self.bitmap_widget.Show() self.Refresh() def draw_grid(self, graphics_context): - gc = graphics_context + gc = graphics_context - x_res_px = self.control_frame.X.GetValue() - y_res_px = self.control_frame.Y.GetValue() + x_res_px = self.control_frame.X.GetValue() + y_res_px = self.control_frame.Y.GetValue() - # Draw outline - path = gc.CreatePath() - path.AddRectangle(0, 0, x_res_px, y_res_px) - path.AddCircle(0, 0, 5.0) - path.AddCircle(0, 0, 14.0) + # Draw outline + path = gc.CreatePath() + path.AddRectangle(0, 0, x_res_px, y_res_px) + path.AddCircle(0, 0, 5.0) + path.AddCircle(0, 0, 14.0) - solid_pen = gc.CreatePen(wx.GraphicsPenInfo(wx.RED).Width(5.0).Style(wx.PENSTYLE_SOLID)) - gc.SetPen(solid_pen) - gc.StrokePath(path) + solid_pen = gc.CreatePen(wx.GraphicsPenInfo(wx.RED).Width(5.0).Style(wx.PENSTYLE_SOLID)) + gc.SetPen(solid_pen) + gc.StrokePath(path) - # Calculate gridlines - aspectRatio = x_res_px / y_res_px + # Calculate gridlines + aspectRatio = x_res_px / y_res_px - projected_x_mm = self.control_frame.projected_X_mm.GetValue() - projected_y_mm = round(projected_x_mm / aspectRatio, 2) + projected_x_mm = self.control_frame.projected_X_mm.GetValue() + projected_y_mm = round(projected_x_mm / aspectRatio, 2) - px_per_mm = x_res_px / projected_x_mm + px_per_mm = x_res_px / projected_x_mm - grid_count_x = int(projected_x_mm / 10) - grid_count_y = int(projected_y_mm / 10) + grid_count_x = int(projected_x_mm / 10) + grid_count_y = int(projected_y_mm / 10) - # Draw gridlines - path = gc.CreatePath() - for y in range(1, grid_count_y + 1): - for x in range(1, grid_count_x + 1): - # horizontal line - path.MoveToPoint(0, int(y * (px_per_mm * 10))) - path.AddLineToPoint(x_res_px, int(y * (px_per_mm * 10))) - # vertical line - path.MoveToPoint(int(x * (px_per_mm * 10)), 0) - path.AddLineToPoint(int(x * (px_per_mm * 10)), y_res_px) + # Draw gridlines + path = gc.CreatePath() + for y in range(1, grid_count_y + 1): + for x in range(1, grid_count_x + 1): + # horizontal line + path.MoveToPoint(0, int(y * (px_per_mm * 10))) + path.AddLineToPoint(x_res_px, int(y * (px_per_mm * 10))) + # vertical line + path.MoveToPoint(int(x * (px_per_mm * 10)), 0) + path.AddLineToPoint(int(x * (px_per_mm * 10)), y_res_px) - thin_pen = gc.CreatePen(wx.GraphicsPenInfo(wx.RED).Width(2.0).Style(wx.PENSTYLE_DOT)) - gc.SetPen(thin_pen) - gc.StrokePath(path) + thin_pen = gc.CreatePen(wx.GraphicsPenInfo(wx.RED).Width(2.0).Style(wx.PENSTYLE_DOT)) + gc.SetPen(thin_pen) + gc.StrokePath(path) def show_img_delay(self, image): - self.status(_("Showing, Timestamp {:.3f} s").format(time.perf_counter())) + self.status(_("Showing, Runtime {:.3f} s").format(time.perf_counter() - self.startime)) self.control_frame.set_current_layer(self.index) self.draw_layer(image) # AGe 2022-07-31 Python 3.10 and CallLater expects delay in milliseconds as # integer value instead of float. Convert float value to int - wx.CallLater(int(1000 * self.interval), self.hide_pic_and_rise) + timer = wx.CallLater(int(1000 * self.interval), self.hide_pic_and_rise) + self.control_frame.timers['delay'] = timer def rise(self): if self.direction == 0: # 0: Top Down - self.status(_("Lowering, Timestamp {:.3f} s").format(time.perf_counter())) + self.status(_("Lowering, Runtime {:.3f} s").format(time.perf_counter() - self.startime)) else: # self.direction == 1, 1: Bottom Up - self.status(_("Rising, Timestamp {:.3f} s").format(time.perf_counter())) + self.status(_("Rising, Runtime {:.3f} s").format(time.perf_counter() - self.startime)) if self.printer is not None and self.printer.online: - self.printer.send_now("G91") + self.printer.send_now('G91') if self.prelift_gcode: for line in self.prelift_gcode.split('\n'): @@ -210,32 +215,35 @@ def rise(self): self.printer.send_now(line) if self.direction == 0: # 0: Top Down - self.printer.send_now("G1 Z-%f F%g" % (self.overshoot, self.z_axis_rate,)) - self.printer.send_now("G1 Z%f F%g" % (self.overshoot - self.thickness, self.z_axis_rate,)) + self.printer.send_now(f"G1 Z-{self.overshoot:.3f} F{self.z_axis_rate}") + self.printer.send_now(f"G1 Z{self.overshoot - self.thickness:.3f} F{self.z_axis_rate}") else: # self.direction == 1, 1: Bottom Up - self.printer.send_now("G1 Z%f F%g" % (self.overshoot, self.z_axis_rate,)) - self.printer.send_now("G1 Z-%f F%g" % (self.overshoot - self.thickness, self.z_axis_rate,)) + self.printer.send_now(f"G1 Z{self.overshoot:.3f} F{self.z_axis_rate}") + self.printer.send_now(f"G1 Z-{self.overshoot - self.thickness:.3f} F{self.z_axis_rate}") if self.postlift_gcode: for line in self.postlift_gcode.split('\n'): if line: self.printer.send_now(line) - self.printer.send_now("G90") + self.printer.send_now('G90') else: time.sleep(self.pause) # AGe 2022-07-31 Python 3.10 expects delay in milliseconds as # integer value instead of float. Convert float value to int - wx.CallLater(int(1000 * self.pause), self.next_img) + timer = wx.CallLater(int(1000 * self.pause), self.next_img) + self.control_frame.timers['rise'] = timer def hide_pic(self): - self.status(_("Hiding, Timestamp {:.3f} s").format(time.perf_counter())) - self.pic.Hide() + self.status(_("Hiding, Runtime {:.3f} s").format(time.perf_counter() - self.startime)) + self.bitmap_widget.Hide() def hide_pic_and_rise(self): wx.CallAfter(self.hide_pic) - wx.CallLater(500, self.rise) + timer = wx.CallLater(500, self.rise) + self.control_frame.timers['hide'] = timer + def next_img(self): if not self.running: @@ -247,7 +255,7 @@ def next_img(self): else: self.control_frame.stop_present(wx.wxEVT_NULL) self.status(_("End")) - wx.CallAfter(self.pic.Hide) + wx.CallAfter(self.bitmap_widget.Hide) wx.CallAfter(self.Refresh) def present(self, @@ -264,7 +272,7 @@ def present(self, size = (1024, 768), offset = (0, 0), layer_red = False): - wx.CallAfter(self.pic.Hide) + wx.CallAfter(self.bitmap_widget.Hide) wx.CallAfter(self.Refresh) self.layers = layers self.scale = scale @@ -284,6 +292,7 @@ def present(self, self.next_img() + class SettingsFrame(wx.Dialog): def _set_setting(self, name, value): @@ -306,6 +315,7 @@ def __init__(self, parent, printer = None): self.image_dir = '' self.current_filename = '' self.slicer = '' + self.timers = {} # In wxPython 4.1.0 gtk3 (phoenix) wxWidgets 3.1.4 # Layout() breaks before Show(), invoke once after Show() @@ -613,6 +623,7 @@ def fit(ev): printer = printer) self.display_frame.Centre() self.Raise() + self.display_frame.Show() self.Show() def __del__(self): @@ -847,9 +858,12 @@ def load_file(self, event): self.reset_loaded_file() dlg = wx.FileDialog(self, _("Open file to print"), style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) # On macOS, the wildcard for *.3dlp.zip is not recognised, so it is just *.zip. - dlg.SetWildcard(_("Slic3r or Skeinforge SVG files") + " (*.svg)|*.svg|" + + dlg.SetWildcard(_("All supported files") + + " (*.svg; *.3dlp.zip; *.sl1; *.sl1s)|*.svg;*.zip;*.sl1;*.sl1s|" + + _("Slic3r or Skeinforge SVG files") + " (*.svg)|*.svg|" + _("3DLP Zip files") + " (*.3dlp.zip)|*.zip|" + - _("Prusa SL1 files") + " (*.sl1;*.sl1s)|*.sl1;*.sl1s") + _("Prusa SL1 files") + " (*.sl1; *.sl1s)|*.sl1;*.sl1s") + if dlg.ShowModal() == wx.ID_OK: name = dlg.GetPath() if not os.path.exists(name): @@ -863,7 +877,7 @@ def load_file(self, event): elif name.lower().endswith('.3dlp.zip'): layers = self.parse_3DLP_zip(name) else: - self.status(_("{} is not a sliced svg-file or zip-file.").format(os.path.split(name)[1])) + self.status(_("{} is not a sliced svg- or zip-file.").format(os.path.split(name)[1])) return if layers[2] in ('Slic3r', 'Skeinforge'): @@ -931,9 +945,7 @@ def present_calibrate(self, event): previous_slicer = self.display_frame.slicer self.display_frame.slicer = 'Calibrate' - self.display_frame.draw_layer() - self.display_frame.slicer = previous_slicer self.display_frame.Raise() @@ -960,7 +972,10 @@ def unpresent_first_layer(): self.first_layer.SetValue(False) # AGE2023-04-19 Python 3.10 expects delay in milliseconds as # integer value instead of float. Convert float value to int - wx.CallLater(int(sfl_timer * 1000), unpresent_first_layer) + if 'first' in self.timers and self.timers['first'].IsRunning(): + self.timers['first'].Stop() + timer = wx.CallLater(int(sfl_timer * 1000), unpresent_first_layer) + self.timers['first'] = timer def update_offset(self, event): offset_x = self.offset_X.GetValue() @@ -1014,31 +1029,27 @@ def update_prelift_gcode(self, event): prelift_gcode = self.prelift_gcode.GetValue().replace('\n', "\\n") self.display_frame.prelift_gcode = prelift_gcode self._set_setting('project_prelift_gcode', prelift_gcode) - self.refresh_display(event) def update_postlift_gcode(self, event): postlift_gcode = self.postlift_gcode.GetValue().replace('\n', "\\n") self.display_frame.postlift_gcode = postlift_gcode self._set_setting('project_postlift_gcode', postlift_gcode) - self.refresh_display(event) def update_z_axis_rate(self, event): z_axis_rate = self.z_axis_rate.GetValue() self.display_frame.z_axis_rate = z_axis_rate self._set_setting('project_z_axis_rate', z_axis_rate) - self.refresh_display(event) def update_direction(self, event): direction = self.direction.GetSelection() self.display_frame.direction = direction self._set_setting('project_direction', direction) - self.refresh_display(event) def update_fullscreen(self, event): - if self.fullscreen.GetValue(): - self.display_frame.ShowFullScreen(1) + if self.fullscreen.GetValue() and not self.display_frame.IsFullScreen(): + self.display_frame.ShowFullScreen(True, wx.FULLSCREEN_ALL) else: - self.display_frame.ShowFullScreen(0) + self.display_frame.ShowFullScreen(False) self.refresh_display(event) self.Raise() @@ -1068,10 +1079,11 @@ def start_present(self, event): self.stop_button.Enable() self.set_current_layer(0) self.display_frame.Raise() - if self.fullscreen.GetValue(): - self.display_frame.ShowFullScreen(1) + if self.fullscreen.GetValue() and not self.display_frame.IsFullScreen(): + self.display_frame.ShowFullScreen(True, wx.FULLSCREEN_ALL) self.display_frame.slicer = self.layers[2] self.display_frame.dpi = self.get_dpi() + self.display_frame.startime = time.perf_counter() self.display_frame.present(self.layers[0][:], thickness = self.thickness.GetValue(), interval = self.interval.GetValue(), @@ -1082,11 +1094,13 @@ def start_present(self, event): prelift_gcode = self.prelift_gcode.GetValue(), postlift_gcode = self.postlift_gcode.GetValue(), direction = self.direction.GetSelection(), - size = (float(self.X.GetValue()), float(self.Y.GetValue())), + size = (self.X.GetValue(), self.Y.GetValue()), offset = (self.offset_X.GetValue(), self.offset_Y.GetValue()), layer_red = self.layer_red.IsChecked()) self.present_button.Disable() self.load_button.Disable() + self.calibrate.SetValue(False) + self.calibrate.Disable() self.Raise() def stop_present(self, event): @@ -1096,6 +1110,7 @@ def stop_present(self, event): self.set_current_layer(0) self.present_button.Enable() self.load_button.Enable() + self.calibrate.Enable() self.pause_button.Disable() self.stop_button.Disable() self.status(_("Stop")) @@ -1104,15 +1119,23 @@ def pause_present(self, event): if self.pause_button.GetLabel() == self.get_btn_label('pause'): self.status(self.get_btn_label('pause')) self.pause_button.SetLabel(self.get_btn_label('continue')) + self.calibrate.Enable() self.display_frame.running = False else: self.status(self.get_btn_label('continue')) self.pause_button.SetLabel(self.get_btn_label('pause')) + self.calibrate.SetValue(False) + self.calibrate.Disable() self.display_frame.running = True self.display_frame.next_img() def on_close(self, event): self.stop_present(event) + # Make sure that all running timers are + # stopped before we destroy the frames + for timer in self.timers.values(): + if timer.IsRunning(): + timer.Stop() self.cleanup_temp() if self.display_frame: self.display_frame.DestroyLater() @@ -1184,7 +1207,6 @@ def reset_setting(self, event, name, value, update_function): update_function(event) - if __name__ == "__main__": a = wx.App() SettingsFrame(None)