# 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)