# HG changeset patch # User mdd # Date 1611134113 -3600 # Node ID cce0af6351f058393999775a93bde7c794a34167 # Parent c82943fb205fcbb172fe6b74bc8ed42bd0a7df34 updated and added new files for printrun diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/eventhandler.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/printrun-src/printrun/eventhandler.py Wed Jan 20 10:15:13 2021 +0100 @@ -0,0 +1,126 @@ +# This file is part of the Printrun suite. +# +# Printrun is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Printrun is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Printrun. If not, see . + +class PrinterEventHandler: + ''' + Defines a skeletton of an event-handler for printer events. It + allows attaching to the printcore and will be triggered for + different events. + ''' + def __init__(self): + ''' + Constructor. + ''' + pass + + def on_init(self): + ''' + Called whenever a new printcore is initialized. + ''' + pass + + def on_send(self, command, gline): + ''' + Called on every command sent to the printer. + + @param command: The command to be sent. + @param gline: The parsed high-level command. + ''' + pass + + def on_recv(self, line): + ''' + Called on every line read from the printer. + + @param line: The data has been read from printer. + ''' + pass + + + def on_connect(self): + ''' + Called whenever printcore is connected. + ''' + pass + + def on_disconnect(self): + ''' + Called whenever printcore is disconnected. + ''' + pass + + def on_error(self, error): + ''' + Called whenever an error occurs. + + @param error: The error that has been triggered. + ''' + pass + + def on_online(self): + ''' + Called when printer got online. + ''' + pass + + def on_temp(self, line): + ''' + Called for temp, status, whatever. + + @param line: Line of data. + ''' + pass + + def on_start(self, resume): + ''' + Called when printing is started. + + @param resume: If true, the print is resumed. + ''' + pass + + def on_end(self): + ''' + Called when printing ends. + ''' + pass + + def on_layerchange(self, layer): + ''' + Called on layer changed. + + @param layer: The new layer. + ''' + pass + + def on_preprintsend(self, gline, index, mainqueue): + ''' + Called pre sending printing command. + + @param gline: Line to be send. + @param index: Index in the mainqueue. + @param mainqueue: The main queue of commands. + ''' + pass + + def on_printsend(self, gline): + ''' + Called whenever a line is sent to the printer. + + @param gline: The line send to the printer. + ''' + pass + + \ No newline at end of file diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/excluder.py --- a/printrun-src/printrun/excluder.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/excluder.py Wed Jan 20 10:15:13 2021 +0100 @@ -24,10 +24,9 @@ def __init__(self, excluder, *args, **kwargs): super(ExcluderWindow, self).__init__(*args, **kwargs) self.SetTitle(_("Part excluder: draw rectangles where print instructions should be ignored")) - self.toolbar.AddLabelTool(128, " " + _("Reset selection"), - wx.Image(imagefile('reset.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), - shortHelp = _("Reset selection"), - longHelp = "") + self.toolbar.AddTool(128, " " + _("Reset selection"), + wx.Image(imagefile('reset.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), + _("Reset selection")) self.Bind(wx.EVT_TOOL, self.reset_selection, id = 128) self.parent = excluder self.p.paint_overlay = self.paint_selection @@ -46,7 +45,7 @@ or event.ButtonUp(wx.MOUSE_BTN_RIGHT): self.initpos = None elif event.Dragging() and event.RightIsDown(): - e = event.GetPositionTuple() + e = event.GetPosition() if not self.initpos or not hasattr(self, "basetrans"): self.initpos = e self.basetrans = self.p.translate @@ -55,7 +54,7 @@ self.p.dirty = 1 wx.CallAfter(self.p.Refresh) elif event.Dragging() and event.LeftIsDown(): - x, y = event.GetPositionTuple() + x, y = event.GetPosition() if not self.initpos: self.basetrans = self.p.translate x = (x - self.basetrans[0]) / self.p.scale[0] @@ -97,7 +96,7 @@ self.parent.rectangles = [] wx.CallAfter(self.p.Refresh) -class Excluder(object): +class Excluder: def __init__(self): self.rectangles = [] @@ -120,7 +119,7 @@ if __name__ == '__main__': import sys - import gcoder + from . import gcoder gcode = gcoder.GCode(open(sys.argv[1])) app = wx.App(False) ex = Excluder() diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gcodeplater.py --- a/printrun-src/printrun/gcodeplater.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gcodeplater.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # This file is part of the Printrun suite. # @@ -79,17 +79,20 @@ def prepare_ui(self, filenames = [], callback = None, parent = None, build_dimensions = None, - circular_platform = False, antialias_samples = 0): + circular_platform = False, + antialias_samples = 0, + grid = (1, 10)): super(GcodePlaterPanel, self).prepare_ui(filenames, callback, parent, build_dimensions) viewer = gcview.GcodeViewPanel(self, build_dimensions = self.build_dimensions, antialias_samples = antialias_samples) self.set_viewer(viewer) self.platform = actors.Platform(self.build_dimensions, - circular = circular_platform) + circular = circular_platform, + grid = grid) self.platform_object = gcview.GCObject(self.platform) def get_objects(self): - return [self.platform_object] + self.models.values() + return [self.platform_object] + list(self.models.values()) objects = property(get_objects) def load_file(self, filename): @@ -99,9 +102,9 @@ if gcode.filament_length > 0: model.display_travels = False generator = model.load_data(gcode) - generator_output = generator.next() + generator_output = next(generator) while generator_output is not None: - generator_output = generator.next() + generator_output = next(generator) obj = gcview.GCObject(model) obj.offsets = [self.build_dimensions[3], self.build_dimensions[4], 0] obj.gcode = gcode @@ -142,7 +145,7 @@ return self.export_sequential(name) def export_combined(self, name): - models = self.models.values() + models = list(self.models.values()) last_real_position = None # Sort models by Z max to print smaller objects first models.sort(key = lambda x: x.dims[-1]) @@ -151,7 +154,7 @@ def add_offset(layer): return layer.z + model.offsets[2] if layer.z is not None else layer.z alllayers += [(add_offset(layer), model_i, layer_i) - for (layer_i, layer) in enumerate(model.gcode.all_layers) if layer] + for (layer_i, layer) in enumerate(model.gcode.all_layers) if add_offset(layer) is not None] alllayers.sort() laste = [0] * len(models) lasttool = [0] * len(models) @@ -196,7 +199,7 @@ logging.info(_("Exported merged G-Codes to %s") % name) def export_sequential(self, name): - models = self.models.values() + models = list(self.models.values()) last_real_position = None # Sort models by Z max to print smaller objects first models.sort(key = lambda x: x.dims[-1]) @@ -223,7 +226,7 @@ else: f.write(rewrite_gline(co, l, math.cos(r), math.sin(r)) + "\n") # Find the current real position - for i in xrange(len(model.gcode) - 1, -1, -1): + for i in range(len(model.gcode) - 1, -1, -1): gline = model.gcode.lines[i] if gline.is_move: last_real_position = (- trans[0] + gline.current_x, diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gcoder.py --- a/printrun-src/printrun/gcoder.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gcoder.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,12 +1,13 @@ -#!/usr/bin/env python -# This file is copied from GCoder. +#!/usr/bin/env python3 # -# GCoder is free software: you can redistribute it and/or modify +# This file is part of the Printrun suite. +# +# Printrun is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# GCoder is distributed in the hope that it will be useful, +# Printrun is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. @@ -22,15 +23,15 @@ from array import array gcode_parsed_args = ["x", "y", "e", "f", "z", "i", "j"] -gcode_parsed_nonargs = ["g", "t", "m", "n"] -to_parse = "".join(gcode_parsed_args + gcode_parsed_nonargs) -gcode_exp = re.compile("\([^\(\)]*\)|;.*|[/\*].*\n|([%s])([-+]?[0-9]*\.?[0-9]*)" % to_parse) +gcode_parsed_nonargs = 'gtmnd' +to_parse = "".join(gcode_parsed_args) + gcode_parsed_nonargs +gcode_exp = re.compile("\([^\(\)]*\)|;.*|[/\*].*\n|([%s])\s*([-+]?[0-9]*\.?[0-9]*)" % to_parse) gcode_strip_comment_exp = re.compile("\([^\(\)]*\)|;.*|[/\*].*\n") m114_exp = re.compile("\([^\(\)]*\)|[/\*].*\n|([XYZ]):?([-+]?[0-9]*\.?[0-9]*)") specific_exp = "(?:\([^\(\)]*\))|(?:;.*)|(?:[/\*].*\n)|(%s[-+]?[0-9]*\.?[0-9]*)" move_gcodes = ["G0", "G1", "G2", "G3"] -class PyLine(object): +class PyLine: __slots__ = ('x', 'y', 'z', 'e', 'f', 'i', 'j', 'raw', 'command', 'is_move', @@ -45,7 +46,7 @@ def __getattr__(self, name): return None -class PyLightLine(object): +class PyLightLine: __slots__ = ('raw', 'command') @@ -56,10 +57,10 @@ return None try: - import gcoder_line + from . import gcoder_line Line = gcoder_line.GLine LightLine = gcoder_line.GLightLine -except Exception, e: +except Exception as e: logging.warning("Memory-efficient GCoder implementation unavailable: %s" % e) Line = PyLine LightLine = PyLightLine @@ -107,8 +108,9 @@ def __init__(self, lines, z = None): super(Layer, self).__init__(lines) self.z = z + self.duration = 0 -class GCode(object): +class GCode: line_class = Line @@ -121,6 +123,7 @@ append_layer_id = None imperial = False + cutting = False relative = False relative_e = False current_tool = 0 @@ -217,7 +220,9 @@ layers_count = property(_get_layers_count) def __init__(self, data = None, home_pos = None, - layer_callback = None, deferred = False): + layer_callback = None, deferred = False, + cutting_as_extrusion = False): + self.cutting_as_extrusion = cutting_as_extrusion if not deferred: self.prepare(data, home_pos, layer_callback) @@ -240,6 +245,8 @@ self.layer_idxs = array('I', []) self.line_idxs = array('I', []) + def has_index(self, i): + return i < len(self) def __len__(self): return len(self.line_idxs) @@ -314,7 +321,7 @@ self.lines.append(gline) self.append_layer.append(gline) self.layer_idxs.append(self.append_layer_id) - self.line_idxs.append(len(self.append_layer)) + self.line_idxs.append(len(self.append_layer)-1) return gline def _preprocess(self, lines = None, build_layers = False, @@ -338,6 +345,7 @@ offset_e = self.offset_e total_e = self.total_e max_e = self.max_e + cutting = self.cutting current_e_multi = self.current_e_multi[current_tool] offset_e_multi = self.offset_e_multi[current_tool] @@ -367,7 +375,8 @@ # get device caps from firmware: max speed, acceleration/axis # (including extruder) # calculate the maximum move duration accounting for above ;) - lastx = lasty = lastz = laste = lastf = 0.0 + lastx = lasty = lastz = None + laste = lastf = 0 lastdx = 0 lastdy = 0 x = y = e = f = 0.0 @@ -383,15 +392,41 @@ layer_idxs = self.layer_idxs = [] line_idxs = self.line_idxs = [] - layer_id = 0 - layer_line = 0 last_layer_z = None prev_z = None - prev_base_z = (None, None) cur_z = None cur_lines = [] + def append_lines(lines, isEnd): + if not build_layers: + return + nonlocal layerbeginduration, last_layer_z + if cur_layer_has_extrusion and prev_z != last_layer_z \ + or not all_layers or isEnd: + layer = Layer([], prev_z) + last_layer_z = prev_z + finished_layer = len(all_layers)-1 if all_layers else None + all_layers.append(layer) + else: + layer = all_layers[-1] + finished_layer = None + layer_id = len(all_layers)-1 + layer_line = len(layer) + for i, ln in enumerate(lines): + layer.append(ln) + layer_idxs.append(layer_id) + line_idxs.append(layer_line+i) + layer.duration += totalduration - layerbeginduration + layerbeginduration = totalduration + if layer_callback: + # we finish a layer when inserting the next + if finished_layer is not None: + layer_callback(self, finished_layer) + # notify about end layer, there will not be next + if isEnd: + layer_callback(self, layer_id) + if self.line_class != Line: get_line = lambda l: Line(l.raw) else: @@ -422,12 +457,20 @@ elif line.command == "M83": relative_e = True elif line.command[0] == "T": - current_tool = int(line.command[1:]) - while(current_tool+1>len(self.current_e_multi)): + try: + current_tool = int(line.command[1:]) + except: + pass #handle T? by treating it as no tool change + while current_tool+1 > len(self.current_e_multi): self.current_e_multi+=[0] self.offset_e_multi+=[0] self.total_e_multi+=[0] self.max_e_multi+=[0] + elif line.command == "M3" or line.command == "M4": + cutting = True + elif line.command == "M5": + cutting = False + current_e_multi = self.current_e_multi[current_tool] offset_e_multi = self.offset_e_multi[current_tool] total_e_multi = self.total_e_multi[current_tool] @@ -504,6 +547,8 @@ elif line.command == "G92": offset_e = current_e - line.e offset_e_multi = current_e_multi - line.e + if cutting and self.cutting_as_extrusion: + line.extruding = True self.current_e_multi[current_tool]=current_e_multi self.offset_e_multi[current_tool]=offset_e_multi @@ -516,11 +561,12 @@ if line.is_move: if line.extruding: if line.current_x is not None: - xmin_e = min(xmin_e, line.current_x) - xmax_e = max(xmax_e, line.current_x) + # G0 X10 ; G1 X20 E5 results in 10..20 even as G0 is not extruding + xmin_e = min(xmin_e, line.current_x, xmin_e if lastx is None else lastx) + xmax_e = max(xmax_e, line.current_x, xmax_e if lastx is None else lastx) if line.current_y is not None: - ymin_e = min(ymin_e, line.current_y) - ymax_e = max(ymax_e, line.current_y) + ymin_e = min(ymin_e, line.current_y, ymin_e if lasty is None else lasty) + ymax_e = max(ymax_e, line.current_y, ymax_e if lasty is None else lasty) if max_e <= 0: if line.current_x is not None: xmin = min(xmin, line.current_x) @@ -531,9 +577,9 @@ # Compute duration if line.command == "G0" or line.command == "G1": - x = line.x if line.x is not None else lastx - y = line.y if line.y is not None else lasty - z = line.z if line.z is not None else lastz + x = line.x if line.x is not None else (lastx or 0) + y = line.y if line.y is not None else (lasty or 0) + z = line.z if line.z is not None else (lastz or 0) e = line.e if line.e is not None else laste # mm/s vs mm/m => divide by 60 f = line.f / 60.0 if line.f is not None else lastf @@ -553,15 +599,15 @@ # The following code tries to fix it by forcing a full # reacceleration if this move is in the opposite direction # of the previous one - dx = x - lastx - dy = y - lasty + dx = x - (lastx or 0) + dy = y - (lasty or 0) if dx * lastdx + dy * lastdy <= 0: lastf = 0 currenttravel = math.hypot(dx, dy) if currenttravel == 0: if line.z is not None: - currenttravel = abs(line.z) if line.relative else abs(line.z - lastz) + currenttravel = abs(line.z) if line.relative else abs(line.z - (lastz or 0)) elif line.e is not None: currenttravel = abs(line.e) if line.relative_e else abs(line.e - laste) # Feedrate hasn't changed, no acceleration/decceleration planned @@ -606,47 +652,14 @@ else: cur_z = line.z - # FIXME: the logic behind this code seems to work, but it might be - # broken - if cur_z != prev_z: - if prev_z is not None and last_layer_z is not None: - offset = self.est_layer_height if self.est_layer_height else 0.01 - if abs(prev_z - last_layer_z) < offset: - if self.est_layer_height is None: - zs = sorted([l.z for l in all_layers if l.z is not None]) - heights = [round(zs[i + 1] - zs[i], 3) for i in range(len(zs) - 1)] - heights = [height for height in heights if height] - if len(heights) >= 2: self.est_layer_height = heights[1] - elif heights: self.est_layer_height = heights[0] - else: self.est_layer_height = 0.1 - base_z = round(prev_z - (prev_z % self.est_layer_height), 2) - else: - base_z = round(prev_z, 2) - else: - base_z = prev_z - - if base_z != prev_base_z: - new_layer = Layer(cur_lines, base_z) - new_layer.duration = totalduration - layerbeginduration - layerbeginduration = totalduration - all_layers.append(new_layer) - if cur_layer_has_extrusion and prev_z not in all_zs: - all_zs.add(prev_z) - cur_lines = [] - cur_layer_has_extrusion = False - layer_id += 1 - layer_line = 0 - last_layer_z = base_z - if layer_callback is not None: - layer_callback(self, len(all_layers) - 1) - - prev_base_z = base_z + if cur_z != prev_z and cur_layer_has_extrusion: + append_lines(cur_lines, False) + all_zs.add(prev_z) + cur_lines = [] + cur_layer_has_extrusion = False if build_layers: cur_lines.append(true_line) - layer_idxs.append(layer_id) - line_idxs.append(layer_line) - layer_line += 1 prev_z = cur_z # ## Loop done @@ -669,17 +682,14 @@ self.offset_e_multi[current_tool]=offset_e_multi self.max_e_multi[current_tool]=max_e_multi self.total_e_multi[current_tool]=total_e_multi + self.cutting = cutting # Finalize layers if build_layers: if cur_lines: - new_layer = Layer(cur_lines, prev_z) - new_layer.duration = totalduration - layerbeginduration - layerbeginduration = totalduration - all_layers.append(new_layer) - if cur_layer_has_extrusion and prev_z not in all_zs: - all_zs.add(prev_z) + append_lines(cur_lines, True) + all_zs.add(prev_z) self.append_layer_id = len(all_layers) self.append_layer = Layer([]) @@ -689,7 +699,7 @@ self.line_idxs = array('I', line_idxs) # Compute bounding box - all_zs = self.all_zs.union(set([zmin])).difference(set([None])) + all_zs = self.all_zs.union({zmin}).difference({None}) zmin = min(all_zs) zmax = max(all_zs) @@ -731,25 +741,25 @@ def main(): if len(sys.argv) < 2: - print "usage: %s filename.gcode" % sys.argv[0] + print("usage: %s filename.gcode" % sys.argv[0]) return - print "Line object size:", sys.getsizeof(Line("G0 X0")) - print "Light line object size:", sys.getsizeof(LightLine("G0 X0")) + print("Line object size:", sys.getsizeof(Line("G0 X0"))) + print("Light line object size:", sys.getsizeof(LightLine("G0 X0"))) gcode = GCode(open(sys.argv[1], "rU")) - print "Dimensions:" + print("Dimensions:") xdims = (gcode.xmin, gcode.xmax, gcode.width) - print "\tX: %0.02f - %0.02f (%0.02f)" % xdims + print("\tX: %0.02f - %0.02f (%0.02f)" % xdims) ydims = (gcode.ymin, gcode.ymax, gcode.depth) - print "\tY: %0.02f - %0.02f (%0.02f)" % ydims + print("\tY: %0.02f - %0.02f (%0.02f)" % ydims) zdims = (gcode.zmin, gcode.zmax, gcode.height) - print "\tZ: %0.02f - %0.02f (%0.02f)" % zdims - print "Filament used: %0.02fmm" % gcode.filament_length + print("\tZ: %0.02f - %0.02f (%0.02f)" % zdims) + print("Filament used: %0.02fmm" % gcode.filament_length) for i in enumerate(gcode.filament_length_multi): - print "E%d %0.02fmm" % (i[0],i[1]) - print "Number of layers: %d" % gcode.layers_count - print "Estimated duration: %s" % gcode.estimate_duration()[1] + print("E%d %0.02fmm" % (i[0],i[1])) + print("Number of layers: %d" % gcode.layers_count) + print("Estimated duration: %s" % gcode.estimate_duration()[1]) if __name__ == '__main__': main() diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gcoder_line.pyx --- a/printrun-src/printrun/gcoder_line.pyx Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gcoder_line.pyx Wed Jan 20 10:15:13 2021 +0100 @@ -1,11 +1,13 @@ -# This file is copied from GCoder. +#cython: language_level=3 # -# GCoder is free software: you can redistribute it and/or modify +# This file is part of the Printrun suite. +# +# Printrun is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# GCoder is distributed in the hope that it will be useful, +# Printrun is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. @@ -18,6 +20,7 @@ from libc.string cimport strlen, strncpy cdef char* copy_string(object value): + value = value.encode('utf-8') cdef char* orig = value str_len = len(orig) cdef char* array = malloc(str_len + 1) @@ -192,7 +195,7 @@ self._status = set_has_var(self._status, pos_gcview_end_vertex) property raw: def __get__(self): - if has_var(self._status, pos_raw): return self._raw + if has_var(self._status, pos_raw): return self._raw.decode('utf-8') else: return None def __set__(self, value): # WARNING: memory leak could happen here, as we don't do the following : @@ -201,7 +204,7 @@ self._status = set_has_var(self._status, pos_raw) property command: def __get__(self): - if has_var(self._status, pos_command): return self._command + if has_var(self._status, pos_command): return self._command.decode('utf-8') else: return None def __set__(self, value): # WARNING: memory leak could happen here, as we don't do the following : @@ -231,7 +234,7 @@ property raw: def __get__(self): - if has_var(self._status, pos_raw): return self._raw + if has_var(self._status, pos_raw): return self._raw.decode('utf-8') else: return None def __set__(self, value): # WARNING: memory leak could happen here, as we don't do the following : @@ -240,7 +243,7 @@ self._status = set_has_var(self._status, pos_raw) property command: def __get__(self): - if has_var(self._status, pos_command): return self._command + if has_var(self._status, pos_command): return self._command.decode('utf-8') else: return None def __set__(self, value): # WARNING: memory leak could happen here, as we don't do the following : diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gcview.py --- a/printrun-src/printrun/gcview.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gcview.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # This file is part of the Printrun suite. # @@ -51,8 +51,8 @@ if hasattr(root, root_fieldname): setattr(model, field, getattr(root, root_fieldname)) -def recreate_platform(self, build_dimensions, circular): - self.platform = actors.Platform(build_dimensions, circular = circular) +def recreate_platform(self, build_dimensions, circular, grid): + self.platform = actors.Platform(build_dimensions, circular = circular, grid = grid) self.objects[0].model = self.platform wx.CallAfter(self.Refresh) @@ -66,32 +66,40 @@ has_changed = True return has_changed +# E selected for Up because is above D +LAYER_UP_KEYS = ord('U'), ord('E'), wx.WXK_UP +LAYER_DOWN_KEYS = ord('D'), wx.WXK_DOWN +ZOOM_IN_KEYS = wx.WXK_PAGEDOWN, 388, wx.WXK_RIGHT, ord('=') +ZOOM_OUT_KEYS = wx.WXK_PAGEUP, 390, wx.WXK_LEFT, ord('-') +FIT_KEYS = [ord('F')] +CURRENT_LAYER_KEYS = [ord('C')] +RESET_KEYS = [ord('R')] + class GcodeViewPanel(wxGLPanel): - def __init__(self, parent, id = wx.ID_ANY, - build_dimensions = None, realparent = None, - antialias_samples = 0): - super(GcodeViewPanel, self).__init__(parent, id, wx.DefaultPosition, + def __init__(self, parent, + build_dimensions = (200, 200, 100, 0, 0, 0), + realparent = None, antialias_samples = 0): + super().__init__(parent, wx.DefaultPosition, wx.DefaultSize, 0, antialias_samples = antialias_samples) self.canvas.Bind(wx.EVT_MOUSE_EVENTS, self.move) self.canvas.Bind(wx.EVT_LEFT_DCLICK, self.double) - self.canvas.Bind(wx.EVT_KEY_DOWN, self.keypress) + # self.canvas.Bind(wx.EVT_KEY_DOWN, self.keypress) + # in Windows event inspector shows only EVT_CHAR_HOOK events + self.canvas.Bind(wx.EVT_CHAR_HOOK, self.keypress) self.initialized = 0 self.canvas.Bind(wx.EVT_MOUSEWHEEL, self.wheel) - self.parent = realparent if realparent else parent + self.parent = realparent or parent self.initpos = None - if build_dimensions: - self.build_dimensions = build_dimensions - else: - self.build_dimensions = [200, 200, 100, 0, 0, 0] - self.dist = max(self.build_dimensions[0], self.build_dimensions[1]) + self.build_dimensions = build_dimensions + self.dist = max(self.build_dimensions[:2]) self.basequat = [0, 0, 0, 1] self.mousepos = [0, 0] def inject(self): l = self.parent.model.num_layers_to_draw - filtered = [k for k, v in self.parent.model.layer_idxs_map.iteritems() if v == l] + filtered = [k for k, v in self.parent.model.layer_idxs_map.items() if v == l] if filtered: injector(self.parent.model.gcode, l, filtered[0]) else: @@ -99,7 +107,7 @@ def editlayer(self): l = self.parent.model.num_layers_to_draw - filtered = [k for k, v in self.parent.model.layer_idxs_map.iteritems() if v == l] + filtered = [k for k, v in self.parent.model.layer_idxs_map.items() if v == l] if filtered: injector_edit(self.parent.model.gcode, l, filtered[0]) else: @@ -109,13 +117,13 @@ pass def OnInitGL(self, *args, **kwargs): - super(GcodeViewPanel, self).OnInitGL(*args, **kwargs) - if hasattr(self.parent, "filenames") and self.parent.filenames: - for filename in self.parent.filenames: + super().OnInitGL(*args, **kwargs) + filenames = getattr(self.parent, 'filenames', None) + if filenames: + for filename in filenames: self.parent.load_file(filename) self.parent.autoplate() - if hasattr(self.parent, "loadcb"): - self.parent.loadcb() + getattr(self.parent, 'loadcb', bool)() self.parent.filenames = None def create_objects(self): @@ -125,7 +133,7 @@ obj.model.init() def update_object_resize(self): - '''called when the window recieves only if opengl is initialized''' + '''called when the window receives only if opengl is initialized''' pass def draw_objects(self): @@ -142,9 +150,14 @@ for obj in self.parent.objects: if not obj.model \ - or not obj.model.loaded \ - or not obj.model.initialized: + or not obj.model.loaded: continue + # Skip (comment out) initialized check, which safely causes empty + # model during progressive load. This can cause exceptions/garbage + # render, but seems fine for now + # May need to lock init() and draw_objects() together + # if not obj.model.initialized: + # continue glPushMatrix() glTranslatef(*(obj.offsets)) glRotatef(obj.rot, 0.0, 0.0, 1.0) @@ -175,8 +188,7 @@ return mvmat def double(self, event): - if hasattr(self.parent, "clickcb") and self.parent.clickcb: - self.parent.clickcb(event) + getattr(self.parent, 'clickcb', bool)(event) def move(self, event): """react to mouse actions: @@ -188,22 +200,18 @@ self.canvas.SetFocus() event.Skip() return - if event.Dragging() and event.LeftIsDown(): - self.handle_rotation(event) - elif event.Dragging() and event.RightIsDown(): - self.handle_translation(event) - elif event.LeftUp(): + if event.Dragging(): + if event.LeftIsDown(): + self.handle_rotation(event) + elif event.RightIsDown(): + self.handle_translation(event) + self.Refresh(False) + elif event.LeftUp() or event.RightUp(): self.initpos = None - elif event.RightUp(): - self.initpos = None - else: - event.Skip() - return event.Skip() - wx.CallAfter(self.Refresh) def layerup(self): - if not hasattr(self.parent, "model") or not self.parent.model: + if not getattr(self.parent, 'model', False): return max_layers = self.parent.model.max_layers current_layer = self.parent.model.num_layers_to_draw @@ -216,7 +224,7 @@ wx.CallAfter(self.Refresh) def layerdown(self): - if not hasattr(self.parent, "model") or not self.parent.model: + if not getattr(self.parent, 'model', False): return current_layer = self.parent.model.num_layers_to_draw new_layer = max(1, current_layer - 1) @@ -224,7 +232,14 @@ self.parent.setlayercb(new_layer) wx.CallAfter(self.Refresh) + wheelTimestamp = None def handle_wheel(self, event): + if self.wheelTimestamp == event.Timestamp: + # filter duplicate event delivery in Ubuntu, Debian issue #1110 + return + + self.wheelTimestamp = event.Timestamp + delta = event.GetWheelRotation() factor = 1.05 if event.ControlDown(): @@ -237,7 +252,7 @@ if delta > 0: self.layerup() else: self.layerdown() return - x, y = event.GetPositionTuple() + x, y = event.GetPosition() x, y, _ = self.mouse_to_3d(x, y) if delta > 0: self.zoom(factor, (x, y)) @@ -269,35 +284,34 @@ wx.CallAfter(self.Refresh) def keypress(self, event): - """gets keypress events and moves/rotates acive shape""" - step = 1.1 - if event.ControlDown(): - step = 1.05 - kup = [85, 315] # Up keys - kdo = [68, 317] # Down Keys - kzi = [wx.WXK_PAGEDOWN, 388, 316, 61] # Zoom In Keys - kzo = [wx.WXK_PAGEUP, 390, 314, 45] # Zoom Out Keys - kfit = [70] # Fit to print keys - kshowcurrent = [67] # Show only current layer keys - kreset = [82] # Reset keys + """gets keypress events and moves/rotates active shape""" + if event.HasModifiers(): + # let alt+c bubble up + event.Skip() + return + step = event.ControlDown() and 1.05 or 1.1 key = event.GetKeyCode() - if key in kup: + if key in LAYER_UP_KEYS: self.layerup() - if key in kdo: + return # prevent shifting focus to other controls + elif key in LAYER_DOWN_KEYS: self.layerdown() - x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) - if key in kzi: + return + # x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) + elif key in ZOOM_IN_KEYS: self.zoom_to_center(step) - if key in kzo: + return + elif key in ZOOM_OUT_KEYS: self.zoom_to_center(1 / step) - if key in kfit: + return + elif key in FIT_KEYS: self.fit() - if key in kshowcurrent: + elif key in CURRENT_LAYER_KEYS: if not self.parent.model or not self.parent.model.loaded: return self.parent.model.only_current = not self.parent.model.only_current wx.CallAfter(self.Refresh) - if key in kreset: + elif key in RESET_KEYS: self.resetview() event.Skip() @@ -307,7 +321,7 @@ self.basequat = [0, 0, 0, 1] wx.CallAfter(self.Refresh) -class GCObject(object): +class GCObject: def __init__(self, model): self.offsets = [0, 0, 0] @@ -317,7 +331,7 @@ self.scale = [1.0, 1.0, 1.0] self.model = model -class GcodeViewLoader(object): +class GcodeViewLoader: path_halfwidth = 0.2 path_halfheight = 0.15 @@ -332,24 +346,25 @@ set_model_colors(self.model, self.root) if gcode is not None: generator = self.model.load_data(gcode) - generator_output = generator.next() + generator_output = next(generator) while generator_output is not None: yield generator_output - generator_output = generator.next() + generator_output = next(generator) wx.CallAfter(self.Refresh) yield None def addfile(self, gcode = None, showall = False): generator = self.addfile_perlayer(gcode, showall) - while generator.next() is not None: + while next(generator) is not None: continue def set_gcview_params(self, path_width, path_height): return set_gcview_params(self, path_width, path_height) -class GcodeViewMainWrapper(GcodeViewLoader): +from printrun.gviz import BaseViz +class GcodeViewMainWrapper(GcodeViewLoader, BaseViz): - def __init__(self, parent, build_dimensions, root, circular, antialias_samples): + def __init__(self, parent, build_dimensions, root, circular, antialias_samples, grid): self.root = root self.glpanel = GcodeViewPanel(parent, realparent = self, build_dimensions = build_dimensions, @@ -361,13 +376,21 @@ self.widget = self.glpanel self.refresh_timer = wx.CallLater(100, self.Refresh) self.p = self # Hack for backwards compatibility with gviz API - self.platform = actors.Platform(build_dimensions, circular = circular) + self.grid = grid + self.platform = actors.Platform(build_dimensions, circular = circular, grid = grid) self.model = None self.objects = [GCObject(self.platform), GCObject(None)] def __getattr__(self, name): return getattr(self.glpanel, name) + def on_settings_change(self, changed_settings): + if self.model: + for s in changed_settings: + if s.name.startswith('gcview_color_'): + self.model.update_colors() + break + def set_current_gline(self, gline): if gline.is_move and gline.gcview_end_vertex is not None \ and self.model and self.model.loaded: @@ -375,11 +398,8 @@ if not self.refresh_timer.IsRunning(): self.refresh_timer.Start() - def recreate_platform(self, build_dimensions, circular): - return recreate_platform(self, build_dimensions, circular) - - def addgcodehighlight(self, *a): - pass + def recreate_platform(self, build_dimensions, circular, grid): + return recreate_platform(self, build_dimensions, circular, grid) def setlayer(self, layer): if layer in self.model.layer_idxs_map: @@ -398,7 +418,8 @@ def __init__(self, parent, ID, title, build_dimensions, objects = None, pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.DEFAULT_FRAME_STYLE, root = None, circular = False, - antialias_samples = 0): + antialias_samples = 0, + grid = (1, 10)): GvizBaseFrame.__init__(self, parent, ID, title, pos, size, style) self.root = root @@ -408,15 +429,12 @@ self.refresh_timer = wx.CallLater(100, self.Refresh) self.p = self # Hack for backwards compatibility with gviz API self.clonefrom = objects - self.platform = actors.Platform(build_dimensions, circular = circular) - if objects: - self.model = objects[1].model - else: - self.model = None + self.platform = actors.Platform(build_dimensions, circular = circular, grid = grid) + self.model = objects[1].model if objects else None self.objects = [GCObject(self.platform), GCObject(None)] fit_image = wx.Image(imagefile('fit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap() - self.toolbar.InsertLabelTool(6, 8, " " + _("Fit to plate"), fit_image, + self.toolbar.InsertTool(6, 8, " " + _("Fit to plate"), fit_image, shortHelp = _("Fit to plate [F]"), longHelp = '') self.toolbar.Realize() @@ -441,7 +459,7 @@ def update_status(self, extra): layer = self.model.num_layers_to_draw - filtered = [k for k, v in self.model.layer_idxs_map.iteritems() if v == layer] + filtered = [k for k, v in self.model.layer_idxs_map.items() if v == layer] if filtered: true_layer = filtered[0] z = self.model.gcode.all_layers[true_layer].z @@ -465,8 +483,8 @@ if not self.refresh_timer.IsRunning(): self.refresh_timer.Start() - def recreate_platform(self, build_dimensions, circular): - return recreate_platform(self, build_dimensions, circular) + def recreate_platform(self, build_dimensions, circular, grid): + return recreate_platform(self, build_dimensions, circular, grid) def addfile(self, gcode = None): if self.clonefrom: diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gl/libtatlin/actors.py --- a/printrun-src/printrun/gl/libtatlin/actors.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gl/libtatlin/actors.py Wed Jan 20 10:15:13 2021 +0100 @@ -65,7 +65,7 @@ return [i1, i2, j2, j2, j1, i1, i2, i3, j3, j3, j2, i2, i3, i4, j4, j4, j3, i3, i4, i1, j1, j1, j4, i4] -class BoundingBox(object): +class BoundingBox: """ A rectangular box (cuboid) enclosing a 3D model, defined by lower and upper corners. """ @@ -89,13 +89,12 @@ return round(height, 2) -class Platform(object): +class Platform: """ Platform on which models are placed. """ - graduations_major = 10 - def __init__(self, build_dimensions, light = False, circular = False): + def __init__(self, build_dimensions, light = False, circular = False, grid = (1, 10)): self.light = light self.circular = circular self.width = build_dimensions[0] @@ -104,6 +103,7 @@ self.xoffset = build_dimensions[3] self.yoffset = build_dimensions[4] self.zoffset = build_dimensions[5] + self.grid = grid self.color_grads_minor = (0xaf / 255, 0xdf / 255, 0x5f / 255, 0.1) self.color_grads_interm = (0xaf / 255, 0xdf / 255, 0x5f / 255, 0.2) @@ -122,9 +122,9 @@ glTranslatef(self.xoffset, self.yoffset, self.zoffset) def color(i): - if i % self.graduations_major == 0: + if i % self.grid[1] == 0: glColor4f(*self.color_grads_major) - elif i % (self.graduations_major / 2) == 0: + elif i % (self.grid[1] // 2) == 0: glColor4f(*self.color_grads_interm) else: if self.light: return False @@ -134,26 +134,26 @@ # draw the grid glBegin(GL_LINES) if self.circular: # Draw a circular grid - for i in range(0, int(math.ceil(self.width + 1))): + for i in numpy.arange(0, int(math.ceil(self.width + 1)), self.grid[0]): angle = math.asin(2 * float(i) / self.width - 1) x = (math.cos(angle) + 1) * self.depth / 2 if color(i): glVertex3f(float(i), self.depth - x, 0.0) glVertex3f(float(i), x, 0.0) - for i in range(0, int(math.ceil(self.depth + 1))): + for i in numpy.arange(0, int(math.ceil(self.depth + 1)), self.grid[0]): angle = math.acos(2 * float(i) / self.depth - 1) x = (math.sin(angle) + 1) * self.width / 2 if color(i): glVertex3f(self.width - x, float(i), 0.0) glVertex3f(x, float(i), 0.0) else: # Draw a rectangular grid - for i in range(0, int(math.ceil(self.width + 1))): + for i in numpy.arange(0, int(math.ceil(self.width + 1)), self.grid[0]): if color(i): glVertex3f(float(i), 0.0, 0.0) glVertex3f(float(i), self.depth, 0.0) - for i in range(0, int(math.ceil(self.depth + 1))): + for i in numpy.arange(0, int(math.ceil(self.depth + 1)), self.grid[0]): if color(i): glVertex3f(0, float(i), 0.0) glVertex3f(self.width, float(i), 0.0) @@ -174,7 +174,7 @@ # glCallList(self.display_list) self.draw() -class PrintHead(object): +class PrintHead: def __init__(self): self.color = (43. / 255, 0., 175. / 255, 1.0) self.scale = 5 @@ -209,7 +209,7 @@ glLineWidth(orig_linewidth) glDisable(GL_LINE_SMOOTH) -class Model(object): +class Model: """ Parent class for models that provides common functionality. """ @@ -315,6 +315,47 @@ gline_idx = 0 return None +def interpolate_arcs(gline, prev_gline): + if gline.command == "G2" or gline.command == "G3": + rx = gline.i if gline.i is not None else 0 + ry = gline.j if gline.j is not None else 0 + r = math.sqrt(rx*rx + ry*ry) + + cx = prev_gline.current_x + rx + cy = prev_gline.current_y + ry + + a_start = math.atan2(-ry, -rx) + dx = gline.current_x - cx + dy = gline.current_y - cy + a_end = math.atan2(dy, dx) + a_delta = a_end - a_start + + if gline.command == "G3" and a_delta <= 0: + a_delta += math.pi * 2 + elif gline.command == "G2" and a_delta >= 0: + a_delta -= math.pi * 2 + + z0 = prev_gline.current_z + dz = gline.current_z - z0 + + # max segment size: 0.5mm, max num of segments: 100 + segments = math.ceil(abs(a_delta) * r * 2 / 0.5) + if segments > 100: + segments = 100 + + for t in range(segments): + a = t / segments * a_delta + a_start + + mid = ( + cx + math.cos(a) * r, + cy + math.sin(a) * r, + z0 + t / segments * dz + ) + yield mid + + yield (gline.current_x, gline.current_y, gline.current_z) + + class GcodeModel(Model): """ Model for displaying Gcode data. @@ -363,6 +404,7 @@ # to store coordinates/colors/normals. # Nicely enough we have 3 per kind of thing for all kinds. coordspervertex = 3 + buffered_color_len = 3 # 4th color component (alpha) is ignored verticesperline = 8 coordsperline = coordspervertex * verticesperline coords_count = lambda nlines: nlines * coordsperline @@ -389,20 +431,19 @@ vertices = self.vertices = numpy.zeros(ncoords, dtype = GLfloat) vertex_k = 0 colors = self.colors = numpy.zeros(ncoords, dtype = GLfloat) + color_k = 0 normals = self.normals = numpy.zeros(ncoords, dtype = GLfloat) - normal_k = 0 indices = self.indices = numpy.zeros(nindices, dtype = GLuint) index_k = 0 self.layer_idxs_map = {} self.layer_stops = [0] - prev_is_extruding = False prev_move_normal_x = None prev_move_normal_y = None prev_move_angle = None - prev_pos = (0, 0, 0) + prev_gline = None layer_idx = 0 self.printed_until = 0 @@ -435,83 +476,122 @@ if gline.x is None and gline.y is None and gline.z is None: continue has_movement = True - current_pos = (gline.current_x, gline.current_y, gline.current_z) - if not gline.extruding: - travel_vertices[travel_vertex_k] = prev_pos[0] - travel_vertices[travel_vertex_k + 1] = prev_pos[1] - travel_vertices[travel_vertex_k + 2] = prev_pos[2] - travel_vertices[travel_vertex_k + 3] = current_pos[0] - travel_vertices[travel_vertex_k + 4] = current_pos[1] - travel_vertices[travel_vertex_k + 5] = current_pos[2] - travel_vertex_k += 6 - prev_is_extruding = False - else: - gline_color = self.movement_color(gline) + for current_pos in interpolate_arcs(gline, prev_gline): + if not gline.extruding: + if self.travels.size < (travel_vertex_k + 100 * 6): + # arc interpolation extra points allocation + # if not enough room for another 100 points now, + # allocate enough and 50% extra to minimize separate allocations + ratio = (travel_vertex_k + 100 * 6) / self.travels.size * 1.5 + # print(f"gl realloc travel {self.travels.size} -> {int(self.travels.size * ratio)}") + self.travels.resize(int(self.travels.size * ratio), refcheck = False) - next_move = get_next_move(model_data, layer_idx, gline_idx) - next_is_extruding = (next_move.extruding - if next_move is not None else False) + travel_vertices[travel_vertex_k:travel_vertex_k+3] = prev_pos + travel_vertices[travel_vertex_k + 3:travel_vertex_k + 6] = current_pos + travel_vertex_k += 6 + else: + delta_x = current_pos[0] - prev_pos[0] + delta_y = current_pos[1] - prev_pos[1] + norm = delta_x * delta_x + delta_y * delta_y + if norm == 0: # Don't draw anything if this move is Z+E only + continue + norm = math.sqrt(norm) + move_normal_x = - delta_y / norm + move_normal_y = delta_x / norm + move_angle = math.atan2(delta_y, delta_x) - delta_x = current_pos[0] - prev_pos[0] - delta_y = current_pos[1] - prev_pos[1] - norm = delta_x * delta_x + delta_y * delta_y - if norm == 0: # Don't draw anything if this move is Z+E only - continue - norm = math.sqrt(norm) - move_normal_x = - delta_y / norm - move_normal_y = delta_x / norm - move_angle = math.atan2(delta_y, delta_x) - - # FIXME: compute these dynamically - path_halfwidth = self.path_halfwidth * 1.2 - path_halfheight = self.path_halfheight * 1.2 + # FIXME: compute these dynamically + path_halfwidth = self.path_halfwidth * 1.2 + path_halfheight = self.path_halfheight * 1.2 - new_indices = [] - new_vertices = [] - new_normals = [] - if prev_is_extruding: - # Store previous vertices indices - prev_id = vertex_k / 3 - 4 - avg_move_normal_x = (prev_move_normal_x + move_normal_x) / 2 - avg_move_normal_y = (prev_move_normal_y + move_normal_y) / 2 - norm = avg_move_normal_x * avg_move_normal_x + avg_move_normal_y * avg_move_normal_y - if norm == 0: - avg_move_normal_x = move_normal_x - avg_move_normal_y = move_normal_y + new_indices = [] + new_vertices = [] + new_normals = [] + if prev_gline and prev_gline.extruding: + # Store previous vertices indices + prev_id = vertex_k // 3 - 4 + avg_move_normal_x = (prev_move_normal_x + move_normal_x) / 2 + avg_move_normal_y = (prev_move_normal_y + move_normal_y) / 2 + norm = avg_move_normal_x * avg_move_normal_x + avg_move_normal_y * avg_move_normal_y + if norm == 0: + avg_move_normal_x = move_normal_x + avg_move_normal_y = move_normal_y + else: + norm = math.sqrt(norm) + avg_move_normal_x /= norm + avg_move_normal_y /= norm + delta_angle = move_angle - prev_move_angle + delta_angle = (delta_angle + twopi) % twopi + fact = abs(math.cos(delta_angle / 2)) + # If move is turning too much, avoid creating a big peak + # by adding an intermediate box + if fact < 0.5: + # FIXME: It looks like there's some heavy code duplication here... + hw = path_halfwidth + p1x = prev_pos[0] - hw * prev_move_normal_x + p2x = prev_pos[0] + hw * prev_move_normal_x + p1y = prev_pos[1] - hw * prev_move_normal_y + p2y = prev_pos[1] + hw * prev_move_normal_y + new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] + path_halfheight)) + new_vertices.extend((p1x, p1y, prev_pos[2])) + new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] - path_halfheight)) + new_vertices.extend((p2x, p2y, prev_pos[2])) + new_normals.extend((0, 0, 1)) + new_normals.extend((-prev_move_normal_x, -prev_move_normal_y, 0)) + new_normals.extend((0, 0, -1)) + new_normals.extend((prev_move_normal_x, prev_move_normal_y, 0)) + first = vertex_k // 3 + # Link to previous + new_indices += triangulate_box(prev_id, prev_id + 1, + prev_id + 2, prev_id + 3, + first, first + 1, + first + 2, first + 3) + p1x = prev_pos[0] - hw * move_normal_x + p2x = prev_pos[0] + hw * move_normal_x + p1y = prev_pos[1] - hw * move_normal_y + p2y = prev_pos[1] + hw * move_normal_y + new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] + path_halfheight)) + new_vertices.extend((p1x, p1y, prev_pos[2])) + new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] - path_halfheight)) + new_vertices.extend((p2x, p2y, prev_pos[2])) + new_normals.extend((0, 0, 1)) + new_normals.extend((-move_normal_x, -move_normal_y, 0)) + new_normals.extend((0, 0, -1)) + new_normals.extend((move_normal_x, move_normal_y, 0)) + prev_id += 4 + first += 4 + # Link to previous + new_indices += triangulate_box(prev_id, prev_id + 1, + prev_id + 2, prev_id + 3, + first, first + 1, + first + 2, first + 3) + else: + hw = path_halfwidth / fact + # Compute vertices + p1x = prev_pos[0] - hw * avg_move_normal_x + p2x = prev_pos[0] + hw * avg_move_normal_x + p1y = prev_pos[1] - hw * avg_move_normal_y + p2y = prev_pos[1] + hw * avg_move_normal_y + new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] + path_halfheight)) + new_vertices.extend((p1x, p1y, prev_pos[2])) + new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] - path_halfheight)) + new_vertices.extend((p2x, p2y, prev_pos[2])) + new_normals.extend((0, 0, 1)) + new_normals.extend((-avg_move_normal_x, -avg_move_normal_y, 0)) + new_normals.extend((0, 0, -1)) + new_normals.extend((avg_move_normal_x, avg_move_normal_y, 0)) + first = vertex_k // 3 + # Link to previous + new_indices += triangulate_box(prev_id, prev_id + 1, + prev_id + 2, prev_id + 3, + first, first + 1, + first + 2, first + 3) else: - norm = math.sqrt(norm) - avg_move_normal_x /= norm - avg_move_normal_y /= norm - delta_angle = move_angle - prev_move_angle - delta_angle = (delta_angle + twopi) % twopi - fact = abs(math.cos(delta_angle / 2)) - # If move is turning too much, avoid creating a big peak - # by adding an intermediate box - if fact < 0.5: - # FIXME: It looks like there's some heavy code duplication here... - hw = path_halfwidth - p1x = prev_pos[0] - hw * prev_move_normal_x - p2x = prev_pos[0] + hw * prev_move_normal_x - p1y = prev_pos[1] - hw * prev_move_normal_y - p2y = prev_pos[1] + hw * prev_move_normal_y - new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] + path_halfheight)) - new_vertices.extend((p1x, p1y, prev_pos[2])) - new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] - path_halfheight)) - new_vertices.extend((p2x, p2y, prev_pos[2])) - new_normals.extend((0, 0, 1)) - new_normals.extend((-prev_move_normal_x, -prev_move_normal_y, 0)) - new_normals.extend((0, 0, -1)) - new_normals.extend((prev_move_normal_x, prev_move_normal_y, 0)) - first = vertex_k / 3 - # Link to previous - new_indices += triangulate_box(prev_id, prev_id + 1, - prev_id + 2, prev_id + 3, - first, first + 1, - first + 2, first + 3) - p1x = prev_pos[0] - hw * move_normal_x - p2x = prev_pos[0] + hw * move_normal_x - p1y = prev_pos[1] - hw * move_normal_y - p2y = prev_pos[1] + hw * move_normal_y + # Compute vertices normal to the current move and cap it + p1x = prev_pos[0] - path_halfwidth * move_normal_x + p2x = prev_pos[0] + path_halfwidth * move_normal_x + p1y = prev_pos[1] - path_halfwidth * move_normal_y + p2y = prev_pos[1] + path_halfwidth * move_normal_y new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] + path_halfheight)) new_vertices.extend((p1x, p1y, prev_pos[2])) new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] - path_halfheight)) @@ -520,97 +600,68 @@ new_normals.extend((-move_normal_x, -move_normal_y, 0)) new_normals.extend((0, 0, -1)) new_normals.extend((move_normal_x, move_normal_y, 0)) - prev_id += 4 - first += 4 - # Link to previous - new_indices += triangulate_box(prev_id, prev_id + 1, - prev_id + 2, prev_id + 3, - first, first + 1, - first + 2, first + 3) - else: - hw = path_halfwidth / fact - # Compute vertices - p1x = prev_pos[0] - hw * avg_move_normal_x - p2x = prev_pos[0] + hw * avg_move_normal_x - p1y = prev_pos[1] - hw * avg_move_normal_y - p2y = prev_pos[1] + hw * avg_move_normal_y - new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] + path_halfheight)) - new_vertices.extend((p1x, p1y, prev_pos[2])) - new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] - path_halfheight)) - new_vertices.extend((p2x, p2y, prev_pos[2])) + first = vertex_k // 3 + new_indices = triangulate_rectangle(first, first + 1, + first + 2, first + 3) + + next_move = get_next_move(model_data, layer_idx, gline_idx) + next_is_extruding = next_move and next_move.extruding + if not next_is_extruding: + # Compute caps and link everything + p1x = current_pos[0] - path_halfwidth * move_normal_x + p2x = current_pos[0] + path_halfwidth * move_normal_x + p1y = current_pos[1] - path_halfwidth * move_normal_y + p2y = current_pos[1] + path_halfwidth * move_normal_y + new_vertices.extend((current_pos[0], current_pos[1], current_pos[2] + path_halfheight)) + new_vertices.extend((p1x, p1y, current_pos[2])) + new_vertices.extend((current_pos[0], current_pos[1], current_pos[2] - path_halfheight)) + new_vertices.extend((p2x, p2y, current_pos[2])) new_normals.extend((0, 0, 1)) - new_normals.extend((-avg_move_normal_x, -avg_move_normal_y, 0)) + new_normals.extend((-move_normal_x, -move_normal_y, 0)) new_normals.extend((0, 0, -1)) - new_normals.extend((avg_move_normal_x, avg_move_normal_y, 0)) - first = vertex_k / 3 - # Link to previous - new_indices += triangulate_box(prev_id, prev_id + 1, - prev_id + 2, prev_id + 3, - first, first + 1, - first + 2, first + 3) - else: - # Compute vertices normal to the current move and cap it - p1x = prev_pos[0] - path_halfwidth * move_normal_x - p2x = prev_pos[0] + path_halfwidth * move_normal_x - p1y = prev_pos[1] - path_halfwidth * move_normal_y - p2y = prev_pos[1] + path_halfwidth * move_normal_y - new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] + path_halfheight)) - new_vertices.extend((p1x, p1y, prev_pos[2])) - new_vertices.extend((prev_pos[0], prev_pos[1], prev_pos[2] - path_halfheight)) - new_vertices.extend((p2x, p2y, prev_pos[2])) - new_normals.extend((0, 0, 1)) - new_normals.extend((-move_normal_x, -move_normal_y, 0)) - new_normals.extend((0, 0, -1)) - new_normals.extend((move_normal_x, move_normal_y, 0)) - first = vertex_k / 3 - new_indices = triangulate_rectangle(first, first + 1, - first + 2, first + 3) + new_normals.extend((move_normal_x, move_normal_y, 0)) + end_first = vertex_k // 3 + len(new_vertices) // 3 - 4 + new_indices += triangulate_rectangle(end_first + 3, end_first + 2, + end_first + 1, end_first) + new_indices += triangulate_box(first, first + 1, + first + 2, first + 3, + end_first, end_first + 1, + end_first + 2, end_first + 3) - if not next_is_extruding: - # Compute caps and link everything - p1x = current_pos[0] - path_halfwidth * move_normal_x - p2x = current_pos[0] + path_halfwidth * move_normal_x - p1y = current_pos[1] - path_halfwidth * move_normal_y - p2y = current_pos[1] + path_halfwidth * move_normal_y - new_vertices.extend((current_pos[0], current_pos[1], current_pos[2] + path_halfheight)) - new_vertices.extend((p1x, p1y, current_pos[2])) - new_vertices.extend((current_pos[0], current_pos[1], current_pos[2] - path_halfheight)) - new_vertices.extend((p2x, p2y, current_pos[2])) - new_normals.extend((0, 0, 1)) - new_normals.extend((-move_normal_x, -move_normal_y, 0)) - new_normals.extend((0, 0, -1)) - new_normals.extend((move_normal_x, move_normal_y, 0)) - end_first = vertex_k / 3 + len(new_vertices) / 3 - 4 - new_indices += triangulate_rectangle(end_first + 3, end_first + 2, - end_first + 1, end_first) - new_indices += triangulate_box(first, first + 1, - first + 2, first + 3, - end_first, end_first + 1, - end_first + 2, end_first + 3) + if self.indices.size < (index_k + len(new_indices) + 100 * indicesperline): + # arc interpolation extra points allocation + ratio = (index_k + len(new_indices) + 100 * indicesperline) / self.indices.size * 1.5 + # print(f"gl realloc print {self.vertices.size} -> {int(self.vertices.size * ratio)}") + self.vertices.resize(int(self.vertices.size * ratio), refcheck = False) + self.colors.resize(int(self.colors.size * ratio), refcheck = False) + self.normals.resize(int(self.normals.size * ratio), refcheck = False) + self.indices.resize(int(self.indices.size * ratio), refcheck = False) + + for new_i, item in enumerate(new_indices): + indices[index_k + new_i] = item + index_k += len(new_indices) - for new_i, item in enumerate(new_indices): - indices[index_k + new_i] = item - index_k += len(new_indices) - for new_i, item in enumerate(new_vertices): - vertices[vertex_k + new_i] = item - vertex_k += len(new_vertices) - for new_i, item in enumerate(new_normals): - normals[normal_k + new_i] = item - normal_k += len(new_normals) - new_colors = list(gline_color)[:-1] * (len(new_vertices) / 3) - for new_i, item in enumerate(new_colors): - colors[color_k + new_i] = item - color_k += len(new_colors) + new_vertices_len = len(new_vertices) + vertices[vertex_k:vertex_k+new_vertices_len] = new_vertices + normals[vertex_k:vertex_k+new_vertices_len] = new_normals + vertex_k += new_vertices_len - prev_is_extruding = True - prev_move_normal_x = move_normal_x - prev_move_normal_y = move_normal_y - prev_move_angle = move_angle + new_vertices_count = new_vertices_len//coordspervertex + # settings support alpha (transperancy), but it is ignored here + gline_color = self.movement_color(gline)[:buffered_color_len] + for vi in range(new_vertices_count): + colors[color_k:color_k+buffered_color_len] = gline_color + color_k += buffered_color_len - prev_pos = current_pos - count_travel_indices.append(travel_vertex_k / 3) + prev_move_normal_x = move_normal_x + prev_move_normal_y = move_normal_y + prev_move_angle = move_angle + + prev_pos = current_pos + prev_gline = gline + count_travel_indices.append(travel_vertex_k // 3) count_print_indices.append(index_k) - count_print_vertices.append(vertex_k / 3) + count_print_vertices.append(vertex_k // 3) gline.gcview_end_vertex = len(count_print_indices) - 1 if has_movement: @@ -637,7 +688,7 @@ self.travels.resize(travel_vertex_k, refcheck = False) self.vertices.resize(vertex_k, refcheck = False) self.colors.resize(color_k, refcheck = False) - self.normals.resize(normal_k, refcheck = False) + self.normals.resize(vertex_k, refcheck = False) self.indices.resize(index_k, refcheck = False) self.layer_stops = array.array('L', self.layer_stops) @@ -655,7 +706,7 @@ t_end = time.time() logging.debug(_('Initialized 3D visualization in %.2f seconds') % (t_end - t_start)) - logging.debug(_('Vertex count: %d') % ((len(self.vertices) + len(self.travels)) / 3)) + logging.debug(_('Vertex count: %d') % ((len(self.vertices) + len(self.travels)) // 3)) yield None def copy(self): @@ -673,6 +724,24 @@ copy.initialized = False return copy + def update_colors(self): + """Rebuild gl color buffer without loading. Used after color settings edit""" + ncoords = self.count_print_vertices[-1] + colors = numpy.empty(ncoords*3, dtype = GLfloat) + cur_vertex = 0 + gline_i = 1 + for gline in self.gcode.lines: + if gline.gcview_end_vertex: + gline_color = self.movement_color(gline)[:3] + last_vertex = self.count_print_vertices[gline_i] + gline_i += 1 + while cur_vertex < last_vertex: + colors[cur_vertex*3:cur_vertex*3+3] = gline_color + cur_vertex += 1 + if self.vertex_color_buffer: + self.vertex_color_buffer.delete() + self.vertex_color_buffer = numpy2vbo(colors, use_vbos = self.use_vbos) + # ------------------------------------------------------------------------ # DRAWING # ------------------------------------------------------------------------ @@ -869,10 +938,11 @@ color_k = 0 self.printed_until = -1 self.only_current = False + prev_gline = None while layer_idx < len(model_data.all_layers): with self.lock: nlines = len(model_data) - if nlines * 6 != vertices.size: + if nlines * 6 > vertices.size: self.vertices.resize(nlines * 6, refcheck = False) self.colors.resize(nlines * 8, refcheck = False) layer = model_data.all_layers[layer_idx] @@ -882,32 +952,43 @@ continue if gline.x is None and gline.y is None and gline.z is None: continue + has_movement = True - vertices[vertex_k] = prev_pos[0] - vertices[vertex_k + 1] = prev_pos[1] - vertices[vertex_k + 2] = prev_pos[2] - current_pos = (gline.current_x, gline.current_y, gline.current_z) - vertices[vertex_k + 3] = current_pos[0] - vertices[vertex_k + 4] = current_pos[1] - vertices[vertex_k + 5] = current_pos[2] - vertex_k += 6 + for current_pos in interpolate_arcs(gline, prev_gline): + + if self.vertices.size < (vertex_k + 100 * 6): + # arc interpolation extra points allocation + ratio = (vertex_k + 100 * 6) / self.vertices.size * 1.5 + # print(f"gl realloc lite {self.vertices.size} -> {int(self.vertices.size * ratio)}") + self.vertices.resize(int(self.vertices.size * ratio), refcheck = False) + self.colors.resize(int(self.colors.size * ratio), refcheck = False) + - vertex_color = self.movement_color(gline) - colors[color_k] = vertex_color[0] - colors[color_k + 1] = vertex_color[1] - colors[color_k + 2] = vertex_color[2] - colors[color_k + 3] = vertex_color[3] - colors[color_k + 4] = vertex_color[0] - colors[color_k + 5] = vertex_color[1] - colors[color_k + 6] = vertex_color[2] - colors[color_k + 7] = vertex_color[3] - color_k += 8 + vertices[vertex_k] = prev_pos[0] + vertices[vertex_k + 1] = prev_pos[1] + vertices[vertex_k + 2] = prev_pos[2] + vertices[vertex_k + 3] = current_pos[0] + vertices[vertex_k + 4] = current_pos[1] + vertices[vertex_k + 5] = current_pos[2] + vertex_k += 6 - prev_pos = current_pos - gline.gcview_end_vertex = vertex_k / 3 + vertex_color = self.movement_color(gline) + colors[color_k] = vertex_color[0] + colors[color_k + 1] = vertex_color[1] + colors[color_k + 2] = vertex_color[2] + colors[color_k + 3] = vertex_color[3] + colors[color_k + 4] = vertex_color[0] + colors[color_k + 5] = vertex_color[1] + colors[color_k + 6] = vertex_color[2] + colors[color_k + 7] = vertex_color[3] + color_k += 8 + + prev_pos = current_pos + prev_gline = gline + gline.gcview_end_vertex = vertex_k // 3 if has_movement: - self.layer_stops.append(vertex_k / 3) + self.layer_stops.append(vertex_k // 3) self.layer_idxs_map[layer_idx] = len(self.layer_stops) - 1 self.max_layers = len(self.layer_stops) - 1 self.num_layers_to_draw = self.max_layers + 1 @@ -936,7 +1017,7 @@ t_end = time.time() logging.debug(_('Initialized 3D visualization in %.2f seconds') % (t_end - t_start)) - logging.debug(_('Vertex count: %d') % (len(self.vertices) / 3)) + logging.debug(_('Vertex count: %d') % (len(self.vertices) // 3)) yield None def copy(self): diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gl/panel.py --- a/printrun-src/printrun/gl/panel.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gl/panel.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # This file is part of the Printrun suite. # # Printrun is free software: you can redistribute it and/or modify @@ -39,81 +37,136 @@ GL_MODELVIEW_MATRIX, GL_ONE_MINUS_SRC_ALPHA, glOrtho, \ GL_PROJECTION, GL_PROJECTION_MATRIX, glScalef, \ GL_SRC_ALPHA, glTranslatef, gluPerspective, gluUnProject, \ - glViewport, GL_VIEWPORT + glViewport, GL_VIEWPORT, glPushMatrix, glPopMatrix, \ + glBegin, glVertex2f, glVertex3f, glEnd, GL_LINE_LOOP, glColor3f, \ + GL_LINE_STIPPLE, glColor4f, glLineStipple + from pyglet import gl -from .trackball import trackball, mulquat +from .trackball import trackball, mulquat, axis_to_quat from .libtatlin.actors import vec +from pyglet.gl.glu import gluOrtho2D -class wxGLPanel(wx.Panel): +# When Subclassing wx.Window in Windows the focus goes to the wx.Window +# instead of GLCanvas and it does not draw the focus rectangle and +# does not consume used keystrokes +# BASE_CLASS = wx.Window +# Subclassing Panel solves problem In Windows +BASE_CLASS = wx.Panel +# BASE_CLASS = wx.ScrolledWindow +# BASE_CLASS = glcanvas.GLCanvas +class wxGLPanel(BASE_CLASS): '''A simple class for using OpenGL with wxPython.''' + orbit_control = True orthographic = True color_background = (0.98, 0.98, 0.78, 1) do_lights = True - def __init__(self, parent, id, pos = wx.DefaultPosition, + def __init__(self, parent, pos = wx.DefaultPosition, size = wx.DefaultSize, style = 0, antialias_samples = 0): - # Forcing a no full repaint to stop flickering - style = style | wx.NO_FULL_REPAINT_ON_RESIZE - super(wxGLPanel, self).__init__(parent, id, pos, size, style) + # Full repaint should not be a performance problem + #TODO: test on windows, tested in Ubuntu + style = style | wx.FULL_REPAINT_ON_RESIZE self.GLinitialized = False self.mview_initialized = False - attribList = (glcanvas.WX_GL_RGBA, # RGBA + attribList = [glcanvas.WX_GL_RGBA, # RGBA glcanvas.WX_GL_DOUBLEBUFFER, # Double Buffered - glcanvas.WX_GL_DEPTH_SIZE, 24) # 24 bit + glcanvas.WX_GL_DEPTH_SIZE, 24 # 24 bit + ] if antialias_samples > 0 and hasattr(glcanvas, "WX_GL_SAMPLE_BUFFERS"): attribList += (glcanvas.WX_GL_SAMPLE_BUFFERS, 1, glcanvas.WX_GL_SAMPLES, antialias_samples) - self.width = None - self.height = None + attribList.append(0) - self.sizer = wx.BoxSizer(wx.HORIZONTAL) - self.canvas = glcanvas.GLCanvas(self, attribList = attribList) + if BASE_CLASS is glcanvas.GLCanvas: + super().__init__(parent, wx.ID_ANY, attribList, pos, size, style) + self.canvas = self + else: + super().__init__(parent, wx.ID_ANY, pos, size, style) + self.canvas = glcanvas.GLCanvas(self, wx.ID_ANY, attribList, pos, size, style) + + self.width = self.height = None + self.context = glcanvas.GLContext(self.canvas) - self.sizer.Add(self.canvas, 1, wx.EXPAND) - self.SetSizerAndFit(self.sizer) self.rot_lock = Lock() self.basequat = [0, 0, 0, 1] self.zoom_factor = 1.0 + self.angle_z = 0 + self.angle_x = 0 self.gl_broken = False # bind events + self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent) + if self.canvas is not self: + self.Bind(wx.EVT_SIZE, self.OnScrollSize) + # do not focus parent (panel like) but its canvas + self.SetCanFocus(False) + self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent) - self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent) + # In wxWidgets 3.0.x there is a clipping bug during resizing + # which could be affected by painting the container + # self.Bind(wx.EVT_PAINT, self.processPaintEvent) + # Upgrade to wxPython 4.1 recommended self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent) + self.canvas.Bind(wx.EVT_SET_FOCUS, self.processFocus) + self.canvas.Bind(wx.EVT_KILL_FOCUS, self.processKillFocus) + + def processFocus(self, ev): + # print('processFocus') + self.Refresh(False) + ev.Skip() + + def processKillFocus(self, ev): + # print('processKillFocus') + self.Refresh(False) + ev.Skip() + # def processIdle(self, event): + # print('processIdle') + # event.Skip() + + def Layout(self): + return super().Layout() + + def Refresh(self, eraseback=True): + # print('Refresh') + return super().Refresh(eraseback) + + def OnScrollSize(self, event): + self.canvas.SetSize(event.Size) + def processEraseBackgroundEvent(self, event): '''Process the erase background event.''' pass # Do nothing, to avoid flashing on MSWin def processSizeEvent(self, event): '''Process the resize event.''' - if self.IsFrozen(): - event.Skip() - return - if (wx.VERSION > (2, 9) and self.canvas.IsShownOnScreen()) or self.canvas.GetContext(): + + # print('processSizeEvent frozen', self.IsFrozen(), event.Size.x, self.ClientSize.x) + if not self.IsFrozen() and self.canvas.IsShownOnScreen(): # Make sure the frame is shown before calling SetCurrent. self.canvas.SetCurrent(self.context) self.OnReshape() - self.Refresh(False) - timer = wx.CallLater(100, self.Refresh) - timer.Start() + + # self.Refresh(False) + # print('Refresh') event.Skip() def processPaintEvent(self, event): '''Process the drawing event.''' + # print('wxGLPanel.processPaintEvent', self.ClientSize.Width) self.canvas.SetCurrent(self.context) if not self.gl_broken: try: self.OnInitGL() - self.OnDraw() + self.DrawCanvas() except pyglet.gl.lib.GLException: self.gl_broken = True logging.error(_("OpenGL failed, disabling it:") @@ -124,7 +177,7 @@ # clean up the pyglet OpenGL context self.pygletcontext.destroy() # call the super method - super(wxGLPanel, self).Destroy() + super().Destroy() # ========================================================================== # GLFrame OpenGL Event Handlers @@ -160,6 +213,7 @@ self.width = max(float(width), 1.0) self.height = max(float(height), 1.0) self.OnInitGL(call_reshape = False) + # print('glViewport', width) glViewport(0, 0, width, height) glMatrixMode(GL_PROJECTION) glLoadIdentity() @@ -223,13 +277,46 @@ self.zoomed_height = hratio / minratio glScalef(factor * minratio, factor * minratio, 1) - def OnDraw(self, *args, **kwargs): + def DrawCanvas(self): """Draw the window.""" + #import time + #start = time.perf_counter() + # print('DrawCanvas', self.canvas.GetClientRect()) self.pygletcontext.set_current() glClearColor(*self.color_background) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) self.draw_objects() + + if self.canvas.HasFocus(): + self.drawFocus() self.canvas.SwapBuffers() + #print('Draw took', '%.2f'%(time.perf_counter()-start)) + + def drawFocus(self): + glColor4f(0, 0, 0, 0.4) + + glPushMatrix() + glLoadIdentity() + + glMatrixMode(GL_PROJECTION) + glPushMatrix() + glLoadIdentity() + gluOrtho2D(0, self.width, 0, self.height) + + glLineStipple(1, 0xf0f0) + glEnable(GL_LINE_STIPPLE) + glBegin(GL_LINE_LOOP) + glVertex2f(1, 0) + glVertex2f(self.width, 0) + glVertex2f(self.width, self.height-1) + glVertex2f(1, self.height-1) + glEnd() + glDisable(GL_LINE_STIPPLE) + + glPopMatrix() # restore PROJECTION + + glMatrixMode(GL_MODELVIEW) + glPopMatrix() # ========================================================================== # To be implemented by a sub class @@ -317,35 +404,54 @@ self.zoom_factor *= factor if to: glTranslatef(-delta_x, -delta_y, 0) - wx.CallAfter(self.Refresh) + # For wxPython (<4.1) and GTK: + # when you resize (enlarge) 3d view fast towards the log pane + # sash garbage may remain in GLCanvas + # The following refresh clears it at the cost of + # doubled frame draws. + # wx.CallAfter(self.Refresh) + self.Refresh(False) def zoom_to_center(self, factor): self.canvas.SetCurrent(self.context) x, y, _ = self.mouse_to_3d(self.width / 2, self.height / 2) self.zoom(factor, (x, y)) + def orbit(self, p1x, p1y, p2x, p2y): + rz = p2x-p1x + self.angle_z-=rz + rotz = axis_to_quat([0.0,0.0,1.0],self.angle_z) + + rx = p2y-p1y + self.angle_x+=rx + rota = axis_to_quat([1.0,0.0,0.0],self.angle_x) + return mulquat(rotz,rota) + def handle_rotation(self, event): if self.initpos is None: - self.initpos = event.GetPositionTuple() + self.initpos = event.GetPosition() else: p1 = self.initpos - p2 = event.GetPositionTuple() + p2 = event.GetPosition() sz = self.GetClientSize() - p1x = float(p1[0]) / (sz[0] / 2) - 1 - p1y = 1 - float(p1[1]) / (sz[1] / 2) - p2x = float(p2[0]) / (sz[0] / 2) - 1 - p2y = 1 - float(p2[1]) / (sz[1] / 2) + p1x = p1[0] / (sz[0] / 2) - 1 + p1y = 1 - p1[1] / (sz[1] / 2) + p2x = p2[0] / (sz[0] / 2) - 1 + p2y = 1 - p2[1] / (sz[1] / 2) quat = trackball(p1x, p1y, p2x, p2y, self.dist / 250.0) with self.rot_lock: - self.basequat = mulquat(self.basequat, quat) + if self.orbit_control: + self.basequat = self.orbit(p1x, p1y, p2x, p2y) + else: + self.basequat = mulquat(self.basequat, quat) self.initpos = p2 def handle_translation(self, event): if self.initpos is None: - self.initpos = event.GetPositionTuple() + self.initpos = event.GetPosition() else: p1 = self.initpos - p2 = event.GetPositionTuple() + p2 = event.GetPosition() if self.orthographic: x1, y1, _ = self.mouse_to_3d(p1[0], p1[1]) x2, y2, _ = self.mouse_to_3d(p2[0], p2[1]) diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gl/trackball.py --- a/printrun-src/printrun/gl/trackball.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gl/trackball.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # This file is part of the Printrun suite. # # Printrun is free software: you can redistribute it and/or modify @@ -35,7 +33,7 @@ a = cross(p2, p1) d = map(lambda x, y: x - y, p1, p2) - t = math.sqrt(sum(map(lambda x: x * x, d))) / (2.0 * TRACKBALLSIZE) + t = math.sqrt(sum(x * x for x in d)) / (2.0 * TRACKBALLSIZE) if t > 1.0: t = 1.0 @@ -46,9 +44,9 @@ return axis_to_quat(a, phi) def axis_to_quat(a, phi): - lena = math.sqrt(sum(map(lambda x: x * x, a))) - q = map(lambda x: x * (1 / lena), a) - q = map(lambda x: x * math.sin(phi / 2.0), q) + lena = math.sqrt(sum(x * x for x in a)) + q = [x * (1 / lena) for x in a] + q = [x * math.sin(phi / 2.0) for x in q] q.append(math.cos(phi / 2.0)) return q diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gui/__init__.py --- a/printrun-src/printrun/gui/__init__.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gui/__init__.py Wed Jan 20 10:15:13 2021 +0100 @@ -17,8 +17,10 @@ try: import wx + if wx.VERSION < (4,): + raise ImportError() except: - logging.error(_("WX is not installed. This program requires WX to run.")) + logging.error(_("WX >= 4 is not installed. This program requires WX >= 4 to run.")) raise from printrun.utils import install_locale @@ -33,13 +35,14 @@ def __init__(self, root, label, parentpanel, parentsizers): super(ToggleablePane, self).__init__(wx.HORIZONTAL) - if not parentpanel: parentpanel = root.panel + if not parentpanel: + parentpanel = root.panel self.root = root self.visible = True self.parentpanel = parentpanel self.parentsizers = parentsizers self.panepanel = root.newPanel(parentpanel) - self.button = wx.Button(parentpanel, -1, label, size = (22, 18), style = wx.BU_EXACTFIT) + self.button = wx.Button(parentpanel, -1, label, size = (35, 18), style = wx.BU_EXACTFIT) self.button.Bind(wx.EVT_BUTTON, self.toggle) def toggle(self, event): @@ -54,7 +57,7 @@ class LeftPaneToggleable(ToggleablePane): def __init__(self, root, parentpanel, parentsizers): - super(LeftPaneToggleable, self).__init__(root, "<", parentpanel, parentsizers) + super().__init__(root, "<", parentpanel, parentsizers) self.Add(self.panepanel, 0, wx.EXPAND) self.Add(self.button, 0) @@ -89,8 +92,9 @@ self.splitter.SetSashPosition(self.splitter.GetSize()[0] - self.orig_width) self.splitter.SetMinimumPaneSize(self.orig_min_size) self.splitter.SetSashGravity(self.orig_gravity) - if hasattr(self.splitter, "SetSashSize"): self.splitter.SetSashSize(self.orig_sash_size) - if hasattr(self.splitter, "SetSashInvisible"): self.splitter.SetSashInvisible(False) + if getattr(self.splitter, 'SetSashSize', False): + self.splitter.SetSashSize(self.orig_sash_size) + getattr(self.splitter, 'SetSashInvisible', bool)(False) for sizer in self.parentsizers: sizer.Layout() @@ -103,20 +107,20 @@ self.splitter.SetMinimumPaneSize(button_width) self.splitter.SetSashGravity(1) self.splitter.SetSashPosition(self.splitter.GetSize()[0] - button_width) - if hasattr(self.splitter, "SetSashSize"): + if getattr(self.splitter, 'SetSashSize', False): self.orig_sash_size = self.splitter.GetSashSize() self.splitter.SetSashSize(0) - if hasattr(self.splitter, "SetSashInvisible"): self.splitter.SetSashInvisible(True) + getattr(self.splitter, 'SetSashInvisible', bool)(True) for sizer in self.parentsizers: sizer.Layout() class MainWindow(wx.Frame): def __init__(self, *args, **kwargs): - super(MainWindow, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # this list will contain all controls that should be only enabled # when we're connected to a printer - self.panel = wx.Panel(self, -1) + self.panel = wx.Panel(self) self.reset_ui() self.statefulControls = [] @@ -131,7 +135,8 @@ def registerPanel(self, panel, add_to_list = True): panel.SetBackgroundColour(self.bgcolor) - if add_to_list: self.panels.append(panel) + if add_to_list: + self.panels.append(panel) def createTabbedGui(self): self.notesizer = wx.BoxSizer(wx.VERTICAL) @@ -199,12 +204,9 @@ self.notebook.AddPage(page4panel, _("G-Code Plater")) self.panel.SetSizer(self.notesizer) self.panel.Bind(wx.EVT_MOUSE_EVENTS, self.editbutton) - self.Bind(wx.EVT_CLOSE, self.kill) # Custom buttons - if wx.VERSION > (2, 9): self.cbuttonssizer = wx.WrapSizer(wx.HORIZONTAL) - else: self.cbuttonssizer = wx.GridBagSizer() - self.cbuttonssizer = wx.GridBagSizer() + self.cbuttonssizer = wx.WrapSizer(wx.HORIZONTAL) self.centerpanel = self.newPanel(page1panel2) self.centerpanel.SetSizer(self.cbuttonssizer) rightsizer.Add(self.centerpanel, 0, wx.ALIGN_CENTER) @@ -237,11 +239,15 @@ left_sizer.Add(controls_panel, 1, wx.EXPAND) left_pane.set_sizer(left_sizer) self.lowersizer.Add(leftpanel, 0, wx.EXPAND) - if not compact: # Use a splitterwindow to group viz and log + if compact: + vizpanel = self.newPanel(lowerpanel) + logpanel = self.newPanel(left_real_panel) + else: + # Use a splitterwindow to group viz and log rightpanel = self.newPanel(lowerpanel) rightsizer = wx.BoxSizer(wx.VERTICAL) rightpanel.SetSizer(rightsizer) - self.splitterwindow = wx.SplitterWindow(rightpanel, style = wx.SP_3D) + self.splitterwindow = wx.SplitterWindow(rightpanel, style = wx.SP_3D | wx.SP_LIVE_UPDATE) self.splitterwindow.SetMinimumPaneSize(150) self.splitterwindow.SetSashGravity(0.8) rightsizer.Add(self.splitterwindow, 1, wx.EXPAND) @@ -250,13 +256,9 @@ self.splitterwindow.SplitVertically(vizpanel, logpanel, self.settings.last_sash_position) self.splitterwindow.shrinked = False - else: - vizpanel = self.newPanel(lowerpanel) - logpanel = self.newPanel(left_real_panel) viz_pane = VizPane(self, vizpanel) # Custom buttons - if wx.VERSION > (2, 9): self.cbuttonssizer = wx.WrapSizer(wx.HORIZONTAL) - else: self.cbuttonssizer = wx.GridBagSizer() + self.cbuttonssizer = wx.WrapSizer(wx.HORIZONTAL) self.centerpanel = self.newPanel(vizpanel) self.centerpanel.SetSizer(self.cbuttonssizer) viz_pane.Add(self.centerpanel, 0, flag = wx.ALIGN_CENTER) @@ -267,16 +269,15 @@ log_pane = LogPaneToggleable(self, logpanel, [self.lowersizer]) left_pane.parentsizers.append(self.splitterwindow) logpanel.SetSizer(log_pane) - if not compact: - self.lowersizer.Add(rightpanel, 1, wx.EXPAND) - else: + if compact: left_sizer.Add(logpanel, 1, wx.EXPAND) self.lowersizer.Add(vizpanel, 1, wx.EXPAND) + else: + self.lowersizer.Add(rightpanel, 1, wx.EXPAND) self.mainsizer.Add(upperpanel, 0, wx.EXPAND) self.mainsizer.Add(lowerpanel, 1, wx.EXPAND) self.panel.SetSizer(self.mainsizer) self.panel.Bind(wx.EVT_MOUSE_EVENTS, self.editbutton) - self.Bind(wx.EVT_CLOSE, self.kill) self.mainsizer.Layout() # This prevents resizing below a reasonnable value diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gui/bufferedcanvas.py --- a/printrun-src/printrun/gui/bufferedcanvas.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gui/bufferedcanvas.py Wed Jan 20 10:15:13 2021 +0100 @@ -90,7 +90,7 @@ self.Refresh() def getWidthHeight(self): - width, height = self.GetClientSizeTuple() + width, height = self.GetClientSize() if width == 0: width = 1 if height == 0: @@ -103,7 +103,7 @@ def onPaint(self, event): # Blit the front buffer to the screen - w, h = self.GetClientSizeTuple() + w, h = self.GetClientSize() if not w or not h: return else: diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gui/controls.py --- a/printrun-src/printrun/gui/controls.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gui/controls.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,6 +1,3 @@ -# FILE MODIFIED BY NEOSOFT - MALTE DI DONATO -# Embed Lasercut controls - # This file is part of the Printrun suite. # # Printrun is free software: you can redistribute it and/or modify @@ -26,18 +23,16 @@ from .utils import make_button, make_custom_button -from .widgets import PronterOptions - class XYZControlsSizer(wx.GridBagSizer): def __init__(self, root, parentpanel = None): super(XYZControlsSizer, self).__init__() if not parentpanel: parentpanel = root.panel root.xyb = XYButtons(parentpanel, root.moveXY, root.homeButtonClicked, root.spacebarAction, root.bgcolor, zcallback=root.moveZ) + root.xyb.SetToolTip(_('[J]og controls. (Shift)+TAB ESC Shift/Ctrl+(arrows PgUp/PgDn)')) self.Add(root.xyb, pos = (0, 1), flag = wx.ALIGN_CENTER) root.zb = ZButtons(parentpanel, root.moveZ, root.bgcolor) self.Add(root.zb, pos = (0, 2), flag = wx.ALIGN_CENTER) - wx.CallAfter(root.xyb.SetFocus) def add_extra_controls(self, root, parentpanel, extra_buttons = None, mini_mode = False): standalone_mode = extra_buttons is not None @@ -55,8 +50,6 @@ else: e_base_line = base_line + 2 - lasercut_base_line = 11 - pos_mapping = { "htemp_label": (base_line + 0, 0), "htemp_off": (base_line + 0, 2), @@ -75,14 +68,6 @@ "tempdisp": (tempdisp_line, 0), "extrude": (3, 0), "reverse": (3, 2), - "lasercut_optionsbtn": (lasercut_base_line, 0), - "lasercut_printbtn": (lasercut_base_line, 2), - "lasercut_material_thickness": (lasercut_base_line+1, 4), - "lasercut_material_thickness_label": (lasercut_base_line+1, 0), - "lasercut_pass_count": (lasercut_base_line+2, 4), - "lasercut_pass_count_label": (lasercut_base_line+2, 0), - "lasercut_pass_zdiff": (lasercut_base_line+3, 4), - "lasercut_pass_zdiff_label": (lasercut_base_line+3, 0), } span_mapping = { @@ -106,8 +91,8 @@ } if standalone_mode: - pos_mapping["tempgraph"] = (base_line + 5, 0) - span_mapping["tempgraph"] = (5, 6) + pos_mapping["tempgraph"] = (base_line + 6, 0) + span_mapping["tempgraph"] = (3, 2) elif mini_mode: pos_mapping["tempgraph"] = (base_line + 2, 0) span_mapping["tempgraph"] = (1, 5) @@ -140,46 +125,17 @@ container = self container.Add(widget, *args, **kwargs) - # Lasercutter quick controls # - - root.lc_optionsbtn = make_button(parentpanel, _("Lasercutter options"), lambda e: PronterOptions(root, "Laser"), _("Open Lasercutter options"), style = wx.BU_EXACTFIT) - root.printerControls.append(root.lc_optionsbtn) - add("lasercut_optionsbtn", root.lc_optionsbtn, flag = wx.EXPAND) - - root.lc_printbtn = make_button(parentpanel, _("Start cutting"), root.on_lc_printfile, _("Start printjob with lasercutting features"), style = wx.BU_EXACTFIT) - root.printerControls.append(root.lc_printbtn) - add("lasercut_printbtn", root.lc_printbtn, flag = wx.EXPAND) - - - root.lc_pass_count = speed_spin = FloatSpin(parentpanel, -1, value = root.settings.lc_pass_count, min_val = 1, max_val = 10, digits = 0, style = wx.ALIGN_LEFT, size = (80, -1)) - add("lasercut_pass_count", root.lc_pass_count) - add("lasercut_pass_count_label", wx.StaticText(parentpanel, -1, _("Number of cutting passes:")), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT) - - root.lc_pass_zdiff = speed_spin = FloatSpin(parentpanel, -1, increment = 0.1, value = root.settings.lc_pass_zdiff, min_val = -2, max_val = 2, digits = 1, style = wx.ALIGN_LEFT, size = (80, -1)) - add("lasercut_pass_zdiff", root.lc_pass_zdiff) - add("lasercut_pass_zdiff_label", wx.StaticText(parentpanel, -1, _("Z movement after each cut:")), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT) - - root.lc_material_thickness = speed_spin = FloatSpin(parentpanel, -1, increment = 0.1, value = root.settings.lc_material_thickness, min_val = 0, max_val = 75, digits = 1, style = wx.ALIGN_LEFT, size = (80, -1)) - add("lasercut_material_thickness", root.lc_material_thickness) - add("lasercut_material_thickness_label", wx.StaticText(parentpanel, -1, _("Material Thickness:")), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT) - - - # Hotend & bed temperatures # # Hotend temp add("htemp_label", wx.StaticText(parentpanel, -1, _("Heat:")), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - htemp_choices = [root.temps[i] + " (" + i + ")" for i in sorted(root.temps.keys(), key = lambda x:root.temps[x])] root.settoff = make_button(parentpanel, _("Off"), lambda e: root.do_settemp("off"), _("Switch Hotend Off"), size = (38, -1), style = wx.BU_EXACTFIT) root.printerControls.append(root.settoff) add("htemp_off", root.settoff) - if root.settings.last_temperature not in map(float, root.temps.values()): - htemp_choices = [str(root.settings.last_temperature)] + htemp_choices - root.htemp = wx.ComboBox(parentpanel, -1, choices = htemp_choices, - style = wx.CB_DROPDOWN, size = (80, -1)) - root.htemp.SetToolTip(wx.ToolTip(_("Select Temperature for Hotend"))) + root.htemp = wx.ComboBox(parentpanel, style = wx.CB_DROPDOWN, size = (115, -1)) + root.htemp.SetToolTip(wx.ToolTip(_("Select Temperature for [H]otend"))) root.htemp.Bind(wx.EVT_COMBOBOX, root.htemp_change) add("htemp_val", root.htemp) @@ -189,17 +145,13 @@ # Bed temp add("btemp_label", wx.StaticText(parentpanel, -1, _("Bed:")), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) - btemp_choices = [root.bedtemps[i] + " (" + i + ")" for i in sorted(root.bedtemps.keys(), key = lambda x:root.temps[x])] root.setboff = make_button(parentpanel, _("Off"), lambda e: root.do_bedtemp("off"), _("Switch Heated Bed Off"), size = (38, -1), style = wx.BU_EXACTFIT) root.printerControls.append(root.setboff) add("btemp_off", root.setboff) - if root.settings.last_bed_temperature not in map(float, root.bedtemps.values()): - btemp_choices = [str(root.settings.last_bed_temperature)] + btemp_choices - root.btemp = wx.ComboBox(parentpanel, -1, choices = btemp_choices, - style = wx.CB_DROPDOWN, size = (80, -1)) - root.btemp.SetToolTip(wx.ToolTip(_("Select Temperature for Heated Bed"))) + root.btemp = wx.ComboBox(parentpanel, style = wx.CB_DROPDOWN, size = (115, -1)) + root.btemp.SetToolTip(wx.ToolTip(_("Select Temperature for Heated [B]ed"))) root.btemp.Bind(wx.EVT_COMBOBOX, root.btemp_change) add("btemp_val", root.btemp) @@ -207,37 +159,31 @@ root.printerControls.append(root.setbbtn) add("btemp_set", root.setbbtn, flag = wx.EXPAND) - root.btemp.SetValue(str(root.settings.last_bed_temperature)) - root.htemp.SetValue(str(root.settings.last_temperature)) - - # added for an error where only the bed would get (pla) or (abs). - # This ensures, if last temp is a default pla or abs, it will be marked so. - # if it is not, then a (user) remark is added. This denotes a manual entry + def set_labeled(temp, choices, widget): + choices = [(float(p[1]), p[0]) for p in choices.items()] + if not next((1 for p in choices if p[0] == temp), False): + choices.append((temp, 'user')) - for i in btemp_choices: - if i.split()[0] == str(root.settings.last_bed_temperature).split('.')[0] or i.split()[0] == str(root.settings.last_bed_temperature): - root.btemp.SetValue(i) - for i in htemp_choices: - if i.split()[0] == str(root.settings.last_temperature).split('.')[0] or i.split()[0] == str(root.settings.last_temperature): - root.htemp.SetValue(i) + choices = sorted(choices) + widget.Items = ['%s (%s)'%tl for tl in choices] + widget.Selection = next((i for i, tl in enumerate(choices) if tl[0] == temp), -1) - if '(' not in root.btemp.Value: - root.btemp.SetValue(root.btemp.Value + ' (user)') - if '(' not in root.htemp.Value: - root.htemp.SetValue(root.htemp.Value + ' (user)') + set_labeled(root.settings.last_bed_temperature, root.bedtemps, root.btemp) + set_labeled(root.settings.last_temperature, root.temps, root.htemp) # Speed control # speedpanel = root.newPanel(parentpanel) speedsizer = wx.BoxSizer(wx.HORIZONTAL) - speedsizer.Add(wx.StaticText(speedpanel, -1, _("Print speed:")), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + speedsizer.Add(wx.StaticText(speedpanel, -1, _("Print speed:")), flag = wx.ALIGN_CENTER_VERTICAL) root.speed_slider = wx.Slider(speedpanel, -1, 100, 1, 300) speedsizer.Add(root.speed_slider, 1, flag = wx.EXPAND) - root.speed_spin = FloatSpin(speedpanel, -1, value = 100, min_val = 1, max_val = 300, digits = 0, style = wx.ALIGN_LEFT, size = (80, -1)) + root.speed_spin = wx.SpinCtrlDouble(speedpanel, -1, initial = 100, min = 1, max = 300, style = wx.ALIGN_LEFT, size = (115, -1)) + root.speed_spin.SetDigits(0) speedsizer.Add(root.speed_spin, 0, flag = wx.ALIGN_CENTER_VERTICAL) root.speed_label = wx.StaticText(speedpanel, -1, _("%")) - speedsizer.Add(root.speed_label, flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + speedsizer.Add(root.speed_label, flag = wx.ALIGN_CENTER_VERTICAL) def speedslider_set(event): root.do_setspeed() @@ -252,7 +198,7 @@ value = root.speed_spin.GetValue() root.speed_setbtn.SetBackgroundColour("red") root.speed_slider.SetValue(value) - root.speed_spin.Bind(wx.EVT_SPINCTRL, speedslider_spin) + root.speed_spin.Bind(wx.EVT_SPINCTRLDOUBLE, speedslider_spin) def speedslider_scroll(event): value = root.speed_slider.GetValue() @@ -263,15 +209,15 @@ # Flow control # flowpanel = root.newPanel(parentpanel) flowsizer = wx.BoxSizer(wx.HORIZONTAL) - flowsizer.Add(wx.StaticText(flowpanel, -1, _("Print flow:")), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + flowsizer.Add(wx.StaticText(flowpanel, -1, _("Print flow:")), flag = wx.ALIGN_CENTER_VERTICAL) root.flow_slider = wx.Slider(flowpanel, -1, 100, 1, 300) flowsizer.Add(root.flow_slider, 1, flag = wx.EXPAND) - root.flow_spin = FloatSpin(flowpanel, -1, value = 100, min_val = 1, max_val = 300, digits = 0, style = wx.ALIGN_LEFT, size = (60, -1)) + root.flow_spin = wx.SpinCtrlDouble(flowpanel, -1, initial = 100, min = 1, max = 300, style = wx.ALIGN_LEFT, size = (115, -1)) flowsizer.Add(root.flow_spin, 0, flag = wx.ALIGN_CENTER_VERTICAL) root.flow_label = wx.StaticText(flowpanel, -1, _("%")) - flowsizer.Add(root.flow_label, flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + flowsizer.Add(root.flow_label, flag = wx.ALIGN_CENTER_VERTICAL) def flowslider_set(event): root.do_setflow() @@ -286,7 +232,7 @@ value = root.flow_spin.GetValue() root.flow_setbtn.SetBackgroundColour("red") root.flow_slider.SetValue(value) - root.flow_spin.Bind(wx.EVT_SPINCTRL, flowslider_spin) + root.flow_spin.Bind(wx.EVT_SPINCTRLDOUBLE, flowslider_spin) def flowslider_scroll(event): value = root.flow_slider.GetValue() @@ -298,25 +244,32 @@ if root.display_gauges: root.hottgauge = TempGauge(parentpanel, size = (-1, 24), title = _("Heater:"), maxval = 300, bgcolor = root.bgcolor) + root.hottgauge.SetTarget(root.settings.last_temperature) + # root.hsetpoint = root.settings.last_temperature add("htemp_gauge", root.hottgauge, flag = wx.EXPAND) root.bedtgauge = TempGauge(parentpanel, size = (-1, 24), title = _("Bed:"), maxval = 150, bgcolor = root.bgcolor) + root.bedtgauge.SetTarget(root.settings.last_bed_temperature) + # root.bsetpoint = root.settings.last_bed_temperature add("btemp_gauge", root.bedtgauge, flag = wx.EXPAND) - def hotendgauge_scroll_setpoint(e): - rot = e.GetWheelRotation() - if rot > 0: - root.do_settemp(str(root.hsetpoint + 1)) - elif rot < 0: - root.do_settemp(str(max(0, root.hsetpoint - 1))) + def scroll_gauge(rot, cmd, setpoint): + if rot: + temp = setpoint + (1 if rot > 0 else -1) + cmd(str(max(0, temp))) + + def hotend_handler(e): + scroll_gauge(e.WheelRotation, root.do_settemp, root.hsetpoint) - def bedgauge_scroll_setpoint(e): - rot = e.GetWheelRotation() - if rot > 0: - root.do_settemp(str(root.bsetpoint + 1)) - elif rot < 0: - root.do_settemp(str(max(0, root.bsetpoint - 1))) - root.hottgauge.Bind(wx.EVT_MOUSEWHEEL, hotendgauge_scroll_setpoint) - root.bedtgauge.Bind(wx.EVT_MOUSEWHEEL, bedgauge_scroll_setpoint) + def bed_handler(e): + scroll_gauge(e.WheelRotation, root.do_bedtemp, root.bsetpoint) + root.hottgauge.Bind(wx.EVT_MOUSEWHEEL, hotend_handler) + root.bedtgauge.Bind(wx.EVT_MOUSEWHEEL, bed_handler) + + def updateGauge(e, gauge): + gauge.SetTarget(float(e.String.split()[0])) + + root.htemp.Bind(wx.EVT_TEXT, lambda e: updateGauge(e, root.hottgauge)) + root.btemp.Bind(wx.EVT_TEXT, lambda e: updateGauge(e, root.bedtgauge)) # Temperature (M105) feedback display # root.tempdisp = wx.StaticText(parentpanel, -1, "", style = wx.ST_NO_AUTORESIZE) @@ -345,10 +298,11 @@ esettingspanel = root.newPanel(parentpanel) esettingssizer = wx.GridBagSizer() esettingssizer.SetEmptyCellSize((0, 0)) - root.edist = FloatSpin(esettingspanel, -1, value = root.settings.last_extrusion, min_val = 0, max_val = 1000, size = (90, -1), digits = 1) + root.edist = wx.SpinCtrlDouble(esettingspanel, -1, initial = root.settings.last_extrusion, min = 0, max = 1000, size = (135, -1)) + root.edist.SetDigits(1) + root.edist.Bind(wx.EVT_SPINCTRLDOUBLE, root.setfeeds) root.edist.SetBackgroundColour((225, 200, 200)) root.edist.SetForegroundColour("black") - root.edist.Bind(wx.EVT_SPINCTRL, root.setfeeds) root.edist.Bind(wx.EVT_TEXT, root.setfeeds) add("edist_label", wx.StaticText(esettingspanel, -1, _("Length:")), container = esettingssizer, flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, border = 5) add("edist_val", root.edist, container = esettingssizer, flag = wx.ALIGN_CENTER | wx.RIGHT, border = 5) @@ -356,11 +310,12 @@ add("edist_unit", wx.StaticText(esettingspanel, -1, unit_label), container = esettingssizer, flag = wx.ALIGN_CENTER | wx.RIGHT, border = 5) root.edist.SetToolTip(wx.ToolTip(_("Amount to Extrude or Retract (mm)"))) if not mini_mode: - root.efeedc = FloatSpin(esettingspanel, -1, value = root.settings.e_feedrate, min_val = 0, max_val = 50000, size = (90, -1), digits = 1) + root.efeedc = wx.SpinCtrlDouble(esettingspanel, -1, initial = root.settings.e_feedrate, min = 0, max = 50000, size = (145, -1)) + root.efeedc.SetDigits(1) + root.efeedc.Bind(wx.EVT_SPINCTRLDOUBLE, root.setfeeds) root.efeedc.SetToolTip(wx.ToolTip(_("Extrude / Retract speed (mm/min)"))) root.efeedc.SetBackgroundColour((225, 200, 200)) root.efeedc.SetForegroundColour("black") - root.efeedc.Bind(wx.EVT_SPINCTRL, root.setfeeds) root.efeedc.Bind(wx.EVT_TEXT, root.setfeeds) add("efeed_val", root.efeedc, container = esettingssizer, flag = wx.ALIGN_CENTER | wx.RIGHT, border = 5) add("efeed_label", wx.StaticText(esettingspanel, -1, _("Speed:")), container = esettingssizer, flag = wx.ALIGN_LEFT) @@ -455,12 +410,12 @@ else: self.extra_buttons[key] = btn - root.xyfeedc = wx.SpinCtrl(lltspanel, -1, str(root.settings.xy_feedrate), min = 0, max = 50000, size = (97, -1)) + root.xyfeedc = wx.SpinCtrl(lltspanel, -1, str(root.settings.xy_feedrate), min = 0, max = 50000, size = (130, -1)) root.xyfeedc.SetToolTip(wx.ToolTip(_("Set Maximum Speed for X & Y axes (mm/min)"))) - llts.Add(wx.StaticText(lltspanel, -1, _("XY:")), flag = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL) + llts.Add(wx.StaticText(lltspanel, -1, _("XY:")), flag = wx.ALIGN_CENTER_VERTICAL) llts.Add(root.xyfeedc) - llts.Add(wx.StaticText(lltspanel, -1, _("mm/min Z:")), flag = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL) - root.zfeedc = wx.SpinCtrl(lltspanel, -1, str(root.settings.z_feedrate), min = 0, max = 50000, size = (90, -1)) + llts.Add(wx.StaticText(lltspanel, -1, _("mm/min Z:")), flag = wx.ALIGN_CENTER_VERTICAL) + root.zfeedc = wx.SpinCtrl(lltspanel, -1, str(root.settings.z_feedrate), min = 0, max = 50000, size = (130, -1)) root.zfeedc.SetToolTip(wx.ToolTip(_("Set Maximum Speed for Z axis (mm/min)"))) llts.Add(root.zfeedc,) diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gui/graph.py --- a/printrun-src/printrun/gui/graph.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gui/graph.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # This file is part of the Printrun suite. # # Printrun is free software: you can redistribute it and/or modify @@ -17,6 +15,7 @@ import wx from math import log10, floor, ceil +from bisect import bisect_left from printrun.utils import install_locale install_locale('pronterface') @@ -25,14 +24,26 @@ class GraphWindow(wx.Frame): def __init__(self, root, parent_graph = None, size = (600, 600)): - super(GraphWindow, self).__init__(None, title = _("Temperature graph"), + super().__init__(None, title = _("Temperature graph"), size = size) - panel = wx.Panel(self, -1) + self.parentg = parent_graph + panel = wx.Panel(self) vbox = wx.BoxSizer(wx.VERTICAL) self.graph = Graph(panel, wx.ID_ANY, root, parent_graph = parent_graph) vbox.Add(self.graph, 1, wx.EXPAND) panel.SetSizer(vbox) + def Destroy(self): + self.graph.StopPlotting() + if self.parentg is not None: + self.parentg.window=None + return super().Destroy() + + def __del__(self): + if self.parentg is not None: + self.parentg.window=None + self.graph.StopPlotting() + class Graph(BufferedCanvas): '''A class to show a Graph with Pronterface.''' @@ -40,7 +51,7 @@ size = wx.Size(150, 80), style = 0, parent_graph = None): # Forcing a no full repaint to stop flickering style = style | wx.NO_FULL_REPAINT_ON_RESIZE - super(Graph, self).__init__(parent, id, pos, size, style) + super().__init__(parent, id, pos, size, style) self.root = root if parent_graph is not None: @@ -50,7 +61,7 @@ self.extruder1targettemps = parent_graph.extruder1targettemps self.bedtemps = parent_graph.bedtemps self.bedtargettemps = parent_graph.bedtargettemps - self.fanpowers=parent_graph.fanpowers + self.fanpowers=parent_graph.fanpowers else: self.extruder0temps = [0] self.extruder0targettemps = [0] @@ -58,10 +69,11 @@ self.extruder1targettemps = [0] self.bedtemps = [0] self.bedtargettemps = [0] - self.fanpowers= [0] + self.fanpowers= [0] self.timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.updateTemperatures, self.timer) + self.Bind(wx.EVT_WINDOW_DESTROY, self.processDestroy) self.minyvalue = 0 self.maxyvalue = 260 @@ -76,9 +88,16 @@ self.xsteps = 60 # Covering 1 minute in the graph self.window = None + self.reserved = [] + + def processDestroy(self, event): + # print('processDestroy') + self.StopPlotting() + self.Unbind(wx.EVT_TIMER) + event.Skip() def show_graph_window(self, event = None): - if not self.window: + if self.window is None or not self.window: self.window = GraphWindow(self.root, self) self.window.Show() if self.timer.IsRunning(): @@ -90,13 +109,14 @@ if self.window: self.window.Close() def updateTemperatures(self, event): + # print('updateTemperatures') self.AddBedTemperature(self.bedtemps[-1]) self.AddBedTargetTemperature(self.bedtargettemps[-1]) self.AddExtruder0Temperature(self.extruder0temps[-1]) self.AddExtruder0TargetTemperature(self.extruder0targettemps[-1]) self.AddExtruder1Temperature(self.extruder1temps[-1]) self.AddExtruder1TargetTemperature(self.extruder1targettemps[-1]) - self.AddFanPower(self.fanpowers[-1]) + self.AddFanPower(self.fanpowers[-1]) if self.rescaley: self._ybounds.update() self.Refresh() @@ -124,11 +144,10 @@ # draw vertical bars dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1)) + xscale = float(self.width - 1) / (self.xbars - 1) for x in range(self.xbars + 1): - dc.DrawLine(x * (float(self.width - 1) / (self.xbars - 1)), - 0, - x * (float(self.width - 1) / (self.xbars - 1)), - self.height) + x = x * xscale + dc.DrawLine(x, 0, x, self.height) # draw horizontal bars spacing = self._calculate_spacing() # spacing between bars, in degrees @@ -141,15 +160,17 @@ degrees = y * spacing y_pos = self._y_pos(degrees) dc.DrawLine(0, y_pos, self.width, y_pos) - gc.DrawText(unicode(y * spacing), - 1, y_pos - (font.GetPointSize() / 2)) + label = str(y * spacing) + label_y = y_pos - font.GetPointSize() / 2 + self.layoutText(label, 1, label_y, gc) + gc.DrawText(label, 1, label_y) - if self.timer.IsRunning() is False: + if not self.timer.IsRunning(): font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.BOLD) gc.SetFont(font, wx.Colour(3, 4, 4)) gc.DrawText("Graph offline", - self.width / 2 - (font.GetPointSize() * 3), - self.height / 2 - (font.GetPointSize() * 1)) + self.width / 2 - font.GetPointSize() * 3, + self.height / 2 - font.GetPointSize() * 1) # dc.DrawCircle(50, 50, 1) @@ -187,37 +208,93 @@ def drawtemperature(self, dc, gc, temperature_list, text, text_xoffset, r, g, b, a): - if self.timer.IsRunning() is False: - dc.SetPen(wx.Pen(wx.Colour(128, 128, 128, 128), 1)) - else: - dc.SetPen(wx.Pen(wx.Colour(r, g, b, a), 1)) + color = self.timer.IsRunning() and (r, g, b, a) or [128] * 4 + dc.SetPen(wx.Pen(color, 1)) x_add = float(self.width) / self.xsteps x_pos = 0.0 lastxvalue = 0.0 lastyvalue = temperature_list[-1] - for temperature in (temperature_list): + for temperature in temperature_list: y_pos = self._y_pos(temperature) - if (x_pos > 0.0): # One need 2 points to draw a line. + if x_pos > 0: # One need 2 points to draw a line. dc.DrawLine(lastxvalue, lastyvalue, x_pos, y_pos) lastxvalue = x_pos - x_pos = float(x_pos) + x_add + x_pos += x_add lastyvalue = y_pos - if len(text) > 0: + if text: font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD) # font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL) - if self.timer.IsRunning() is False: - gc.SetFont(font, wx.Colour(128, 128, 128)) - else: - gc.SetFont(font, wx.Colour(r, g, b)) + gc.SetFont(font, color[:3]) text_size = len(text) * text_xoffset + 1 - gc.DrawText(text, - x_pos - x_add - (font.GetPointSize() * text_size), - lastyvalue - (font.GetPointSize() / 2)) + pos = self.layoutText(text, lastxvalue, lastyvalue, gc) + gc.DrawText(text, pos.x, pos.y) + + def layoutRect(self, rc): + res = LtRect(rc) + reserved = sorted((rs for rs in self.reserved + if not (rc.bottom < rs.top or rc.top > rs.bottom)), + key=wx.Rect.GetLeft) + self.boundRect(res) + # search to the left for gaps large enough to accomodate res + rci = bisect_left(reserved, res) + + for i in range(rci, len(reserved)-1): + res.x = reserved[i].right + 1 + if res.right < reserved[i+1].left: + #found good res + break + else: + # did not find gap to the right + if reserved: + #try to respect rc.x at the cost of a gap (50...Bed) + if res.left < reserved[-1].right: + res.x = reserved[-1].right + 1 + if res.right >= self.width: + #goes beyond window bounds + # try to the left + for i in range(min(rci, len(reserved)-1), 0, -1): + res.x = reserved[i].left - rc.width + if reserved[i-1].right < res.left: + break + else: + res = LtRect(self.layoutRectY(rc)) + + self.reserved.append(res) + return res + + def boundRect(self, rc): + rc.x = min(rc.x, self.width - rc.width) + return rc + + def layoutRectY(self, rc): + top = self.height + bottom = 0 + collision = False + res = LtRect(rc) + res.x = max(self.gridLabelsRight+1, min(rc.x, self.width-rc.width)) + for rs in self.reserved: + if not (res.right < rs.left or res.left > rs.right): + collision = True + top = min(top, rs.Top) + bottom = max(bottom, rs.bottom) + if collision: + res.y = top - rc.height + if res.y < 0: + res.y = bottom+1 + if res.bottom >= self.height: + res.y = rc.y + return res + + def layoutText(self, text, x, y, gc): + ext = gc.GetTextExtent(text) + rc = self.layoutRect(wx.Rect(x, y, *ext)) + # print('layoutText', text, rc.TopLeft) + return rc def drawfanpower(self, dc, gc): self.drawtemperature(dc, gc, self.fanpowers, @@ -315,27 +392,37 @@ self.timer.Start(time) if self.window: self.window.graph.StartPlotting(time) + def Destroy(self): + # print(__class__, '.Destroy') + self.StopPlotting() + return super(BufferedCanvas, self).Destroy() + def StopPlotting(self): self.timer.Stop() - self.Refresh() + #self.Refresh() # do not refresh when stopping in case the underlying object has been destroyed already if self.window: self.window.graph.StopPlotting() def draw(self, dc, w, h): - dc.SetBackground(wx.Brush(self.root.bgcolor)) + dc.SetBackground(wx.Brush(self.root.settings.graph_color_background)) dc.Clear() gc = wx.GraphicsContext.Create(dc) self.width = w self.height = h + + self.reserved.clear() self.drawgrid(dc, gc) + self.gridLabelsRight = self.reserved[-1].Right + self.drawbedtargettemp(dc, gc) self.drawbedtemp(dc, gc) self.drawfanpower(dc, gc) self.drawextruder0targettemp(dc, gc) self.drawextruder0temp(dc, gc) - self.drawextruder1targettemp(dc, gc) - self.drawextruder1temp(dc, gc) + if self.extruder1targettemps[-1]>0 or self.extruder1temps[-1]>5: + self.drawextruder1targettemp(dc, gc) + self.drawextruder1temp(dc, gc) - class _YBounds(object): + class _YBounds: """Small helper class to claculate y bounds dynamically""" def __init__(self, graph, minimum_scale=5.0, buffer=0.10): @@ -402,8 +489,8 @@ if bed_target > 0 or bed_max > 5: # use HBP miny = min(miny, bed_min, bed_target) maxy = max(maxy, bed_max, bed_target) - miny=min(0,miny); - maxy=max(260,maxy); + miny = min(0, miny) + maxy = max(260, maxy) padding = (maxy - miny) * self.buffer / (1.0 - 2 * self.buffer) miny -= padding @@ -436,8 +523,8 @@ if bed_target > 0 or bed_max > 5: # use HBP miny = min(miny, bed_min, bed_target) maxy = max(maxy, bed_max, bed_target) - miny=min(0,miny); - maxy=max(260,maxy); + miny = min(0, miny) + maxy = max(260, maxy) # We have to rescale, so add padding bufratio = self.buffer / (1.0 - self.buffer) @@ -450,3 +537,7 @@ return (min(miny, self.graph.minyvalue), max(maxy, self.graph.maxyvalue)) + +class LtRect(wx.Rect): + def __lt__(self, other): + return self.x < other.x diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gui/log.py --- a/printrun-src/printrun/gui/log.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gui/log.py Wed Jan 20 10:15:13 2021 +0100 @@ -30,9 +30,17 @@ lbrs = wx.BoxSizer(wx.HORIZONTAL) root.commandbox = wx.TextCtrl(bottom_panel, style = wx.TE_PROCESS_ENTER) root.commandbox.SetToolTip(wx.ToolTip(_("Send commands to printer\n(Type 'help' for simple\nhelp function)"))) + root.commandbox.Hint = 'Command to [S]end' root.commandbox.Bind(wx.EVT_TEXT_ENTER, root.sendline) root.commandbox.Bind(wx.EVT_CHAR, root.cbkey) - root.commandbox.history = [u""] + def deselect(ev): + # In Ubuntu 19.10, when focused, all text is selected + lp = root.commandbox.LastPosition + # print(f"SetSelection({lp}, {lp})") + wx.CallAfter(root.commandbox.SetSelection, lp, lp) + ev.Skip() + root.commandbox.Bind(wx.EVT_SET_FOCUS, deselect) + root.commandbox.history = [""] root.commandbox.histindex = 1 lbrs.Add(root.commandbox, 1) root.sendbtn = make_button(bottom_panel, _("Send"), root.sendline, _("Send Command to Printer"), style = wx.BU_EXACTFIT, container = lbrs) diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gui/toolbar.py --- a/printrun-src/printrun/gui/toolbar.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gui/toolbar.py Wed Jan 20 10:15:13 2021 +0100 @@ -27,7 +27,7 @@ parentpanel = root.newPanel(parentpanel) glob.Add(parentpanel, 1, flag = wx.EXPAND) glob.Add(root.locker, 0, flag = wx.ALIGN_CENTER) - ToolbarSizer = wx.WrapSizer if use_wrapsizer and wx.VERSION > (2, 9) else wx.BoxSizer + ToolbarSizer = wx.WrapSizer if use_wrapsizer else wx.BoxSizer self = ToolbarSizer(wx.HORIZONTAL) root.rescanbtn = make_autosize_button(parentpanel, _("Port"), root.rescanports, _("Communication Settings\nClick to rescan ports")) self.Add(root.rescanbtn, 0, wx.TOP | wx.LEFT, 0) @@ -42,7 +42,7 @@ root.baud = wx.ComboBox(parentpanel, -1, choices = ["2400", "9600", "19200", "38400", "57600", "115200", "250000"], - style = wx.CB_DROPDOWN, size = (100, -1)) + style = wx.CB_DROPDOWN, size = (110, -1)) root.baud.SetToolTip(wx.ToolTip(_("Select Baud rate for printer communication"))) try: root.baud.SetValue("115200") @@ -52,7 +52,8 @@ self.Add(root.baud) if not hasattr(root, "connectbtn"): - root.connectbtn = make_autosize_button(parentpanel, _("Connect"), root.connect, _("Connect to the printer")) + root.connectbtn_cb_var = root.connect + root.connectbtn = make_autosize_button(parentpanel, _("&Connect"), root.connectbtn_cb, _("Connect to the printer")) root.statefulControls.append(root.connectbtn) else: root.connectbtn.Reparent(parentpanel) diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gui/viz.py --- a/printrun-src/printrun/gui/viz.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gui/viz.py Wed Jan 20 10:15:13 2021 +0100 @@ -18,10 +18,7 @@ import wx -class NoViz(object): - - showall = False - +class BaseViz: def clear(self, *a): pass @@ -35,19 +32,21 @@ def addfile(self, *a, **kw): pass - def addgcode(self, *a, **kw): - pass - def addgcodehighlight(self, *a, **kw): pass - def Refresh(self, *a): - pass - def setlayer(self, *a): pass -class NoVizWindow(object): + def on_settings_change(self, changed_settings): + pass + +class NoViz(BaseViz): + showall = False + def Refresh(self, *a): + pass + +class NoVizWindow: def __init__(self): self.p = NoViz() @@ -68,7 +67,13 @@ if root.settings.mainviz == "3D": try: import printrun.gcview - root.gviz = printrun.gcview.GcodeViewMainWrapper(parentpanel, root.build_dimensions_list, root = root, circular = root.settings.circular_bed, antialias_samples = int(root.settings.antialias3dsamples)) + root.gviz = printrun.gcview.GcodeViewMainWrapper( + parentpanel, + root.build_dimensions_list, + root = root, + circular = root.settings.circular_bed, + antialias_samples = int(root.settings.antialias3dsamples), + grid = (root.settings.preview_grid_step1, root.settings.preview_grid_step2)) root.gviz.clickcb = root.show_viz_window except: use2dview = True @@ -92,7 +97,14 @@ objects = None if isinstance(root.gviz, printrun.gcview.GcodeViewMainWrapper): objects = root.gviz.objects - root.gwindow = printrun.gcview.GcodeViewFrame(None, wx.ID_ANY, 'Gcode view, shift to move view, mousewheel to set layer', size = (600, 600), build_dimensions = root.build_dimensions_list, objects = objects, root = root, circular = root.settings.circular_bed, antialias_samples = int(root.settings.antialias3dsamples)) + root.gwindow = printrun.gcview.GcodeViewFrame(None, wx.ID_ANY, 'Gcode view, shift to move view, mousewheel to set layer', + size = (600, 600), + build_dimensions = root.build_dimensions_list, + objects = objects, + root = root, + circular = root.settings.circular_bed, + antialias_samples = int(root.settings.antialias3dsamples), + grid = (root.settings.preview_grid_step1, root.settings.preview_grid_step2)) except: use3dview = False logging.error("3D view mode requested, but we failed to initialize it.\n" @@ -106,4 +118,4 @@ bgcolor = root.bgcolor) root.gwindow.Bind(wx.EVT_CLOSE, lambda x: root.gwindow.Hide()) if not isinstance(root.gviz, NoViz): - self.Add(root.gviz.widget, 1, flag = wx.EXPAND | wx.ALIGN_CENTER_HORIZONTAL) + self.Add(root.gviz.widget, 1, flag = wx.EXPAND) diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gui/widgets.py --- a/printrun-src/printrun/gui/widgets.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gui/widgets.py Wed Jan 20 10:15:13 2021 +0100 @@ -120,8 +120,7 @@ "UI": _("User interface"), "Viewer": _("Viewer"), "Colors": _("Colors"), - "External": _("External commands"), - "Laser": "Lasercut options"} + "External": _("External commands")} class PronterOptionsDialog(wx.Dialog): """Options editor""" @@ -131,11 +130,11 @@ panel = wx.Panel(self) header = wx.StaticBox(panel, label = _("Settings")) sbox = wx.StaticBoxSizer(header, wx.VERTICAL) - notebook = wx.Notebook(panel) + self.notebook = notebook = wx.Notebook(panel) all_settings = pronterface.settings._all_settings() group_list = [] groups = {} - for group in ["Printer", "UI", "Viewer", "Colors", "External", "Laser"]: + for group in ["Printer", "UI", "Viewer", "Colors", "External"]: group_list.append(group) groups[group] = [] for setting in all_settings: @@ -162,8 +161,9 @@ label.SetFont(font) grid.Add(label, pos = (current_row, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + expand = 0 if isinstance(widget, (wx.SpinCtrlDouble, wx.Choice, wx.ComboBox)) else wx.EXPAND grid.Add(widget, pos = (current_row, 1), - flag = wx.ALIGN_CENTER_VERTICAL | wx.EXPAND) + flag = wx.ALIGN_CENTER_VERTICAL | expand) if hasattr(label, "set_default"): label.Bind(wx.EVT_MOUSE_EVENTS, label.set_default) if hasattr(widget, "Bind"): @@ -175,28 +175,25 @@ panel.SetSizer(sbox) topsizer = wx.BoxSizer(wx.VERTICAL) topsizer.Add(panel, 1, wx.ALL | wx.EXPAND) - topsizer.Add(self.CreateButtonSizer(wx.OK | wx.CANCEL), 0, wx.ALIGN_RIGHT) + topsizer.Add(self.CreateButtonSizer(wx.OK | wx.CANCEL), 0, wx.ALIGN_CENTER) self.SetSizerAndFit(topsizer) self.SetMinSize(self.GetSize()) - self.notebook = notebook - self.group_list = group_list - - def setPage(self, name): - self.notebook.ChangeSelection(self.group_list.index(name)) - -def PronterOptions(pronterface, defaulttab = None): +notebookSelection = 0 +def PronterOptions(pronterface): dialog = PronterOptionsDialog(pronterface) - if defaulttab: - # set the active tab before open dialog - dialog.setPage(defaulttab) - + global notebookSelection + dialog.notebook.Selection = notebookSelection if dialog.ShowModal() == wx.ID_OK: + changed_settings = [] for setting in pronterface.settings._all_settings(): old_value = setting.value setting.update() if setting.value != old_value: pronterface.set(setting.name, setting.value) + changed_settings.append(setting) + pronterface.on_settings_change(changed_settings) + notebookSelection = dialog.notebook.Selection dialog.Destroy() class ButtonEdit(wx.Dialog): @@ -236,7 +233,7 @@ valid = True elif macro in self.pronterface.macros: valid = True - elif hasattr(self.pronterface.__class__, u"do_" + macro): + elif hasattr(self.pronterface.__class__, "do_" + macro): valid = False elif len([c for c in macro if not c.isalnum() and c != "_"]): valid = False @@ -268,7 +265,7 @@ self.Bind(wx.EVT_PAINT, self.paint) self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) self.bgcolor = wx.Colour() - self.bgcolor.SetFromName(bgcolor) + self.bgcolor.Set(bgcolor) self.width, self.height = size self.title = title self.max = maxval @@ -299,17 +296,17 @@ lo, hi, val, valhi = cmid, cmax, val - vmid, vmax - vmid vv = float(val) / valhi rgb = lo.Red() + (hi.Red() - lo.Red()) * vv, lo.Green() + (hi.Green() - lo.Green()) * vv, lo.Blue() + (hi.Blue() - lo.Blue()) * vv - rgb = map(lambda x: x * 0.8, rgb) - return wx.Colour(*map(int, rgb)) + rgb = (int(x * 0.8) for x in rgb) + return wx.Colour(*rgb) def paint(self, ev): - self.width, self.height = self.GetClientSizeTuple() + self.width, self.height = self.GetClientSize() self.recalc() x0, y0, x1, y1, xE, yE = 1, 1, self.ypt + 1, 1, self.width + 1 - 2, 20 dc = wx.PaintDC(self) dc.SetBackground(wx.Brush(self.bgcolor)) dc.Clear() - cold, medium, hot = wx.Colour(0, 167, 223), wx.Colour(239, 233, 119), wx.Colour(210, 50.100) + cold, medium, hot = wx.Colour(0, 167, 223), wx.Colour(239, 233, 119), wx.Colour(210, 50, 0) # gauge1, gauge2 = wx.Colour(255, 255, 210), (self.gaugeColour or wx.Colour(234, 82, 0)) gauge1 = wx.Colour(255, 255, 210) shadow1, shadow2 = wx.Colour(110, 110, 110), self.bgcolor @@ -361,7 +358,7 @@ setp_path.AddLineToPoint(setpoint, yE - 5) gc.DrawPath(setp_path) # draw readout - text = u"T\u00B0 %u/%u" % (self.value, self.setpoint) + text = "T\u00B0 %u/%u" % (self.value, self.setpoint) # gc.SetFont(gc.CreateFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD), wx.WHITE)) # gc.DrawText(text, 29,-2) gc.SetFont(gc.CreateFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD), wx.WHITE)) @@ -371,7 +368,7 @@ gc.DrawText(self.title, x0 + 18, y0 + 3) gc.DrawText(text, x0 + 118, y0 + 3) -class SpecialButton(object): +class SpecialButton: label = None command = None diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gui/xybuttons.py --- a/printrun-src/printrun/gui/xybuttons.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gui/xybuttons.py Wed Jan 20 10:15:13 2021 +0100 @@ -23,13 +23,36 @@ elif n > 0: return 1 else: return 0 -class XYButtons(BufferedCanvas): +DASHES = [4, 7] +# Brush and pen for grey overlay when mouse hovers over +HOVER_PEN_COLOR = wx.Colour(100, 100, 100, 172) +HOVER_BRUSH_COLOR = wx.Colour(0, 0, 0, 128) + +class FocusCanvas(BufferedCanvas): + def __init__(self, *args, **kwds): + super().__init__(*args, **kwds) + self.Bind(wx.EVT_SET_FOCUS, self.onFocus) + + def onFocus(self, evt): + self.Refresh() + evt.Skip() + + def drawFocusRect(self, dc): + if self.HasFocus(): + pen = wx.Pen(wx.BLACK, 1, wx.PENSTYLE_USER_DASH) + pen.SetDashes(DASHES) + dc.Pen = pen + dc.Brush = wx.Brush(wx.TRANSPARENT_BRUSH) + dc.DrawRectangle(self.ClientRect) + +class XYButtons(FocusCanvas): keypad_positions = { - 0: (106, 100), + 0: (104, 99), 1: (86, 83), 2: (68, 65), 3: (53, 50) } + keypad_radius = 9 corner_size = (49, 49) corner_inset = (7, 13) label_overlay_positions = { @@ -55,6 +78,7 @@ self.bg_bmp = wx.Image(imagefile(self.imagename), wx.BITMAP_TYPE_PNG).ConvertToBitmap() self.keypad_bmp = wx.Image(imagefile("arrow_keys.png"), wx.BITMAP_TYPE_PNG).ConvertToBitmap() self.keypad_idx = -1 + self.hovered_keypad = None self.quadrant = None self.concentric = None self.corner = None @@ -68,10 +92,10 @@ self.lastCorner = None self.bgcolor = wx.Colour() - self.bgcolor.SetFromName(bgcolor) + self.bgcolor.Set(bgcolor) self.bgcolormask = wx.Colour(self.bgcolor.Red(), self.bgcolor.Green(), self.bgcolor.Blue(), 128) - BufferedCanvas.__init__(self, parent, ID, size=self.bg_bmp.GetSize()) + super().__init__(parent, ID, size=self.bg_bmp.GetSize()) self.bind_events() @@ -81,15 +105,19 @@ self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown) self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow) - self.Bind(wx.EVT_KEY_UP, self.OnKey) - wx.GetTopLevelParent(self).Bind(wx.EVT_CHAR_HOOK, self.OnTopLevelKey) + self.Bind(wx.EVT_CHAR_HOOK, self.OnKey) + self.Bind(wx.EVT_KILL_FOCUS, self.onKillFocus) + + def onKillFocus(self, evt): + self.setKeypadIndex(-1) + evt.Skip() def disable(self): - self.enabled = False + self.Enabled = self.enabled = False self.update() def enable(self): - self.enabled = True + self.Enabled = self.enabled = True self.update() def repeatLast(self): @@ -112,21 +140,23 @@ def distanceToPoint(self, x1, y1, x2, y2): return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) - def cycleKeypadIndex(self): - idx = self.keypad_idx + 1 - if idx > 2: idx = 0 - return idx + def cycleKeypadIndex(self, forward): + idx = self.keypad_idx + (1 if forward else -1) + # do not really cycle to allow exiting of jog controls widget + return idx if idx < len(self.keypad_positions) else -1 def setKeypadIndex(self, idx): self.keypad_idx = idx self.update() - def getMovement(self): + def getMovement(self, event): xdir = [1, 0, -1, 0, 0, 0][self.quadrant] ydir = [0, 1, 0, -1, 0, 0][self.quadrant] zdir = [0, 0, 0, 0, 1, -1][self.quadrant] magnitude = math.pow(10, self.concentric - 2) - if not zdir == 0: + magnitude *= event.ShiftDown() and 2 or event.ControlDown() and 0.5 or 1 + + if zdir: magnitude = min(magnitude, 10) return (magnitude * xdir, magnitude * ydir, magnitude * zdir) @@ -141,7 +171,6 @@ def getQuadrantConcentricFromPosition(self, pos): rel_x = pos[0] - self.center[0] rel_y = pos[1] - self.center[1] - radius = math.sqrt(rel_x ** 2 + rel_y ** 2) if rel_x > rel_y and rel_x > -rel_y: quadrant = 0 # Right elif rel_x <= rel_y and rel_x > -rel_y: @@ -151,13 +180,14 @@ else: quadrant = 2 # Left + radius = math.sqrt(rel_x ** 2 + rel_y ** 2) idx = self.lookupConcentric(radius) return (quadrant, idx) def mouseOverKeypad(self, mpos): for idx, kpos in self.keypad_positions.items(): radius = self.distanceToPoint(mpos[0], mpos[1], kpos[0], kpos[1]) - if radius < 9: + if radius < XYButtons.keypad_radius: return idx return None @@ -172,7 +202,8 @@ gc.DrawPath(path) def highlightQuadrant(self, gc, quadrant, concentric): - assert(quadrant >= 0 and quadrant <= 3) + if not 0 <= quadrant <= 3: + return assert(concentric >= 0 and concentric <= 4) inner_ring_radius = self.concentric_inset @@ -218,7 +249,7 @@ w, h = self.corner_size xinset, yinset = self.corner_inset cx, cy = self.center - ww, wh = self.GetSizeTuple() + ww, wh = self.GetSize() if corner == 0: x, y = (cx - ww / 2 + xinset + 1, cy - wh / 2 + yinset) @@ -247,9 +278,8 @@ gc.DrawBitmap(self.bg_bmp, 0, 0, w, h) if self.enabled and self.IsEnabled(): - # Brush and pen for grey overlay when mouse hovers over - gc.SetPen(wx.Pen(wx.Colour(100, 100, 100, 172), 4)) - gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 128))) + gc.SetPen(wx.Pen(HOVER_PEN_COLOR, 4)) + gc.SetBrush(wx.Brush(HOVER_BRUSH_COLOR)) if self.concentric is not None: if self.concentric < len(self.concentric_circle_radii): @@ -266,6 +296,11 @@ pos = (pos[0] - padw / 2 - 3, pos[1] - padh / 2 - 3) gc.DrawBitmap(self.keypad_bmp, pos[0], pos[1], padw, padh) + if self.hovered_keypad is not None and self.hovered_keypad != self.keypad_idx: + pos = self.keypad_positions[self.hovered_keypad] + r = XYButtons.keypad_radius + gc.DrawEllipse(pos[0]-r/2, pos[1]-r/2, r, r) + # Draw label overlays gc.SetPen(wx.Pen(wx.Colour(255, 255, 255, 128), 1)) gc.SetBrush(wx.Brush(wx.Colour(255, 255, 255, 128 + 64))) @@ -277,6 +312,9 @@ gc.SetPen(wx.Pen(self.bgcolor, 0)) gc.SetBrush(wx.Brush(self.bgcolormask)) gc.DrawRectangle(0, 0, w, h) + + self.drawFocusRect(dc) + # Used to check exact position of keypad dots, should we ever resize the bg image # for idx, kpos in self.label_overlay_positions.items(): # dc.DrawCircle(kpos[0], kpos[1], kpos[2]) @@ -284,43 +322,57 @@ # ------ # # Events # # ------ # - def OnTopLevelKey(self, evt): - # Let user press escape on any control, and return focus here - if evt.GetKeyCode() == wx.WXK_ESCAPE: - self.SetFocus() - evt.Skip() - def OnKey(self, evt): + # print('XYButtons key', evt.GetKeyCode()) if not self.enabled: + evt.Skip() return + key = evt.KeyCode if self.keypad_idx >= 0: - if evt.GetKeyCode() == wx.WXK_TAB: - self.setKeypadIndex(self.cycleKeypadIndex()) - elif evt.GetKeyCode() == wx.WXK_UP: + if key == wx.WXK_TAB: + keypad = self.cycleKeypadIndex(not evt.ShiftDown()) + self.setKeypadIndex(keypad) + if keypad == -1: + # exit widget after largest step + # evt.Skip() + # On MS Windows if tab event is delivered, + # it is not handled + self.Navigate(not evt.ShiftDown()) + return + elif key == wx.WXK_ESCAPE: + self.setKeypadIndex(-1) + elif key == wx.WXK_UP: self.quadrant = 1 - elif evt.GetKeyCode() == wx.WXK_DOWN: + elif key == wx.WXK_DOWN: self.quadrant = 3 - elif evt.GetKeyCode() == wx.WXK_LEFT: + elif key == wx.WXK_LEFT: self.quadrant = 2 - elif evt.GetKeyCode() == wx.WXK_RIGHT: + elif key == wx.WXK_RIGHT: self.quadrant = 0 - elif evt.GetKeyCode() == wx.WXK_PAGEUP: + elif key == wx.WXK_PAGEUP: self.quadrant = 4 - elif evt.GetKeyCode() == wx.WXK_PAGEDOWN: + elif key == wx.WXK_PAGEDOWN: self.quadrant = 5 else: evt.Skip() return - self.concentric = self.keypad_idx - x, y, z = self.getMovement() - - if x != 0 or y != 0 and self.moveCallback: - self.moveCallback(x, y) - if z != 0 and self.zCallback: - self.zCallback(z) - elif evt.GetKeyCode() == wx.WXK_SPACE: + self.concentric = self.keypad_idx + 1 + + if self.quadrant is not None: + x, y, z = self.getMovement(evt) + if (x or y) and self.moveCallback: + self.moveCallback(x, y) + if z and self.zCallback: + self.zCallback(z) + self.Refresh() + elif key == wx.WXK_SPACE: self.spacebarCallback() + elif key == wx.WXK_TAB: + self.setKeypadIndex(len(self.keypad_positions)-1 if evt.ShiftDown() else 0) + else: + # handle arrows elsewhere + evt.Skip() def OnMotion(self, event): if not self.enabled: @@ -328,12 +380,13 @@ oldcorner = self.corner oldq, oldc = self.quadrant, self.concentric + old_hovered_keypad = self.hovered_keypad mpos = event.GetPosition() - idx = self.mouseOverKeypad(mpos) + self.hovered_keypad = self.mouseOverKeypad(mpos) self.quadrant = None self.concentric = None - if idx is None: + if self.hovered_keypad is None: center = wx.Point(self.center[0], self.center[1]) riseDist = self.distanceToLine(mpos, center.x - 1, center.y - 1, center.x + 1, center.y + 1) fallDist = self.distanceToLine(mpos, center.x - 1, center.y + 1, center.x + 1, center.y - 1) @@ -353,7 +406,8 @@ if mpos.x < cx and mpos.y >= cy: self.corner = 3 - if oldq != self.quadrant or oldc != self.concentric or oldcorner != self.corner: + if oldq != self.quadrant or oldc != self.concentric or oldcorner != self.corner \ + or old_hovered_keypad != self.hovered_keypad: self.update() def OnLeftDown(self, event): @@ -375,7 +429,7 @@ self.lastMove = None self.cornerCallback(self.corner_to_axis[-1]) elif self.quadrant is not None: - x, y, z = self.getMovement() + x, y, z = self.getMovement(event) if self.moveCallback: self.lastMove = (x, y) self.lastCorner = None @@ -386,10 +440,7 @@ self.lastMove = None self.cornerCallback(self.corner_to_axis[self.corner]) else: - if self.keypad_idx == idx: - self.setKeypadIndex(-1) - else: - self.setKeypadIndex(idx) + self.setKeypadIndex(-1 if self.keypad_idx == idx else idx) def OnLeaveWindow(self, evt): self.quadrant = None @@ -486,9 +537,8 @@ gc.DrawBitmap(self.bg_bmp, 0, 0, w, h) if self.enabled and self.IsEnabled(): - # Brush and pen for grey overlay when mouse hovers over - gc.SetPen(wx.Pen(wx.Colour(100, 100, 100, 172), 4)) - gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 128))) + gc.SetPen(wx.Pen(HOVER_PEN_COLOR, 4)) + gc.SetBrush(wx.Brush(HOVER_BRUSH_COLOR)) if self.concentric is not None: if self.concentric < len(self.concentric_circle_radii): diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gui/zbuttons.py --- a/printrun-src/printrun/gui/zbuttons.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gui/zbuttons.py Wed Jan 20 10:15:13 2021 +0100 @@ -14,7 +14,7 @@ # along with Printrun. If not, see . import wx -from .bufferedcanvas import BufferedCanvas +from printrun.gui.xybuttons import FocusCanvas from printrun.utils import imagefile def sign(n): @@ -22,7 +22,7 @@ elif n > 0: return 1 else: return 0 -class ZButtons(BufferedCanvas): +class ZButtons(FocusCanvas): button_ydistances = [7, 30, 55, 83] # ,112 move_values = [0.1, 1, 10] center = (30, 118) @@ -44,22 +44,32 @@ self.lastValue = None self.bgcolor = wx.Colour() - self.bgcolor.SetFromName(bgcolor) + self.bgcolor.Set(bgcolor) self.bgcolormask = wx.Colour(self.bgcolor.Red(), self.bgcolor.Green(), self.bgcolor.Blue(), 128) - BufferedCanvas.__init__(self, parent, ID, size=self.bg_bmp.GetSize()) + # On MS Windows super(style=WANTS_CHARS) prevents tab cycling + # pass empty style explicitly + super().__init__(parent, ID, size=self.bg_bmp.GetSize(), style=0) # Set up mouse and keyboard event capture self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown) self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow) + self.Bind(wx.EVT_SET_FOCUS, self.RefreshFocus) + self.Bind(wx.EVT_KILL_FOCUS, self.RefreshFocus) + + def RefreshFocus(self, evt): + self.Refresh() + evt.Skip() def disable(self): + self.Enabled = False # prevents focus self.enabled = False self.update() def enable(self): + self.Enabled = True self.enabled = True self.update() @@ -123,6 +133,7 @@ gc.SetPen(wx.Pen(self.bgcolor, 0)) gc.SetBrush(wx.Brush(self.bgcolormask)) gc.DrawRectangle(0, 0, w, h) + self.drawFocusRect(dc) # ------ # # Events # @@ -146,7 +157,7 @@ mpos = event.GetPosition() r, d = self.getRangeDir(mpos) - if r >= 0: + if r is not None and r >= 0: value = d * self.move_values[r] if self.moveCallback: self.lastValue = value diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/gviz.py --- a/printrun-src/printrun/gviz.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/gviz.py Wed Jan 20 10:15:13 2021 +0100 @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Printrun. If not, see . -from Queue import Queue +from queue import Queue from collections import deque import numpy import wx @@ -38,15 +38,15 @@ vbox = wx.BoxSizer(wx.VERTICAL) self.toolbar = wx.ToolBar(panel, -1, style = wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_HORZ_TEXT) - self.toolbar.AddSimpleTool(1, wx.Image(imagefile('zoom_in.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Zoom In [+]"), '') - self.toolbar.AddSimpleTool(2, wx.Image(imagefile('zoom_out.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Zoom Out [-]"), '') + self.toolbar.AddTool(1, '', wx.Image(imagefile('zoom_in.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Zoom In [+]"),) + self.toolbar.AddTool(2, '', wx.Image(imagefile('zoom_out.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Zoom Out [-]")) self.toolbar.AddSeparator() - self.toolbar.AddSimpleTool(3, wx.Image(imagefile('arrow_up.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Move Up a Layer [U]"), '') - self.toolbar.AddSimpleTool(4, wx.Image(imagefile('arrow_down.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Move Down a Layer [D]"), '') - self.toolbar.AddLabelTool(5, " " + _("Reset view"), wx.Image(imagefile('reset.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), shortHelp = _("Reset view"), longHelp = '') + self.toolbar.AddTool(3, '', wx.Image(imagefile('arrow_up.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Move Up a Layer [U]")) + self.toolbar.AddTool(4, '', wx.Image(imagefile('arrow_down.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Move Down a Layer [D]")) + self.toolbar.AddTool(5, " " + _("Reset view"), wx.Image(imagefile('reset.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), shortHelp = _("Reset view")) self.toolbar.AddSeparator() - self.toolbar.AddSimpleTool(6, wx.Image(imagefile('inject.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), shortHelpString = _("Inject G-Code"), longHelpString = _("Insert code at the beginning of this layer")) - self.toolbar.AddSimpleTool(7, wx.Image(imagefile('edit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), shortHelpString = _("Edit layer"), longHelpString = _("Edit the G-Code of this layer")) + self.toolbar.AddTool(6, '', wx.Image(imagefile('inject.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), wx.NullBitmap, shortHelp = _("Inject G-Code"), longHelp = _("Insert code at the beginning of this layer")) + self.toolbar.AddTool(7, '', wx.Image(imagefile('edit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), wx.NullBitmap, shortHelp = _("Edit layer"), longHelp = _("Edit the G-Code of this layer")) vbox.Add(self.toolbar, 0, border = 5) @@ -120,7 +120,7 @@ if self.initpos is not None: self.initpos = None elif event.Dragging(): - e = event.GetPositionTuple() + e = event.GetPosition() if self.initpos is None: self.initpos = e self.basetrans = self.p.translate @@ -157,7 +157,8 @@ if z > 0: self.p.zoom(event.GetX(), event.GetY(), 1.2) elif z < 0: self.p.zoom(event.GetX(), event.GetY(), 1 / 1.2) -class Gviz(wx.Panel): +from printrun.gui.viz import BaseViz +class Gviz(wx.Panel, BaseViz): # Mark canvas as dirty when setting showall _showall = 0 @@ -197,19 +198,19 @@ self.arcpen = wx.Pen(wx.Colour(255, 0, 0), penwidth) self.travelpen = wx.Pen(wx.Colour(10, 80, 80), penwidth) self.hlpen = wx.Pen(wx.Colour(200, 50, 50), penwidth) - self.fades = [wx.Pen(wx.Colour(250 - 0.6 ** i * 100, 250 - 0.6 ** i * 100, 200 - 0.4 ** i * 50), penwidth) for i in xrange(6)] - self.penslist = [self.mainpen, self.travelpen, self.hlpen] + self.fades + self.fades = [wx.Pen(wx.Colour(int(250 - 0.6 ** i * 100), int(250 - 0.6 ** i * 100), int(200 - 0.4 ** i * 50)), penwidth) for i in range(6)] + self.penslist = [self.mainpen, self.arcpen, self.travelpen, self.hlpen] + self.fades self.bgcolor = wx.Colour() - self.bgcolor.SetFromName(bgcolor) - self.blitmap = wx.EmptyBitmap(self.GetClientSize()[0], self.GetClientSize()[1], -1) + self.bgcolor.Set(bgcolor) + self.blitmap = wx.Bitmap(self.GetClientSize()[0], self.GetClientSize()[1], -1) self.paint_overlay = None def inject(self): - layer = self.layers.index(self.layerindex) + layer = self.layers[self.layerindex] injector(self.gcode, self.layerindex, layer) def editlayer(self): - layer = self.layers.index(self.layerindex) + layer = self.layers[self.layerindex] injector_edit(self.gcode, self.layerindex, layer) def clearhilights(self): @@ -275,7 +276,7 @@ def resize(self, event): old_basescale = self.basescale - width, height = self.GetClientSizeTuple() + width, height = self.GetClientSize() if width < 1 or height < 1: return self.size = (width, height) @@ -312,20 +313,20 @@ self.scale[1] * x[5],) def _drawlines(self, dc, lines, pens): - scaled_lines = map(self._line_scaler, lines) + scaled_lines = [self._line_scaler(l) for l in lines] dc.DrawLineList(scaled_lines, pens) def _drawarcs(self, dc, arcs, pens): - scaled_arcs = map(self._arc_scaler, arcs) + scaled_arcs = [self._arc_scaler(a) for a in arcs] dc.SetBrush(wx.TRANSPARENT_BRUSH) for i in range(len(scaled_arcs)): - dc.SetPen(pens[i] if type(pens) == list else pens) + dc.SetPen(pens[i] if isinstance(pens, numpy.ndarray) else pens) dc.DrawArc(*scaled_arcs[i]) def repaint_everything(self): width = self.scale[0] * self.build_dimensions[0] height = self.scale[1] * self.build_dimensions[1] - self.blitmap = wx.EmptyBitmap(width + 1, height + 1, -1) + self.blitmap = wx.Bitmap(width + 1, height + 1, -1) dc = wx.MemoryDC() dc.SelectObject(self.blitmap) dc.SetBackground(wx.Brush((250, 250, 200))) @@ -333,10 +334,10 @@ dc.SetPen(wx.Pen(wx.Colour(180, 180, 150))) for grid_unit in self.grid: if grid_unit > 0: - for x in xrange(int(self.build_dimensions[0] / grid_unit) + 1): + for x in range(int(self.build_dimensions[0] / grid_unit) + 1): draw_x = self.scale[0] * x * grid_unit dc.DrawLine(draw_x, 0, draw_x, height) - for y in xrange(int(self.build_dimensions[1] / grid_unit) + 1): + for y in range(int(self.build_dimensions[1] / grid_unit) + 1): draw_y = self.scale[1] * (self.build_dimensions[1] - y * grid_unit) dc.DrawLine(0, draw_y, width, draw_y) dc.SetPen(wx.Pen(wx.Colour(0, 0, 0))) @@ -418,10 +419,10 @@ self.gcode = gcode self.showall = showall generator = self.add_parsed_gcodes(gcode) - generator_output = generator.next() + generator_output = next(generator) while generator_output is not None: yield generator_output - generator_output = generator.next() + generator_output = next(generator) max_layers = len(self.layers) if hasattr(self.parent, "layerslider"): self.parent.layerslider.SetRange(0, max_layers - 1) @@ -430,7 +431,7 @@ def addfile(self, gcode = None, showall = False): generator = self.addfile_perlayer(gcode, showall) - while generator.next() is not None: + while next(generator) is not None: continue def _get_movement(self, start_pos, gline): @@ -501,7 +502,7 @@ if line is not None: self.lines[viz_layer].append(line) - self.pens[viz_layer].append(self.mainpen if target[3] != self.lastpos[3] else self.travelpen) + self.pens[viz_layer].append(self.mainpen if target[3] != self.lastpos[3] or gline.extruding else self.travelpen) elif arc is not None: self.arcs[viz_layer].append(arc) self.arcpens[viz_layer].append(self.arcpen) diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/objectplater.py --- a/printrun-src/printrun/objectplater.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/objectplater.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # This file is part of the Printrun suite. # # Printrun is free software: you can redistribute it and/or modify @@ -35,14 +33,22 @@ def __init__(self, **kwargs): self.destroy_on_done = False parent = kwargs.get("parent", None) - super(PlaterPanel, self).__init__(parent = parent) + super().__init__(parent = parent) self.prepare_ui(**kwargs) def prepare_ui(self, filenames = [], callback = None, parent = None, build_dimensions = None): self.filenames = filenames - self.mainsizer = wx.BoxSizer(wx.HORIZONTAL) - panel = self.menupanel = wx.Panel(self, -1) + panel = self.menupanel = wx.Panel(self) sizer = self.menusizer = wx.GridBagSizer() + # Load button + loadbutton = wx.Button(panel, label = _("Load")) + loadbutton.Bind(wx.EVT_BUTTON, self.load) + sizer.Add(loadbutton, pos = (0, 0), span = (1, 1), flag = wx.EXPAND) + # Export button + exportbutton = wx.Button(panel, label = _("Export")) + exportbutton.Bind(wx.EVT_BUTTON, self.export) + sizer.Add(exportbutton, pos = (0, 1), span = (1, 1), flag = wx.EXPAND) + self.l = wx.ListBox(panel) sizer.Add(self.l, pos = (1, 0), span = (1, 2), flag = wx.EXPAND) sizer.AddGrowableRow(1, 1) @@ -50,10 +56,6 @@ clearbutton = wx.Button(panel, label = _("Clear")) clearbutton.Bind(wx.EVT_BUTTON, self.clear) sizer.Add(clearbutton, pos = (2, 0), span = (1, 2), flag = wx.EXPAND) - # Load button - loadbutton = wx.Button(panel, label = _("Load")) - loadbutton.Bind(wx.EVT_BUTTON, self.load) - sizer.Add(loadbutton, pos = (0, 0), span = (1, 1), flag = wx.EXPAND) # Snap to Z = 0 button snapbutton = wx.Button(panel, label = _("Snap to Z = 0")) snapbutton.Bind(wx.EVT_BUTTON, self.snap) @@ -70,10 +72,6 @@ autobutton = wx.Button(panel, label = _("Auto arrange")) autobutton.Bind(wx.EVT_BUTTON, self.autoplate) sizer.Add(autobutton, pos = (5, 0), span = (1, 2), flag = wx.EXPAND) - # Export button - exportbutton = wx.Button(panel, label = _("Export")) - exportbutton.Bind(wx.EVT_BUTTON, self.export) - sizer.Add(exportbutton, pos = (0, 1), span = (1, 1), flag = wx.EXPAND) if callback is not None: donebutton = wx.Button(panel, label = _("Done")) donebutton.Bind(wx.EVT_BUTTON, lambda e: self.done(e, callback)) @@ -83,24 +81,22 @@ sizer.Add(cancelbutton, pos = (6, 1), span = (1, 1), flag = wx.EXPAND) self.basedir = "." self.models = {} - panel.SetSizerAndFit(sizer) + panel.SetSizer(sizer) + self.mainsizer = wx.BoxSizer(wx.HORIZONTAL) self.mainsizer.Add(panel, flag = wx.EXPAND) self.SetSizer(self.mainsizer) - if build_dimensions: - self.build_dimensions = build_dimensions - else: - self.build_dimensions = [200, 200, 100, 0, 0, 0] + self.build_dimensions = build_dimensions or [200, 200, 100, 0, 0, 0] def set_viewer(self, viewer): # Patch handle_rotation on the fly if hasattr(viewer, "handle_rotation"): def handle_rotation(self, event, orig_handler): if self.initpos is None: - self.initpos = event.GetPositionTuple() + self.initpos = event.GetPosition() else: if event.ShiftDown(): p1 = self.initpos - p2 = event.GetPositionTuple() + p2 = event.GetPosition() x1, y1, _ = self.mouse_to_3d(p1[0], p1[1]) x2, y2, _ = self.mouse_to_3d(p2[0], p2[1]) self.parent.move_shape((x2 - x1, y2 - y1)) @@ -112,12 +108,10 @@ if hasattr(viewer, "handle_wheel"): def handle_wheel(self, event, orig_handler): if event.ShiftDown(): - delta = event.GetWheelRotation() angle = 10 - if delta > 0: - self.parent.rotate_shape(angle / 2) - else: - self.parent.rotate_shape(-angle / 2) + if event.GetWheelRotation() < 0: + angle = -angle + self.parent.rotate_shape(angle / 2) else: orig_handler(event) patch_method(viewer, "handle_wheel", handle_wheel) @@ -246,8 +240,8 @@ def add_model(self, name, model): newname = os.path.split(name.lower())[1] - if not isinstance(newname, unicode): - newname = unicode(newname, "utf-8") + if not isinstance(newname, str): + newname = str(newname, "utf-8") c = 1 while newname in self.models: newname = os.path.split(name.lower())[1] diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/packer.py --- a/printrun-src/printrun/packer.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/packer.py Wed Jan 20 10:15:13 2021 +0100 @@ -23,7 +23,7 @@ import Polygon.Utils -class Vector2(object): +class Vector2: """Simple 2d vector / point class.""" def __init__(self, x=0, y=0): @@ -60,7 +60,7 @@ ) -class Rect(object): +class Rect: """Simple rectangle object.""" def __init__(self, width, height, data={}): self.width = width @@ -110,7 +110,7 @@ return self.width * self.height -class PointList(object): +class PointList: """Methods for transforming a list of points.""" def __init__(self, points=[]): self.points = points @@ -142,7 +142,7 @@ return segs -class LineSegment(object): +class LineSegment: def __init__(self, start, end): self.start = start self.end = end @@ -177,7 +177,7 @@ return closest_point.distance(point) -class Packer(object): +class Packer: def __init__(self): self._rects = [] diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/plugins/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/printrun-src/printrun/plugins/__init__.py Wed Jan 20 10:15:13 2021 +0100 @@ -0,0 +1,20 @@ +# This file is part of the Printrun suite. +# +# Printrun is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Printrun is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Printrun. If not, see . + +#from printrun.plugins.sample import SampleHandler +# +#PRINTCORE_HANDLER = [SampleHandler()] +PRINTCORE_HANDLER = [] + diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/plugins/sample.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/printrun-src/printrun/plugins/sample.py Wed Jan 20 10:15:13 2021 +0100 @@ -0,0 +1,67 @@ +# This file is part of the Printrun suite. +# +# Printrun is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Printrun is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Printrun. If not, see . + +from printrun.eventhandler import PrinterEventHandler + +class SampleHandler(PrinterEventHandler): + ''' + Sample event handler for printcore. + ''' + + def __init__(self): + pass + + def __write(self, field, text = ""): + print("%-15s - %s" % (field, text)) + + def on_init(self): + self.__write("on_init") + + def on_send(self, command, gline): + self.__write("on_send", command) + + def on_recv(self, line): + self.__write("on_recv", line.strip()) + + def on_connect(self): + self.__write("on_connect") + + def on_disconnect(self): + self.__write("on_disconnect") + + def on_error(self, error): + self.__write("on_error", error) + + def on_online(self): + self.__write("on_online") + + def on_temp(self, line): + self.__write("on_temp", line) + + def on_start(self, resume): + self.__write("on_start", "true" if resume else "false") + + def on_end(self): + self.__write("on_end") + + def on_layerchange(self, layer): + self.__write("on_layerchange", "%f" % (layer)) + + def on_preprintsend(self, gline, index, mainqueue): + self.__write("on_preprintsend", gline) + + def on_printsend(self, gline): + self.__write("on_printsend", gline) + diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/power/__init__.py --- a/printrun-src/printrun/power/__init__.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/power/__init__.py Wed Jan 20 10:15:13 2021 +0100 @@ -39,10 +39,16 @@ inhibit_sleep_token = None bus = dbus.SessionBus() try: - # GNOME uses the right object path, try it first - service_name = "org.freedesktop.ScreenSaver" - proxy = bus.get_object(service_name, - "/org/freedesktop/ScreenSaver") + if os.environ.get('DESKTOP_SESSION') == "mate": + # Mate uses a special service + service_name = "org.mate.ScreenSaver" + object_path = "/org/mate/ScreenSaver" + else: + # standard service name + service_name = "org.freedesktop.ScreenSaver" + object_path = "/org/freedesktop/ScreenSaver" + # GNOME and Mate use the right object path, try it first + proxy = bus.get_object(service_name, object_path) inhibit_sleep_handler = dbus.Interface(proxy, service_name) # Do a test run token = inhibit_sleep_handler.Inhibit("printrun", "test") @@ -65,7 +71,7 @@ return inhibit_sleep_handler.UnInhibit(inhibit_sleep_token) inhibit_sleep_token = None - except Exception, e: + except Exception as e: logging.warning("Could not setup DBus for sleep inhibition: %s" % e) def inhibit_sleep(reason): @@ -107,7 +113,7 @@ set_nice(i, p) high_priority_nice = i break - except psutil.AccessDenied, e: + except psutil.AccessDenied as e: pass set_nice(orig_nice, p) @@ -132,7 +138,7 @@ def powerset_print_stop(): reset_priority() deinhibit_sleep() -except ImportError, e: +except ImportError as e: logging.warning("psutil unavailable, could not import power utils:" + str(e)) def powerset_print_start(reason): diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/power/osx.py --- a/printrun-src/printrun/power/osx.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/power/osx.py Wed Jan 20 10:15:13 2021 +0100 @@ -41,6 +41,7 @@ encoding = CoreFoundation.kCFStringEncodingASCII except AttributeError: encoding = 0x600 + string = string.encode('ascii') cfstring = CoreFoundation.CFStringCreateWithCString(None, string, encoding) return objc.pyobjc_id(cfstring.nsstring()) diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/printcore.py --- a/printrun-src/printrun/printcore.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/printcore.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # This file is part of the Printrun suite. # # Printrun is free software: you can redistribute it and/or modify @@ -15,29 +13,36 @@ # You should have received a copy of the GNU General Public License # along with Printrun. If not, see . -__version__ = "2015.03.10" +__version__ = "2.0.0rc7" -from serialWrapper import Serial, SerialException, PARITY_ODD, PARITY_NONE +import sys +if sys.version_info.major < 3: + print("You need to run this on Python 3") + sys.exit(-1) + +from serial import Serial, SerialException, PARITY_ODD, PARITY_NONE from select import error as SelectError import threading -from Queue import Queue, Empty as QueueEmpty +from queue import Queue, Empty as QueueEmpty import time import platform import os -import sys -stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr -reload(sys).setdefaultencoding('utf8') -sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr import logging import traceback import errno import socket import re -from functools import wraps +import selectors +from functools import wraps, reduce from collections import deque from printrun import gcoder -from .utils import install_locale, decode_utf8 +from .utils import set_utf8_locale, install_locale, decode_utf8 +try: + set_utf8_locale() +except: + pass install_locale('pronterface') +from printrun.plugins import PRINTCORE_HANDLER def locked(f): @wraps(f) @@ -61,6 +66,11 @@ def disable_hup(port): control_ttyhup(port, True) +PR_EOF = None #printrun's marker for EOF +PR_AGAIN = b'' #printrun's marker for timeout/no data +SYS_EOF = b'' #python's marker for EOF +SYS_AGAIN = None #python's marker for timeout/no data + class printcore(): def __init__(self, port = None, baud = None, dtr=None): """Initializes a printcore instance. Pass the port and baud rate to @@ -108,12 +118,34 @@ self.send_thread = None self.stop_send_thread = False self.print_thread = None + self.readline_buf = [] + self.selector = None + self.event_handler = PRINTCORE_HANDLER + # Not all platforms need to do this parity workaround, and some drivers + # don't support it. Limit it to platforms that actually require it + # here to avoid doing redundant work elsewhere and potentially breaking + # things. + self.needs_parity_workaround = platform.system() == "linux" and os.path.exists("/etc/debian") + for handler in self.event_handler: + try: handler.on_init() + except: logging.error(traceback.format_exc()) if port is not None and baud is not None: self.connect(port, baud) self.xy_feedrate = None self.z_feedrate = None + def addEventHandler(self, handler): + ''' + Adds an event handler. + + @param handler: The handler to be added. + ''' + self.event_handler.append(handler) + def logError(self, error): + for handler in self.event_handler: + try: handler.on_error(error) + except: logging.error(traceback.format_exc()) if self.errorcb: try: self.errorcb(error) except: logging.error(traceback.format_exc()) @@ -135,11 +167,23 @@ self.print_thread.join() self._stop_sender() try: + if self.selector is not None: + self.selector.unregister(self.printer_tcp) + self.selector.close() + self.selector = None + if self.printer_tcp is not None: + self.printer_tcp.close() + self.printer_tcp = None self.printer.close() except socket.error: + logger.error(traceback.format_exc()) pass except OSError: + logger.error(traceback.format_exc()) pass + for handler in self.event_handler: + try: handler.on_disconnect() + except: logging.error(traceback.format_exc()) self.printer = None self.online = False self.printing = False @@ -160,13 +204,13 @@ # Connect to socket if "port" is an IP, device if not host_regexp = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$") is_serial = True - if ":" in port: - bits = port.split(":") + if ":" in self.port: + bits = self.port.split(":") if len(bits) == 2: hostname = bits[0] try: - port = int(bits[1]) - if host_regexp.match(hostname) and 1 <= port <= 65535: + port_number = int(bits[1]) + if host_regexp.match(hostname) and 1 <= port_number <= 65535: is_serial = False except: pass @@ -178,12 +222,17 @@ self.timeout = 0.25 self.printer_tcp.settimeout(1.0) try: - self.printer_tcp.connect((hostname, port)) - self.printer_tcp.settimeout(self.timeout) - self.printer = self.printer_tcp.makefile() + self.printer_tcp.connect((hostname, port_number)) + #a single read timeout raises OSError for all later reads + #probably since python 3.5 + #use non blocking instead + self.printer_tcp.settimeout(0) + self.printer = self.printer_tcp.makefile('rwb', buffering=0) + self.selector = selectors.DefaultSelector() + self.selector.register(self.printer_tcp, selectors.EVENT_READ) except socket.error as e: if(e.strerror is None): e.strerror="" - self.logError(_("Could not connect to %s:%s:") % (hostname, port) + + self.logError(_("Could not connect to %s:%s:") % (hostname, port_number) + "\n" + _("Socket error %s:") % e.errno + "\n" + e.strerror) self.printer = None @@ -193,14 +242,20 @@ disable_hup(self.port) self.printer_tcp = None try: - self.printer = Serial(port = self.port, - baudrate = self.baud, - timeout = 0.25, - parity = PARITY_ODD) - self.printer.close() - self.printer.parity = PARITY_NONE + if self.needs_parity_workaround: + self.printer = Serial(port = self.port, + baudrate = self.baud, + timeout = 0.25, + parity = PARITY_ODD) + self.printer.close() + self.printer.parity = PARITY_NONE + else: + self.printer = Serial(baudrate = self.baud, + timeout = 0.25, + parity = PARITY_NONE) + self.printer.port = self.port try: #this appears not to work on many platforms, so we're going to call it but not care if it fails - self.printer.setDTR(dtr); + self.printer.dtr = dtr except: #self.logError(_("Could not set DTR on this platform")) #not sure whether to output an error message pass @@ -215,8 +270,12 @@ "\n" + _("IO error: %s") % e) self.printer = None return + for handler in self.event_handler: + try: handler.on_connect() + except: logging.error(traceback.format_exc()) self.stop_read_thread = False - self.read_thread = threading.Thread(target = self._listen) + self.read_thread = threading.Thread(target = self._listen, + name='read thread') self.read_thread.start() self._start_sender() @@ -224,43 +283,90 @@ """Reset the printer """ if self.printer and not self.printer_tcp: - self.printer.setDTR(1) + self.printer.dtr = 1 time.sleep(0.2) - self.printer.setDTR(0) + self.printer.dtr = 0 + + def _readline_buf(self): + "Try to readline from buffer" + if len(self.readline_buf): + chunk = self.readline_buf[-1] + eol = chunk.find(b'\n') + if eol >= 0: + line = b''.join(self.readline_buf[:-1]) + chunk[:(eol+1)] + self.readline_buf = [] + if eol + 1 < len(chunk): + self.readline_buf.append(chunk[(eol+1):]) + return line + return PR_AGAIN + + def _readline_nb(self): + "Non blocking readline. Socket based files do not support non blocking or timeouting readline" + if self.printer_tcp: + line = self._readline_buf() + if line: + return line + chunk_size = 256 + while True: + chunk = self.printer.read(chunk_size) + if chunk is SYS_AGAIN and self.selector.select(self.timeout): + chunk = self.printer.read(chunk_size) + #print('_readline_nb chunk', chunk, type(chunk)) + if chunk: + self.readline_buf.append(chunk) + line = self._readline_buf() + if line: + return line + elif chunk is SYS_AGAIN: + return PR_AGAIN + else: + #chunk == b'' means EOF + line = b''.join(self.readline_buf) + self.readline_buf = [] + self.stop_read_thread = True + return line if line else PR_EOF + else: # serial port + return self.printer.readline() def _readline(self): try: - try: - line = self.printer.readline() - if self.printer_tcp and not line: - raise OSError(-1, "Read EOF from socket") - except socket.timeout: - return "" + line_bytes = self._readline_nb() + if line_bytes is PR_EOF: + self.logError(_("Can't read from printer (disconnected?). line_bytes is None")) + return PR_EOF + line = line_bytes.decode('utf-8') if len(line) > 1: self.log.append(line) + for handler in self.event_handler: + try: handler.on_recv(line) + except: logging.error(traceback.format_exc()) if self.recvcb: try: self.recvcb(line) except: self.logError(traceback.format_exc()) if self.loud: logging.info("RECV: %s" % line.rstrip()) return line + except UnicodeDecodeError: + self.logError(_("Got rubbish reply from %s at baudrate %s:") % (self.port, self.baud) + + "\n" + _("Maybe a bad baudrate?")) + return None except SelectError as e: if 'Bad file descriptor' in e.args[1]: - self.logError(_(u"Can't read from printer (disconnected?) (SelectError {0}): {1}").format(e.errno, decode_utf8(e.strerror))) + self.logError(_("Can't read from printer (disconnected?) (SelectError {0}): {1}").format(e.errno, decode_utf8(e.strerror))) return None else: - self.logError(_(u"SelectError ({0}): {1}").format(e.errno, decode_utf8(e.strerror))) + self.logError(_("SelectError ({0}): {1}").format(e.errno, decode_utf8(e.strerror))) raise except SerialException as e: - self.logError(_(u"Can't read from printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e)))) + self.logError(_("Can't read from printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e)))) return None except socket.error as e: - self.logError(_(u"Can't read from printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror))) + self.logError(_("Can't read from printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror))) return None except OSError as e: if e.errno == errno.EAGAIN: # Not a real error, no data was available return "" - self.logError(_(u"Can't read from printer (disconnected?) (OS Error {0}): {1}").format(e.errno, e.strerror)) + self.logError(_("Can't read from printer (disconnected?) (OS Error {0}): {1}").format(e.errno, e.strerror)) return None def _listen_can_continue(self): @@ -268,7 +374,7 @@ return not self.stop_read_thread and self.printer return (not self.stop_read_thread and self.printer - and self.printer.isOpen()) + and self.printer.is_open) def _listen_until_online(self): while not self.online and self._listen_can_continue(): @@ -296,6 +402,9 @@ if line.startswith(tuple(self.greetings)) \ or line.startswith('ok') or "T:" in line: self.online = True + for handler in self.event_handler: + try: handler.on_online() + except: logging.error(traceback.format_exc()) if self.onlinecb: try: self.onlinecb() except: self.logError(traceback.format_exc()) @@ -310,15 +419,20 @@ while self._listen_can_continue(): line = self._readline() if line is None: + logging.debug('_readline() is None, exiting _listen()') break if line.startswith('DEBUG_'): continue if line.startswith(tuple(self.greetings)) or line.startswith('ok'): self.clear = True - if line.startswith('ok') and "T:" in line and self.tempcb: - # callback for temp, status, whatever - try: self.tempcb(line) - except: self.logError(traceback.format_exc()) + if line.startswith('ok') and "T:" in line: + for handler in self.event_handler: + try: handler.on_temp(line) + except: logging.error(traceback.format_exc()) + if self.tempcb: + # callback for temp, status, whatever + try: self.tempcb(line) + except: self.logError(traceback.format_exc()) elif line.startswith('Error'): self.logError(line) # Teststrings for resend parsing # Firmware exp. result @@ -336,10 +450,12 @@ pass self.clear = True self.clear = True + logging.debug('Exiting read thread') def _start_sender(self): self.stop_send_thread = False - self.send_thread = threading.Thread(target = self._sender) + self.send_thread = threading.Thread(target = self._sender, + name = 'send thread') self.send_thread.start() def _stop_sender(self): @@ -383,6 +499,7 @@ self.clear = False resuming = (startindex != 0) self.print_thread = threading.Thread(target = self._print, + name = 'print thread', kwargs = {"resuming": resuming}) self.print_thread.start() return True @@ -413,17 +530,12 @@ self.paused = True self.printing = False - # try joining the print thread: enclose it in try/except because we - # might be calling it from the thread itself - try: - self.print_thread.join() - except RuntimeError, e: - if e.message == "cannot join current thread": - pass - else: + # ';@pause' in the gcode file calls pause from the print thread + if not threading.current_thread() is self.print_thread: + try: + self.print_thread.join() + except: self.logError(traceback.format_exc()) - except: - self.logError(traceback.format_exc()) self.print_thread = None @@ -434,35 +546,33 @@ self.pauseE = self.analyzer.abs_e self.pauseF = self.analyzer.current_f self.pauseRelative = self.analyzer.relative + self.pauseRelativeE = self.analyzer.relative_e def resume(self): - """Resumes a paused print. - """ + """Resumes a paused print.""" if not self.paused: return False - if self.paused: - # restores the status - self.send_now("G90") # go to absolute coordinates + # restores the status + self.send_now("G90") # go to absolute coordinates + + xyFeed = '' if self.xy_feedrate is None else ' F' + str(self.xy_feedrate) + zFeed = '' if self.z_feedrate is None else ' F' + str(self.z_feedrate) - xyFeedString = "" - zFeedString = "" - if self.xy_feedrate is not None: - xyFeedString = " F" + str(self.xy_feedrate) - if self.z_feedrate is not None: - zFeedString = " F" + str(self.z_feedrate) + self.send_now("G1 X%s Y%s%s" % (self.pauseX, self.pauseY, xyFeed)) + self.send_now("G1 Z" + str(self.pauseZ) + zFeed) + self.send_now("G92 E" + str(self.pauseE)) - self.send_now("G1 X%s Y%s%s" % (self.pauseX, self.pauseY, - xyFeedString)) - self.send_now("G1 Z" + str(self.pauseZ) + zFeedString) - self.send_now("G92 E" + str(self.pauseE)) - - # go back to relative if needed - if self.pauseRelative: self.send_now("G91") - # reset old feed rate - self.send_now("G1 F" + str(self.pauseF)) + # go back to relative if needed + if self.pauseRelative: + self.send_now("G91") + if self.pauseRelativeE: + self.send_now('M83') + # reset old feed rate + self.send_now("G1 F" + str(self.pauseF)) self.paused = False self.printing = True self.print_thread = threading.Thread(target = self._print, + name = 'print thread', kwargs = {"resuming": True}) self.print_thread.start() @@ -489,6 +599,9 @@ def _print(self, resuming = False): self._stop_sender() try: + for handler in self.event_handler: + try: handler.on_start(resuming) + except: logging.error(traceback.format_exc()) if self.startcb: # callback for printing started try: self.startcb(resuming) @@ -500,6 +613,9 @@ self.sentlines = {} self.log.clear() self.sent = [] + for handler in self.event_handler: + try: handler.on_end() + except: logging.error(traceback.format_exc()) if self.endcb: # callback for printing done try: self.endcb() @@ -540,16 +656,25 @@ self._send(self.priqueue.get_nowait()) self.priqueue.task_done() return - if self.printing and self.queueindex < len(self.mainqueue): + if self.printing and self.mainqueue.has_index(self.queueindex): (layer, line) = self.mainqueue.idxs(self.queueindex) gline = self.mainqueue.all_layers[layer][line] + if self.queueindex > 0: + (prev_layer, prev_line) = self.mainqueue.idxs(self.queueindex - 1) + if prev_layer != layer: + for handler in self.event_handler: + try: handler.on_layerchange(layer) + except: logging.error(traceback.format_exc()) if self.layerchangecb and self.queueindex > 0: (prev_layer, prev_line) = self.mainqueue.idxs(self.queueindex - 1) if prev_layer != layer: try: self.layerchangecb(layer) except: self.logError(traceback.format_exc()) + for handler in self.event_handler: + try: handler.on_preprintsend(gline, self.queueindex, self.mainqueue) + except: logging.error(traceback.format_exc()) if self.preprintsendcb: - if self.queueindex + 1 < len(self.mainqueue): + if self.mainqueue.has_index(self.queueindex + 1): (next_layer, next_line) = self.mainqueue.idxs(self.queueindex + 1) next_gline = self.mainqueue.all_layers[next_layer][next_line] else: @@ -571,6 +696,9 @@ if tline: self._send(tline, self.lineno, True) self.lineno += 1 + for handler in self.event_handler: + try: handler.on_printsend(gline) + except: logging.error(traceback.format_exc()) if self.printsendcb: try: self.printsendcb(gline) except: self.logError(traceback.format_exc()) @@ -603,11 +731,15 @@ "\n" + traceback.format_exc()) if self.loud: logging.info("SENT: %s" % command) + + for handler in self.event_handler: + try: handler.on_send(command, gline) + except: logging.error(traceback.format_exc()) if self.sendcb: try: self.sendcb(command, gline) except: self.logError(traceback.format_exc()) try: - self.printer.write(str(command + "\n")) + self.printer.write((command + "\n").encode('ascii')) if self.printer_tcp: try: self.printer.flush() @@ -616,14 +748,14 @@ self.writefailures = 0 except socket.error as e: if e.errno is None: - self.logError(_(u"Can't write to printer (disconnected ?):") + + self.logError(_("Can't write to printer (disconnected ?):") + "\n" + traceback.format_exc()) else: - self.logError(_(u"Can't write to printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror))) + self.logError(_("Can't write to printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror))) self.writefailures += 1 except SerialException as e: - self.logError(_(u"Can't write to printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e)))) + self.logError(_("Can't write to printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e)))) self.writefailures += 1 except RuntimeError as e: - self.logError(_(u"Socket connection broken, disconnected. ({0}): {1}").format(e.errno, decode_utf8(e.strerror))) + self.logError(_("Socket connection broken, disconnected. ({0}): {1}").format(e.errno, decode_utf8(e.strerror))) self.writefailures += 1 diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/projectlayer.py --- a/printrun-src/printrun/projectlayer.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/projectlayer.py Wed Jan 20 10:15:13 2021 +0100 @@ -22,12 +22,11 @@ import tempfile import shutil from cairosvg.surface import PNGSurface -import cStringIO +import io import imghdr import copy import re from collections import OrderedDict -import itertools import math class DisplayFrame(wx.Frame): @@ -36,8 +35,8 @@ self.printer = printer self.control_frame = parent self.pic = wx.StaticBitmap(self) - self.bitmap = wx.EmptyBitmap(*res) - self.bbitmap = wx.EmptyBitmap(*res) + self.bitmap = wx.Bitmap(*res) + self.bbitmap = wx.Bitmap(*res) self.slicer = 'bitmap' self.dpi = 96 dc = wx.MemoryDC() @@ -73,8 +72,8 @@ pass def resize(self, res = (1024, 768)): - self.bitmap = wx.EmptyBitmap(*res) - self.bbitmap = wx.EmptyBitmap(*res) + self.bitmap = wx.Bitmap(*res) + self.bbitmap = wx.Bitmap(*res) dc = wx.MemoryDC() dc.SelectObject(self.bbitmap) dc.SetBackground(wx.Brush("black")) @@ -101,9 +100,9 @@ g = layercopy.find("{http://www.w3.org/2000/svg}g") g.set('transform', 'scale(' + str(self.scale) + ')') - stream = cStringIO.StringIO(PNGSurface.convert(dpi = self.dpi, bytestring = xml.etree.ElementTree.tostring(layercopy))) + stream = io.StringIO(PNGSurface.convert(dpi = self.dpi, bytestring = xml.etree.ElementTree.tostring(layercopy))) else: - stream = cStringIO.StringIO(PNGSurface.convert(dpi = self.dpi, bytestring = xml.etree.ElementTree.tostring(image))) + stream = io.StringIO(PNGSurface.convert(dpi = self.dpi, bytestring = xml.etree.ElementTree.tostring(image))) pngImage = wx.ImageFromStream(stream) @@ -132,16 +131,16 @@ pass def show_img_delay(self, image): - print "Showing", str(time.clock()) + print("Showing", str(time.clock())) self.control_frame.set_current_layer(self.index) self.draw_layer(image) wx.FutureCall(1000 * self.interval, self.hide_pic_and_rise) def rise(self): if (self.direction == "Top Down"): - print "Lowering", str(time.clock()) + print("Lowering", str(time.clock())) else: - print "Rising", str(time.clock()) + print("Rising", str(time.clock())) if self.printer is not None and self.printer.online: self.printer.send_now("G91") @@ -170,7 +169,7 @@ wx.FutureCall(1000 * self.pause, self.next_img) def hide_pic(self): - print "Hiding", str(time.clock()) + print("Hiding", str(time.clock())) self.pic.Hide() def hide_pic_and_rise(self): @@ -181,11 +180,11 @@ if not self.running: return if self.index < len(self.layers): - print self.index + print(self.index) wx.CallAfter(self.show_img_delay, self.layers[self.index]) self.index += 1 else: - print "end" + print("end") wx.CallAfter(self.pic.Hide) wx.CallAfter(self.Refresh) @@ -278,38 +277,40 @@ # Left Column fieldsizer.Add(wx.StaticText(self.panel, -1, "Layer (mm):"), pos = (0, 0), flag = wx.ALIGN_CENTER_VERTICAL) - self.thickness = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_layer", "0.1")), size = (80, -1)) + self.thickness = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_layer", "0.1")), size = (125, -1)) self.thickness.Bind(wx.EVT_TEXT, self.update_thickness) self.thickness.SetHelpText("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(self.panel, -1, "Exposure (s):"), pos = (1, 0), flag = wx.ALIGN_CENTER_VERTICAL) - self.interval = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_interval", "0.5")), size = (80, -1)) + self.interval = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_interval", "0.5")), size = (125, -1)) self.interval.Bind(wx.EVT_TEXT, self.update_interval) self.interval.SetHelpText("How long each slice should be displayed.") fieldsizer.Add(self.interval, pos = (1, 1)) fieldsizer.Add(wx.StaticText(self.panel, -1, "Blank (s):"), pos = (2, 0), flag = wx.ALIGN_CENTER_VERTICAL) - self.pause = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_pause", "0.5")), size = (80, -1)) + self.pause = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_pause", "0.5")), size = (125, -1)) self.pause.Bind(wx.EVT_TEXT, self.update_pause) self.pause.SetHelpText("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(self.panel, -1, "Scale:"), pos = (3, 0), flag = wx.ALIGN_CENTER_VERTICAL) - self.scale = floatspin.FloatSpin(self.panel, -1, value = self._get_setting('project_scale', 1.0), increment = 0.1, digits = 3, size = (80, -1)) - self.scale.Bind(floatspin.EVT_FLOATSPIN, self.update_scale) + self.scale = wx.SpinCtrlDouble(self.panel, -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.SetHelpText("The additional scaling of each slice.") fieldsizer.Add(self.scale, pos = (3, 1)) fieldsizer.Add(wx.StaticText(self.panel, -1, "Direction:"), pos = (4, 0), flag = wx.ALIGN_CENTER_VERTICAL) - self.direction = wx.ComboBox(self.panel, -1, choices = ["Top Down", "Bottom Up"], value = self._get_setting('project_direction', "Top Down"), size = (80, -1)) + self.direction = wx.ComboBox(self.panel, -1, choices = ["Top Down", "Bottom Up"], value = self._get_setting('project_direction', "Top Down"), size = (125, -1)) self.direction.Bind(wx.EVT_COMBOBOX, self.update_direction) self.direction.SetHelpText("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(self.panel, -1, "Overshoot (mm):"), pos = (5, 0), flag = wx.ALIGN_CENTER_VERTICAL) - self.overshoot = floatspin.FloatSpin(self.panel, -1, value = self._get_setting('project_overshoot', 3.0), increment = 0.1, digits = 1, min_val = 0, size = (80, -1)) - self.overshoot.Bind(floatspin.EVT_FLOATSPIN, self.update_overshoot) + self.overshoot = wx.SpinCtrlDouble(self.panel, -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.SetHelpText("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)) @@ -329,38 +330,41 @@ fieldsizer.Add(wx.StaticText(self.panel, -1, "X (px):"), pos = (0, 2), flag = wx.ALIGN_CENTER_VERTICAL) projectX = int(math.floor(float(self._get_setting("project_x", 1920)))) - self.X = wx.SpinCtrl(self.panel, -1, str(projectX), max = 999999, size = (80, -1)) + self.X = wx.SpinCtrl(self.panel, -1, str(projectX), max = 999999, size = (125, -1)) self.X.Bind(wx.EVT_SPINCTRL, self.update_resolution) self.X.SetHelpText("The projector resolution in the X axis.") fieldsizer.Add(self.X, pos = (0, 3)) fieldsizer.Add(wx.StaticText(self.panel, -1, "Y (px):"), pos = (1, 2), flag = wx.ALIGN_CENTER_VERTICAL) projectY = int(math.floor(float(self._get_setting("project_y", 1200)))) - self.Y = wx.SpinCtrl(self.panel, -1, str(projectY), max = 999999, size = (80, -1)) + self.Y = wx.SpinCtrl(self.panel, -1, str(projectY), max = 999999, size = (125, -1)) self.Y.Bind(wx.EVT_SPINCTRL, self.update_resolution) self.Y.SetHelpText("The projector resolution in the Y axis.") fieldsizer.Add(self.Y, pos = (1, 3)) fieldsizer.Add(wx.StaticText(self.panel, -1, "OffsetX (mm):"), pos = (2, 2), flag = wx.ALIGN_CENTER_VERTICAL) - self.offset_X = floatspin.FloatSpin(self.panel, -1, value = self._get_setting("project_offset_x", 0.0), increment = 1, digits = 1, size = (80, -1)) - self.offset_X.Bind(floatspin.EVT_FLOATSPIN, self.update_offset) + self.offset_X = wx.SpinCtrlDouble(self.panel, -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.SetHelpText("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(self.panel, -1, "OffsetY (mm):"), pos = (3, 2), flag = wx.ALIGN_CENTER_VERTICAL) - self.offset_Y = floatspin.FloatSpin(self.panel, -1, value = self._get_setting("project_offset_y", 0.0), increment = 1, digits = 1, size = (80, -1)) - self.offset_Y.Bind(floatspin.EVT_FLOATSPIN, self.update_offset) + self.offset_Y = wx.SpinCtrlDouble(self.panel, -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.SetHelpText("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(self.panel, -1, "ProjectedX (mm):"), pos = (4, 2), flag = wx.ALIGN_CENTER_VERTICAL) - self.projected_X_mm = floatspin.FloatSpin(self.panel, -1, value = self._get_setting("project_projected_x", 505.0), increment = 1, digits = 1, size = (80, -1)) - self.projected_X_mm.Bind(floatspin.EVT_FLOATSPIN, self.update_projected_Xmm) + self.projected_X_mm = wx.SpinCtrlDouble(self.panel, -1, initial = self._get_setting("project_projected_x", 505.0), inc = 1, size = (125, -1)) + self.projected_X_mm.SetDigits(1) + self.projected_X_mm.Bind(wx.EVT_SPINCTRLDOUBLE, self.update_projected_Xmm) self.projected_X_mm.SetHelpText("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(self.panel, -1, "Z Axis Speed (mm/min):"), pos = (5, 2), flag = wx.ALIGN_CENTER_VERTICAL) - self.z_axis_rate = wx.SpinCtrl(self.panel, -1, str(self._get_setting("project_z_axis_rate", 200)), max = 9999, size = (80, -1)) + self.z_axis_rate = wx.SpinCtrl(self.panel, -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.SetHelpText("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)) @@ -394,7 +398,8 @@ first_layer_boxer.Add(self.first_layer, flag = wx.ALIGN_CENTER_VERTICAL) first_layer_boxer.Add(wx.StaticText(self.panel, -1, " (s):"), flag = wx.ALIGN_CENTER_VERTICAL) - self.show_first_layer_timer = floatspin.FloatSpin(self.panel, -1, value=-1, increment = 1, digits = 1, size = (55, -1)) + self.show_first_layer_timer = wx.SpinCtrlDouble(self.panel, -1, initial = -1, inc = 1, size = (125, -1)) + self.show_first_layer_timer.SetDigits(1) self.show_first_layer_timer.SetHelpText("How long to display the first layer for. -1 = unlimited.") first_layer_boxer.Add(self.show_first_layer_timer, flag = wx.ALIGN_CENTER_VERTICAL) displaysizer.Add(first_layer_boxer, pos = (0, 6), flag = wx.ALIGN_CENTER_VERTICAL) @@ -557,9 +562,9 @@ # 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) - vals = filter(os.path.isfile, os.listdir('.')) - keys = map(lambda p: int(re.search('\d+', p).group()), vals) - imagefilesDict = dict(itertools.izip(keys, vals)) + vals = [f for f in os.listdir('.') if os.path.isfile(f)] + keys = (int(re.search('\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(): @@ -584,8 +589,8 @@ layers = self.parse_svg(name) layerHeight = layers[1] self.thickness.SetValue(str(layers[1])) - print "Layer thickness detected:", layerHeight, "mm" - print len(layers[0]), "layers found, total height", layerHeight * len(layers[0]), "mm" + print("Layer thickness detected:", layerHeight, "mm") + print(len(layers[0]), "layers found, total height", layerHeight * len(layers[0]), "mm") self.layers = layers self.set_total_layers(len(layers[0])) self.set_current_layer(0) @@ -624,7 +629,7 @@ resolution_x_pixels = int(self.X.GetValue()) resolution_y_pixels = int(self.Y.GetValue()) - gridBitmap = wx.EmptyBitmap(resolution_x_pixels, resolution_y_pixels) + gridBitmap = wx.Bitmap(resolution_x_pixels, resolution_y_pixels) dc = wx.MemoryDC() dc.SelectObject(gridBitmap) dc.SetBackground(wx.Brush("black")) @@ -648,8 +653,8 @@ gridCountX = int(projectedXmm / 10) gridCountY = int(projectedYmm / 10) - for y in xrange(0, gridCountY + 1): - for x in xrange(0, gridCountX + 1): + for y in range(0, gridCountY + 1): + for x in range(0, gridCountX + 1): dc.DrawLine(0, y * (pixelsYPerMM * 10), resolution_x_pixels, y * (pixelsYPerMM * 10)) dc.DrawLine(x * (pixelsXPerMM * 10), 0, x * (pixelsXPerMM * 10), resolution_y_pixels) @@ -660,7 +665,7 @@ def present_first_layer(self, event): if (self.first_layer.GetValue()): if not hasattr(self, "layers"): - print "No model loaded!" + print("No model loaded!") self.first_layer.SetValue(False) return self.display_frame.offset = (float(self.offset_X.GetValue()), float(self.offset_Y.GetValue())) @@ -768,7 +773,7 @@ def start_present(self, event): if not hasattr(self, "layers"): - print "No model loaded!" + print("No model loaded!") return self.pause_button.SetLabel("Pause") @@ -793,18 +798,18 @@ layer_red = self.layer_red.IsChecked()) def stop_present(self, event): - print "Stop" + print("Stop") self.pause_button.SetLabel("Pause") self.set_current_layer(0) self.display_frame.running = False def pause_present(self, event): if self.pause_button.GetLabel() == 'Pause': - print "Pause" + print("Pause") self.pause_button.SetLabel("Continue") self.display_frame.running = False else: - print "Continue" + print("Continue") self.pause_button.SetLabel("Pause") self.display_frame.running = True self.display_frame.next_img() diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/pronsole.py --- a/printrun-src/printrun/pronsole.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/pronsole.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # This file is part of the Printrun suite. # # Printrun is free software: you can redistribute it and/or modify @@ -18,6 +16,7 @@ import cmd import glob import os +import platform import time import threading import sys @@ -30,6 +29,7 @@ import traceback import re +from appdirs import user_cache_dir, user_config_dir, user_data_dir from serial import SerialException from . import printcore @@ -42,10 +42,11 @@ from .power import powerset_print_start, powerset_print_stop from printrun import gcoder from .rpc import ProntRPC +from printrun.spoolmanager import spoolmanager if os.name == "nt": try: - import _winreg + import winreg except: pass READLINE = True @@ -64,8 +65,9 @@ REPORT_POS = 1 REPORT_TEMP = 2 REPORT_MANUAL = 4 +DEG = "\N{DEGREE SIGN}" -class Status(object): +class Status: def __init__(self): self.extruder_temp = 0 @@ -102,6 +104,37 @@ def extruder_enabled(self): return self.extruder_temp != 0 +class RGSGCoder(): + """Bare alternative to gcoder.LightGCode which does not preload all lines in memory, +but still allows run_gcode_script (hence the RGS) to be processed by do_print (checksum,threading,ok waiting)""" + def __init__(self, line): + self.lines = True + self.filament_length = 0. + self.filament_length_multi = [0] + self.proc = run_command(line, {"$s": 'str(self.filename)'}, stdout = subprocess.PIPE, universal_newlines = True) + lr = gcoder.Layer([]) + lr.duration = 0. + self.all_layers = [lr] + self.read() #empty layer causes division by zero during progress calculation + def read(self): + ln = self.proc.stdout.readline() + if not ln: + self.proc.stdout.close() + return None + ln = ln.strip() + if not ln: + return None + pyLn = gcoder.PyLightLine(ln) + self.all_layers[0].append(pyLn) + return pyLn + def has_index(self, i): + while i >= len(self.all_layers[0]) and not self.proc.stdout.closed: + self.read() + return i < len(self.all_layers[0]) + def __len__(self): + return len(self.all_layers[0]) + def idxs(self, i): + return 0, i #layer, line class pronsole(cmd.Cmd): def __init__(self): @@ -143,28 +176,30 @@ self.userm105 = 0 self.m105_waitcycles = 0 self.macros = {} - self.history_file = "~/.pronsole-history" self.rc_loaded = False self.processing_rc = False self.processing_args = False self.settings = Settings(self) self.settings._add(BuildDimensionsSetting("build_dimensions", "200x200x100+0+0+0+0+0+0", _("Build dimensions"), _("Dimensions of Build Platform\n & optional offset of origin\n & optional switch position\n\nExamples:\n XXXxYYY\n XXX,YYY,ZZZ\n XXXxYYYxZZZ+OffX+OffY+OffZ\nXXXxYYYxZZZ+OffX+OffY+OffZ+HomeX+HomeY+HomeZ"), "Printer"), self.update_build_dimensions) self.settings._port_list = self.scanserial - self.settings._temperature_abs_cb = self.set_temp_preset - self.settings._temperature_pla_cb = self.set_temp_preset - self.settings._bedtemp_abs_cb = self.set_temp_preset - self.settings._bedtemp_pla_cb = self.set_temp_preset self.update_build_dimensions(None, self.settings.build_dimensions) self.update_tcp_streaming_mode(None, self.settings.tcp_streaming_mode) self.monitoring = 0 self.starttime = 0 self.extra_print_time = 0 self.silent = False - self.commandprefixes = 'MGT$' + self.commandprefixes = 'MGTD$' self.promptstrs = {"offline": "%(bold)soffline>%(normal)s ", - "fallback": "%(bold)sPC>%(normal)s ", + "fallback": "%(bold)s%(red)s%(port)s%(white)s PC>%(normal)s ", "macro": "%(bold)s..>%(normal)s ", - "online": "%(bold)sT:%(extruder_temp_fancy)s%(progress_fancy)s>%(normal)s "} + "online": "%(bold)s%(green)s%(port)s%(white)s %(extruder_temp_fancy)s%(progress_fancy)s>%(normal)s "} + self.spool_manager = spoolmanager.SpoolManager(self) + self.current_tool = 0 # Keep track of the extruder being used + self.cache_dir = os.path.join(user_cache_dir("Printrun")) + self.history_file = os.path.join(self.cache_dir,"history") + self.config_dir = os.path.join(user_config_dir("Printrun")) + self.data_dir = os.path.join(user_data_dir("Printrun")) + self.lineignorepattern=re.compile("ok ?\d*$|.*busy: ?processing|.*busy: ?heating|.*Active Extruder: ?\d*$") # -------------------------------------------------------------- # General console handling @@ -196,7 +231,11 @@ self.old_completer = readline.get_completer() readline.set_completer(self.complete) readline.parse_and_bind(self.completekey + ": complete") - history = os.path.expanduser(self.history_file) + history = (self.history_file) + if not os.path.exists(history): + if not os.path.exists(self.cache_dir): + os.makedirs(self.cache_dir) + history = os.path.join(self.cache_dir, "history") if os.path.exists(history): readline.read_history_file(history) except ImportError: @@ -213,7 +252,7 @@ else: if self.use_rawinput: try: - line = raw_input(self.prompt) + line = input(self.prompt) except EOFError: self.log("") self.do_exit("") @@ -237,12 +276,12 @@ try: import readline readline.set_completer(self.old_completer) - readline.write_history_file(history) + readline.write_history_file(self.history_file) except ImportError: pass def confirm(self): - y_or_n = raw_input("y/n: ") + y_or_n = input("y/n: ") if y_or_n == "y": return True elif y_or_n != "n": @@ -250,11 +289,11 @@ return False def log(self, *msg): - msg = u"".join(unicode(i) for i in msg) + msg = "".join(str(i) for i in msg) logging.info(msg) def logError(self, *msg): - msg = u"".join(unicode(i) for i in msg) + msg = "".join(str(i) for i in msg) logging.error(msg) if not self.settings.error_command: return @@ -279,10 +318,12 @@ specials = {} specials["extruder_temp"] = str(int(self.status.extruder_temp)) specials["extruder_temp_target"] = str(int(self.status.extruder_temp_target)) + # port: /dev/tty* | netaddress:port + specials["port"] = self.settings.port.replace('/dev/', '') if self.status.extruder_temp_target == 0: - specials["extruder_temp_fancy"] = str(int(self.status.extruder_temp)) + specials["extruder_temp_fancy"] = str(int(self.status.extruder_temp)) + DEG else: - specials["extruder_temp_fancy"] = "%s/%s" % (str(int(self.status.extruder_temp)), str(int(self.status.extruder_temp_target))) + specials["extruder_temp_fancy"] = "%s%s/%s%s" % (str(int(self.status.extruder_temp)), DEG, str(int(self.status.extruder_temp_target)), DEG) if self.p.printing: progress = int(1000 * float(self.p.queueindex) / len(self.p.mainqueue)) / 10 elif self.sdprinting: @@ -294,6 +335,9 @@ specials["progress_fancy"] = " " + str(progress) + "%" else: specials["progress_fancy"] = "" + specials["red"] = "\033[31m" + specials["green"] = "\033[32m" + specials["white"] = "\033[37m" specials["bold"] = "\033[01m" specials["normal"] = "\033[00m" return promptstr % specials @@ -376,7 +420,7 @@ self.log("Setting bed temp to 0") self.p.send_now("M140 S0.0") self.log("Disconnecting from printer...") - if self.p.printing: + if self.p.printing and l != "force": self.log(_("Are you sure you want to exit while printing?\n\ (this will terminate the print).")) if not self.confirm(): @@ -453,6 +497,7 @@ self.logError("Empty macro - cancelled") return macro = None + namespace={} pycode = "def macro(self,*arg):\n" if "\n" not in macro_def.strip(): pycode += self.compile_macro_line(" " + macro_def.strip()) @@ -460,7 +505,11 @@ lines = macro_def.split("\n") for l in lines: pycode += self.compile_macro_line(l) - exec pycode + exec(pycode,namespace) + try: + macro=namespace['macro'] + except: + pass return macro def start_macro(self, macro_name, prev_definition = "", suppress_instructions = False): @@ -484,7 +533,7 @@ def do_macro(self, args): if args.strip() == "": - self.print_topics("User-defined macros", map(str, self.macros.keys()), 15, 80) + self.print_topics("User-defined macros", [str(k) for k in self.macros.keys()], 15, 80) return arglist = args.split(None, 1) macro_name = arglist[0] @@ -540,7 +589,7 @@ self.save_in_rc("set " + var, "set %s %s" % (var, value)) except AttributeError: logging.debug(_("Unknown variable '%s'") % var) - except ValueError, ve: + except ValueError as ve: if hasattr(ve, "from_validator"): self.logError(_("Bad value %s for variable '%s': %s") % (str, var, ve.args[0])) else: @@ -582,6 +631,7 @@ self.rc_filename = os.path.abspath(rc_filename) for rc_cmd in rc: if not rc_cmd.lstrip().startswith("#"): + logging.debug(rc_cmd.rstrip()) self.onecmd(rc_cmd) rc.close() if hasattr(self, "cur_macro_def"): @@ -590,17 +640,33 @@ finally: self.processing_rc = False - def load_default_rc(self, rc_filename = ".pronsolerc"): - if rc_filename == ".pronsolerc" and hasattr(sys, "frozen") and sys.frozen in ["windows_exe", "console_exe"]: - rc_filename = "printrunconf.ini" + def load_default_rc(self): + # Check if a configuration file exists in an "old" location, + # if not, use the "new" location provided by appdirs + for f in '~/.pronsolerc', '~/printrunconf.ini': + expanded = os.path.expanduser(f) + if os.path.exists(expanded): + config = expanded + break + else: + if not os.path.exists(self.config_dir): + os.makedirs(self.config_dir) + + config_name = ('printrunconf.ini' + if platform.system() == 'Windows' + else 'pronsolerc') + + config = os.path.join(self.config_dir, config_name) + logging.info('Loading config file ' + config) + + # Load the default configuration file try: - try: - self.load_rc(os.path.join(os.path.expanduser("~"), rc_filename)) - except IOError: - self.load_rc(rc_filename) - except IOError: - # make sure the filename is initialized - self.rc_filename = os.path.abspath(os.path.join(os.path.expanduser("~"), rc_filename)) + self.load_rc(config) + except FileNotFoundError: + # Make sure the filename is initialized, + # and create the file if it doesn't exist + self.rc_filename = config + open(self.rc_filename, 'a').close() def save_in_rc(self, key, definition): """ @@ -620,9 +686,14 @@ try: written = False if os.path.exists(self.rc_filename): - shutil.copy(self.rc_filename, self.rc_filename + "~bak") - rci = codecs.open(self.rc_filename + "~bak", "r", "utf-8") - rco = codecs.open(self.rc_filename + "~new", "w", "utf-8") + if not os.path.exists(self.cache_dir): + os.makedirs(self.cache_dir) + configcache = os.path.join(self.cache_dir, os.path.basename(self.rc_filename)) + configcachebak = configcache + "~bak" + configcachenew = configcache + "~new" + shutil.copy(self.rc_filename, configcachebak) + rci = codecs.open(configcachebak, "r", "utf-8") + rco = codecs.open(configcachenew, "w", "utf-8") if rci is not None: overwriting = False for rc_cmd in rci: @@ -643,12 +714,12 @@ if rci is not None: rci.close() rco.close() - shutil.move(self.rc_filename + "~new", self.rc_filename) + shutil.move(configcachenew, self.rc_filename) # if definition != "": # self.log("Saved '"+key+"' to '"+self.rc_filename+"'") # else: # self.log("Removed '"+key+"' from '"+self.rc_filename+"'") - except Exception, e: + except Exception as e: self.logError("Saving failed for ", key + ":", str(e)) finally: del rci, rco @@ -688,7 +759,12 @@ logger = logging.getLogger() logger.setLevel(logging.DEBUG) for config in args.conf: - self.load_rc(config) + try: + self.load_rc(config) + except EnvironmentError as err: + print(("ERROR: Unable to load configuration file: %s" % + str(err)[10:])) + sys.exit(1) if not self.rc_loaded: self.load_default_rc() self.processing_args = True @@ -697,8 +773,7 @@ self.processing_args = False self.update_rpc_server(None, self.settings.rpc_server) if args.filename: - filename = args.filename.decode(locale.getpreferredencoding()) - self.cmdline_filename_callback(filename) + self.cmdline_filename_callback(args.filename) def cmdline_filename_callback(self, filename): self.do_load(filename) @@ -736,7 +811,8 @@ self.logError(traceback.format_exc()) return False self.statuscheck = True - self.status_thread = threading.Thread(target = self.statuschecker) + self.status_thread = threading.Thread(target = self.statuschecker, + name = 'status thread') self.status_thread.start() return True @@ -790,17 +866,19 @@ baselist = [] if os.name == "nt": try: - key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM") + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM") i = 0 while(1): - baselist += [_winreg.EnumValue(key, i)[1]] + baselist += [winreg.EnumValue(key, i)[1]] i += 1 except: pass for g in ['/dev/ttyUSB*', '/dev/ttyACM*', "/dev/tty.*", "/dev/cu.*", "/dev/rfcomm*"]: baselist += glob.glob(g) - return filter(self._bluetoothSerialFilter, baselist) + if(sys.platform!="win32" and self.settings.devicepath): + baselist += glob.glob(self.settings.devicepath) + return [p for p in baselist if self._bluetoothSerialFilter(p)] def _bluetoothSerialFilter(self, serial): return not ("Bluetooth" in serial or "FireFly" in serial) @@ -832,7 +910,7 @@ if self.p.writefailures >= 4: self.logError(_("Disconnecting after 4 failed writes.")) self.status_thread = None - self.disconnect() + self.p.disconnect() return if do_monitoring: if self.sdprinting and not self.paused: @@ -883,7 +961,7 @@ self.fgcode = gcoder.LightGCode(deferred = True) else: self.fgcode = gcode - self.fgcode.prepare(open(filename, "rU"), + self.fgcode.prepare(open(filename, "r", encoding="utf-8"), get_home_pos(self.build_dimensions_list), layer_callback = layer_callback) self.fgcode.estimate_duration() @@ -917,11 +995,11 @@ return try: if settings: - command = self.settings.sliceoptscommand + command = self.settings.slicecommandpath+self.settings.sliceoptscommand self.log(_("Entering slicer settings: %s") % command) run_command(command, blocking = True) else: - command = self.settings.slicecommand + command = self.settings.slicecommandpath+self.settings.slicecommand stl_name = l[0] gcode_name = stl_name.replace(".stl", "_export.gcode").replace(".STL", "_export.gcode") run_command(command, @@ -930,7 +1008,7 @@ blocking = True) self.log(_("Loading sliced file.")) self.do_load(l[0].replace(".stl", "_export.gcode")) - except Exception, e: + except Exception as e: self.logError(_("Slicing failed: %s") % e) def complete_slice(self, text, line, begidx, endidx): @@ -1067,7 +1145,7 @@ self.log(_("Files on SD card:")) self.log("\n".join(self.sdfiles)) elif self.sdlisting: - self.sdfiles.append(line.strip().lower()) + self.sdfiles.append(re.sub(" \d+$","",line.strip().lower())) def _do_ls(self, echo): # FIXME: this was 2, but I think it should rather be 0 as in do_upload @@ -1191,6 +1269,18 @@ new_total = self.settings.total_filament_used + self.fgcode.filament_length self.set("total_filament_used", new_total) + # Update the length of filament in the spools + self.spool_manager.refresh() + if(len(self.fgcode.filament_length_multi)>1): + for i in enumerate(self.fgcode.filament_length_multi): + if self.spool_manager.getSpoolName(i[0]) != None: + self.spool_manager.editLength( + -i[1], extruder = i[0]) + else: + if self.spool_manager.getSpoolName(0) != None: + self.spool_manager.editLength( + -self.fgcode.filament_length, extruder = 0) + if not self.settings.final_command: return output = get_command_output(self.settings.final_command, @@ -1202,7 +1292,7 @@ def recvcb_report(self, l): isreport = REPORT_NONE - if "ok C:" in l or "Count" in l \ + if "ok C:" in l or " Count " in l \ or ("X:" in l and len(gcoder.m114_exp.findall(l)) == 6): self.posreport = l isreport = REPORT_POS @@ -1260,7 +1350,7 @@ report_type = self.recvcb_report(l) if report_type & REPORT_TEMP: self.status.update_tempreading(l) - if l != "ok" and not self.sdlisting \ + if not self.lineignorepattern.match(l) and l[:4] != "wait" and not self.sdlisting \ and not self.monitoring and (report_type == REPORT_NONE or report_type & REPORT_MANUAL): if l[:5] == "echo:": l = l[5:].lstrip() @@ -1332,10 +1422,10 @@ self.p.send_now("M105") time.sleep(0.75) if not self.status.bed_enabled: - self.log(_("Hotend: %s/%s") % (self.status.extruder_temp, self.status.extruder_temp_target)) + self.log(_("Hotend: %s%s/%s%s") % (self.status.extruder_temp, DEG, self.status.extruder_temp_target, DEG)) else: - self.log(_("Hotend: %s/%s") % (self.status.extruder_temp, self.status.extruder_temp_target)) - self.log(_("Bed: %s/%s") % (self.status.bed_temp, self.status.bed_temp_target)) + self.log(_("Hotend: %s%s/%s%s") % (self.status.extruder_temp, DEG, self.status.extruder_temp_target, DEG)) + self.log(_("Bed: %s%s/%s%s") % (self.status.bed_temp, DEG, self.status.bed_temp_target, DEG)) def help_gettemp(self): self.log(_("Read the extruder and bed temperature.")) @@ -1366,7 +1456,7 @@ def help_settemp(self): self.log(_("Sets the hotend temperature to the value entered.")) self.log(_("Enter either a temperature in celsius or one of the following keywords")) - self.log(", ".join([i + "(" + self.temps[i] + ")" for i in self.temps.keys()])) + self.log(', '.join('%s (%s)'%kv for kv in self.temps.items())) def complete_settemp(self, text, line, begidx, endidx): if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "): @@ -1456,6 +1546,7 @@ if self.p.online: self.p.send_now("T%d" % tool) self.log(_("Using tool %d.") % tool) + self.current_tool = tool else: self.logError(_("Printer is not online.")) else: @@ -1560,6 +1651,12 @@ self.p.send_now("G1 E" + str(length) + " F" + str(feed)) self.p.send_now("G90") + # Update the length of filament in the current spool + self.spool_manager.refresh() + if self.spool_manager.getSpoolName(self.current_tool) != None: + self.spool_manager.editLength(-length, + extruder = self.current_tool) + def help_extrude(self): self.log(_("Extrudes a length of filament, 5mm by default, or the number of mm given as a parameter")) self.log(_("extrude - extrudes 5mm of filament at 300mm/min (5mm/s)")) @@ -1658,7 +1755,7 @@ self.onecmd(command) def do_run_script(self, l): - p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE) + p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE, universal_newlines = True) for line in p.stdout.readlines(): self.log("<< " + line.strip()) @@ -1666,9 +1763,24 @@ self.log(_("Runs a custom script. Current gcode filename can be given using $s token.")) def do_run_gcode_script(self, l): - p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE) - for line in p.stdout.readlines(): - self.onecmd(line.strip()) + try: + self.fgcode = RGSGCoder(l) + self.do_print(None) + except BaseException as e: + self.logError(traceback.format_exc()) def help_run_gcode_script(self): self.log(_("Runs a custom script which output gcode which will in turn be executed. Current gcode filename can be given using $s token.")) + + def complete_run_gcode_script(self, text, line, begidx, endidx): + words = line.split() + sep = os.path.sep + if len(words) < 2: + return ['.' + sep , sep] + corrected_text = words[-1] # text arg skips leading '/', include it + if corrected_text == '.': + return ['./'] # guide user that in linux, PATH does not include . and relative executed scripts must start with ./ + prefix_len = len(corrected_text) - len(text) + res = [((f + sep) if os.path.isdir(f) else f)[prefix_len:] #skip unskipped prefix_len + for f in glob.glob(corrected_text + '*')] + return res diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/rpc.py --- a/printrun-src/printrun/rpc.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/rpc.py Wed Jan 20 10:15:13 2021 +0100 @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Printrun. If not, see . -from SimpleXMLRPCServer import SimpleXMLRPCServer +from xmlrpc.server import SimpleXMLRPCServer from threading import Thread import socket import logging @@ -23,7 +23,7 @@ RPC_PORT = 7978 -class ProntRPC(object): +class ProntRPC: server = None @@ -45,6 +45,16 @@ else: raise self.server.register_function(self.get_status, 'status') + self.server.register_function(self.set_extruder_temperature,'settemp') + self.server.register_function(self.set_bed_temperature,'setbedtemp') + self.server.register_function(self.load_file,'load_file') + self.server.register_function(self.startprint,'startprint') + self.server.register_function(self.pauseprint,'pauseprint') + self.server.register_function(self.resumeprint,'resumeprint') + self.server.register_function(self.sendhome,'sendhome') + self.server.register_function(self.connect,'connect') + self.server.register_function(self.disconnect, 'disconnect') + self.server.register_function(self.send, 'send') self.thread = Thread(target = self.run_server) self.thread.start() @@ -76,3 +86,30 @@ "temps": temps, "z": z, } + def set_extruder_temperature(self, targettemp): + if self.pronsole.p.online: + self.pronsole.p.send_now("M104 S" + targettemp) + + def set_bed_temperature(self,targettemp): + if self.pronsole.p.online: + self.pronsole.p.send_now("M140 S" + targettemp) + + def load_file(self,filename): + self.pronsole.do_load(filename) + + def startprint(self): + self.pronsole.do_print("") + + def pauseprint(self): + self.pronsole.do_pause("") + + def resumeprint(self): + self.pronsole.do_resume("") + def sendhome(self): + self.pronsole.do_home("") + def connect(self): + self.pronsole.do_connect("") + def disconnect(self): + self.pronsole.do_disconnect("") + def send(self, command): + self.pronsole.p.send_now(command) diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/settings.py --- a/printrun-src/printrun/settings.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/settings.py Wed Jan 20 10:15:13 2021 +0100 @@ -15,6 +15,8 @@ import logging import traceback +import os +import sys from functools import wraps @@ -30,7 +32,7 @@ sep = "\n" if helptxt.find("\n") >= 0: sep = "\n\n" - if self.default is not "": + if self.default != "": deftxt = _("Default: ") resethelp = _("(Control-doubleclick to reset to default value)") if len(repr(self.default)) > 10: @@ -39,11 +41,11 @@ deftxt += repr(self.default) + " " + resethelp helptxt += sep + deftxt if len(helptxt): - widget.SetToolTipString(helptxt) + widget.SetToolTip(helptxt) return widget return decorator -class Setting(object): +class Setting: DEFAULT_GROUP = "Printer" @@ -64,15 +66,6 @@ raise NotImplementedError value = property(_get_value, _set_value) - def set_default(self, e): - import wx - if e.CmdDown() and e.ButtonDClick() and self.default is not "": - confirmation = wx.MessageDialog(None, _("Are you sure you want to reset the setting to the default value: {0!r} ?").format(self.default), _("Confirm set default"), wx.ICON_EXCLAMATION | wx.YES_NO | wx.NO_DEFAULT) - if confirmation.ShowModal() == wx.ID_YES: - self._set_value(self.default) - else: - e.Skip() - @setting_add_tooltip def get_label(self, parent): import wx @@ -90,6 +83,8 @@ def update(self): raise NotImplementedError + def validate(self, value): pass + def __str__(self): return self.name @@ -117,6 +112,12 @@ def update(self): self.value = self.widget.GetValue() + def set_default(self, e): + if e.CmdDown() and e.ButtonDClick() and self.default != "": + self.widget.SetValue(self.default) + else: + e.Skip() + class StringSetting(wxSetting): def get_specific_widget(self, parent): @@ -124,6 +125,31 @@ self.widget = wx.TextCtrl(parent, -1, str(self.value)) return self.widget +def wxColorToStr(color, withAlpha = True): + # including Alpha seems to be non standard in CSS + format = '#{0.red:02X}{0.green:02X}{0.blue:02X}' \ + + ('{0.alpha:02X}' if withAlpha else '') + return format.format(color) + +class ColorSetting(wxSetting): + def __init__(self, name, default, label = None, help = None, group = None, isRGBA=True): + super().__init__(name, default, label, help, group) + self.isRGBA = isRGBA + + def validate(self, value): + from .utils import check_rgb_color, check_rgba_color + validate = check_rgba_color if self.isRGBA else check_rgb_color + validate(value) + + def get_specific_widget(self, parent): + import wx + self.widget = wx.ColourPickerCtrl(parent, colour=wx.Colour(self.value), style=wx.CLRP_USE_TEXTCTRL) + self.widget.SetValue = self.widget.SetColour + self.widget.LayoutDirection = wx.Layout_RightToLeft + return self.widget + def update(self): + self._value = wxColorToStr(self.widget.Colour, self.isRGBA) + class ComboSetting(wxSetting): def __init__(self, name, default, choices, label = None, help = None, group = None): @@ -132,30 +158,66 @@ def get_specific_widget(self, parent): import wx - self.widget = wx.ComboBox(parent, -1, str(self.value), choices = self.choices, style = wx.CB_DROPDOWN) + readonly = isinstance(self.choices, tuple) + if readonly: + # wx.Choice drops its list on click, no need to click down arrow + # which is far to the right because of wx.EXPAND + self.widget = wx.Choice(parent, -1, choices = self.choices) + self.widget.GetValue = lambda: self.choices[self.widget.Selection] + self.widget.SetValue = lambda v: self.widget.SetSelection(self.choices.index(v)) + self.widget.SetValue(self.value) + else: + self.widget = wx.ComboBox(parent, -1, str(self.value), choices = self.choices, style = wx.CB_DROPDOWN) return self.widget class SpinSetting(wxSetting): def __init__(self, name, default, min, max, label = None, help = None, group = None, increment = 0.1): - super(SpinSetting, self).__init__(name, default, label, help, group) + super().__init__(name, default, label, help, group) self.min = min self.max = max self.increment = increment def get_specific_widget(self, parent): - from wx.lib.agw.floatspin import FloatSpin - self.widget = FloatSpin(parent, -1, min_val = self.min, max_val = self.max, digits = 0) + import wx + self.widget = wx.SpinCtrlDouble(parent, -1, min = self.min, max = self.max) + self.widget.SetDigits(0) self.widget.SetValue(self.value) orig = self.widget.GetValue self.widget.GetValue = lambda: int(orig()) return self.widget +def MySpin(parent, digits, *args, **kw): + # in GTK 3.[01], spinner is not large enough to fit text + # Could be a class, but use function to avoid load errors if wx + # not installed + # If native wx.SpinCtrlDouble has problems in different platforms + # try agw + # from wx.lib.agw.floatspin import FloatSpin + import wx + sp = wx.SpinCtrlDouble(parent, *args, **kw) + # sp = FloatSpin(parent) + sp.SetDigits(digits) + # sp.SetValue(kw['initial']) + def fitValue(ev): + text = '%%.%df'% digits % sp.Max + # native wx.SpinCtrlDouble does not return good size + # in GTK 3.0 + tex = sp.GetTextExtent(text) + tsz = sp.GetSizeFromTextSize(tex.x) + + if sp.MinSize.x < tsz.x: + # print('fitValue', getattr(sp, 'setting', None), sp.Value, sp.Digits, tsz.x) + sp.MinSize = tsz + # sp.Size = tsz + # sp.Bind(wx.EVT_TEXT, fitValue) + fitValue(None) + return sp + class FloatSpinSetting(SpinSetting): def get_specific_widget(self, parent): - from wx.lib.agw.floatspin import FloatSpin - self.widget = FloatSpin(parent, -1, value = self.value, min_val = self.min, max_val = self.max, increment = self.increment, digits = 2) + self.widget = MySpin(parent, 2, initial = self.value, min = self.min, max = self.max, inc = self.increment) return self.widget class BooleanSetting(wxSetting): @@ -216,9 +278,12 @@ import wx build_dimensions = parse_build_dimensions(self.value) self.widgets = [] - w = lambda val, m, M: self.widgets.append(FloatSpin(parent, -1, value = val, min_val = m, max_val = M, digits = 2)) - addlabel = lambda name, pos: self.widget.Add(wx.StaticText(parent, -1, name), pos = pos, flag = wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border = 5) - addwidget = lambda *pos: self.widget.Add(self.widgets[-1], pos = pos, flag = wx.RIGHT, border = 5) + def w(val, m, M): + self.widgets.append(MySpin(parent, 2, initial = val, min = m, max = M)) + def addlabel(name, pos): + self.widget.Add(wx.StaticText(parent, -1, name), pos = pos, flag = wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border = 5) + def addwidget(*pos): + self.widget.Add(self.widgets[-1], pos = pos, flag = wx.RIGHT | wx.EXPAND, border = 5) self.widget = wx.GridBagSizer() addlabel(_("Width"), (0, 0)) w(build_dimensions[0], 0, 2000) @@ -240,20 +305,20 @@ addwidget(1, 5) addlabel(_("X home pos."), (2, 0)) w(build_dimensions[6], -2000, 2000) - self.widget.Add(self.widgets[-1], pos = (2, 1)) + addwidget(2, 1) addlabel(_("Y home pos."), (2, 2)) w(build_dimensions[7], -2000, 2000) - self.widget.Add(self.widgets[-1], pos = (2, 3)) + addwidget(2, 3) addlabel(_("Z home pos."), (2, 4)) w(build_dimensions[8], -2000, 2000) - self.widget.Add(self.widgets[-1], pos = (2, 5)) + addwidget(2, 5) return self.widget def update(self): values = [float(w.GetValue()) for w in self.widgets] self.value = "%.02fx%.02fx%.02f%+.02f%+.02f%+.02f%+.02f%+.02f%+.02f" % tuple(values) -class Settings(object): +class Settings: def __baudrate_list(self): return ["2400", "9600", "19200", "38400", "57600", "115200", "250000"] def __init__(self, root): @@ -264,15 +329,25 @@ self._add(BooleanSetting("tcp_streaming_mode", False, _("TCP streaming mode"), _("When using a TCP connection to the printer, the streaming mode will not wait for acks from the printer to send new commands. This will break things such as ETA prediction, but can result in smoother prints.")), root.update_tcp_streaming_mode) self._add(BooleanSetting("rpc_server", True, _("RPC server"), _("Enable RPC server to allow remotely querying print status")), root.update_rpc_server) self._add(BooleanSetting("dtr", True, _("DTR"), _("Disabling DTR would prevent Arduino (RAMPS) from resetting upon connection"), "Printer")) - self._add(SpinSetting("bedtemp_abs", 110, 0, 400, _("Bed temperature for ABS"), _("Heated Build Platform temp for ABS (deg C)"), "Printer")) - self._add(SpinSetting("bedtemp_pla", 60, 0, 400, _("Bed temperature for PLA"), _("Heated Build Platform temp for PLA (deg C)"), "Printer")) - self._add(SpinSetting("temperature_abs", 230, 0, 400, _("Extruder temperature for ABS"), _("Extruder temp for ABS (deg C)"), "Printer")) - self._add(SpinSetting("temperature_pla", 185, 0, 400, _("Extruder temperature for PLA"), _("Extruder temp for PLA (deg C)"), "Printer")) + if sys.platform != "win32": + self._add(StringSetting("devicepath", "", _("Device name pattern"), _("Custom device pattern: for example /dev/3DP_* "), "Printer")) + self._add(SpinSetting("bedtemp_abs", 110, 0, 400, _("Bed temperature for ABS"), _("Heated Build Platform temp for ABS (deg C)"), "Printer"), root.set_temp_preset) + self._add(SpinSetting("bedtemp_pla", 60, 0, 400, _("Bed temperature for PLA"), _("Heated Build Platform temp for PLA (deg C)"), "Printer"), root.set_temp_preset) + self._add(SpinSetting("temperature_abs", 230, 0, 400, _("Extruder temperature for ABS"), _("Extruder temp for ABS (deg C)"), "Printer"), root.set_temp_preset) + self._add(SpinSetting("temperature_pla", 185, 0, 400, _("Extruder temperature for PLA"), _("Extruder temp for PLA (deg C)"), "Printer"), root.set_temp_preset) self._add(SpinSetting("xy_feedrate", 3000, 0, 50000, _("X && Y manual feedrate"), _("Feedrate for Control Panel Moves in X and Y (mm/min)"), "Printer")) self._add(SpinSetting("z_feedrate", 100, 0, 50000, _("Z manual feedrate"), _("Feedrate for Control Panel Moves in Z (mm/min)"), "Printer")) self._add(SpinSetting("e_feedrate", 100, 0, 1000, _("E manual feedrate"), _("Feedrate for Control Panel Moves in Extrusions (mm/min)"), "Printer")) - self._add(StringSetting("slicecommand", "python skeinforge/skeinforge_application/skeinforge_utilities/skeinforge_craft.py $s", _("Slice command"), _("Slice command"), "External")) - self._add(StringSetting("sliceoptscommand", "python skeinforge/skeinforge_application/skeinforge.py", _("Slicer options command"), _("Slice settings command"), "External")) + defaultslicerpath = "" + if getattr(sys, 'frozen', False): + if sys.platform == "darwin": + defaultslicerpath = "/Applications/Slic3r.app/Contents/MacOS/" + elif sys.platform == "win32": + defaultslicerpath = ".\\slic3r\\" + self._add(StringSetting("slicecommandpath", defaultslicerpath, _("Path to slicer"), _("Path to slicer"), "External")) + slicer = 'slic3r-console' if sys.platform == 'win32' else 'slic3r' + self._add(StringSetting("slicecommand", slicer + ' $s --output $o', _("Slice command"), _("Slice command"), "External")) + self._add(StringSetting("sliceoptscommand", "slic3r", _("Slicer options command"), _("Slice settings command"), "External")) self._add(StringSetting("start_command", "", _("Start command"), _("Executable to run when the print is started"), "External")) self._add(StringSetting("final_command", "", _("Final command"), _("Executable to run when the print is finished"), "External")) self._add(StringSetting("error_command", "", _("Error command"), _("Executable to run when an error occurs"), "External")) @@ -296,6 +371,7 @@ self._add(HiddenSetting("default_extrusion", 5.0)) self._add(HiddenSetting("last_extrusion", 5.0)) self._add(HiddenSetting("total_filament_used", 0.0)) + self._add(HiddenSetting("spool_list", "")) _settings = [] @@ -316,13 +392,11 @@ return object.__getattribute__(self, name) return getattr(self, "_" + name).value - def _add(self, setting, callback = None, validate = None, + def _add(self, setting, callback = None, alias = None, autocomplete_list = None): setattr(self, setting.name, setting) if callback: setattr(self, "__" + setting.name + "_cb", callback) - if validate: - setattr(self, "__" + setting.name + "_validate", validate) if alias: setattr(self, "__" + setting.name + "_alias", alias) if autocomplete_list: @@ -335,20 +409,16 @@ pass except AttributeError: pass - try: - getattr(self, "__%s_validate" % key)(value) - except AttributeError: - pass + setting = getattr(self, '_'+key) + setting.validate(value) t = type(getattr(self, key)) - if t == bool and value == "False": setattr(self, key, False) - else: setattr(self, key, t(value)) + if t == bool and value == "False": + value = False + setattr(self, key, t(value)) try: - cb = None - try: - cb = getattr(self, "__%s_cb" % key) - except AttributeError: - pass - if cb is not None: cb(key, value) + cb = getattr(self, "__%s_cb" % key, None) + if cb is not None: + cb(key, value) except: logging.warning((_("Failed to run callback after setting \"%s\":") % key) + "\n" + traceback.format_exc()) @@ -360,7 +430,7 @@ except AttributeError: pass try: - return getattr(self, "__%s_alias" % key)().keys() + return list(getattr(self, "__%s_alias" % key)().keys()) except AttributeError: pass return [] diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/spoolmanager/__init__.py diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/spoolmanager/spoolmanager.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/printrun-src/printrun/spoolmanager/spoolmanager.py Wed Jan 20 10:15:13 2021 +0100 @@ -0,0 +1,262 @@ +# This file is part of the Printrun suite. +# +# Printrun is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Printrun is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Printrun. If not, see . +# +# Copyright 2017 Rock Storm + +# This module indirectly depends of pronsole and settings but it does not +# import them + +class SpoolManager(): + """ + Back-end for the Spool Manager. + + It is expected to be called from an object which has the contents of + settings.py and pronsole.py. This way the class is able to '_add' and + 'set' settings. + + This class basically handles a single variable called '_spool_list'. It is + a list of spool_items. A spool_item is in turn a list three elements: a + string, a float and an integer. Namely: the name of the spool, the + remaining length of filament and the extruder it is loaded to. E.g.: + + spool_item = [string name, float length, int extruder] + + _spool_list = [spool_item spool_1, ... , spool_item spool_n ] + + '_spool_list' is somehow a Nx3 matrix where N is the number of recorded + spools. The first column contains the names of the spools, the second the + lengths of remaining filament and the third column contains which extruder + is the spool loaded for. + + The variable '_spool_list' is saved in the configuration file using a + setting with the same name: 'spool_list'. It is saved as a single string. + It concatenates every item from the list and separates them by a comma and + a space. For instance, if the variable '_spool_list' was: + + _spool_list = [["spool_1", 100.0, 0], ["spool_2", 200.0, -1]] + + The 'spool_list' setting will look like: + + "spool_1, 100.0, 0, spool_2, 200.0, -1" + """ + + def __init__(self, parent): + self.parent = parent + self.refresh() + + def refresh(self): + """ + Read the configuration file and populate the list of recorded spools. + """ + self._spool_list = self._readSetting(self.parent.settings.spool_list) + + def add(self, spool_name, spool_length): + """Add the given spool to the list of recorded spools.""" + self._spool_list.append([spool_name, spool_length, -1]) + self._save() + + def load(self, spool_name, extruder): + """Set the extruder field of the given spool item.""" + + # If there was a spool already loaded for this extruder unload it + previous_spool = self._findByColumn(extruder, 2) + if previous_spool != -1: + self.unload(extruder) + + # Load the given spool + new_spool = self._findByColumn(spool_name, 0) + self.remove(spool_name) + self._spool_list.append([new_spool[0], new_spool[1], extruder]) + self._save() + + def remove(self, spool_name): + """Remove the given spool item from the list of recorded spools.""" + spool_item = self._findByColumn(spool_name, 0) + self._spool_list.remove(spool_item) + self._save() + + def unload(self, extruder): + """Set to -1 the extruder field of the spool item currently on.""" + + spool_item = self._findByColumn(extruder, 2) + if spool_item != -1: + self.remove(spool_item[0]) + self._spool_list.append([spool_item[0], spool_item[1], -1]) + self._save() + + def isLoaded(self, spool_name): + """ + int isLoaded( string name ) + + Return the extruder that the given spool is loaded to. -1 if it is + not loaded for any extruder or None if the given name does not match + any known spool. + """ + + spool_item = self._findByColumn(spool_name, 0) + if spool_item != -1: + return spool_item[2] + else: + return None + + def isListed(self, spool_name): + """Return 'True' if the given spool is on the list.""" + + spool_item = self._findByColumn(spool_name, 0) + if not spool_item == -1: + return True + else: + return False + + def getSpoolName(self, extruder): + """ + string getSpoolName( int extruder ) + + Return the name of the spool loaded for the given extruder. + """ + + spool_item = self._findByColumn(extruder, 2) + if spool_item != -1: + return spool_item[0] + else: + return None + + def getRemainingFilament(self, extruder): + """ + float getRemainingFilament( int extruder ) + + Return the name of the spool loaded for the given extruder. + """ + + spool_item = self._findByColumn(extruder, 2) + if spool_item != -1: + return spool_item[1] + else: + return float("NaN") + + def editLength(self, increment, spool_name = None, extruder = -1): + """ + int editLength ( float increment, string spool_name, int extruder ) + + Add the given 'increment' amount to the length of filament of the + given spool. Spool can be specified either by name or by the extruder + it is loaded to. + """ + + if spool_name != None: + spool_item = self._findByColumn(spool_name, 0) + elif extruder != -1: + spool_item = self._findByColumn(extruder, 2) + else: + return -1 # Not enough arguments + + if spool_item == -1: + return -2 # No spool found for the given name or extruder + + length = spool_item[1] + increment + self.remove(spool_item[0]) + self.add(spool_item[0], length) + if spool_item[2] > -1: + self.load(spool_item[0], spool_item[2]) + self._save() + + return 0 + + def getExtruderCount(self): + """int getExtruderCount()""" + return self.parent.settings.extruders + + def getSpoolCount(self): + """ + int getSpoolCount() + + Return the number of currently recorded spools. + """ + return len(self._spool_list) + + def getSpoolList(self): + """ + [N][2] getSpoolList () + + Returns a list of the recorded spools. Returns a Nx2 matrix where N is + the number of recorded spools. The first column contains the names of + the spools and the second the lengths of remaining filament. + """ + + slist = [] + for i in range(self.getSpoolCount()): + item = [self._spool_list[i][0], self._spool_list[i][1]] + slist.append(item) + return slist + + def _findByColumn(self, data, col = 0): + """ + Find which spool_item from the list contains certain data. + + The 'col' argument specifies in which field from the spool_item to + look for. For instance, with the following list: + + _spool_list = [["spool_1", 100.0, 1], + ["spool_2", 200.0, 0], + . + . + . + ["spool_10", 1000.0, 0]] + + A call like: _findByColumn("spool_2", 0) + + Will produce: ["spool_2", 200.0, 0] + + col = 0, would look into the "name's column" + col = 1, would look into the "length's column" + col = 2, would look into the "extruder's column" + """ + + for spool_item in self._spool_list: + if data == spool_item[col]: + return spool_item + + return -1 + + def _save(self): + """Update the list of recorded spools in the configuration file.""" + self._setSetting(self._spool_list, "spool_list") + + def _setSetting(self, variable, setting): + """ + Write the given variable to the given setting of the configuration + file. + """ + n = 3 # number of fields in spool_item + string_list = [] + for i in range(len(variable)): + for j in range(n): + string_list.append(str(variable[i][j])) + separator = ", " + self.parent.set(setting, separator.join(string_list)) + + def _readSetting(self, setting): + """ + Return the variable read. + """ + n = 3 # number of fields in spool_item + string_list = setting.split(", ") + variable = [] + for i in range(len(string_list)//n): + variable.append( + [string_list[n*i], + float(string_list[n*i+1]), + int(string_list[n*i+2])]) + return variable diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/spoolmanager/spoolmanager_gui.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/printrun-src/printrun/spoolmanager/spoolmanager_gui.py Wed Jan 20 10:15:13 2021 +0100 @@ -0,0 +1,639 @@ +# This file is part of the Printrun suite. +# +# Printrun is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Printrun is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Printrun. If not, see . +# +# Copyright 2017 Rock Storm + +import wx +from . import spoolmanager + +class SpoolManagerMainWindow(wx.Frame): + """ + Front-end for the Spool Manager. + + Main window which displays the currently loaded spools and the list of + recorded ones with buttons to add, load, edit or delete them. + """ + + def __init__(self, parent, spool_manager): + wx.Frame.__init__(self, parent, + title = "Spool Manager", + style = wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT) + + self.statusbar = self.CreateStatusBar() + + self.SetIcon(parent.GetIcon()) + + # Initiate the back-end + self.spool_manager = spool_manager + self.spool_manager.refresh() + + # Generate the dialogs showing the current spools + self.current_spools_dialog = CurrentSpoolDialog(self, + self.spool_manager) + + # Generate the list of recorded spools + self.spool_list = SpoolListView(self, self.spool_manager) + + # Generate the buttons + self.new_button = wx.Button(self, wx.ID_ADD) + self.new_button.SetToolTip("Add a new spool") + self.edit_button = wx.Button(self, wx.ID_EDIT) + self.edit_button.SetToolTip("Edit the selected spool") + self.delete_button = wx.Button(self, wx.ID_DELETE) + self.delete_button.SetToolTip("Delete the selected spool") + + # "Program" the buttons + self.new_button.Bind(wx.EVT_BUTTON, self.onClickAdd) + self.edit_button.Bind(wx.EVT_BUTTON, self.onClickEdit) + self.delete_button.Bind(wx.EVT_BUTTON, self.onClickDelete) + + # Layout + ## Group the buttons + self.button_sizer = wx.BoxSizer(wx.VERTICAL) + self.button_sizer.Add(self.new_button, 1, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + self.button_sizer.Add(self.edit_button, 1, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + self.button_sizer.Add(self.delete_button, 1, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + + ## Group the buttons with the spool list + self.list_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.list_sizer.Add(self.spool_list, 1, wx.EXPAND) + self.list_sizer.Add(self.button_sizer, 0, wx.ALIGN_CENTER) + + ## Layout the whole thing + self.full_sizer = wx.BoxSizer(wx.VERTICAL) + self.full_sizer.Add(self.current_spools_dialog, 0, wx.EXPAND) + self.full_sizer.Add(self.list_sizer, 1, wx.ALL | wx.EXPAND, 10) + + self.SetSizerAndFit(self.full_sizer) + + def onClickAdd(self, event): + """Open the window for customizing the new spool.""" + SpoolManagerAddWindow(self).Show(True) + + def onClickLoad(self, event, extruder): + """Load the selected spool to the correspondent extruder.""" + + # Check whether there is a spool selected + spool_index = self.spool_list.GetFirstSelected() + if spool_index == -1 : + self.statusbar.SetStatusText( + "Could not load the spool. No spool selected.") + return 0 + else: + spool_name = self.spool_list.GetItemText(spool_index) + self.statusbar.SetStatusText("") + + # If selected spool is already loaded, do nothing + spool_extruder = self.spool_manager.isLoaded(spool_name) + if spool_extruder > -1: + self.statusbar.SetStatusText( + "Spool '%s' is already loaded for Extruder %d." % + (spool_name, spool_extruder)) + return 0 + + # Load the selected spool and refresh the current spools dialog + self.spool_manager.load(spool_name, extruder) + self.current_spools_dialog.refreshDialog(self.spool_manager) + self.statusbar.SetStatusText( + "Loaded spool '%s' for Extruder %d." % (spool_name, extruder)) + + def onClickUnload(self, event, extruder): + """Unload the spool from the correspondent extruder.""" + + spool_name = self.spool_manager.getSpoolName(extruder) + if spool_name != None: + self.spool_manager.unload(extruder) + self.current_spools_dialog.refreshDialog(self.spool_manager) + self.statusbar.SetStatusText( + "Unloaded spool from Extruder %d." % extruder) + else: + self.statusbar.SetStatusText( + "There is no spool loaded for Extruder %d." % extruder) + + def onClickEdit(self, event): + """Open the window for editing the data of the selected spool.""" + + # Check whether there is a spool selected + spool_index = self.spool_list.GetFirstSelected() + if spool_index == -1 : + self.statusbar.SetStatusText( + "Could not edit the spool. No spool selected.") + return 0 + + # Open the edit window + spool_name = self.spool_list.GetItemText(spool_index) + spool_length = self.spool_list.GetItemText(spool_index, 1) + SpoolManagerEditWindow(self, spool_name, spool_length).Show(True) + self.statusbar.SetStatusText("") + + def onClickDelete(self, event): + """Delete the selected spool.""" + + # Get the selected spool + spool_index = self.spool_list.GetFirstSelected() + if spool_index == -1 : + self.statusbar.SetStatusText( + "Could not delete the spool. No spool selected.") + return 0 + else: + spool_name = self.spool_list.GetItemText(spool_index) + self.statusbar.SetStatusText("") + + # Ask confirmation for deleting + delete_dialog = wx.MessageDialog(self, + message = "Are you sure you want to delete the '%s' spool" % + spool_name, + caption = "Delete Spool", + style = wx.YES_NO | wx.ICON_EXCLAMATION) + + if delete_dialog.ShowModal() == wx.ID_YES: + # Remove spool + self.spool_manager.remove(spool_name) + self.spool_list.refreshList(self.spool_manager) + self.current_spools_dialog.refreshDialog(self.spool_manager) + self.statusbar.SetStatusText( + "Deleted spool '%s'." % spool_name) + + +class SpoolListView(wx.ListView): + """ + Custom wxListView object which visualizes the list of available spools. + """ + + def __init__(self, parent, spool_manager): + wx.ListView.__init__(self, parent, + style = wx.LC_REPORT | wx.LC_SINGLE_SEL) + self.InsertColumn(0, "Spool", width = wx.LIST_AUTOSIZE_USEHEADER) + self.InsertColumn(1, "Filament", width = wx.LIST_AUTOSIZE_USEHEADER) + self.populateList(spool_manager) + + # "Program" the layout + self.Bind(wx.EVT_SIZE, self.onResizeList) + + def populateList(self, spool_manager): + """Get the list of recorded spools from the Spool Manager.""" + spool_list = spool_manager.getSpoolList() + for i in range(len(spool_list)): + self.Append(spool_list[i]) + + def refreshList(self, spool_manager): + """Refresh the list by re-reading the Spool Manager list.""" + self.DeleteAllItems() + self.populateList(spool_manager) + + def onResizeList(self, event): + list_size = self.GetSize() + self.SetColumnWidth(1, -2) + filament_column_width = self.GetColumnWidth(1) + self.SetColumnWidth(col = 0, + width = list_size.width - filament_column_width) + event.Skip() + + +class CurrentSpoolDialog(wx.Panel): + """ + Custom wxStaticText object to display the currently loaded spools and + their remaining filament. + """ + + def __init__(self, parent, spool_manager): + wx.Panel.__init__(self, parent) + self.parent = parent + self.extruders = spool_manager.getExtruderCount() + + full_sizer = wx.BoxSizer(wx.VERTICAL) + + # Calculate the minimum size needed to properly display the + # extruder information + min_size = self.GetTextExtent(" Remaining filament: 0000000.00") + + # Generate a dialog for every extruder + self.extruder_dialog = [] + load_button = [] + unload_button = [] + button_sizer = [] + dialog_sizer = [] + for i in range(self.extruders): + # Generate the dialog with the spool information + self.extruder_dialog.append( + wx.StaticText(self, style = wx.ST_ELLIPSIZE_END)) + self.extruder_dialog[i].SetMinSize(wx.Size(min_size.width, -1)) + + # Generate the "load" and "unload" buttons + load_button.append(wx.Button(self, label = "Load")) + load_button[i].SetToolTip( + "Load selected spool for Extruder %d" % i) + unload_button.append(wx.Button(self, label = "Unload")) + unload_button[i].SetToolTip( + "Unload the spool for Extruder %d" % i) + + # "Program" the buttons + load_button[i].Bind(wx.EVT_BUTTON, + lambda event, extruder=i: parent.onClickLoad(event, extruder)) + unload_button[i].Bind(wx.EVT_BUTTON, + lambda event, extruder=i: parent.onClickUnload(event, extruder)) + + # Layout + button_sizer.append(wx.BoxSizer(wx.VERTICAL)) + button_sizer[i].Add(load_button[i], 0, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + button_sizer[i].Add(unload_button[i], 0, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + + dialog_sizer.append(wx.BoxSizer(wx.HORIZONTAL)) + dialog_sizer[i].Add(self.extruder_dialog[i], 1, wx.ALIGN_CENTER) + dialog_sizer[i].AddSpacer(10) + dialog_sizer[i].Add(button_sizer[i], 0, wx.EXPAND) + + full_sizer.Add(dialog_sizer[i], 0, wx.ALL | wx.EXPAND, 10) + + self.refreshDialog(spool_manager) + + self.SetSizerAndFit(full_sizer) + + + def refreshDialog(self, spool_manager): + """Retrieve the current spools from the Spool Manager.""" + + for i in range(self.extruders): + spool_name = spool_manager.getSpoolName(i) + spool_filament = spool_manager.getRemainingFilament(i) + label = ("Spool for Extruder %d:\n" % i + + " Name: %s\n" % spool_name + + " Remaining filament: %.2f" % spool_filament) + self.extruder_dialog[i].SetLabelText(label) + + +# --------------------------------------------------------------------------- +def checkOverwrite(parent, spool_name): + """Ask the user whether or not to overwrite the existing spool.""" + + overwrite_dialog = wx.MessageDialog(parent, + message = "A spool with the name '%s'' already exists." % + spool_name + + "Do you wish to overwrite it?", + caption = "Overwrite", + style = wx.YES_NO | wx.ICON_EXCLAMATION) + + if overwrite_dialog.ShowModal() == wx.ID_YES: + return True + else: + return False + +def getFloat(parent, number): + """ + Check whether the input number is a float. Either return the number or + return False. + """ + try: + return float(number) + except ValueError: + parent.statusbar.SetStatusText("Unrecognized number: %s" % number) + return False + + +# --------------------------------------------------------------------------- +class SpoolManagerAddWindow(wx.Frame): + """Window for adding spools.""" + + def __init__(self, parent): + + wx.Frame.__init__(self, parent, + title = "Add Spool", + style = wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT) + + self.statusbar = self.CreateStatusBar() + + self.parent = parent + + self.SetIcon(parent.GetIcon()) + + # Generate the dialogs + self.name_dialog = LabeledTextCtrl(self, + "Name", "Default Spool", "") + self.diameter_dialog = LabeledTextCtrl(self, + "Diameter", "1.75", "mm") + self.diameter_dialog.SetToolTip( + "Typically, either 1.75 mm or 2.85 mm (a.k.a '3')") + self.weight_dialog = LabeledTextCtrl(self, + "Weight", "1", "Kg") + self.density_dialog = LabeledTextCtrl(self, + "Density", "1.25", "g/cm^3") + self.density_dialog.SetToolTip( + "Typical densities are 1.25 g/cm^3 for PLA and 1.08 g/cm^3 for" + + " ABS") + self.length_dialog = LabeledTextCtrl(self, + "Length", "332601.35", "mm") + + # "Program" the dialogs + self.diameter_dialog.Bind(wx.EVT_TEXT, self.calculateLength) + self.weight_dialog.Bind(wx.EVT_TEXT, self.calculateLength) + self.density_dialog.Bind(wx.EVT_TEXT, self.calculateLength) + self.length_dialog.Bind(wx.EVT_TEXT, self.calculateWeight) + + # Generate the bottom buttons + self.add_button = wx.Button(self, wx.ID_ADD) + self.cancel_button = wx.Button(self, wx.ID_CANCEL) + + # "Program" the bottom buttons + self.add_button.Bind(wx.EVT_BUTTON, self.onClickAdd) + self.cancel_button.Bind(wx.EVT_BUTTON, self.onClickCancel) + + # Layout + ## Group the bottom buttons + self.bottom_buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.bottom_buttons_sizer.Add(self.add_button, 0, wx.FIXED_MINSIZE) + self.bottom_buttons_sizer.Add(self.cancel_button, 0, wx.FIXED_MINSIZE) + + ## Group the whole window + self.full_sizer = wx.BoxSizer(wx.VERTICAL) + self.full_sizer.Add(self.name_dialog, 0, + wx.TOP | wx.BOTTOM | wx.EXPAND, 10) + self.full_sizer.Add(self.diameter_dialog, 0, wx.EXPAND) + self.full_sizer.Add(self.weight_dialog, 0, wx.EXPAND) + self.full_sizer.Add(self.density_dialog, 0, wx.EXPAND) + self.full_sizer.Add(self.length_dialog, 0, wx.EXPAND) + self.full_sizer.Add(self.bottom_buttons_sizer, 0, + wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 10) + + self.SetSizerAndFit(self.full_sizer) + + # Don't allow this window to be resized in height + add_window_size = self.GetSize() + self.SetMaxSize((-1, add_window_size.height)) + + def onClickAdd(self, event): + """Add the new spool and close the window.""" + + spool_name = self.name_dialog.field.GetValue() + spool_length = getFloat(self, self.length_dialog.field.GetValue()) + + # Check whether the length is actually a number + if not spool_length: + self.statusbar.SetStatusText( + "ERROR: Unrecognized length: %s." % + self.length_dialog.field.GetValue()) + return -1 + + # The remaining filament should always be a positive number + if not spool_length > 0: + self.statusbar.SetStatusText( + "ERROR: Length is zero or negative: %.2f." % spool_length) + return -1 + + # Check whether the name is already used. If it is used, prompt the + # user before overwriting it + if self.parent.spool_manager.isListed(spool_name): + if checkOverwrite(self, spool_name): + # Remove the "will be overwritten" spool + self.parent.spool_manager.remove(spool_name) + else: + return 0 + + # Add the new spool + self.parent.spool_manager.add(spool_name, spool_length) + self.parent.spool_list.refreshList(self.parent.spool_manager) + self.parent.current_spools_dialog.refreshDialog( + self.parent.spool_manager) + self.parent.statusbar.SetStatusText( + "Added new spool '%s'" % spool_name + + " with %.2f mm of remaining filament." % spool_length) + + self.Close(True) + + def onClickCancel(self, event): + """Do nothing and close the window.""" + self.Close(True) + self.parent.statusbar.SetStatusText("") + + def calculateLength(self, event): + """ + Calculate the length of the filament given the mass, diameter and + density of the filament. Set the 'Length' field to this quantity. + """ + + mass = getFloat(self, self.weight_dialog.field.GetValue()) + diameter = getFloat(self, self.diameter_dialog.field.GetValue()) + density = getFloat(self, self.density_dialog.field.GetValue()) + if mass and diameter and density: + pi = 3.14159265359 + length = 4e6 * mass / pi / diameter**2 / density + self.length_dialog.field.ChangeValue("%.2f" % length) + self.statusbar.SetStatusText("") + else: + self.length_dialog.field.ChangeValue("---") + + def calculateWeight(self, event): + """ + Calculate the weight of the filament given the length, diameter and + density of the filament. Set the 'Weight' field to this value. + """ + + length = getFloat(self, self.length_dialog.field.GetValue()) + diameter = getFloat(self, self.diameter_dialog.field.GetValue()) + density = getFloat(self, self.density_dialog.field.GetValue()) + if length and diameter and density: + pi = 3.14159265359 + mass = length * pi * diameter**2 * density / 4e6 + self.weight_dialog.field.ChangeValue("%.2f" % mass) + self.statusbar.SetStatusText("") + else: + self.weight_dialog.field.ChangeValue("---") + + +class LabeledTextCtrl(wx.Panel): + """ + Group together a wxTextCtrl with a preceding and a subsequent wxStaticText. + """ + + def __init__(self, parent, preceding_text, field_value, subsequent_text): + wx.Panel.__init__(self, parent) + self.pretext = wx.StaticText(self, label = preceding_text, + style = wx.ALIGN_RIGHT) + self.field = wx.TextCtrl(self, value = field_value) + self.subtext = wx.StaticText(self, label = subsequent_text) + + # Layout the panel + self.sizer = wx.BoxSizer(wx.HORIZONTAL) + self.sizer.Add(self.pretext, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 10) + self.sizer.SetItemMinSize(self.pretext, (80, -1)) + self.sizer.Add(self.field, 1, wx.EXPAND) + self.sizer.Add(self.subtext, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) + self.sizer.SetItemMinSize(self.subtext, (50, -1)) + + self.SetSizerAndFit(self.sizer) + + +# --------------------------------------------------------------------------- +class SpoolManagerEditWindow(wx.Frame): + """Window for editing the name or the length of a spool.""" + + def __init__(self, parent, spool_name, spool_length): + + wx.Frame.__init__(self, parent, + title = "Edit Spool", + style = wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT) + + self.statusbar = self.CreateStatusBar() + + self.parent = parent + + self.SetIcon(parent.GetIcon()) + + self.old_spool_name = spool_name + self.old_spool_length = getFloat(self, spool_length) + + # Set how many millimeters will the buttons add or subtract + self.quantities = [-100.0, -50.0, -10.0, 10.0, 50.0, 100.0] + + # Generate the name field + self.name_field = LabeledTextCtrl(self, + "Name", self.old_spool_name, "") + + # Generate the length field and buttons + self.length_title = wx.StaticText(self, label = "Remaining filament:") + self.minus3_button = wx.Button(self, + label = str(self.quantities[0]), style = wx.BU_EXACTFIT) + self.minus2_button = wx.Button(self, + label = str(self.quantities[1]), style = wx.BU_EXACTFIT) + self.minus1_button = wx.Button(self, + label = str(self.quantities[2]), style = wx.BU_EXACTFIT) + self.length_field = wx.TextCtrl(self, + value = str(self.old_spool_length)) + self.plus1_button = wx.Button(self, + label = "+" + str(self.quantities[3]), style = wx.BU_EXACTFIT) + self.plus2_button = wx.Button(self, + label = "+" + str(self.quantities[4]), style = wx.BU_EXACTFIT) + self.plus3_button = wx.Button(self, + label = "+" + str(self.quantities[5]), style = wx.BU_EXACTFIT) + + # "Program" the length buttons + self.minus3_button.Bind(wx.EVT_BUTTON, self.changeLength) + self.minus2_button.Bind(wx.EVT_BUTTON, self.changeLength) + self.minus1_button.Bind(wx.EVT_BUTTON, self.changeLength) + self.plus1_button.Bind(wx.EVT_BUTTON, self.changeLength) + self.plus2_button.Bind(wx.EVT_BUTTON, self.changeLength) + self.plus3_button.Bind(wx.EVT_BUTTON, self.changeLength) + + # Generate the bottom buttons + self.save_button = wx.Button(self, wx.ID_SAVE) + self.cancel_button = wx.Button(self, wx.ID_CANCEL) + + # "Program" the bottom buttons + self.save_button.Bind(wx.EVT_BUTTON, self.onClickSave) + self.cancel_button.Bind(wx.EVT_BUTTON, self.onClickCancel) + + # Layout + ## Group the length field and its correspondent buttons + self.length_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.length_sizer.Add(self.minus3_button, 0, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + self.length_sizer.Add(self.minus2_button, 0, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + self.length_sizer.Add(self.minus1_button, 0, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + self.length_sizer.Add(self.length_field, 1, wx.EXPAND) + self.length_sizer.Add(self.plus1_button, 0, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + self.length_sizer.Add(self.plus2_button, 0, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + self.length_sizer.Add(self.plus3_button, 0, + wx.FIXED_MINSIZE | wx.ALIGN_CENTER) + + ## Group the bottom buttons + self.bottom_buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.bottom_buttons_sizer.Add(self.save_button, 0, wx.EXPAND) + self.bottom_buttons_sizer.Add(self.cancel_button, 0, wx.EXPAND) + + ## Lay out the whole window + self.full_sizer = wx.BoxSizer(wx.VERTICAL) + self.full_sizer.Add(self.name_field, 0, wx.EXPAND) + self.full_sizer.AddSpacer(10) + self.full_sizer.Add(self.length_title, 0, + wx.LEFT | wx.RIGHT | wx.EXPAND, 10) + self.full_sizer.Add(self.length_sizer, 0, + wx.LEFT | wx.RIGHT | wx.EXPAND, 10) + self.full_sizer.AddSpacer(10) + self.full_sizer.Add(self.bottom_buttons_sizer, 0, wx.ALIGN_CENTER) + + self.SetSizerAndFit(self.full_sizer) + + # Don't allow this window to be resized in height + edit_window_size = self.GetSize() + self.SetMaxSize((-1, edit_window_size.height)) + + def changeLength(self, event): + new_length = getFloat(self, self.length_field.GetValue()) + if new_length: + new_length = new_length + float(event.GetEventObject().GetLabel()) + self.length_field.ChangeValue("%.2f" % new_length) + self.statusbar.SetStatusText("") + + def onClickSave(self, event): + + new_spool_name = self.name_field.field.GetValue() + new_spool_length = getFloat(self, self.length_field.GetValue()) + + # Check whether the length is actually a number + if not new_spool_length: + self.statusbar.SetStatusText( + "ERROR: Unrecognized length: %s." % + self.length_field.GetValue()) + return -1 + + if not new_spool_length > 0: + self.statusbar.SetStatusText( + "ERROR: Length is zero or negative: %.2f." % new_spool_length) + return -1 + + # Check whether the "old" spool was loaded + new_spool_extruder = self.parent.spool_manager.isLoaded( + self.old_spool_name) + + # Check whether the name has changed + if new_spool_name == self.old_spool_name: + # Remove only the "old" spool + self.parent.spool_manager.remove(self.old_spool_name) + else: + # Check whether the new name is already used + if self.parent.spool_manager.isListed(new_spool_name): + if checkOverwrite(self, new_spool_name): + # Remove the "old" and the "will be overwritten" spools + self.parent.spool_manager.remove(self.old_spool_name) + self.parent.spool_manager.remove(new_spool_name) + else: + return 0 + else: + # Remove only the "old" spool + self.parent.spool_manager.remove(self.old_spool_name) + + # Add "new" or edited spool + self.parent.spool_manager.add(new_spool_name, new_spool_length) + self.parent.spool_manager.load(new_spool_name, new_spool_extruder) + self.parent.spool_list.refreshList(self.parent.spool_manager) + self.parent.current_spools_dialog.refreshDialog( + self.parent.spool_manager) + self.parent.statusbar.SetStatusText( + "Edited spool '%s'" % new_spool_name + + " with %.2f mm of remaining filament." % new_spool_length) + + self.Close(True) + + def onClickCancel(self, event): + self.Close(True) + self.parent.statusbar.SetStatusText("") diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/stlplater.py --- a/printrun-src/printrun/stlplater.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/stlplater.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # This file is part of the Printrun suite. # @@ -36,12 +36,12 @@ from printrun import stltool from printrun.objectplater import make_plater, PlaterPanel -glview = False -if "-nogl" not in sys.argv: +glview = '--no-gl' not in sys.argv +if glview: try: from printrun import stlview - glview = True except: + glview = False logging.warning("Could not load 3D viewer for plater:" + "\n" + traceback.format_exc()) @@ -61,7 +61,7 @@ class showstl(wx.Window): def __init__(self, parent, size, pos): - wx.Window.__init__(self, parent, size = size, pos = pos) + super().__init__(parent, size = size, pos = pos) self.i = 0 self.parent = parent self.previ = 0 @@ -74,7 +74,7 @@ self.prevsel = -1 def prepare_model(self, m, scale): - m.bitmap = wx.EmptyBitmap(800, 800, 32) + m.bitmap = wx.Bitmap(800, 800, 32) dc = wx.MemoryDC() dc.SelectObject(m.bitmap) dc.SetBackground(wx.Brush((0, 0, 0, 0))) @@ -105,7 +105,7 @@ def move(self, event): if event.ButtonUp(wx.MOUSE_BTN_LEFT): if self.initpos is not None: - currentpos = event.GetPositionTuple() + currentpos = event.GetPosition() delta = (0.5 * (currentpos[0] - self.initpos[0]), -0.5 * (currentpos[1] - self.initpos[1]) ) @@ -116,10 +116,10 @@ self.parent.right(event) elif event.Dragging(): if self.initpos is None: - self.initpos = event.GetPositionTuple() + self.initpos = event.GetPosition() self.Refresh() dc = wx.ClientDC(self) - p = event.GetPositionTuple() + p = event.GetPosition() dc.DrawLine(self.initpos[0], self.initpos[1], p[0], p[1]) del dc else: @@ -181,10 +181,7 @@ if self.prevsel != s: self.i = 0 self.prevsel = s - if z < 0: - self.rotate_shape(-1) - else: - self.rotate_shape(1) + self.rotate_shape(-1 if z < 0 else 1) #TEST def repaint(self, event): dc = wx.PaintDC(self) @@ -195,11 +192,11 @@ dc = wx.ClientDC(self) scale = 2 dc.SetPen(wx.Pen(wx.Colour(100, 100, 100))) - for i in xrange(20): + for i in range(20): dc.DrawLine(0, i * scale * 10, 400, i * scale * 10) dc.DrawLine(i * scale * 10, 0, i * scale * 10, 400) dc.SetPen(wx.Pen(wx.Colour(0, 0, 0))) - for i in xrange(4): + for i in range(4): dc.DrawLine(0, i * scale * 50, 400, i * scale * 50) dc.DrawLine(i * scale * 50, 0, i * scale * 50, 400) dc.SetBrush(wx.Brush(wx.Colour(128, 255, 128))) @@ -224,12 +221,12 @@ def prepare_ui(self, filenames = [], callback = None, parent = None, build_dimensions = None, circular_platform = False, simarrange_path = None, antialias_samples = 0): - super(StlPlaterPanel, self).prepare_ui(filenames, callback, parent, build_dimensions) + super().prepare_ui(filenames, callback, parent, build_dimensions) self.cutting = False self.cutting_axis = None self.cutting_dist = None if glview: - viewer = stlview.StlViewPanel(self, (580, 580), + viewer = stlview.StlViewPanel(self, wx.DefaultSize, build_dimensions = self.build_dimensions, circular = circular_platform, antialias_samples = antialias_samples) @@ -242,7 +239,7 @@ cutconfirmbutton.Disable() self.cutconfirmbutton = cutconfirmbutton self.menusizer.Add(cutconfirmbutton, pos = (nrows, 1), span = (1, 1), flag = wx.EXPAND) - cutpanel = wx.Panel(self.menupanel, -1) + cutpanel = wx.Panel(self.menupanel) cutsizer = self.cutsizer = wx.BoxSizer(wx.HORIZONTAL) cutpanel.SetSizer(cutsizer) cutxplusbutton = wx.ToggleButton(cutpanel, label = _(">X"), style = wx.BU_EXACTFIT) @@ -270,22 +267,20 @@ self.set_viewer(viewer) def start_cutting_tool(self, event, axis, direction): - toggle = event.GetEventObject() - if toggle.GetValue(): + toggle = event.EventObject + self.cutting = toggle.Value + if toggle.Value: # Disable the other toggles - for child in self.cutsizer.GetChildren(): - child = child.GetWindow() + for child in self.cutsizer.Children: + child = child.Window if child != toggle: - child.SetValue(False) - self.cutting = True + child.Value = False self.cutting_axis = axis - self.cutting_dist = None self.cutting_direction = direction else: - self.cutting = False self.cutting_axis = None - self.cutting_dist = None self.cutting_direction = None + self.cutting_dist = None def cut_confirm(self, event): name = self.l.GetSelection() @@ -335,7 +330,7 @@ best_match = None best_facet = None best_dist = float("inf") - for key, model in self.models.iteritems(): + for key, model in self.models.items(): transformation = transformation_matrix(model) transformed = model.transform(transformation) if not transformed.intersect_box(ray_near, ray_far): @@ -478,7 +473,7 @@ if self.simarrange_path: try: self.autoplate_simarrange() - except Exception, e: + except Exception as e: logging.warning(_("Failed to use simarrange for plating, " "falling back to the standard method. " "The error was: ") + e) @@ -494,7 +489,7 @@ "-m", # Pack around center "-x", str(int(self.build_dimensions[0])), "-y", str(int(self.build_dimensions[1]))] + files - p = subprocess.Popen(command, stdout = subprocess.PIPE) + p = subprocess.Popen(command, stdout = subprocess.PIPE, universal_newlines = True) pos_regexp = re.compile("File: (.*) minx: ([0-9]+), miny: ([0-9]+), minrot: ([0-9]+)") for line in p.stdout: @@ -510,7 +505,7 @@ x = float(bits[1]) y = float(bits[2]) rot = -float(bits[3]) - for name, model in models.items(): + for name, model in list(models.items()): # FIXME: not sure this is going to work superwell with utf8 if model.filename == filename: model.offsets[0] = x + self.build_dimensions[3] diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/stltool.py --- a/printrun-src/printrun/stltool.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/stltool.py Wed Jan 20 10:15:13 2021 +0100 @@ -42,13 +42,14 @@ return numpy.append(v, w) def applymatrix(facet, matrix = I): - return genfacet(map(lambda x: matrix.dot(homogeneous(x))[:3], facet[1])) + return genfacet([matrix.dot(homogeneous(x))[:3] for x in facet[1]]) -def ray_triangle_intersection(ray_near, ray_dir, (v1, v2, v3)): +def ray_triangle_intersection(ray_near, ray_dir, v123): """ Möller–Trumbore intersection algorithm in pure python Based on http://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm """ + v1, v2, v3 = v123 eps = 0.000001 edge1 = v2 - v1 edge2 = v3 - v1 @@ -99,7 +100,7 @@ return if binary: with open(filename, "wb") as f: - buf = "".join(["\0"] * 80) + buf = b"".join([b"\0"] * 80) buf += struct.pack(" i: @@ -385,6 +386,6 @@ else: break - print i, len(working) + print(i, len(working)) emitstl("../../Downloads/frame-vertex-neo-foot-x4-a.stl", s.facets, "emitted_object") # stl("../prusamendel/stl/mendelplate.stl") diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/stlview.py --- a/printrun-src/printrun/stlview.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/stlview.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # This file is part of the Printrun suite. # @@ -42,7 +42,7 @@ def vec(*args): return (GLfloat * len(args))(*args) -class stlview(object): +class stlview: def __init__(self, facets, batch): # Create the vertex and normal arrays. vertices = [] @@ -54,7 +54,7 @@ normals.extend(i[0]) # Create a list of triangle indices. - indices = range(3 * len(facets)) # [[3*i, 3*i+1, 3*i+2] for i in xrange(len(facets))] + indices = list(range(3 * len(facets))) # [[3*i, 3*i+1, 3*i+2] for i in xrange(len(facets))] self.vertex_list = batch.add_indexed(len(vertices) // 3, GL_TRIANGLES, None, # group, @@ -69,10 +69,11 @@ do_lights = False - def __init__(self, parent, size, id = wx.ID_ANY, + def __init__(self, parent, size, build_dimensions = None, circular = False, - antialias_samples = 0): - super(StlViewPanel, self).__init__(parent, id, wx.DefaultPosition, size, 0, + antialias_samples = 0, + grid = (1, 10)): + super().__init__(parent, wx.DefaultPosition, size, 0, antialias_samples = antialias_samples) self.batches = [] self.rot = 0 @@ -87,10 +88,11 @@ else: self.build_dimensions = [200, 200, 100, 0, 0, 0] self.platform = actors.Platform(self.build_dimensions, - circular = circular) + circular = circular, + grid = grid) self.dist = max(self.build_dimensions[0], self.build_dimensions[1]) self.basequat = [0, 0, 0, 1] - wx.CallAfter(self.forceresize) + wx.CallAfter(self.forceresize) #why needed self.mousepos = (0, 0) def OnReshape(self): @@ -155,8 +157,11 @@ self.parent.clickcb(event) def forceresize(self): - self.SetClientSize((self.GetClientSize()[0], self.GetClientSize()[1] + 1)) - self.SetClientSize((self.GetClientSize()[0], self.GetClientSize()[1] - 1)) + #print('forceresize') + x, y = self.GetClientSize() + #TODO: probably not needed + self.SetClientSize((x, y+1)) + self.SetClientSize((x, y)) self.initialized = False def move(self, event): @@ -167,27 +172,22 @@ RMB: nothing with shift move viewport """ - self.mousepos = event.GetPositionTuple() - if event.Dragging() and event.LeftIsDown(): - self.handle_rotation(event) - elif event.Dragging() and event.RightIsDown(): - self.handle_translation(event) - elif event.ButtonUp(wx.MOUSE_BTN_LEFT): - if self.initpos is not None: - self.initpos = None - elif event.ButtonUp(wx.MOUSE_BTN_RIGHT): - if self.initpos is not None: - self.initpos = None - else: - event.Skip() - return + self.mousepos = event.GetPosition() + if event.Dragging(): + if event.LeftIsDown(): + self.handle_rotation(event) + elif event.RightIsDown(): + self.handle_translation(event) + self.Refresh(False) + elif event.ButtonUp(wx.MOUSE_BTN_LEFT) or \ + event.ButtonUp(wx.MOUSE_BTN_RIGHT): + self.initpos = None event.Skip() - wx.CallAfter(self.Refresh) def handle_wheel(self, event): delta = event.GetWheelRotation() factor = 1.05 - x, y = event.GetPositionTuple() + x, y = event.GetPosition() x, y, _ = self.mouse_to_3d(x, y, local_transform = True) if delta > 0: self.zoom(factor, (x, y)) @@ -257,6 +257,8 @@ if not self.platform.initialized: self.platform.init() self.initialized = 1 + #TODO: this probably creates constant redraw + # create_objects is called during OnDraw, remove wx.CallAfter(self.Refresh) def prepare_model(self, m, scale): diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/utils.py --- a/printrun-src/printrun/utils.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/utils.py Wed Jan 20 10:15:13 2021 +0100 @@ -20,19 +20,27 @@ import datetime import subprocess import shlex +import locale import logging +DATADIR = os.path.join(sys.prefix, 'share') + + +def set_utf8_locale(): + """Make sure we read/write all text files in UTF-8""" + lang, encoding = locale.getlocale() + if encoding != 'UTF-8': + locale.setlocale(locale.LC_CTYPE, (lang, 'UTF-8')) + # Set up Internationalization using gettext # searching for installed locales on /usr/share; uses relative folder if not # found (windows) def install_locale(domain): - if os.path.exists('/usr/share/pronterface/locale'): - gettext.install(domain, '/usr/share/pronterface/locale', unicode = 1) - elif os.path.exists('/usr/local/share/pronterface/locale'): - gettext.install(domain, '/usr/local/share/pronterface/locale', - unicode = 1) + shared_locale_dir = os.path.join(DATADIR, 'locale') + if os.path.exists(shared_locale_dir): + gettext.install(domain, shared_locale_dir) else: - gettext.install(domain, './locale', unicode = 1) + gettext.install(domain, './locale') class LogFormatter(logging.Formatter): def __init__(self, format_default, format_info): @@ -71,15 +79,17 @@ return pixmapfile(filename) def imagefile(filename): - for prefix in ['/usr/local/share/pronterface/images', - '/usr/share/pronterface/images']: - candidate = os.path.join(prefix, filename) - if os.path.exists(candidate): - return candidate + shared_pronterface_images_dir = os.path.join(DATADIR, 'pronterface/images') + candidate = os.path.join(shared_pronterface_images_dir, filename) + if os.path.exists(candidate): + return candidate local_candidate = os.path.join(os.path.dirname(sys.argv[0]), "images", filename) if os.path.exists(local_candidate): return local_candidate + frozen_candidate=os.path.join(getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))),"images",filename) + if os.path.exists(frozen_candidate): + return frozen_candidate else: return os.path.join("images", filename) @@ -87,6 +97,7 @@ local_candidate = os.path.join(os.path.dirname(sys.argv[0]), filename) if os.path.exists(local_candidate): return local_candidate + if getattr(sys,"frozen",False): prefixes+=[getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))),] for prefix in prefixes: candidate = os.path.join(prefix, filename) if os.path.exists(candidate): @@ -94,12 +105,12 @@ return filename def pixmapfile(filename): - return lookup_file(filename, ['/usr/local/share/pixmaps', - '/usr/share/pixmaps']) + shared_pixmaps_dir = os.path.join(DATADIR, 'pixmaps') + return lookup_file(filename, [shared_pixmaps_dir]) def sharedfile(filename): - return lookup_file(filename, ['/usr/local/share/pronterface', - '/usr/share/pronterface']) + shared_pronterface_dir = os.path.join(DATADIR, 'pronterface') + return lookup_file(filename, [shared_pronterface_dir]) def configfile(filename): return lookup_file(filename, [os.path.expanduser("~/.printrun/"), ]) @@ -118,31 +129,30 @@ return str(datetime.timedelta(seconds = int(delta))) def prepare_command(command, replaces = None): - command = shlex.split(command.replace("\\", "\\\\").encode()) + command = shlex.split(command.replace("\\", "\\\\")) if replaces: replaces["$python"] = sys.executable for pattern, rep in replaces.items(): command = [bit.replace(pattern, rep) for bit in command] - command = [bit.encode() for bit in command] return command -def run_command(command, replaces = None, stdout = subprocess.STDOUT, stderr = subprocess.STDOUT, blocking = False): +def run_command(command, replaces = None, stdout = subprocess.STDOUT, stderr = subprocess.STDOUT, blocking = False, universal_newlines = False): command = prepare_command(command, replaces) if blocking: - return subprocess.call(command) + return subprocess.call(command, universal_newlines = universal_newlines) else: - return subprocess.Popen(command, stderr = stderr, stdout = stdout) + return subprocess.Popen(command, stderr = stderr, stdout = stdout, universal_newlines = universal_newlines) def get_command_output(command, replaces): p = run_command(command, replaces, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, - blocking = False) + blocking = False, universal_newlines = True) return p.stdout.read() def dosify(name): return os.path.split(name)[1].split(".")[0][:8] + ".g" -class RemainingTimeEstimator(object): +class RemainingTimeEstimator: drift = None gcode = None @@ -191,7 +201,7 @@ # etc bdl = re.findall("([-+]?[0-9]*\.?[0-9]*)", bdim) defaults = [200, 200, 100, 0, 0, 0, 0, 0, 0] - bdl = filter(None, bdl) + bdl = [b for b in bdl if b] bdl_float = [float(value) if value else defaults[i] for i, value in enumerate(bdl)] if len(bdl_float) < len(defaults): bdl_float += [defaults[i] for i in range(len(bdl_float), len(defaults))] @@ -205,7 +215,7 @@ def hexcolor_to_float(color, components): color = color[1:] numel = len(color) - ndigits = numel / components + ndigits = numel // components div = 16 ** ndigits - 1 return tuple(round(float(int(color[i:i + ndigits], 16)) / div, 2) for i in range(0, numel, ndigits)) @@ -226,3 +236,21 @@ def parse_temperature_report(report): matches = tempreport_exp.findall(report) return dict((m[0], (m[1], m[2])) for m in matches) + +def compile_file(filename): + with open(filename) as f: + return compile(f.read(), filename, 'exec') + +def read_history_from(filename): + history=[] + if os.path.exists(filename): + _hf=open(filename,encoding="utf-8") + for i in _hf: + history.append(i.rstrip()) + return history + +def write_history_to(filename, hist): + _hf=open(filename,"w",encoding="utf-8") + for i in hist: + _hf.write(i+"\n") + _hf.close() diff -r c82943fb205f -r cce0af6351f0 printrun-src/printrun/zscaper.py --- a/printrun-src/printrun/zscaper.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/zscaper.py Wed Jan 20 10:15:13 2021 +0100 @@ -14,7 +14,7 @@ # along with Printrun. If not, see . import wx -from stltool import stl, genfacet, emitstl +from .stltool import stl, genfacet, emitstl a = wx.App() def genscape(data = [[0, 1, 0, 0], [1, 0, 2, 0], [1, 0, 0, 0], [0, 1, 0, 1]], @@ -25,7 +25,7 @@ # create bottom: bmidpoint = (pscale * (datal - 1) / 2.0, pscale * (datah - 1) / 2.0) # print range(datal), bmidpoint - for i in zip(range(datal + 1)[:-1], range(datal + 1)[1:])[:-1]: + for i in list(zip(range(datal + 1)[:-1], range(datal + 1)[1:]))[:-1]: # print (pscale*i[0], pscale*i[1]) o.facets += [[[0, 0, -1], [[0.0, pscale * i[0], 0.0], [0.0, pscale * i[1], 0.0], [bmidpoint[0], bmidpoint[1], 0.0]]]] o.facets += [[[0, 0, -1], [[2.0 * bmidpoint[1], pscale * i[1], 0.0], [2.0 * bmidpoint[1], pscale * i[0], 0.0], [bmidpoint[0], bmidpoint[1], 0.0]]]] @@ -33,7 +33,7 @@ o.facets += [genfacet([[2.0 * bmidpoint[1], pscale * i[1], data[i[1]][datah - 1] * zscale + bheight], [2.0 * bmidpoint[1], pscale * i[0], data[i[0]][datah - 1] * zscale + bheight], [2.0 * bmidpoint[1], pscale * i[1], 0.0]])] o.facets += [genfacet([[0.0, pscale * i[0], data[i[0]][0] * zscale + bheight], [0.0, pscale * i[1], 0.0], [0.0, pscale * i[0], 0.0]])] o.facets += [genfacet([[2.0 * bmidpoint[1], pscale * i[1], 0.0], [2.0 * bmidpoint[1], pscale * i[0], data[i[0]][datah - 1] * zscale + bheight], [2.0 * bmidpoint[1], pscale * i[0], 0.0]])] - for i in zip(range(datah + 1)[: - 1], range(datah + 1)[1:])[: - 1]: + for i in list(zip(range(datah + 1)[: - 1], range(datah + 1)[1:]))[: - 1]: # print (pscale * i[0], pscale * i[1]) o.facets += [[[0, 0, -1], [[pscale * i[1], 0.0, 0.0], [pscale * i[0], 0.0, 0.0], [bmidpoint[0], bmidpoint[1], 0.0]]]] o.facets += [[[0, 0, -1], [[pscale * i[0], 2.0 * bmidpoint[0], 0.0], [pscale * i[1], 2.0 * bmidpoint[0], 0.0], [bmidpoint[0], bmidpoint[1], 0.0]]]] @@ -41,8 +41,8 @@ o.facets += [genfacet([[pscale * i[0], 2.0 * bmidpoint[0], data[datal - 1][i[0]] * zscale + bheight], [pscale * i[1], 2.0 * bmidpoint[0], data[datal - 1][i[1]] * zscale + bheight], [pscale * i[1], 2.0 * bmidpoint[0], 0.0]])] o.facets += [genfacet([[pscale * i[1], 0.0, 0.0], [pscale * i[0], 0.0, data[0][i[0]] * zscale + bheight], [pscale * i[0], 0.0, 0.0]])] o.facets += [genfacet([[pscale * i[0], 2.0 * bmidpoint[0], data[datal - 1][i[0]] * zscale + bheight], [pscale * i[1], 2.0 * bmidpoint[0], 0.0], [pscale * i[0], 2.0 * bmidpoint[0], 0.0]])] - for i in xrange(datah - 1): - for j in xrange(datal - 1): + for i in range(datah - 1): + for j in range(datal - 1): o.facets += [genfacet([[pscale * i, pscale * j, data[j][i] * zscale + bheight], [pscale * (i + 1), pscale * (j), data[j][i + 1] * zscale + bheight], [pscale * (i + 1), pscale * (j + 1), data[j + 1][i + 1] * zscale + bheight]])] o.facets += [genfacet([[pscale * (i), pscale * (j + 1), data[j + 1][i] * zscale + bheight], [pscale * i, pscale * j, data[j][i] * zscale + bheight], [pscale * (i + 1), pscale * (j + 1), data[j + 1][i + 1] * zscale + bheight]])] # print o.facets[-1] @@ -50,10 +50,10 @@ def zimage(name, out): i = wx.Image(name) s = i.GetSize() - print len(map(ord, i.GetData()[::3])) - b = map(ord, i.GetData()[::3]) + b = list(map(ord, i.GetData()[::3])) + print(b) data = [] - for i in xrange(s[0]): + for i in range(s[0]): data += [b[i * s[1]:(i + 1) * s[1]]] # data = [i[::5] for i in data[::5]] emitstl(out, genscape(data, zscale = 0.1).facets, name)