Commit a1703466 authored by D1plo1d's avatar D1plo1d

Merging

parents 5d9df9df 1900f305
...@@ -2,4 +2,5 @@ ...@@ -2,4 +2,5 @@
.pronsolerc .pronsolerc
*.swp *.swp
*.bak *.bak
uploads
.DS_Store .DS_Store
# This file is part of the Printrun suite.
#
# Copyright 2013 Francesco Santini francesco.santini@gmail.com
#
# 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 <http://www.gnu.org/licenses/>.
#
# This code is imported from RepetierHost - Original copyright and license:
# Copyright 2011 repetier repetierdev@gmail.com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
class GCodeAnalyzer():
def __init__(self):
self.x = 0
self.y = 0
self.z = 0
self.e = 0
self.emax = 0
self.f = 1000
self.lastX = 0
self.lastY = 0
self.lastZ = 0
self.lastE = 0
self.xOffset = 0
self.yOffset = 0
self.zOffset = 0
self.eOffset = 0
self.lastZPrint = 0
self.layerZ = 0
self.relative = False
self.eRelative = False
self.homeX = 0
self.homeY = 0
self.homeZ = 0
self.maxX = 150
self.maxY = 150
self.maxZ = 150
self.minX = 0
self.minY = 0
self.minZ = 0
self.hasHomeX = False
self.hasHomeY = False
self.hasHomeZ = False
# find a code in a gstring line
def findCode(self, gcode, codeStr):
pattern = re.compile(codeStr + "\\s*(-?[\d.]*)",re.I)
m=re.search(pattern, gcode)
if m == None:
return None
else:
return m.group(1)
def Analyze(self, gcode):
gcode = gcode[:gcode.find(";")].lstrip() # remove comments
if gcode.startswith("@"): return # code is a host command
code_g = self.findCode(gcode, "G")
code_m = self.findCode(gcode, "M")
# we have a g_code
if code_g != None:
code_g = int(code_g)
#get movement codes
if code_g == 0 or code_g == 1 or code_g == 2 or code_g == 3:
self.lastX = self.x
self.lastY = self.y
self.lastZ = self.z
self.lastE = self.e
eChanged = False;
code_f = self.findCode(gcode, "F")
if code_f != None:
self.f=float(code_f)
code_x = self.findCode(gcode, "X")
code_y = self.findCode(gcode, "Y")
code_z = self.findCode(gcode, "Z")
code_e = self.findCode(gcode, "E")
if self.relative:
if code_x != None: self.x += float(code_x)
if code_y != None: self.y += float(code_y)
if code_z != None: self.z += float(code_z)
if code_e != None:
e = float(code_e)
if e != 0:
eChanged = True
self.e += e
else:
#absolute coordinates
if code_x != None: self.x = self.xOffset + float(code_x)
if code_y != None: self.y = self.yOffset + float(code_y)
if code_z != None: self.z = self.zOffset + float(code_z)
if code_e != None:
e = float(code_e)
if self.eRelative:
if e != 0:
eChanged = True
self.e += e
else:
# e is absolute. Is it changed?
if self.e != self.eOffset + e:
eChanged = True
self.e = self.eOffset + e
#limit checking
if self.x < self.minX: self.x = self.minX
if self.y < self.minY: self.y = self.minY
if self.z < self.minZ: self.z = self.minZ
if self.x > self.maxX: self.x = self.maxX
if self.y > self.maxY: self.y = self.maxY
if self.z > self.maxZ: self.z = self.maxZ
#Repetier has a bunch of limit-checking code here and time calculations: we are leaving them for now
elif code_g == 28 or code_g == 161:
self.lastX = self.x
self.lastY = self.y
self.lastZ = self.z
self.lastE = self.e
code_x = self.findCode(gcode, "X")
code_y = self.findCode(gcode, "Y")
code_z = self.findCode(gcode, "Z")
code_e = self.findCode(gcode, "E")
homeAll = False
if code_x == None and code_y == None and code_z == None: homeAll = True
if code_x != None or homeAll:
self.hasHomeX = True
self.xOffset = 0
self.x = self.homeX
if code_y != None or homeAll:
self.hasHomeY = True
self.yOffset = 0
self.y = self.homeY
if code_z != None or homeAll:
self.hasHomeZ = True
self.zOffset = 0
self.z = self.homeZ
if code_e != None:
self.eOffset = 0
self.e = 0
elif code_g == 162:
self.lastX = self.x
self.lastY = self.y
self.lastZ = self.z
self.lastE = self.e
code_x = self.findCode(gcode, "X")
code_y = self.findCode(gcode, "Y")
code_z = self.findCode(gcode, "Z")
homeAll = False
if code_x == None and code_y == None and code_z == None: homeAll = True
if code_x != None or homeAll:
self.hasHomeX = True
self.xOffset = 0
self.x = self.maxX
if code_y != None or homeAll:
self.hasHomeY = True
self.yOffset = 0
self.y = self.maxY
if code_z != None or homeAll:
self.hasHomeZ = True
self.zOffset = 0
self.z = self.maxZ
elif code_g == 90: self.relative = False
elif code_g == 91: self.relative = True
elif code_g == 92:
code_x = self.findCode(gcode, "X")
code_y = self.findCode(gcode, "Y")
code_z = self.findCode(gcode, "Z")
code_e = self.findCode(gcode, "E")
if code_x != None:
self.xOffset = self.x - float(code_x)
self.x = self.xOffset
if code_y != None:
self.yOffset = self.y - float(code_y)
self.y = self.yOffset
if code_z != None:
self.zOffset = self.z - float(code_z)
self.z = self.zOffset
if code_e != None:
self.xOffset = self.e - float(code_e)
self.e = self.eOffset
#End code_g != None
if code_m != None:
code_m = int(code_m)
if code_m == 82: self.eRelative = False
elif code_m == 83: self.eRelative = True
def print_status(self):
attrs = vars(self)
print '\n'.join("%s: %s" % item for item in attrs.items())
\ No newline at end of file
...@@ -31,9 +31,19 @@ You can run Printrun directly from source, as there are no packages available ye ...@@ -31,9 +31,19 @@ You can run Printrun directly from source, as there are no packages available ye
2. `sudo easy_install pybonjour tornado` 2. `sudo easy_install pybonjour tornado`
3. `sudo easy_install https://github.com/D1plo1d/py-mdns/archive/master.zip` 3. `sudo easy_install https://github.com/D1plo1d/py-mdns/archive/master.zip`
### Fedora 15 and newer (untested) ### Fedora 17 and newer
You can run Printrun directly from source, as there are no packages available yet. Fetch and install the dependencies using You can install Printrun from official packages. Install the whole package using
`sudo yum install printrun`
Or get only apps you need by
`sudo yum install pronsole` or `pronterface` or `plater`
Adding `--enablerepo updates-testing` option to `yum` might give you newer packages (but also not very tested).
You can also run Printrun directly from source, if the packages are too old for you anyway, or you have Fedora 15 or 16. Fetch and install the dependencies using
1. `sudo yum install pyserial wxpython pyglet python-tornado` 1. `sudo yum install pyserial wxpython pyglet python-tornado`
2. `sudo apt-get install avahi-daemon python-avahi tornado` 2. `sudo apt-get install avahi-daemon python-avahi tornado`
...@@ -104,10 +114,13 @@ Run Printrun for source if you want to test out the latest features. ...@@ -104,10 +114,13 @@ Run Printrun for source if you want to test out the latest features.
To use pronterface, you need: To use pronterface, you need:
* python (ideally 2.6.x or 2.7.x), * python (ideally 2.6.x or 2.7.x),
* pyserial (or python-serial on ubuntu/debian), * pyserial (or python-serial on ubuntu/debian)
* pyglet * pyglet
* numpy (for 3D view)
* pyreadline (not needed on Linux) and * pyreadline (not needed on Linux) and
* argparse (installed by default with python >= 2.7)
* wxPython * wxPython
* pycairo (to use Projector feature)
Please see specific instructions for Windows and Mac OS X below. Under Linux, you should use your package manager directly (see the "GETTING PRINTRUN" section) Please see specific instructions for Windows and Mac OS X below. Under Linux, you should use your package manager directly (see the "GETTING PRINTRUN" section)
......
#!/usr/bin/env python
# This file is copied from GCoder.
#
# GCoder 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,
# 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 <http://www.gnu.org/licenses/>.
import sys
import re
import math
def deltalen(a,b):
d = object()
d.x = b.x - a.x
d.y = b.y - a.y
d.z = b.z - a.z
return math.sqrt((d.x*d.x)+(d.y*d.y)+(d.z*d.z))
class Line(object):
def __init__(self,l):
self._x = None
self._y = None
self._z = None
self.e = None
self.f = 0
self.regex = re.compile("[-]?\d+[.]?\d*")
self.raw = l.upper().lstrip()
self.imperial = False
self.relative = False
self.relative_e = False
if ";" in self.raw:
self.raw = self.raw.split(";")[0]
self._parse_coordinates()
def _to_mm(self,v):
if v and self.imperial:
return v*25.4
return v
def _getx(self):
return self._to_mm(self._x)
def _setx(self,v):
self._x = v
def _gety(self):
return self._to_mm(self._y)
def _sety(self,v):
self._y = v
def _getz(self):
return self._to_mm(self._z)
def _setz(self,v):
self._z = v
def _gete(self):
return self._to_mm(self._e)
def _sete(self,v):
self._e = v
x = property(_getx,_setx)
y = property(_gety,_sety)
z = property(_getz,_setz)
e = property(_gete,_sete)
def command(self):
try:
return self.raw.split(" ")[0]
except:
return ""
def _get_float(self,which):
try:
return float(self.regex.findall(self.raw.split(which)[1])[0])
except:
return None
def _parse_coordinates(self):
try:
if "X" in self.raw:
self._x = self._get_float("X")
except:
pass
try:
if "Y" in self.raw:
self._y = self._get_float("Y")
except:
pass
try:
if "Z" in self.raw:
self._z = self._get_float("Z")
except:
pass
try:
if "E" in self.raw:
self.e = self._get_float("E")
except:
pass
try:
if "F" in self.raw:
self.f = self._get_float("F")
except:
pass
def is_move(self):
return self.command() and ("G1" in self.raw or "G0" in self.raw)
def __str__(self):
return self.raw
class Layer(object):
def __init__(self,lines):
self.lines = lines
def measure(self):
xmin = 999999999
ymin = 999999999
zmin = 0
xmax = -999999999
ymax = -999999999
zmax = -999999999
relative = False
relative_e = False
current_x = 0
current_y = 0
current_z = 0
for line in self.lines:
if line.command() == "G92":
current_x = line.x or current_x
current_y = line.y or current_y
current_z = line.z or current_z
if line.is_move():
x = line.x
y = line.y
z = line.z
if line.relative:
x = current_x + (x or 0)
y = current_y + (y or 0)
z = current_z + (z or 0)
if x and line.e:
if x < xmin:
xmin = x
if x > xmax:
xmax = x
if y and line.e:
if y < ymin:
ymin = y
if y > ymax:
ymax = y
if z:
if z < zmin:
zmin = z
if z > zmax:
zmax = z
current_x = x or current_x
current_y = y or current_y
current_z = z or current_z
return ( (xmin,xmax),(ymin,ymax),(zmin,zmax) )
class GCode(object):
def __init__(self,data):
self.lines = [Line(i) for i in data]
self._preprocess()
self._create_layers()
def _preprocess(self):
#checks for G20, G21, G90 and G91, sets imperial and relative flags
imperial = False
relative = False
relative_e = False
for line in self.lines:
if line.command() == "G20":
imperial = True
elif line.command() == "G21":
imperial = False
elif line.command() == "G90":
relative = False
relative_e = False
elif line.command() == "G91":
relative = True
relative_e = True
elif line.command() == "M82":
relative_e = False
elif line.command() == "M83":
relative_e = True
elif line.is_move():
line.imperial = imperial
line.relative = relative
line.relative_e = relative_e
def _create_layers(self):
self.layers = []
prev_z = None
cur_z = 0
cur_lines = []
layer_index = []
temp_layers = {}
for line in self.lines:
if line.command() == "G92" and line.z != None:
cur_z = line.z
elif line.is_move():
if line.z != None:
if line.relative:
cur_z += line.z
else:
cur_z = line.z
if cur_z != prev_z:
old_lines = temp_layers.pop(prev_z,[])
old_lines += cur_lines
temp_layers[prev_z] = old_lines
if not prev_z in layer_index:
layer_index.append(prev_z)
cur_lines = []
cur_lines.append(line)
prev_z = cur_z
old_lines = temp_layers.pop(prev_z,[])
old_lines += cur_lines
temp_layers[prev_z] = old_lines
if not prev_z in layer_index:
layer_index.append(prev_z)
layer_index.sort()
for idx in layer_index:
cur_lines = temp_layers[idx]
has_movement = False
for l in cur_lines:
if l.is_move() and l.e != None:
has_movement = True
break
if has_movement:
self.layers.append(Layer(cur_lines))
def num_layers(self):
return len(self.layers)
def measure(self):
xmin = 999999999
ymin = 999999999
zmin = 0
xmax = -999999999
ymax = -999999999
zmax = -999999999
for l in self.layers:
xd,yd,zd = l.measure()
if xd[0] < xmin:
xmin = xd[0]
if xd[1] > xmax:
xmax = xd[1]
if yd[0] < ymin:
ymin = yd[0]
if yd[1] > ymax:
ymax = yd[1]
if zd[0] < zmin:
zmin = zd[0]
if zd[1] > zmax:
zmax = zd[1]
self.xmin = xmin
self.xmax = xmax
self.ymin = ymin
self.ymax = ymax
self.zmin = zmin
self.zmax = zmax
self.width = xmax - xmin
self.depth = ymax - ymin
self.height = zmax - zmin
def filament_length(self):
total_e = 0
cur_e = 0
for line in self.lines:
if line.command() == "G92":
if line.e != None:
total_e += cur_e
cur_e = line.e
elif line.is_move() and line.e:
if line.relative_e:
cur_e += line.e
else:
cur_e = line.e
return total_e
def main():
if len(sys.argv) < 2:
print "usage: %s filename.gcode" % sys.argv[0]
return
# d = [i.replace("\n","") for i in open(sys.argv[1])]
# gcode = GCode(d)
gcode = GCode(list(open(sys.argv[1])))
gcode.measure()
print "Dimensions:"
print "\tX: %0.02f - %0.02f (%0.02f)" % (gcode.xmin,gcode.xmax,gcode.width)
print "\tY: %0.02f - %0.02f (%0.02f)" % (gcode.ymin,gcode.ymax,gcode.depth)
print "\tZ: %0.02f - %0.02f (%0.02f)" % (gcode.zmin,gcode.zmax,gcode.height)
print "Filament used: %0.02fmm" % gcode.filament_length()
print "Number of layers: %d" % gcode.num_layers()
if __name__ == '__main__':
main()
...@@ -5,134 +5,840 @@ ...@@ -5,134 +5,840 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Pronterface jm1\n" "Project-Id-Version: Pronterface jm1\n"
"POT-Creation-Date: 2012-08-08 10:09+CEST\n" "POT-Creation-Date: 2013-05-22 12:58+CEST\n"
"PO-Revision-Date: 2012-03-16 03:50+0100\n" "PO-Revision-Date: 2013-05-22 14:23+0100\n"
"Last-Translator: Guillaume Seguin <guillaume@segu.in>\n" "Last-Translator: Guillaume Seguin <guillaume@segu.in>\n"
"Language-Team: FR <c.laguilhon.debat@gmail.com>\n" "Language-Team: FR <c.laguilhon.debat@gmail.com>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 1.5.5\n"
#: printrun/pronterface_widgets.py:34 #: plater.py:246
msgid "Plate building tool"
msgstr ""
#: plater.py:252
msgid "Clear"
msgstr "Vider"
#: plater.py:253
msgid "Load"
msgstr "Charger"
#: plater.py:255 plater.py:258
msgid "Export"
msgstr "Exporter"
#: plater.py:260
msgid "Done"
msgstr "Terminé"
#: plater.py:262 printrun/pronterface_widgets.py:42 pronterface.py:494
#: pronterface.py:1253
msgid "Cancel"
msgstr "Annuler"
#: plater.py:264
msgid "Snap to Z = 0"
msgstr ""
#: plater.py:265
msgid "Put at 100, 100"
msgstr ""
#: plater.py:266
msgid "Delete"
msgstr "Supprimer"
#: plater.py:267
msgid "Auto"
msgstr ""
#: plater.py:291
msgid "Autoplating"
msgstr ""
#: plater.py:319
msgid "Bed full, sorry sir :("
msgstr "Plateau plein, désolé :("
#: plater.py:329
msgid ""
"Are you sure you want to clear the grid? All unsaved changes will be lost."
msgstr ""
#: plater.py:329
msgid "Clear the grid?"
msgstr "Vider la grille ?"
#: plater.py:371
msgid "Pick file to save to"
msgstr "Veuillez choisir le fichier dans lequel enregistrer"
#: plater.py:372
msgid "STL files (;*.stl;*.STL;)"
msgstr ""
#: plater.py:393
msgid "wrote %s"
msgstr ""
#: plater.py:396
msgid "Pick file to load"
msgstr "Veuillez choisir le fichier à charger"
#: plater.py:397
msgid "STL files (;*.stl;*.STL;)|*.stl|OpenSCAD files (;*.scad;)|*.scad"
msgstr ""
#: printrun/gui.py:21 pronterface.py:26
msgid "WX is not installed. This program requires WX to run."
msgstr ""
"wxWidgets n'est pas installé. Ce programme nécessite la librairie wxWidgets "
"pour fonctionner."
#: printrun/gui.py:81
msgid "XY:"
msgstr "XY:"
#: printrun/gui.py:83
msgid "mm/min Z:"
msgstr "mm/min Z:"
#: printrun/gui.py:88
msgid "Watch"
msgstr "Surveiller"
#: printrun/gui.py:93
msgid "Heat:"
msgstr "Buse:"
#: printrun/gui.py:96
msgid "Switch Hotend Off"
msgstr "Éteindre la tête chauffante"
#: printrun/gui.py:96 printrun/gui.py:115
msgid "Off"
msgstr "Off"
#: printrun/gui.py:108
msgid "Switch Hotend On"
msgstr "Allumer la tête chauffante"
#: printrun/gui.py:108 printrun/gui.py:127
msgid "Set"
msgstr "Régler"
#: printrun/gui.py:112 printrun/gui.py:176
msgid "Bed:"
msgstr "Plateau :"
#: printrun/gui.py:115
msgid "Switch Heated Bed Off"
msgstr "Éteindre le plateau chauffant"
#: printrun/gui.py:156
msgid "mm"
msgstr "mm"
#: printrun/gui.py:165
msgid ""
"mm/\n"
"min"
msgstr ""
"mm/\n"
"min"
#: printrun/gui.py:174
msgid "Heater:"
msgstr "Buse:"
#: printrun/gui.py:248
msgid "Send"
msgstr "Envoyer"
#: printrun/gui.py:248
msgid "Send Command to Printer"
msgstr "Envoyer une commande à l'imprimante"
#: printrun/gui.py:256
msgid ""
"Communication Settings\n"
"Click to rescan ports"
msgstr ""
#: printrun/gui.py:256
msgid "Port"
msgstr "Port"
#: printrun/gui.py:277
msgid "Connect to the printer"
msgstr "Se connecter à l'imprimante"
#: printrun/gui.py:277 pronterface.py:1475
msgid "Connect"
msgstr "Connecter"
#: printrun/gui.py:279
msgid "Reset"
msgstr "Réinitialiser"
#: printrun/gui.py:279
msgid "Reset the printer"
msgstr "Remettre à zéro l'imprimante"
#: printrun/gui.py:280
msgid "Load a 3D model file"
msgstr "Charger un modèle 3D"
#: printrun/gui.py:280
msgid "Load file"
msgstr "Charger un fichier"
#: printrun/gui.py:281
msgid "Compose"
msgstr "Composer"
#: printrun/gui.py:281
msgid "Simple Plater System"
msgstr ""
#: printrun/gui.py:282
msgid "SD"
msgstr "SD"
#: printrun/gui.py:282
msgid "SD Card Printing"
msgstr "Impression depuis une carte SD"
#: printrun/gui.py:284
msgid "Start Printing Loaded File"
msgstr "Commencer l'impression du fichier chargé"
#: printrun/gui.py:284 pronterface.py:227 pronterface.py:1243
#: pronterface.py:1301 pronterface.py:1421 pronterface.py:1493
#: pronterface.py:1506
msgid "Print"
msgstr "Imprimer"
#: printrun/gui.py:286
msgid "Pause Current Print"
msgstr "Mettre en pause l'impression"
#: printrun/gui.py:286 pronterface.py:1302 pronterface.py:1345
#: pronterface.py:1397 pronterface.py:1420 pronterface.py:1492
#: pronterface.py:1509
msgid "Pause"
msgstr "Pause"
#: printrun/gui.py:287
msgid "Recover"
msgstr "Récupérer"
#: printrun/gui.py:287
msgid "Recover previous Print"
msgstr "Récupérer l'impression précédente"
#: printrun/gui.py:308 pronsole.py:625 pronsole.py:681 pronterface.py:1113
#: pronterface.py:1339 pronterface.py:1451
msgid "Not connected to printer."
msgstr "Imprimante non connectée."
#: printrun/libtatlin/actors.py:272
msgid "Initialized 3D visualization in %.2f seconds"
msgstr "Visualisation 3D initialisée en %.2f secondes."
#: printrun/libtatlin/actors.py:273
msgid "Vertex count: %d"
msgstr "Nombre de sommets: %d"
#: printrun/pronterface_widgets.py:35
msgid "Find" msgid "Find"
msgstr "Trouver" msgstr "Trouver"
#: printrun/pronterface_widgets.py:36 #: printrun/pronterface_widgets.py:37
msgid "Save" msgid "Save"
msgstr "Enregistrer" msgstr "Enregistrer"
#: printrun/pronterface_widgets.py:41 pronterface.py:477 pronterface.py:1535 #: printrun/pronterface_widgets.py:126
msgid "Cancel"
msgstr "Annuler"
#: printrun/pronterface_widgets.py:125
msgid "Edit settings" msgid "Edit settings"
msgstr "Modifier les paramètres" msgstr "Modifier les paramètres"
#: printrun/pronterface_widgets.py:127 #: printrun/pronterface_widgets.py:128
msgid "Defaults" msgid "Defaults"
msgstr "Paramètres par défaut" msgstr "Paramètres par défaut"
#: printrun/pronterface_widgets.py:156 #: printrun/pronterface_widgets.py:157
msgid "Custom button" msgid "Custom button"
msgstr "Commande personnalisée" msgstr "Commande personnalisée"
#: printrun/pronterface_widgets.py:161 #: printrun/pronterface_widgets.py:162
msgid "Button title" msgid "Button title"
msgstr "Titre du bouton" msgstr "Titre du bouton"
#: printrun/pronterface_widgets.py:164 #: printrun/pronterface_widgets.py:165
msgid "Command" msgid "Command"
msgstr "Commande" msgstr "Commande"
#: printrun/pronterface_widgets.py:173 #: printrun/pronterface_widgets.py:174
msgid "Color" msgid "Color"
msgstr "Couleur" msgstr "Couleur"
#: pronterface.py:26 #: pronsole.py:173
msgid "WX is not installed. This program requires WX to run." msgid "Communications Speed (default: 115200)"
msgstr "Vitesse de communication (default : 115200)"
#: pronsole.py:174
msgid "Heated Build Platform temp for ABS (default: 110 deg C)"
msgstr ""
#: pronsole.py:175
msgid "Heated Build Platform temp for PLA (default: 60 deg C)"
msgstr ""
#: pronsole.py:176
msgid "Feedrate for Control Panel Moves in Extrusions (default: 300mm/min)"
msgstr ""
#: pronsole.py:177
msgid "Port used to communicate with printer"
msgstr "Port utilisé pour communiquer avec l'imprimante"
#: pronsole.py:178
msgid ""
"Slice command\n"
" default:\n"
" python skeinforge/skeinforge_application/skeinforge_utilities/"
"skeinforge_craft.py $s)"
msgstr ""
#: pronsole.py:179
msgid ""
"Slice settings command\n"
" default:\n"
" python skeinforge/skeinforge_application/skeinforge.py"
msgstr ""
#: pronsole.py:180
msgid "Extruder temp for ABS (default: 230 deg C)"
msgstr ""
#: pronsole.py:181
msgid "Extruder temp for PLA (default: 185 deg C)"
msgstr ""
#: pronsole.py:182
msgid "Feedrate for Control Panel Moves in X and Y (default: 3000mm/min)"
msgstr ""
#: pronsole.py:183
msgid "Feedrate for Control Panel Moves in Z (default: 200mm/min)"
msgstr ""
#: pronsole.py:184
msgid "Executable to run when the print is finished"
msgstr ""
#: pronsole.py:622
msgid "Please enter target name in 8.3 format."
msgstr "Veuillez entrer un nom au format 8.3."
#: pronsole.py:628
msgid "Uploading as %s"
msgstr "Envoi en tant que %s"
#: pronsole.py:629
msgid "Uploading %s"
msgstr "Envoi de %s"
#: pronsole.py:631
msgid "Press Ctrl-C to interrupt upload."
msgstr "Appuyer sur Ctrl-C pour interrompre l'envoi."
#: pronsole.py:634
msgid "Progress: "
msgstr "Progrès :"
#: pronsole.py:646
msgid "Upload completed. %s should now be on the card."
msgstr "Envoi terminé. %s devrait maintenant se trouver sur la carte."
#: pronsole.py:649
msgid "...interrupted!"
msgstr "...interrompu !"
#: pronsole.py:655
msgid "A partial file named %s may have been written to the sd card."
msgstr "Un fichier incomplet nommé %s peut avoir été écrit sur la carte SD."
#: pronsole.py:672
msgid ""
"Send a loaded gcode file to the printer. Load a file with the load command "
"first."
msgstr ""
#: pronsole.py:674
msgid "Send a loaded gcode file to the printer. You have %s loaded right now."
msgstr ""
#: pronsole.py:678 pronterface.py:1336
msgid "No file loaded. Please use load first."
msgstr "Aucun fichier chargé. Veuillez charger un fichier avant."
#: pronsole.py:683
msgid "Printing %s"
msgstr "Impression de %s"
#: pronsole.py:684
msgid "You can monitor the print with the monitor command."
msgstr ""
#: pronsole.py:692
msgid "Not printing, cannot pause."
msgstr "Pas d'impression en cours, impossible de mettre en pause."
#: pronsole.py:698
msgid "Pauses a running print"
msgstr "Met en pause l'impression en cours"
#: pronsole.py:702
msgid "Not paused, unable to resume. Start a print first."
msgstr ""
"Pas d'impression en pause, impossible de continuer. Veuillez d'abord lancer "
"une impression."
#: pronsole.py:712
msgid "Resumes a paused print."
msgstr "Continue une impression en pause."
#: pronsole.py:727
msgid "Files on SD card:"
msgstr "Fichiers sur la carte SD :"
#: pronsole.py:741 pronsole.py:783 pronsole.py:1088
msgid "Printer is not online. Please connect to it first."
msgstr "Imprimante déconnectée. Veuillez vous y connecter."
#: pronsole.py:746
msgid "Lists files on the SD card"
msgstr "Liste les fichiers sur la carte SD"
#: pronsole.py:750 pronterface.py:1163
msgid "Opening file failed."
msgstr "L'ouverture du fichier a échoué"
#: pronsole.py:756 pronterface.py:1169
msgid "Starting print"
msgstr "Début de l'impression..."
#: pronsole.py:779
msgid "Resets the printer."
msgstr "Remet à zéro l'imprimante."
#: pronsole.py:789
msgid "File is not present on card. Please upload it first."
msgstr "Le fichier n'est pas sur la carte, veuillez d'abord l'y envoyer."
#: pronsole.py:793
msgid "Printing file: %s from SD card."
msgstr "Impression du fichier %s depuis la carte SD"
#: pronsole.py:794
msgid "Requesting SD print..."
msgstr "Demande de l'impression depuis la carte SD..."
#: pronsole.py:798
msgid "Print a file from the SD card. Tab completes with available file names."
msgstr ""
#: pronsole.py:799
msgid "sdprint filename.g"
msgstr ""
#: pronsole.py:834 pronsole.py:842 pronsole.py:892 pronsole.py:918
#: pronterface.py:354 pronterface.py:374 pronterface.py:436
msgid "Printer is not online."
msgstr "Imprimante déconnectée."
#: pronsole.py:875
msgid "Read the extruder and bed temperature."
msgstr "Lire la température de l'extrudeuse et du plateau."
#: pronsole.py:885
msgid ""
"%s is a high temperature to set your extruder to. Are you sure you want to "
"do that?"
msgstr ""
#: pronsole.py:890
msgid "Setting hotend temperature to %s degrees Celsius."
msgstr "Réglage de la température de la buse à %s degrés Celsius."
#: pronsole.py:894 pronterface.py:356
msgid ""
"You cannot set negative temperatures. To turn the hotend off entirely, set "
"its temperature to 0."
msgstr ""
"Vous ne pouvez pas régler une température négative. Pour éteindre la buse, "
"réglez sa température à 0°C."
#: pronsole.py:896 pronsole.py:922
msgid "You must enter a temperature."
msgstr "Vous devez saisir une température."
#: pronsole.py:899
msgid "Sets the hotend temperature to the value entered."
msgstr "Règle de la température de la buse à la valeur saisie."
#: pronsole.py:900 pronsole.py:926
msgid "Enter either a temperature in celsius or one of the following keywords"
msgstr ""
#: pronsole.py:916
msgid "Setting bed temperature to %s degrees Celsius."
msgstr "Réglage de la température du plateau à %s degrés Celsius."
#: pronsole.py:920 pronterface.py:376
msgid ""
"You cannot set negative temperatures. To turn the bed off entirely, set its "
"temperature to 0."
msgstr ""
"Vous ne pouvez pas régler une température négative. Pour désactiver votre "
"plateau chauffant, réglez sa température à 0°C."
#: pronsole.py:925
msgid "Sets the bed temperature to the value entered."
msgstr "Règle de la température du plateau à la valeur saisie."
#: pronsole.py:935
msgid "No move specified."
msgstr "Aucun déplacement spécifié."
#: pronsole.py:938 pronsole.py:1041 pronsole.py:1182
msgid ""
"Printer is currently printing. Please pause the print before you issue "
"manual commands."
msgstr ""
#: pronsole.py:941 pronsole.py:1179
msgid "Printer is not online. Unable to move."
msgstr "Imprimante déconnectée. Impossible de bouger."
#: pronsole.py:957
msgid "Unknown axis."
msgstr "Axe inconnu."
#: pronsole.py:963
msgid "Invalid distance"
msgstr "Distance invalide"
#: pronsole.py:974
msgid "Move an axis. Specify the name of the axis and the amount. "
msgstr ""
#: pronsole.py:975
msgid ""
"move X 10 will move the X axis forward by 10mm at %s mm/min (default XY "
"speed)"
msgstr ""
#: pronsole.py:976
msgid "move Y 10 5000 will move the Y axis forward by 10mm at 5000mm/min"
msgstr ""
#: pronsole.py:977
msgid ""
"move Z -1 will move the Z axis down by 1mm at %s mm/min (default Z speed)"
msgstr ""
#: pronsole.py:978
msgid "Common amounts are in the tabcomplete list."
msgstr "" msgstr ""
"wxWidgets n'est pas installé. Ce programme nécessite la librairie wxWidgets "
"pour fonctionner."
#: pronterface.py:93 #: pronsole.py:1008 pronsole.py:1048
msgid "Invalid length given."
msgstr "La longueur donnée est invalide."
#: pronsole.py:1013 pronsole.py:1053
msgid "Invalid speed given."
msgstr "La vitesse donnée est invalide."
#: pronsole.py:1018
msgid "Extruding %fmm of filament."
msgstr ""
#: pronsole.py:1020
msgid "Reversing %fmm of filament."
msgstr ""
#: pronsole.py:1022
msgid "Length is 0, not doing anything."
msgstr "Longueur nulle, rien à faire."
#: pronsole.py:1028
msgid ""
"Extrudes a length of filament, 5mm by default, or the number of mm given as "
"a parameter"
msgstr ""
#: pronsole.py:1029
msgid "extrude - extrudes 5mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: pronsole.py:1030
msgid "extrude 20 - extrudes 20mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: pronsole.py:1031
msgid "extrude -5 - REVERSES 5mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: pronsole.py:1032
msgid "extrude 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)"
msgstr ""
#: pronsole.py:1038
msgid "Printer is not online. Unable to reverse."
msgstr "Imprimante déconnectée. Impossible de retirer."
#: pronsole.py:1057
msgid ""
"Reverses the extruder, 5mm by default, or the number of mm given as a "
"parameter"
msgstr ""
#: pronsole.py:1058
msgid "reverse - reverses 5mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: pronsole.py:1059
msgid "reverse 20 - reverses 20mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: pronsole.py:1060
msgid "reverse 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)"
msgstr ""
#: pronsole.py:1061
msgid "reverse -5 - EXTRUDES 5mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: pronsole.py:1078
msgid "Exiting program. Goodbye!"
msgstr "Fin du programme. Au revoir !"
#: pronsole.py:1083
msgid "Disconnects from the printer and exits the program."
msgstr "Déconnecte l'imprimante et quitte le programme."
#: pronsole.py:1091
msgid "Printer is not printing. Please print something before monitoring."
msgstr ""
"L'imprimante n'est pas en cours d'impression. Veuillez lancer une impression "
"avant d'activer le suivi."
#: pronsole.py:1093
msgid "Monitoring printer, use ^C to interrupt."
msgstr "Imprimante sous surveillance, utiliser ^C pour interrompre."
#: pronsole.py:1098 pronterface.py:1015
msgid "Invalid period given."
msgstr "La période donnée est invalide"
#: pronsole.py:1099
msgid "Updating values every %f seconds."
msgstr "Mise à jour des valeurs toutes les %f secondes."
#: pronsole.py:1110 pronsole.py:1113
msgid "Print progress: "
msgstr "Progrès de l'impression : "
#: pronsole.py:1122 pronterface.py:1020
msgid "Done monitoring."
msgstr "Surveillance de l'imprimante effectuée."
#: pronsole.py:1126
msgid "Monitor a machine's temperatures and an SD print's status."
msgstr ""
#: pronsole.py:1127
msgid ""
"monitor - Reports temperature and SD print status (if SD printing) every 5 "
"seconds"
msgstr ""
#: pronsole.py:1128
msgid ""
"monitor 2 - Reports temperature and SD print status (if SD printing) every 2 "
"seconds"
msgstr ""
#: pronsole.py:1136
msgid "No file name given."
msgstr "Aucun nom de fichier fourni."
#: pronsole.py:1142
msgid "Skeining file: %s"
msgstr ""
#: pronsole.py:1144 pronterface.py:1287
msgid "File not found!"
msgstr "Fichier non trouvé"
#: pronsole.py:1149
msgid "Entering slicer settings: %s"
msgstr ""
#: pronsole.py:1153
msgid "Slicing: "
msgstr "Slicing :"
#: pronsole.py:1156
msgid "Loading sliced file."
msgstr "Chargement du fichier slicé."
#: pronsole.py:1159
msgid "Skeinforge execution failed: %s"
msgstr ""
#: pronsole.py:1172
msgid ""
"Creates a gcode file from an stl model using the slicer (with tab-completion)"
msgstr ""
#: pronsole.py:1173
msgid "skein filename.stl - create gcode file"
msgstr ""
#: pronsole.py:1174
msgid "skein filename.stl view - create gcode file and view using skeiniso"
msgstr ""
#: pronsole.py:1175
#, fuzzy
msgid "skein set - adjust slicer settings"
msgstr " Régler les paramètres de slicing"
#: pronsole.py:1197
msgid "Homes the printer"
msgstr ""
#: pronsole.py:1198
msgid "home - homes all axes and zeroes the extruder(Using G28 and G92)"
msgstr ""
#: pronsole.py:1199
msgid "home xy - homes x and y axes (Using G28)"
msgstr ""
#: pronsole.py:1200
msgid "home z - homes z axis only (Using G28)"
msgstr ""
#: pronsole.py:1201
msgid "home e - set extruder position to zero (Using G92)"
msgstr ""
#: pronsole.py:1202
msgid "home xyze - homes all axes and zeroes the extruder (Using G28 and G92)"
msgstr ""
#: pronsole.py:1205
msgid ""
"load this file on startup instead of .pronsolerc ; you may chain config "
"files, if so settings auto-save will use the last specified file"
msgstr ""
#: pronsole.py:1206
msgid ""
"executes command after configuration/.pronsolerc is loaded ; macros/settings "
"from these commands are not autosaved"
msgstr ""
#: pronsole.py:1207
msgid "file to load"
msgstr "fichier à charger"
#: pronterface.py:100
msgid "" msgid ""
"Dimensions of Build Platform\n" "Dimensions of Build Platform\n"
" & optional offset of origin\n" " & optional offset of origin\n"
" & optional switch position\n"
"\n" "\n"
"Examples:\n" "Examples:\n"
" XXXxYYY\n" " XXXxYYY\n"
" XXX,YYY,ZZZ\n" " XXX,YYY,ZZZ\n"
" XXXxYYYxZZZ+OffX+OffY+OffZ" " XXXxYYYxZZZ+OffX+OffY+OffZ\n"
"XXXxYYYxZZZ+OffX+OffY+OffZ+HomeX+HomeY+HomeZ"
msgstr "" msgstr ""
#: pronterface.py:94 #: pronterface.py:101
msgid "Last Set Temperature for the Heated Print Bed" msgid "Last Set Temperature for the Heated Print Bed"
msgstr "Dernière température du plateau chauffant définie" msgstr "Dernière température du plateau chauffant définie"
#: pronterface.py:95 #: pronterface.py:102
msgid "Folder of last opened file" msgid "Folder of last opened file"
msgstr "Dossier du dernier fichier ouvert" msgstr "Dossier du dernier fichier ouvert"
#: pronterface.py:96 #: pronterface.py:103
msgid "Last Temperature of the Hot End" msgid "Last Temperature of the Hot End"
msgstr "Dernière température de la buse définie" msgstr "Dernière température de la buse définie"
#: pronterface.py:97 #: pronterface.py:104
msgid "Width of Extrusion in Preview (default: 0.5)" msgid "Width of Extrusion in Preview (default: 0.5)"
msgstr "Largeur de l'extrusion dans la prévisualisation (défaut : 0.5)" msgstr "Largeur de l'extrusion dans la prévisualisation (défaut : 0.5)"
#: pronterface.py:98 #: pronterface.py:105
msgid "Fine Grid Spacing (default: 10)" msgid "Fine Grid Spacing (default: 10)"
msgstr "Espacement fin de la grille (défaut : 10)" msgstr "Espacement fin de la grille (défaut : 10)"
#: pronterface.py:99 #: pronterface.py:106
msgid "Coarse Grid Spacing (default: 50)" msgid "Coarse Grid Spacing (default: 50)"
msgstr "Espacement large de la grille (défaut : 50)" msgstr "Espacement large de la grille (défaut : 50)"
#: pronterface.py:100 #: pronterface.py:107
msgid "Pronterface background color (default: #FFFFFF)" msgid "Pronterface background color (default: #FFFFFF)"
msgstr "Couleur de fond de la Pronterface (défaut : #FFFFFF)" msgstr "Couleur de fond de la Pronterface (défaut : #FFFFFF)"
#: pronterface.py:103 #: pronterface.py:110
msgid "Printer Interface" msgid "Printer Interface"
msgstr "Interface de l'imprimante" msgstr "Interface de l'imprimante"
#: pronterface.py:122 #: pronterface.py:127
msgid "Motors off" msgid "Motors off"
msgstr "Arrêter les moteurs" msgstr "Arrêter les moteurs"
#: pronterface.py:122 #: pronterface.py:127
msgid "Switch all motors off" msgid "Switch all motors off"
msgstr "Arrêter tous les moteurs" msgstr "Arrêter tous les moteurs"
#: pronterface.py:123 #: pronterface.py:128
msgid "Check current hotend temperature" msgid "Check current hotend temperature"
msgstr "Vérifier la température actuelle de la buse" msgstr "Vérifier la température actuelle de la buse"
#: pronterface.py:123 #: pronterface.py:128
msgid "Check temp" msgid "Check temp"
msgstr "Lire les températures" msgstr "Lire les températures"
#: pronterface.py:124 #: pronterface.py:129
msgid "Advance extruder by set length" msgid "Advance extruder by set length"
msgstr "Extruder sur la longueur donnée" msgstr "Extruder sur la longueur donnée"
#: pronterface.py:124 #: pronterface.py:129
msgid "Extrude" msgid "Extrude"
msgstr "Extruder" msgstr "Extruder"
#: pronterface.py:125 #: pronterface.py:130
msgid "Reverse" msgid "Reverse"
msgstr "Inverser" msgstr "Inverser"
#: pronterface.py:125 #: pronterface.py:130
msgid "Reverse extruder by set length" msgid "Reverse extruder by set length"
msgstr "Inverser l'extrudeur sur la longueur donnée" msgstr "Inverser l'extrudeur sur la longueur donnée"
#: pronterface.py:143 #: pronterface.py:173
msgid "" msgid ""
"# I moved all your custom buttons into .pronsolerc.\n" "# I moved all your custom buttons into .pronsolerc.\n"
"# Please don't add them here any more.\n" "# Please don't add them here any more.\n"
...@@ -143,7 +849,7 @@ msgstr "" ...@@ -143,7 +849,7 @@ msgstr ""
"# Veuillez ne plus en ajouter ici.\n" "# Veuillez ne plus en ajouter ici.\n"
"# Une sauvegarde de vos anciens boutons est dans le fichier custombtn.old\n" "# Une sauvegarde de vos anciens boutons est dans le fichier custombtn.old\n"
#: pronterface.py:148 #: pronterface.py:178
msgid "" msgid ""
"Note!!! You have specified custom buttons in both custombtn.txt and ." "Note!!! You have specified custom buttons in both custombtn.txt and ."
"pronsolerc" "pronsolerc"
...@@ -151,389 +857,283 @@ msgstr "" ...@@ -151,389 +857,283 @@ msgstr ""
"Remarque! Vous avez spécifié des boutons personnalisés dans custombtn.txt et " "Remarque! Vous avez spécifié des boutons personnalisés dans custombtn.txt et "
"aussi dans .pronsolerc" "aussi dans .pronsolerc"
#: pronterface.py:149 #: pronterface.py:179
msgid "" msgid ""
"Ignoring custombtn.txt. Remove all current buttons to revert to custombtn.txt" "Ignoring custombtn.txt. Remove all current buttons to revert to custombtn.txt"
msgstr "" msgstr ""
"custombtn.txt ignoré. Retirez tous les boutons en cours pour revenir à " "custombtn.txt ignoré. Retirez tous les boutons en cours pour revenir à "
"custombtn.txt" "custombtn.txt"
#: pronterface.py:181 #: pronterface.py:209
msgid "Failed to start web interface" msgid ""
msgstr "Échec du lancement de l'interface web" "display graphical temperature gauges in addition to the temperatures graph"
msgstr "afficher des jauges de température graphiques en plus du graphe"
#: pronterface.py:185 #: pronterface.py:210
msgid "CherryPy is not installed. Web Interface Disabled." msgid "automatically try to connect to printer on startup"
msgstr "CherryPy n'est pas installé. L'interface web est désactivée." msgstr "tenter de se connecter automatiquement à l'imprimante au démarrage"
#: pronterface.py:197 pronterface.py:603 pronterface.py:1525 #: pronterface.py:219
#: pronterface.py:1578 pronterface.py:1705 pronterface.py:1765 msgid "Print Started at: %s"
#: pronterface.py:1778 msgstr "Impression lancée à : %s"
msgid "Print"
msgstr "Imprimer" #: pronterface.py:224
msgid "Print ended at: %(end_time)s and took %(duration)s"
msgstr "Impression terminée à : %(end_time)s après avoir duré %(duration)s"
#: pronterface.py:207 #: pronterface.py:238
msgid "Printer is now online." msgid "Printer is now online."
msgstr "Imprimante connectée." msgstr "Imprimante connectée."
#: pronterface.py:208 #: pronterface.py:239
msgid "Disconnect" msgid "Disconnect"
msgstr "Déconnecter" msgstr "Déconnecter"
#: pronterface.py:331 #: pronterface.py:351
msgid "Setting hotend temperature to %f degrees Celsius." msgid "Setting hotend temperature to %f degrees Celsius."
msgstr "Réglage de la température de la buse à %f degrés Celsius." msgstr "Réglage de la température de la buse à %f degrés Celsius."
#: pronterface.py:334 pronterface.py:356 pronterface.py:428 #: pronterface.py:358 pronterface.py:378
msgid "Printer is not online."
msgstr "Imprimante déconnectée."
#: pronterface.py:336
msgid ""
"You cannot set negative temperatures. To turn the hotend off entirely, set "
"its temperature to 0."
msgstr ""
"Vous ne pouvez pas régler une température négative. Pour éteindre la buse, "
"réglez sa température à 0°C."
#: pronterface.py:338 pronterface.py:364
msgid "You must enter a temperature. (%s)" msgid "You must enter a temperature. (%s)"
msgstr "Vous devez saisir une température. (%s)" msgstr "Vous devez saisir une température. (%s)"
#: pronterface.py:353 #: pronterface.py:371
msgid "Setting bed temperature to %f degrees Celsius." msgid "Setting bed temperature to %f degrees Celsius."
msgstr "Réglage de la température du plateau à %f degrés Celsius." msgstr "Réglage de la température du plateau à %f degrés Celsius."
#: pronterface.py:360 #: pronterface.py:393
msgid ""
"You cannot set negative temperatures. To turn the bed off entirely, set its "
"temperature to 0."
msgstr ""
"Vous ne pouvez pas régler une température négative. Pour désactiver votre "
"plateau chauffant, réglez sa température à 0°C."
#: pronterface.py:381
msgid "Do you want to erase the macro?" msgid "Do you want to erase the macro?"
msgstr "Voulez-vous effacer la macro ?" msgstr "Voulez-vous effacer la macro ?"
#: pronterface.py:385 #: pronterface.py:397
msgid "Cancelled." msgid "Cancelled."
msgstr "Annulé" msgstr "Annulé"
#: pronterface.py:436 #: pronterface.py:442
msgid " Opens file" msgid " Opens file"
msgstr " Ouvrir un fichier" msgstr " Ouvrir un fichier"
#: pronterface.py:436 #: pronterface.py:442
msgid "&Open..." msgid "&Open..."
msgstr "&Ouvrir..." msgstr "&Ouvrir..."
#: pronterface.py:437 #: pronterface.py:443
msgid " Edit open file" msgid " Edit open file"
msgstr " Éditer le fichier ouvert" msgstr " Éditer le fichier ouvert"
#: pronterface.py:437 #: pronterface.py:443
msgid "&Edit..." msgid "&Edit..."
msgstr "&Éditer..." msgstr "&Éditer..."
#: pronterface.py:438 #: pronterface.py:444
msgid " Clear output console" msgid " Clear output console"
msgstr " Effacer le contenu de la console de sortie" msgstr " Effacer le contenu de la console de sortie"
#: pronterface.py:438 #: pronterface.py:444
msgid "Clear console" msgid "Clear console"
msgstr "Effacer la console" msgstr "Effacer la console"
#: pronterface.py:439 #: pronterface.py:445
msgid " Project slices" msgid " Project slices"
msgstr " Projeter les couches" msgstr " Projeter les couches"
#: pronterface.py:439 #: pronterface.py:445
msgid "Projector" msgid "Projector"
msgstr "Projecteur" msgstr "Projecteur"
#: pronterface.py:440 #: pronterface.py:446
msgid " Closes the Window" msgid " Closes the Window"
msgstr " Quitter le programme" msgstr " Quitter le programme"
#: pronterface.py:440 #: pronterface.py:446
msgid "E&xit" msgid "E&xit"
msgstr "&Quitter" msgstr "&Quitter"
#: pronterface.py:441 #: pronterface.py:447
msgid "&File" msgid "&File"
msgstr "&Fichier" msgstr "&Fichier"
#: pronterface.py:446 #: pronterface.py:452
msgid "&Macros" msgid "&Macros"
msgstr "&Macros" msgstr "&Macros"
#: pronterface.py:447 #: pronterface.py:453
msgid "<&New...>" msgid "<&New...>"
msgstr "<&Nouvelle...>" msgstr "<&Nouvelle...>"
#: pronterface.py:448 #: pronterface.py:454
msgid " Options dialog" msgid " Options dialog"
msgstr " Fenêtre des options" msgstr " Fenêtre des options"
#: pronterface.py:448 #: pronterface.py:454
msgid "&Options" msgid "&Options"
msgstr "&Options" msgstr "&Options"
#: pronterface.py:450 #: pronterface.py:456
msgid " Adjust slicing settings" msgid " Adjust slicing settings"
msgstr " Régler les paramètres de slicing" msgstr " Régler les paramètres de slicing"
#: pronterface.py:450 #: pronterface.py:456
msgid "Slicing Settings" msgid "Slicing Settings"
msgstr "Paramètres de slicing" msgstr "Paramètres de slicing"
#: pronterface.py:452 #: pronterface.py:458
msgid "Debug G-code"
msgstr ""
#: pronterface.py:459
msgid "Print all G-code sent to and received from the printer."
msgstr ""
#: pronterface.py:469
msgid "&Settings" msgid "&Settings"
msgstr "&Paramètres" msgstr "&Paramètres"
#: pronterface.py:467 #: pronterface.py:484
msgid "Enter macro name" msgid "Enter macro name"
msgstr "Saisissez le nom de la macro" msgstr "Saisissez le nom de la macro"
#: pronterface.py:470 #: pronterface.py:487
msgid "Macro name:" msgid "Macro name:"
msgstr "Nom :" msgstr "Nom :"
#: pronterface.py:473 #: pronterface.py:490
msgid "Ok" msgid "Ok"
msgstr "Valider" msgstr "Valider"
#: pronterface.py:495 #: pronterface.py:512
msgid "Macro name may contain only ASCII alphanumeric symbols and underscores" msgid "Macro name may contain only ASCII alphanumeric symbols and underscores"
msgstr "" msgstr ""
"Un nom de macro ne peut contenir que des caractères alphanumérique ASCII et " "Un nom de macro ne peut contenir que des caractères alphanumérique ASCII et "
"des underscore (_)" "des underscore (_)"
#: pronterface.py:500 #: pronterface.py:515
msgid "Name '%s' is being used by built-in command" msgid "Name '%s' is being used by built-in command"
msgstr "Le nom '%s' est utilisé par une commande interne" msgstr "Le nom '%s' est utilisé par une commande interne"
#: pronterface.py:548 #: pronterface.py:583
msgid "Port"
msgstr "Port"
#: pronterface.py:570 pronterface.py:1747
msgid "Connect"
msgstr "Connecter"
#: pronterface.py:574
msgid "Reset"
msgstr "Réinitialiser"
#: pronterface.py:589
msgid "Load file"
msgstr "Charger un fichier"
#: pronterface.py:593
msgid "Compose"
msgstr "Composer"
#: pronterface.py:598
msgid "SD"
msgstr "SD"
#: pronterface.py:608 pronterface.py:1579 pronterface.py:1631
#: pronterface.py:1681 pronterface.py:1704 pronterface.py:1764
#: pronterface.py:1781
msgid "Pause"
msgstr "Pause"
#: pronterface.py:612
msgid "Recover"
msgstr "Récupérer"
#: pronterface.py:630
msgid "Send"
msgstr "Envoyer"
#: pronterface.py:671
msgid "XY:"
msgstr "XY:"
#: pronterface.py:673
msgid "mm/min Z:"
msgstr "mm/min Z:"
#: pronterface.py:678
msgid "Watch"
msgstr "Surveiller"
#: pronterface.py:683
msgid "Heat:"
msgstr "Buse:"
#: pronterface.py:686 pronterface.py:709
msgid "Off"
msgstr "Off"
#: pronterface.py:700 pronterface.py:723
msgid "Set"
msgstr "Régler"
#: pronterface.py:706
msgid "Bed:"
msgstr "Plateau :"
#: pronterface.py:756
msgid "mm"
msgstr "mm"
#: pronterface.py:764
msgid ""
"mm/\n"
"min"
msgstr ""
"mm/\n"
"min"
#: pronterface.py:821 pronterface.py:1387 pronterface.py:1625
#: pronterface.py:1723
msgid "Not connected to printer."
msgstr "Imprimante non connectée."
#: pronterface.py:869
msgid "SD Upload" msgid "SD Upload"
msgstr "Copier sur SD" msgstr "Copier sur SD"
#: pronterface.py:873 #: pronterface.py:587
msgid "SD Print" msgid "SD Print"
msgstr "Imprimer depuis SD" msgstr "Imprimer depuis SD"
#: pronterface.py:914 #: pronterface.py:628
msgid "Mini mode" msgid "Mini mode"
msgstr "Mode réduit" msgstr "Mode réduit"
#: pronterface.py:921 #: pronterface.py:635
msgid "Full mode" msgid "Full mode"
msgstr "Mode complet" msgstr "Mode complet"
#: pronterface.py:946 #: pronterface.py:660
msgid "Execute command: " msgid "Execute command: "
msgstr "Exécuter la commande :" msgstr "Exécuter la commande :"
#: pronterface.py:957 #: pronterface.py:671
msgid "click to add new custom button" msgid "click to add new custom button"
msgstr "Ajouter un bouton personnalisé" msgstr "Ajouter un bouton personnalisé"
#: pronterface.py:979 #: pronterface.py:693
msgid "" msgid ""
"Defines custom button. Usage: button <num> \"title\" [/c \"colour\"] command" "Defines custom button. Usage: button <num> \"title\" [/c \"colour\"] command"
msgstr "" msgstr ""
"Définit des boutons personnalidés. Utilisation : <numero> \"Libelle\" [/c " "Définit des boutons personnalidés. Utilisation : <numero> \"Libelle\" [/c "
"\"couleur\"] commande" "\"couleur\"] commande"
#: pronterface.py:1003 #: pronterface.py:715
msgid "Custom button number should be between 0 and 63" msgid "Custom button number should be between 0 and 63"
msgstr "" msgstr ""
"Les numéros des boutons personnalisés doivent être compris entre 0 et 63." "Les numéros des boutons personnalisés doivent être compris entre 0 et 63."
#: pronterface.py:1096 #: pronterface.py:806
msgid "Edit custom button '%s'" msgid "Edit custom button '%s'"
msgstr "Editer le bouton personnalisé '%s'" msgstr "Editer le bouton personnalisé '%s'"
#: pronterface.py:1098 #: pronterface.py:808
msgid "Move left <<" msgid "Move left <<"
msgstr "Déplacer vers la gauche <<" msgstr "Déplacer vers la gauche <<"
#: pronterface.py:1101 #: pronterface.py:811
msgid "Move right >>" msgid "Move right >>"
msgstr "Déplacer vers la droite >>" msgstr "Déplacer vers la droite >>"
#: pronterface.py:1105 #: pronterface.py:815
msgid "Remove custom button '%s'" msgid "Remove custom button '%s'"
msgstr "Supprimer le bouton personnalisé '%s'" msgstr "Supprimer le bouton personnalisé '%s'"
#: pronterface.py:1108 #: pronterface.py:818
msgid "Add custom button" msgid "Add custom button"
msgstr "Ajouter un bouton personnalisé" msgstr "Ajouter un bouton personnalisé"
#: pronterface.py:1270 #: pronterface.py:987
msgid "event object missing" msgid "event object missing"
msgstr "événement d'objet manquant" msgstr "événement d'objet manquant"
#: pronterface.py:1306 #: pronterface.py:1018
msgid "Invalid period given."
msgstr "La période donnée est invalide"
#: pronterface.py:1311
msgid "Monitoring printer." msgid "Monitoring printer."
msgstr "Imprimante sous surveillance." msgstr "Imprimante sous surveillance."
#: pronterface.py:1315 #: pronterface.py:1033
msgid "Done monitoring." msgid ""
msgstr "Surveillance de l'imprimante effectuée." "Attempted to write invalid text to console, which could be due to an invalid "
"baudrate"
msgstr ""
#: pronterface.py:1355 #: pronterface.py:1084
msgid " SD printing:%04.2f %%" msgid " SD printing:%04.2f %%"
msgstr " Impression SD : %04.2f %%" msgstr " Impression SD : %04.2f %%"
#: pronterface.py:1358 #: pronterface.py:1087
msgid " Printing: %04.2f%% |" msgid " Printing: %04.2f%% |"
msgstr " Impression : %04.2f%% |" msgstr " Impression : %04.2f%% |"
#: pronterface.py:1359 #: pronterface.py:1088
msgid " Line# %d of %d lines |" msgid " Line# %d of %d lines |"
msgstr " Ligne# %d sur %d lignes |" msgstr " Ligne# %d sur %d lignes |"
#: pronterface.py:1364 #: pronterface.py:1093
msgid " Est: %s of %s remaining | " msgid " Est: %s of %s remaining | "
msgstr " ETA: %s restant sur %s | " msgstr " ETA: %s restant sur %s | "
#: pronterface.py:1366 #: pronterface.py:1095
msgid " Z: %0.2f mm" msgid " Z: %0.2f mm"
msgstr " Z: %0.2f mm" msgstr " Z: %0.2f mm"
#: pronterface.py:1439 #: pronterface.py:1190
msgid "Opening file failed."
msgstr "L'ouverture du fichier a échoué"
#: pronterface.py:1445
msgid "Starting print"
msgstr "Début de l'impression..."
#: pronterface.py:1466
msgid "Pick SD file" msgid "Pick SD file"
msgstr "Choisir un fichier sur la carte SD" msgstr "Choisir un fichier sur la carte SD"
#: pronterface.py:1466 #: pronterface.py:1190
msgid "Select the file to print" msgid "Select the file to print"
msgstr "Sélectionnez le fichier à imprimer :" msgstr "Sélectionnez le fichier à imprimer :"
#: pronterface.py:1501 #: pronterface.py:1222
msgid "Failed to execute slicing software: " msgid "Failed to execute slicing software: "
msgstr "Une erreur s'est produite lors du slicing : " msgstr "Une erreur s'est produite lors du slicing : "
#: pronterface.py:1510 #: pronterface.py:1229
msgid "Slicing..." msgid "Slicing..."
msgstr "Slicing..." msgstr "Slicing..."
#: pronterface.py:1523 #: pronterface.py:1241 pronterface.py:1300
msgid ", %d lines" msgid "Loaded %s, %d lines"
msgstr ", %d lignes" msgstr "%s chargé, %d lignes"
#: pronterface.py:1523
msgid "Loaded "
msgstr "Chargé "
#: pronterface.py:1530 #: pronterface.py:1248
msgid "Load File" msgid "Load File"
msgstr "Charger un fichier" msgstr "Charger un fichier"
#: pronterface.py:1536 #: pronterface.py:1254
msgid "Slicing " msgid "Slicing "
msgstr "Slicing " msgstr "Slicing "
#: pronterface.py:1555 #: pronterface.py:1279
msgid "Open file to print" msgid "Open file to print"
msgstr "Ouvrir un fichier à imprimer" msgstr "Ouvrir un fichier à imprimer"
#: pronterface.py:1556 #: pronterface.py:1280
msgid "" msgid ""
"OBJ, STL, and GCODE files (*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ)|*." "OBJ, STL, and GCODE files (*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ)|*."
"gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ|All Files (*.*)|*.*" "gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ|All Files (*.*)|*.*"
...@@ -541,86 +1141,87 @@ msgstr "" ...@@ -541,86 +1141,87 @@ msgstr ""
"Fichiers OBJ, STL et GCODE (;*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ;)|*." "Fichiers OBJ, STL et GCODE (;*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ;)|*."
"gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ|Tous les fichiers (*.*)|*.*" "gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ|Tous les fichiers (*.*)|*.*"
#: pronterface.py:1563 #: pronterface.py:1311
msgid "File not found!" msgid "mm of filament used in this print"
msgstr "Fichier non trouvé" msgstr "mm de filament utilisés pour cette impression"
#: pronterface.py:1577
msgid "Loaded %s, %d lines"
msgstr "%s chargé, %d lignes"
#: pronterface.py:1588 #: pronterface.py:1312
msgid "mm of filament used in this print\n" msgid "The print goes:"
msgstr "mm de filament utilisés pour cette impression\n" msgstr "L'impression va :"
#: pronterface.py:1589 pronterface.py:1591 #: pronterface.py:1313
msgid "" msgid "- from %.2f mm to %.2f mm in X and is %.2f mm wide"
"the print goes from %f mm to %f mm in X\n" msgstr "- de %.02f mm à %.02f mm en X et mesure %.02f mm de large"
"and is %f mm wide\n"
msgstr ""
"L'impression va de %f mm à %f mm en X\n"
"et mesure %f mm de large\n"
#: pronterface.py:1592 #: pronterface.py:1314
msgid "" msgid "- from %.2f mm to %.2f mm in Y and is %.2f mm deep"
"the print goes from %f mm to %f mm in Y\n" msgstr "- de %.02f mm à %.02f mm en Y et mesure %.02f mm de profondeur"
"and is %f mm wide\n"
msgstr ""
"L'impression va de %f mm à %f mm en Y\n"
"et mesure %f mm de large\n"
#: pronterface.py:1593 #: pronterface.py:1315
msgid "" msgid "- from %.2f mm to %.2f mm in Z and is %.2f mm high"
"the print goes from %f mm to %f mm in Z\n" msgstr "- de %.02f mm à %.02f mm en Y et mesure %.02f mm de haut"
"and is %f mm high\n"
msgstr ""
"L'impression va de %f mm à %f mm en Y\n"
"et mesure %f mm de haut\n"
#: pronterface.py:1595
msgid "Estimated duration (pessimistic): "
msgstr "Durée estimée (pessimiste) : "
#: pronterface.py:1622 #: pronterface.py:1316
msgid "No file loaded. Please use load first." msgid "Estimated duration: %s"
msgstr "Aucun fichier chargé. Veuillez charger un fichier avant." msgstr "Durée estimée : %s"
#: pronterface.py:1633 #: pronterface.py:1347
msgid "Restart" msgid "Restart"
msgstr "Recommencer" msgstr "Recommencer"
#: pronterface.py:1637 #: pronterface.py:1351
msgid "File upload complete" msgid "File upload complete"
msgstr "Envoi du fichier terminé" msgstr "Envoi du fichier terminé"
#: pronterface.py:1656 #: pronterface.py:1370
msgid "Pick SD filename" msgid "Pick SD filename"
msgstr "Lister les fichiers sur la carte SD" msgstr "Lister les fichiers sur la carte SD"
#: pronterface.py:1663 #: pronterface.py:1377
msgid "Paused." msgid "Paused."
msgstr "En pause." msgstr "En pause."
#: pronterface.py:1674 #: pronterface.py:1390
msgid "Resume" msgid "Resume"
msgstr "Reprendre" msgstr "Reprendre"
#: pronterface.py:1688 #: pronterface.py:1404
msgid "Connecting..." msgid "Connecting..."
msgstr "Connexion en cours..." msgstr "Connexion en cours..."
#: pronterface.py:1738 #: pronterface.py:1430
msgid "Error: You are trying to connect to a non-exisiting port."
msgstr ""
#: pronterface.py:1432
msgid "Error: You don't have permission to open %s."
msgstr ""
#: pronterface.py:1433
msgid "You might need to add yourself to the dialout group."
msgstr ""
#: pronterface.py:1466
msgid "Disconnected." msgid "Disconnected."
msgstr "Déconnecté." msgstr "Déconnecté."
#: pronterface.py:1771 #: pronterface.py:1499
msgid "Reset." msgid "Reset."
msgstr "Réinitialisée." msgstr "Réinitialisée."
#: pronterface.py:1772 #: pronterface.py:1500
msgid "Are you sure you want to reset the printer?" msgid "Are you sure you want to reset the printer?"
msgstr "Etes-vous sûr de vouloir réinitialiser l'imprimante?" msgstr "Etes-vous sûr de vouloir réinitialiser l'imprimante?"
#: pronterface.py:1772 #: pronterface.py:1500
msgid "Reset?" msgid "Reset?"
msgstr "Réinitialiser ?" msgstr "Réinitialiser ?"
#~ msgid "Failed to start web interface"
#~ msgstr "Échec du lancement de l'interface web"
#~ msgid "CherryPy is not installed. Web Interface Disabled."
#~ msgstr "CherryPy n'est pas installé. L'interface web est désactivée."
#~ msgid ", %d lines"
#~ msgstr ", %d lignes"
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2012-08-08 10:09+CEST\n" "POT-Creation-Date: 2013-05-22 12:58+CEST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -15,575 +15,1140 @@ msgstr "" ...@@ -15,575 +15,1140 @@ msgstr ""
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"
#: printrun/pronterface_widgets.py:34 #: ./plater.py:246
msgid "Find" msgid "Plate building tool"
msgstr "" msgstr ""
#: printrun/pronterface_widgets.py:36 #: ./plater.py:252
msgid "Save" msgid "Clear"
msgstr ""
#: ./plater.py:253
msgid "Load"
msgstr ""
#: ./plater.py:255 ./plater.py:258
msgid "Export"
msgstr ""
#: ./plater.py:260
msgid "Done"
msgstr "" msgstr ""
#: printrun/pronterface_widgets.py:41 pronterface.py:477 pronterface.py:1535 #: ./plater.py:262 ./printrun/pronterface_widgets.py:42 ./pronterface.py:494
#: ./pronterface.py:1253
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: printrun/pronterface_widgets.py:125 #: ./plater.py:264
msgid "Snap to Z = 0"
msgstr ""
#: ./plater.py:265
msgid "Put at 100, 100"
msgstr ""
#: ./plater.py:266
msgid "Delete"
msgstr ""
#: ./plater.py:267
msgid "Auto"
msgstr ""
#: ./plater.py:291
msgid "Autoplating"
msgstr ""
#: ./plater.py:319
msgid "Bed full, sorry sir :("
msgstr ""
#: ./plater.py:329
msgid "Are you sure you want to clear the grid? All unsaved changes will be lost."
msgstr ""
#: ./plater.py:329
msgid "Clear the grid?"
msgstr ""
#: ./plater.py:371
msgid "Pick file to save to"
msgstr ""
#: ./plater.py:372
msgid "STL files (;*.stl;*.STL;)"
msgstr ""
#: ./plater.py:393
msgid "wrote %s"
msgstr ""
#: ./plater.py:396
msgid "Pick file to load"
msgstr ""
#: ./plater.py:397
msgid "STL files (;*.stl;*.STL;)|*.stl|OpenSCAD files (;*.scad;)|*.scad"
msgstr ""
#: ./printrun/gui.py:21 ./pronterface.py:26
msgid "WX is not installed. This program requires WX to run."
msgstr ""
#: ./printrun/gui.py:81
msgid "XY:"
msgstr ""
#: ./printrun/gui.py:83
msgid "mm/min Z:"
msgstr ""
#: ./printrun/gui.py:88
msgid "Watch"
msgstr ""
#: ./printrun/gui.py:93
msgid "Heat:"
msgstr ""
#: ./printrun/gui.py:96
msgid "Switch Hotend Off"
msgstr ""
#: ./printrun/gui.py:96 ./printrun/gui.py:115
msgid "Off"
msgstr ""
#: ./printrun/gui.py:108
msgid "Switch Hotend On"
msgstr ""
#: ./printrun/gui.py:108 ./printrun/gui.py:127
msgid "Set"
msgstr ""
#: ./printrun/gui.py:112 ./printrun/gui.py:176
msgid "Bed:"
msgstr ""
#: ./printrun/gui.py:115
msgid "Switch Heated Bed Off"
msgstr ""
#: ./printrun/gui.py:156
msgid "mm"
msgstr ""
#: ./printrun/gui.py:165
msgid ""
"mm/\n"
"min"
msgstr ""
#: ./printrun/gui.py:174
msgid "Heater:"
msgstr ""
#: ./printrun/gui.py:248
msgid "Send"
msgstr ""
#: ./printrun/gui.py:248
msgid "Send Command to Printer"
msgstr ""
#: ./printrun/gui.py:256
msgid ""
"Communication Settings\n"
"Click to rescan ports"
msgstr ""
#: ./printrun/gui.py:256
msgid "Port"
msgstr ""
#: ./printrun/gui.py:277
msgid "Connect to the printer"
msgstr ""
#: ./printrun/gui.py:277 ./pronterface.py:1475
msgid "Connect"
msgstr ""
#: ./printrun/gui.py:279
msgid "Reset"
msgstr ""
#: ./printrun/gui.py:279
msgid "Reset the printer"
msgstr ""
#: ./printrun/gui.py:280
msgid "Load a 3D model file"
msgstr ""
#: ./printrun/gui.py:280
msgid "Load file"
msgstr ""
#: ./printrun/gui.py:281
msgid "Compose"
msgstr ""
#: ./printrun/gui.py:281
msgid "Simple Plater System"
msgstr ""
#: ./printrun/gui.py:282
msgid "SD"
msgstr ""
#: ./printrun/gui.py:282
msgid "SD Card Printing"
msgstr ""
#: ./printrun/gui.py:284
msgid "Start Printing Loaded File"
msgstr ""
#: ./printrun/gui.py:284 ./pronterface.py:227 ./pronterface.py:1243
#: ./pronterface.py:1301 ./pronterface.py:1421 ./pronterface.py:1493
#: ./pronterface.py:1506
msgid "Print"
msgstr ""
#: ./printrun/gui.py:286
msgid "Pause Current Print"
msgstr ""
#: ./printrun/gui.py:286 ./pronterface.py:1302 ./pronterface.py:1345
#: ./pronterface.py:1397 ./pronterface.py:1420 ./pronterface.py:1492
#: ./pronterface.py:1509
msgid "Pause"
msgstr ""
#: ./printrun/gui.py:287
msgid "Recover"
msgstr ""
#: ./printrun/gui.py:287
msgid "Recover previous Print"
msgstr ""
#: ./printrun/gui.py:308 ./pronsole.py:625 ./pronsole.py:681
#: ./pronterface.py:1113 ./pronterface.py:1339 ./pronterface.py:1451
msgid "Not connected to printer."
msgstr ""
#: ./printrun/libtatlin/actors.py:272
msgid "Initialized 3D visualization in %.2f seconds"
msgstr ""
#: ./printrun/libtatlin/actors.py:273
msgid "Vertex count: %d"
msgstr ""
#: ./printrun/pronterface_widgets.py:35
msgid "Find"
msgstr ""
#: ./printrun/pronterface_widgets.py:37
msgid "Save"
msgstr ""
#: ./printrun/pronterface_widgets.py:126
msgid "Edit settings" msgid "Edit settings"
msgstr "" msgstr ""
#: printrun/pronterface_widgets.py:127 #: ./printrun/pronterface_widgets.py:128
msgid "Defaults" msgid "Defaults"
msgstr "" msgstr ""
#: printrun/pronterface_widgets.py:156 #: ./printrun/pronterface_widgets.py:157
msgid "Custom button" msgid "Custom button"
msgstr "" msgstr ""
#: printrun/pronterface_widgets.py:161 #: ./printrun/pronterface_widgets.py:162
msgid "Button title" msgid "Button title"
msgstr "" msgstr ""
#: printrun/pronterface_widgets.py:164 #: ./printrun/pronterface_widgets.py:165
msgid "Command" msgid "Command"
msgstr "" msgstr ""
#: printrun/pronterface_widgets.py:173 #: ./printrun/pronterface_widgets.py:174
msgid "Color" msgid "Color"
msgstr "" msgstr ""
#: pronterface.py:26 #: ./pronsole.py:173
msgid "WX is not installed. This program requires WX to run." msgid "Communications Speed (default: 115200)"
msgstr ""
#: ./pronsole.py:174
msgid "Heated Build Platform temp for ABS (default: 110 deg C)"
msgstr "" msgstr ""
#: pronterface.py:93 #: ./pronsole.py:175
msgid "Heated Build Platform temp for PLA (default: 60 deg C)"
msgstr ""
#: ./pronsole.py:176
msgid "Feedrate for Control Panel Moves in Extrusions (default: 300mm/min)"
msgstr ""
#: ./pronsole.py:177
msgid "Port used to communicate with printer"
msgstr ""
#: ./pronsole.py:178
msgid ""
"Slice command\n"
" default:\n"
" python skeinforge/skeinforge_application/skeinforge_utilities/skeinforge_craft.py $s)"
msgstr ""
#: ./pronsole.py:179
msgid ""
"Slice settings command\n"
" default:\n"
" python skeinforge/skeinforge_application/skeinforge.py"
msgstr ""
#: ./pronsole.py:180
msgid "Extruder temp for ABS (default: 230 deg C)"
msgstr ""
#: ./pronsole.py:181
msgid "Extruder temp for PLA (default: 185 deg C)"
msgstr ""
#: ./pronsole.py:182
msgid "Feedrate for Control Panel Moves in X and Y (default: 3000mm/min)"
msgstr ""
#: ./pronsole.py:183
msgid "Feedrate for Control Panel Moves in Z (default: 200mm/min)"
msgstr ""
#: ./pronsole.py:184
msgid "Executable to run when the print is finished"
msgstr ""
#: ./pronsole.py:622
msgid "Please enter target name in 8.3 format."
msgstr ""
#: ./pronsole.py:628
msgid "Uploading as %s"
msgstr ""
#: ./pronsole.py:629
msgid "Uploading %s"
msgstr ""
#: ./pronsole.py:631
msgid "Press Ctrl-C to interrupt upload."
msgstr ""
#: ./pronsole.py:634
msgid "Progress: "
msgstr ""
#: ./pronsole.py:646
msgid "Upload completed. %s should now be on the card."
msgstr ""
#: ./pronsole.py:649
msgid "...interrupted!"
msgstr ""
#: ./pronsole.py:655
msgid "A partial file named %s may have been written to the sd card."
msgstr ""
#: ./pronsole.py:672
msgid "Send a loaded gcode file to the printer. Load a file with the load command first."
msgstr ""
#: ./pronsole.py:674
msgid "Send a loaded gcode file to the printer. You have %s loaded right now."
msgstr ""
#: ./pronsole.py:678 ./pronterface.py:1336
msgid "No file loaded. Please use load first."
msgstr ""
#: ./pronsole.py:683
msgid "Printing %s"
msgstr ""
#: ./pronsole.py:684
msgid "You can monitor the print with the monitor command."
msgstr ""
#: ./pronsole.py:692
msgid "Not printing, cannot pause."
msgstr ""
#: ./pronsole.py:698
msgid "Pauses a running print"
msgstr ""
#: ./pronsole.py:702
msgid "Not paused, unable to resume. Start a print first."
msgstr ""
#: ./pronsole.py:712
msgid "Resumes a paused print."
msgstr ""
#: ./pronsole.py:727
msgid "Files on SD card:"
msgstr ""
#: ./pronsole.py:741 ./pronsole.py:783 ./pronsole.py:1088
msgid "Printer is not online. Please connect to it first."
msgstr ""
#: ./pronsole.py:746
msgid "Lists files on the SD card"
msgstr ""
#: ./pronsole.py:750 ./pronterface.py:1163
msgid "Opening file failed."
msgstr ""
#: ./pronsole.py:756 ./pronterface.py:1169
msgid "Starting print"
msgstr ""
#: ./pronsole.py:779
msgid "Resets the printer."
msgstr ""
#: ./pronsole.py:789
msgid "File is not present on card. Please upload it first."
msgstr ""
#: ./pronsole.py:793
msgid "Printing file: %s from SD card."
msgstr ""
#: ./pronsole.py:794
msgid "Requesting SD print..."
msgstr ""
#: ./pronsole.py:798
msgid "Print a file from the SD card. Tab completes with available file names."
msgstr ""
#: ./pronsole.py:799
msgid "sdprint filename.g"
msgstr ""
#: ./pronsole.py:834 ./pronsole.py:842 ./pronsole.py:892 ./pronsole.py:918
#: ./pronterface.py:354 ./pronterface.py:374 ./pronterface.py:436
msgid "Printer is not online."
msgstr ""
#: ./pronsole.py:875
msgid "Read the extruder and bed temperature."
msgstr ""
#: ./pronsole.py:885
msgid "%s is a high temperature to set your extruder to. Are you sure you want to do that?"
msgstr ""
#: ./pronsole.py:890
msgid "Setting hotend temperature to %s degrees Celsius."
msgstr ""
#: ./pronsole.py:894 ./pronterface.py:356
msgid "You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0."
msgstr ""
#: ./pronsole.py:896 ./pronsole.py:922
msgid "You must enter a temperature."
msgstr ""
#: ./pronsole.py:899
msgid "Sets the hotend temperature to the value entered."
msgstr ""
#: ./pronsole.py:900 ./pronsole.py:926
msgid "Enter either a temperature in celsius or one of the following keywords"
msgstr ""
#: ./pronsole.py:916
msgid "Setting bed temperature to %s degrees Celsius."
msgstr ""
#: ./pronsole.py:920 ./pronterface.py:376
msgid "You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0."
msgstr ""
#: ./pronsole.py:925
msgid "Sets the bed temperature to the value entered."
msgstr ""
#: ./pronsole.py:935
msgid "No move specified."
msgstr ""
#: ./pronsole.py:938 ./pronsole.py:1041 ./pronsole.py:1182
msgid "Printer is currently printing. Please pause the print before you issue manual commands."
msgstr ""
#: ./pronsole.py:941 ./pronsole.py:1179
msgid "Printer is not online. Unable to move."
msgstr ""
#: ./pronsole.py:957
msgid "Unknown axis."
msgstr ""
#: ./pronsole.py:963
msgid "Invalid distance"
msgstr ""
#: ./pronsole.py:974
msgid "Move an axis. Specify the name of the axis and the amount. "
msgstr ""
#: ./pronsole.py:975
msgid "move X 10 will move the X axis forward by 10mm at %s mm/min (default XY speed)"
msgstr ""
#: ./pronsole.py:976
msgid "move Y 10 5000 will move the Y axis forward by 10mm at 5000mm/min"
msgstr ""
#: ./pronsole.py:977
msgid "move Z -1 will move the Z axis down by 1mm at %s mm/min (default Z speed)"
msgstr ""
#: ./pronsole.py:978
msgid "Common amounts are in the tabcomplete list."
msgstr ""
#: ./pronsole.py:1008 ./pronsole.py:1048
msgid "Invalid length given."
msgstr ""
#: ./pronsole.py:1013 ./pronsole.py:1053
msgid "Invalid speed given."
msgstr ""
#: ./pronsole.py:1018
msgid "Extruding %fmm of filament."
msgstr ""
#: ./pronsole.py:1020
msgid "Reversing %fmm of filament."
msgstr ""
#: ./pronsole.py:1022
msgid "Length is 0, not doing anything."
msgstr ""
#: ./pronsole.py:1028
msgid "Extrudes a length of filament, 5mm by default, or the number of mm given as a parameter"
msgstr ""
#: ./pronsole.py:1029
msgid "extrude - extrudes 5mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: ./pronsole.py:1030
msgid "extrude 20 - extrudes 20mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: ./pronsole.py:1031
msgid "extrude -5 - REVERSES 5mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: ./pronsole.py:1032
msgid "extrude 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)"
msgstr ""
#: ./pronsole.py:1038
msgid "Printer is not online. Unable to reverse."
msgstr ""
#: ./pronsole.py:1057
msgid "Reverses the extruder, 5mm by default, or the number of mm given as a parameter"
msgstr ""
#: ./pronsole.py:1058
msgid "reverse - reverses 5mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: ./pronsole.py:1059
msgid "reverse 20 - reverses 20mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: ./pronsole.py:1060
msgid "reverse 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)"
msgstr ""
#: ./pronsole.py:1061
msgid "reverse -5 - EXTRUDES 5mm of filament at 300mm/min (5mm/s)"
msgstr ""
#: ./pronsole.py:1078
msgid "Exiting program. Goodbye!"
msgstr ""
#: ./pronsole.py:1083
msgid "Disconnects from the printer and exits the program."
msgstr ""
#: ./pronsole.py:1091
msgid "Printer is not printing. Please print something before monitoring."
msgstr ""
#: ./pronsole.py:1093
msgid "Monitoring printer, use ^C to interrupt."
msgstr ""
#: ./pronsole.py:1098 ./pronterface.py:1015
msgid "Invalid period given."
msgstr ""
#: ./pronsole.py:1099
msgid "Updating values every %f seconds."
msgstr ""
#: ./pronsole.py:1110 ./pronsole.py:1113
msgid "Print progress: "
msgstr ""
#: ./pronsole.py:1122 ./pronterface.py:1020
msgid "Done monitoring."
msgstr ""
#: ./pronsole.py:1126
msgid "Monitor a machine's temperatures and an SD print's status."
msgstr ""
#: ./pronsole.py:1127
msgid "monitor - Reports temperature and SD print status (if SD printing) every 5 seconds"
msgstr ""
#: ./pronsole.py:1128
msgid "monitor 2 - Reports temperature and SD print status (if SD printing) every 2 seconds"
msgstr ""
#: ./pronsole.py:1136
msgid "No file name given."
msgstr ""
#: ./pronsole.py:1142
msgid "Skeining file: %s"
msgstr ""
#: ./pronsole.py:1144 ./pronterface.py:1287
msgid "File not found!"
msgstr ""
#: ./pronsole.py:1149
msgid "Entering slicer settings: %s"
msgstr ""
#: ./pronsole.py:1153
msgid "Slicing: "
msgstr ""
#: ./pronsole.py:1156
msgid "Loading sliced file."
msgstr ""
#: ./pronsole.py:1159
msgid "Skeinforge execution failed: %s"
msgstr ""
#: ./pronsole.py:1172
msgid "Creates a gcode file from an stl model using the slicer (with tab-completion)"
msgstr ""
#: ./pronsole.py:1173
msgid "skein filename.stl - create gcode file"
msgstr ""
#: ./pronsole.py:1174
msgid "skein filename.stl view - create gcode file and view using skeiniso"
msgstr ""
#: ./pronsole.py:1175
msgid "skein set - adjust slicer settings"
msgstr ""
#: ./pronsole.py:1197
msgid "Homes the printer"
msgstr ""
#: ./pronsole.py:1198
msgid "home - homes all axes and zeroes the extruder(Using G28 and G92)"
msgstr ""
#: ./pronsole.py:1199
msgid "home xy - homes x and y axes (Using G28)"
msgstr ""
#: ./pronsole.py:1200
msgid "home z - homes z axis only (Using G28)"
msgstr ""
#: ./pronsole.py:1201
msgid "home e - set extruder position to zero (Using G92)"
msgstr ""
#: ./pronsole.py:1202
msgid "home xyze - homes all axes and zeroes the extruder (Using G28 and G92)"
msgstr ""
#: ./pronsole.py:1205
msgid "load this file on startup instead of .pronsolerc ; you may chain config files, if so settings auto-save will use the last specified file"
msgstr ""
#: ./pronsole.py:1206
msgid "executes command after configuration/.pronsolerc is loaded ; macros/settings from these commands are not autosaved"
msgstr ""
#: ./pronsole.py:1207
msgid "file to load"
msgstr ""
#: ./pronterface.py:100
msgid "" msgid ""
"Dimensions of Build Platform\n" "Dimensions of Build Platform\n"
" & optional offset of origin\n" " & optional offset of origin\n"
" & optional switch position\n"
"\n" "\n"
"Examples:\n" "Examples:\n"
" XXXxYYY\n" " XXXxYYY\n"
" XXX,YYY,ZZZ\n" " XXX,YYY,ZZZ\n"
" XXXxYYYxZZZ+OffX+OffY+OffZ" " XXXxYYYxZZZ+OffX+OffY+OffZ\n"
"XXXxYYYxZZZ+OffX+OffY+OffZ+HomeX+HomeY+HomeZ"
msgstr "" msgstr ""
#: pronterface.py:94 #: ./pronterface.py:101
msgid "Last Set Temperature for the Heated Print Bed" msgid "Last Set Temperature for the Heated Print Bed"
msgstr "" msgstr ""
#: pronterface.py:95 #: ./pronterface.py:102
msgid "Folder of last opened file" msgid "Folder of last opened file"
msgstr "" msgstr ""
#: pronterface.py:96 #: ./pronterface.py:103
msgid "Last Temperature of the Hot End" msgid "Last Temperature of the Hot End"
msgstr "" msgstr ""
#: pronterface.py:97 #: ./pronterface.py:104
msgid "Width of Extrusion in Preview (default: 0.5)" msgid "Width of Extrusion in Preview (default: 0.5)"
msgstr "" msgstr ""
#: pronterface.py:98 #: ./pronterface.py:105
msgid "Fine Grid Spacing (default: 10)" msgid "Fine Grid Spacing (default: 10)"
msgstr "" msgstr ""
#: pronterface.py:99 #: ./pronterface.py:106
msgid "Coarse Grid Spacing (default: 50)" msgid "Coarse Grid Spacing (default: 50)"
msgstr "" msgstr ""
#: pronterface.py:100 #: ./pronterface.py:107
msgid "Pronterface background color (default: #FFFFFF)" msgid "Pronterface background color (default: #FFFFFF)"
msgstr "" msgstr ""
#: pronterface.py:103 #: ./pronterface.py:110
msgid "Printer Interface" msgid "Printer Interface"
msgstr "" msgstr ""
#: pronterface.py:122 #: ./pronterface.py:127
msgid "Motors off" msgid "Motors off"
msgstr "" msgstr ""
#: pronterface.py:122 #: ./pronterface.py:127
msgid "Switch all motors off" msgid "Switch all motors off"
msgstr "" msgstr ""
#: pronterface.py:123 #: ./pronterface.py:128
msgid "Check current hotend temperature" msgid "Check current hotend temperature"
msgstr "" msgstr ""
#: pronterface.py:123 #: ./pronterface.py:128
msgid "Check temp" msgid "Check temp"
msgstr "" msgstr ""
#: pronterface.py:124 #: ./pronterface.py:129
msgid "Advance extruder by set length" msgid "Advance extruder by set length"
msgstr "" msgstr ""
#: pronterface.py:124 #: ./pronterface.py:129
msgid "Extrude" msgid "Extrude"
msgstr "" msgstr ""
#: pronterface.py:125 #: ./pronterface.py:130
msgid "Reverse" msgid "Reverse"
msgstr "" msgstr ""
#: pronterface.py:125 #: ./pronterface.py:130
msgid "Reverse extruder by set length" msgid "Reverse extruder by set length"
msgstr "" msgstr ""
#: pronterface.py:143 #: ./pronterface.py:173
msgid "" msgid ""
"# I moved all your custom buttons into .pronsolerc.\n" "# I moved all your custom buttons into .pronsolerc.\n"
"# Please don't add them here any more.\n" "# Please don't add them here any more.\n"
"# Backup of your old buttons is in custombtn.old\n" "# Backup of your old buttons is in custombtn.old\n"
msgstr "" msgstr ""
#: pronterface.py:148 #: ./pronterface.py:178
msgid "Note!!! You have specified custom buttons in both custombtn.txt and .pronsolerc" msgid "Note!!! You have specified custom buttons in both custombtn.txt and .pronsolerc"
msgstr "" msgstr ""
#: pronterface.py:149 #: ./pronterface.py:179
msgid "Ignoring custombtn.txt. Remove all current buttons to revert to custombtn.txt" msgid "Ignoring custombtn.txt. Remove all current buttons to revert to custombtn.txt"
msgstr "" msgstr ""
#: pronterface.py:181 #: ./pronterface.py:209
msgid "Failed to start web interface" msgid "display graphical temperature gauges in addition to the temperatures graph"
msgstr "" msgstr ""
#: pronterface.py:185 #: ./pronterface.py:210
msgid "CherryPy is not installed. Web Interface Disabled." msgid "automatically try to connect to printer on startup"
msgstr "" msgstr ""
#: pronterface.py:197 pronterface.py:603 pronterface.py:1525 #: ./pronterface.py:219
#: pronterface.py:1578 pronterface.py:1705 pronterface.py:1765 msgid "Print Started at: %s"
#: pronterface.py:1778 msgstr ""
msgid "Print"
#: ./pronterface.py:224
msgid "Print ended at: %(end_time)s and took %(duration)s"
msgstr "" msgstr ""
#: pronterface.py:207 #: ./pronterface.py:238
msgid "Printer is now online." msgid "Printer is now online."
msgstr "" msgstr ""
#: pronterface.py:208 #: ./pronterface.py:239
msgid "Disconnect" msgid "Disconnect"
msgstr "" msgstr ""
#: pronterface.py:331 #: ./pronterface.py:351
msgid "Setting hotend temperature to %f degrees Celsius." msgid "Setting hotend temperature to %f degrees Celsius."
msgstr "" msgstr ""
#: pronterface.py:334 pronterface.py:356 pronterface.py:428 #: ./pronterface.py:358 ./pronterface.py:378
msgid "Printer is not online."
msgstr ""
#: pronterface.py:336
msgid "You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0."
msgstr ""
#: pronterface.py:338 pronterface.py:364
msgid "You must enter a temperature. (%s)" msgid "You must enter a temperature. (%s)"
msgstr "" msgstr ""
#: pronterface.py:353 #: ./pronterface.py:371
msgid "Setting bed temperature to %f degrees Celsius." msgid "Setting bed temperature to %f degrees Celsius."
msgstr "" msgstr ""
#: pronterface.py:360 #: ./pronterface.py:393
msgid "You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0."
msgstr ""
#: pronterface.py:381
msgid "Do you want to erase the macro?" msgid "Do you want to erase the macro?"
msgstr "" msgstr ""
#: pronterface.py:385 #: ./pronterface.py:397
msgid "Cancelled." msgid "Cancelled."
msgstr "" msgstr ""
#: pronterface.py:436 #: ./pronterface.py:442
msgid " Opens file" msgid " Opens file"
msgstr "" msgstr ""
#: pronterface.py:436 #: ./pronterface.py:442
msgid "&Open..." msgid "&Open..."
msgstr "" msgstr ""
#: pronterface.py:437 #: ./pronterface.py:443
msgid " Edit open file" msgid " Edit open file"
msgstr "" msgstr ""
#: pronterface.py:437 #: ./pronterface.py:443
msgid "&Edit..." msgid "&Edit..."
msgstr "" msgstr ""
#: pronterface.py:438 #: ./pronterface.py:444
msgid " Clear output console" msgid " Clear output console"
msgstr "" msgstr ""
#: pronterface.py:438 #: ./pronterface.py:444
msgid "Clear console" msgid "Clear console"
msgstr "" msgstr ""
#: pronterface.py:439 #: ./pronterface.py:445
msgid " Project slices" msgid " Project slices"
msgstr "" msgstr ""
#: pronterface.py:439 #: ./pronterface.py:445
msgid "Projector" msgid "Projector"
msgstr "" msgstr ""
#: pronterface.py:440 #: ./pronterface.py:446
msgid " Closes the Window" msgid " Closes the Window"
msgstr "" msgstr ""
#: pronterface.py:440 #: ./pronterface.py:446
msgid "E&xit" msgid "E&xit"
msgstr "" msgstr ""
#: pronterface.py:441 #: ./pronterface.py:447
msgid "&File" msgid "&File"
msgstr "" msgstr ""
#: pronterface.py:446 #: ./pronterface.py:452
msgid "&Macros" msgid "&Macros"
msgstr "" msgstr ""
#: pronterface.py:447 #: ./pronterface.py:453
msgid "<&New...>" msgid "<&New...>"
msgstr "" msgstr ""
#: pronterface.py:448 #: ./pronterface.py:454
msgid " Options dialog" msgid " Options dialog"
msgstr "" msgstr ""
#: pronterface.py:448 #: ./pronterface.py:454
msgid "&Options" msgid "&Options"
msgstr "" msgstr ""
#: pronterface.py:450 #: ./pronterface.py:456
msgid " Adjust slicing settings" msgid " Adjust slicing settings"
msgstr "" msgstr ""
#: pronterface.py:450 #: ./pronterface.py:456
msgid "Slicing Settings" msgid "Slicing Settings"
msgstr "" msgstr ""
#: pronterface.py:452 #: ./pronterface.py:458
msgid "Debug G-code"
msgstr ""
#: ./pronterface.py:459
msgid "Print all G-code sent to and received from the printer."
msgstr ""
#: ./pronterface.py:469
msgid "&Settings" msgid "&Settings"
msgstr "" msgstr ""
#: pronterface.py:467 #: ./pronterface.py:484
msgid "Enter macro name" msgid "Enter macro name"
msgstr "" msgstr ""
#: pronterface.py:470 #: ./pronterface.py:487
msgid "Macro name:" msgid "Macro name:"
msgstr "" msgstr ""
#: pronterface.py:473 #: ./pronterface.py:490
msgid "Ok" msgid "Ok"
msgstr "" msgstr ""
#: pronterface.py:495 #: ./pronterface.py:512
msgid "Macro name may contain only ASCII alphanumeric symbols and underscores" msgid "Macro name may contain only ASCII alphanumeric symbols and underscores"
msgstr "" msgstr ""
#: pronterface.py:500 #: ./pronterface.py:515
msgid "Name '%s' is being used by built-in command" msgid "Name '%s' is being used by built-in command"
msgstr "" msgstr ""
#: pronterface.py:548 #: ./pronterface.py:583
msgid "Port"
msgstr ""
#: pronterface.py:570 pronterface.py:1747
msgid "Connect"
msgstr ""
#: pronterface.py:574
msgid "Reset"
msgstr ""
#: pronterface.py:589
msgid "Load file"
msgstr ""
#: pronterface.py:593
msgid "Compose"
msgstr ""
#: pronterface.py:598
msgid "SD"
msgstr ""
#: pronterface.py:608 pronterface.py:1579 pronterface.py:1631
#: pronterface.py:1681 pronterface.py:1704 pronterface.py:1764
#: pronterface.py:1781
msgid "Pause"
msgstr ""
#: pronterface.py:612
msgid "Recover"
msgstr ""
#: pronterface.py:630
msgid "Send"
msgstr ""
#: pronterface.py:671
msgid "XY:"
msgstr ""
#: pronterface.py:673
msgid "mm/min Z:"
msgstr ""
#: pronterface.py:678
msgid "Watch"
msgstr ""
#: pronterface.py:683
msgid "Heat:"
msgstr ""
#: pronterface.py:686 pronterface.py:709
msgid "Off"
msgstr ""
#: pronterface.py:700 pronterface.py:723
msgid "Set"
msgstr ""
#: pronterface.py:706
msgid "Bed:"
msgstr ""
#: pronterface.py:756
msgid "mm"
msgstr ""
#: pronterface.py:764
msgid ""
"mm/\n"
"min"
msgstr ""
#: pronterface.py:821 pronterface.py:1387 pronterface.py:1625
#: pronterface.py:1723
msgid "Not connected to printer."
msgstr ""
#: pronterface.py:869
msgid "SD Upload" msgid "SD Upload"
msgstr "" msgstr ""
#: pronterface.py:873 #: ./pronterface.py:587
msgid "SD Print" msgid "SD Print"
msgstr "" msgstr ""
#: pronterface.py:914 #: ./pronterface.py:628
msgid "Mini mode" msgid "Mini mode"
msgstr "" msgstr ""
#: pronterface.py:921 #: ./pronterface.py:635
msgid "Full mode" msgid "Full mode"
msgstr "" msgstr ""
#: pronterface.py:946 #: ./pronterface.py:660
msgid "Execute command: " msgid "Execute command: "
msgstr "" msgstr ""
#: pronterface.py:957 #: ./pronterface.py:671
msgid "click to add new custom button" msgid "click to add new custom button"
msgstr "" msgstr ""
#: pronterface.py:979 #: ./pronterface.py:693
msgid "Defines custom button. Usage: button <num> \"title\" [/c \"colour\"] command" msgid "Defines custom button. Usage: button <num> \"title\" [/c \"colour\"] command"
msgstr "" msgstr ""
#: pronterface.py:1003 #: ./pronterface.py:715
msgid "Custom button number should be between 0 and 63" msgid "Custom button number should be between 0 and 63"
msgstr "" msgstr ""
#: pronterface.py:1096 #: ./pronterface.py:806
msgid "Edit custom button '%s'" msgid "Edit custom button '%s'"
msgstr "" msgstr ""
#: pronterface.py:1098 #: ./pronterface.py:808
msgid "Move left <<" msgid "Move left <<"
msgstr "" msgstr ""
#: pronterface.py:1101 #: ./pronterface.py:811
msgid "Move right >>" msgid "Move right >>"
msgstr "" msgstr ""
#: pronterface.py:1105 #: ./pronterface.py:815
msgid "Remove custom button '%s'" msgid "Remove custom button '%s'"
msgstr "" msgstr ""
#: pronterface.py:1108 #: ./pronterface.py:818
msgid "Add custom button" msgid "Add custom button"
msgstr "" msgstr ""
#: pronterface.py:1270 #: ./pronterface.py:987
msgid "event object missing" msgid "event object missing"
msgstr "" msgstr ""
#: pronterface.py:1306 #: ./pronterface.py:1018
msgid "Invalid period given."
msgstr ""
#: pronterface.py:1311
msgid "Monitoring printer." msgid "Monitoring printer."
msgstr "" msgstr ""
#: pronterface.py:1315 #: ./pronterface.py:1033
msgid "Done monitoring." msgid "Attempted to write invalid text to console, which could be due to an invalid baudrate"
msgstr "" msgstr ""
#: pronterface.py:1355 #: ./pronterface.py:1084
msgid " SD printing:%04.2f %%" msgid " SD printing:%04.2f %%"
msgstr "" msgstr ""
#: pronterface.py:1358 #: ./pronterface.py:1087
msgid " Printing: %04.2f%% |" msgid " Printing: %04.2f%% |"
msgstr "" msgstr ""
#: pronterface.py:1359 #: ./pronterface.py:1088
msgid " Line# %d of %d lines |" msgid " Line# %d of %d lines |"
msgstr "" msgstr ""
#: pronterface.py:1364 #: ./pronterface.py:1093
msgid " Est: %s of %s remaining | " msgid " Est: %s of %s remaining | "
msgstr "" msgstr ""
#: pronterface.py:1366 #: ./pronterface.py:1095
msgid " Z: %0.2f mm" msgid " Z: %0.2f mm"
msgstr "" msgstr ""
#: pronterface.py:1439 #: ./pronterface.py:1190
msgid "Opening file failed."
msgstr ""
#: pronterface.py:1445
msgid "Starting print"
msgstr ""
#: pronterface.py:1466
msgid "Pick SD file" msgid "Pick SD file"
msgstr "" msgstr ""
#: pronterface.py:1466 #: ./pronterface.py:1190
msgid "Select the file to print" msgid "Select the file to print"
msgstr "" msgstr ""
#: pronterface.py:1501 #: ./pronterface.py:1222
msgid "Failed to execute slicing software: " msgid "Failed to execute slicing software: "
msgstr "" msgstr ""
#: pronterface.py:1510 #: ./pronterface.py:1229
msgid "Slicing..." msgid "Slicing..."
msgstr "" msgstr ""
#: pronterface.py:1523 #: ./pronterface.py:1241 ./pronterface.py:1300
msgid ", %d lines" msgid "Loaded %s, %d lines"
msgstr ""
#: pronterface.py:1523
msgid "Loaded "
msgstr "" msgstr ""
#: pronterface.py:1530 #: ./pronterface.py:1248
msgid "Load File" msgid "Load File"
msgstr "" msgstr ""
#: pronterface.py:1536 #: ./pronterface.py:1254
msgid "Slicing " msgid "Slicing "
msgstr "" msgstr ""
#: pronterface.py:1555 #: ./pronterface.py:1279
msgid "Open file to print" msgid "Open file to print"
msgstr "" msgstr ""
#: pronterface.py:1556 #: ./pronterface.py:1280
msgid "OBJ, STL, and GCODE files (*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ)|*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ|All Files (*.*)|*.*" msgid "OBJ, STL, and GCODE files (*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ)|*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ|All Files (*.*)|*.*"
msgstr "" msgstr ""
#: pronterface.py:1563 #: ./pronterface.py:1311
msgid "File not found!" msgid "mm of filament used in this print"
msgstr "" msgstr ""
#: pronterface.py:1577 #: ./pronterface.py:1312
msgid "Loaded %s, %d lines" msgid "The print goes:"
msgstr "" msgstr ""
#: pronterface.py:1588 #: ./pronterface.py:1313
msgid "" msgid "- from %.2f mm to %.2f mm in X and is %.2f mm wide"
"mm of filament used in this print\n"
msgstr "" msgstr ""
#: pronterface.py:1589 pronterface.py:1591 #: ./pronterface.py:1314
msgid "" msgid "- from %.2f mm to %.2f mm in Y and is %.2f mm deep"
"the print goes from %f mm to %f mm in X\n"
"and is %f mm wide\n"
msgstr ""
#: pronterface.py:1592
msgid ""
"the print goes from %f mm to %f mm in Y\n"
"and is %f mm wide\n"
msgstr ""
#: pronterface.py:1593
msgid ""
"the print goes from %f mm to %f mm in Z\n"
"and is %f mm high\n"
msgstr "" msgstr ""
#: pronterface.py:1595 #: ./pronterface.py:1315
msgid "Estimated duration (pessimistic): " msgid "- from %.2f mm to %.2f mm in Z and is %.2f mm high"
msgstr "" msgstr ""
#: pronterface.py:1622 #: ./pronterface.py:1316
msgid "No file loaded. Please use load first." msgid "Estimated duration: %s"
msgstr "" msgstr ""
#: pronterface.py:1633 #: ./pronterface.py:1347
msgid "Restart" msgid "Restart"
msgstr "" msgstr ""
#: pronterface.py:1637 #: ./pronterface.py:1351
msgid "File upload complete" msgid "File upload complete"
msgstr "" msgstr ""
#: pronterface.py:1656 #: ./pronterface.py:1370
msgid "Pick SD filename" msgid "Pick SD filename"
msgstr "" msgstr ""
#: pronterface.py:1663 #: ./pronterface.py:1377
msgid "Paused." msgid "Paused."
msgstr "" msgstr ""
#: pronterface.py:1674 #: ./pronterface.py:1390
msgid "Resume" msgid "Resume"
msgstr "" msgstr ""
#: pronterface.py:1688 #: ./pronterface.py:1404
msgid "Connecting..." msgid "Connecting..."
msgstr "" msgstr ""
#: pronterface.py:1738 #: ./pronterface.py:1430
msgid "Error: You are trying to connect to a non-exisiting port."
msgstr ""
#: ./pronterface.py:1432
msgid "Error: You don't have permission to open %s."
msgstr ""
#: ./pronterface.py:1433
msgid "You might need to add yourself to the dialout group."
msgstr ""
#: ./pronterface.py:1466
msgid "Disconnected." msgid "Disconnected."
msgstr "" msgstr ""
#: pronterface.py:1771 #: ./pronterface.py:1499
msgid "Reset." msgid "Reset."
msgstr "" msgstr ""
#: pronterface.py:1772 #: ./pronterface.py:1500
msgid "Are you sure you want to reset the printer?" msgid "Are you sure you want to reset the printer?"
msgstr "" msgstr ""
#: pronterface.py:1772 #: ./pronterface.py:1500
msgid "Reset?" msgid "Reset?"
msgstr "" msgstr ""
...@@ -244,6 +244,9 @@ class showstl(wx.Window): ...@@ -244,6 +244,9 @@ class showstl(wx.Window):
class stlwin(wx.Frame): class stlwin(wx.Frame):
def __init__(self, size = (800, 580), callback = None, parent = None): def __init__(self, size = (800, 580), callback = None, parent = None):
wx.Frame.__init__(self, parent, title = _("Plate building tool"), size = size) wx.Frame.__init__(self, parent, title = _("Plate building tool"), size = size)
if hasattr(sys,"frozen") and sys.frozen=="windows_exe":
self.SetIcon(wx.Icon(sys.executable, wx.BITMAP_TYPE_ICO))
else:
self.SetIcon(wx.Icon(pixmapfile("plater.ico"), wx.BITMAP_TYPE_ICO)) self.SetIcon(wx.Icon(pixmapfile("plater.ico"), wx.BITMAP_TYPE_ICO))
self.mainsizer = wx.BoxSizer(wx.HORIZONTAL) self.mainsizer = wx.BoxSizer(wx.HORIZONTAL)
self.panel = wx.Panel(self, -1, size = (150, 600), pos = (0, 0)) self.panel = wx.Panel(self, -1, size = (150, 600), pos = (0, 0))
......
...@@ -16,11 +16,15 @@ ...@@ -16,11 +16,15 @@
# along with Printrun. If not, see <http://www.gnu.org/licenses/>. # along with Printrun. If not, see <http://www.gnu.org/licenses/>.
from serial import Serial, SerialException from serial import Serial, SerialException
from threading import Thread
from select import error as SelectError from select import error as SelectError
from threading import Thread
import time, getopt, sys import time, getopt, sys
import platform, os import platform, os, traceback
from GCodeAnalyzer import GCodeAnalyzer import socket
import re
from collections import deque
from printrun.GCodeAnalyzer import GCodeAnalyzer
from printrun import gcoder
def control_ttyhup(port, disable_hup): def control_ttyhup(port, disable_hup):
"""Controls the HUPCL""" """Controls the HUPCL"""
...@@ -42,22 +46,25 @@ class printcore(): ...@@ -42,22 +46,25 @@ class printcore():
""" """
self.baud = None self.baud = None
self.port = None self.port = None
self.analyzer = GCodeAnalyzer()
self.printer = None #Serial instance connected to the printer, None when disconnected self.printer = None #Serial instance connected to the printer, None when disconnected
self.clear = 0 #clear to send, enabled after responses self.clear = 0 #clear to send, enabled after responses
self.online = False #The printer has responded to the initial command and is active self.online = False #The printer has responded to the initial command and is active
self.printing = False #is a print currently running, true if printing, false if paused self.printing = False #is a print currently running, true if printing, false if paused
self.mainqueue = [] self.mainqueue = None
self.priqueue = [] self.priqueue = []
self.queueindex = 0 self.queueindex = 0
self.lineno = 0 self.lineno = 0
self.resendfrom = -1 self.resendfrom = -1
self.paused = False self.paused = False
self.sentlines = {} self.sentlines = {}
self.log = [] self.log = deque(maxlen = 10000)
self.sent = [] self.sent = []
self.tempcb = None #impl (wholeline) self.tempcb = None #impl (wholeline)
self.recvcb = None #impl (wholeline) self.recvcb = None #impl (wholeline)
self.sendcb = None #impl (wholeline) self.sendcb = None #impl (wholeline)
self.printsendcb = None #impl (wholeline)
self.layerchangecb = None #impl (wholeline)
self.errorcb = None #impl (wholeline) self.errorcb = None #impl (wholeline)
self.startcb = None #impl () self.startcb = None #impl ()
self.endcb = None #impl () self.endcb = None #impl ()
...@@ -70,7 +77,6 @@ class printcore(): ...@@ -70,7 +77,6 @@ class printcore():
self.print_thread = None self.print_thread = None
if port is not None and baud is not None: if port is not None and baud is not None:
self.connect(port, baud) self.connect(port, baud)
self.analyzer = GCodeAnalyzer()
self.xy_feedrate = None self.xy_feedrate = None
self.z_feedrate = None self.z_feedrate = None
self.pronterface = None self.pronterface = None
...@@ -83,7 +89,10 @@ class printcore(): ...@@ -83,7 +89,10 @@ class printcore():
self.stop_read_thread = True self.stop_read_thread = True
self.read_thread.join() self.read_thread.join()
self.read_thread = None self.read_thread = None
try:
self.printer.close() self.printer.close()
except socket.error:
pass
self.printer = None self.printer = None
self.online = False self.online = False
self.printing = False self.printing = False
...@@ -98,8 +107,41 @@ class printcore(): ...@@ -98,8 +107,41 @@ class printcore():
if baud is not None: if baud is not None:
self.baud = baud self.baud = baud
if self.port is not None and self.baud is not None: if self.port is not None and self.baud is not None:
# 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 len(bits) == 2:
hostname = bits[0]
try:
port = int(bits[1])
if host_regexp.match(hostname) and 1 <= port <= 65535:
is_serial = False
except:
pass
if not is_serial:
self.printer_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.timeout = 0.25
try:
self.printer_tcp.connect((hostname, port))
self.printer = self.printer_tcp.makefile()
except socket.error:
print _("Could not connect to %s:%s:") % (hostname, port)
self.printer = None
self.printer_tcp = None
traceback.print_exc()
return
else:
disable_hup(self.port) disable_hup(self.port)
self.printer_tcp = None
try:
self.printer = Serial(port = self.port, baudrate = self.baud, timeout = 0.25) self.printer = Serial(port = self.port, baudrate = self.baud, timeout = 0.25)
except SerialException:
print _("Could not connect to %s at baudrate %s:") % (self.port, self.baud)
self.printer = None
traceback.print_exc()
return
self.stop_read_thread = False self.stop_read_thread = False
self.read_thread = Thread(target = self._listen) self.read_thread = Thread(target = self._listen)
self.read_thread.start() self.read_thread.start()
...@@ -107,14 +149,20 @@ class printcore(): ...@@ -107,14 +149,20 @@ class printcore():
def reset(self): def reset(self):
"""Reset the printer """Reset the printer
""" """
if self.printer: if self.printer and not self.printer_tcp:
self.printer.setDTR(1) self.printer.setDTR(1)
time.sleep(0.2) time.sleep(0.2)
self.printer.setDTR(0) self.printer.setDTR(0)
def _readline(self): def _readline(self):
try:
try: try:
line = self.printer.readline() line = self.printer.readline()
if self.printer_tcp and not line:
raise OSError(-1, "Read EOF from socket")
except socket.timeout:
return ""
if len(line) > 1: if len(line) > 1:
self.log.append(line) self.log.append(line)
if self.recvcb: if self.recvcb:
...@@ -122,20 +170,26 @@ class printcore(): ...@@ -122,20 +170,26 @@ class printcore():
except: pass except: pass
if self.loud: print "RECV: ", line.rstrip() if self.loud: print "RECV: ", line.rstrip()
return line return line
except SelectError, e: except SelectError as e:
if 'Bad file descriptor' in e.args[1]: if 'Bad file descriptor' in e.args[1]:
print "Can't read from printer (disconnected?)." print "Can't read from printer (disconnected?) (SelectError {0}): {1}".format(e.errno, e.strerror)
return None return None
else: else:
print "SelectError ({0}): {1}".format(e.errno, e.strerror)
raise raise
except SerialException, e: except SerialException as e:
print "Can't read from printer (disconnected?)." print "Can't read from printer (disconnected?) (SerialException {0}): {1}".format(e.errno, e.strerror)
return None
except socket.error as e:
print "Can't read from printer (disconnected?) (Socket error {0}): {1}".format(e.errno, e.strerror)
return None return None
except OSError, e: except OSError as e:
print "Can't read from printer (disconnected?)." print "Can't read from printer (disconnected?) (OS Error {0}): {1}".format(e.errno, e.strerror)
return None return None
def _listen_can_continue(self): def _listen_can_continue(self):
if self.printer_tcp:
return not self.stop_read_thread and self.printer
return not self.stop_read_thread and self.printer and self.printer.isOpen() return not self.stop_read_thread and self.printer and self.printer.isOpen()
def _listen_until_online(self): def _listen_until_online(self):
...@@ -203,8 +257,8 @@ class printcore(): ...@@ -203,8 +257,8 @@ class printcore():
def _checksum(self, command): def _checksum(self, command):
return reduce(lambda x, y:x^y, map(ord, command)) return reduce(lambda x, y:x^y, map(ord, command))
def startprint(self, data, startindex = 0): def startprint(self, gcode, startindex = 0):
"""Start a print, data is an array of gcode commands. """Start a print, gcode is an array of gcode commands.
returns True on success, False if already printing. returns True on success, False if already printing.
The print queue will be replaced with the contents of the data array, the next line will be set to 0 and the firmware notified. The print queue will be replaced with the contents of the data array, the next line will be set to 0 and the firmware notified.
Printing will then start in a parallel thread. Printing will then start in a parallel thread.
...@@ -212,12 +266,12 @@ class printcore(): ...@@ -212,12 +266,12 @@ class printcore():
if self.printing or not self.online or not self.printer: if self.printing or not self.online or not self.printer:
return False return False
self.printing = True self.printing = True
self.mainqueue = [] + data self.mainqueue = gcode
self.lineno = 0 self.lineno = 0
self.queueindex = startindex self.queueindex = startindex
self.resendfrom = -1 self.resendfrom = -1
self._send("M110", -1, True) self._send("M110", -1, True)
if len(data) == 0: if not gcode.lines:
return True return True
self.clear = False self.clear = False
self.print_thread = Thread(target = self._print) self.print_thread = Thread(target = self._print)
...@@ -317,7 +371,7 @@ class printcore(): ...@@ -317,7 +371,7 @@ class printcore():
def send_now(self, command, wait = 0): def send_now(self, command, wait = 0):
"""Sends a command to the printer ahead of the command queue, without a checksum """Sends a command to the printer ahead of the command queue, without a checksum
""" """
if self.online or force: if self.online:
if self.printing: if self.printing:
self.priqueue.append(command) self.priqueue.append(command)
else: else:
...@@ -342,7 +396,7 @@ class printcore(): ...@@ -342,7 +396,7 @@ class printcore():
while self.printing and self.printer and self.online: while self.printing and self.printer and self.online:
self._sendnext() self._sendnext()
self.sentlines = {} self.sentlines = {}
self.log = [] self.log.clear()
self.sent = [] self.sent = []
try: try:
self.print_thread.join() self.print_thread.join()
...@@ -376,23 +430,30 @@ class printcore(): ...@@ -376,23 +430,30 @@ class printcore():
self.resendfrom += 1 self.resendfrom += 1
return return
self.resendfrom = -1 self.resendfrom = -1
for i in self.priqueue[:]: if self.priqueue:
self._send(i) self._send(self.priqueue.pop(0))
del self.priqueue[0]
return return
if self.printing and self.queueindex < len(self.mainqueue): if self.printing and self.queueindex < len(self.mainqueue):
tline = self.mainqueue[self.queueindex] (layer, line) = self.mainqueue.idxs(self.queueindex)
gline = self.mainqueue.all_layers[layer].lines[line]
if self.layerchangecb and self.queueindex > 0:
(prev_layer, prev_line) = self.mainqueue.idxs(self.queueindex - 1)
if prev_layer != layer:
self.layerchangecb(layer)
tline = gline.raw
#check for host command #check for host command
if tline.lstrip().startswith(";@"): if tline.lstrip().startswith(";@"):
#it is a host command: pop it from the list
self.mainqueue.pop(self.queueindex)
self.processHostCommand(tline) self.processHostCommand(tline)
self.queueindex += 1
return return
tline = tline.split(";")[0] tline = tline.split(";")[0]
if len(tline) > 0: if len(tline) > 0:
self._send(tline, self.lineno, True) self._send(tline, self.lineno, True)
self.lineno += 1 self.lineno += 1
if self.printsendcb:
try: self.printsendcb(gline)
except: pass
else: else:
self.clear = True self.clear = True
self.queueindex += 1 self.queueindex += 1
...@@ -419,9 +480,14 @@ class printcore(): ...@@ -419,9 +480,14 @@ class printcore():
try: self.sendcb(command) try: self.sendcb(command)
except: pass except: pass
try: try:
self.printer.write(str(command+"\n")) self.printer.write(str(command + "\n"))
except SerialException, e: self.printer.flush()
print "Can't write to printer (disconnected?)." except socket.error as e:
print "Can't write to printer (disconnected?) (Socket error {0}): {1}".format(e.errno, e.strerror)
except SerialException as e:
print "Can't write to printer (disconnected?) (SerialException {0}): {1}".format(e.errno, e.strerror)
except RuntimeError as e:
print "Socket connection broken, disconnected. ({0}): {1}".format(e.errno, e.strerror)
if __name__ == '__main__': if __name__ == '__main__':
baud = 115200 baud = 115200
...@@ -455,18 +521,19 @@ if __name__ == '__main__': ...@@ -455,18 +521,19 @@ if __name__ == '__main__':
p = printcore(port, baud) p = printcore(port, baud)
p.loud = loud p.loud = loud
time.sleep(2) time.sleep(2)
gcode = [i.replace("\n", "") for i in open(filename)] gcode = [i.strip() for i in open(filename)]
gcode = gcoder.GCode(gcode)
p.startprint(gcode) p.startprint(gcode)
try: try:
if statusreport: if statusreport:
p.loud = False p.loud = False
sys.stdout.write("Progress: 00.0%") sys.stdout.write("Progress: 00.0%\r")
sys.stdout.flush() sys.stdout.flush()
while p.printing: while p.printing:
time.sleep(1) time.sleep(1)
if statusreport: if statusreport:
sys.stdout.write("%02.1f%%\r" % (100 * float(p.queueindex) / len(p.mainqueue),) ) sys.stdout.write("Progress: %02.1f%%\r" % (100 * float(p.queueindex) / len(p.mainqueue),) )
sys.stdout.flush() sys.stdout.flush()
p.disconnect() p.disconnect()
sys.exit(0) sys.exit(0)
......
# This file is part of the Printrun suite.
#
# Copyright 2013 Francesco Santini francesco.santini@gmail.com
#
# 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 <http://www.gnu.org/licenses/>.
#
# This code is imported from RepetierHost - Original copyright and license:
# Copyright 2011 repetier repetierdev@gmail.com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
import gcoder
class GCodeAnalyzer():
def __init__(self):
self.x = 0
self.y = 0
self.z = 0
self.e = 0
self.emax = 0
self.f = 1000
self.lastX = 0
self.lastY = 0
self.lastZ = 0
self.lastE = 0
self.xOffset = 0
self.yOffset = 0
self.zOffset = 0
self.eOffset = 0
self.lastZPrint = 0
self.layerZ = 0
self.relative = False
self.eRelative = False
self.homeX = 0
self.homeY = 0
self.homeZ = 0
self.maxX = 150
self.maxY = 150
self.maxZ = 150
self.minX = 0
self.minY = 0
self.minZ = 0
self.hasHomeX = False
self.hasHomeY = False
self.hasHomeZ = False
def Analyze(self, gcode):
gline = gcoder.Line(gcode)
if gline.command.startswith(";@"): return # code is a host command
code_g = int(gline.command[1:]) if gline.command.startswith("G") else None
code_m = int(gline.command[1:]) if gline.command.startswith("M") else None
#get movement codes
if gline.is_move:
self.lastX = self.x
self.lastY = self.y
self.lastZ = self.z
self.lastE = self.e
eChanged = False;
code_f = gline.f
if code_f != None:
self.f = code_f
code_x = gline.x
code_y = gline.y
code_z = gline.z
code_e = gline.e
if self.relative:
if code_x != None: self.x += code_x
if code_y != None: self.y += code_y
if code_z != None: self.z += code_z
if code_e != None:
if code_e != 0:
eChanged = True
self.e += code_e
else:
#absolute coordinates
if code_x != None: self.x = self.xOffset + code_x
if code_y != None: self.y = self.yOffset + code_y
if code_z != None: self.z = self.zOffset + code_z
if code_e != None:
if self.eRelative:
if code_e != 0:
eChanged = True
self.e += code_e
else:
# e is absolute. Is it changed?
if self.e != self.eOffset + code_e:
eChanged = True
self.e = self.eOffset + code_e
#limit checking
if self.x < self.minX: self.x = self.minX
if self.y < self.minY: self.y = self.minY
if self.z < self.minZ: self.z = self.minZ
if self.x > self.maxX: self.x = self.maxX
if self.y > self.maxY: self.y = self.maxY
if self.z > self.maxZ: self.z = self.maxZ
#Repetier has a bunch of limit-checking code here and time calculations: we are leaving them for now
elif code_g == 28 or code_g == 161:
self.lastX = self.x
self.lastY = self.y
self.lastZ = self.z
self.lastE = self.e
code_x = gline.x
code_y = gline.y
code_z = gline.z
code_e = gline.e
homeAll = False
if code_x == None and code_y == None and code_z == None: homeAll = True
if code_x != None or homeAll:
self.hasHomeX = True
self.xOffset = 0
self.x = self.homeX
if code_y != None or homeAll:
self.hasHomeY = True
self.yOffset = 0
self.y = self.homeY
if code_z != None or homeAll:
self.hasHomeZ = True
self.zOffset = 0
self.z = self.homeZ
if code_e != None:
self.eOffset = 0
self.e = 0
elif code_g == 162:
self.lastX = self.x
self.lastY = self.y
self.lastZ = self.z
self.lastE = self.e
code_x = gline.x
code_y = gline.y
code_z = gline.z
homeAll = False
if code_x == None and code_y == None and code_z == None: homeAll = True
if code_x != None or homeAll:
self.hasHomeX = True
self.xOffset = 0
self.x = self.maxX
if code_y != None or homeAll:
self.hasHomeY = True
self.yOffset = 0
self.y = self.maxY
if code_z != None or homeAll:
self.hasHomeZ = True
self.zOffset = 0
self.z = self.maxZ
elif code_g == 90: self.relative = False
elif code_g == 91: self.relative = True
elif code_g == 92:
code_x = gline.x
code_y = gline.y
code_z = gline.z
code_e = gline.e
if code_x != None:
self.xOffset = self.x - float(code_x)
self.x = self.xOffset
if code_y != None:
self.yOffset = self.y - float(code_y)
self.y = self.yOffset
if code_z != None:
self.zOffset = self.z - float(code_z)
self.z = self.zOffset
if code_e != None:
self.xOffset = self.e - float(code_e)
self.e = self.eOffset
#End code_g != None
if code_m != None:
code_m = int(code_m)
if code_m == 82: self.eRelative = False
elif code_m == 83: self.eRelative = True
def print_status(self):
attrs = vars(self)
print '\n'.join("%s: %s" % item for item in attrs.items())
...@@ -77,7 +77,7 @@ class BufferedCanvas(wx.Panel): ...@@ -77,7 +77,7 @@ class BufferedCanvas(wx.Panel):
## General methods ## General methods
## ##
def draw(self, dc): def draw(self, dc, w, h):
""" """
Stub: called when the canvas needs to be re-drawn. Stub: called when the canvas needs to be re-drawn.
""" """
......
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
CairoSVG - A simple SVG converter for Cairo.
"""
import os
import sys
import optparse
from . import surface
VERSION = '0.4.4'
SURFACES = {
'SVG': surface.SVGSurface, # Tell us if you actually use this one!
'PNG': surface.PNGSurface,
'PDF': surface.PDFSurface,
'PS': surface.PSSurface}
# Generate the svg2* functions from SURFACES
for _output_format, _surface_type in SURFACES.items():
_function = (
# Two lambdas needed for the closure
lambda surface_type: lambda *args, **kwargs: # pylint: disable=W0108
surface_type.convert(*args, **kwargs))(_surface_type)
_name = 'svg2%s' % _output_format.lower()
_function.__name__ = _name
_function.__doc__ = surface.Surface.convert.__doc__.replace(
'the format for this class', _output_format)
setattr(sys.modules[__name__], _name, _function)
def main():
"""Entry-point of the executable."""
# Get command-line options
option_parser = optparse.OptionParser(
usage = "usage: %prog filename [options]", version = VERSION)
option_parser.add_option(
"-f", "--format", help = "output format")
option_parser.add_option(
"-d", "--dpi", help = "svg resolution", default = 96)
option_parser.add_option(
"-o", "--output",
default = "", help = "output filename")
options, args = option_parser.parse_args()
# Print help if no argument is given
if not args:
option_parser.print_help()
sys.exit()
kwargs = {'dpi': float(options.dpi)}
if not options.output or options.output == '-':
# Python 2/3 hack
bytes_stdout = getattr(sys.stdout, "buffer", sys.stdout)
kwargs['write_to'] = bytes_stdout
else:
kwargs['write_to'] = options.output
url = args[0]
if url == "-":
# Python 2/3 hack
bytes_stdin = getattr(sys.stdin, "buffer", sys.stdin)
kwargs['file_obj'] = bytes_stdin
else:
kwargs['url'] = url
output_format = (
options.format or
os.path.splitext(options.output)[1].lstrip(".") or
"pdf")
SURFACES[output_format.upper()].convert(**kwargs)
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Optionally handle CSS stylesheets.
"""
from .parser import HAS_LXML
# Detect optional depedencies
# pylint: disable=W0611
try:
import tinycss
import cssselect
CSS_CAPABLE = HAS_LXML
except ImportError:
CSS_CAPABLE = False
# pylint: enable=W0611
# Python 2/3 compat
iteritems = getattr(dict, "iteritems", dict.items) # pylint: disable=C0103
def find_stylesheets(tree):
"""Find the stylesheets included in ``tree``."""
# TODO: support contentStyleType on <svg>
default_type = "text/css"
for element in tree.iter():
# http://www.w3.org/TR/SVG/styling.html#StyleElement
if (element.tag == "style" and
element.get("type", default_type) == "text/css"):
# TODO: pass href for relative URLs
# TODO: support media types
# TODO: what if <style> has children elements?
yield tinycss.make_parser().parse_stylesheet(element.text)
# TODO: support <?xml-stylesheet ... ?>
def find_style_rules(tree):
"""Find the style rules in ``tree``."""
for stylesheet in find_stylesheets(tree):
# TODO: warn for each stylesheet.errors
for rule in stylesheet.rules:
# TODO: support @import and @media
if not rule.at_keyword:
yield rule
def get_declarations(rule):
"""Get the declarations in ``rule``."""
for declaration in rule.declarations:
if declaration.name.startswith("-"):
# Ignore properties prefixed by "-"
continue
# TODO: filter out invalid values
yield (
declaration.name,
declaration.value.as_css(),
bool(declaration.priority))
def match_selector(rule, tree):
"""Yield the ``(element, specificity)`` in ``tree`` matching ``rule``."""
selector_list = cssselect.parse(rule.selector.as_css())
translator = cssselect.GenericTranslator()
for selector in selector_list:
if not selector.pseudo_element:
specificity = selector.specificity()
for element in tree.xpath(translator.selector_to_xpath(selector)):
yield element, specificity
def apply_stylesheets(tree):
"""Apply the stylesheet in ``tree`` to ``tree``."""
if not CSS_CAPABLE:
# TODO: warn?
return
style_by_element = {}
for rule in find_style_rules(tree):
declarations = list(get_declarations(rule))
for element, specificity in match_selector(rule, tree):
style = style_by_element.setdefault(element, {})
for name, value, important in declarations:
weight = important, specificity
if name in style:
_old_value, old_weight = style[name]
if old_weight > weight:
continue
style[name] = value, weight
for element, style in iteritems(style_by_element):
values = ["%s: %s" % (name, value)
for name, (value, weight) in iteritems(style)]
values.append(element.get("style", ""))
element.set("style", ";".join(values))
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
SVG Parser.
"""
# Fallbacks for Python 2/3 and lxml/ElementTree
# pylint: disable=E0611,F0401,W0611
try:
import lxml.etree as ElementTree
from lxml.etree import XMLSyntaxError as ParseError
HAS_LXML = True
except ImportError:
from xml.etree import ElementTree
from xml.parsers import expat
# ElementTree's API changed between 2.6 and 2.7
# pylint: disable=C0103
ParseError = getattr(ElementTree, 'ParseError', expat.ExpatError)
# pylint: enable=C0103
HAS_LXML = False
try:
from urllib import urlopen
import urlparse
except ImportError:
from urllib.request import urlopen
from urllib import parse as urlparse # Python 3
# pylint: enable=E0611,F0401,W0611
import gzip
import os.path
from .css import apply_stylesheets
# Python 2/3 compat
# pylint: disable=C0103,W0622
try:
basestring
except NameError:
basestring = str
# pylint: enable=C0103,W0622
def remove_svg_namespace(tree):
"""Remove the SVG namespace from ``tree`` tags.
``lxml.cssselect`` does not support empty/default namespaces, so remove any
SVG namespace.
"""
prefix = "{http://www.w3.org/2000/svg}"
prefix_len = len(prefix)
iterator = (
tree.iter() if hasattr(tree, 'iter')
else tree.getiterator())
for element in iterator:
tag = element.tag
if hasattr(tag, "startswith") and tag.startswith(prefix):
element.tag = tag[prefix_len:]
class Node(dict):
"""SVG node with dict-like properties and children."""
def __init__(self, node, parent = None):
"""Create the Node from ElementTree ``node``, with ``parent`` Node."""
super(Node, self).__init__()
self.children = ()
self.root = False
self.tag = node.tag
self.text = node.text
# Inherits from parent properties
# TODO: drop other attributes that should not be inherited
if parent is not None:
items = parent.copy()
not_inherited = (
"transform", "opacity", "style", "viewBox", "stop-color",
"stop-opacity")
if self.tag in ("tspan", "pattern"):
not_inherited += ("x", "y")
for attribute in not_inherited:
if attribute in items:
del items[attribute]
self.update(items)
self.url = parent.url
self.xml_tree = parent.xml_tree
self.parent = parent
self.update(dict(node.attrib.items()))
# Handle the CSS
style = self.pop("style", "")
for declaration in style.split(";"):
if ":" in declaration:
name, value = declaration.split(":", 1)
self[name.strip()] = value.strip()
# Replace currentColor by a real color value
color_attributes = (
"fill", "stroke", "stop-color", "flood-color",
"lighting-color")
for attribute in color_attributes:
if self.get(attribute) == "currentColor":
self[attribute] = self.get("color", "black")
# Replace inherit by the parent value
for attribute, value in dict(self).items():
if value == "inherit":
if parent is not None and attribute in parent:
self[attribute] = parent.get(attribute)
else:
del self[attribute]
# Manage text by creating children
if self.tag == "text" or self.tag == "textPath":
self.children = self.text_children(node)
if not self.children:
self.children = tuple(
Node(child, self) for child in node
if isinstance(child.tag, basestring))
def text_children(self, node):
"""Create children and return them."""
children = []
for child in node:
children.append(Node(child, parent = self))
if child.tail:
anonymous = ElementTree.Element('tspan')
anonymous.text = child.tail
children.append(Node(anonymous, parent = self))
return list(children)
class Tree(Node):
"""SVG tree."""
def __init__(self, **kwargs):
"""Create the Tree from SVG ``text``."""
# Make the parameters keyword-only:
bytestring = kwargs.pop('bytestring', None)
file_obj = kwargs.pop('file_obj', None)
url = kwargs.pop('url', None)
parent = kwargs.pop('parent', None)
if bytestring is not None:
tree = ElementTree.fromstring(bytestring)
self.url = url
elif file_obj is not None:
tree = ElementTree.parse(file_obj).getroot()
if url:
self.url = url
else:
self.url = getattr(file_obj, 'name', None)
elif url is not None:
if "#" in url:
url, element_id = url.split("#", 1)
else:
element_id = None
if parent and parent.url:
if url:
url = urlparse.urljoin(parent.url, url)
elif element_id:
url = parent.url
self.url = url
if url:
if urlparse.urlparse(url).scheme:
input_ = urlopen(url)
else:
input_ = url # filename
if os.path.splitext(url)[1].lower() == "svgz":
input_ = gzip.open(url)
tree = ElementTree.parse(input_).getroot()
else:
tree = parent.xml_tree
if element_id:
iterator = (
tree.iter() if hasattr(tree, 'iter')
else tree.getiterator())
for element in iterator:
if element.get("id") == element_id:
tree = element
break
else:
raise TypeError(
'No tag with id="%s" found.' % element_id)
else:
raise TypeError(
'No input. Use one of bytestring, file_obj or url.')
remove_svg_namespace(tree)
apply_stylesheets(tree)
self.xml_tree = tree
super(Tree, self).__init__(tree, parent)
self.root = True
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Cairo surface creators.
"""
import cairo
import io
from ..parser import Tree
from .colors import color
from .defs import gradient_or_pattern, parse_def
from .helpers import (
node_format, transform, normalize, filter_fill_or_stroke,
apply_matrix_transform, PointError)
from .path import PATH_TAGS
from .tags import TAGS
from .units import size
from . import units
class Surface(object):
"""Abstract base class for CairoSVG surfaces.
The ``width`` and ``height`` attributes are in device units (pixels for
PNG, else points).
The ``context_width`` and ``context_height`` attributes are in user units
(i.e. in pixels), they represent the size of the active viewport.
"""
# Subclasses must either define this or override _create_surface()
surface_class = None
@classmethod
def convert(cls, bytestring = None, **kwargs):
"""Convert a SVG document to the format for this class.
Specify the input by passing one of these:
:param bytestring: The SVG source as a byte-string.
:param file_obj: A file-like object.
:param url: A filename.
And the output with:
:param write_to: The filename of file-like object where to write the
output. If None or not provided, return a byte string.
Only ``source`` can be passed as a positional argument, other
parameters are keyword-only.
"""
dpi = kwargs.pop('dpi', 96)
write_to = kwargs.pop('write_to', None)
kwargs['bytestring'] = bytestring
tree = Tree(**kwargs)
if write_to is None:
output = io.BytesIO()
else:
output = write_to
cls(tree, output, dpi).finish()
if write_to is None:
return output.getvalue()
def __init__(self, tree, output, dpi):
"""Create the surface from a filename or a file-like object.
The rendered content is written to ``output`` which can be a filename,
a file-like object, ``None`` (render in memory but do not write
anything) or the built-in ``bytes`` as a marker.
Call the ``.finish()`` method to make sure that the output is
actually written.
"""
self.cairo = None
self.context_width, self.context_height = None, None
self.cursor_position = 0, 0
self.total_width = 0
self.markers = {}
self.gradients = {}
self.patterns = {}
self.paths = {}
self.page_sizes = []
self._old_parent_node = self.parent_node = None
self.output = output
self.dpi = dpi
self.font_size = size(self, "12pt")
width, height, viewbox = node_format(self, tree)
# Actual surface dimensions: may be rounded on raster surfaces types
self.cairo, self.width, self.height = self._create_surface(
width * self.device_units_per_user_units,
height * self.device_units_per_user_units)
self.page_sizes.append((self.width, self.height))
self.context = cairo.Context(self.cairo)
# We must scale the context as the surface size is using physical units
self.context.scale(
self.device_units_per_user_units, self.device_units_per_user_units)
# Initial, non-rounded dimensions
self.set_context_size(width, height, viewbox)
self.context.move_to(0, 0)
self.draw_root(tree)
@property
def points_per_pixel(self):
"""Surface resolution."""
return 1 / (self.dpi * units.UNITS["pt"])
@property
def device_units_per_user_units(self):
"""Ratio between Cairo device units and user units.
Device units are points for everything but PNG, and pixels for
PNG. User units are pixels.
"""
return self.points_per_pixel
def _create_surface(self, width, height):
"""Create and return ``(cairo_surface, width, height)``."""
# self.surface_class should not be None when called here
# pylint: disable=E1102
cairo_surface = self.surface_class(self.output, width, height)
# pylint: enable=E1102
return cairo_surface, width, height
def set_context_size(self, width, height, viewbox):
"""Set the Cairo context size, set the SVG viewport size."""
if viewbox:
x, y, x_size, y_size = viewbox
self.context_width, self.context_height = x_size, y_size
x_ratio, y_ratio = width / x_size, height / y_size
matrix = cairo.Matrix()
if x_ratio > y_ratio:
matrix.translate((width - x_size * y_ratio) / 2, 0)
matrix.scale(y_ratio, y_ratio)
matrix.translate(-x, -y / y_ratio * x_ratio)
elif x_ratio < y_ratio:
matrix.translate(0, (height - y_size * x_ratio) / 2)
matrix.scale(x_ratio, x_ratio)
matrix.translate(-x / x_ratio * y_ratio, -y)
else:
matrix.scale(x_ratio, y_ratio)
matrix.translate(-x, -y)
apply_matrix_transform(self, matrix)
else:
self.context_width, self.context_height = width, height
def finish(self):
"""Read the surface content."""
self.cairo.finish()
def draw_root(self, node):
"""Draw the root ``node``."""
self.draw(node)
def draw(self, node, stroke_and_fill = True):
"""Draw ``node`` and its children."""
old_font_size = self.font_size
self.font_size = size(self, node.get("font-size", "12pt"))
# Do not draw defs
if node.tag == "defs":
for child in node.children:
parse_def(self, child)
return
# Do not draw elements with width or height of 0
if (("width" in node and size(self, node["width"]) == 0) or
("height" in node and size(self, node["height"]) == 0)):
return
node.tangents = [None]
node.pending_markers = []
self._old_parent_node = self.parent_node
self.parent_node = node
opacity = float(node.get("opacity", 1))
if opacity < 1:
self.context.push_group()
self.context.save()
self.context.move_to(
size(self, node.get("x"), "x"),
size(self, node.get("y"), "y"))
# Transform the context according to the ``transform`` attribute
transform(self, node.get("transform"))
if node.tag in PATH_TAGS:
# Set 1 as default stroke-width
if not node.get("stroke-width"):
node["stroke-width"] = "1"
# Set node's drawing informations if the ``node.tag`` method exists
line_cap = node.get("stroke-linecap")
if line_cap == "square":
self.context.set_line_cap(cairo.LINE_CAP_SQUARE)
if line_cap == "round":
self.context.set_line_cap(cairo.LINE_CAP_ROUND)
join_cap = node.get("stroke-linejoin")
if join_cap == "round":
self.context.set_line_join(cairo.LINE_JOIN_ROUND)
if join_cap == "bevel":
self.context.set_line_join(cairo.LINE_JOIN_BEVEL)
dash_array = normalize(node.get("stroke-dasharray", "")).split()
if dash_array:
dashes = [size(self, dash) for dash in dash_array]
if sum(dashes):
offset = size(self, node.get("stroke-dashoffset"))
self.context.set_dash(dashes, offset)
miter_limit = float(node.get("stroke-miterlimit", 4))
self.context.set_miter_limit(miter_limit)
if node.tag in TAGS:
try:
TAGS[node.tag](self, node)
except PointError:
# Error in point parsing, do nothing
pass
# Get stroke and fill opacity
stroke_opacity = float(node.get("stroke-opacity", 1))
fill_opacity = float(node.get("fill-opacity", 1))
# Manage dispaly and visibility
display = node.get("display", "inline") != "none"
visible = display and (node.get("visibility", "visible") != "hidden")
if stroke_and_fill and visible:
# Fill
if "url(#" in (node.get("fill") or ""):
name = filter_fill_or_stroke(node.get("fill"))
gradient_or_pattern(self, node, name)
else:
if node.get("fill-rule") == "evenodd":
self.context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
self.context.set_source_rgba(
*color(node.get("fill", "black"), fill_opacity))
self.context.fill_preserve()
# Stroke
self.context.set_line_width(size(self, node.get("stroke-width")))
if "url(#" in (node.get("stroke") or ""):
name = filter_fill_or_stroke(node.get("stroke"))
gradient_or_pattern(self, node, name)
else:
self.context.set_source_rgba(
*color(node.get("stroke"), stroke_opacity))
self.context.stroke()
elif not visible:
self.context.new_path()
# Draw children
if display and node.tag not in (
"linearGradient", "radialGradient", "marker", "pattern"):
for child in node.children:
self.draw(child, stroke_and_fill)
if not node.root:
# Restoring context is useless if we are in the root tag, it may
# raise an exception if we have multiple svg tags
self.context.restore()
if opacity < 1:
self.context.pop_group_to_source()
self.context.paint_with_alpha(opacity)
self.parent_node = self._old_parent_node
self.font_size = old_font_size
class MultipageSurface(Surface):
"""Abstract base class for surfaces that can handle multiple pages."""
def draw_root(self, node):
self.width = None
self.height = None
svg_children = [child for child in node.children if child.tag == 'svg']
if svg_children:
# Multi-page
for page in svg_children:
width, height, viewbox = node_format(self, page)
self.context.save()
self.set_context_size(width, height, viewbox)
width *= self.device_units_per_user_units
height *= self.device_units_per_user_units
self.page_sizes.append((width, height))
self.cairo.set_size(width, height)
self.draw(page)
self.context.restore()
self.cairo.show_page()
else:
self.draw(node)
class PDFSurface(MultipageSurface):
"""A surface that writes in PDF format."""
surface_class = cairo.PDFSurface
class PSSurface(MultipageSurface):
"""A surface that writes in PostScript format."""
surface_class = cairo.PSSurface
class PNGSurface(Surface):
"""A surface that writes in PNG format."""
device_units_per_user_units = 1
def _create_surface(self, width, height):
"""Create and return ``(cairo_surface, width, height)``."""
width = int(width)
height = int(height)
cairo_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
return cairo_surface, width, height
def finish(self):
"""Read the PNG surface content."""
if self.output is not None:
self.cairo.write_to_png(self.output)
return super(PNGSurface, self).finish()
class SVGSurface(Surface):
"""A surface that writes in SVG format.
It may seem pointless to render SVG to SVG, but this can be used
with ``output=None`` to get a vector-based single page cairo surface.
"""
surface_class = cairo.SVGSurface
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
SVG colors.
"""
COLORS = {
"aliceblue": "rgb(240, 248, 255)",
"antiquewhite": "rgb(250, 235, 215)",
"aqua": "rgb(0, 255, 255)",
"aquamarine": "rgb(127, 255, 212)",
"azure": "rgb(240, 255, 255)",
"beige": "rgb(245, 245, 220)",
"bisque": "rgb(255, 228, 196)",
"black": "rgb(0, 0, 0)",
"blanchedalmond": "rgb(255, 235, 205)",
"blue": "rgb(0, 0, 255)",
"blueviolet": "rgb(138, 43, 226)",
"brown": "rgb(165, 42, 42)",
"burlywood": "rgb(222, 184, 135)",
"cadetblue": "rgb(95, 158, 160)",
"chartreuse": "rgb(127, 255, 0)",
"chocolate": "rgb(210, 105, 30)",
"coral": "rgb(255, 127, 80)",
"cornflowerblue": "rgb(100, 149, 237)",
"cornsilk": "rgb(255, 248, 220)",
"crimson": "rgb(220, 20, 60)",
"cyan": "rgb(0, 255, 255)",
"darkblue": "rgb(0, 0, 139)",
"darkcyan": "rgb(0, 139, 139)",
"darkgoldenrod": "rgb(184, 134, 11)",
"darkgray": "rgb(169, 169, 169)",
"darkgreen": "rgb(0, 100, 0)",
"darkgrey": "rgb(169, 169, 169)",
"darkkhaki": "rgb(189, 183, 107)",
"darkmagenta": "rgb(139, 0, 139)",
"darkolivegreen": "rgb(85, 107, 47)",
"darkorange": "rgb(255, 140, 0)",
"darkorchid": "rgb(153, 50, 204)",
"darkred": "rgb(139, 0, 0)",
"darksalmon": "rgb(233, 150, 122)",
"darkseagreen": "rgb(143, 188, 143)",
"darkslateblue": "rgb(72, 61, 139)",
"darkslategray": "rgb(47, 79, 79)",
"darkslategrey": "rgb(47, 79, 79)",
"darkturquoise": "rgb(0, 206, 209)",
"darkviolet": "rgb(148, 0, 211)",
"deeppink": "rgb(255, 20, 147)",
"deepskyblue": "rgb(0, 191, 255)",
"dimgray": "rgb(105, 105, 105)",
"dimgrey": "rgb(105, 105, 105)",
"dodgerblue": "rgb(30, 144, 255)",
"firebrick": "rgb(178, 34, 34)",
"floralwhite": "rgb(255, 250, 240)",
"forestgreen": "rgb(34, 139, 34)",
"fuchsia": "rgb(255, 0, 255)",
"gainsboro": "rgb(220, 220, 220)",
"ghostwhite": "rgb(248, 248, 255)",
"gold": "rgb(255, 215, 0)",
"goldenrod": "rgb(218, 165, 32)",
"gray": "rgb(128, 128, 128)",
"grey": "rgb(128, 128, 128)",
"green": "rgb(0, 128, 0)",
"greenyellow": "rgb(173, 255, 47)",
"honeydew": "rgb(240, 255, 240)",
"hotpink": "rgb(255, 105, 180)",
"indianred": "rgb(205, 92, 92)",
"indigo": "rgb(75, 0, 130)",
"ivory": "rgb(255, 255, 240)",
"khaki": "rgb(240, 230, 140)",
"lavender": "rgb(230, 230, 250)",
"lavenderblush": "rgb(255, 240, 245)",
"lawngreen": "rgb(124, 252, 0)",
"lemonchiffon": "rgb(255, 250, 205)",
"lightblue": "rgb(173, 216, 230)",
"lightcoral": "rgb(240, 128, 128)",
"lightcyan": "rgb(224, 255, 255)",
"lightgoldenrodyellow": "rgb(250, 250, 210)",
"lightgray": "rgb(211, 211, 211)",
"lightgreen": "rgb(144, 238, 144)",
"lightgrey": "rgb(211, 211, 211)",
"lightpink": "rgb(255, 182, 193)",
"lightsalmon": "rgb(255, 160, 122)",
"lightseagreen": "rgb(32, 178, 170)",
"lightskyblue": "rgb(135, 206, 250)",
"lightslategray": "rgb(119, 136, 153)",
"lightslategrey": "rgb(119, 136, 153)",
"lightsteelblue": "rgb(176, 196, 222)",
"lightyellow": "rgb(255, 255, 224)",
"lime": "rgb(0, 255, 0)",
"limegreen": "rgb(50, 205, 50)",
"linen": "rgb(250, 240, 230)",
"magenta": "rgb(255, 0, 255)",
"maroon": "rgb(128, 0, 0)",
"mediumaquamarine": "rgb(102, 205, 170)",
"mediumblue": "rgb(0, 0, 205)",
"mediumorchid": "rgb(186, 85, 211)",
"mediumpurple": "rgb(147, 112, 219)",
"mediumseagreen": "rgb(60, 179, 113)",
"mediumslateblue": "rgb(123, 104, 238)",
"mediumspringgreen": "rgb(0, 250, 154)",
"mediumturquoise": "rgb(72, 209, 204)",
"mediumvioletred": "rgb(199, 21, 133)",
"midnightblue": "rgb(25, 25, 112)",
"mintcream": "rgb(245, 255, 250)",
"mistyrose": "rgb(255, 228, 225)",
"moccasin": "rgb(255, 228, 181)",
"navajowhite": "rgb(255, 222, 173)",
"navy": "rgb(0, 0, 128)",
"oldlace": "rgb(253, 245, 230)",
"olive": "rgb(128, 128, 0)",
"olivedrab": "rgb(107, 142, 35)",
"orange": "rgb(255, 165, 0)",
"orangered": "rgb(255, 69, 0)",
"orchid": "rgb(218, 112, 214)",
"palegoldenrod": "rgb(238, 232, 170)",
"palegreen": "rgb(152, 251, 152)",
"paleturquoise": "rgb(175, 238, 238)",
"palevioletred": "rgb(219, 112, 147)",
"papayawhip": "rgb(255, 239, 213)",
"peachpuff": "rgb(255, 218, 185)",
"peru": "rgb(205, 133, 63)",
"pink": "rgb(255, 192, 203)",
"plum": "rgb(221, 160, 221)",
"powderblue": "rgb(176, 224, 230)",
"purple": "rgb(128, 0, 128)",
"red": "rgb(255, 0, 0)",
"rosybrown": "rgb(188, 143, 143)",
"royalblue": "rgb(65, 105, 225)",
"saddlebrown": "rgb(139, 69, 19)",
"salmon": "rgb(250, 128, 114)",
"sandybrown": "rgb(244, 164, 96)",
"seagreen": "rgb(46, 139, 87)",
"seashell": "rgb(255, 245, 238)",
"sienna": "rgb(160, 82, 45)",
"silver": "rgb(192, 192, 192)",
"skyblue": "rgb(135, 206, 235)",
"slateblue": "rgb(106, 90, 205)",
"slategray": "rgb(112, 128, 144)",
"slategrey": "rgb(112, 128, 144)",
"snow": "rgb(255, 250, 250)",
"springgreen": "rgb(0, 255, 127)",
"steelblue": "rgb(70, 130, 180)",
"tan": "rgb(210, 180, 140)",
"teal": "rgb(0, 128, 128)",
"thistle": "rgb(216, 191, 216)",
"tomato": "rgb(255, 99, 71)",
"turquoise": "rgb(64, 224, 208)",
"violet": "rgb(238, 130, 238)",
"wheat": "rgb(245, 222, 179)",
"white": "rgb(255, 255, 255)",
"whitesmoke": "rgb(245, 245, 245)",
"yellow": "rgb(255, 255, 0)",
"yellowgreen": "rgb(154, 205, 50)",
"activeborder": "#0000ff",
"activecaption": "#0000ff",
"appworkspace": "#ffffff",
"background": "#ffffff",
"buttonface": "#000000",
"buttonhighlight": "#cccccc",
"buttonshadow": "#333333",
"buttontext": "#000000",
"captiontext": "#000000",
"graytext": "#333333",
"highlight": "#0000ff",
"highlighttext": "#cccccc",
"inactiveborder": "#333333",
"inactivecaption": "#cccccc",
"inactivecaptiontext": "#333333",
"infobackground": "#cccccc",
"infotext": "#000000",
"menu": "#cccccc",
"menutext": "#333333",
"scrollbar": "#cccccc",
"threeddarkshadow": "#333333",
"threedface": "#cccccc",
"threedhighlight": "#ffffff",
"threedlightshadow": "#333333",
"threedshadow": "#333333",
"window": "#cccccc",
"windowframe": "#cccccc",
"windowtext": "#000000"}
def color(string = None, opacity = 1):
"""Replace ``string`` representing a color by a RGBA tuple."""
if not string or string in ("none", "transparent"):
return (0, 0, 0, 0)
string = string.strip().lower()
if string in COLORS:
string = COLORS[string]
if string.startswith("rgba"):
r, g, b, a = tuple(
float(i.strip(" %")) * 2.55 if "%" in i else float(i)
for i in string.strip(" rgba()").split(","))
return r / 255, g / 255, b / 255, a * opacity
elif string.startswith("rgb"):
r, g, b = tuple(
float(i.strip(" %")) / 100 if "%" in i else float(i) / 255
for i in string.strip(" rgb()").split(","))
return r, g, b, opacity
if len(string) in (4, 5):
string = "#" + "".join(2 * char for char in string[1:])
if len(string) == 9:
opacity *= int(string[7:9], 16) / 255
try:
plain_color = tuple(
int(value, 16) / 255. for value in (
string[1:3], string[3:5], string[5:7]))
except ValueError:
# Unknown color, return black
return (0, 0, 0, 1)
else:
return plain_color + (opacity,)
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Externally defined elements managers.
This module handles gradients and patterns.
"""
import cairo
from math import radians
from copy import deepcopy
from .colors import color
from .helpers import node_format, preserve_ratio, urls, transform
from .units import size
from ..parser import Tree
def parse_def(surface, node):
"""Parse the SVG definitions."""
for def_type in ("marker", "gradient", "pattern", "path"):
if def_type in node.tag.lower():
def_list = getattr(surface, def_type + "s")
name = node["id"]
href = node.get("{http://www.w3.org/1999/xlink}href")
if href and href[0] == "#" and href[1:] in def_list:
new_node = deepcopy(def_list[href[1:]])
new_node.update(node)
node = new_node
def_list[name] = node
def gradient_or_pattern(surface, node, name):
"""Gradient or pattern color."""
if name in surface.gradients:
return draw_gradient(surface, node, name)
elif name in surface.patterns:
return draw_pattern(surface, name)
def marker(surface, node):
"""Store a marker definition."""
parse_def(surface, node)
def linear_gradient(surface, node):
"""Store a linear gradient definition."""
parse_def(surface, node)
def radial_gradient(surface, node):
"""Store a radial gradient definition."""
parse_def(surface, node)
def pattern(surface, node):
"""Store a pattern definition."""
parse_def(surface, node)
def draw_gradient(surface, node, name):
"""Gradients colors."""
gradient_node = surface.gradients[name]
transform(surface, gradient_node.get("gradientTransform"))
if gradient_node.get("gradientUnits") == "userSpaceOnUse":
width_ref, height_ref = "x", "y"
diagonal_ref = "xy"
else:
x = float(size(surface, node.get("x"), "x"))
y = float(size(surface, node.get("y"), "y"))
width = float(size(surface, node.get("width"), "x"))
height = float(size(surface, node.get("height"), "y"))
width_ref = height_ref = diagonal_ref = 1
if gradient_node.tag == "linearGradient":
x1 = float(size(surface, gradient_node.get("x1", "0%"), width_ref))
x2 = float(size(surface, gradient_node.get("x2", "100%"), width_ref))
y1 = float(size(surface, gradient_node.get("y1", "0%"), height_ref))
y2 = float(size(surface, gradient_node.get("y2", "0%"), height_ref))
gradient_pattern = cairo.LinearGradient(x1, y1, x2, y2)
elif gradient_node.tag == "radialGradient":
r = float(size(surface, gradient_node.get("r", "50%"), diagonal_ref))
cx = float(size(surface, gradient_node.get("cx", "50%"), width_ref))
cy = float(size(surface, gradient_node.get("cy", "50%"), height_ref))
fx = float(size(surface, gradient_node.get("fx", str(cx)), width_ref))
fy = float(size(surface, gradient_node.get("fy", str(cy)), height_ref))
gradient_pattern = cairo.RadialGradient(fx, fy, 0, cx, cy, r)
if gradient_node.get("gradientUnits") != "userSpaceOnUse":
gradient_pattern.set_matrix(cairo.Matrix(
1 / width, 0, 0, 1 / height, -x / width, -y / height))
gradient_pattern.set_extend(getattr(
cairo, "EXTEND_%s" % node.get("spreadMethod", "pad").upper()))
offset = 0
for child in gradient_node.children:
offset = max(offset, size(surface, child.get("offset"), 1))
stop_color = color(
child.get("stop-color", "black"),
float(child.get("stop-opacity", 1)))
gradient_pattern.add_color_stop_rgba(offset, *stop_color)
gradient_pattern.set_extend(getattr(
cairo, "EXTEND_%s" % gradient_node.get("spreadMethod", "pad").upper()))
surface.context.set_source(gradient_pattern)
def draw_pattern(surface, name):
"""Draw a pattern image."""
pattern_node = surface.patterns[name]
pattern_node.tag = "g"
transform(surface, "translate(%s %s)" % (
pattern_node.get("x"), pattern_node.get("y")))
transform(surface, pattern_node.get("patternTransform"))
from . import SVGSurface # circular import
pattern_surface = SVGSurface(pattern_node, None, surface.dpi)
pattern_pattern = cairo.SurfacePattern(pattern_surface.cairo)
pattern_pattern.set_extend(cairo.EXTEND_REPEAT)
surface.context.set_source(pattern_pattern)
def draw_marker(surface, node, position = "mid"):
"""Draw a marker."""
# TODO: manage markers for other tags than path
if position == "start":
node.markers = {
"start": list(urls(node.get("marker-start", ""))),
"mid": list(urls(node.get("marker-mid", ""))),
"end": list(urls(node.get("marker-end", "")))}
all_markers = list(urls(node.get("marker", "")))
for markers_list in node.markers.values():
markers_list.extend(all_markers)
pending_marker = (
surface.context.get_current_point(), node.markers[position])
if position == "start":
node.pending_markers.append(pending_marker)
return
elif position == "end":
node.pending_markers.append(pending_marker)
while node.pending_markers:
next_point, markers = node.pending_markers.pop(0)
angle1 = node.tangents.pop(0)
angle2 = node.tangents.pop(0)
if angle1 is None:
angle1 = angle2
for active_marker in markers:
if not active_marker.startswith("#"):
continue
active_marker = active_marker[1:]
if active_marker in surface.markers:
marker_node = surface.markers[active_marker]
angle = marker_node.get("orient", "0")
if angle == "auto":
angle = float(angle1 + angle2) / 2
else:
angle = radians(float(angle))
temp_path = surface.context.copy_path()
current_x, current_y = next_point
if node.get("markerUnits") == "userSpaceOnUse":
base_scale = 1
else:
base_scale = size(
surface, surface.parent_node.get("stroke-width"))
# Returns 4 values
scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, marker_node)
viewbox = node_format(surface, marker_node)[-1]
viewbox_width = viewbox[2] - viewbox[0]
viewbox_height = viewbox[3] - viewbox[1]
surface.context.new_path()
for child in marker_node.children:
surface.context.save()
surface.context.translate(current_x, current_y)
surface.context.rotate(angle)
surface.context.scale(
base_scale / viewbox_width * float(scale_x),
base_scale / viewbox_height * float(scale_y))
surface.context.translate(translate_x, translate_y)
surface.draw(child)
surface.context.restore()
surface.context.append_path(temp_path)
if position == "mid":
node.pending_markers.append(pending_marker)
def use(surface, node):
"""Draw the content of another SVG file."""
surface.context.save()
surface.context.translate(
size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y"))
if "x" in node:
del node["x"]
if "y" in node:
del node["y"]
if "viewBox" in node:
del node["viewBox"]
href = node.get("{http://www.w3.org/1999/xlink}href")
url = list(urls(href))[0]
tree = Tree(url = url, parent = node)
surface.set_context_size(*node_format(surface, tree))
surface.draw(tree)
surface.context.restore()
# Restore twice, because draw does not restore at the end of svg tags
surface.context.restore()
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Surface helpers.
"""
import cairo
from math import cos, sin, tan, atan2, radians
from .units import size
# Python 2/3 management
# pylint: disable=C0103
try:
Error = cairo.Error
except AttributeError:
Error = SystemError
# pylint: enable=C0103
class PointError(Exception):
"""Exception raised when parsing a point fails."""
def distance(x1, y1, x2, y2):
"""Get the distance between two points."""
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
def filter_fill_or_stroke(value):
"""Remove unnecessary characters from fill or stroke value."""
if not value:
return
content = list(urls(value))[0]
if "url" in value:
if not content.startswith("#"):
return
content = content[1:]
return content
def node_format(surface, node):
"""Return ``(width, height, viewbox)`` of ``node``."""
width = size(surface, node.get("width"), "x")
height = size(surface, node.get("height"), "y")
viewbox = node.get("viewBox")
if viewbox:
viewbox = tuple(float(position) for position in viewbox.split())
width = width or viewbox[2]
height = height or viewbox[3]
return width, height, viewbox
def normalize(string = None):
"""Normalize a string corresponding to an array of various values."""
string = string.replace("-", " -")
string = string.replace(",", " ")
while " " in string:
string = string.replace(" ", " ")
string = string.replace("e -", "e-")
values = string.split(" ")
string = ""
for value in values:
if value.count(".") > 1:
numbers = value.split(".")
string += "%s.%s " % (numbers.pop(0), numbers.pop(0))
string += ".%s " % " .".join(numbers)
else:
string += value + " "
return string.strip()
def point(surface, string = None):
"""Return ``(x, y, trailing_text)`` from ``string``."""
if not string:
return (0, 0, "")
try:
x, y, string = (string.strip() + " ").split(" ", 2)
except ValueError:
raise PointError("The point cannot be found in string %s" % string)
return size(surface, x, "x"), size(surface, y, "y"), string
def point_angle(cx, cy, px, py):
"""Return angle between x axis and point knowing given center."""
return atan2(py - cy, px - cx)
def preserve_ratio(surface, node):
"""Manage the ratio preservation."""
if node.tag == "marker":
scale_x = size(surface, node.get("markerWidth", "3"), "x")
scale_y = size(surface, node.get("markerHeight", "3"), "y")
translate_x = -size(surface, node.get("refX"))
translate_y = -size(surface, node.get("refY"))
elif node.tag in ("svg", "image"):
width, height, _ = node_format(surface, node)
scale_x = width / node.image_width
scale_y = height / node.image_height
align = node.get("preserveAspectRatio", "xMidYMid").split(" ")[0]
if align == "none":
return scale_x, scale_y, 0, 0
else:
mos_properties = node.get("preserveAspectRatio", "").split()
meet_or_slice = (
mos_properties[1] if len(mos_properties) > 1 else None)
if meet_or_slice == "slice":
scale_value = max(scale_x, scale_y)
else:
scale_value = min(scale_x, scale_y)
scale_x = scale_y = scale_value
x_position = align[1:4].lower()
y_position = align[5:].lower()
if x_position == "min":
translate_x = 0
if y_position == "min":
translate_y = 0
if x_position == "mid":
translate_x = (width / scale_x - node.image_width) / 2.
if y_position == "mid":
translate_y = (height / scale_y - node.image_height) / 2.
if x_position == "max":
translate_x = width / scale_x - node.image_width
if y_position == "max":
translate_y = height / scale_y - node.image_height
return scale_x, scale_y, translate_x, translate_y
def quadratic_points(x1, y1, x2, y2, x3, y3):
"""Return the quadratic points to create quadratic curves."""
xq1 = x2 * 2 / 3 + x1 / 3
yq1 = y2 * 2 / 3 + y1 / 3
xq2 = x2 * 2 / 3 + x3 / 3
yq2 = y2 * 2 / 3 + y3 / 3
return xq1, yq1, xq2, yq2, x3, y3
def rotate(x, y, angle):
"""Rotate a point of an angle around the origin point."""
return x * cos(angle) - y * sin(angle), y * cos(angle) + x * sin(angle)
def transform(surface, string):
"""Update ``surface`` matrix according to transformation ``string``."""
if not string:
return
transformations = string.split(")")
matrix = cairo.Matrix()
for transformation in transformations:
for ttype in (
"scale", "translate", "matrix", "rotate", "skewX",
"skewY"):
if ttype in transformation:
transformation = transformation.replace(ttype, "")
transformation = transformation.replace("(", "")
transformation = normalize(transformation).strip() + " "
values = []
while transformation:
value, transformation = \
transformation.split(" ", 1)
# TODO: manage the x/y sizes here
values.append(size(surface, value))
if ttype == "matrix":
matrix = cairo.Matrix(*values).multiply(matrix)
elif ttype == "rotate":
angle = radians(float(values.pop(0)))
x, y = values or (0, 0)
matrix.translate(x, y)
matrix.rotate(angle)
matrix.translate(-x, -y)
elif ttype == "skewX":
tangent = tan(radians(float(values[0])))
matrix = \
cairo.Matrix(1, 0, tangent, 1, 0, 0).multiply(matrix)
elif ttype == "skewY":
tangent = tan(radians(float(values[0])))
matrix = \
cairo.Matrix(1, tangent, 0, 1, 0, 0).multiply(matrix)
elif ttype == "translate":
if len(values) == 1:
values += (0,)
matrix.translate(*values)
elif ttype == "scale":
if len(values) == 1:
values = 2 * values
matrix.scale(*values)
apply_matrix_transform(surface, matrix)
def apply_matrix_transform(surface, matrix):
try:
matrix.invert()
except Error:
# Matrix not invertible, clip the surface to an empty path
active_path = surface.context.copy_path()
surface.context.new_path()
surface.context.clip()
surface.context.append_path(active_path)
else:
matrix.invert()
surface.context.transform(matrix)
def urls(string):
"""Parse a comma-separated list of url() strings."""
for link in string.split(","):
link = link.strip()
if link.startswith("url"):
link = link[3:]
yield link.strip("() ")
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Images manager.
"""
import base64
import cairo
from io import BytesIO
try:
from urllib import urlopen, unquote
import urlparse
unquote_to_bytes = lambda data: unquote(
data.encode('ascii') if isinstance(data, unicode) else data)
except ImportError:
from urllib.request import urlopen
from urllib import parse as urlparse # Python 3
from urllib.parse import unquote_to_bytes
from .helpers import node_format, size, preserve_ratio
from ..parser import Tree
def open_data_url(url):
"""Decode URLs with the 'data' scheme. urllib can handle them
in Python 2, but that is broken in Python 3.
Inspired from Python 2.7.2’s urllib.py.
"""
# syntax of data URLs:
# dataurl := "data:" [ mediatype ] [ ";base64" ] "," data
# mediatype := [ type "/" subtype ] *( ";" parameter )
# data := *urlchar
# parameter := attribute "=" value
try:
header, data = url.split(",", 1)
except ValueError:
raise IOError("bad data URL")
header = header[5:] # len("data:") == 5
if header:
semi = header.rfind(";")
if semi >= 0 and "=" not in header[semi:]:
encoding = header[semi + 1:]
else:
encoding = ""
else:
encoding = ""
data = unquote_to_bytes(data)
if encoding == "base64":
missing_padding = 4 - len(data) % 4
if missing_padding:
data += b"=" * missing_padding
return base64.decodestring(data)
return data
def image(surface, node):
"""Draw an image ``node``."""
url = node.get("{http://www.w3.org/1999/xlink}href")
if not url:
return
if url.startswith("data:"):
image_bytes = open_data_url(url)
else:
base_url = node.get("{http://www.w3.org/XML/1998/namespace}base")
if base_url:
url = urlparse.urljoin(base_url, url)
if node.url:
url = urlparse.urljoin(node.url, url)
if urlparse.urlparse(url).scheme:
input_ = urlopen(url)
else:
input_ = open(url, 'rb') # filename
image_bytes = input_.read()
if len(image_bytes) < 5:
return
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
width = size(surface, node.get("width"), "x")
height = size(surface, node.get("height"), "y")
surface.context.rectangle(x, y, width, height)
surface.context.clip()
if image_bytes[:4] == b"\x89PNG":
png_bytes = image_bytes
elif image_bytes[:5] == b"\x3csvg ":
surface.context.save()
surface.context.translate(x, y)
if "x" in node:
del node["x"]
if "y" in node:
del node["y"]
if "viewBox" in node:
del node["viewBox"]
tree = Tree(bytestring = image_bytes)
tree_width, tree_height, viewbox = node_format(surface, tree)
if not tree_width or not tree_height:
tree_width = tree["width"] = width
tree_height = tree["height"] = height
node.image_width = tree_width or width
node.image_height = tree_height or height
scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, node)
surface.set_context_size(*node_format(surface, tree))
surface.context.translate(*surface.context.get_current_point())
surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y)
surface.draw(tree)
surface.context.restore()
# Restore twice, because draw does not restore at the end of svg tags
surface.context.restore()
return
else:
try:
from pystacia import read_blob
png_bytes = read_blob(image_bytes).get_blob('png')
except:
# No way to handle the image
return
image_surface = cairo.ImageSurface.create_from_png(BytesIO(png_bytes))
node.image_width = image_surface.get_width()
node.image_height = image_surface.get_height()
scale_x, scale_y, translate_x, translate_y = preserve_ratio(surface, node)
surface.context.rectangle(x, y, width, height)
pattern_pattern = cairo.SurfacePattern(image_surface)
surface.context.save()
surface.context.translate(*surface.context.get_current_point())
surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y)
surface.context.set_source(pattern_pattern)
surface.context.fill()
surface.context.restore()
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Paths manager.
"""
from math import pi, radians
from .defs import draw_marker
from .helpers import normalize, point, point_angle, quadratic_points, rotate
from .units import size
PATH_LETTERS = "achlmqstvzACHLMQSTVZ"
PATH_TAGS = (
"circle", "ellipse", "line", "path", "polygon", "polyline", "rect")
def path(surface, node):
"""Draw a path ``node``."""
string = node.get("d", "")
if not string.strip():
# Don't draw empty paths at all
return
draw_marker(surface, node, "start")
for letter in PATH_LETTERS:
string = string.replace(letter, " %s " % letter)
last_letter = None
string = normalize(string)
while string:
string = string.strip()
if string.split(" ", 1)[0] in PATH_LETTERS:
letter, string = (string + " ").split(" ", 1)
elif letter == "M":
letter = "L"
elif letter == "m":
letter = "l"
if letter in "aA":
# Elliptic curve
x1, y1 = surface.context.get_current_point()
rx, ry, string = point(surface, string)
rotation, string = string.split(" ", 1)
rotation = radians(float(rotation))
# The large and sweep values are not always separated from the
# following values, here is the crazy parser
large, string = string[0], string[1:].strip()
while not large[-1].isdigit():
large, string = large + string[0], string[1:].strip()
sweep, string = string[0], string[1:].strip()
while not sweep[-1].isdigit():
sweep, string = sweep + string[0], string[1:].strip()
large, sweep = bool(int(large)), bool(int(sweep))
x3, y3, string = point(surface, string)
if letter == "A":
# Absolute x3 and y3, convert to relative
x3 -= x1
y3 -= y1
# rx=0 or ry=0 means straight line
if not rx or not ry:
string = "l %f %f %s" % (x3, y3, string)
continue
radii_ratio = ry / rx
# Cancel the rotation of the second point
xe, ye = rotate(x3, y3, -rotation)
ye /= radii_ratio
# Find the angle between the second point and the x axis
angle = point_angle(0, 0, xe, ye)
# Put the second point onto the x axis
xe = (xe ** 2 + ye ** 2) ** .5
ye = 0
# Update the x radius if it is too small
rx = max(rx, xe / 2)
# Find one circle centre
xc = xe / 2
yc = (rx ** 2 - xc ** 2) ** .5
# Choose between the two circles according to flags
if not (large ^ sweep):
yc = -yc
# Define the arc sweep
arc = \
surface.context.arc if sweep else surface.context.arc_negative
# Put the second point and the center back to their positions
xe, ye = rotate(xe, 0, angle)
xc, yc = rotate(xc, yc, angle)
# Find the drawing angles
angle1 = point_angle(xc, yc, 0, 0)
angle2 = point_angle(xc, yc, xe, ye)
# Store the tangent angles
node.tangents.extend((-angle1, -angle2))
# Draw the arc
surface.context.save()
surface.context.translate(x1, y1)
surface.context.rotate(rotation)
surface.context.scale(1, radii_ratio)
arc(xc, yc, rx, angle1, angle2)
surface.context.restore()
elif letter == "c":
# Relative curve
x1, y1, string = point(surface, string)
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "C":
# Curve
x1, y1, string = point(surface, string)
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "h":
# Relative horizontal line
x, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = 0 if size(surface, x, "x") > 0 else pi
node.tangents.extend((-angle, angle))
surface.context.rel_line_to(size(surface, x, "x"), 0)
elif letter == "H":
# Horizontal line
x, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = 0 if size(surface, x, "x") > old_x else pi
node.tangents.extend((-angle, angle))
surface.context.line_to(size(surface, x, "x"), old_y)
elif letter == "l":
# Relative straight line
x, y, string = point(surface, string)
angle = point_angle(0, 0, x, y)
node.tangents.extend((-angle, angle))
surface.context.rel_line_to(x, y)
elif letter == "L":
# Straight line
x, y, string = point(surface, string)
old_x, old_y = surface.context.get_current_point()
angle = point_angle(old_x, old_y, x, y)
node.tangents.extend((-angle, angle))
surface.context.line_to(x, y)
elif letter == "m":
# Current point relative move
x, y, string = point(surface, string)
surface.context.rel_move_to(x, y)
elif letter == "M":
# Current point move
x, y, string = point(surface, string)
surface.context.move_to(x, y)
elif letter == "q":
# Relative quadratic curve
x1, y1 = 0, 0
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
node.tangents.extend((0, 0))
elif letter == "Q":
# Quadratic curve
x1, y1 = surface.context.get_current_point()
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
node.tangents.extend((0, 0))
elif letter == "s":
# Relative smooth curve
# TODO: manage last_letter in "CS"
x1 = x3 - x2 if last_letter in "cs" else 0
y1 = y3 - y2 if last_letter in "cs" else 0
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "S":
# Smooth curve
# TODO: manage last_letter in "cs"
x, y = surface.context.get_current_point()
x1 = 2 * x3 - x2 if last_letter in "CS" else x
y1 = 2 * y3 - y2 if last_letter in "CS" else y
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "t":
# Relative quadratic curve end
if last_letter not in "QqTt":
x2, y2, x3, y3 = 0, 0, 0, 0
elif last_letter in "QT":
x2 -= x1
y2 -= y1
x3 -= x1
y3 -= y1
x2 = x3 - x2
y2 = y3 - y2
x1, y1 = 0, 0
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
node.tangents.extend((0, 0))
surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
elif letter == "T":
# Quadratic curve end
abs_x, abs_y = surface.context.get_current_point()
if last_letter not in "QqTt":
x2, y2, x3, y3 = abs_x, abs_y, abs_x, abs_y
elif last_letter in "qt":
x2 += x1
y2 += y1
x2 = 2 * abs_x - x2
y2 = 2 * abs_y - y2
x1, y1 = abs_x, abs_y
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
node.tangents.extend((0, 0))
surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
elif letter == "v":
# Relative vertical line
y, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = pi / 2 if size(surface, y, "y") > 0 else -pi / 2
node.tangents.extend((-angle, angle))
surface.context.rel_line_to(0, size(surface, y, "y"))
elif letter == "V":
# Vertical line
y, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = pi / 2 if size(surface, y, "y") > 0 else -pi / 2
node.tangents.extend((-angle, angle))
surface.context.line_to(old_x, size(surface, y, "y"))
elif letter in "zZ":
# End of path
node.tangents.extend((0, 0))
surface.context.close_path()
string = string.strip()
if letter in "hHvV":
if string.split(" ", 1)[0] not in PATH_LETTERS:
surface.context.move_to(*surface.context.get_current_point())
if string and letter not in "mMzZ":
draw_marker(surface, node, "mid")
last_letter = letter
node.tangents.append(node.tangents[-1])
draw_marker(surface, node, "end")
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Shapes drawers.
"""
from math import pi
from .helpers import normalize, point, size
def circle(surface, node):
"""Draw a circle ``node`` on ``surface``."""
r = size(surface, node.get("r"))
if not r:
return
surface.context.new_sub_path()
surface.context.arc(
size(surface, node.get("x"), "x") + size(surface, node.get("cx"), "x"),
size(surface, node.get("y"), "y") + size(surface, node.get("cy"), "y"),
r, 0, 2 * pi)
def ellipse(surface, node):
"""Draw an ellipse ``node`` on ``surface``."""
rx = size(surface, node.get("rx"), "x")
ry = size(surface, node.get("ry"), "y")
if not rx or not ry:
return
ratio = ry / rx
surface.context.new_sub_path()
surface.context.save()
surface.context.scale(1, ratio)
surface.context.arc(
size(surface, node.get("x"), "x") + size(surface, node.get("cx"), "x"),
(size(surface, node.get("y"), "y") +
size(surface, node.get("cy"), "y")) / ratio,
size(surface, node.get("rx"), "x"), 0, 2 * pi)
surface.context.restore()
def line(surface, node):
"""Draw a line ``node``."""
x1, y1, x2, y2 = tuple(
size(surface, node.get(position), position[0])
for position in ("x1", "y1", "x2", "y2"))
surface.context.move_to(x1, y1)
surface.context.line_to(x2, y2)
def polygon(surface, node):
"""Draw a polygon ``node`` on ``surface``."""
polyline(surface, node)
surface.context.close_path()
def polyline(surface, node):
"""Draw a polyline ``node``."""
points = normalize(node.get("points"))
if points:
x, y, points = point(surface, points)
surface.context.move_to(x, y)
while points:
x, y, points = point(surface, points)
surface.context.line_to(x, y)
def rect(surface, node):
"""Draw a rect ``node`` on ``surface``."""
# TODO: handle ry
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
width = size(surface, node.get("width"), "x")
height = size(surface, node.get("height"), "y")
if size(surface, node.get("rx"), "x") == 0:
surface.context.rectangle(x, y, width, height)
else:
r = size(surface, node.get("rx"), "x")
a, b, c, d = x, width + x, y, height + y
if r > width - r:
r = width / 2
surface.context.move_to(x, y + height / 2)
surface.context.arc(a + r, c + r, r, 2 * pi / 2, 3 * pi / 2)
surface.context.arc(b - r, c + r, r, 3 * pi / 2, 0 * pi / 2)
surface.context.arc(b - r, d - r, r, 0 * pi / 2, 1 * pi / 2)
surface.context.arc(a + r, d - r, r, 1 * pi / 2, 2 * pi / 2)
surface.context.close_path()
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Root tag drawer.
"""
from .helpers import preserve_ratio, node_format
def svg(surface, node):
"""Draw a svg ``node``."""
if node.get("preserveAspectRatio", "none") != "none":
width, height, viewbox = node_format(surface, node)
node.image_width, node.image_height = viewbox[2:]
scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, node)
surface.context.rectangle(0, 0, width, height)
surface.context.clip()
surface.context.translate(*surface.context.get_current_point())
surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y)
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
SVG tags functions.
"""
from .defs import linear_gradient, marker, pattern, radial_gradient, use
from .image import image
from .path import path
from .shapes import circle, ellipse, line, polygon, polyline, rect
from .svg import svg
from .text import text, text_path, tspan
TAGS = {
"a": tspan,
"circle": circle,
"ellipse": ellipse,
"image": image,
"line": line,
"linearGradient": linear_gradient,
"marker": marker,
"path": path,
"pattern": pattern,
"polyline": polyline,
"polygon": polygon,
"radialGradient": radial_gradient,
"rect": rect,
"svg": svg,
"text": text,
"textPath": text_path,
"tref": use,
"tspan": tspan,
"use": use}
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Text drawers.
"""
import cairo
from math import cos, sin
# Python 2/3 management
# pylint: disable=E0611
try:
from itertools import zip_longest
except ImportError:
from itertools import izip_longest as zip_longest
# pylint: enable=E0611
from .colors import color
from .helpers import distance, normalize, point_angle
from .units import size
def path_length(path):
"""Get the length of ``path``."""
total_length = 0
for item in path:
if item[0] == cairo.PATH_MOVE_TO:
old_point = item[1]
elif item[0] == cairo.PATH_LINE_TO:
new_point = item[1]
length = distance(
old_point[0], old_point[1], new_point[0], new_point[1])
total_length += length
old_point = new_point
return total_length
def point_following_path(path, width):
"""Get the point at ``width`` distance on ``path``."""
total_length = 0
for item in path:
if item[0] == cairo.PATH_MOVE_TO:
old_point = item[1]
elif item[0] == cairo.PATH_LINE_TO:
new_point = item[1]
length = distance(
old_point[0], old_point[1], new_point[0], new_point[1])
total_length += length
if total_length < width:
old_point = new_point
else:
length -= total_length - width
angle = point_angle(
old_point[0], old_point[1], new_point[0], new_point[1])
x = cos(angle) * length + old_point[0]
y = sin(angle) * length + old_point[1]
return x, y
def text(surface, node):
"""Draw a text ``node``."""
# Set black as default text color
if not node.get("fill"):
node["fill"] = "#000000"
# TODO: find a better way to manage white spaces in text nodes
node.text = (node.text or "").lstrip()
node.text = node.text.rstrip() + " "
# TODO: manage font variant
font_size = size(surface, node.get("font-size", "12pt"))
font_family = (node.get("font-family") or "sans-serif").split(",")[0]
font_style = getattr(
cairo, ("font_slant_%s" % node.get("font-style")).upper(),
cairo.FONT_SLANT_NORMAL)
font_weight = getattr(
cairo, ("font_weight_%s" % node.get("font-weight")).upper(),
cairo.FONT_WEIGHT_NORMAL)
surface.context.select_font_face(font_family, font_style, font_weight)
surface.context.set_font_size(font_size)
text_extents = surface.context.text_extents(node.text)
x_bearing = text_extents[0]
width = text_extents[2]
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
text_anchor = node.get("text-anchor")
if text_anchor == "middle":
x -= width / 2. + x_bearing
elif text_anchor == "end":
x -= width + x_bearing
surface.context.move_to(x, y)
surface.context.text_path(node.text)
# Remember the absolute cursor position
surface.cursor_position = surface.context.get_current_point()
def text_path(surface, node):
"""Draw text on a path."""
surface.context.save()
if "url(#" not in (node.get("fill") or ""):
surface.context.set_source_rgba(*color(node.get("fill")))
id_path = node.get("{http://www.w3.org/1999/xlink}href", "")
if not id_path.startswith("#"):
return
id_path = id_path[1:]
if id_path in surface.paths:
path = surface.paths.get(id_path)
else:
return
surface.draw(path, False)
cairo_path = surface.context.copy_path_flat()
surface.context.new_path()
start_offset = size(
surface, node.get("startOffset", 0), path_length(cairo_path))
surface.total_width += start_offset
x, y = point_following_path(cairo_path, surface.total_width)
string = (node.text or "").strip(" \n")
letter_spacing = size(surface, node.get("letter-spacing"))
for letter in string:
surface.total_width += (
surface.context.text_extents(letter)[4] + letter_spacing)
point_on_path = point_following_path(cairo_path, surface.total_width)
if point_on_path:
x2, y2 = point_on_path
else:
continue
angle = point_angle(x, y, x2, y2)
surface.context.save()
surface.context.translate(x, y)
surface.context.rotate(angle)
surface.context.translate(0, size(surface, node.get("y"), "y"))
surface.context.move_to(0, 0)
surface.context.show_text(letter)
surface.context.restore()
x, y = x2, y2
surface.context.restore()
# Remember the relative cursor position
surface.cursor_position = \
size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
def tspan(surface, node):
"""Draw a tspan ``node``."""
x, y = [[i] for i in surface.cursor_position]
if "x" in node:
x = [size(surface, i, "x")
for i in normalize(node["x"]).strip().split(" ")]
if "y" in node:
y = [size(surface, i, "y")
for i in normalize(node["y"]).strip().split(" ")]
string = (node.text or "").strip()
if not string:
return
fill = node.get("fill")
positions = list(zip_longest(x, y))
letters_positions = list(zip(positions, string))
letters_positions = letters_positions[:-1] + [
(letters_positions[-1][0], string[len(letters_positions) - 1:])]
for (x, y), letters in letters_positions:
if x == None:
x = surface.cursor_position[0]
if y == None:
y = surface.cursor_position[1]
node["x"] = str(x + size(surface, node.get("dx"), "x"))
node["y"] = str(y + size(surface, node.get("dy"), "y"))
node["fill"] = fill
node.text = letters
if node.parent.tag == "text":
text(surface, node)
else:
node["x"] = str(x + size(surface, node.get("dx"), "x"))
node["y"] = str(y + size(surface, node.get("dy"), "y"))
text_path(surface, node)
if node.parent.children[-1] == node:
surface.total_width = 0
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Units functions.
"""
UNITS = {
"mm": 1 / 25.4,
"cm": 1 / 2.54,
"in": 1,
"pt": 1 / 72.,
"pc": 1 / 6.,
"px": None}
def size(surface, string, reference = "xy"):
"""Replace a ``string`` with units by a float value.
If ``reference`` is a float, it is used as reference for percentages. If it
is ``'x'``, we use the viewport width as reference. If it is ``'y'``, we
use the viewport height as reference. If it is ``'xy'``, we use
``(viewport_width ** 2 + viewport_height ** 2) ** .5 / 2 ** .5`` as
reference.
"""
if not string:
return 0
try:
return float(string)
except ValueError:
# Not a float, try something else
pass
if "%" in string:
if reference == "x":
reference = surface.context_width or 0
elif reference == "y":
reference = surface.context_height or 0
elif reference == "xy":
reference = (
(surface.context_width ** 2 + surface.context_height ** 2)
** .5 / 2 ** .5)
return float(string.strip(" %")) * reference / 100
elif "em" in string:
return surface.font_size * float(string.strip(" em"))
elif "ex" in string:
# Assume that 1em == 2ex
return surface.font_size * float(string.strip(" ex")) / 2
for unit, coefficient in UNITS.items():
if unit in string:
number = float(string.strip(" " + unit))
return number * (surface.dpi * coefficient if coefficient else 1)
# Try to return the number at the beginning of the string
return_string = ""
while string and (string[0].isdigit() or string[0] in "+-."):
return_string += string[0]
string = string[1:]
# Unknown size or multiple sizes
return float(return_string) if return_string else 0
#!/usr/bin/env python
# This file is copied from GCoder.
#
# GCoder 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,
# 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 <http://www.gnu.org/licenses/>.
import sys
import re
import math
import datetime
from array import array
gcode_parsed_args = ["x", "y", "e", "f", "z", "p", "i", "j", "s"]
gcode_exp = re.compile("\([^\(\)]*\)|;.*|[/\*].*\n|[a-z][-+]?[0-9]*\.?[0-9]*")
m114_exp = re.compile("\([^\(\)]*\)|[/\*].*\n|[A-Z]:?[-+]?[0-9]*\.?[0-9]*")
move_gcodes = ["G0", "G1", "G2", "G3"]
class Line(object):
__slots__ = ('x','y','z','e','f','i','j','s','p',
'raw','split_raw',
'command','is_move',
'relative','relative_e',
'current_x', 'current_y', 'current_z', 'extruding', 'current_tool',
'gcview_end_vertex')
def __init__(self, l):
self.raw = l
self.split_raw = gcode_exp.findall(self.raw.lower())
self.command = self.split_raw[0].upper() if not self.split_raw[0].startswith("n") else self.split_raw[1].upper()
self.is_move = self.command in move_gcodes
def __getattr__(self, name):
return None
def parse_coordinates(self, imperial = False, force = False):
# Not a G-line, we don't want to parse its arguments
if not force and not self.command[0] == "G":
return
if imperial:
for bit in self.split_raw:
code = bit[0]
if code in gcode_parsed_args and len(bit) > 1:
setattr(self, code, 25.4*float(bit[1:]))
else:
for bit in self.split_raw:
code = bit[0]
if code in gcode_parsed_args and len(bit) > 1:
setattr(self, code, float(bit[1:]))
del self.split_raw
def __repr__(self):
return self.raw
class Layer(object):
lines = None
duration = None
def __init__(self, lines):
self.lines = lines
def _preprocess(self, current_x, current_y, current_z):
xmin = float("inf")
ymin = float("inf")
zmin = 0
xmax = float("-inf")
ymax = float("-inf")
zmax = float("-inf")
relative = False
relative_e = False
for line in self.lines:
if not line.is_move and line.command != "G92":
continue
if line.is_move:
x = line.x
y = line.y
z = line.z
if line.relative:
x = current_x + (x or 0)
y = current_y + (y or 0)
z = current_z + (z or 0)
current_x = x or current_x
current_y = y or current_y
current_z = z or current_z
if line.e:
if x:
xmin = min(xmin, x)
xmax = max(xmax, x)
if y:
ymin = min(ymin, y)
ymax = max(ymax, y)
if current_z:
zmin = min(zmin, current_z)
zmax = max(zmax, current_z)
else:
current_x = line.x or current_x
current_y = line.y or current_y
current_z = line.z or current_z
line.current_x = current_x
line.current_y = current_y
line.current_z = current_z
return (current_x, current_y, current_z), (xmin, xmax), (ymin, ymax), (zmin, zmax)
class GCode(object):
lines = None
layers = None
all_layers = None
layer_idxs = None
line_idxs = None
append_layer = None
append_layer_id = None
imperial = False
relative = False
relative_e = False
current_tool = 0
filament_length = None
xmin = None
xmax = None
ymin = None
ymax = None
zmin = None
zmax = None
width = None
depth = None
height = None
def __init__(self,data):
self.lines = [Line(l2) for l2 in
(l.strip() for l in data)
if l2]
self._preprocess_lines()
self.filament_length = self._preprocess_extrusion()
self._create_layers()
self._preprocess_layers()
def __len__(self):
return len(self.line_idxs)
def __iter__(self):
return self.lines.__iter__()
def append(self, command):
command = command.strip()
if not command:
return
gline = Line(command)
self.lines.append(gline)
self._preprocess_lines([gline])
self._preprocess_extrusion([gline])
self.append_layer.lines.append(gline)
self.layer_idxs.append(self.append_layer_id)
self.line_idxs.append(len(self.append_layer.lines))
return gline
def _preprocess_lines(self, lines = None):
"""Checks for G20, G21, G90 and G91, sets imperial and relative flags"""
if not lines:
lines = self.lines
imperial = self.imperial
relative = self.relative
relative_e = self.relative_e
current_tool = self.current_tool
for line in lines:
if line.is_move:
line.relative = relative
line.relative_e = relative_e
line.current_tool = current_tool
elif line.command == "G20":
imperial = True
elif line.command == "G21":
imperial = False
elif line.command == "G90":
relative = False
relative_e = False
elif line.command == "G91":
relative = True
relative_e = True
elif line.command == "M82":
relative_e = False
elif line.command == "M83":
relative_e = True
elif line.command[0] == "T":
current_tool = int(line.command[1:])
if line.command[0] == "G":
line.parse_coordinates(imperial)
self.imperial = imperial
self.relative = relative
self.relative_e = relative_e
self.current_tool = current_tool
def _preprocess_extrusion(self, lines = None, cur_e = 0):
if not lines:
lines = self.lines
total_e = 0
max_e = 0
for line in lines:
if line.e == None:
continue
if line.is_move:
if line.relative_e:
line.extruding = line.e != 0
total_e += line.e
else:
line.extruding = line.e != cur_e
total_e += line.e - cur_e
cur_e = line.e
max_e = max(max_e, total_e)
elif line.command == "G92":
cur_e = line.e
return max_e
# FIXME : looks like this needs to be tested with list Z on move
def _create_layers(self):
layers = {}
all_layers = []
layer_idxs = []
line_idxs = []
layer_id = 0
layer_line = 0
prev_z = None
cur_z = 0
cur_lines = []
for line in self.lines:
if line.command == "G92" and line.z != None:
cur_z = line.z
elif line.is_move:
if line.z != None:
if line.relative:
cur_z += line.z
else:
cur_z = line.z
if cur_z != prev_z:
all_layers.append(Layer(cur_lines))
old_lines = layers.get(prev_z, [])
old_lines += cur_lines
layers[prev_z] = old_lines
cur_lines = []
layer_id += 1
layer_line = 0
cur_lines.append(line)
layer_idxs.append(layer_id)
line_idxs.append(layer_line)
layer_line += 1
prev_z = cur_z
if cur_lines:
all_layers.append(Layer(cur_lines))
old_lines = layers.pop(prev_z, [])
old_lines += cur_lines
layers[prev_z] = old_lines
for idx in layers.keys():
cur_lines = layers[idx]
has_movement = False
for l in layers[idx]:
if l.is_move and l.e != None:
has_movement = True
break
if has_movement:
layers[idx] = Layer(cur_lines)
else:
del layers[idx]
self.append_layer_id = len(all_layers)
self.append_layer = Layer([])
all_layers.append(self.append_layer)
self.all_layers = all_layers
self.layers = layers
self.layer_idxs = array('I', layer_idxs)
self.line_idxs = array('I', line_idxs)
def idxs(self, i):
return self.layer_idxs[i], self.line_idxs[i]
def num_layers(self):
return len(self.layers)
def _preprocess_layers(self):
xmin = float("inf")
ymin = float("inf")
zmin = 0
xmax = float("-inf")
ymax = float("-inf")
zmax = float("-inf")
current_x = 0
current_y = 0
current_z = 0
for l in self.all_layers:
(current_x, current_y, current_z), (xm, xM), (ym, yM), (zm, zM) = l._preprocess(current_x, current_y, current_z)
xmin = min(xm, xmin)
xmax = max(xM, xmax)
ymin = min(ym, ymin)
ymax = max(yM, ymax)
zmin = min(zm, zmin)
zmax = max(zM, zmax)
self.xmin = xmin if not math.isinf(xmin) else 0
self.xmax = xmax if not math.isinf(xmax) else 0
self.ymin = ymin if not math.isinf(ymin) else 0
self.ymax = ymax if not math.isinf(ymax) else 0
self.zmin = zmin if not math.isinf(zmin) else 0
self.zmax = zmax if not math.isinf(zmax) else 0
self.width = self.xmax - self.xmin
self.depth = self.ymax - self.ymin
self.height = self.zmax - self.zmin
def estimate_duration(self):
lastx = lasty = lastz = laste = lastf = 0.0
x = y = z = e = f = 0.0
currenttravel = 0.0
totaltravel = 0.0
moveduration = 0.0
totalduration = 0.0
acceleration = 1500.0 #mm/s/s ASSUMING THE DEFAULT FROM SPRINTER !!!!
layerduration = 0.0
layerbeginduration = 0.0
layercount = 0
#TODO:
# get device caps from firmware: max speed, acceleration/axis (including extruder)
# calculate the maximum move duration accounting for above ;)
for layer in self.all_layers:
for line in layer.lines:
if line.command not in ["G1", "G0", "G4"]:
continue
if line.command == "G4":
moveduration = line.p
if not moveduration:
continue
else:
moveduration /= 1000.0
else:
x = line.x if line.x != None else lastx
y = line.y if line.y != None else lasty
e = line.e if line.e != None else laste
f = line.f / 60.0 if line.f != None else lastf # mm/s vs mm/m => divide by 60
# given last feedrate and current feedrate calculate the distance needed to achieve current feedrate.
# if travel is longer than req'd distance, then subtract distance to achieve full speed, and add the time it took to get there.
# then calculate the time taken to complete the remaining distance
currenttravel = math.hypot(x - lastx, y - lasty)
# FIXME: review this better
# this looks wrong : there's little chance that the feedrate we'll decelerate to is the previous feedrate
# shouldn't we instead look at three consecutive moves ?
distance = 2 * abs(((lastf + f) * (f - lastf) * 0.5) / acceleration) # multiply by 2 because we have to accelerate and decelerate
if distance <= currenttravel and lastf + f != 0 and f != 0:
# Unsure about this formula -- iXce reviewing this code
moveduration = 2 * distance / (lastf + f)
currenttravel -= distance
moveduration += currenttravel/f
else:
moveduration = math.sqrt(2 * distance / acceleration) # probably buggy : not taking actual travel into account
totalduration += moveduration
lastx = x
lasty = y
laste = e
lastf = f
layer.duration = totalduration - layerbeginduration
layerbeginduration = totalduration
return "%d layers, %s" % (len(self.layers), str(datetime.timedelta(seconds = int(totalduration))))
def main():
if len(sys.argv) < 2:
print "usage: %s filename.gcode" % sys.argv[0]
return
gcode = GCode(open(sys.argv[1]))
print "Dimensions:"
print "\tX: %0.02f - %0.02f (%0.02f)" % (gcode.xmin,gcode.xmax,gcode.width)
print "\tY: %0.02f - %0.02f (%0.02f)" % (gcode.ymin,gcode.ymax,gcode.depth)
print "\tZ: %0.02f - %0.02f (%0.02f)" % (gcode.zmin,gcode.zmax,gcode.height)
print "Filament used: %0.02fmm" % gcode.filament_length
print "Number of layers: %d" % gcode.num_layers()
print "Estimated duration: %s" % gcode.estimate_duration()
if __name__ == '__main__':
main()
#!/usr/bin/python #!/usr/bin/python
# This file is part of the Printrun suite. # This file is part of the Printrun suite.
# #
# Printrun is free software: you can redistribute it and/or modify # Printrun is free software: you can redistribute it and/or modify
...@@ -16,85 +17,67 @@ ...@@ -16,85 +17,67 @@
import os import os
import math import math
import wx import wx
from wx import glcanvas from wx import glcanvas
import time
import threading
import pyglet import pyglet
pyglet.options['shadow_window'] = False pyglet.options['debug_gl'] = True
pyglet.options['debug_gl'] = False
from pyglet.gl import *
import stltool from pyglet.gl import *
from pyglet import gl
import threading
from . import gcoder
from . import stltool
from .libtatlin import actors
class GLPanel(wx.Panel): class wxGLPanel(wx.Panel):
'''A simple class for using OpenGL with wxPython.''' '''A simple class for using OpenGL with wxPython.'''
def __init__(self, parent, id, pos = wx.DefaultPosition, def __init__(self, parent, id, pos = wx.DefaultPosition,
size = wx.DefaultSize, style = 0): size = wx.DefaultSize, style = 0):
# Forcing a no full repaint to stop flickering # Forcing a no full repaint to stop flickering
style = style | wx.NO_FULL_REPAINT_ON_RESIZE style = style | wx.NO_FULL_REPAINT_ON_RESIZE
#call super function super(wxGLPanel, self).__init__(parent, id, pos, size, style)
super(GLPanel, self).__init__(parent, id, pos, size, style)
#init gl canvas data
self.GLinitialized = False self.GLinitialized = False
attribList = (glcanvas.WX_GL_RGBA, # RGBA attribList = (glcanvas.WX_GL_RGBA, # RGBA
glcanvas.WX_GL_DOUBLEBUFFER, # Double Buffered glcanvas.WX_GL_DOUBLEBUFFER, # Double Buffered
glcanvas.WX_GL_DEPTH_SIZE, 24) # 24 bit glcanvas.WX_GL_DEPTH_SIZE, 24) # 24 bit
# Create the canvas
self.sizer = wx.BoxSizer(wx.HORIZONTAL) self.sizer = wx.BoxSizer(wx.HORIZONTAL)
self.canvas = glcanvas.GLCanvas(self, attribList = attribList) self.canvas = glcanvas.GLCanvas(self, attribList = attribList)
self.context = glcanvas.GLContext(self.canvas)
self.sizer.Add(self.canvas, 1, wx.EXPAND) self.sizer.Add(self.canvas, 1, wx.EXPAND)
self.SetSizer(self.sizer) self.SetSizer(self.sizer)
#self.sizer.Fit(self) self.sizer.Fit(self)
self.Layout()
# bind events # bind events
self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent) self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent)
self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent) self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent)
self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent) self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent)
#==========================================================================
# Canvas Proxy Methods
#==========================================================================
def GetGLExtents(self):
'''Get the extents of the OpenGL canvas.'''
return self.canvas.GetClientSize()
def SwapBuffers(self):
'''Swap the OpenGL buffers.'''
self.canvas.SwapBuffers()
#==========================================================================
# wxPython Window Handlers
#==========================================================================
def processEraseBackgroundEvent(self, event): def processEraseBackgroundEvent(self, event):
'''Process the erase background event.''' '''Process the erase background event.'''
pass # Do nothing, to avoid flashing on MSWin pass # Do nothing, to avoid flashing on MSWin
def processSizeEvent(self, event): def processSizeEvent(self, event):
'''Process the resize event.''' '''Process the resize event.'''
if self.canvas.GetContext(): if (wx.VERSION > (2,9) and self.canvas.IsShownOnScreen()) or self.canvas.GetContext():
# Make sure the frame is shown before calling SetCurrent. # Make sure the frame is shown before calling SetCurrent.
self.Show() size = self.GetClientSize()
self.canvas.SetCurrent()
size = self.GetGLExtents()
self.winsize = (size.width, size.height) self.winsize = (size.width, size.height)
self.width, self.height = size.width, size.height self.width, self.height = size.width, size.height
self.canvas.SetCurrent(self.context)
self.OnReshape(size.width, size.height) self.OnReshape(size.width, size.height)
self.canvas.Refresh(False) self.canvas.Refresh(False)
event.Skip() event.Skip()
#wx.CallAfter(self.Refresh)
def processPaintEvent(self, event): def processPaintEvent(self, event):
'''Process the drawing event.''' '''Process the drawing event.'''
self.canvas.SetCurrent() self.canvas.SetCurrent(self.context)
# This is a 'perfect' time to initialize OpenGL ... only if we need to
if not self.GLinitialized: if not self.GLinitialized:
self.OnInitGL() self.OnInitGL()
self.GLinitialized = True self.GLinitialized = True
...@@ -104,7 +87,7 @@ class GLPanel(wx.Panel): ...@@ -104,7 +87,7 @@ class GLPanel(wx.Panel):
def Destroy(self): def Destroy(self):
#clean up the pyglet OpenGL context #clean up the pyglet OpenGL context
#self.pygletcontext.destroy() self.pygletcontext.destroy()
#call the super method #call the super method
super(wx.Panel, self).Destroy() super(wx.Panel, self).Destroy()
...@@ -116,62 +99,35 @@ class GLPanel(wx.Panel): ...@@ -116,62 +99,35 @@ class GLPanel(wx.Panel):
#create a pyglet context for this panel #create a pyglet context for this panel
self.pmat = (GLdouble * 16)() self.pmat = (GLdouble * 16)()
self.mvmat = (GLdouble * 16)() self.mvmat = (GLdouble * 16)()
self.pygletcontext = Context(current_context) self.pygletcontext = gl.Context(gl.current_context)
self.pygletcontext.canvas = self
self.pygletcontext.set_current() self.pygletcontext.set_current()
self.dist = 1000 self.dist = 1000
self.vpmat = None self.vpmat = None
#normal gl init #normal gl init
glClearColor(0, 0, 0, 1) glClearColor(0.98, 0.98, 0.78, 1)
glColor3f(1, 0, 0) glClearDepth(1.0) # set depth value to 1
glDepthFunc(GL_LEQUAL)
glEnable(GL_COLOR_MATERIAL)
glEnable(GL_DEPTH_TEST) glEnable(GL_DEPTH_TEST)
glEnable(GL_CULL_FACE) glEnable(GL_CULL_FACE)
# Uncomment this line for a wireframe view glEnable(GL_BLEND)
#glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
self.OnReshape(*self.GetClientSize())
# Simple light setup. On Windows GL_LIGHT0 is enabled by default,
# but this is not the case on Linux or Mac, so remember to always
# include it.
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glEnable(GL_LIGHT1)
# Define a simple function to create ctypes arrays of floats:
def vec(*args):
return (GLfloat * len(args))(*args)
glLightfv(GL_LIGHT0, GL_POSITION, vec(.5, .5, 1, 0))
glLightfv(GL_LIGHT0, GL_SPECULAR, vec(.5, .5, 1, 1))
glLightfv(GL_LIGHT0, GL_DIFFUSE, vec(1, 1, 1, 1))
glLightfv(GL_LIGHT1, GL_POSITION, vec(1, 0, .5, 0))
glLightfv(GL_LIGHT1, GL_DIFFUSE, vec(.5, .5, .5, 1))
glLightfv(GL_LIGHT1, GL_SPECULAR, vec(1, 1, 1, 1))
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.5, 0, 0.3, 1))
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, vec(1, 1, 1, 1))
glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 50)
glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, vec(0, 0.1, 0, 0.9))
#create objects to draw
#self.create_objects()
def OnReshape(self, width, height): def OnReshape(self, width, height):
'''Reshape the OpenGL viewport based on the dimensions of the window.''' '''Reshape the OpenGL viewport based on the dimensions of the window.'''
if not self.GLinitialized: if not self.GLinitialized:
self.OnInitGL()
self.GLinitialized = True self.GLinitialized = True
self.pmat = (GLdouble * 16)() self.OnInitGL()
self.mvmat = (GLdouble * 16)()
glViewport(0, 0, width, height) glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION) glMatrixMode(GL_PROJECTION)
glLoadIdentity() glLoadIdentity()
gluPerspective(60., width / float(height), .1, 1000.) gluPerspective(60., width / float(height), .1, 1000.)
glMatrixMode(GL_MODELVIEW) glMatrixMode(GL_MODELVIEW)
glLoadIdentity() glLoadIdentity()
#pyglet stuff
self.vpmat = (GLint * 4)(0, 0, *list(self.GetClientSize())) self.vpmat = (GLint * 4)(0, 0, *list(self.GetClientSize()))
glGetDoublev(GL_PROJECTION_MATRIX, self.pmat) glGetDoublev(GL_PROJECTION_MATRIX, self.pmat)
glGetDoublev(GL_MODELVIEW_MATRIX, self.mvmat)
#glMatrixMode(GL_PROJECTION)
# Wrap text to the width of the window # Wrap text to the width of the window
if self.GLinitialized: if self.GLinitialized:
...@@ -180,14 +136,10 @@ class GLPanel(wx.Panel): ...@@ -180,14 +136,10 @@ class GLPanel(wx.Panel):
def OnDraw(self, *args, **kwargs): def OnDraw(self, *args, **kwargs):
"""Draw the window.""" """Draw the window."""
#clear the context
self.canvas.SetCurrent()
self.pygletcontext.set_current() self.pygletcontext.set_current()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
#draw objects
self.draw_objects() self.draw_objects()
#update screen self.canvas.SwapBuffers()
self.SwapBuffers()
#========================================================================== #==========================================================================
# To be implemented by a sub class # To be implemented by a sub class
...@@ -204,262 +156,8 @@ class GLPanel(wx.Panel): ...@@ -204,262 +156,8 @@ class GLPanel(wx.Panel):
'''called in the middle of ondraw after the buffer has been cleared''' '''called in the middle of ondraw after the buffer has been cleared'''
pass pass
def _dist(dist):
"""return axis length, or 0 if None"""
if dist is None:
return 0
else:
return float(dist)
class gcpoint(object):
"""gcode point
stub for first line"""
def __init__(self, x = 0, y = 0, z = 0, e = 0):
self.x = x
self.y = y
self.z = z
self.e = e
self.length = 0
class gcline(object):
"""gcode move line
Once initialised, it knows its position, length and extrusion ratio
Returns lines into gcview batch()
"""
def __init__(self, x = None, y = None, z = None, e = None, f = None, prev_gcline = None, orgline = False):
if prev_gcline is None:
self.prev_gcline = gcpoint()
else:
self.prev_gcline = prev_gcline
if x is None:
self.x = self.prev_gcline.x
else:
self.x = float(x)
if y is None:
self.y = self.prev_gcline.y
else:
self.y = float(y)
if z is None:
self.z = self.prev_gcline.z
else:
self.z = float(z)
if e is None:
self.e = self.prev_gcline.e
else:
self.e = float(e)
self.f = f
self.orgline = orgline
self.calc_delta()
self.calc_len()
def __str__(self):
return u"line from %s,%s,%s to %s,%s,%s with extrusion ratio %s and feedrate %s\n%s" % (
self.prev_gcline.x,
self.prev_gcline.y,
self.prev_gcline.z,
self.x,
self.y,
self.z,
self.extrusion_ratio,
self.f,
self.orgline,
)
def calc_delta(self, prev_gcline = None):
if prev_gcline is None:
prev_gcline = self.prev_gcline
if self.prev_gcline is not None:
self.dx = self.x - prev_gcline.x
self.dy = self.y - prev_gcline.y
self.dz = self.z - prev_gcline.z
self.de = self.e - prev_gcline.e
else:
self.dx = self.x
self.dy = self.y
self.dz = self.z
self.de = self.e
def calc_len(self):
if self.dz != 0:
self.length = math.sqrt(self.dx**2 + self.dy**2 + self.dz**2)
else:
self.length = math.sqrt(self.dx**2 + self.dy**2)
if self.de:
self.extrusion_ratio = self.length / self.de
else:
self.extrusion_ratio = 0
def glline(self):
return [
self.prev_gcline.x,
self.prev_gcline.y,
self.prev_gcline.z,
self.x,
self.y,
self.z,
]
def glcolor(self, upper_limit = None, lower_limit = 0, max_feedrate = 0):
if self.extrusion_ratio == 0:
return [255, 255, 255, 0, 0, 0]
else:
blue_color = 0
green_color = 0
if upper_limit is not None:
if self.extrusion_ratio <= lower_limit:
blue_color = 0
else:
blue_color = int ((self.extrusion_ratio - lower_limit) / (upper_limit - lower_limit) * 255)
else:
blue_color = 0
if max_feedrate > 0 and self.f > 0:
green_color = int((self.f/max_feedrate) * 255)
if green_color > 255:
green_color = 255
if green_color < 0:
green_color = 0
if blue_color > 255:
blue_color = 255
if blue_color < 0:
blue_color = 0
return[255, green_color, blue_color, 128, green_color, blue_color/4]
def float_from_line(axe, line):
return float(line.split(axe)[1].split(" ")[0])
class gcThreadRenderer(threading.Thread):
def __init__(self, gcview, lines):
threading.Thread.__init__(self)
self.gcview = gcview
self.lines = lines
print "q init"
def run(self):
for line in self.lines:
layer_name = line.z
if line.z not in self.gcview.layers:
self.gcview.layers[line.z] = pyglet.graphics.Batch()
self.gcview.layerlist = self.gcview.layers.keys()
self.gcview.layerlist.sort()
self.gcview.layers[line.z].add(2, GL_LINES, None, ("v3f", line.glline()), ("c3B", line.glcolor(self.gcview.upper_limit, self.gcview.lower_limit, self.gcview.max_feedrate)))
self.gcview.t2 = time.time()
print "Rendered lines in %fs" % (self.gcview.t2-self.gcview.t1)
class gcview(object):
"""gcode visualiser
Holds opengl objects for all layers
"""
def __init__(self, lines, batch, w = 0.5, h = 0.5):
if len(lines) == 0:
return
print "Loading %s lines" % (len(lines))
#End pos of previous mode
self.prev = gcpoint()
# Correction for G92 moves
self.delta = [0, 0, 0, 0]
self.layers = {}
self.t0 = time.time()
self.lastf = 0
lines = [self.transform(i) for i in lines]
lines = [i for i in lines if i is not None]
self.t1 = time.time()
print "transformed %s lines in %fs" % (len(lines), self.t1- self.t0)
self.upper_limit = 0
self.lower_limit = None
self.max_feedrate = 0
for line in lines:
if line.extrusion_ratio and line.length > 0.005: #lines shorter than 0.003 can have large extrusion ratio
if line.extrusion_ratio > self.upper_limit:
self.upper_limit = line.extrusion_ratio
if self.lower_limit is None or line.extrusion_ratio < self.lower_limit:
self.lower_limit = line.extrusion_ratio
if line.f > self.max_feedrate:
self.max_feedrate = line.f
#print upper_limit, lower_limit
#self.render_gl(lines)
q = gcThreadRenderer(self, lines)
q.setDaemon(True)
q.start()
def transform(self, line):
"""transforms line of gcode into gcline object (or None if its not move)
Tracks coordinates across resets in self.delta
"""
orgline = line
line = line.split(";")[0]
cur = [None, None, None, None, None]
if len(line) > 0:
if "G92" in line:
#Recalculate delta on G92 (reset)
if("X" in line):
try:
self.delta[0] = float_from_line("X", line) + self.prev.x
except:
self.delta[0] = 0
if("Y" in line):
try:
self.delta[1] = float_from_line("Y", line) + self.prev.y
except:
self.delta[1] = 0
if("Z" in line):
try:
self.delta[2] = float_from_line("Z", line) + self.prev.z
except:
self.delta[2] = 0
if("E" in line):
try:
self.delta[3] = float_from_line("E", line) + self.prev.e
except:
self.delta[3] = 0
return None
if "G1" in line or "G0" in line:
#Create new gcline
if("X" in line):
cur[0] = float_from_line("X", line) + self.delta[0]
if("Y" in line):
cur[1] = float_from_line("Y", line) + self.delta[1]
if("Z" in line):
cur[2] = float_from_line("Z", line) + self.delta[2]
if("E" in line):
cur[3] = float_from_line("E", line) + self.delta[3]
if "F" in line:
cur[4] = float_from_line("F", line)
if cur == [None, None, None, None, None]:
return None
else:
#print cur
if cur[4] is None:
cur[4] = self.lastf
else:
self.lastf = cur[4]
r = gcline(x = cur[0], y = cur[1], z = cur[2], e = cur[3], f = cur[4], prev_gcline = self.prev, orgline = orgline)
self.prev = r
return r
return None
def delete(self):
#for i in self.vlists:
# i.delete()
#self.vlists = []
pass
def trackball(p1x, p1y, p2x, p2y, r): def trackball(p1x, p1y, p2x, p2y, r):
TRACKBALLSIZE = r TRACKBALLSIZE = r
#float a[3]; /* Axis of rotation */ #float a[3]; /* Axis of rotation */
#float phi; /* how much to rotate about axis */ #float phi; /* how much to rotate about axis */
#float p1[3], p2[3], d[3]; #float p1[3], p2[3], d[3];
...@@ -537,80 +235,83 @@ def mulquat(q1, rq): ...@@ -537,80 +235,83 @@ def mulquat(q1, rq):
q1[3] * rq[3] - q1[0] * rq[0] - q1[1] * rq[1] - q1[2] * rq[2]] q1[3] * rq[3] - q1[0] * rq[0] - q1[1] * rq[1] - q1[2] * rq[2]]
class TestGlPanel(GLPanel): class GcodeViewPanel(wxGLPanel):
def __init__(self, parent, size, id = wx.ID_ANY): def __init__(self, parent, id = wx.ID_ANY, build_dimensions = None, realparent = None):
super(TestGlPanel, self).__init__(parent, id, wx.DefaultPosition, size, 0) super(GcodeViewPanel, self).__init__(parent, id, wx.DefaultPosition, wx.DefaultSize, 0)
self.batches = [] self.batches = []
self.rot = 0 self.rot = 0
self.canvas.Bind(wx.EVT_MOUSE_EVENTS, self.move) self.canvas.Bind(wx.EVT_MOUSE_EVENTS, self.move)
self.canvas.Bind(wx.EVT_LEFT_DCLICK, self.double) self.canvas.Bind(wx.EVT_LEFT_DCLICK, self.double)
self.initialized = 1 self.canvas.Bind(wx.EVT_KEY_DOWN, self.keypress)
self.initialized = 0
self.canvas.Bind(wx.EVT_MOUSEWHEEL, self.wheel) self.canvas.Bind(wx.EVT_MOUSEWHEEL, self.wheel)
self.parent = parent self.parent = realparent if realparent else parent
self.initpos = None self.initpos = None
if build_dimensions:
self.dist = max(build_dimensions[0], build_dimensions[1])
else:
self.dist = 200 self.dist = 200
self.bedsize = [200, 200]
self.transv = [0, 0, -self.dist] self.transv = [0, 0, -self.dist]
self.basequat = [0, 0, 0, 1] self.basequat = [0, 0, 0, 1]
wx.CallAfter(self.forceresize)
self.mousepos = [0, 0] self.mousepos = [0, 0]
def double(self, event): def create_objects(self):
p = event.GetPositionTuple() '''create opengl objects when opengl is initialized'''
sz = self.GetClientSize() for obj in self.parent.objects:
v = map(lambda m, w, b: b * m / w, p, sz, self.bedsize) if obj.model and obj.model.loaded and not obj.model.initialized:
v[1] = self.bedsize[1] - v[1] obj.model.init()
v += [300]
print v def update_object_resize(self):
self.add_file("../prusa/metric-prusa/x-end-idler.stl", v) '''called when the window recieves only if opengl is initialized'''
pass
def forceresize(self):
self.SetClientSize((self.GetClientSize()[0], self.GetClientSize()[1] + 1))
self.SetClientSize((self.GetClientSize()[0], self.GetClientSize()[1] - 1))
threading.Thread(target = self.update).start()
self.initialized = 0
def move_shape(self, delta): def draw_objects(self):
"""moves shape (selected in l, which is list ListBox of shapes) '''called in the middle of ondraw after the buffer has been cleared'''
by an offset specified in tuple delta. if self.vpmat is None:
Positive numbers move to (rigt, down)""" return
name = self.parent.l.GetSelection() self.create_objects()
if name == wx.NOT_FOUND:
return False if self.rot == 1:
glLoadIdentity()
name = self.parent.l.GetString(name) glMultMatrixd(self.mvmat)
else:
model = self.parent.models[name] glLoadIdentity()
model.offsets = [ glTranslatef(*self.transv)
model.offsets[0] + delta[0],
model.offsets[1] + delta[1], glPushMatrix()
model.offsets[2] glTranslatef(-self.parent.platform.width/2, -self.parent.platform.depth/2, 0)
]
self.Refresh() for obj in self.parent.objects:
return True if not obj.model or not obj.model.loaded or not obj.model.initialized:
continue
glPushMatrix()
glTranslatef(*(obj.offsets))
glRotatef(obj.rot, 0.0, 0.0, 1.0)
glScalef(*obj.scale)
obj.model.display()
glPopMatrix()
glPopMatrix()
def double(self, event):
if self.parent.clickcb:
self.parent.clickcb(event)
def move(self, event): def move(self, event):
"""react to mouse actions: """react to mouse actions:
no mouse: show red mousedrop no mouse: show red mousedrop
LMB: move active object, LMB: rotate viewport
with shift rotate viewport RMB: move viewport
RMB: nothing
with shift move viewport
""" """
if event.Entering():
self.canvas.SetFocus()
event.Skip()
return
if event.Dragging() and event.LeftIsDown(): if event.Dragging() and event.LeftIsDown():
if self.initpos == None: if self.initpos == None:
self.initpos = event.GetPositionTuple() self.initpos = event.GetPositionTuple()
else: else:
if not event.ShiftDown():
currentpos = event.GetPositionTuple()
delta = (
(currentpos[0] - self.initpos[0]),
-(currentpos[1] - self.initpos[1])
)
self.move_shape(delta)
self.initpos = None
return
#print self.initpos #print self.initpos
p1 = self.initpos p1 = self.initpos
self.initpos = None self.initpos = None
...@@ -642,371 +343,225 @@ class TestGlPanel(GLPanel): ...@@ -642,371 +343,225 @@ class TestGlPanel(GLPanel):
if self.initpos is not None: if self.initpos is not None:
self.initpos = None self.initpos = None
elif event.Dragging() and event.RightIsDown() and event.ShiftDown(): elif event.Dragging() and event.RightIsDown():
if self.initpos is None: if self.initpos is None:
self.initpos = event.GetPositionTuple() self.initpos = event.GetPositionTuple()
else: else:
p1 = self.initpos p1 = self.initpos
p2 = event.GetPositionTuple() p2 = event.GetPositionTuple()
sz = self.GetClientSize() sz = self.GetClientSize()
p1 = list(p1) p1 = list(p1) + [0]
p2 = list(p2) p2 = list(p2) + [0]
p1[1] *= -1 p1[1] *= -1
p2[1] *= -1 p2[1] *= -1
sz = list(sz) + [1]
sz[0] *= 2
sz[1] *= 2
self.transv = map(lambda x, y, z, c: c - self.dist * (x - y) / z, list(p1) + [0], list(p2) + [0], list(sz) + [1], self.transv) self.transv = map(lambda x, y, z, c: c - self.dist * (x - y) / z, p1, p2, sz, self.transv)
glLoadIdentity() glLoadIdentity()
glTranslatef(self.transv[0], self.transv[1], 0) glTranslatef(self.transv[0], self.transv[1], 0)
glTranslatef(0, 0, self.transv[2]) glTranslatef(0, 0, self.transv[2])
if(self.rot): if self.rot:
glMultMatrixd(build_rotmatrix(self.basequat)) glMultMatrixd(build_rotmatrix(self.basequat))
glGetDoublev(GL_MODELVIEW_MATRIX, self.mvmat) glGetDoublev(GL_MODELVIEW_MATRIX, self.mvmat)
self.rot = 1 self.rot = 1
self.initpos = None self.initpos = None
else: else:
#mouse is moving without a button press event.Skip()
p = event.GetPositionTuple() return
sz = self.GetClientSize() event.Skip()
v = map(lambda m, w, b: b * m / w, p, sz, self.bedsize) wx.CallAfter(self.Refresh)
v[1] = self.bedsize[1] - v[1]
self.mousepos = v
def rotate_shape(self, angle): def layerup(self):
"""rotates acive shape if not self.parent.model:
positive angle is clockwise return
""" max_layers = self.parent.model.max_layers
name = self.parent.l.GetSelection() current_layer = self.parent.model.num_layers_to_draw
if name == wx.NOT_FOUND: new_layer = min(max_layers, current_layer + 1)
return False self.parent.model.num_layers_to_draw = new_layer
name = self.parent.l.GetString(name) wx.CallAfter(self.Refresh)
model = self.parent.models[name]
model.rot += angle def layerdown(self):
if not self.parent.model:
return
current_layer = self.parent.model.num_layers_to_draw
new_layer = max(1, current_layer - 1)
self.parent.model.num_layers_to_draw = new_layer
wx.CallAfter(self.Refresh)
def zoom(self, dist):
self.transv[2] += dist
glLoadIdentity()
glTranslatef(*self.transv)
if self.rot:
glMultMatrixd(build_rotmatrix(self.basequat))
glGetDoublev(GL_MODELVIEW_MATRIX, self.mvmat)
self.rot = 1
wx.CallAfter(self.Refresh)
def wheel(self, event): def wheel(self, event):
"""react to mouse wheel actions: """react to mouse wheel actions:
rotate object without shift: set max layer
with shift zoom viewport with shift: zoom viewport
""" """
z = event.GetWheelRotation() z = event.GetWheelRotation()
angle = 10 dist = 10
if not event.ShiftDown(): if event.ShiftDown():
i = self.parent.l.GetSelection() if not self.parent.model:
if i < 0:
try:
self.parent.setlayerindex(z)
except:
pass
return return
if z > 0: if z > 0:
self.rotate_shape(angle / 2) self.layerup()
else: else:
self.rotate_shape(-angle / 2) self.layerdown()
return return
if z > 0: if z > 0:
self.transv[2] += angle self.zoom(dist)
else: else:
self.transv[2] -= angle self.zoom(-dist)
glLoadIdentity()
glTranslatef(*self.transv)
if(self.rot):
glMultMatrixd(build_rotmatrix(self.basequat))
glGetDoublev(GL_MODELVIEW_MATRIX, self.mvmat)
self.rot = 1
def keypress(self, event): def keypress(self, event):
"""gets keypress events and moves/rotates acive shape""" """gets keypress events and moves/rotates acive shape"""
keycode = event.GetKeyCode() keycode = event.GetKeyCode()
print keycode step = 10
step = 5
angle = 18
if event.ControlDown(): if event.ControlDown():
step = 1 step = 3
angle = 1 kup = [85, 315] # Up keys
#h kdo = [68, 317] # Down Keys
if keycode == 72: kzi = [wx.WXK_PAGEDOWN, 388, 316, 61] # Zoom In Keys
self.move_shape((-step, 0)) kzo = [wx.WXK_PAGEUP, 390, 314, 45] # Zoom Out Keys
#l x = event.GetKeyCode()
if keycode == 76: if x in kup:
self.move_shape((step, 0)) self.layerup()
#j if x in kdo:
if keycode == 75: self.layerdown()
self.move_shape((0, step)) if x in kzi:
#k self.zoom(step)
if keycode == 74: if x in kzo:
self.move_shape((0, -step)) self.zoom(-step)
#[
if keycode == 91:
self.rotate_shape(-angle)
#]
if keycode == 93:
self.rotate_shape(angle)
event.Skip() event.Skip()
def update(self):
while(1):
dt = 0.05
time.sleep(0.05)
try:
wx.CallAfter(self.Refresh) wx.CallAfter(self.Refresh)
except:
return
def anim(self, obj):
g = 50 * 9.8
v = 20
dt = 0.05
basepos = obj.offsets[2]
obj.offsets[2] += obj.animoffset
while obj.offsets[2] > -1:
time.sleep(dt)
obj.offsets[2] -= v * dt
v += g * dt
if(obj.offsets[2] < 0):
obj.scale[2] *= 1 - 3 * dt
#return
v = v / 4
while obj.offsets[2] < basepos:
time.sleep(dt)
obj.offsets[2] += v * dt
v -= g * dt
obj.scale[2] *= 1 + 5 * dt
obj.scale[2] = 1.0
def create_objects(self): class GCObject(object):
'''create opengl objects when opengl is initialized'''
self.initialized = 1
wx.CallAfter(self.Refresh)
def drawmodel(self, m, n): def __init__(self, model):
batch = pyglet.graphics.Batch() self.offsets = [0, 0, 0]
stl = stlview(m.facets, batch = batch) self.rot = 0
m.batch = batch self.curlayer = 0.0
m.animoffset = 300 self.scale = [1.0, 1.0, 1.0]
#print m self.batch = pyglet.graphics.Batch()
#threading.Thread(target = self.anim, args = (m, )).start() self.model = model
wx.CallAfter(self.Refresh)
class GcodeViewMainWrapper(object):
def update_object_resize(self):
'''called when the window recieves only if opengl is initialized''' def __init__(self, parent, build_dimensions):
self.glpanel = GcodeViewPanel(parent, realparent = self, build_dimensions = build_dimensions)
self.glpanel.SetMinSize((150, 150))
self.clickcb = None
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)
self.model = None
self.objects = [GCObject(self.platform), GCObject(None)]
def __getattr__(self, name):
return getattr(self.glpanel, name)
def set_current_gline(self, gline):
if gline.is_move and self.model and self.model.loaded:
self.model.printed_until = gline.gcview_end_vertex
if not self.refresh_timer.IsRunning():
self.refresh_timer.Start()
def addgcode(self, *a):
pass pass
def draw_objects(self): def setlayer(self, *a):
'''called in the middle of ondraw after the buffer has been cleared''' pass
if self.vpmat is None:
return
if not self.initialized:
self.create_objects()
#glLoadIdentity()
#print list(self.pmat)
if self.rot == 1:
glLoadIdentity()
glMultMatrixd(self.mvmat)
else:
glLoadIdentity()
glTranslatef(*self.transv)
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.2, 0.2, 0.2, 1))
glBegin(GL_LINES)
glNormal3f(0, 0, 1)
rows = 10
cols = 10
zheight = 50
for i in xrange(-rows, rows + 1):
if i % 5 == 0:
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.6, 0.6, 0.6, 1))
else:
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.2, 0.2, 0.2, 1))
glVertex3f(10 * -cols, 10 * i, 0)
glVertex3f(10 * cols, 10 * i, 0)
for i in xrange(-cols, cols + 1):
if i % 5 == 0:
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.6, 0.6, 0.6, 1))
else:
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.2, 0.2, 0.2, 1))
glVertex3f(10 * i, 10 * -rows, 0)
glVertex3f(10 * i, 10 * rows, 0)
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.6, 0.6, 0.6, 1))
glVertex3f(10 * -cols, 10 * -rows, 0)
glVertex3f(10 * -cols, 10 * -rows, zheight)
glVertex3f(10 * cols, 10 * rows, 0)
glVertex3f(10 * cols, 10 * rows, zheight)
glVertex3f(10 * cols, 10 * -rows, 0)
glVertex3f(10 * cols, 10 * -rows, zheight)
glVertex3f(10 * -cols, 10 * rows, 0)
glVertex3f(10 * -cols, 10 * rows, zheight)
glVertex3f(10 * -cols, 10 * rows, zheight)
glVertex3f(10 * cols, 10 * rows, zheight)
glVertex3f(10 * cols, 10 * rows, zheight)
glVertex3f(10 * cols, 10 * -rows, zheight)
glVertex3f(10 * cols, 10 * -rows, zheight)
glVertex3f(10 * -cols, 10 * -rows, zheight)
glVertex3f(10 * -cols, 10 * -rows, zheight)
glVertex3f(10 * -cols, 10 * rows, zheight)
glEnd()
glPushMatrix()
glTranslatef(self.mousepos[0] - self.bedsize[0] / 2, self.mousepos[1] - self.bedsize[1] / 2, 0)
glBegin(GL_TRIANGLES)
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(1, 0, 0, 1))
glNormal3f(0, 0, 1)
glVertex3f(2, 2, 0)
glVertex3f(-2, 2, 0)
glVertex3f(-2, -2, 0)
glVertex3f(2, -2, 0)
glVertex3f(2, 2, 0)
glVertex3f(-2, -2, 0)
glEnd()
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.3, 0.7, 0.5, 1))
#glTranslatef(0, 40, 0)
glPopMatrix()
glPushMatrix()
glTranslatef(-100, -100, 0)
glEnable(GL_LINE_SMOOTH)
glEnable(GL_BLEND)
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glHint (GL_LINE_SMOOTH_HINT, GL_NICEST)
glLineWidth (1.5)
for i in self.parent.models.values():
glPushMatrix()
glTranslatef(*(i.offsets))
glRotatef(i.rot, 0.0, 0.0, 1.0)
glScalef(*i.scale)
#glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, vec(0.93, 0.37, 0.25, 1))
glEnable(GL_COLOR_MATERIAL)
if i.curlayer == -1:
# curlayer == -1 means we are over the top.
glLineWidth (0.8)
[i.gc.layers[j].draw() for j in i.gc.layerlist]
else:
glLineWidth (0.6)
tmpindex = i.gc.layerlist.index(i.curlayer)
if tmpindex >= 5:
thin_layer = i.gc.layerlist[tmpindex - 5]
[i.gc.layers[j].draw() for j in i.gc.layerlist if j <= thin_layer]
if tmpindex > 4:
glLineWidth (0.9)
i.gc.layers[i.gc.layerlist[tmpindex - 4]].draw()
if tmpindex > 3:
glLineWidth (1.1)
i.gc.layers[i.gc.layerlist[tmpindex - 3]].draw()
if tmpindex > 2:
glLineWidth (1.3)
i.gc.layers[i.gc.layerlist[tmpindex - 2]].draw()
if tmpindex > 1:
glLineWidth (2.2)
i.gc.layers[i.gc.layerlist[tmpindex - 1]].draw()
glLineWidth (3.5)
i.gc.layers[i.curlayer].draw()
glLineWidth (1.5)
glDisable(GL_COLOR_MATERIAL)
glPopMatrix() def addfile(self, gcode = None):
glPopMatrix() self.model = actors.GcodeModel()
#print "drawn batch" if gcode:
self.model.load_data(gcode)
self.objects[-1].model = self.model
wx.CallAfter(self.Refresh)
def clear(self):
self.model = None
self.objects[-1].model = None
wx.CallAfter(self.Refresh)
class GCFrame(wx.Frame): class GcodeViewFrame(wx.Frame):
'''A simple class for using OpenGL with wxPython.''' '''A simple class for using OpenGL with wxPython.'''
def __init__(self, parent, ID, title, pos = wx.DefaultPosition, def __init__(self, parent, ID, title, build_dimensions, objects = None,
size = wx.DefaultSize, style = wx.DEFAULT_FRAME_STYLE): pos = wx.DefaultPosition, size = wx.DefaultSize,
super(GCFrame, self).__init__(parent, ID, title, pos, (size[0] + 150, size[1]), style) style = wx.DEFAULT_FRAME_STYLE):
super(GcodeViewFrame, self).__init__(parent, ID, title, pos, size, style)
class d: self.refresh_timer = wx.CallLater(100, self.Refresh)
def GetSelection(self): self.p = self # Hack for backwards compatibility with gviz API
return wx.NOT_FOUND self.clonefrom = objects
self.p = self self.platform = actors.Platform(build_dimensions)
m = d() if objects:
m.offsets = [0, 0, 0] self.model = objects[1].model
m.rot = 0
m.curlayer = -1
m.scale = [1.0, 1.0, 1.0]
m.batch = pyglet.graphics.Batch()
m.gc = gcview([], batch = m.batch)
self.models = {"GCODE": m}
self.l = d()
self.modelindex = 0
self.GLPanel1 = TestGlPanel(self, size)
def addfile(self, gcode = []):
self.models["GCODE"].gc.delete()
self.models["GCODE"].gc = gcview(gcode, batch = self.models["GCODE"].batch)
self.setlayerindex(None)
def clear(self):
self.models["GCODE"].gc.delete()
self.models["GCODE"].gc = gcview([], batch = self.models["GCODE"].batch)
def Show(self, arg = True):
wx.Frame.Show(self, arg)
self.SetClientSize((self.GetClientSize()[0], self.GetClientSize()[1] + 1))
self.SetClientSize((self.GetClientSize()[0], self.GetClientSize()[1] - 1))
self.Refresh()
wx.FutureCall(500, self.GLPanel1.forceresize)
#threading.Thread(target = self.update).start()
#self.initialized = 0
def setlayerindex(self, z):
m = self.models["GCODE"]
try:
mlk = m.gc.layerlist
except:
mlk = []
if z is None:
self.modelindex = -1
elif z > 0:
if self.modelindex < len(mlk) - 1:
if self.modelindex > -1:
self.modelindex += 1
else: else:
self.modelindex = -1 self.model = None
elif z < 0: self.objects = [GCObject(self.platform), GCObject(None)]
if self.modelindex > 0: self.glpanel = GcodeViewPanel(self, build_dimensions = build_dimensions)
self.modelindex -= 1
elif self.modelindex == -1: def set_current_gline(self, gline):
self.modelindex = len(mlk) if gline.is_move and self.model and self.model.loaded:
self.model.printed_until = gline.gcview_end_vertex
if self.modelindex >= 0: if not self.refresh_timer.IsRunning():
m.curlayer = mlk[self.modelindex] self.refresh_timer.Start()
wx.CallAfter(self.SetTitle, "Gcode view, shift to move. Layer %d/%d, Z = %f" % (self.modelindex, len(mlk), m.curlayer))
def addfile(self, gcode = None):
if self.clonefrom:
self.model = self.clonefrom[-1].model.copy()
else: else:
m.curlayer = -1 self.model = actors.GcodeModel()
wx.CallAfter(self.SetTitle, "Gcode view, shift to move view, mousewheel to set layer") if gcode:
self.model.load_data(gcode)
self.objects[-1].model = self.model
wx.CallAfter(self.Refresh)
def clear(self):
self.model = None
self.objects[-1].model = None
wx.CallAfter(self.Refresh)
def main(): if __name__ == "__main__":
app = wx.App(redirect = False)
frame = GCFrame(None, wx.ID_ANY, 'Gcode view, shift to move view, mousewheel to set layer', size = (400, 400))
import sys import sys
for filename in sys.argv: app = wx.App(redirect = False)
if ".gcode" in filename: build_dimensions = [200, 200, 100, 0, 0, 0]
frame.addfile(list(open(filename))) frame = GcodeViewFrame(None, wx.ID_ANY, 'Gcode view, shift to move view, mousewheel to set layer', size = (400, 400), build_dimensions = build_dimensions)
elif ".stl" in filename: gcode = gcoder.GCode(open(sys.argv[1]))
#TODO: add stl here frame.addfile(gcode)
pass
first_move = None
for i in range(len(gcode.lines)):
if gcode.lines[i].is_move:
first_move = gcode.lines[i]
break
last_move = None
for i in range(len(gcode.lines)-1,-1,-1):
if gcode.lines[i].is_move:
last_move = gcode.lines[i]
break
nsteps = 20
steptime = 500
lines = [first_move] + [gcode.lines[int(float(i)*(len(gcode.lines)-1)/nsteps)] for i in range(1, nsteps)] + [last_move]
current_line = 0
def setLine():
global current_line
frame.set_current_gline(lines[current_line])
current_line = (current_line + 1) % len(lines)
timer.Start()
timer = wx.CallLater(steptime, setLine)
timer.Start()
#frame = wx.Frame(None, -1, "GL Window", size = (400, 400))
#panel = TestGlPanel(frame, size = (300, 300))
frame.Show(True) frame.Show(True)
app.MainLoop() app.MainLoop()
app.Destroy() app.Destroy()
if __name__ == "__main__":
#import cProfile
#print cProfile.run("main()")
main()
...@@ -16,21 +16,19 @@ ...@@ -16,21 +16,19 @@
# along with Printrun. If not, see <http://www.gnu.org/licenses/>. # along with Printrun. If not, see <http://www.gnu.org/licenses/>.
import wx, random import wx, random
from math import log10, floor, ceil
from bufferedcanvas import * from bufferedcanvas import *
class Graph(BufferedCanvas): class Graph(BufferedCanvas):
'''A class to show a Graph with Pronterface.''' '''A class to show a Graph with Pronterface.'''
def __init__(self, parent, id, pos = wx.DefaultPosition, def __init__(self, parent, id, root, pos = wx.DefaultPosition,
size = wx.DefaultSize, style = 0): size = wx.Size(150, 80), style = 0):
# Forcing a no full repaint to stop flickering # Forcing a no full repaint to stop flickering
style = style | wx.NO_FULL_REPAINT_ON_RESIZE style = style | wx.NO_FULL_REPAINT_ON_RESIZE
#call super function super(Graph, self).__init__(parent, id, pos, size, style)
#super(Graph, self).__init__(parent, id, pos, size, style) self.root = root
BufferedCanvas.__init__(self, parent, id)
self.SetSize(wx.Size(150, 80))
self.extruder0temps = [0] self.extruder0temps = [0]
self.extruder0targettemps = [0] self.extruder0targettemps = [0]
...@@ -42,34 +40,31 @@ class Graph(BufferedCanvas): ...@@ -42,34 +40,31 @@ class Graph(BufferedCanvas):
self.timer = wx.Timer(self) self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.updateTemperatures, self.timer) self.Bind(wx.EVT_TIMER, self.updateTemperatures, self.timer)
self.minyvalue = 0
self.maxyvalue = 250 self.maxyvalue = 250
self.rescaley = True # should the Y axis be rescaled dynamically?
if self.rescaley:
self._ybounds = Graph._YBounds(self)
#If rescaley is set then ybars gives merely an estimate
#Note that "bars" actually indicate the number of grid _intervals_
self.ybars = 5 self.ybars = 5
self.xbars = 6 # One bar per 10 second self.xbars = 6 # One bar per 10 second
self.xsteps = 60 # Covering 1 minute in the graph self.xsteps = 60 # Covering 1 minute in the graph
self.y_offset = 1 # This is to show the line even when value is 0 and maxyvalue
self._lastyvalue = 0
#self.sizer = wx.BoxSizer(wx.HORIZONTAL)
#self.sizer.Add(wx.Button(self, -1, "Button1", (0, 0)))
#self.SetSizer(self.sizer)
def OnPaint(self, evt): def OnPaint(self, evt):
dc = wx.PaintDC(self) dc = wx.PaintDC(self)
gc = wx.GraphicsContext.Create(dc) gc = wx.GraphicsContext.Create(dc)
def Destroy(self):
#call the super method
super(wx.Panel, self).Destroy()
def updateTemperatures(self, event): def updateTemperatures(self, event):
self.AddBedTemperature(self.bedtemps[-1]) self.AddBedTemperature(self.bedtemps[-1])
self.AddBedTargetTemperature(self.bedtargettemps[-1]) self.AddBedTargetTemperature(self.bedtargettemps[-1])
self.AddExtruder0Temperature(self.extruder0temps[-1]) self.AddExtruder0Temperature(self.extruder0temps[-1])
self.AddExtruder0TargetTemperature(self.extruder0targettemps[-1]) self.AddExtruder0TargetTemperature(self.extruder0targettemps[-1])
#self.AddExtruder1Temperature(self.extruder1temps[-1]) self.AddExtruder1Temperature(self.extruder1temps[-1])
#self.AddExtruder1TargetTemperature(self.extruder1targettemps[-1]) self.AddExtruder1TargetTemperature(self.extruder1targettemps[-1])
if self.rescaley:
self._ybounds.update()
self.Refresh() self.Refresh()
def drawgrid(self, dc, gc): def drawgrid(self, dc, gc):
...@@ -79,39 +74,35 @@ class Graph(BufferedCanvas): ...@@ -79,39 +74,35 @@ class Graph(BufferedCanvas):
#b = gc.CreateLinearGradientBrush(0, 0, w, h, col1, col2) #b = gc.CreateLinearGradientBrush(0, 0, w, h, col1, col2)
gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 4)) gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1))
#gc.SetBrush(gc.CreateBrush(wx.Brush(wx.Colour(245, 245, 255, 252))))
#gc.SetBrush(b)
gc.DrawRectangle(0, 0, self.width, self.height)
#gc.SetBrush(wx.Brush(wx.Colour(245, 245, 255, 52))) #gc.SetBrush(wx.Brush(wx.Colour(245, 245, 255, 52)))
#gc.SetBrush(gc.CreateBrush(wx.Brush(wx.Colour(0, 0, 0, 255)))) #gc.SetBrush(gc.CreateBrush(wx.Brush(wx.Colour(0, 0, 0, 255))))
#gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 4)) gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 255), 1))
#gc.DrawLines(wx.Point(0, 0), wx.Point(50, 10)) #gc.DrawLines(wx.Point(0, 0), wx.Point(50, 10))
#path = gc.CreatePath()
#path.MoveToPoint(0.0, 0.0)
#path.AddLineToPoint(0.0, 100.0)
#path.AddLineToPoint(100.0, 0.0)
#path.AddCircle( 50.0, 50.0, 50.0 )
#path.CloseSubpath()
#gc.DrawPath(path)
#gc.StrokePath(path)
font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD) font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD)
gc.SetFont(font, wx.Colour(23, 44, 44)) gc.SetFont(font, wx.Colour(23, 44, 44))
# draw vertical bars
dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1)) dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1))
for x in range(self.xbars): for x in range(self.xbars+1):
dc.DrawLine(x*(float(self.width)/self.xbars), 0, x*(float(self.width)/self.xbars), self.height) dc.DrawLine(x*(float(self.width-1)/(self.xbars-1)), 0, x*(float(self.width-1)/(self.xbars-1)), self.height)
# draw horizontal bars
spacing = self._calculate_spacing() #spacing between bars, in degrees
yspan = self.maxyvalue-self.minyvalue
ybars = int(yspan/spacing) #Should be close to self.ybars
firstbar = int(ceil(self.minyvalue/spacing)) #in degrees
dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1)) dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1))
for y in range(self.ybars): for y in xrange(firstbar,firstbar+ybars+1):
y_pos = y*(float(self.height)/self.ybars) #y_pos = y*(float(self.height)/self.ybars)
degrees = y*spacing
y_pos = self._y_pos(degrees)
dc.DrawLine(0, y_pos, self.width, y_pos) dc.DrawLine(0, y_pos, self.width, y_pos)
gc.DrawText(unicode(int(self.maxyvalue - (y * (self.maxyvalue/self.ybars)))), 1, y_pos - (font.GetPointSize() / 2)) gc.DrawText(unicode(y*spacing), 1, y_pos - (font.GetPointSize() / 2))
if self.timer.IsRunning() == False: if self.timer.IsRunning() == False:
font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.BOLD) font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.BOLD)
...@@ -124,6 +115,33 @@ class Graph(BufferedCanvas): ...@@ -124,6 +115,33 @@ class Graph(BufferedCanvas):
#gc.DrawLines([[20, 30], [10, 53]]) #gc.DrawLines([[20, 30], [10, 53]])
#dc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1)) #dc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1))
def _y_pos(self,temperature):
"""Converts a temperature, in degrees, to a pixel position"""
#fraction of the screen from the bottom
frac = float(temperature-self.minyvalue)/(self.maxyvalue-self.minyvalue)
return int( (1.0-frac)*(self.height-1) )
def _calculate_spacing(self):
# Allow grids of spacings 1,2.5,5,10,25,50,100,etc
yspan = float(self.maxyvalue-self.minyvalue)
log_yspan = log10( yspan/self.ybars )
exponent = int( floor(log_yspan) )
#calculate boundary points between allowed spacings
log1_25 = log10(2)+log10(1)+log10(2.5)-log10(1+2.5)
log25_5 = log10(2)+log10(2.5)+log10(5)-log10(2.5+5)
log5_10 = log10(2)+log10(5)+log10(10)-log10(5+10)
if log_yspan-exponent < log1_25:
return 10**exponent
elif log1_25 <= log_yspan-exponent < log25_5:
return 25*10**(exponent-1)
elif log25_5 <= log_yspan-exponent < log5_10:
return 5*10**exponent
else:
return 10**(exponent+1)
def drawtemperature(self, dc, gc, temperature_list, text, text_xoffset, r, g, b, a): def drawtemperature(self, dc, gc, temperature_list, text, text_xoffset, r, g, b, a):
if self.timer.IsRunning() == False: if self.timer.IsRunning() == False:
dc.SetPen(wx.Pen(wx.Colour(128, 128, 128, 128), 1)) dc.SetPen(wx.Pen(wx.Colour(128, 128, 128, 128), 1))
...@@ -131,17 +149,18 @@ class Graph(BufferedCanvas): ...@@ -131,17 +149,18 @@ class Graph(BufferedCanvas):
dc.SetPen(wx.Pen(wx.Colour(r, g, b, a), 1)) dc.SetPen(wx.Pen(wx.Colour(r, g, b, a), 1))
x_add = float(self.width)/self.xsteps x_add = float(self.width)/self.xsteps
x_pos = float(0.0) x_pos = 0.0
lastxvalue = float(0.0) lastxvalue = 0.0
lastyvalue = temperature_list[-1]
for temperature in (temperature_list): for temperature in (temperature_list):
y_pos = int((float(self.height-self.y_offset)/self.maxyvalue)*temperature) + self.y_offset y_pos = self._y_pos(temperature)
if (x_pos > 0.0): # One need 2 points to draw a line. if (x_pos > 0.0): # One need 2 points to draw a line.
dc.DrawLine(lastxvalue, self.height-self._lastyvalue, x_pos, self.height-y_pos) dc.DrawLine(lastxvalue, lastyvalue, x_pos, y_pos)
lastxvalue = x_pos lastxvalue = x_pos
x_pos = float(x_pos) + x_add x_pos = float(x_pos) + x_add
self._lastyvalue = y_pos lastyvalue = y_pos
if len(text) > 0: if len(text) > 0:
font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD) font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD)
...@@ -151,9 +170,7 @@ class Graph(BufferedCanvas): ...@@ -151,9 +170,7 @@ class Graph(BufferedCanvas):
else: else:
gc.SetFont(font, wx.Colour(r, g, b)) gc.SetFont(font, wx.Colour(r, g, b))
#gc.DrawText(text, self.width - (font.GetPointSize() * ((len(text) * text_xoffset + 1))), self.height - self._lastyvalue - (font.GetPointSize() / 2)) gc.DrawText(text, x_pos - x_add - (font.GetPointSize() * ((len(text) * text_xoffset + 1))), lastyvalue - (font.GetPointSize() / 2))
gc.DrawText(text, x_pos - x_add - (font.GetPointSize() * ((len(text) * text_xoffset + 1))), self.height - self._lastyvalue - (font.GetPointSize() / 2))
#gc.DrawText(text, self.width - (font.GetPixelSize().GetWidth() * ((len(text) * text_xoffset + 1) + 1)), self.height - self._lastyvalue - (font.GetPointSize() / 2))
def drawbedtemp(self, dc, gc): def drawbedtemp(self, dc, gc):
...@@ -240,6 +257,7 @@ class Graph(BufferedCanvas): ...@@ -240,6 +257,7 @@ class Graph(BufferedCanvas):
self.Refresh() self.Refresh()
def draw(self, dc, w, h): def draw(self, dc, w, h):
dc.SetBackground(wx.Brush(self.root.settings.bgcolor))
dc.Clear() dc.Clear()
gc = wx.GraphicsContext.Create(dc) gc = wx.GraphicsContext.Create(dc)
self.width = w self.width = w
...@@ -251,3 +269,110 @@ class Graph(BufferedCanvas): ...@@ -251,3 +269,110 @@ class Graph(BufferedCanvas):
self.drawextruder0temp(dc, gc) self.drawextruder0temp(dc, gc)
self.drawextruder1targettemp(dc, gc) self.drawextruder1targettemp(dc, gc)
self.drawextruder1temp(dc, gc) self.drawextruder1temp(dc, gc)
class _YBounds(object):
"""Small helper class to claculate y bounds dynamically"""
def __init__(self, graph, minimum_scale=5.0,buffer=0.10):
"""_YBounds(Graph,float,float)
graph parent object to calculate scales for
minimum_scale minimum range to show on the graph
buffer amount of padding to add above & below the
displayed temperatures. Given as a fraction of the
total range. (Eg .05 to use 90% of the range for
temperatures)
"""
self.graph = graph
self.min_scale = minimum_scale
self.buffer = buffer
# Frequency to rescale the graph
self.update_freq = 10
self._last_update = self.update_freq #number of updates since last full refresh
def update(self,forceUpdate=False):
"""Updates graph.minyvalue and graph.maxyvalue based on current temperatures
"""
self._last_update += 1
#TODO Smart update. Only do full calculation every 10s. Otherwise, just look at current graph & expand if necessary
if forceUpdate or self._last_update >= self.update_freq:
self.graph.minyvalue, self.graph.maxyvalue = self.getBounds()
self._last_update = 0
else:
self.graph.minyvalue, self.graph.maxyvalue = self.getBoundsQuick()
def getBounds(self):
"""
Calculates the bounds based on the current temperatures
Rules:
* Include the full extruder0 history
* Include the current target temp (but not necessarily old settings)
* Include the extruder1 and/or bed temp if
1) The target temp is >0
2) The history has ever been above 5
* Include at least min_scale
* Include at least buffer above & below the extreme temps
"""
extruder0_min = min(self.graph.extruder0temps)
extruder0_max = max(self.graph.extruder0temps)
extruder0_target = self.graph.extruder0targettemps[-1]
extruder1_min = min(self.graph.extruder1temps)
extruder1_max = max(self.graph.extruder1temps)
extruder1_target = self.graph.extruder1targettemps[-1]
bed_min = min(self.graph.bedtemps)
bed_max = max(self.graph.bedtemps)
bed_target = self.graph.bedtargettemps[-1]
miny = min(extruder0_min, extruder0_target)
maxy = max(extruder0_max, extruder0_target)
if extruder1_target > 0 or extruder1_max > 5: #use extruder1
miny = min(miny, extruder1_min, extruder1_target)
maxy = max(maxy, extruder1_max, extruder1_target)
if bed_target > 0 or bed_max > 5: #use HBP
miny = min(miny, bed_min, bed_target)
maxy = max(maxy, bed_max, bed_target)
padding = (maxy-miny)*self.buffer/(1.0-2*self.buffer)
miny -= padding
maxy += padding
if maxy-miny < self.min_scale:
extrapadding = (self.min_scale-maxy+miny)/2.0
miny -= extrapadding
maxy += extrapadding
return (miny,maxy)
def getBoundsQuick(self):
# Only look at current temps
extruder0_min = self.graph.extruder0temps[-1]
extruder0_max = self.graph.extruder0temps[-1]
extruder0_target = self.graph.extruder0targettemps[-1]
extruder1_min = self.graph.extruder1temps[-1]
extruder1_max = self.graph.extruder1temps[-1]
extruder1_target = self.graph.extruder1targettemps[-1]
bed_min = self.graph.bedtemps[-1]
bed_max = self.graph.bedtemps[-1]
bed_target = self.graph.bedtargettemps[-1]
miny = min(extruder0_min, extruder0_target)
maxy = max(extruder0_max, extruder0_target)
if extruder1_target > 0 or extruder1_max > 5: #use extruder1
miny = min(miny, extruder1_min, extruder1_target)
maxy = max(maxy, extruder1_max, extruder1_target)
if bed_target > 0 or bed_max > 5: #use HBP
miny = min(miny, bed_min, bed_target)
maxy = max(maxy, bed_max, bed_target)
#We have to rescale, so add padding
if miny < self.graph.minyvalue:
padding = (self.graph.maxyvalue-miny)*self.buffer/(1.0-self.buffer)
miny -= padding
if maxy > self.graph.maxyvalue:
padding = (maxy-self.graph.minyvalue)*self.buffer/(1.0-self.buffer)
maxy += padding
return min(miny,self.graph.minyvalue),max(maxy,self.graph.maxyvalue)
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>. # along with Printrun. If not, see <http://www.gnu.org/licenses/>.
import traceback
try: try:
import wx import wx
except: except:
...@@ -26,6 +28,7 @@ from printrun import gviz ...@@ -26,6 +28,7 @@ from printrun import gviz
from printrun.xybuttons import XYButtons from printrun.xybuttons import XYButtons
from printrun.zbuttons import ZButtons from printrun.zbuttons import ZButtons
from printrun.graph import Graph from printrun.graph import Graph
from printrun.pronterface_widgets import TempGauge
def make_button(parent, label, callback, tooltip, container = None, size = wx.DefaultSize, style = 0): def make_button(parent, label, callback, tooltip, container = None, size = wx.DefaultSize, style = 0):
button = wx.Button(parent, -1, label, style = style, size = size) button = wx.Button(parent, -1, label, style = style, size = size)
...@@ -43,87 +46,64 @@ def make_autosize_button(*args): ...@@ -43,87 +46,64 @@ def make_autosize_button(*args):
class XYZControlsSizer(wx.GridBagSizer): class XYZControlsSizer(wx.GridBagSizer):
def __init__(self, root): def __init__(self, root, parentpanel = None):
super(XYZControlsSizer, self).__init__() super(XYZControlsSizer, self).__init__()
root.xyb = XYButtons(root.panel, root.moveXY, root.homeButtonClicked, root.spacebarAction, root.settings.bgcolor) if not parentpanel: parentpanel = root.panel
root.xyb = XYButtons(parentpanel, root.moveXY, root.homeButtonClicked, root.spacebarAction, root.settings.bgcolor, zcallback=root.moveZ)
self.Add(root.xyb, pos = (0, 1), flag = wx.ALIGN_CENTER) self.Add(root.xyb, pos = (0, 1), flag = wx.ALIGN_CENTER)
root.zb = ZButtons(root.panel, root.moveZ, root.settings.bgcolor) root.zb = ZButtons(parentpanel, root.moveZ, root.settings.bgcolor)
self.Add(root.zb, pos = (0, 2), flag = wx.ALIGN_CENTER) self.Add(root.zb, pos = (0, 2), flag = wx.ALIGN_CENTER)
wx.CallAfter(root.xyb.SetFocus) wx.CallAfter(root.xyb.SetFocus)
class LeftPane(wx.GridBagSizer): def add_extra_controls(self, root, parentpanel, extra_buttons = None):
standalone_mode = extra_buttons is not None
def __init__(self, root): base_line = 1 if standalone_mode else 2
super(LeftPane, self).__init__() root.monitorbox = wx.CheckBox(parentpanel,-1, _("Watch"))
llts = wx.BoxSizer(wx.HORIZONTAL) root.monitorbox.SetValue(bool(root.settings.monitor))
self.Add(llts, pos = (0, 0), span = (1, 9))
self.xyzsizer = XYZControlsSizer(root)
self.Add(self.xyzsizer, pos = (1, 0), span = (1, 8), flag = wx.ALIGN_CENTER)
for i in root.cpbuttons:
btn = make_button(root.panel, i.label, root.procbutton, i.tooltip, style = wx.BU_EXACTFIT)
btn.SetBackgroundColour(i.background)
btn.SetForegroundColour("black")
btn.properties = i
root.btndict[i.command] = btn
root.printerControls.append(btn)
if i.pos == None:
if i.span == 0:
llts.Add(btn)
else:
self.Add(btn, pos = i.pos, span = i.span)
root.xyfeedc = wx.SpinCtrl(root.panel,-1, str(root.settings.xy_feedrate), min = 0, max = 50000, size = (70,-1))
root.xyfeedc.SetToolTip(wx.ToolTip("Set Maximum Speed for X & Y axes (mm/min)"))
llts.Add(wx.StaticText(root.panel,-1, _("XY:")), flag = wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL)
llts.Add(root.xyfeedc)
llts.Add(wx.StaticText(root.panel,-1, _("mm/min Z:")), flag = wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL)
root.zfeedc = wx.SpinCtrl(root.panel,-1, str(root.settings.z_feedrate), min = 0, max = 50000, size = (70,-1))
root.zfeedc.SetToolTip(wx.ToolTip("Set Maximum Speed for Z axis (mm/min)"))
llts.Add(root.zfeedc,)
root.monitorbox = wx.CheckBox(root.panel,-1, _("Watch"))
root.monitorbox.SetToolTip(wx.ToolTip("Monitor Temperatures in Graph")) root.monitorbox.SetToolTip(wx.ToolTip("Monitor Temperatures in Graph"))
self.Add(root.monitorbox, pos = (2, 6)) if standalone_mode:
self.Add(root.monitorbox, pos = (0, 3), span = (1, 3))
else:
self.Add(root.monitorbox, pos = (base_line + 1, 5))
root.monitorbox.Bind(wx.EVT_CHECKBOX, root.setmonitor) root.monitorbox.Bind(wx.EVT_CHECKBOX, root.setmonitor)
self.Add(wx.StaticText(root.panel,-1, _("Heat:")), pos = (2, 0), span = (1, 1), flag = wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) self.Add(wx.StaticText(parentpanel,-1, _("Heat:")), pos = (base_line + 0, 0), span = (1, 1), 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])] htemp_choices = [root.temps[i]+" ("+i+")" for i in sorted(root.temps.keys(), key = lambda x:root.temps[x])]
root.settoff = make_button(root.panel, _("Off"), lambda e: root.do_settemp("off"), _("Switch Hotend Off"), size = (36,-1), style = wx.BU_EXACTFIT) root.settoff = make_button(parentpanel, _("Off"), lambda e: root.do_settemp("off"), _("Switch Hotend Off"), size = (36,-1), style = wx.BU_EXACTFIT)
root.printerControls.append(root.settoff) root.printerControls.append(root.settoff)
self.Add(root.settoff, pos = (2, 1), span = (1, 1)) self.Add(root.settoff, pos = (base_line + 0, 1), span = (1, 1))
if root.settings.last_temperature not in map(float, root.temps.values()): if root.settings.last_temperature not in map(float, root.temps.values()):
htemp_choices = [str(root.settings.last_temperature)] + htemp_choices htemp_choices = [str(root.settings.last_temperature)] + htemp_choices
root.htemp = wx.ComboBox(root.panel, -1, root.htemp = wx.ComboBox(parentpanel, -1,
choices = htemp_choices, style = wx.CB_DROPDOWN, size = (70,-1)) choices = htemp_choices, style = wx.CB_DROPDOWN, size = (80,-1))
root.htemp.SetToolTip(wx.ToolTip("Select Temperature for Hotend")) root.htemp.SetToolTip(wx.ToolTip("Select Temperature for Hotend"))
root.htemp.Bind(wx.EVT_COMBOBOX, root.htemp_change) root.htemp.Bind(wx.EVT_COMBOBOX, root.htemp_change)
self.Add(root.htemp, pos = (2, 2), span = (1, 2)) self.Add(root.htemp, pos = (base_line + 0, 2), span = (1, 2))
root.settbtn = make_button(root.panel, _("Set"), root.do_settemp, _("Switch Hotend On"), size = (38, -1), style = wx.BU_EXACTFIT) root.settbtn = make_button(parentpanel, _("Set"), root.do_settemp, _("Switch Hotend On"), size = (38, -1), style = wx.BU_EXACTFIT)
root.printerControls.append(root.settbtn) root.printerControls.append(root.settbtn)
self.Add(root.settbtn, pos = (2, 4), span = (1, 1)) self.Add(root.settbtn, pos = (base_line + 0, 4), span = (1, 1))
self.Add(wx.StaticText(root.panel,-1, _("Bed:")), pos = (3, 0), span = (1, 1), flag = wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) self.Add(wx.StaticText(parentpanel,-1, _("Bed:")), pos = (base_line + 1, 0), span = (1, 1), 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])] btemp_choices = [root.bedtemps[i]+" ("+i+")" for i in sorted(root.bedtemps.keys(), key = lambda x:root.temps[x])]
root.setboff = make_button(root.panel, _("Off"), lambda e:root.do_bedtemp("off"), _("Switch Heated Bed Off"), size = (36,-1), style = wx.BU_EXACTFIT) root.setboff = make_button(parentpanel, _("Off"), lambda e:root.do_bedtemp("off"), _("Switch Heated Bed Off"), size = (36,-1), style = wx.BU_EXACTFIT)
root.printerControls.append(root.setboff) root.printerControls.append(root.setboff)
self.Add(root.setboff, pos = (3, 1), span = (1, 1)) self.Add(root.setboff, pos = (base_line + 1, 1), span = (1, 1))
if root.settings.last_bed_temperature not in map(float, root.bedtemps.values()): if root.settings.last_bed_temperature not in map(float, root.bedtemps.values()):
btemp_choices = [str(root.settings.last_bed_temperature)] + btemp_choices btemp_choices = [str(root.settings.last_bed_temperature)] + btemp_choices
root.btemp = wx.ComboBox(root.panel, -1, root.btemp = wx.ComboBox(parentpanel, -1,
choices = btemp_choices, style = wx.CB_DROPDOWN, size = (70,-1)) choices = btemp_choices, style = wx.CB_DROPDOWN, size = (80,-1))
root.btemp.SetToolTip(wx.ToolTip("Select Temperature for Heated Bed")) root.btemp.SetToolTip(wx.ToolTip("Select Temperature for Heated Bed"))
root.btemp.Bind(wx.EVT_COMBOBOX, root.btemp_change) root.btemp.Bind(wx.EVT_COMBOBOX, root.btemp_change)
self.Add(root.btemp, pos = (3, 2), span = (1, 2)) self.Add(root.btemp, pos = (base_line + 1, 2), span = (1, 2))
root.setbbtn = make_button(root.panel, _("Set"), root.do_bedtemp, ("Switch Heated Bed On"), size = (38, -1), style = wx.BU_EXACTFIT) root.setbbtn = make_button(parentpanel, _("Set"), root.do_bedtemp, ("Switch Heated Bed On"), size = (38, -1), style = wx.BU_EXACTFIT)
root.printerControls.append(root.setbbtn) root.printerControls.append(root.setbbtn)
self.Add(root.setbbtn, pos = (3, 4), span = (1, 1)) self.Add(root.setbbtn, pos = (base_line + 1, 4), span = (1, 1))
root.btemp.SetValue(str(root.settings.last_bed_temperature)) root.btemp.SetValue(str(root.settings.last_bed_temperature))
root.htemp.SetValue(str(root.settings.last_temperature)) root.htemp.SetValue(str(root.settings.last_temperature))
...@@ -144,65 +124,189 @@ class LeftPane(wx.GridBagSizer): ...@@ -144,65 +124,189 @@ class LeftPane(wx.GridBagSizer):
if( '(' not in root.htemp.Value): if( '(' not in root.htemp.Value):
root.htemp.SetValue(root.htemp.Value + ' (user)') root.htemp.SetValue(root.htemp.Value + ' (user)')
root.tempdisp = wx.StaticText(root.panel,-1, "") root.tempdisp = wx.StaticText(parentpanel,-1, "")
root.edist = wx.SpinCtrl(root.panel,-1, "5", min = 0, max = 1000, size = (60,-1)) root.edist = wx.SpinCtrl(parentpanel,-1, "5", min = 0, max = 1000, size = (70,-1))
root.edist.SetBackgroundColour((225, 200, 200)) root.edist.SetBackgroundColour((225, 200, 200))
root.edist.SetForegroundColour("black") root.edist.SetForegroundColour("black")
self.Add(root.edist, pos = (4, 2), span = (1, 2)) self.Add(root.edist, pos = (base_line + 2, 2), span = (1, 2), flag = wx.EXPAND | wx.RIGHT, border = 10)
self.Add(wx.StaticText(root.panel,-1, _("mm")), pos = (4, 4), span = (1, 1)) self.Add(wx.StaticText(parentpanel,-1, _("mm")), pos = (base_line + 2, 4), span = (1, 1))
root.edist.SetToolTip(wx.ToolTip("Amount to Extrude or Retract (mm)")) root.edist.SetToolTip(wx.ToolTip("Amount to Extrude or Retract (mm)"))
root.efeedc = wx.SpinCtrl(root.panel,-1, str(root.settings.e_feedrate), min = 0, max = 50000, size = (60,-1)) root.efeedc = wx.SpinCtrl(parentpanel,-1, str(root.settings.e_feedrate), min = 0, max = 50000, size = (70,-1))
root.efeedc.SetToolTip(wx.ToolTip("Extrude / Retract speed (mm/min)")) root.efeedc.SetToolTip(wx.ToolTip("Extrude / Retract speed (mm/min)"))
root.efeedc.SetBackgroundColour((225, 200, 200)) root.efeedc.SetBackgroundColour((225, 200, 200))
root.efeedc.SetForegroundColour("black") root.efeedc.SetForegroundColour("black")
root.efeedc.Bind(wx.EVT_SPINCTRL, root.setfeeds) root.efeedc.Bind(wx.EVT_SPINCTRL, root.setfeeds)
self.Add(root.efeedc, pos = (5, 2), span = (1, 2)) root.efeedc.Bind(wx.EVT_TEXT, root.setfeeds)
self.Add(wx.StaticText(root.panel,-1, _("mm/\nmin")), pos = (5, 4), span = (1, 1)) self.Add(root.efeedc, pos = (base_line + 3, 2), span = (1, 2), flag = wx.EXPAND | wx.RIGHT, border = 10)
self.Add(wx.StaticText(parentpanel,-1, _("mm/\nmin")), pos = (base_line + 3, 4), span = (2, 1))
gauges_base_line = base_line + 8 if standalone_mode else base_line + 5
if root.display_gauges:
root.hottgauge = TempGauge(parentpanel, size = (-1, 24), title = _("Heater:"), maxval = 300)
self.Add(root.hottgauge, pos = (gauges_base_line + 0, 0), span = (1, 6), flag = wx.EXPAND)
root.bedtgauge = TempGauge(parentpanel, size = (-1, 24), title = _("Bed:"), maxval = 150)
self.Add(root.bedtgauge, pos = (gauges_base_line + 1, 0), span = (1, 6), 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 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)
self.Add(root.tempdisp, pos = (gauges_base_line + 2, 0), span = (1, 6))
else:
self.Add(root.tempdisp, pos = (gauges_base_line + 0, 0), span = (1, 6))
root.graph = Graph(parentpanel, wx.ID_ANY, root)
if standalone_mode:
self.Add(root.graph, pos = (base_line + 5, 0), span = (3, 6))
else:
self.Add(root.graph, pos = (base_line + 2, 5), span = (3, 1))
if extra_buttons:
pos_mapping = {
(2,5):(0,0),
(4,0):(3,0),
(5,0):(4,0),
}
span_mapping = {
(2,5):(1,3),
(4,0):(1,2),
(5,0):(1,2),
}
for i in extra_buttons:
btn = extra_buttons[i]
self.Add(btn, pos = pos_mapping[i.pos], span = span_mapping[i.pos], flag = wx.EXPAND)
class LeftPane(wx.GridBagSizer):
def __init__(self, root, parentpanel = None, standalone_mode = False):
super(LeftPane, self).__init__()
if not parentpanel: parentpanel = root.panel
llts = wx.BoxSizer(wx.HORIZONTAL)
self.Add(llts, pos = (0, 0), span = (1, 6))
self.xyzsizer = XYZControlsSizer(root, parentpanel)
self.Add(self.xyzsizer, pos = (1, 0), span = (1, 6), flag = wx.ALIGN_CENTER)
self.extra_buttons = {}
for i in root.cpbuttons:
btn = make_button(parentpanel, i.label, root.procbutton, i.tooltip)
btn.SetBackgroundColour(i.background)
btn.SetForegroundColour("black")
btn.properties = i
root.btndict[i.command] = btn
root.printerControls.append(btn)
if i.pos == None:
if i.span == 0:
llts.Add(btn)
elif not standalone_mode:
self.Add(btn, pos = i.pos, span = i.span, flag = wx.EXPAND)
else:
self.extra_buttons[i] = btn
root.xyfeedc = wx.SpinCtrl(parentpanel,-1, str(root.settings.xy_feedrate), min = 0, max = 50000, size = (70,-1))
root.xyfeedc.SetToolTip(wx.ToolTip("Set Maximum Speed for X & Y axes (mm/min)"))
llts.Add(wx.StaticText(parentpanel,-1, _("XY:")), flag = wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL)
llts.Add(root.xyfeedc)
llts.Add(wx.StaticText(parentpanel,-1, _("mm/min Z:")), flag = wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL)
root.zfeedc = wx.SpinCtrl(parentpanel,-1, str(root.settings.z_feedrate), min = 0, max = 50000, size = (70,-1))
root.zfeedc.SetToolTip(wx.ToolTip("Set Maximum Speed for Z axis (mm/min)"))
llts.Add(root.zfeedc,)
root.xyfeedc.Bind(wx.EVT_SPINCTRL, root.setfeeds) root.xyfeedc.Bind(wx.EVT_SPINCTRL, root.setfeeds)
root.zfeedc.Bind(wx.EVT_SPINCTRL, root.setfeeds) root.zfeedc.Bind(wx.EVT_SPINCTRL, root.setfeeds)
root.xyfeedc.Bind(wx.EVT_TEXT, root.setfeeds)
root.zfeedc.Bind(wx.EVT_TEXT, root.setfeeds)
root.zfeedc.SetBackgroundColour((180, 255, 180)) root.zfeedc.SetBackgroundColour((180, 255, 180))
root.zfeedc.SetForegroundColour("black") root.zfeedc.SetForegroundColour("black")
root.graph = Graph(root.panel, wx.ID_ANY) if not standalone_mode:
self.Add(root.graph, pos = (3, 5), span = (3, 3)) add_extra_controls(self, root, parentpanel, None)
self.Add(root.tempdisp, pos = (6, 0), span = (1, 9))
class NoViz(object):
showall = False
def clear(self, *a):
pass
def addfile(self, *a, **kw):
pass
def addgcode(self, *a, **kw):
pass
def Refresh(self, *a):
pass
def setlayer(self, *a):
pass
class VizPane(wx.BoxSizer): class VizPane(wx.BoxSizer):
def __init__(self, root): def __init__(self, root, parentpanel = None):
super(VizPane, self).__init__(wx.VERTICAL) super(VizPane, self).__init__(wx.VERTICAL)
root.gviz = gviz.gviz(root.panel, (300, 300), if not parentpanel: parentpanel = root.panel
if root.settings.mainviz == "None":
root.gviz = NoViz()
use2dview = root.settings.mainviz == "2D"
if root.settings.mainviz == "3D":
try:
import printrun.gcview
root.gviz = printrun.gcview.GcodeViewMainWrapper(parentpanel, root.build_dimensions_list)
root.gviz.clickcb = root.showwin
except:
use2dview = True
print "3D view mode requested, but we failed to initialize it."
print "Falling back to 2D view, and here is the backtrace:"
traceback.print_exc()
if use2dview:
root.gviz = gviz.gviz(parentpanel, (300, 300),
build_dimensions = root.build_dimensions_list, build_dimensions = root.build_dimensions_list,
grid = (root.settings.preview_grid_step1, root.settings.preview_grid_step2), grid = (root.settings.preview_grid_step1, root.settings.preview_grid_step2),
extrusion_width = root.settings.preview_extrusion_width) extrusion_width = root.settings.preview_extrusion_width)
root.gviz.SetToolTip(wx.ToolTip("Click to examine / edit\n layers of loaded file")) root.gviz.SetToolTip(wx.ToolTip("Click to examine / edit\n layers of loaded file"))
root.gviz.showall = 1 root.gviz.showall = 1
root.gviz.Bind(wx.EVT_LEFT_DOWN, root.showwin)
use3dview = root.settings.viz3d
if use3dview:
try: try:
raise "" import printrun.gcview
import printrun.stlview objects = None
root.gwindow = printrun.stlview.GCFrame(None, wx.ID_ANY, 'Gcode view, shift to move view, mousewheel to set layer', size = (600, 600)) 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)
except: except:
use3dview = False
print "3D view mode requested, but we failed to initialize it."
print "Falling back to 2D view, and here is the backtrace:"
traceback.print_exc()
if not use3dview:
root.gwindow = gviz.window([], root.gwindow = gviz.window([],
build_dimensions = root.build_dimensions_list, build_dimensions = root.build_dimensions_list,
grid = (root.settings.preview_grid_step1, root.settings.preview_grid_step2), grid = (root.settings.preview_grid_step1, root.settings.preview_grid_step2),
extrusion_width = root.settings.preview_extrusion_width) extrusion_width = root.settings.preview_extrusion_width)
root.gviz.Bind(wx.EVT_LEFT_DOWN, root.showwin) root.gwindow.Bind(wx.EVT_CLOSE, lambda x: root.gwindow.Hide())
root.gwindow.Bind(wx.EVT_CLOSE, lambda x:root.gwindow.Hide()) if not isinstance(root.gviz, NoViz):
self.Add(root.gviz, 1, flag = wx.SHAPED) self.Add(root.gviz.widget, 1, flag = wx.SHAPED)
cs = root.centersizer = wx.GridBagSizer() root.centersizer = wx.GridBagSizer()
self.Add(cs, 0, flag = wx.EXPAND) self.Add(root.centersizer, 0, flag = wx.EXPAND)
class LogPane(wx.BoxSizer): class LogPane(wx.BoxSizer):
def __init__(self, root): def __init__(self, root, parentpanel = None):
super(LogPane, self).__init__(wx.VERTICAL) super(LogPane, self).__init__(wx.VERTICAL)
root.lowerrsizer = self if not parentpanel: parentpanel = root.panel
root.logbox = wx.TextCtrl(root.panel, style = wx.TE_MULTILINE, size = (350,-1)) root.logbox = wx.TextCtrl(parentpanel, style = wx.TE_MULTILINE, size = (350,-1))
root.logbox.SetMinSize((100,-1))
root.logbox.SetEditable(0) root.logbox.SetEditable(0)
self.Add(root.logbox, 1, wx.EXPAND) self.Add(root.logbox, 1, wx.EXPAND)
lbrs = wx.BoxSizer(wx.HORIZONTAL) lbrs = wx.BoxSizer(wx.HORIZONTAL)
root.commandbox = wx.TextCtrl(root.panel, style = wx.TE_PROCESS_ENTER) root.commandbox = wx.TextCtrl(parentpanel, style = wx.TE_PROCESS_ENTER)
root.commandbox.SetToolTip(wx.ToolTip("Send commands to printer\n(Type 'help' for simple\nhelp function)")) root.commandbox.SetToolTip(wx.ToolTip("Send commands to printer\n(Type 'help' for simple\nhelp function)"))
root.commandbox.Bind(wx.EVT_TEXT_ENTER, root.sendline) root.commandbox.Bind(wx.EVT_TEXT_ENTER, root.sendline)
root.commandbox.Bind(wx.EVT_CHAR, root.cbkey) root.commandbox.Bind(wx.EVT_CHAR, root.cbkey)
...@@ -210,26 +314,27 @@ class LogPane(wx.BoxSizer): ...@@ -210,26 +314,27 @@ class LogPane(wx.BoxSizer):
root.commandbox.histindex = 1 root.commandbox.histindex = 1
#root.printerControls.append(root.commandbox) #root.printerControls.append(root.commandbox)
lbrs.Add(root.commandbox, 1) lbrs.Add(root.commandbox, 1)
root.sendbtn = make_button(root.panel, _("Send"), root.sendline, _("Send Command to Printer"), style = wx.BU_EXACTFIT, container = lbrs) root.sendbtn = make_button(parentpanel, _("Send"), root.sendline, _("Send Command to Printer"), style = wx.BU_EXACTFIT, container = lbrs)
#root.printerControls.append(root.sendbtn) #root.printerControls.append(root.sendbtn)
self.Add(lbrs, 0, wx.EXPAND) self.Add(lbrs, 0, wx.EXPAND)
class MainToolbar(wx.BoxSizer):
def __init__(self, root): def MainToolbar(root, parentpanel = None, use_wrapsizer = False):
super(MainToolbar, self).__init__(wx.HORIZONTAL) ToolbarSizer = wx.WrapSizer if use_wrapsizer and wx.VERSION > (2, 9) else wx.BoxSizer
root.rescanbtn = make_sized_button(root.panel, _("Port"), root.rescanports, _("Communication Settings\nClick to rescan ports")) self = ToolbarSizer(wx.HORIZONTAL)
if not parentpanel: parentpanel = root.panel
root.rescanbtn = make_sized_button(parentpanel, _("Port"), root.rescanports, _("Communication Settings\nClick to rescan ports"))
self.Add(root.rescanbtn, 0, wx.TOP|wx.LEFT, 0) self.Add(root.rescanbtn, 0, wx.TOP|wx.LEFT, 0)
root.serialport = wx.ComboBox(root.panel, -1, root.serialport = wx.ComboBox(parentpanel, -1,
choices = root.scanserial(), choices = root.scanserial(),
style = wx.CB_DROPDOWN, size = (150, 25)) style = wx.CB_DROPDOWN, size = (-1, 25))
root.serialport.SetToolTip(wx.ToolTip("Select Port Printer is connected to")) root.serialport.SetToolTip(wx.ToolTip("Select Port Printer is connected to"))
root.rescanports() root.rescanports()
self.Add(root.serialport) self.Add(root.serialport)
self.Add(wx.StaticText(root.panel,-1, "@"), 0, wx.RIGHT|wx.ALIGN_CENTER, 0) self.Add(wx.StaticText(parentpanel,-1, "@"), 0, wx.RIGHT|wx.ALIGN_CENTER, 0)
root.baud = wx.ComboBox(root.panel, -1, root.baud = wx.ComboBox(parentpanel, -1,
choices = ["2400", "9600", "19200", "38400", "57600", "115200", "250000"], choices = ["2400", "9600", "19200", "38400", "57600", "115200", "250000"],
style = wx.CB_DROPDOWN, size = (100, 25)) style = wx.CB_DROPDOWN, size = (100, 25))
root.baud.SetToolTip(wx.ToolTip("Select Baud rate for printer communication")) root.baud.SetToolTip(wx.ToolTip("Select Baud rate for printer communication"))
...@@ -239,17 +344,18 @@ class MainToolbar(wx.BoxSizer): ...@@ -239,17 +344,18 @@ class MainToolbar(wx.BoxSizer):
except: except:
pass pass
self.Add(root.baud) self.Add(root.baud)
root.connectbtn = make_sized_button(root.panel, _("Connect"), root.connect, _("Connect to the printer"), self) root.connectbtn = make_sized_button(parentpanel, _("Connect"), root.connect, _("Connect to the printer"), self)
root.resetbtn = make_autosize_button(root.panel, _("Reset"), root.reset, _("Reset the printer"), self) root.resetbtn = make_autosize_button(parentpanel, _("Reset"), root.reset, _("Reset the printer"), self)
root.loadbtn = make_autosize_button(root.panel, _("Load file"), root.loadfile, _("Load a 3D model file"), self) root.loadbtn = make_autosize_button(parentpanel, _("Load file"), root.loadfile, _("Load a 3D model file"), self)
root.platebtn = make_autosize_button(root.panel, _("Compose"), root.plate, _("Simple Plater System"), self) root.platebtn = make_autosize_button(parentpanel, _("Compose"), root.plate, _("Simple Plater System"), self)
root.sdbtn = make_autosize_button(root.panel, _("SD"), root.sdmenu, _("SD Card Printing"), self) root.sdbtn = make_autosize_button(parentpanel, _("SD"), root.sdmenu, _("SD Card Printing"), self)
root.printerControls.append(root.sdbtn) root.printerControls.append(root.sdbtn)
root.printbtn = make_sized_button(root.panel, _("Print"), root.printfile, _("Start Printing Loaded File"), self) root.printbtn = make_sized_button(parentpanel, _("Print"), root.printfile, _("Start Printing Loaded File"), self)
root.printbtn.Disable() root.printbtn.Disable()
root.pausebtn = make_sized_button(root.panel, _("Pause"), root.pause, _("Pause Current Print"), self) root.pausebtn = make_sized_button(parentpanel, _("Pause"), root.pause, _("Pause Current Print"), self)
root.recoverbtn = make_sized_button(root.panel, _("Recover"), root.recover, _("Recover previous Print"), self) root.recoverbtn = make_sized_button(parentpanel, _("Recover"), root.recover, _("Recover previous Print"), self)
return self
class MainWindow(wx.Frame): class MainWindow(wx.Frame):
...@@ -259,14 +365,83 @@ class MainWindow(wx.Frame): ...@@ -259,14 +365,83 @@ class MainWindow(wx.Frame):
# when we're connected to a printer # when we're connected to a printer
self.printerControls = [] self.printerControls = []
def newPanel(self, parent):
panel = wx.Panel(parent)
panel.SetBackgroundColour(self.settings.bgcolor)
return panel
def createTabbedGui(self):
self.notesizer = wx.BoxSizer(wx.VERTICAL)
self.notebook = wx.Notebook(self.panel)
self.notebook.SetBackgroundColour(self.settings.bgcolor)
page1panel = self.newPanel(self.notebook)
page2panel = self.newPanel(self.notebook)
self.mainsizer_page1 = wx.BoxSizer(wx.VERTICAL)
page1panel1 = self.newPanel(page1panel)
page1panel2 = self.newPanel(page1panel)
self.uppersizer = MainToolbar(self, page1panel1, use_wrapsizer = True)
page1panel1.SetSizer(self.uppersizer)
self.mainsizer_page1.Add(page1panel1, 0, wx.EXPAND)
self.lowersizer = wx.BoxSizer(wx.HORIZONTAL)
page1panel2.SetSizer(self.lowersizer)
leftsizer = wx.BoxSizer(wx.VERTICAL)
left_pane = LeftPane(self, page1panel2, True)
leftsizer.Add(left_pane, 1, wx.ALIGN_CENTER)
rightsizer = wx.BoxSizer(wx.VERTICAL)
extracontrols = wx.GridBagSizer()
add_extra_controls(extracontrols, self, page1panel2, left_pane.extra_buttons)
rightsizer.Add(extracontrols, 1, wx.ALIGN_CENTER)
self.lowersizer.Add(leftsizer, 1, wx.ALIGN_CENTER)
self.lowersizer.Add(rightsizer, 1, wx.ALIGN_CENTER)
self.mainsizer_page1.Add(page1panel2, 1, wx.EXPAND)
self.mainsizer = wx.BoxSizer(wx.HORIZONTAL)
self.splitterwindow = wx.SplitterWindow(page2panel, style = wx.SP_3D)
page2sizer1 = wx.BoxSizer(wx.HORIZONTAL)
page2panel1 = self.newPanel(self.splitterwindow)
page2sizer2 = wx.BoxSizer(wx.HORIZONTAL)
page2panel2 = self.newPanel(self.splitterwindow)
vizpane = VizPane(self, page2panel1)
page2sizer1.Add(vizpane, 1, wx.EXPAND)
page2sizer2.Add(LogPane(self, page2panel2), 1, wx.EXPAND)
page2panel1.SetSizer(page2sizer1)
page2panel2.SetSizer(page2sizer2)
self.splitterwindow.SetSashGravity(0.5)
self.splitterwindow.SplitVertically(page2panel1, page2panel2, 0)
self.mainsizer.Add(self.splitterwindow, 1, wx.EXPAND)
page1panel.SetSizer(self.mainsizer_page1)
page2panel.SetSizer(self.mainsizer)
self.notesizer.Add(self.notebook, 1, wx.EXPAND)
self.notebook.AddPage(page1panel, _("Commands"))
self.notebook.AddPage(page2panel, _("Status"))
self.panel.SetSizer(self.notesizer)
self.status = self.CreateStatusBar()
self.status.SetStatusText(_("Not connected to printer."))
self.panel.Bind(wx.EVT_MOUSE_EVENTS, self.editbutton)
self.Bind(wx.EVT_CLOSE, self.kill)
vizpane.Detach(self.centersizer)
rightsizer.Add(self.centersizer, 0, wx.EXPAND)
self.panel.SetSizerAndFit(self.notesizer)
# disable all printer controls until we connect to a printer
self.pausebtn.Disable()
self.recoverbtn.Disable()
for i in self.printerControls:
i.Disable()
#self.panel.Fit()
self.cbuttons_panel = page1panel
self.cbuttons_reload()
def createGui(self): def createGui(self):
self.mainsizer = wx.BoxSizer(wx.VERTICAL) self.mainsizer = wx.BoxSizer(wx.VERTICAL)
self.uppersizer = MainToolbar(self) self.uppersizer = MainToolbar(self)
self.lowersizer = wx.BoxSizer(wx.HORIZONTAL) self.lowersizer = wx.BoxSizer(wx.HORIZONTAL)
self.lowersizer.Add(LeftPane(self)) self.lowersizer.Add(LeftPane(self), 0)
self.lowersizer.Add(VizPane(self), 1, wx.EXPAND|wx.ALIGN_CENTER_HORIZONTAL) self.lowersizer.Add(VizPane(self), 1, wx.EXPAND|wx.ALIGN_CENTER_HORIZONTAL)
self.lowersizer.Add(LogPane(self), 0, wx.EXPAND) self.lowersizer.Add(LogPane(self), 1, wx.EXPAND)
self.mainsizer.Add(self.uppersizer) self.mainsizer.Add(self.uppersizer, 0)
self.mainsizer.Add(self.lowersizer, 1, wx.EXPAND) self.mainsizer.Add(self.lowersizer, 1, wx.EXPAND)
self.panel.SetSizer(self.mainsizer) self.panel.SetSizer(self.mainsizer)
self.status = self.CreateStatusBar() self.status = self.CreateStatusBar()
...@@ -276,6 +451,12 @@ class MainWindow(wx.Frame): ...@@ -276,6 +451,12 @@ class MainWindow(wx.Frame):
self.mainsizer.Layout() self.mainsizer.Layout()
self.mainsizer.Fit(self) self.mainsizer.Fit(self)
# This prevents resizing below a reasonnable value
# We sum the lowersizer (left pane / viz / log) min size
# the toolbar height and the statusbar/menubar sizes
minsize = self.lowersizer.GetMinSize() # lower pane
minsize[1] += self.uppersizer.GetMinSize()[1] # toolbar height
self.SetMinSize(self.ClientToWindowSize(minsize)) # client to window
# disable all printer controls until we connect to a printer # disable all printer controls until we connect to a printer
self.pausebtn.Disable() self.pausebtn.Disable()
...@@ -284,4 +465,4 @@ class MainWindow(wx.Frame): ...@@ -284,4 +465,4 @@ class MainWindow(wx.Frame):
i.Disable() i.Disable()
#self.panel.Fit() #self.panel.Fit()
self.cbuttons_reload() self.cbuttons_panel = self.panel
...@@ -12,7 +12,11 @@ ...@@ -12,7 +12,11 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>. # along with Printrun. If not, see <http://www.gnu.org/licenses/>.
from Queue import Queue
from collections import deque
import wx, time import wx, time
from printrun import gcoder
from printrun_utils import imagefile from printrun_utils import imagefile
...@@ -20,64 +24,51 @@ ID_ABOUT = 101 ...@@ -20,64 +24,51 @@ ID_ABOUT = 101
ID_EXIT = 110 ID_EXIT = 110
class window(wx.Frame): class window(wx.Frame):
def __init__(self, f, size = (600, 600), build_dimensions = [200, 200, 100, 0, 0, 0], grid = (10, 50), extrusion_width = 0.5): def __init__(self, f, size = (600, 600), build_dimensions = [200, 200, 100, 0, 0, 0], grid = (10, 50), extrusion_width = 0.5):
wx.Frame.__init__(self, None, title = "Gcode view, shift to move view, mousewheel to set layer", size = (size[0], size[1])) wx.Frame.__init__(self, None, title = "Gcode view, shift to move view, mousewheel to set layer", size = size)
self.p = gviz(self, size = size, build_dimensions = build_dimensions, grid = grid, extrusion_width = extrusion_width)
self.CreateStatusBar(1);
self.SetStatusText("Layer number and Z position show here when you scroll");
panel = wx.Panel(self, -1)
self.p = gviz(panel, size = size, build_dimensions = build_dimensions, grid = grid, extrusion_width = extrusion_width, realparent = self)
vbox = wx.BoxSizer(wx.VERTICAL) vbox = wx.BoxSizer(wx.VERTICAL)
toolbar = wx.ToolBar(self, -1, style = wx.TB_HORIZONTAL | wx.NO_BORDER) toolbar = wx.ToolBar(panel, -1, style = wx.TB_HORIZONTAL | wx.NO_BORDER)
toolbar.AddSimpleTool(1, wx.Image(imagefile('zoom_in.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Zoom In [+]', '') toolbar.AddSimpleTool(1, wx.Image(imagefile('zoom_in.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Zoom In [+]', '')
toolbar.AddSimpleTool(2, wx.Image(imagefile('zoom_out.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Zoom Out [-]', '') toolbar.AddSimpleTool(2, wx.Image(imagefile('zoom_out.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Zoom Out [-]', '')
toolbar.AddSeparator() toolbar.AddSeparator()
toolbar.AddSimpleTool(3, wx.Image(imagefile('arrow_up.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Move Up a Layer [U]', '') toolbar.AddSimpleTool(3, wx.Image(imagefile('arrow_up.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Move Up a Layer [U]', '')
toolbar.AddSimpleTool(4, wx.Image(imagefile('arrow_down.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Move Down a Layer [D]', '') toolbar.AddSimpleTool(4, wx.Image(imagefile('arrow_down.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Move Down a Layer [D]', '')
toolbar.AddSimpleTool(5, wx.EmptyBitmap(16, 16), 'Reset view', '') toolbar.AddSimpleTool(5, wx.Image(imagefile('reset.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Reset view', '')
toolbar.AddSeparator() toolbar.AddSeparator()
#toolbar.AddSimpleTool(5, wx.Image('./images/inject.png', wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Insert Code at start of this layer', '') #toolbar.AddSimpleTool(6, wx.Image(imagefile('inject.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), 'Insert Code at start of this layer', '')
toolbar.Realize() toolbar.Realize()
vbox.Add(toolbar, 0, border = 5) vbox.Add(toolbar, 0, border = 5)
self.SetSizer(vbox) vbox.Add(self.p, 1, wx.EXPAND)
self.Bind(wx.EVT_TOOL, lambda x:self.p.zoom(200, 200, 1.2), id = 1) panel.SetSizer(vbox)
self.Bind(wx.EVT_TOOL, lambda x:self.p.zoom(200, 200, 1/1.2), id = 2) self.SetMinSize(self.ClientToWindowSize(vbox.GetMinSize()))
self.Bind(wx.EVT_TOOL, lambda x:self.p.zoom(-1, -1, 1.2), id = 1)
self.Bind(wx.EVT_TOOL, lambda x:self.p.zoom(-1, -1, 1/1.2), id = 2)
self.Bind(wx.EVT_TOOL, lambda x:self.p.layerup(), id = 3) self.Bind(wx.EVT_TOOL, lambda x:self.p.layerup(), id = 3)
self.Bind(wx.EVT_TOOL, lambda x:self.p.layerdown(), id = 4) self.Bind(wx.EVT_TOOL, lambda x:self.p.layerdown(), id = 4)
self.Bind(wx.EVT_TOOL, self.resetview, id = 5) self.Bind(wx.EVT_TOOL, self.resetview, id = 5)
#self.Bind(wx.EVT_TOOL, lambda x:self.p.inject(), id = 5) #self.Bind(wx.EVT_TOOL, lambda x:self.p.inject(), id = 6)
self.CreateStatusBar(1);
self.SetStatusText("Layer number and Z position show here when you scroll");
#self.bu = wx.Button(self.p,-1, "U", pos = (0, 100), size = (40, 140))
#self.bd = wx.Button(self.p,-1, "D", pos = (0, 140), size = (40, 140))
#self.bi = wx.Button(self.p,-1, "+", pos = (40, 100), size = (40, 140))
#self.bo = wx.Button(self.p,-1, "-", pos = (40, 140), size = (40, 140))
#self.bs = wx.Button(self.p, -1, "Inject", pos = (85, 103), size = (50, 20))
#self.bu.SetToolTip(wx.ToolTip("Move up one layer"))
#self.bd.SetToolTip(wx.ToolTip("Move down one layer"))
#self.bi.SetToolTip(wx.ToolTip("Zoom view in"))
#self.bo.SetToolTip(wx.ToolTip("Zoom view out"))
#self.bs.SetToolTip(wx.ToolTip("Insert Code at start of this layer"))
#self.bu.Bind(wx.EVT_BUTTON, lambda x:self.p.layerup())
#self.bd.Bind(wx.EVT_BUTTON, lambda x:self.p.layerdown())
#self.bi.Bind(wx.EVT_BUTTON, lambda x:self.p.zoom(200, 200, 1.2))
#self.bo.Bind(wx.EVT_BUTTON, lambda x:self.p.zoom(200, 200, 1/1.2))
#self.bs.Bind(wx.EVT_BUTTON, lambda x:self.p.inject())
s = time.time()
#print time.time()-s
self.initpos = [0, 0] self.initpos = [0, 0]
self.p.Bind(wx.EVT_KEY_DOWN, self.key) self.p.Bind(wx.EVT_KEY_DOWN, self.key)
#self.bu.Bind(wx.EVT_KEY_DOWN, self.key)
#self.bd.Bind(wx.EVT_KEY_DOWN, self.key)
#self.bi.Bind(wx.EVT_KEY_DOWN, self.key)
#self.bo.Bind(wx.EVT_KEY_DOWN, self.key)
self.Bind(wx.EVT_KEY_DOWN, self.key) self.Bind(wx.EVT_KEY_DOWN, self.key)
self.p.Bind(wx.EVT_MOUSEWHEEL, self.zoom) self.p.Bind(wx.EVT_MOUSEWHEEL, self.zoom)
self.Bind(wx.EVT_MOUSEWHEEL, self.zoom) self.Bind(wx.EVT_MOUSEWHEEL, self.zoom)
self.p.Bind(wx.EVT_MOUSE_EVENTS, self.mouse) self.p.Bind(wx.EVT_MOUSE_EVENTS, self.mouse)
self.Bind(wx.EVT_MOUSE_EVENTS, self.mouse) self.Bind(wx.EVT_MOUSE_EVENTS, self.mouse)
if f:
gcode = gcoder.GCode(f)
self.p.addfile(gcode)
def set_current_gline(self, gline):
return
def resetview(self, event): def resetview(self, event):
self.p.translate = [0.0, 0.0] self.p.translate = [0.0, 0.0]
self.p.scale = self.p.basescale self.p.scale = self.p.basescale
...@@ -85,19 +76,17 @@ class window(wx.Frame): ...@@ -85,19 +76,17 @@ class window(wx.Frame):
def mouse(self, event): def mouse(self, event):
if event.ButtonUp(wx.MOUSE_BTN_LEFT): if event.ButtonUp(wx.MOUSE_BTN_LEFT):
if(self.initpos is not None): if self.initpos is not None:
self.initpos = None self.initpos = None
elif event.Dragging(): elif event.Dragging():
e = event.GetPositionTuple() e = event.GetPositionTuple()
if self.initpos is None or not hasattr(self, "basetrans"): if self.initpos is None or not hasattr(self, "basetrans"):
self.initpos = e self.initpos = e
self.basetrans = self.p.translate self.basetrans = self.p.translate
#print self.p.translate, e, self.initpos self.p.translate = [self.basetrans[0] + (e[0] - self.initpos[0]),
self.p.translate = [ self.basetrans[0]+(e[0]-self.initpos[0]), self.basetrans[1] + (e[1] - self.initpos[1])]
self.basetrans[1]+(e[1]-self.initpos[1]) ] self.p.dirty = 1
self.p.repaint() wx.CallAfter(self.p.Refresh)
self.p.Refresh()
else: else:
event.Skip() event.Skip()
...@@ -108,18 +97,7 @@ class window(wx.Frame): ...@@ -108,18 +97,7 @@ class window(wx.Frame):
kzi = [388, 316, 61] # Zoom In Keys kzi = [388, 316, 61] # Zoom In Keys
kzo = [390, 314, 45] # Zoom Out Keys kzo = [390, 314, 45] # Zoom Out Keys
x = event.GetKeyCode() x = event.GetKeyCode()
#print "Key event - "+str(x)
#if event.ShiftDown():
cx, cy = self.p.translate cx, cy = self.p.translate
# if x == wx.WXK_UP:
# self.p.zoom(cx, cy, 1.2)
# if x == wx.WXK_DOWN:
# self.p.zoom(cx, cy, 1/1.2)
#else:
# if x == wx.WXK_UP:
# self.p.layerup()
# if x == wx.WXK_DOWN:
# self.p.layerdown()
if x in kup: if x in kup:
self.p.layerup() self.p.layerup()
if x in kdo: if x in kdo:
...@@ -129,7 +107,6 @@ class window(wx.Frame): ...@@ -129,7 +107,6 @@ class window(wx.Frame):
if x in kzo: if x in kzo:
self.p.zoom(cx, cy, 1/1.2) self.p.zoom(cx, cy, 1/1.2)
#print p.lines.keys()
def zoom(self, event): def zoom(self, event):
z = event.GetWheelRotation() z = event.GetWheelRotation()
if event.ShiftDown(): if event.ShiftDown():
...@@ -140,9 +117,22 @@ class window(wx.Frame): ...@@ -140,9 +117,22 @@ class window(wx.Frame):
elif z < 0: self.p.zoom(event.GetX(), event.GetY(), 1/1.2) elif z < 0: self.p.zoom(event.GetX(), event.GetY(), 1/1.2)
class gviz(wx.Panel): class gviz(wx.Panel):
def __init__(self, parent, size = (200, 200), build_dimensions = [200, 200, 100, 0, 0, 0], grid = (10, 50), extrusion_width = 0.5):
wx.Panel.__init__(self, parent,-1, size = (size[0], size[1])) # Mark canvas as dirty when setting showall
self.parent = parent _showall = 0
def _get_showall(self):
return self._showall
def _set_showall(self, showall):
if showall != self._showall:
self.dirty = 1
self._showall = showall
showall = property(_get_showall, _set_showall)
def __init__(self, parent, size = (200, 200), build_dimensions = [200, 200, 100, 0, 0, 0], grid = (10, 50), extrusion_width = 0.5, realparent = None):
wx.Panel.__init__(self, parent, -1, size = size)
self.widget = self
self.SetMinSize((150, 150))
self.parent = realparent if realparent else parent
self.size = size self.size = size
self.build_dimensions = build_dimensions self.build_dimensions = build_dimensions
self.grid = grid self.grid = grid
...@@ -157,7 +147,7 @@ class gviz(wx.Panel): ...@@ -157,7 +147,7 @@ class gviz(wx.Panel):
self.layers = [] self.layers = []
self.layerindex = 0 self.layerindex = 0
self.filament_width = extrusion_width # set it to 0 to disable scaling lines with zoom self.filament_width = extrusion_width # set it to 0 to disable scaling lines with zoom
self.basescale = [min(float(size[0])/build_dimensions[0], float(size[1])/build_dimensions[1])]*2 self.update_basescale()
self.scale = self.basescale self.scale = self.basescale
penwidth = max(1.0, self.filament_width*((self.scale[0]+self.scale[1])/2.0)) penwidth = max(1.0, self.filament_width*((self.scale[0]+self.scale[1])/2.0))
self.translate = [0.0, 0.0] self.translate = [0.0, 0.0]
...@@ -168,8 +158,10 @@ class gviz(wx.Panel): ...@@ -168,8 +158,10 @@ class gviz(wx.Panel):
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.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.penslist = [self.mainpen, self.travelpen, self.hlpen]+self.fades
self.showall = 0 self.showall = 0
self.hilight = [] self.hilight = deque()
self.hilightarcs = [] self.hilightarcs = deque()
self.hilightqueue = Queue(0)
self.hilightarcsqueue = Queue(0)
self.dirty = 1 self.dirty = 1
self.blitmap = wx.EmptyBitmap(self.GetClientSize()[0], self.GetClientSize()[1],-1) self.blitmap = wx.EmptyBitmap(self.GetClientSize()[0], self.GetClientSize()[1],-1)
...@@ -178,6 +170,14 @@ class gviz(wx.Panel): ...@@ -178,6 +170,14 @@ class gviz(wx.Panel):
print"Inject code here..." print"Inject code here..."
print "Layer "+str(self.layerindex +1)+" - Z = "+str(self.layers[self.layerindex])+" mm" print "Layer "+str(self.layerindex +1)+" - Z = "+str(self.layers[self.layerindex])+" mm"
def clearhilights(self):
self.hilight.clear()
self.hilightarcs.clear()
while not self.hilightqueue.empty():
self.hilightqueue.get_nowait()
while not self.hilightarcsqueue.empty():
self.hilightarcsqueue.get_nowait()
def clear(self): def clear(self):
self.lastpos = [0, 0, 0, 0, 0, 0, 0] self.lastpos = [0, 0, 0, 0, 0, 0, 0]
self.lines = {} self.lines = {}
...@@ -185,47 +185,49 @@ class gviz(wx.Panel): ...@@ -185,47 +185,49 @@ class gviz(wx.Panel):
self.arcs = {} self.arcs = {}
self.arcpens = {} self.arcpens = {}
self.layers = [] self.layers = []
self.hilight = [] self.clearhilights()
self.hilightarcs = []
self.layerindex = 0 self.layerindex = 0
self.showall = 0 self.showall = 0
self.dirty = 1 self.dirty = 1
#self.repaint() wx.CallAfter(self.Refresh)
def layerup(self): def layerup(self):
if(self.layerindex+1<len(self.layers)): if self.layerindex + 1 < len(self.layers):
self.layerindex+=1 self.layerindex += 1
# Display layer info on statusbar (Jezmy) self.parent.SetStatusText("Layer %d - Going Up - Z = %.03f mm" % (self.layerindex + 1, self.layers[self.layerindex]), 0)
self.parent.SetStatusText("Layer "+str(self.layerindex +1)+" - Going Up - Z = "+str(self.layers[self.layerindex])+" mm", 0) self.dirty = 1
self.repaint() wx.CallAfter(self.Refresh)
self.Refresh()
def layerdown(self): def layerdown(self):
if(self.layerindex>0): if self.layerindex > 0:
self.layerindex-=1 self.layerindex -= 1
# Display layer info on statusbar (Jezmy) self.parent.SetStatusText("Layer %d - Going Down - Z = %.03f mm" % (self.layerindex + 1, self.layers[self.layerindex]), 0)
self.parent.SetStatusText("Layer "+str(self.layerindex + 1)+" - Going Down - Z = "+str(self.layers[self.layerindex])+ " mm", 0) self.dirty = 1
self.repaint() wx.CallAfter(self.Refresh)
self.Refresh()
def setlayer(self, layer): def setlayer(self, layer):
try: if layer in self.layers:
self.layerindex = self.layers.index(layer) self.layerindex = self.layers.index(layer)
self.repaint() self.dirty = 1
wx.CallAfter(self.Refresh)
self.showall = 0 self.showall = 0
except: wx.CallAfter(self.Refresh)
pass
def resize(self, event): def update_basescale(self):
size = self.GetClientSize() self.basescale = 2*[min(float(self.size[0] - 1)/self.build_dimensions[0],
size = [max(1.0, size[0]), max(1.0, size[1])] float(self.size[1] - 1)/self.build_dimensions[1])]
self.size = [max(1.0, self.size[0]), max(1.0, self.size[1])]
newsize = min(float(size[0])/self.size[0], float(size[1])/self.size[1])
self.size = self.GetClientSize()
wx.CallAfter(self.zoom, 0, 0, newsize)
def resize(self, event):
oldside = max(1.0, min(self.size))
self.size = self.GetClientSizeTuple()
self.update_basescale()
newside = max(1.0, min(self.size))
zoomratio = float(newside) / oldside
wx.CallLater(200, self.zoom, 0, 0, zoomratio)
def zoom(self, x, y, factor): def zoom(self, x, y, factor):
if x == -1 and y == -1:
side = min(self.size)
x = y = side / 2
self.scale = [s * factor for s in self.scale] self.scale = [s * factor for s in self.scale]
self.translate = [ x - (x-self.translate[0]) * factor, self.translate = [ x - (x-self.translate[0]) * factor,
...@@ -233,12 +235,35 @@ class gviz(wx.Panel): ...@@ -233,12 +235,35 @@ class gviz(wx.Panel):
penwidth = max(1.0, self.filament_width*((self.scale[0]+self.scale[1])/2.0)) penwidth = max(1.0, self.filament_width*((self.scale[0]+self.scale[1])/2.0))
for pen in self.penslist: for pen in self.penslist:
pen.SetWidth(penwidth) pen.SetWidth(penwidth)
#self.dirty = 1 self.dirty = 1
self.repaint() wx.CallAfter(self.Refresh)
self.Refresh()
def _line_scaler(self, x):
return (self.scale[0]*x[0]+self.translate[0],
self.scale[1]*x[1]+self.translate[1],
self.scale[0]*x[2]+self.translate[0],
self.scale[1]*x[3]+self.translate[1],)
def repaint(self): def _arc_scaler(self, x):
return (self.scale[0]*x[0]+self.translate[0],
self.scale[1]*x[1]+self.translate[1],
self.scale[0]*x[2]+self.translate[0],
self.scale[1]*x[3]+self.translate[1],
self.scale[0]*x[4]+self.translate[0],
self.scale[1]*x[5]+self.translate[1],)
def _drawlines(self, dc, lines, pens):
scaled_lines = map(self._line_scaler, lines)
dc.DrawLineList(scaled_lines, pens)
def _drawarcs(self, dc, arcs, pens):
scaled_arcs = map(self._arc_scaler, arcs)
dc.SetBrush(wx.TRANSPARENT_BRUSH)
for i in range(len(scaled_arcs)):
dc.SetPen(pens[i] if type(pens) == list else pens)
dc.DrawArc(*scaled_arcs[i])
def repaint_everything(self):
self.blitmap = wx.EmptyBitmap(self.GetClientSize()[0], self.GetClientSize()[1],-1) self.blitmap = wx.EmptyBitmap(self.GetClientSize()[0], self.GetClientSize()[1],-1)
dc = wx.MemoryDC() dc = wx.MemoryDC()
dc.SelectObject(self.blitmap) dc.SelectObject(self.blitmap)
...@@ -252,150 +277,194 @@ class gviz(wx.Panel): ...@@ -252,150 +277,194 @@ class gviz(wx.Panel):
for y in xrange(int(self.build_dimensions[1]/grid_unit)+1): for y in xrange(int(self.build_dimensions[1]/grid_unit)+1):
dc.DrawLine(self.translate[0], self.translate[1]+y*self.scale[1]*grid_unit, self.translate[0]+self.scale[0]*self.build_dimensions[0], self.translate[1]+y*self.scale[1]*grid_unit) dc.DrawLine(self.translate[0], self.translate[1]+y*self.scale[1]*grid_unit, self.translate[0]+self.scale[0]*self.build_dimensions[0], self.translate[1]+y*self.scale[1]*grid_unit)
dc.SetPen(wx.Pen(wx.Colour(0, 0, 0))) dc.SetPen(wx.Pen(wx.Colour(0, 0, 0)))
if not self.showall: if not self.showall:
self.size = self.GetSize()
dc.SetBrush(wx.Brush((43, 144, 255))) dc.SetBrush(wx.Brush((43, 144, 255)))
dc.DrawRectangle(self.size[0]-15, 0, 15, self.size[1]) dc.DrawRectangle(self.size[0]-15, 0, 15, self.size[1])
dc.SetBrush(wx.Brush((0, 255, 0))) dc.SetBrush(wx.Brush((0, 255, 0)))
if len(self.layers): if len(self.layers):
dc.DrawRectangle(self.size[0]-14, (1.0-(1.0*(self.layerindex+1))/len(self.layers))*self.size[1], 13, self.size[1]-1) dc.DrawRectangle(self.size[0]-14, (1.0-(1.0*(self.layerindex+1))/len(self.layers))*self.size[1], 13, self.size[1]-1)
def _drawlines(lines, pens):
def _scaler(x):
return (self.scale[0]*x[0]+self.translate[0],
self.scale[1]*x[1]+self.translate[1],
self.scale[0]*x[2]+self.translate[0],
self.scale[1]*x[3]+self.translate[1],)
scaled_lines = map(_scaler, lines)
dc.DrawLineList(scaled_lines, pens)
def _drawarcs(arcs, pens):
def _scaler(x):
return (self.scale[0]*x[0]+self.translate[0],
self.scale[1]*x[1]+self.translate[1],
self.scale[0]*x[2]+self.translate[0],
self.scale[1]*x[3]+self.translate[1],
self.scale[0]*x[4]+self.translate[0],
self.scale[1]*x[5]+self.translate[1],)
scaled_arcs = map(_scaler, arcs)
for i in range(len(scaled_arcs)):
dc.SetPen(pens[i] if type(pens).__name__ == 'list' else pens)
dc.SetBrush(wx.TRANSPARENT_BRUSH)
dc.DrawArc(*scaled_arcs[i])
if self.showall: if self.showall:
l = [] l = []
for i in self.layers: for i in self.layers:
dc.DrawLineList(l, self.fades[0]) self._drawlines(dc, self.lines[i], self.pens[i])
_drawlines(self.lines[i], self.pens[i]) self._drawarcs(dc, self.arcs[i], self.arcpens[i])
_drawarcs(self.arcs[i], self.arcpens[i])
return return
if self.layerindex<len(self.layers) and self.layers[self.layerindex] in self.lines.keys():
for layer_i in xrange(max(0, self.layerindex-6), self.layerindex):
#print i, self.layerindex, self.layerindex-i
_drawlines(self.lines[self.layers[layer_i]], self.fades[self.layerindex-layer_i-1])
_drawarcs(self.arcs[self.layers[layer_i]], self.fades[self.layerindex-layer_i-1])
_drawlines(self.lines[self.layers[self.layerindex]], self.pens[self.layers[self.layerindex]])
_drawarcs(self.arcs[self.layers[self.layerindex]], self.arcpens[self.layers[self.layerindex]])
_drawlines(self.hilight, self.hlpen) if self.layerindex < len(self.layers) and self.layers[self.layerindex] in self.lines:
_drawarcs(self.hilightarcs, self.hlpen) for layer_i in range(max(0, self.layerindex - 6), self.layerindex):
self._drawlines(dc, self.lines[self.layers[layer_i]], self.fades[self.layerindex - layer_i - 1])
self._drawarcs(dc, self.arcs[self.layers[layer_i]], self.fades[self.layerindex - layer_i - 1])
self._drawlines(dc, self.lines[self.layers[self.layerindex]], self.pens[self.layers[self.layerindex]])
self._drawarcs(dc, self.arcs[self.layers[self.layerindex]], self.arcpens[self.layers[self.layerindex]])
self._drawlines(dc, self.hilight, self.hlpen)
self._drawarcs(dc, self.hilightarcs, self.hlpen)
self.paint_hilights(dc)
dc.SelectObject(wx.NullBitmap) dc.SelectObject(wx.NullBitmap)
def paint_hilights(self, dc = None):
if self.hilightqueue.empty() and self.hilightarcsqueue.empty():
return
hl = []
if not dc:
dc = wx.MemoryDC()
dc.SelectObject(self.blitmap)
while not self.hilightqueue.empty():
hl.append(self.hilightqueue.get_nowait())
self._drawlines(dc, hl, self.hlpen)
hlarcs = []
while not self.hilightarcsqueue.empty():
hlarcs.append(self.hilightarcsqueue.get_nowait())
self._drawarcs(dc, hlarcs, self.hlpen)
def paint(self, event): def paint(self, event):
dc = wx.PaintDC(self) if self.dirty:
if(self.dirty):
self.repaint()
self.dirty = 0 self.dirty = 0
sz = self.GetClientSize() self.repaint_everything()
self.paint_hilights()
dc = wx.PaintDC(self)
dc.DrawBitmap(self.blitmap, 0, 0) dc.DrawBitmap(self.blitmap, 0, 0)
del dc
def addfile(self, gcodes = []): def addfile(self, gcode):
self.clear() self.clear()
for i in gcodes: self.add_parsed_gcodes(gcode.lines)
self.addgcode(i)
# FIXME : there's code duplication going on there, we should factor it (but
# the reason addgcode is not factored as a add_parsed_gcodes([gline]) is
# because when loading a file there's no hilight, so it simply lets us not
# do the if hilight: all the time for nothing when loading a lot of lines
def add_parsed_gcodes(self, lines):
def _y(y):
return self.build_dimensions[1] - (y - self.build_dimensions[4])
def _x(x):
return x - self.build_dimensions[3]
for gline in lines:
if gline.command not in ["G0", "G1", "G2", "G3"]:
continue
target = self.lastpos[:]
target[5] = 0.0
target[6] = 0.0
if gline.relative:
if gline.x != None: target[0] += gline.x
if gline.y != None: target[1] += gline.y
if gline.z != None: target[2] += gline.z
else:
if gline.x != None: target[0] = gline.x
if gline.y != None: target[1] = gline.y
if gline.z != None: target[2] = gline.z
if gline.e != None:
if gline.relative_e:
target[3] += gline.e
else:
target[3] = gline.e
if gline.f != None: target[4] = gline.f
if gline.i != None: target[5] = gline.i
if gline.j != None: target[6] = gline.j
z = target[2]
if z not in self.layers:
self.lines[z] = []
self.pens[z] = []
self.arcs[z] = []
self.arcpens[z] = []
self.layers.append(z)
start_pos = self.lastpos[:]
if gline.command in ["G0", "G1"]:
self.lines[z].append((_x(start_pos[0]), _y(start_pos[1]), _x(target[0]), _y(target[1])))
self.pens[z].append(self.mainpen if target[3] != self.lastpos[3] else self.travelpen)
elif gline.command in ["G2", "G3"]:
# startpos, endpos, arc center
arc = [_x(start_pos[0]), _y(start_pos[1]),
_x(target[0]), _y(target[1]),
_x(start_pos[0] + target[5]), _y(start_pos[1] + target[6])]
if gline.command == "G2": # clockwise, reverse endpoints
arc[0], arc[1], arc[2], arc[3] = arc[2], arc[3], arc[0], arc[1]
self.arcs[z].append(arc)
self.arcpens[z].append(self.arcpen)
self.lastpos = target
self.dirty = 1
self.Refresh()
def addgcode(self, gcode = "M105", hilight = 0): def addgcode(self, gcode = "M105", hilight = 0):
gcode = gcode.split("*")[0] gcode = gcode.split("*")[0]
gcode = gcode.split(";")[0] gcode = gcode.split(";")[0]
gcode = gcode.lower().strip().split() gcode = gcode.lower().strip()
if len(gcode) == 0: if not gcode:
return return
if gcode[0][0] == 'n': gline = gcoder.Line(gcode)
gcode.pop(0) gline.parse_coordinates(False)
def _readgcode():
target = self.lastpos[:]
target[5]=0.0
target[6]=0.0
if hilight:
target = self.hilightpos[:]
for i in gcode:
if i[0]=="x":
target[0]=float(i[1:])
elif i[0]=="y":
target[1]=float(i[1:])
elif i[0]=="z":
target[2]=float(i[1:])
elif i[0]=="e":
target[3]=float(i[1:])
elif i[0]=="f":
target[4]=float(i[1:])
elif i[0]=="i":
target[5]=float(i[1:])
elif i[0]=="j":
target[6]=float(i[1:])
if not hilight:
if not target[2] in self.lines.keys():
self.lines[target[2]]=[]
self.pens[target[2]]=[]
self.arcs[target[2]]=[]
self.arcpens[target[2]]=[]
self.layers+=[target[2]]
return target
def _y(y): def _y(y):
return self.build_dimensions[1]-(y-self.build_dimensions[4]) return self.build_dimensions[1] - (y - self.build_dimensions[4])
def _x(x): def _x(x):
return x-self.build_dimensions[3] return x - self.build_dimensions[3]
if gline.command not in ["G0", "G1", "G2", "G3"]:
return
start_pos = self.hilightpos[:] if hilight else self.lastpos[:] start_pos = self.hilightpos[:] if hilight else self.lastpos[:]
if gcode[0] in [ "g0", "g1" ]: target = start_pos[:]
target = _readgcode() target[5] = 0.0
line = [ _x(start_pos[0]), _y(start_pos[1]), _x(target[0]), _y(target[1]) ] target[6] = 0.0
if gline.x != None: target[0] = gline.x
if gline.y != None: target[1] = gline.y
if gline.z != None: target[2] = gline.z
if gline.e != None: target[3] = gline.e
if gline.f != None: target[4] = gline.f
if gline.i != None: target[5] = gline.i
if gline.j != None: target[6] = gline.j
z = target[2]
if not hilight and z not in self.layers:
self.lines[z] = []
self.pens[z] = []
self.arcs[z] = []
self.arcpens[z] = []
self.layers.append(z)
if gline.command in ["G0", "G1"]:
line = [_x(start_pos[0]), _y(start_pos[1]), _x(target[0]), _y(target[1])]
if not hilight: if not hilight:
self.lines[ target[2] ] += [line] self.lines[z].append((_x(start_pos[0]), _y(start_pos[1]), _x(target[0]), _y(target[1])))
self.pens[ target[2] ] += [self.mainpen if target[3] != self.lastpos[3] else self.travelpen] self.pens[z].append(self.mainpen if target[3] != self.lastpos[3] else self.travelpen)
self.lastpos = target
else: else:
self.hilight += [line] self.hilight.append(line)
self.hilightpos = target self.hilightqueue.put_nowait(line)
self.dirty = 1 elif gline.command in ["G2", "G3"]:
# startpos, endpos, arc center
if gcode[0] in [ "g2", "g3" ]: arc = [_x(start_pos[0]), _y(start_pos[1]),
target = _readgcode() _x(target[0]), _y(target[1]),
arc = [] _x(start_pos[0] + target[5]), _y(start_pos[1] + target[6])]
arc += [ _x(start_pos[0]), _y(start_pos[1]) ] if gline.command == "G2": # clockwise, reverse endpoints
arc += [ _x(target[0]), _y(target[1]) ]
arc += [ _x(start_pos[0] + target[5]), _y(start_pos[1] + target[6]) ] # center
if gcode[0] == "g2": # clockwise, reverse endpoints
arc[0], arc[1], arc[2], arc[3] = arc[2], arc[3], arc[0], arc[1] arc[0], arc[1], arc[2], arc[3] = arc[2], arc[3], arc[0], arc[1]
if not hilight: if not hilight:
self.arcs[ target[2] ] += [arc] self.arcs[z].append(arc)
self.arcpens[ target[2] ] += [self.arcpen] self.arcpens[z].append(self.arcpen)
else:
self.hilightarcs.append(arc)
self.hilightarcsqueue.put_nowait(arc)
if not hilight:
self.lastpos = target self.lastpos = target
self.dirty = 1
else: else:
self.hilightarcs += [arc]
self.hilightpos = target self.hilightpos = target
self.dirty = 1 self.Refresh()
if __name__ == '__main__': if __name__ == '__main__':
import sys
app = wx.App(False) app = wx.App(False)
#main = window(open("/home/kliment/designs/spinner/arm_export.gcode")) main = window(open(sys.argv[1]))
main = window(open("jam.gcode"))
main.Show() main.Show()
app.MainLoop() app.MainLoop()
# -*- coding: utf-8 -*-
# Copyright (C) 2013 Guillaume Seguin
# Copyright (C) 2011 Denis Kobozev
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import time
import numpy
import math
import logging
from pyglet.gl import *
from pyglet import gl
from pyglet.graphics.vertexbuffer import create_buffer, VertexBufferObject
from printrun.printrun_utils import install_locale
install_locale('pronterface')
def compile_display_list(func, *options):
display_list = glGenLists(1)
glNewList(display_list, GL_COMPILE)
func(*options)
glEndList()
return display_list
def numpy2vbo(nparray, target = GL_ARRAY_BUFFER, usage = GL_STATIC_DRAW, use_vbos = True):
vbo = create_buffer(nparray.nbytes, target = target, usage = usage, vbo = use_vbos)
vbo.bind()
vbo.set_data(nparray.ctypes.data)
return vbo
class BoundingBox(object):
"""
A rectangular box (cuboid) enclosing a 3D model, defined by lower and upper corners.
"""
def __init__(self, upper_corner, lower_corner):
self.upper_corner = upper_corner
self.lower_corner = lower_corner
@property
def width(self):
width = abs(self.upper_corner[0] - self.lower_corner[0])
return round(width, 2)
@property
def depth(self):
depth = abs(self.upper_corner[1] - self.lower_corner[1])
return round(depth, 2)
@property
def height(self):
height = abs(self.upper_corner[2] - self.lower_corner[2])
return round(height, 2)
class Platform(object):
"""
Platform on which models are placed.
"""
graduations_major = 10
def __init__(self, build_dimensions):
self.width = build_dimensions[0]
self.depth = build_dimensions[1]
self.height = build_dimensions[2]
self.xoffset = build_dimensions[3]
self.yoffset = build_dimensions[4]
self.zoffset = build_dimensions[5]
self.color_grads_minor = (0xaf / 255, 0xdf / 255, 0x5f / 255, 0.1)
self.color_grads_interm = (0xaf / 255, 0xdf / 255, 0x5f / 255, 0.2)
self.color_grads_major = (0xaf / 255, 0xdf / 255, 0x5f / 255, 0.33)
self.color_fill = (0xaf / 255, 0xdf / 255, 0x5f / 255, 0.05)
self.initialized = False
self.loaded = True
def init(self):
self.display_list = compile_display_list(self.draw)
self.initialized = True
def draw(self):
glPushMatrix()
glTranslatef(self.xoffset, self.yoffset, self.zoffset)
def color(i):
if i % self.graduations_major == 0:
glColor4f(*self.color_grads_major)
elif i % (self.graduations_major / 2) == 0:
glColor4f(*self.color_grads_interm)
else:
glColor4f(*self.color_grads_minor)
# draw the grid
glBegin(GL_LINES)
for i in range(0, int(math.ceil(self.width + 1))):
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))):
color(i)
glVertex3f(0, float(i), 0.0)
glVertex3f(self.width, float(i), 0.0)
glEnd()
# draw fill
glColor4f(*self.color_fill)
glRectf(0.0, 0.0, float(self.width), float(self.depth))
glPopMatrix()
def display(self, mode_2d=False):
glCallList(self.display_list)
class PrintHead(object):
def __init__(self):
self.color = (43. / 255, 0., 175. / 255, 1.0)
self.scale = 5
self.height = 5
self.initialized = False
self.loaded = True
def init(self):
self.display_list = compile_display_list(self.draw)
self.initialized = True
def draw(self):
glPushMatrix()
glBegin(GL_LINES)
glColor4f(*self.color)
for di in [-1, 1]:
for dj in [-1, 1]:
glVertex3f(0, 0, 0)
glVertex3f(self.scale * di, self.scale * dj, self.height)
glEnd()
glPopMatrix()
def display(self, mode_2d=False):
glEnable(GL_LINE_SMOOTH)
orig_linewidth = (GLfloat)()
glGetFloatv(GL_LINE_WIDTH, orig_linewidth)
glLineWidth(3.0)
glCallList(self.display_list)
glLineWidth(orig_linewidth)
glDisable(GL_LINE_SMOOTH)
class Model(object):
"""
Parent class for models that provides common functionality.
"""
AXIS_X = (1, 0, 0)
AXIS_Y = (0, 1, 0)
AXIS_Z = (0, 0, 1)
letter_axis_map = {
'x': AXIS_X,
'y': AXIS_Y,
'z': AXIS_Z,
}
axis_letter_map = dict([(v, k) for k, v in letter_axis_map.items()])
def __init__(self, offset_x=0, offset_y=0):
self.offset_x = offset_x
self.offset_y = offset_y
self.init_model_attributes()
def init_model_attributes(self):
"""
Set/reset saved properties.
"""
self.invalidate_bounding_box()
self.modified = False
def invalidate_bounding_box(self):
self._bounding_box = None
@property
def bounding_box(self):
"""
Get a bounding box for the model.
"""
if self._bounding_box is None:
self._bounding_box = self._calculate_bounding_box()
return self._bounding_box
def _calculate_bounding_box(self):
"""
Calculate an axis-aligned box enclosing the model.
"""
# swap rows and columns in our vertex arrays so that we can do max and
# min on axis 1
xyz_rows = self.vertices.reshape(-1, order='F').reshape(3, -1)
lower_corner = xyz_rows.min(1)
upper_corner = xyz_rows.max(1)
box = BoundingBox(upper_corner, lower_corner)
return box
@property
def width(self):
return self.bounding_box.width
@property
def depth(self):
return self.bounding_box.depth
@property
def height(self):
return self.bounding_box.height
def movement_angle(src, dst, precision=0):
x = dst[0] - src[0]
y = dst[1] - src[1]
angle = math.degrees(math.atan2(y, -x)) # negate x for clockwise rotation angle
return round(angle, precision)
class GcodeModel(Model):
"""
Model for displaying Gcode data.
"""
color_travel = (0.6, 0.6, 0.6, 0.6)
color_tool0 = (1.0, 0.0, 0.0, 0.6)
color_tool1 = (0.0, 0.0, 1.0, 0.6)
color_printed = (0.2, 0.75, 0, 0.6)
use_vbos = True
loaded = False
def load_data(self, model_data, callback=None):
t_start = time.time()
vertex_list = []
color_list = []
self.layer_stops = [0]
arrow_list = []
num_layers = len(model_data.all_layers)
prev_pos = (0, 0, 0)
for layer_idx, layer in enumerate(model_data.all_layers):
for gline in layer.lines:
if not gline.is_move:
continue
vertex_list.append(prev_pos)
current_pos = (gline.current_x, gline.current_y, gline.current_z)
vertex_list.append(current_pos)
vertex_color = self.movement_color(gline)
color_list.append(vertex_color)
prev_pos = current_pos
gline.gcview_end_vertex = len(vertex_list)
self.layer_stops.append(len(vertex_list))
if callback:
callback(layer_idx + 1, num_layers)
self.vertices = numpy.array(vertex_list, dtype = GLfloat)
self.colors = numpy.array(color_list, dtype = GLfloat).repeat(2, 0)
self.max_layers = len(self.layer_stops) - 1
self.num_layers_to_draw = self.max_layers
self.printed_until = -1
self.initialized = False
self.loaded = True
t_end = time.time()
logging.log(logging.INFO, _('Initialized 3D visualization in %.2f seconds') % (t_end - t_start))
logging.log(logging.INFO, _('Vertex count: %d') % len(self.vertices))
def copy(self):
copy = GcodeModel()
for var in ["vertices", "colors", "max_layers", "num_layers_to_draw", "printed_until", "layer_stops"]:
setattr(copy, var, getattr(self, var))
copy.loaded = True
copy.initialized = False
return copy
def movement_color(self, move):
"""
Return the color to use for particular type of movement.
"""
if move.extruding:
if move.current_tool == 0:
return self.color_tool0
else:
return self.color_tool1
return self.color_travel
# ------------------------------------------------------------------------
# DRAWING
# ------------------------------------------------------------------------
def init(self):
self.vertex_buffer = numpy2vbo(self.vertices, use_vbos = self.use_vbos)
self.vertex_color_buffer = numpy2vbo(self.colors, use_vbos = self.use_vbos) # each pair of vertices shares the color
self.initialized = True
def display(self, mode_2d=False):
glPushMatrix()
glTranslatef(self.offset_x, self.offset_y, 0)
glEnableClientState(GL_VERTEX_ARRAY)
glEnableClientState(GL_COLOR_ARRAY)
self._display_movements(mode_2d)
glDisableClientState(GL_COLOR_ARRAY)
glDisableClientState(GL_VERTEX_ARRAY)
glPopMatrix()
def _display_movements(self, mode_2d=False):
self.vertex_buffer.bind()
has_vbo = isinstance(self.vertex_buffer, VertexBufferObject)
if has_vbo:
glVertexPointer(3, GL_FLOAT, 0, None)
else:
glVertexPointer(3, GL_FLOAT, 0, self.vertex_buffer.ptr)
if mode_2d:
glScale(1.0, 1.0, 0.0) # discard z coordinates
start = self.layer_stops[self.num_layers_to_draw - 1]
end = self.layer_stops[self.num_layers_to_draw] - start
else: # 3d
start = 0
end = self.layer_stops[self.num_layers_to_draw]
glDisableClientState(GL_COLOR_ARRAY)
glColor4f(*self.color_printed)
printed_end = min(self.printed_until, end)
if start < printed_end:
glDrawArrays(GL_LINES, start, printed_end)
glEnableClientState(GL_COLOR_ARRAY)
self.vertex_color_buffer.bind()
if has_vbo:
glColorPointer(4, GL_FLOAT, 0, None)
else:
glColorPointer(4, GL_FLOAT, 0, self.vertex_color_buffer.ptr)
start = max(self.printed_until, 0)
end = end - start
if start >= 0 and end > 0:
glDrawArrays(GL_LINES, start, end)
self.vertex_buffer.unbind()
self.vertex_color_buffer.unbind()
...@@ -56,3 +56,42 @@ def sharedfile(filename): ...@@ -56,3 +56,42 @@ def sharedfile(filename):
def configfile(filename): def configfile(filename):
return lookup_file(filename, [os.path.expanduser("~/.printrun/"),]) return lookup_file(filename, [os.path.expanduser("~/.printrun/"),])
class RemainingTimeEstimator(object):
drift = None
gcode = None
def __init__(self, gcode):
self.drift = 1
self.previous_layers_estimate = 0
self.current_layer_estimate = 0
self.current_layer_lines = 0
self.gcode = gcode
self.remaining_layers_estimate = sum(layer.duration for layer in gcode.all_layers)
if len(gcode) > 0:
self.update_layer(0, 0)
def update_layer(self, layer, printtime):
self.previous_layers_estimate += self.current_layer_estimate
if self.previous_layers_estimate > 0 and printtime > 0:
self.drift = printtime / self.previous_layers_estimate
self.current_layer_estimate = self.gcode.all_layers[layer].duration
self.current_layer_lines = len(self.gcode.all_layers[layer].lines)
self.remaining_layers_estimate -= self.current_layer_estimate
self.last_idx = -1
self.last_estimate = None
def __call__(self, idx, printtime):
if not self.current_layer_lines:
return (0, 0)
if idx == self.last_idx:
return self.last_estimate
layer, line = self.gcode.idxs(idx)
layer_progress = (1 - (float(line+1) / self.current_layer_lines))
remaining = layer_progress * self.current_layer_estimate + self.remaining_layers_estimate
estimate = self.drift * remaining
total = estimate + printtime
self.last_idx = idx
self.last_estimate = (estimate, total)
return self.last_estimate
...@@ -15,21 +15,30 @@ ...@@ -15,21 +15,30 @@
import xml.etree.ElementTree import xml.etree.ElementTree
import wx import wx
import wx.lib.agw.floatspin as floatspin
import os import os
import time
import zipfile import zipfile
import tempfile import tempfile
import shutil import shutil
import svg.document as wxpsvgdocument from printrun.cairosvg.surface import PNGSurface
import cStringIO
import imghdr import imghdr
import copy
class dispframe(wx.Frame): import re
def __init__(self, parent, title, res = (800, 600), printer = None): from collections import OrderedDict
wx.Frame.__init__(self, parent = parent, title = title) import itertools
self.p = printer
class DisplayFrame(wx.Frame):
def __init__(self, parent, title, res=(1024, 768), printer=None, scale=1.0, offset=(0,0)):
wx.Frame.__init__(self, parent=parent, title=title, size=res)
self.printer = printer
self.control_frame = parent
self.pic = wx.StaticBitmap(self) self.pic = wx.StaticBitmap(self)
self.bitmap = wx.EmptyBitmap(*res) self.bitmap = wx.EmptyBitmap(*res)
self.bbitmap = wx.EmptyBitmap(*res) self.bbitmap = wx.EmptyBitmap(*res)
self.slicer = 'Skeinforge' self.slicer = 'bitmap'
self.dpi = 96
dc = wx.MemoryDC() dc = wx.MemoryDC()
dc.SelectObject(self.bbitmap) dc.SelectObject(self.bbitmap)
dc.SetBackground(wx.Brush("black")) dc.SetBackground(wx.Brush("black"))
...@@ -38,145 +47,447 @@ class dispframe(wx.Frame): ...@@ -38,145 +47,447 @@ class dispframe(wx.Frame):
self.SetBackgroundColour("black") self.SetBackgroundColour("black")
self.pic.Hide() self.pic.Hide()
self.pen = wx.Pen("white")
self.brush = wx.Brush("white")
self.SetDoubleBuffered(True) self.SetDoubleBuffered(True)
self.SetPosition((self.control_frame.GetSize().x, 0))
self.Show() self.Show()
def drawlayer(self, image): self.scale = scale
self.index = 0
self.size = res
self.offset = offset
self.running = False
self.layer_red = False
def clear_layer(self):
try: try:
dc = wx.MemoryDC() dc = wx.MemoryDC()
dc.SelectObject(self.bitmap) dc.SelectObject(self.bitmap)
dc.SetBackground(wx.Brush("black")) dc.SetBackground(wx.Brush("black"))
dc.Clear() dc.Clear()
dc.SetPen(self.pen) self.pic.SetBitmap(self.bitmap)
dc.SetBrush(self.brush) self.pic.Show()
self.Refresh()
if self.slicer == 'Skeinforge': except:
for i in image: raise
#print i pass
points = [wx.Point(*map(lambda x:int(round(float(x) * self.scale)), j.strip().split())) for j in i.strip().split("M")[1].split("L")]
dc.DrawPolygon(points, self.size[0] / 2, self.size[1] / 2) def resize(self, res=(1024, 768)):
elif self.slicer == 'Slic3r': self.bitmap = wx.EmptyBitmap(*res)
gc = wx.GraphicsContext_Create(dc) self.bbitmap = wx.EmptyBitmap(*res)
gc.Translate(*self.offset) dc = wx.MemoryDC()
gc.Scale(self.scale, self.scale) dc.SelectObject(self.bbitmap)
wxpsvgdocument.SVGDocument(image).render(gc) dc.SetBackground(wx.Brush("black"))
dc.Clear()
dc.SelectObject(wx.NullBitmap)
def draw_layer(self, image):
try:
dc = wx.MemoryDC()
dc.SelectObject(self.bitmap)
dc.SetBackground(wx.Brush("black"))
dc.Clear()
if self.slicer == 'Slic3r' or self.slicer == 'Skeinforge':
if int(self.scale) != 1:
layercopy = copy.deepcopy(image)
height = float(layercopy.get('height').replace('m',''))
width = float(layercopy.get('width').replace('m',''))
layercopy.set('height', str(height*self.scale) + 'mm')
layercopy.set('width', str(width*self.scale) + 'mm')
layercopy.set('viewBox', '0 0 ' + str(height*self.scale) + ' ' + str(width*self.scale))
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)))
else:
stream = cStringIO.StringIO(PNGSurface.convert(dpi=self.dpi, bytestring=xml.etree.ElementTree.tostring(image)))
image = wx.ImageFromStream(stream)
if self.layer_red:
image = image.AdjustChannels(1,0,0,1)
dc.DrawBitmap(wx.BitmapFromImage(image), self.offset[0], self.offset[1], True)
elif self.slicer == 'bitmap': elif self.slicer == 'bitmap':
dc.DrawBitmap(image, self.offset[0], -self.offset[1], True) if isinstance(image, str):
image = wx.Image(image)
if self.layer_red:
image = image.AdjustChannels(1,0,0,1)
dc.DrawBitmap(wx.BitmapFromImage(image.Scale(image.Width * self.scale, image.Height * self.scale)), self.offset[0], -self.offset[1], True)
else: else:
raise Exception(self.slicer + " is an unknown method.") raise Exception(self.slicer + " is an unknown method.")
self.pic.SetBitmap(self.bitmap) self.pic.SetBitmap(self.bitmap)
self.pic.Show() self.pic.Show()
self.Refresh() self.Refresh()
except: except:
raise raise
pass pass
def showimgdelay(self, image): def show_img_delay(self, image):
self.drawlayer(image) print "Showing "+ str(time.clock())
self.pic.Show() self.control_frame.set_current_layer(self.index)
self.Refresh() self.draw_layer(image)
wx.FutureCall(1000 * self.interval, self.hide_pic_and_rise)
self.Refresh() def rise(self):
if self.p != None and self.p.online: if (self.direction == "Top Down"):
self.p.send_now("G91") print "Lowering "+ str(time.clock())
self.p.send_now("G1 Z%f F300" % (self.thickness,)) else:
self.p.send_now("G90") print "Rising "+ str(time.clock())
def nextimg(self, event): if self.printer != None and self.printer.online:
if self.index < len(self.layers): self.printer.send_now("G91")
i = self.index
if (self.prelift_gcode):
for line in self.prelift_gcode.split('\n'):
if line:
self.printer.send_now(line)
if (self.direction == "Top Down"):
self.printer.send_now("G1 Z-%f F%g" % (self.overshoot,self.z_axis_rate,))
self.printer.send_now("G1 Z%f F%g" % (self.overshoot-self.thickness,self.z_axis_rate,))
else: # self.direction == "Bottom Up"
self.printer.send_now("G1 Z%f F%g" % (self.overshoot,self.z_axis_rate,))
self.printer.send_now("G1 Z-%f F%g" % (self.overshoot-self.thickness,self.z_axis_rate,))
if (self.postlift_gcode):
for line in self.postlift_gcode.split('\n'):
if line:
self.printer.send_now(line)
self.printer.send_now("G90")
else:
time.sleep(self.pause)
wx.FutureCall(1000 * self.pause, self.next_img)
print i def hide_pic(self):
wx.CallAfter(self.showimgdelay, self.layers[i]) print "Hiding "+ str(time.clock())
wx.FutureCall(1000 * self.interval, self.pic.Hide) self.pic.Hide()
def hide_pic_and_rise(self):
wx.CallAfter(self.hide_pic)
wx.FutureCall(500, self.rise)
def next_img(self):
if not self.running:
return
if self.index < len(self.layers):
print self.index
wx.CallAfter(self.show_img_delay, self.layers[self.index])
self.index += 1 self.index += 1
else: else:
print "end" print "end"
wx.CallAfter(self.pic.Hide) wx.CallAfter(self.pic.Hide)
wx.CallAfter(self.Refresh) wx.CallAfter(self.Refresh)
wx.CallAfter(self.ShowFullScreen, 0)
wx.CallAfter(self.timer.Stop)
def present(self, layers, interval = 0.5, pause = 0.2, thickness = 0.4, scale = 20, size = (800, 600), offset = (0, 0)): def present(self,
layers,
interval=0.5,
pause=0.2,
overshoot=0.0,
z_axis_rate=200,
prelift_gcode="",
postlift_gcode="",
direction="Top Down",
thickness=0.4,
scale=1,
size=(1024, 768),
offset=(0, 0),
layer_red=False):
wx.CallAfter(self.pic.Hide) wx.CallAfter(self.pic.Hide)
wx.CallAfter(self.Refresh) wx.CallAfter(self.Refresh)
self.layers = layers self.layers = layers
self.scale = scale self.scale = scale
self.thickness = thickness self.thickness = thickness
self.index = 0 self.size = size
self.size = (size[0] + offset[0], size[1] + offset[1])
self.interval = interval self.interval = interval
self.pause = pause
self.overshoot = overshoot
self.z_axis_rate = z_axis_rate
self.prelift_gcode = prelift_gcode
self.postlift_gcode = postlift_gcode
self.direction = direction
self.layer_red = layer_red
self.offset = offset self.offset = offset
self.timer = wx.Timer(self, 1) self.index = 0
self.timer.Bind(wx.EVT_TIMER, self.nextimg) self.running = True
self.Bind(wx.EVT_TIMER, self.nextimg)
self.timer.Start(1000 * interval + 1000 * pause)
class setframe(wx.Frame): self.next_img()
def __init__(self, parent, printer = None): class SettingsFrame(wx.Frame):
wx.Frame.__init__(self, parent, title = "Projector setup")
self.f = dispframe(None, "", printer = printer) def _set_setting(self, name, value):
self.panel = wx.Panel(self) if self.pronterface:
self.panel.SetBackgroundColour("orange") self.pronterface.set(name,value)
self.bload = wx.Button(self.panel, -1, "Load", pos = (0, 0))
self.bload.Bind(wx.EVT_BUTTON, self.loadfile) def _get_setting(self,name, val):
if self.pronterface:
try:
return getattr(self.pronterface.settings, name)
except AttributeError, x:
return val
else:
return val
def __init__(self, parent, printer=None):
wx.Frame.__init__(self, parent, title="ProjectLayer Control",style=(wx.DEFAULT_FRAME_STYLE | wx.WS_EX_CONTEXTHELP))
self.SetExtraStyle(wx.FRAME_EX_CONTEXTHELP)
self.pronterface = parent
self.display_frame = DisplayFrame(self, title="ProjectLayer Display", printer=printer)
wx.StaticText(self.panel, -1, "Layer:", pos = (0, 30)) self.panel = wx.Panel(self)
wx.StaticText(self.panel, -1, "mm", pos = (130, 30))
self.thickness = wx.TextCtrl(self.panel, -1, "0.5", pos = (50, 30))
wx.StaticText(self.panel, -1, "Exposure:", pos = (0, 60)) vbox = wx.BoxSizer(wx.VERTICAL)
wx.StaticText(self.panel, -1, "s", pos = (130, 60)) buttonbox = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Controls"), wx.HORIZONTAL)
self.interval = wx.TextCtrl(self.panel, -1, "0.5", pos = (50, 60))
load_button = wx.Button(self.panel, -1, "Load")
load_button.Bind(wx.EVT_BUTTON, self.load_file)
load_button.SetHelpText("Choose an SVG file created from Slic3r or Skeinforge, or a zip file of bitmap images (with extension: .3dlp.zip).")
buttonbox.Add(load_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
present_button = wx.Button(self.panel, -1, "Present")
present_button.Bind(wx.EVT_BUTTON, self.start_present)
present_button.SetHelpText("Starts the presentation of the slices.")
buttonbox.Add(present_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
self.pause_button = wx.Button(self.panel, -1, "Pause")
self.pause_button.Bind(wx.EVT_BUTTON, self.pause_present)
self.pause_button.SetHelpText("Pauses the presentation. Can be resumed afterwards by clicking this button, or restarted by clicking present again.")
buttonbox.Add(self.pause_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
stop_button = wx.Button(self.panel, -1, "Stop")
stop_button.Bind(wx.EVT_BUTTON, self.stop_present)
stop_button.SetHelpText("Stops presenting the slices.")
buttonbox.Add(stop_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
self.help_button = wx.ContextHelpButton(self.panel)
buttonbox.Add(self.help_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
fieldboxsizer = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Settings"), wx.VERTICAL)
fieldsizer = wx.GridBagSizer(10,10)
# 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.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.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.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.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.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.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))
fieldsizer.Add(wx.StaticText(self.panel, -1, "Pre-lift Gcode:"), pos=(6, 0), flag=wx.ALIGN_CENTER_VERTICAL)
self.prelift_gcode = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_prelift_gcode", "").replace("\\n",'\n')), size=(-1, 35), style=wx.TE_MULTILINE)
self.prelift_gcode.SetHelpText("Additional gcode to run before raising the Z axis. Be sure to take into account any additional time needed in the pause value, and be careful what gcode is added!")
self.prelift_gcode.Bind(wx.EVT_TEXT, self.update_prelift_gcode)
fieldsizer.Add(self.prelift_gcode, pos=(6, 1), span=(2,1))
fieldsizer.Add(wx.StaticText(self.panel, -1, "Post-lift Gcode:"), pos=(6, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.postlift_gcode = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_postlift_gcode", "").replace("\\n",'\n')), size=(-1, 35), style=wx.TE_MULTILINE)
self.postlift_gcode.SetHelpText("Additional gcode to run after raising the Z axis. Be sure to take into account any additional time needed in the pause value, and be careful what gcode is added!")
self.postlift_gcode.Bind(wx.EVT_TEXT, self.update_postlift_gcode)
fieldsizer.Add(self.postlift_gcode, pos=(6, 3), span=(2,1))
# Right Column
fieldsizer.Add(wx.StaticText(self.panel, -1, "X (px):"), pos=(0, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.X = wx.SpinCtrl(self.panel, -1, str(int(self._get_setting("project_x", 1024))), max=999999, size=(80,-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)
self.Y = wx.SpinCtrl(self.panel, -1, str(int(self._get_setting("project_y", 768))), max=999999, size=(80,-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.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.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", 415.0), increment=1, digits=1, size=(80,-1))
self.projected_X_mm.Bind(floatspin.EVT_FLOATSPIN, 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.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))
fieldboxsizer.Add(fieldsizer)
# Display
displayboxsizer = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Display"), wx.VERTICAL)
displaysizer = wx.GridBagSizer(10,10)
displaysizer.Add(wx.StaticText(self.panel, -1, "Fullscreen:"), pos=(0,0), flag=wx.ALIGN_CENTER_VERTICAL)
self.fullscreen = wx.CheckBox(self.panel, -1)
self.fullscreen.Bind(wx.EVT_CHECKBOX, self.update_fullscreen)
self.fullscreen.SetHelpText("Toggles the project screen to full size.")
displaysizer.Add(self.fullscreen, pos=(0, 1), flag=wx.ALIGN_CENTER_VERTICAL)
displaysizer.Add(wx.StaticText(self.panel, -1, "Calibrate:"), pos=(0,2), flag=wx.ALIGN_CENTER_VERTICAL)
self.calibrate = wx.CheckBox(self.panel, -1)
self.calibrate.Bind(wx.EVT_CHECKBOX, self.show_calibrate)
self.calibrate.SetHelpText("Toggles the calibration grid. Each grid should be 10mmx10mm in size. Use the grid to ensure the projected size is correct. See also the help for the ProjectedX field.")
displaysizer.Add(self.calibrate, pos=(0,3), flag=wx.ALIGN_CENTER_VERTICAL)
displaysizer.Add(wx.StaticText(self.panel, -1, "1st Layer:"), pos=(0,4), flag=wx.ALIGN_CENTER_VERTICAL)
first_layer_boxer = wx.BoxSizer(wx.HORIZONTAL)
self.first_layer = wx.CheckBox(self.panel, -1)
self.first_layer.Bind(wx.EVT_CHECKBOX, self.show_first_layer)
self.first_layer.SetHelpText("Displays the first layer of the model. Use this to project the first layer for longer so it holds to the base. Note: this value does not affect the first layer when the \"Present\" run is started, it should be used manually.")
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.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)
displaysizer.Add(wx.StaticText(self.panel, -1, "Red:"), pos=(0,7), flag=wx.ALIGN_CENTER_VERTICAL)
self.layer_red = wx.CheckBox(self.panel, -1)
self.layer_red.Bind(wx.EVT_CHECKBOX, self.show_layer_red)
self.layer_red.SetHelpText("Toggles whether the image should be red. Useful for positioning whilst resin is in the printer as it should not cause a reaction.")
displaysizer.Add(self.layer_red, pos=(0,8), flag=wx.ALIGN_CENTER_VERTICAL)
displayboxsizer.Add(displaysizer)
# Info
infosizer = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Info"), wx.VERTICAL)
infofieldsizer = wx.GridBagSizer(10,10)
filelabel = wx.StaticText(self.panel, -1, "File:")
filelabel.SetHelpText("The name of the model currently loaded.")
infofieldsizer.Add(filelabel, pos=(0,0))
self.filename = wx.StaticText(self.panel, -1, "")
wx.StaticText(self.panel, -1, "Blank:", pos = (0, 90)) infofieldsizer.Add(self.filename, pos=(0,1))
wx.StaticText(self.panel, -1, "s", pos = (130, 90))
self.delay = wx.TextCtrl(self.panel, -1, "0.5", pos = (50, 90))
wx.StaticText(self.panel, -1, "Scale:", pos = (0, 120)) totallayerslabel = wx.StaticText(self.panel, -1, "Total Layers:")
wx.StaticText(self.panel, -1, "x", pos = (130, 120)) totallayerslabel.SetHelpText("The total number of layers found in the model.")
self.scale = wx.TextCtrl(self.panel, -1, "5", pos = (50, 120)) infofieldsizer.Add(totallayerslabel, pos=(1,0))
self.total_layers = wx.StaticText(self.panel, -1)
wx.StaticText(self.panel, -1, "X:", pos = (160, 30)) infofieldsizer.Add(self.total_layers, pos=(1,1))
self.X = wx.TextCtrl(self.panel, -1, "1024", pos = (210, 30))
wx.StaticText(self.panel, -1, "Y:", pos = (160, 60)) currentlayerlabel = wx.StaticText(self.panel, -1, "Current Layer:")
self.Y = wx.TextCtrl(self.panel, -1, "768", pos = (210, 60)) currentlayerlabel.SetHelpText("The current layer being displayed.")
infofieldsizer.Add(currentlayerlabel, pos=(2,0))
self.current_layer = wx.StaticText(self.panel, -1, "0")
infofieldsizer.Add(self.current_layer, pos=(2,1))
wx.StaticText(self.panel, -1, "OffsetX:", pos = (160, 90)) estimatedtimelabel = wx.StaticText(self.panel, -1, "Estimated Time:")
self.offsetX = wx.TextCtrl(self.panel, -1, "50", pos = (210, 90)) estimatedtimelabel.SetHelpText("An estimate of the remaining time until print completion.")
infofieldsizer.Add(estimatedtimelabel, pos=(3,0))
self.estimated_time = wx.StaticText(self.panel, -1, "")
infofieldsizer.Add(self.estimated_time, pos=(3,1))
wx.StaticText(self.panel, -1, "OffsetY:", pos = (160, 120)) infosizer.Add(infofieldsizer)
self.offsetY = wx.TextCtrl(self.panel, -1, "50", pos = (210, 120))
self.bload = wx.Button(self.panel, -1, "Present", pos = (0, 150)) #
self.bload.Bind(wx.EVT_BUTTON, self.startdisplay)
wx.StaticText(self.panel, -1, "Fullscreen:", pos = (160, 150)) vbox.Add(buttonbox, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP|wx.BOTTOM, border=10)
self.fullscreen = wx.CheckBox(self.panel, -1, pos = (220, 150)) vbox.Add(fieldboxsizer, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10);
self.fullscreen.SetValue(True) vbox.Add(displayboxsizer, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10);
vbox.Add(infosizer, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10)
self.panel.SetSizer(vbox)
self.panel.Fit()
self.Fit()
self.SetPosition((0, 0))
self.Show() self.Show()
def __del__(self): def __del__(self):
if hasattr(self, 'image_dir') and self.image_dir != '': if hasattr(self, 'image_dir') and self.image_dir != '':
shutil.rmtree(self.image_dir) shutil.rmtree(self.image_dir)
if self.display_frame:
self.display_frame.Destroy()
def set_total_layers(self, total):
self.total_layers.SetLabel(str(total))
self.set_estimated_time()
def set_current_layer(self, index):
self.current_layer.SetLabel(str(index))
self.set_estimated_time()
def display_filename(self,name):
self.filename.SetLabel(name)
def set_estimated_time(self):
if not hasattr(self, 'layers'):
return
current_layer = int(self.current_layer.GetLabel())
remaining_layers = len(self.layers[0]) - current_layer
# 0.5 for delay between hide and rise
estimated_time = remaining_layers * (float(self.interval.GetValue()) + float(self.pause.GetValue()) + 0.5)
self.estimated_time.SetLabel(time.strftime("%H:%M:%S",time.gmtime(estimated_time)))
def parsesvg(self, name): def parse_svg(self, name):
et = xml.etree.ElementTree.ElementTree(file = name) et = xml.etree.ElementTree.ElementTree(file=name)
#xml.etree.ElementTree.dump(et) #xml.etree.ElementTree.dump(et)
slicer = 'Slic3r' if et.getroot().find('{http://www.w3.org/2000/svg}metadata') == None else 'Skeinforge' slicer = 'Slic3r' if et.getroot().find('{http://www.w3.org/2000/svg}metadata') == None else 'Skeinforge'
zlast = 0 zlast = 0
zdiff = 0 zdiff = 0
ol = [] ol = []
if (slicer == 'Slic3r'): if (slicer == 'Slic3r'):
height = et.getroot().get('height') height = et.getroot().get('height').replace('m','')
width = et.getroot().get('width') width = et.getroot().get('width').replace('m','')
for i in et.findall("{http://www.w3.org/2000/svg}g"): for i in et.findall("{http://www.w3.org/2000/svg}g"):
z = float(i.get('{http://slic3r.org/namespaces/slic3r}z')) z = float(i.get('{http://slic3r.org/namespaces/slic3r}z'))
...@@ -186,35 +497,77 @@ class setframe(wx.Frame): ...@@ -186,35 +497,77 @@ class setframe(wx.Frame):
svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg') svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg')
svgSnippet.set('height', height + 'mm') svgSnippet.set('height', height + 'mm')
svgSnippet.set('width', width + 'mm') svgSnippet.set('width', width + 'mm')
svgSnippet.set('viewBox', '0 0 ' + height + ' ' + width) svgSnippet.set('viewBox', '0 0 ' + height + ' ' + width)
svgSnippet.set('style','background-color:black')
svgSnippet.append(i) svgSnippet.append(i)
ol += [svgSnippet] ol += [svgSnippet]
else : else :
for i in et.findall("{http://www.w3.org/2000/svg}g")[0].findall("{http://www.w3.org/2000/svg}g"):
z = float(i.get('id').split("z:")[-1]) slice_layers = et.findall("{http://www.w3.org/2000/svg}metadata")[0].findall("{http://www.reprap.org/slice}layers")[0]
minX = slice_layers.get('minX')
maxX = slice_layers.get('maxX')
minY = slice_layers.get('minY')
maxY = slice_layers.get('maxY')
height = str(abs(float(minY)) + abs(float(maxY)))
width = str(abs(float(minX)) + abs(float(maxX)))
for g in et.findall("{http://www.w3.org/2000/svg}g")[0].findall("{http://www.w3.org/2000/svg}g"):
g.set('transform','')
text_element = g.findall("{http://www.w3.org/2000/svg}text")[0]
g.remove(text_element)
path_elements = g.findall("{http://www.w3.org/2000/svg}path")
for p in path_elements:
p.set('transform', 'translate('+maxX+','+maxY+')')
p.set('fill', 'white')
z = float(g.get('id').split("z:")[-1])
zdiff = z - zlast zdiff = z - zlast
zlast = z zlast = z
path = i.find('{http://www.w3.org/2000/svg}path')
ol += [(path.get("d").split("z"))[:-1]] svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg')
svgSnippet.set('height', height + 'mm')
svgSnippet.set('width', width + 'mm')
svgSnippet.set('viewBox', '0 0 ' + height + ' ' + width)
svgSnippet.set('style','background-color:black;fill:white;')
svgSnippet.append(g)
ol += [svgSnippet]
return ol, zdiff, slicer return ol, zdiff, slicer
def parse3DLPzip(self, name): def parse_3DLP_zip(self, name):
if not zipfile.is_zipfile(name): if not zipfile.is_zipfile(name):
raise Exception(name + " is not a zip file!") raise Exception(name + " is not a zip file!")
acceptedImageTypes = ['gif','tiff','jpg','jpeg','bmp','png'] accepted_image_types = ['gif','tiff','jpg','jpeg','bmp','png']
zipFile = zipfile.ZipFile(name, 'r') zipFile = zipfile.ZipFile(name, 'r')
self.image_dir = tempfile.mkdtemp() self.image_dir = tempfile.mkdtemp()
zipFile.extractall(self.image_dir) zipFile.extractall(self.image_dir)
ol = [] ol = []
for f in os.listdir(self.image_dir):
# Note: the following funky code extracts any numbers from the filenames, matches
# them with the original then sorts them. It allows for filenames of the
# format: abc_1.png, which would be followed by abc_10.png alphabetically.
os.chdir(self.image_dir)
vals = filter(os.path.isfile, os.listdir('.'))
keys = map(lambda p:int(re.search('\d+', p).group()), vals)
imagefilesDict = dict(itertools.izip(keys, vals))
imagefilesOrderedDict = OrderedDict(sorted(imagefilesDict.items(), key=lambda t: t[0]))
for f in imagefilesOrderedDict.values():
path = os.path.join(self.image_dir, f) path = os.path.join(self.image_dir, f)
if os.path.isfile(path) and imghdr.what(path) in acceptedImageTypes: if os.path.isfile(path) and imghdr.what(path) in accepted_image_types:
ol.append(wx.Bitmap(path)) ol.append(path)
return ol, -1, "bitmap" return ol, -1, "bitmap"
def loadfile(self, event): def load_file(self, event):
dlg = wx.FileDialog(self, ("Open file to print"), style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) dlg = wx.FileDialog(self, ("Open file to print"), style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
dlg.SetWildcard(("Slic3r or Skeinforge svg files (;*.svg;*.SVG;);3DLP Zip (;*.3dlp.zip;)")) dlg.SetWildcard(("Slic3r or Skeinforge svg files (;*.svg;*.SVG;);3DLP Zip (;*.3dlp.zip;)"))
if(dlg.ShowModal() == wx.ID_OK): if(dlg.ShowModal() == wx.ID_OK):
name = dlg.GetPath() name = dlg.GetPath()
...@@ -222,32 +575,241 @@ class setframe(wx.Frame): ...@@ -222,32 +575,241 @@ class setframe(wx.Frame):
self.status.SetStatusText(("File not found!")) self.status.SetStatusText(("File not found!"))
return return
if name.endswith(".3dlp.zip"): if name.endswith(".3dlp.zip"):
layers = self.parse3DLPzip(name) layers = self.parse_3DLP_zip(name)
layerHeight = float(self.thickness.GetValue()) layerHeight = float(self.thickness.GetValue())
else: else:
layers = self.parsesvg(name) layers = self.parse_svg(name)
layerHeight = layers[1] layerHeight = layers[1]
self.thickness.SetValue(str(layers[1])) self.thickness.SetValue(str(layers[1]))
print "Layer thickness detected:", layerHeight, "mm" print "Layer thickness detected:", layerHeight, "mm"
print len(layers[0]), "layers found, total height", layerHeight * len(layers[0]), "mm" print len(layers[0]), "layers found, total height", layerHeight * len(layers[0]), "mm"
self.layers = layers self.layers = layers
self.f.slicer = layers[2] self.set_total_layers(len(layers[0]))
self.set_current_layer(0)
self.current_filename = os.path.basename(name)
self.display_filename(self.current_filename)
self.slicer = layers[2]
self.display_frame.slicer = self.slicer
dlg.Destroy() dlg.Destroy()
def startdisplay(self, event): def show_calibrate(self, event):
self.f.Raise() if self.calibrate.IsChecked():
self.present_calibrate(event)
else:
if hasattr(self, 'layers'):
self.display_frame.slicer = self.layers[2]
self.display_frame.scale = float(self.scale.GetValue())
self.display_frame.clear_layer()
def show_first_layer(self, event):
if self.first_layer.IsChecked():
self.present_first_layer(event)
else:
if hasattr(self, 'layers'):
self.display_frame.slicer = self.layers[2]
self.display_frame.scale = float(self.scale.GetValue())
self.display_frame.clear_layer()
def show_layer_red(self, event):
self.display_frame.layer_red = self.layer_red.IsChecked()
def present_calibrate(self, event):
if self.calibrate.IsChecked():
self.display_frame.Raise()
self.display_frame.offset = (float(self.offset_X.GetValue()), -float(self.offset_Y.GetValue()))
self.display_frame.scale = 1.0
resolution_x_pixels = int(self.X.GetValue())
resolution_y_pixels = int(self.Y.GetValue())
gridBitmap = wx.EmptyBitmap(resolution_x_pixels, resolution_y_pixels)
dc = wx.MemoryDC()
dc.SelectObject(gridBitmap)
dc.SetBackground(wx.Brush("black"))
dc.Clear()
dc.SetPen(wx.Pen("red", 7))
dc.DrawLine(0, 0, resolution_x_pixels, 0);
dc.DrawLine(0, 0, 0, resolution_y_pixels);
dc.DrawLine(resolution_x_pixels, 0, resolution_x_pixels, resolution_y_pixels);
dc.DrawLine(0, resolution_y_pixels, resolution_x_pixels, resolution_y_pixels);
dc.SetPen(wx.Pen("red", 2))
aspectRatio = float(resolution_x_pixels) / float(resolution_y_pixels)
projectedXmm = float(self.projected_X_mm.GetValue())
projectedYmm = round(projectedXmm / aspectRatio)
pixelsXPerMM = resolution_x_pixels / projectedXmm
pixelsYPerMM = resolution_y_pixels / projectedYmm
gridCountX = int(projectedXmm / 10)
gridCountY = int(projectedYmm / 10)
for y in xrange(0, gridCountY + 1):
for x in xrange(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);
self.first_layer.SetValue(False)
self.display_frame.slicer = 'bitmap'
self.display_frame.draw_layer(gridBitmap.ConvertToImage())
def present_first_layer(self, event):
if (self.first_layer.GetValue()):
if not hasattr(self, "layers"):
print "No model loaded!"
self.first_layer.SetValue(False)
return
self.display_frame.offset = (float(self.offset_X.GetValue()), float(self.offset_Y.GetValue()))
self.display_frame.scale = float(self.scale.GetValue())
self.display_frame.slicer = self.layers[2]
self.display_frame.dpi = self.get_dpi()
self.display_frame.draw_layer(copy.deepcopy(self.layers[0][0]))
self.calibrate.SetValue(False)
if self.show_first_layer_timer != -1.0 :
def unpresent_first_layer():
self.display_frame.clear_layer()
self.first_layer.SetValue(False)
wx.CallLater(self.show_first_layer_timer.GetValue() * 1000, unpresent_first_layer)
def update_offset(self, event):
offset_x = float(self.offset_X.GetValue())
offset_y = float(self.offset_Y.GetValue())
self.display_frame.offset = (offset_x, offset_y)
self._set_setting('project_offset_x',offset_x)
self._set_setting('project_offset_y',offset_y)
self.refresh_display(event)
def refresh_display(self, event):
self.present_calibrate(event)
self.present_first_layer(event)
def update_thickness(self, event):
self._set_setting('project_layer',self.thickness.GetValue())
self.refresh_display(event)
def update_projected_Xmm(self, event):
self._set_setting('project_projected_x',self.projected_X_mm.GetValue())
self.refresh_display(event)
def update_scale(self, event):
scale = float(self.scale.GetValue())
self.display_frame.scale = scale
self._set_setting('project_scale',scale)
self.refresh_display(event)
def update_interval(self, event):
interval = float(self.interval.GetValue())
self.display_frame.interval = interval
self._set_setting('project_interval',interval)
self.set_estimated_time()
self.refresh_display(event)
def update_pause(self, event):
pause = float(self.pause.GetValue())
self.display_frame.pause = pause
self._set_setting('project_pause',pause)
self.set_estimated_time()
self.refresh_display(event)
def update_overshoot(self, event):
overshoot = float(self.overshoot.GetValue())
self.display_frame.pause = overshoot
self._set_setting('project_overshoot',overshoot)
def update_prelift_gcode(self, event):
prelift_gcode = self.prelift_gcode.GetValue().replace('\n', "\\n")
self.display_frame.prelift_gcode = prelift_gcode
self._set_setting('project_prelift_gcode',prelift_gcode)
def update_postlift_gcode(self, event):
postlift_gcode = self.postlift_gcode.GetValue().replace('\n', "\\n")
self.display_frame.postlift_gcode = postlift_gcode
self._set_setting('project_postlift_gcode',postlift_gcode)
def update_z_axis_rate(self, event):
z_axis_rate = int(self.z_axis_rate.GetValue())
self.display_frame.z_axis_rate = z_axis_rate
self._set_setting('project_z_axis_rate',z_axis_rate)
def update_direction(self, event):
direction = self.direction.GetValue()
self.display_frame.direction = direction
self._set_setting('project_direction',direction)
def update_fullscreen(self, event):
if (self.fullscreen.GetValue()):
self.display_frame.ShowFullScreen(1)
else:
self.display_frame.ShowFullScreen(0)
self.refresh_display(event)
def update_resolution(self, event):
x = float(self.X.GetValue())
y = float(self.Y.GetValue())
self.display_frame.resize((x,y))
self._set_setting('project_x',x)
self._set_setting('project_y',y)
self.refresh_display(event)
def get_dpi(self):
resolution_x_pixels = int(self.X.GetValue())
projected_x_mm = float(self.projected_X_mm.GetValue())
projected_x_inches = projected_x_mm / 25.4
return resolution_x_pixels / projected_x_inches
def start_present(self, event):
if not hasattr(self, "layers"):
print "No model loaded!"
return
self.pause_button.SetLabel("Pause")
self.set_current_layer(0)
self.display_frame.Raise()
if (self.fullscreen.GetValue()): if (self.fullscreen.GetValue()):
self.f.ShowFullScreen(1) self.display_frame.ShowFullScreen(1)
l = self.layers[0][:] self.display_frame.slicer = self.layers[2]
self.f.present(l, self.display_frame.dpi = self.get_dpi()
thickness = float(self.thickness.GetValue()), self.display_frame.present(self.layers[0][:],
interval = float(self.interval.GetValue()), thickness=float(self.thickness.GetValue()),
scale = float(self.scale.GetValue()), interval=float(self.interval.GetValue()),
pause = float(self.delay.GetValue()), scale=float(self.scale.GetValue()),
size = (float(self.X.GetValue()), float(self.Y.GetValue())), pause=float(self.pause.GetValue()),
offset = (float(self.offsetX.GetValue()), float(self.offsetY.GetValue()))) overshoot=float(self.overshoot.GetValue()),
z_axis_rate=int(self.z_axis_rate.GetValue()),
prelift_gcode=self.prelift_gcode.GetValue(),
postlift_gcode=self.postlift_gcode.GetValue(),
direction=self.direction.GetValue(),
size=(float(self.X.GetValue()), float(self.Y.GetValue())),
offset=(float(self.offset_X.GetValue()), float(self.offset_Y.GetValue())),
layer_red=self.layer_red.IsChecked())
def stop_present(self, event):
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"
self.pause_button.SetLabel("Continue")
self.display_frame.running = False
else:
print "Continue"
self.pause_button.SetLabel("Pause")
self.display_frame.running = True
self.display_frame.next_img()
if __name__ == "__main__": if __name__ == "__main__":
provider = wx.SimpleHelpProvider()
wx.HelpProvider_Set(provider)
#a = wx.App(redirect=True,filename="mylogfile.txt")
a = wx.App() a = wx.App()
setframe(None).Show() SettingsFrame(None).Show()
a.MainLoop() a.MainLoop()
...@@ -120,36 +120,59 @@ class MacroEditor(wx.Dialog): ...@@ -120,36 +120,59 @@ class MacroEditor(wx.Dialog):
reindented += self.indent_chars + line + "\n" reindented += self.indent_chars + line + "\n"
return reindented return reindented
class options(wx.Dialog): SETTINGS_GROUPS = {"General": _("General"),
"Printer": _("Printer settings"),
"UI": _("User interface"),
"External": _("External commands")}
class PronterOptionsDialog(wx.Dialog):
"""Options editor""" """Options editor"""
def __init__(self, pronterface): def __init__(self, pronterface):
wx.Dialog.__init__(self, None, title = _("Edit settings"), style = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) wx.Dialog.__init__(self, parent = None, title = _("Edit settings"), size = (400, 500), style = wx.DEFAULT_DIALOG_STYLE)
topsizer = wx.BoxSizer(wx.VERTICAL) panel = wx.Panel(self)
vbox = wx.StaticBoxSizer(wx.StaticBox(self, label = _("Defaults")) ,wx.VERTICAL) header = wx.StaticBox(panel, label = _("Settings"))
topsizer.Add(vbox, 1, wx.ALL+wx.EXPAND) sbox = wx.StaticBoxSizer(header, wx.VERTICAL)
notebook = wx.Notebook(panel)
all_settings = pronterface.settings._all_settings()
group_list = []
groups = {}
for group in ["General", "UI", "Printer"]:
group_list.append(group)
groups[group] = []
for setting in all_settings:
if setting.group not in group_list:
group_list.append(setting.group)
groups[setting.group] = []
groups[setting.group].append(setting)
for group in group_list:
grouppanel = wx.Panel(notebook, -1)
notebook.AddPage(grouppanel, SETTINGS_GROUPS[group])
settings = groups[group]
grid = wx.FlexGridSizer(rows = 0, cols = 2, hgap = 8, vgap = 2) grid = wx.FlexGridSizer(rows = 0, cols = 2, hgap = 8, vgap = 2)
grid.SetFlexibleDirection( wx.BOTH ) grid.SetFlexibleDirection(wx.BOTH)
grid.AddGrowableCol( 1 ) grid.AddGrowableCol(1)
grid.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED ) grid.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED)
vbox.Add(grid, 0, wx.EXPAND) for setting in settings:
ctrls = {} grid.Add(setting.get_label(grouppanel), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL|wx.ALIGN_RIGHT)
for k, v in sorted(pronterface.settings._all_settings().items()): grid.Add(setting.get_widget(grouppanel), 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL|wx.EXPAND)
ctrls[k, 0] = wx.StaticText(self,-1, k) grouppanel.SetSizer(grid)
ctrls[k, 1] = wx.TextCtrl(self,-1, str(v)) sbox.Add(notebook, 1, wx.EXPAND)
if k in pronterface.helpdict: panel.SetSizer(sbox)
ctrls[k, 0].SetToolTipString(pronterface.helpdict.get(k)) topsizer = wx.BoxSizer(wx.VERTICAL)
ctrls[k, 1].SetToolTipString(pronterface.helpdict.get(k)) topsizer.Add(panel, 1, wx.ALL | wx.EXPAND)
grid.Add(ctrls[k, 0], 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL|wx.ALIGN_RIGHT) topsizer.Add(self.CreateButtonSizer(wx.OK | wx.CANCEL), 0, wx.ALIGN_RIGHT)
grid.Add(ctrls[k, 1], 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL|wx.EXPAND) self.SetSizerAndFit(topsizer)
topsizer.Add(self.CreateSeparatedButtonSizer(wx.OK+wx.CANCEL), 0, wx.EXPAND) self.SetMinSize(self.GetSize())
self.SetSizer(topsizer)
topsizer.Layout() def PronterOptions(pronterface):
topsizer.Fit(self) dialog = PronterOptionsDialog(pronterface)
if self.ShowModal() == wx.ID_OK: if dialog.ShowModal() == wx.ID_OK:
for k, v in pronterface.settings._all_settings().items(): for setting in pronterface.settings._all_settings():
if ctrls[k, 1].GetValue() != str(v): old_value = setting.value
pronterface.set(k, str(ctrls[k, 1].GetValue())) setting.update()
self.Destroy() if setting.value != old_value:
pronterface.set(setting.name, setting.value)
dialog.Destroy()
class ButtonEdit(wx.Dialog): class ButtonEdit(wx.Dialog):
"""Custom button edit dialog""" """Custom button edit dialog"""
...@@ -211,6 +234,113 @@ class ButtonEdit(wx.Dialog): ...@@ -211,6 +234,113 @@ class ButtonEdit(wx.Dialog):
if self.name.GetValue()=="": if self.name.GetValue()=="":
self.name.SetValue(macro) self.name.SetValue(macro)
class TempGauge(wx.Panel):
def __init__(self, parent, size = (200, 22), title = "", maxval = 240, gaugeColour = None):
wx.Panel.__init__(self, parent,-1, size = size)
self.Bind(wx.EVT_PAINT, self.paint)
self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
self.width, self.height = size
self.title = title
self.max = maxval
self.gaugeColour = gaugeColour
self.value = 0
self.setpoint = 0
self.recalc()
def recalc(self):
mmax = max(int(self.setpoint*1.05), self.max)
self.scale = float(self.width-2)/float(mmax)
self.ypt = max(16, int(self.scale*max(self.setpoint, self.max/6)))
def SetValue(self, value):
self.value = value
wx.CallAfter(self.Refresh)
def SetTarget(self, value):
self.setpoint = value
wx.CallAfter(self.Refresh)
def interpolatedColour(self, val, vmin, vmid, vmax, cmin, cmid, cmax):
if val < vmin: return cmin
if val > vmax: return cmax
if val <= vmid:
lo, hi, val, valhi = cmin, cmid, val-vmin, vmid-vmin
else:
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))
def paint(self, ev):
self.width, self.height = self.GetClientSizeTuple()
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((255, 255, 255)))
dc.Clear()
cold, medium, hot = wx.Colour(0, 167, 223), wx.Colour(239, 233, 119), wx.Colour(210, 50.100)
gauge1, gauge2 = wx.Colour(255, 255, 210), (self.gaugeColour or wx.Colour(234, 82, 0))
shadow1, shadow2 = wx.Colour(110, 110, 110), wx.Colour(255, 255, 255)
gc = wx.GraphicsContext.Create(dc)
# draw shadow first
# corners
gc.SetBrush(gc.CreateRadialGradientBrush(xE-7, 9, xE-7, 9, 8, shadow1, shadow2))
gc.DrawRectangle(xE-7, 1, 8, 8)
gc.SetBrush(gc.CreateRadialGradientBrush(xE-7, 17, xE-7, 17, 8, shadow1, shadow2))
gc.DrawRectangle(xE-7, 17, 8, 8)
gc.SetBrush(gc.CreateRadialGradientBrush(x0+6, 17, x0+6, 17, 8, shadow1, shadow2))
gc.DrawRectangle(0, 17, x0+6, 8)
# edges
gc.SetBrush(gc.CreateLinearGradientBrush(xE-6, 0, xE+1, 0, shadow1, shadow2))
gc.DrawRectangle(xE-7, 9, 8, 8)
gc.SetBrush(gc.CreateLinearGradientBrush(x0, yE-2, x0, yE+5, shadow1, shadow2))
gc.DrawRectangle(x0+6, yE-2, xE-12, 7)
# draw gauge background
gc.SetBrush(gc.CreateLinearGradientBrush(x0, y0, x1+1, y1, cold, medium))
gc.DrawRoundedRectangle(x0, y0, x1+4, yE, 6)
gc.SetBrush(gc.CreateLinearGradientBrush(x1-2, y1, xE, y1, medium, hot))
gc.DrawRoundedRectangle(x1-2, y1, xE-x1, yE, 6)
# draw gauge
width = 12
w1 = y0+9-width/2
w2 = w1+width
value = x0+max(10, min(self.width+1-2, int(self.value*self.scale)))
#gc.SetBrush(gc.CreateLinearGradientBrush(x0, y0+3, x0, y0+15, gauge1, gauge2))
#gc.SetBrush(gc.CreateLinearGradientBrush(0, 3, 0, 15, wx.Colour(255, 255, 255), wx.Colour(255, 90, 32)))
gc.SetBrush(gc.CreateLinearGradientBrush(x0, y0+3, x0, y0+15, gauge1, self.interpolatedColour(value, x0, x1, xE, cold, medium, hot)))
val_path = gc.CreatePath()
val_path.MoveToPoint(x0, w1)
val_path.AddLineToPoint(value, w1)
val_path.AddLineToPoint(value+2, w1+width/4)
val_path.AddLineToPoint(value+2, w2-width/4)
val_path.AddLineToPoint(value, w2)
#val_path.AddLineToPoint(value-4, 10)
val_path.AddLineToPoint(x0, w2)
gc.DrawPath(val_path)
# draw setpoint markers
setpoint = x0+max(10, int(self.setpoint*self.scale))
gc.SetBrush(gc.CreateBrush(wx.Brush(wx.Colour(0, 0, 0))))
setp_path = gc.CreatePath()
setp_path.MoveToPoint(setpoint-4, y0)
setp_path.AddLineToPoint(setpoint+4, y0)
setp_path.AddLineToPoint(setpoint, y0+5)
setp_path.MoveToPoint(setpoint-4, yE)
setp_path.AddLineToPoint(setpoint+4, yE)
setp_path.AddLineToPoint(setpoint, yE-5)
gc.DrawPath(setp_path)
# draw readout
text = u"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))
gc.DrawText(self.title, x0+19, y0+4)
gc.DrawText(text, x0+119, y0+4)
gc.SetFont(gc.CreateFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)))
gc.DrawText(self.title, x0+18, y0+3)
gc.DrawText(text, x0+118, y0+3)
class SpecialButton(object): class SpecialButton(object):
label = None label = None
......
...@@ -24,8 +24,8 @@ import time ...@@ -24,8 +24,8 @@ import time
import threading import threading
import pyglet import pyglet
pyglet.options['shadow_window'] = False pyglet.options['debug_gl'] = True
pyglet.options['debug_gl'] = False
from pyglet.gl import * from pyglet.gl import *
...@@ -115,6 +115,7 @@ class GLPanel(wx.Panel): ...@@ -115,6 +115,7 @@ class GLPanel(wx.Panel):
self.pmat = (GLdouble * 16)() self.pmat = (GLdouble * 16)()
self.mvmat = (GLdouble * 16)() self.mvmat = (GLdouble * 16)()
self.pygletcontext = Context(current_context) self.pygletcontext = Context(current_context)
self.pygletcontext.canvas = self
self.pygletcontext.set_current() self.pygletcontext.set_current()
self.dist = 1000 self.dist = 1000
self.vpmat = None self.vpmat = None
...@@ -233,7 +234,7 @@ def vdiff(v, o): ...@@ -233,7 +234,7 @@ def vdiff(v, o):
class gcview(object): class gcview(object):
def __init__(self, lines, batch, w = 0.5, h = 0.5): def __init__(self, gcode, batch, w = 0.5, h = 0.5):
# Create the vertex and normal arrays. # Create the vertex and normal arrays.
vertices = [] vertices = []
normals = [] normals = []
...@@ -242,8 +243,12 @@ class gcview(object): ...@@ -242,8 +243,12 @@ class gcview(object):
self.vlists = [] self.vlists = []
self.layers = {} self.layers = {}
t0 = time.time() t0 = time.time()
lines = [self.transform(i) for i in lines] if gcode:
lines = [i for i in lines if i is not None] lines = [self.transform(gline) for gline in gcode.lines]
lines = [line for line in lines if line]
print len(lines)
else:
lines = []
print "transformed lines in %fs" % (time.time() - t0) print "transformed lines in %fs" % (time.time() - t0)
t0 = time.time() t0 = time.time()
layertemp = {} layertemp = {}
...@@ -355,22 +360,21 @@ class gcview(object): ...@@ -355,22 +360,21 @@ class gcview(object):
epoints = map(lambda x: vadd(E, x), points) epoints = map(lambda x: vadd(E, x), points)
return spoints, epoints, S, E return spoints, epoints, S, E
def transform(self, line): def transform(self, gline):
line = line.split(";")[0]
cur = self.prev[:] cur = self.prev[:]
if len(line) > 0: isg92 = (gline.command == "G92")
if "G1" in line or "G0" in line or "G92" in line: if gline.is_move or isg92:
if("X" in line): if gline.x is not None:
cur[0] = float(line.split("X")[1].split(" ")[0]) cur[0] = gline.x
if("Y" in line): if gline.y is not None:
cur[1] = float(line.split("Y")[1].split(" ")[0]) cur[1] = gline.y
if("Z" in line): if gline.z is not None:
cur[2] = float(line.split("Z")[1].split(" ")[0]) cur[2] = gline.z
if("E" in line): if gline.e is not None:
cur[3] = float(line.split("E")[1].split(" ")[0]) cur[3] = gline.e
if self.prev == cur: if self.prev == cur:
return None return None
if self.fline or "G92" in line: elif self.fline or isg92:
self.prev = cur self.prev = cur
self.fline = 0 self.fline = 0
return None return None
...@@ -378,6 +382,8 @@ class gcview(object): ...@@ -378,6 +382,8 @@ class gcview(object):
r = [self.prev, cur] r = [self.prev, cur]
self.prev = cur self.prev = cur
return r return r
else:
return None
def delete(self): def delete(self):
for i in self.vlists: for i in self.vlists:
...@@ -833,7 +839,7 @@ class GCFrame(wx.Frame): ...@@ -833,7 +839,7 @@ class GCFrame(wx.Frame):
m.curlayer = 0.0 m.curlayer = 0.0
m.scale = [1.0, 1.0, 1.0] m.scale = [1.0, 1.0, 1.0]
m.batch = pyglet.graphics.Batch() m.batch = pyglet.graphics.Batch()
m.gc = gcview([], batch = m.batch) m.gc = gcview(None, batch = m.batch)
self.models = {"": m} self.models = {"": m}
self.l = d() self.l = d()
self.modelindex = 0 self.modelindex = 0
......
# 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 <http://www.gnu.org/licenses/>.
"""
"""
import wx
def AddEllipticalArc(self, x, y, w, h, startAngle, endAngle, clockwise = False):
""" Draws an arc of an ellipse within bounding rect (x, y, w, h)
from startArc to endArc (in radians, relative to the horizontal line of the eclipse)"""
if True:
import warnings
warnings.warn("elliptical arcs are not supported")
w = w/2.0
h = h/2.0
self.AddArc(x+w, y+h, ((w+h)/2), startAngle, endAngle, clockwise)
return
else:
#implement in terms of AddArc by applying a transformation matrix
#Sigh this can't work, still need to patch wx to allow
#either a) AddPath that's not a closed path or
#b) allow pushing and popping of states on a path, not just on a context
#a) is possible in GDI+, need to investigate other renderers.
#b) is possible in Quartz and Cairo, but not in GDI+. It could
#possibly be simulated by combining the current transform with option a.
mtx = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix()
path = wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
mtx.Translate(x+(w/2.0), y+(h/2.0))
mtx.Scale(w/2.0, y/2.0)
path.AddArc(0, 0, 1, startAngle, endAngle, clockwise)
path.Transform(mtx)
self.AddPath(path)
self.MoveToPoint(path.GetCurrentPoint())
self.CloseSubpath()
if not hasattr(wx.GraphicsPath, "AddEllipticalArc"):
wx.GraphicsPath.AddEllipticalArc = AddEllipticalArc
del AddEllipticalArc
# 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 <http://www.gnu.org/licenses/>.
"""
Parsers for specific attributes
"""
import urlparse
from pyparsing import (Literal,
Optional, oneOf, Group, StringEnd, Combine, Word, alphas, hexnums,
CaselessLiteral, SkipTo
)
from css.colour import colourValue
import string
##Paint values
none = CaselessLiteral("none").setParseAction(lambda t: ["NONE", ()])
currentColor = CaselessLiteral("currentColor").setParseAction(lambda t: ["CURRENTCOLOR", ()])
def parsePossibleURL(t):
possibleURL, fallback = t[0]
return [urlparse.urlsplit(possibleURL), fallback]
#Normal color declaration
colorDeclaration = none | currentColor | colourValue
urlEnd = (
Literal(")").suppress() +
Optional(Group(colorDeclaration), default = ()) +
StringEnd()
)
url = (
CaselessLiteral("URL")
+
Literal("(").suppress()+
Group(SkipTo(urlEnd, include = True).setParseAction(parsePossibleURL))
)
#paint value will parse into a (type, details) tuple.
#For none and currentColor, the details tuple will be the empty tuple
#for CSS color declarations, it will be (type, (R, G, B))
#for URLs, it will be ("URL", ((url tuple), fallback))
#The url tuple will be as returned by urlparse.urlsplit, and can be
#an empty tuple if the parser has an error
#The fallback will be another (type, details) tuple as a parsed
#colorDeclaration, but may be the empty tuple if it is not present
paintValue = url | colorDeclaration
# 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 <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from .transform import transformList
from .inline import inlineStyle
# 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 <http://www.gnu.org/licenses/>.
""" CSS at-rules"""
from pyparsing import Literal, Combine
from .identifier import identifier
atkeyword = Combine(Literal("@") + identifier)
# 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 <http://www.gnu.org/licenses/>.
"""
CSS blocks
"""
from pyparsing import nestedExpr
block = nestedExpr(opener="{", closer="}")
# 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 <http://www.gnu.org/licenses/>.
"""
Parsing for CSS colour values.
Supported formats:
hex literal short: #fff
hex literal long: #fafafa
rgb bytes: rgb(255,100,0)
rgb percent: rgb(100%,100%,0%)
named color: black
"""
import wx
import string
import urlparse
from pyparsing import nums, Literal, Optional, oneOf, Group, StringEnd, Combine, Word, alphas, hexnums
from ..pathdata import number, sign
number = number.copy()
integerConstant = Word(nums+"+-").setParseAction(lambda t:int(t[0]))
#rgb format parser
comma = Literal(",").suppress()
def clampColourByte(val):
val = int(val)
return min(max(0,val), 255)
def clampColourPerc(val):
val = float(val)
return min(max(0,val), 100)
def parseColorPerc(token):
val = token[0]
val = clampColourPerc(val)
#normalize to bytes
return int(255 * (val / 100.0))
colorByte = Optional(sign) + integerConstant.setParseAction(lambda t: clampColourByte(t[0]))
colorPerc = number.setParseAction(parseColorPerc) + Literal("%").suppress()
rgb = (
Literal("rgb(").setParseAction(lambda t: "RGB") +
(
#integer constants, ie 255,255,255
Group(colorByte + comma + colorByte + comma + colorByte) ^
#percentage values, ie 100%, 50%
Group(colorPerc + comma + colorPerc + comma + colorPerc)
)
+
Literal(")").suppress() + StringEnd()
)
def parseShortHex(t):
return tuple(int(x*2, 16) for x in t[0])
doubleHex = Word(hexnums, exact=2).setParseAction(lambda t: int(t[0], 16))
hexLiteral = (Literal("#").setParseAction(lambda t: "RGB") +
(
Group(doubleHex + doubleHex + doubleHex) |
Word(hexnums, exact=3).setParseAction(parseShortHex)
) + StringEnd()
)
def parseNamedColour(t):
try:
return ["RGB", NamedColours[t[0].lower()]]
except KeyError:
return ["RGB", (0,0,0)]
namedColour = Word(alphas).setParseAction(parseNamedColour)
colourValue = rgb | hexLiteral | namedColour
##constants
NamedColours = {
#~ #html named colours
#~ "black":(0,0,0),
#~ "silver": (0xc0, 0xc0, 0xc0, 255),
#~ "gray": (0x80, 0x80, 0x80),
#~ "white":(255,255,255),
#~ "maroon":(0x80, 0, 0),
#~ "red":(0xff, 0, 0),
#~ "purple":(0x80, 0, 0x80),
#~ "fuchsia":(0xff, 0, 0xff),
#~ "green": (0, 0x80, 0),
#~ "lime": (0, 0xff, 0),
#~ "olive": (0x80, 0x80, 00),
#~ "yellow":(0xff, 0xff, 00),
#~ "navy": (0, 0, 0x80),
#~ "blue": (0, 0, 0xff),
#~ "teal": (0, 0x80, 0x80),
#~ "aqua": (0, 0xff, 0xff),
#expanded named colors from SVG spc
'aliceblue' : (240, 248, 255) ,
'antiquewhite' : (250, 235, 215) ,
'aqua' : ( 0, 255, 255) ,
'aquamarine' : (127, 255, 212) ,
'azure' : (240, 255, 255) ,
'beige' : (245, 245, 220) ,
'bisque' : (255, 228, 196) ,
'black' : ( 0, 0, 0) ,
'blanchedalmond' : (255, 235, 205) ,
'blue' : ( 0, 0, 255) ,
'blueviolet' : (138, 43, 226) ,
'brown' : (165, 42, 42) ,
'burlywood' : (222, 184, 135) ,
'cadetblue' : ( 95, 158, 160) ,
'chartreuse' : (127, 255, 0) ,
'chocolate' : (210, 105, 30) ,
'coral' : (255, 127, 80) ,
'cornflowerblue' : (100, 149, 237) ,
'cornsilk' : (255, 248, 220) ,
'crimson' : (220, 20, 60) ,
'cyan' : ( 0, 255, 255) ,
'darkblue' : ( 0, 0, 139) ,
'darkcyan' : ( 0, 139, 139) ,
'darkgoldenrod' : (184, 134, 11) ,
'darkgray' : (169, 169, 169) ,
'darkgreen' : ( 0, 100, 0) ,
'darkgrey' : (169, 169, 169) ,
'darkkhaki' : (189, 183, 107) ,
'darkmagenta' : (139, 0, 139) ,
'darkolivegreen' : ( 85, 107, 47) ,
'darkorange' : (255, 140, 0) ,
'darkorchid' : (153, 50, 204) ,
'darkred' : (139, 0, 0) ,
'darksalmon' : (233, 150, 122) ,
'darkseagreen' : (143, 188, 143) ,
'darkslateblue' : ( 72, 61, 139) ,
'darkslategray' : ( 47, 79, 79) ,
'darkslategrey' : ( 47, 79, 79) ,
'darkturquoise' : ( 0, 206, 209) ,
'darkviolet' : (148, 0, 211) ,
'deeppink' : (255, 20, 147) ,
'deepskyblue' : ( 0, 191, 255) ,
'dimgray' : (105, 105, 105) ,
'dimgrey' : (105, 105, 105) ,
'dodgerblue' : ( 30, 144, 255) ,
'firebrick' : (178, 34, 34) ,
'floralwhite' : (255, 250, 240) ,
'forestgreen' : ( 34, 139, 34) ,
'fuchsia' : (255, 0, 255) ,
'gainsboro' : (220, 220, 220) ,
'ghostwhite' : (248, 248, 255) ,
'gold' : (255, 215, 0) ,
'goldenrod' : (218, 165, 32) ,
'gray' : (128, 128, 128) ,
'grey' : (128, 128, 128) ,
'green' : ( 0, 128, 0) ,
'greenyellow' : (173, 255, 47) ,
'honeydew' : (240, 255, 240) ,
'hotpink' : (255, 105, 180) ,
'indianred' : (205, 92, 92) ,
'indigo' : ( 75, 0, 130) ,
'ivory' : (255, 255, 240) ,
'khaki' : (240, 230, 140) ,
'lavender' : (230, 230, 250) ,
'lavenderblush' : (255, 240, 245) ,
'lawngreen' : (124, 252, 0) ,
'lemonchiffon' : (255, 250, 205) ,
'lightblue' : (173, 216, 230) ,
'lightcoral' : (240, 128, 128) ,
'lightcyan' : (224, 255, 255) ,
'lightgoldenrodyellow' : (250, 250, 210) ,
'lightgray' : (211, 211, 211) ,
'lightgreen' : (144, 238, 144) ,
'lightgrey' : (211, 211, 211) ,
'lightpink' : (255, 182, 193) ,
'lightsalmon' : (255, 160, 122) ,
'lightseagreen' : ( 32, 178, 170) ,
'lightskyblue' : (135, 206, 250) ,
'lightslategray' : (119, 136, 153) ,
'lightslategrey' : (119, 136, 153) ,
'lightsteelblue' : (176, 196, 222) ,
'lightyellow' : (255, 255, 224) ,
'lime' : ( 0, 255, 0) ,
'limegreen' : ( 50, 205, 50) ,
'linen' : (250, 240, 230) ,
'magenta' : (255, 0, 255) ,
'maroon' : (128, 0, 0) ,
'mediumaquamarine' : (102, 205, 170) ,
'mediumblue' : ( 0, 0, 205) ,
'mediumorchid' : (186, 85, 211) ,
'mediumpurple' : (147, 112, 219) ,
'mediumseagreen' : ( 60, 179, 113) ,
'mediumslateblue' : (123, 104, 238) ,
'mediumspringgreen' : ( 0, 250, 154) ,
'mediumturquoise' : ( 72, 209, 204) ,
'mediumvioletred' : (199, 21, 133) ,
'midnightblue' : ( 25, 25, 112) ,
'mintcream' : (245, 255, 250) ,
'mistyrose' : (255, 228, 225) ,
'moccasin' : (255, 228, 181) ,
'navajowhite' : (255, 222, 173) ,
'navy' : ( 0, 0, 128) ,
'oldlace' : (253, 245, 230) ,
'olive' : (128, 128, 0) ,
'olivedrab' : (107, 142, 35) ,
'orange' : (255, 165, 0) ,
'orangered' : (255, 69, 0) ,
'orchid' : (218, 112, 214) ,
'palegoldenrod' : (238, 232, 170) ,
'palegreen' : (152, 251, 152) ,
'paleturquoise' : (175, 238, 238) ,
'palevioletred' : (219, 112, 147) ,
'papayawhip' : (255, 239, 213) ,
'peachpuff' : (255, 218, 185) ,
'peru' : (205, 133, 63) ,
'pink' : (255, 192, 203) ,
'plum' : (221, 160, 221) ,
'powderblue' : (176, 224, 230) ,
'purple' : (128, 0, 128) ,
'red' : (255, 0, 0) ,
'rosybrown' : (188, 143, 143) ,
'royalblue' : ( 65, 105, 225) ,
'saddlebrown' : (139, 69, 19) ,
'salmon' : (250, 128, 114) ,
'sandybrown' : (244, 164, 96) ,
'seagreen' : ( 46, 139, 87) ,
'seashell' : (255, 245, 238) ,
'sienna' : (160, 82, 45) ,
'silver' : (192, 192, 192) ,
'skyblue' : (135, 206, 235) ,
'slateblue' : (106, 90, 205) ,
'slategray' : (112, 128, 144) ,
'slategrey' : (112, 128, 144) ,
'snow' : (255, 250, 250) ,
'springgreen' : ( 0, 255, 127) ,
'steelblue' : ( 70, 130, 180) ,
'tan' : (210, 180, 140) ,
'teal' : ( 0, 128, 128) ,
'thistle' : (216, 191, 216) ,
'tomato' : (255, 99, 71) ,
'turquoise' : ( 64, 224, 208) ,
'violet' : (238, 130, 238) ,
'wheat' : (245, 222, 179) ,
'white' : (255, 255, 255) ,
'whitesmoke' : (245, 245, 245) ,
'yellow' : (255, 255, 0) ,
'yellowgreen' : (154, 205, 50) ,
}
def fillCSS2SystemColours():
#The system colours require a wxApp to be present to retrieve,
#so if you wnat support for them you'll need
#to call this function after your wxApp instance starts
systemColors = {
"ActiveBorder": wx.SYS_COLOUR_ACTIVEBORDER,
"ActiveCaption": wx.SYS_COLOUR_ACTIVECAPTION,
"AppWorkspace": wx.SYS_COLOUR_APPWORKSPACE,
"Background": wx.SYS_COLOUR_BACKGROUND,
"ButtonFace": wx.SYS_COLOUR_BTNFACE,
"ButtonHighlight": wx.SYS_COLOUR_BTNHIGHLIGHT,
"ButtonShadow": wx.SYS_COLOUR_BTNSHADOW,
"ButtonText": wx.SYS_COLOUR_BTNTEXT,
"CaptionText": wx.SYS_COLOUR_CAPTIONTEXT,
"GrayText": wx.SYS_COLOUR_GRAYTEXT,
"Highlight": wx.SYS_COLOUR_HIGHLIGHT,
"HighlightText": wx.SYS_COLOUR_HIGHLIGHTTEXT,
"InactiveBorder": wx.SYS_COLOUR_INACTIVEBORDER,
"InactiveCaption": wx.SYS_COLOUR_INACTIVECAPTION,
"InfoBackground": wx.SYS_COLOUR_INFOBK,
"InfoText": wx.SYS_COLOUR_INFOTEXT,
"Menu": wx.SYS_COLOUR_MENU,
"MenuText": wx.SYS_COLOUR_MENUTEXT,
"Scrollbar": wx.SYS_COLOUR_SCROLLBAR,
"ThreeDDarkShadow": wx.SYS_COLOUR_3DDKSHADOW,
"ThreeDFace": wx.SYS_COLOUR_3DFACE,
"ThreeDHighlight": wx.SYS_COLOUR_3DHIGHLIGHT,
"ThreeDLightShadow": wx.SYS_COLOUR_3DLIGHT,
"ThreeDShadow": wx.SYS_COLOUR_3DSHADOW,
"Window": wx.SYS_COLOUR_WINDOW,
"WindowFrame": wx.SYS_COLOUR_WINDOWFRAME,
"WindowText": wx.SYS_COLOUR_WINDOWTEXT
}
NamedColours.update(
#strip the alpha from the system colors. Is this really what we want to do?
(k.lower(), wx.SystemSettings.GetColour(v)[:3]) for (k,v) in systemColors.iteritems()
)
# 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 <http://www.gnu.org/licenses/>.
""" Parse CSS identifiers. More complicated than it sounds"""
from pyparsing import Word, Literal, Regex, Combine, Optional, White, oneOf, ZeroOrMore
import string
import re
class White(White):
""" Customize whitespace to match the CSS spec values"""
def __init__(self, ws=" \t\r\n\f", min=1, max=0, exact=0):
super(White, self).__init__(ws, min, max, exact)
escaped = (
Literal("\\").suppress() +
#chr(20)-chr(126) + chr(128)-unichr(sys.maxunicode)
Regex(u"[\u0020-\u007e\u0080-\uffff]", re.IGNORECASE)
)
def convertToUnicode(t):
return unichr(int(t[0], 16))
hex_unicode = (
Literal("\\").suppress() +
Regex("[0-9a-f]{1,6}", re.IGNORECASE) +
Optional(White(exact=1)).suppress()
).setParseAction(convertToUnicode)
escape = hex_unicode | escaped
#any unicode literal outside the 0-127 ascii range
nonascii = Regex(u"[^\u0000-\u007f]")
#single character for starting an identifier.
nmstart = Regex(u"[A-Z]", re.IGNORECASE) | nonascii | escape
nmchar = Regex(u"[0-9A-Z-]", re.IGNORECASE) | nonascii | escape
identifier = Combine(nmstart + ZeroOrMore(nmchar))
# 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 <http://www.gnu.org/licenses/>.
""" Parser for inline CSS in style attributes """
def inlineStyle(styleString):
if not styleString:
return {}
styles = styleString.split(";")
rv = dict(style.split(":") for style in styles if len(style) != 0)
return rv
# 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 <http://www.gnu.org/licenses/>.
"""
Parsing for CSS and CSS-style values, such as transform and filter attributes.
"""
from pyparsing import (Literal, Word, CaselessLiteral,
Optional, Combine, Forward, ZeroOrMore, nums, oneOf, Group, delimitedList)
#some shared definitions from pathdata
from ..pathdata import number, maybeComma
paren = Literal("(").suppress()
cparen = Literal(")").suppress()
def Parenthised(exp):
return Group(paren + exp + cparen)
skewY = Literal("skewY") + Parenthised(number)
skewX = Literal("skewX") + Parenthised(number)
rotate = Literal("rotate") + Parenthised(
number + Optional(maybeComma + number + maybeComma + number)
)
scale = Literal("scale") + Parenthised(
number + Optional(maybeComma + number)
)
translate = Literal("translate") + Parenthised(
number + Optional(maybeComma + number)
)
matrix = Literal("matrix") + Parenthised(
#there's got to be a better way to write this
number + maybeComma +
number + maybeComma +
number + maybeComma +
number + maybeComma +
number + maybeComma +
number
)
transform = (skewY | skewX | rotate | scale | translate | matrix)
transformList = delimitedList(Group(transform), delim=maybeComma)
if __name__ == '__main__':
from tests.test_css import *
unittest.main()
# 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 <http://www.gnu.org/licenses/>.
"""
Parser for various kinds of CSS values as per CSS2 spec section 4.3
"""
from pyparsing import Word, Combine, Optional, Literal, oneOf, CaselessLiteral, StringEnd
def asInt(s,l,t):
return int(t[0])
def asFloat(s,l,t):
return float(t[0])
def asFloatOrInt(s,l,t):
""" Return an int if possible, otherwise a float"""
v = t[0]
try:
return int(v)
except ValueError:
return float(v)
integer = Word("0123456789").setParseAction(asInt)
number = Combine(
Optional(Word("0123456789")) + Literal(".") + Word("01234567890")
| integer
)
number.setName('number')
sign = oneOf("+ -")
signedNumber = Combine(Optional(sign) + number).setParseAction(asFloat)
lengthValue = Combine(Optional(sign) + number).setParseAction(asFloatOrInt)
lengthValue.setName('lengthValue')
#TODO: The physical units like in, mm
lengthUnit = oneOf(['em', 'ex', 'px', 'pt', '%'], caseless=True)
#the spec says that the unit is only optional for a 0 length, but
#there are just too many places where a default is permitted.
#TODO: Maybe should use a ctor like optional to let clients declare it?
length = lengthValue + Optional(lengthUnit, default=None) + StringEnd()
length.leaveWhitespace()
#set the parse action aftward so it doesn't "infect" the parsers that build on it
number.setParseAction(asFloat)
# 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 <http://www.gnu.org/licenses/>.
"""
SVGDocument
"""
import wx
from cStringIO import StringIO
import warnings
import math
from functools import wraps
import pathdata
import css
from svg.css.colour import colourValue
from svg.css import values
from attributes import paintValue
document = """<?xml version = "1.0" standalone = "no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width = "4cm" height = "4cm" viewBox = "0 0 400 400"
xmlns = "http://www.w3.org/2000/svg" version = "1.1">
<title>Example triangle01- simple example of a 'path'</title>
<desc>A path that draws a triangle</desc>
<rect x = "1" y = "1" width = "398" height = "398"
fill = "none" stroke = "blue" />
<path d = "M 100 100 L 300 100 L 200 300 z"
fill = "red" stroke = "blue" stroke-width = "3" />
</svg>"""
makePath = lambda: wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
def attrAsFloat(node, attr, defaultValue = "0"):
val = node.get(attr, defaultValue)
#TODO: process stuff like "inherit" by walking back up the nodes
#fast path optimization - if it's a valid float, don't
#try to parse it.
try:
return float(val)
except ValueError:
return valueToPixels(val)
def valueToPixels(val, defaultUnits = "px"):
#TODO manage default units
from pyparsing import ParseException
try:
val, unit = values.length.parseString(val)
except ParseException:
print "***", val
raise
#todo: unit conversion to something other than pixels
return val
def pathHandler(func):
"""decorator for methods which return a path operation
Creates the path they will fill,
and generates the path operations for the node
"""
@wraps(func)
def inner(self, node):
#brush = self.getBrushFromState()
#pen = self.getPenFromState()
#if not (brush or pen):
# return None, []
path = wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
func(self, node, path)
ops = self.generatePathOps(path)
return path, ops
return inner
class SVGDocument(object):
lastControl = None
brushCache = {}
penCache = {}
def __init__(self, element):
"""
Create an SVG document from an ElementTree node.
"""
self.handlers = {
'{http://www.w3.org/2000/svg}svg':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}a':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}g':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}rect':self.addRectToDocument,
'{http://www.w3.org/2000/svg}circle': self.addCircleToDocument,
'{http://www.w3.org/2000/svg}ellipse': self.addEllipseToDocument,
'{http://www.w3.org/2000/svg}line': self.addLineToDocument,
'{http://www.w3.org/2000/svg}polyline': self.addPolyLineToDocument,
'{http://www.w3.org/2000/svg}polygon': self.addPolygonToDocument,
'{http://www.w3.org/2000/svg}path':self.addPathDataToDocument,
'{http://www.w3.org/2000/svg}text':self.addTextToDocument
}
assert element.tag == '{http://www.w3.org/2000/svg}svg', 'Not an SVG fragment'
self.tree = element
self.paths = {}
self.stateStack = [{}]
path, ops = self.processElement(element)
self.ops = ops
@property
def state(self):
""" Retrieve the current state, without popping"""
return self.stateStack[-1]
def processElement(self, element):
""" Process one element of the XML tree.
Returns the path representing the node,
and an operation list for drawing the node.
Parent nodes should return a path (for hittesting), but
no draw operations
"""
#copy the current state
current = dict(self.state)
current.update(element.items())
current.update(css.inlineStyle(element.get("style", "")))
self.stateStack.append(current)
handler = self.handlers.get(element.tag, lambda *any: (None, None))
path, ops = handler(element)
self.paths[element] = path
self.stateStack.pop()
return path, ops
def createTransformOpsFromNode(self, node):
""" Returns an oplist for transformations.
This applies to a node, not the current state because
the transform stack is saved in the wxGraphicsContext.
This oplist does *not* include the push/pop state commands
"""
ops = []
transform = node.get('transform')
#todo: replace this with a mapping list
if transform:
for transform, args in css.transformList.parseString(transform):
if transform == 'scale':
if len(args) == 1:
x = y = args[0]
else:
x, y = args
ops.append(
(wx.GraphicsContext.Scale, (x, y))
)
if transform == 'translate':
if len(args) == 1:
x = args[0]
y = 0
else:
x, y = args
ops.append(
(wx.GraphicsContext.Translate, (x, y))
)
if transform == 'rotate':
if len(args) == 3:
angle, cx, cy = args
angle = math.radians(angle)
ops.extend([
(wx.GraphicsContext.Translate, (cx, cy)),
(wx.GraphicsContext.Rotate, (angle,)),
(wx.GraphicsContext.Translate, (-cx, -cy)),
])
else:
angle = args[0]
angle = math.radians(angle)
ops.append(
(wx.GraphicsContext.Rotate, (angle,))
)
if transform == 'matrix':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
*args
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
if transform == 'skewX':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
1, 0, math.tan(math.radians(args[0])), 1, 0, 0
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
if transform == 'skewY':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
1, math.tan(math.radians(args[0])), 0, 1, 0, 0
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
return ops
def addGroupToDocument(self, node):
""" For parent elements: push on a state,
then process all child elements
"""
ops = [
(wx.GraphicsContext.PushState, ())
]
path = makePath()
ops.extend(self.createTransformOpsFromNode(node))
for child in node.getchildren():
cpath, cops = self.processElement(child)
if cpath:
path.AddPath(cpath)
if cops:
ops.extend(cops)
ops.append(
(wx.GraphicsContext.PopState, ())
)
return path, ops
def getFontFromState(self):
font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
family = self.state.get("font-family")
if False:#family:
#print "setting font", family
font.SetFaceName(family)
size = self.state.get("font-size")
#I'm not sure if this is right or not
if size:
val, unit = values.length.parseString(size)
if '__WXMSW__' in wx.PlatformInfo:
i = int(val)
font.SetPixelSize((i, i))
else:
font.SetPointSize(int(val))
return font
def addTextToDocument(self, node):
x, y = [attrAsFloat(node, attr) for attr in ('x', 'y')]
def DoDrawText(context, text, x, y, brush = wx.NullGraphicsBrush):
#SVG spec appears to originate text from the bottom
#rather than the top as with our API. This function
#will measure and then re-orient the text as needed.
w, h = context.GetTextExtent(text)
y -= h
context.DrawText(text, x, y, brush)
font = self.getFontFromState()
brush = self.getBrushFromState()
if not (brush and brush.IsOk()):
brush = wx.BLACK_BRUSH
#normalize whitespace
#TODO: SVG probably has rules for this
text = ' '.join(node.text.split() if node.text else "")
if text is None:
return None, []
ops = [
(wx.GraphicsContext.SetFont, (font, brush.Colour)),
(DoDrawText, (text, x, y))
]
return None, ops
@pathHandler
def addRectToDocument(self, node, path):
x, y, w, h = (attrAsFloat(node, attr) for attr in ['x', 'y', 'width', 'height'])
rx = node.get('rx')
ry = node.get('ry')
if not (w and h):
path.MoveToPoint(x, y) #keep the current point correct
return
if rx or ry:
if rx and ry:
rx, ry = float(rx), float(ry)
elif rx:
rx = ry = float(rx)
elif ry:
rx = ry = float(ry)
#value clamping as per spec section 9.2
rx = min(rx, w/2)
ry = min(ry, h/2)
#origin
path.MoveToPoint(x+rx, y)
path.AddLineToPoint(x+w-rx, y)
#top right
cx = rx * 2
cy = ry * 2
path.AddEllipticalArc(
x+w-cx, y,
cx, cy,
math.radians(270), math.radians(0),
True
)
path.AddLineToPoint(x+w, y+h-ry)
#bottom right
path.AddEllipticalArc(
x+w-cx, y+h-cy,
cx, cy,
math.radians(0), math.radians(90),
True
)
path.AddLineToPoint(x+rx, y+h)
#bottom left
path.AddEllipticalArc(
x, y+h-cy,
cx, cy,
math.radians(90),
math.radians(180),
True
)
path.AddLineToPoint(x, y+ry)
#bottom right
path.AddEllipticalArc(
x, y,
cx, cy,
math.radians(180),
math.radians(270),
True
)
path.CloseSubpath()
else:
path.AddRectangle(
x, y, w, h
)
@pathHandler
def addCircleToDocument(self, node, path):
cx, cy, r = [attrAsFloat(node, attr) for attr in ('cx', 'cy', 'r')]
path.AddCircle(cx, cy, r)
@pathHandler
def addEllipseToDocument(self, node, path):
cx, cy, rx, ry = [float(node.get(attr, 0)) for attr in ('cx', 'cy', 'rx', 'ry')]
#cx, cy are centerpoint.
#rx, ry are radius.
#wxGC coords are the rect of the ellipse bounding box.
if rx <= 0 or ry <= 0:
return
x = cx - rx
y = cy - ry
path.AddEllipse(x, y, rx*2, ry*2)
@pathHandler
def addLineToDocument(self, node, path):
x1, y1, x2, y2 = [attrAsFloat(node, attr) for attr in ('x1', 'y1', 'x2', 'y2')]
path.MoveToPoint(x1, y1)
path.AddLineToPoint(x2, y2)
@pathHandler
def addPolyLineToDocument(self, node, path):
#translate to pathdata and render that
data = "M " + node.get("points")
self.addPathDataToPath(data, path)
@pathHandler
def addPolygonToDocument(self, node, path):
#translate to pathdata and render that
data = "M " + node.get("points") + " Z"
self.addPathDataToPath(data, path)
@pathHandler
def addPathDataToDocument(self, node, path):
self.addPathDataToPath(node.get('d', ''), path)
def addPathDataToPath(self, data, path):
self.lastControl = None
self.lastControlQ = None
self.firstPoints = []
def normalizeStrokes(parseResults):
""" The data comes from the parser in the
form of (command, [list of arguments]).
We translate that to [(command, args[0]), (command, args[1])]
via a generator.
M is special cased because its subsequent arguments
become linetos.
"""
for command, arguments in parseResults:
if not arguments:
yield (command, ())
else:
arguments = iter(arguments)
if command == 'm':
yield (command, arguments.next())
command = "l"
elif command == "M":
yield (command, arguments.next())
command = "L"
for arg in arguments:
yield (command, arg)
#print "data length", len(data)
import time
t = time.time()
parsed = pathdata.svg.parseString(data)
#print "parsed in", time.time()-t
for stroke in normalizeStrokes(parsed):
self.addStrokeToPath(path, stroke)
def generatePathOps(self, path):
""" Look at the current state and generate the
draw operations (fill, stroke, neither) for the path"""
ops = []
brush = self.getBrushFromState(path)
fillRule = self.state.get('fill-rule', 'nonzero')
frMap = {'nonzero':wx.WINDING_RULE, 'evenodd': wx.ODDEVEN_RULE}
fr = frMap.get(fillRule, wx.ODDEVEN_RULE)
if brush:
ops.append(
(wx.GraphicsContext.SetBrush, (brush,))
)
ops.append(
(wx.GraphicsContext.FillPath, (path, fr))
)
pen = self.getPenFromState()
if pen:
ops.append(
(wx.GraphicsContext.SetPen, (pen,))
)
ops.append(
(wx.GraphicsContext.StrokePath, (path,))
)
return ops
def getPenFromState(self):
pencolour = self.state.get('stroke', 'none')
if pencolour == 'currentColor':
pencolour = self.state.get('color', 'none')
if pencolour == 'transparent':
return wx.TRANSPARENT_PEN
if pencolour == 'none':
return wx.NullPen
type, value = colourValue.parseString(pencolour)
if type == 'URL':
warnings.warn("Color servers for stroking not implemented")
return wx.NullPen
else:
if value[:3] == (-1, -1, -1):
return wx.NullPen
pen = wx.Pen(wx.Colour(*value))
width = self.state.get('stroke-width')
if width:
width, units = values.length.parseString(width)
pen.SetWidth(width)
capmap = {
'butt':wx.CAP_BUTT,
'round':wx.CAP_ROUND,
'square':wx.CAP_PROJECTING
}
joinmap = {
'miter':wx.JOIN_MITER,
'round':wx.JOIN_ROUND,
'bevel':wx.JOIN_BEVEL
}
pen.SetCap(capmap.get(self.state.get('stroke-linecap', None), wx.CAP_BUTT))
pen.SetJoin(joinmap.get(self.state.get('stroke-linejoin', None), wx.JOIN_MITER))
return wx.GraphicsRenderer_GetDefaultRenderer().CreatePen(pen)
def getBrushFromState(self, path = None):
brushcolour = self.state.get('fill', 'black').strip()
type, details = paintValue.parseString(brushcolour)
if type == "URL":
url, fallback = details
element = self.resolveURL(url)
if not element:
if fallback:
type, details = fallback
else:
r, g, b, = 0, 0, 0
else:
if element.tag == '{http://www.w3.org/2000/svg}linearGradient':
box = path.GetBox()
x, y, w, h = box.Get()
return wx.GraphicsRenderer.GetDefaultRenderer().CreateLinearGradientBrush(
x, y, x+w, y+h, wx.Colour(0, 0, 255, 128), wx.RED
)
elif element.tag == '{http://www.w3.org/2000/svg}radialGradient':
box = path.GetBox()
x, y, w, h = box.Get()
#print w
mx = wx.GraphicsRenderer.GetDefaultRenderer().CreateMatrix(x, y, w, h)
cx, cy = mx.TransformPoint(0.5, 0.5)
fx, fy = cx, cy
return wx.GraphicsRenderer.GetDefaultRenderer().CreateRadialGradientBrush(
cx, cy,
fx, fy,
(max(w, h))/2,
wx.Colour(0, 0, 255, 128), wx.RED
)
else:
#invlid gradient specified
return wx.NullBrush
r, g, b = 0, 0, 0
if type == 'CURRENTCOLOR':
type, details = paintValue.parseString(self.state.get('color', 'none'))
if type == 'RGB':
r, g, b = details
elif type == "NONE":
return wx.NullBrush
opacity = self.state.get('fill-opacity', self.state.get('opacity', '1'))
opacity = float(opacity)
opacity = min(max(opacity, 0.0), 1.0)
a = 255 * opacity
#using try/except block instead of
#just setdefault because the wxBrush and wxColour would
#be created every time anyway in order to pass them,
#defeating the purpose of the cache
try:
return SVGDocument.brushCache[(r, g, b, a)]
except KeyError:
return SVGDocument.brushCache.setdefault((r, g, b, a), wx.Brush(wx.Colour(r, g, b, a)))
def resolveURL(self, urlData):
"""
Resolve a URL and return an elementTree Element from it.
Return None if unresolvable
"""
scheme, netloc, path, query, fragment = urlData
if scheme == netloc == path == '':
#horrible. There's got to be a better way?
if self.tree.get("id") == fragment:
return self.tree
else:
for child in self.tree.getiterator():
#print child.get("id")
if child.get("id") == fragment:
return child
return None
else:
return self.resolveRemoteURL(urlData)
def resolveRemoteURL(self, url):
return None
def addStrokeToPath(self, path, stroke):
""" Given a stroke from a path command
(in the form (command, arguments)) create the path
commands that represent it.
TODO: break out into (yet another) class/module,
especially so we can get O(1) dispatch on type?
"""
type, arg = stroke
relative = False
if type == type.lower():
relative = True
ox, oy = path.GetCurrentPoint().Get()
else:
ox = oy = 0
def normalizePoint(arg):
x, y = arg
return x+ox, y+oy
def reflectPoint(point, relativeTo):
x, y = point
a, b = relativeTo
return ((a*2)-x), ((b*2)-y)
type = type.upper()
if type == 'M':
pt = normalizePoint(arg)
self.firstPoints.append(pt)
path.MoveToPoint(pt)
elif type == 'L':
path.AddLineToPoint(normalizePoint(arg))
elif type == 'C':
#control1, control2, endpoint = arg
control1, control2, endpoint = map(
normalizePoint, arg
)
self.lastControl = control2
path.AddCurveToPoint(
control1,
control2,
endpoint
)
#~ cp = path.GetCurrentPoint()
#~ path.AddCircle(c1x, c1y, 5)
#~ path.AddCircle(c2x, c2y, 3)
#~ path.AddCircle(x, y, 7)
#~ path.MoveToPoint(cp)
#~ print "C", control1, control2, endpoint
elif type == 'S':
#control2, endpoint = arg
control2, endpoint = map(
normalizePoint, arg
)
if self.lastControl:
control1 = reflectPoint(self.lastControl, path.GetCurrentPoint())
else:
control1 = path.GetCurrentPoint()
#~ print "S", self.lastControl, ":", control1, control2, endpoint
self.lastControl = control2
path.AddCurveToPoint(
control1,
control2,
endpoint
)
elif type == "Q":
(cx, cy), (x, y) = map(normalizePoint, arg)
self.lastControlQ = (cx, cy)
path.AddQuadCurveToPoint(cx, cy, x, y)
elif type == "T":
x, y, = normalizePoint(arg)
if self.lastControlQ:
cx, cy = reflectPoint(self.lastControlQ, path.GetCurrentPoint())
else:
cx, cy = path.GetCurrentPoint()
self.lastControlQ = (cx, cy)
path.AddQuadCurveToPoint(cx, cy, x, y)
elif type == "V":
_, y = normalizePoint((0, arg))
x, _ = path.GetCurrentPoint()
path.AddLineToPoint(x, y)
elif type == "H":
x, _ = normalizePoint((arg, 0))
_, y = path.GetCurrentPoint()
path.AddLineToPoint(x, y)
elif type == "A":
#wxGC currently only supports circular arcs,
#not eliptical ones
(
(rx, ry), #radii of ellipse
angle, #angle of rotation on the ellipse in degrees
(fa, fs), #arc and stroke angle flags
(x, y) #endpoint on the arc
) = arg
x, y = normalizePoint((x, y))
cx, cy = path.GetCurrentPoint()
if (cx, cy) == (x, y):
return #noop
if (rx == 0 or ry == 0):
#no radius is effectively a line
path.AddLineToPoint(x, y)
return
#find the center point for the ellipse
#translation via the angle
angle = angle % 360
angle = math.radians(angle)
#translated endpoint
xPrime = math.cos(angle) * ((cx-x)/2)
xPrime += math.sin(angle) * ((cx-x)/2)
yPrime = -(math.sin(angle)) * ((cy-y)/2)
yPrime += (math.cos(angle)) * ((cy-y)/2)
temp = ((rx**2) * (ry**2)) - ((rx**2) * (yPrime**2)) - ((ry**2) * (xPrime**2))
temp /= ((rx**2) * (yPrime**2)) + ((ry**2)*(xPrime**2))
temp = abs(temp)
try:
temp = math.sqrt(temp)
except ValueError:
import pdb
pdb.set_trace()
cxPrime = temp * ((rx * yPrime) / ry)
cyPrime = temp * -((ry * xPrime) / rx)
if fa == fs:
cxPrime, cyPrime = -cxPrime, -cyPrime
#reflect backwards now for the origin
cnx = math.cos(angle) * cxPrime
cnx += math.sin(angle) * cxPrime
cny = -(math.sin(angle)) * cyPrime
cny += (math.cos(angle)) * cyPrime
cnx += ((cx+x)/2.0)
cny += ((cy+y)/2.0)
#calculate the angle between the two endpoints
lastArc = wx.Point2D(x-cnx, y-cny).GetVectorAngle()
firstArc = wx.Point2D(cx-cnx, cy-cny).GetVectorAngle()
lastArc = math.radians(lastArc)
firstArc = math.radians(firstArc)
#aargh buggines.
#AddArc draws a straight line between
#the endpoints of the arc.
#putting it in a subpath makes the strokes come out
#correctly, but it still only fills the slice
#taking out the MoveToPoint fills correctly.
path.AddEllipse(cnx-rx, cny-ry, rx*2, ry*2)
path.MoveToPoint(x, y)
#~ npath = makePath()
#~ npath.AddEllipticalArc(cnx-rx, cny-ry, rx*2, ry*2, firstArc, lastArc, False)
#~ npath.MoveToPoint(x, y)
#~ path.AddPath(npath)
elif type == 'Z':
#~ Bugginess:
#~ CloseSubpath() doesn't change the
#~ current point, as SVG spec requires.
#~ However, manually moving to the endpoint afterward opens a new subpath
#~ and (apparently) messes with stroked but not filled paths.
#~ This is possibly a bug in GDI+?
#~ Manually closing the path via AddLineTo gives incorrect line join
#~ results
#~ Manually closing the path *and* calling CloseSubpath() appears
#~ to give correct results on win32
pt = self.firstPoints.pop()
path.AddLineToPoint(pt)
path.CloseSubpath()
def render(self, context):
if not hasattr(self, "ops"):
return
for op, args in self.ops:
op(context, *args)
if __name__ == '__main__':
from tests.test_document import *
unittest.main()
# 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 <http://www.gnu.org/licenses/>.
"""
SVG path data parser
Usage:
steps = svg.parseString(pathdata)
for command, arguments in steps:
pass
"""
from pyparsing import (ParserElement, Literal, Word, CaselessLiteral,
Optional, Combine, Forward, ZeroOrMore, nums, oneOf, Group, ParseException, OneOrMore)
#ParserElement.enablePackrat()
def Command(char):
""" Case insensitive but case preserving"""
return CaselessPreservingLiteral(char)
def Arguments(token):
return Group(token)
class CaselessPreservingLiteral(CaselessLiteral):
""" Like CaselessLiteral, but returns the match as found
instead of as defined.
"""
def __init__( self, matchString ):
super(CaselessPreservingLiteral, self).__init__( matchString.upper() )
self.name = "'%s'" % matchString
self.errmsg = "Expected " + self.name
self.myException.msg = self.errmsg
def parseImpl( self, instring, loc, doActions = True ):
test = instring[ loc:loc+self.matchLen ]
if test.upper() == self.match:
return loc+self.matchLen, test
#~ raise ParseException( instring, loc, self.errmsg )
exc = self.myException
exc.loc = loc
exc.pstr = instring
raise exc
def Sequence(token):
""" A sequence of the token"""
return OneOrMore(token+maybeComma)
digit_sequence = Word(nums)
sign = oneOf("+ -")
def convertToFloat(s, loc, toks):
try:
return float(toks[0])
except:
raise ParseException(loc, "invalid float format %s"%toks[0])
exponent = CaselessLiteral("e")+Optional(sign)+Word(nums)
#note that almost all these fields are optional,
#and this can match almost anything. We rely on Pythons built-in
#float() function to clear out invalid values - loosely matching like this
#speeds up parsing quite a lot
floatingPointConstant = Combine(
Optional(sign) +
Optional(Word(nums)) +
Optional(Literal(".") + Optional(Word(nums)))+
Optional(exponent)
)
floatingPointConstant.setParseAction(convertToFloat)
number = floatingPointConstant
#same as FP constant but don't allow a - sign
nonnegativeNumber = Combine(
Optional(Word(nums)) +
Optional(Literal(".") + Optional(Word(nums)))+
Optional(exponent)
)
nonnegativeNumber.setParseAction(convertToFloat)
coordinate = number
#comma or whitespace can seperate values all over the place in SVG
maybeComma = Optional(Literal(',')).suppress()
coordinateSequence = Sequence(coordinate)
coordinatePair = (coordinate + maybeComma + coordinate).setParseAction(lambda t: tuple(t))
coordinatePairSequence = Sequence(coordinatePair)
coordinatePairPair = coordinatePair + maybeComma + coordinatePair
coordinatePairPairSequence = Sequence(Group(coordinatePairPair))
coordinatePairTriple = coordinatePair + maybeComma + coordinatePair + maybeComma + coordinatePair
coordinatePairTripleSequence = Sequence(Group(coordinatePairTriple))
#commands
lineTo = Group(Command("L") + Arguments(coordinatePairSequence))
moveTo = Group(Command("M") + Arguments(coordinatePairSequence))
closePath = Group(Command("Z")).setParseAction(lambda t: ('Z', (None,)))
flag = oneOf("1 0").setParseAction(lambda t: bool(int((t[0]))))
arcRadius = (
nonnegativeNumber + maybeComma + #rx
nonnegativeNumber #ry
).setParseAction(lambda t: tuple(t))
arcFlags = (flag + maybeComma + flag).setParseAction(lambda t: tuple(t))
ellipticalArcArgument = Group(
arcRadius + maybeComma + #rx, ry
number + maybeComma +#rotation
arcFlags + #large-arc-flag, sweep-flag
coordinatePair #(x, y)
)
ellipticalArc = Group(Command("A") + Arguments(Sequence(ellipticalArcArgument)))
smoothQuadraticBezierCurveto = Group(Command("T") + Arguments(coordinatePairSequence))
quadraticBezierCurveto = Group(Command("Q") + Arguments(coordinatePairPairSequence))
smoothCurve = Group(Command("S") + Arguments(coordinatePairPairSequence))
curve = Group(Command("C") + Arguments(coordinatePairTripleSequence))
horizontalLine = Group(Command("H") + Arguments(coordinateSequence))
verticalLine = Group(Command("V") + Arguments(coordinateSequence))
drawToCommand = (
lineTo | moveTo | closePath | ellipticalArc | smoothQuadraticBezierCurveto |
quadraticBezierCurveto | smoothCurve | curve | horizontalLine | verticalLine
)
#~ number.debug = True
moveToDrawToCommands = moveTo + ZeroOrMore(drawToCommand)
svg = ZeroOrMore(moveToDrawToCommands)
svg.keepTabs = True
def profile():
import cProfile
p = cProfile.Profile()
p.enable()
ptest()
ptest()
ptest()
p.disable()
p.print_stats()
bpath = """M204.33 139.83 C196.33 133.33 206.68 132.82 206.58 132.58 C192.33 97.08 169.35
81.41 167.58 80.58 C162.12 78.02 159.48 78.26 160.45 76.97 C161.41 75.68 167.72 79.72 168.58
80.33 C193.83 98.33 207.58 132.33 207.58 132.33 C207.58 132.33 209.33 133.33 209.58 132.58
C219.58 103.08 239.58 87.58 246.33 81.33 C253.08 75.08 256.63 74.47 247.33 81.58 C218.58 103.58
210.34 132.23 210.83 132.33 C222.33 134.83 211.33 140.33 211.83 139.83 C214.85 136.81 214.83 145.83 214.83
145.83 C214.83 145.83 231.83 110.83 298.33 66.33 C302.43 63.59 445.83 -14.67 395.83 80.83 C393.24 85.79 375.83
105.83 375.83 105.83 C375.83 105.83 377.33 114.33 371.33 121.33 C370.3 122.53 367.83 134.33 361.83 140.83 C360.14 142.67
361.81 139.25 361.83 140.83 C362.33 170.83 337.76 170.17 339.33 170.33 C348.83 171.33 350.19 183.66 350.33 183.83 C355.83
190.33 353.83 191.83 355.83 194.83 C366.63 211.02 355.24 210.05 356.83 212.83 C360.83 219.83 355.99 222.72 357.33 224.83
C360.83 230.33 354.75 233.84 354.83 235.33 C355.33 243.83 349.67 240.73 349.83 244.33 C350.33 255.33 346.33 250.83 343.83 254.83
C336.33 266.83 333.46 262.38 332.83 263.83 C329.83 270.83 325.81 269.15 324.33 270.83 C320.83 274.83 317.33 274.83 315.83 276.33
C308.83 283.33 304.86 278.39 303.83 278.83 C287.83 285.83 280.33 280.17 277.83 280.33 C270.33 280.83 271.48 279.67 269.33 277.83
C237.83 250.83 219.33 211.83 215.83 206.83 C214.4 204.79 211.35 193.12 212.33 195.83 C214.33 201.33 213.33 250.33 207.83 250.33
C202.33 250.33 201.83 204.33 205.33 195.83 C206.43 193.16 204.4 203.72 201.79 206.83 C196.33 213.33 179.5 250.83 147.59 277.83
C145.42 279.67 146.58 280.83 138.98 280.33 C136.46 280.17 128.85 285.83 112.65 278.83 C111.61 278.39 107.58 283.33 100.49 276.33
C98.97 274.83 95.43 274.83 91.88 270.83 C90.39 269.15 86.31 270.83 83.27 263.83 C82.64 262.38 79.73 266.83 72.13 254.83 C69.6 250.83
65.54 255.33 66.05 244.33 C66.22 240.73 60.48 243.83 60.99 235.33 C61.08 233.84 54.91 230.33 58.45 224.83 C59.81 222.72 54.91 219.83
58.96 212.83 C60.57 210.05 49.04 211.02 59.97 194.83 C62 191.83 59.97 190.33 65.54 183.83 C65.69 183.66 67.06 171.33 76.69 170.33
C78.28 170.17 53.39 170.83 53.9 140.83 C53.92 139.25 55.61 142.67 53.9 140.83 C47.82 134.33 45.32 122.53 44.27 121.33 C38.19 114.33
39.71 105.83 39.71 105.83 C39.71 105.83 22.08 85.79 19.46 80.83 C-31.19 -14.67 114.07 63.59 118.22 66.33 C185.58 110.83 202 145.83
202 145.83 C202 145.83 202.36 143.28 203 141.83 C203.64 140.39 204.56 140.02 204.33 139.83 z"""
def ptest():
svg.parseString(bpath)
if __name__ == '__main__':
#~ from tests.test_pathdata import *
#~ unittest.main()
profile()
...@@ -41,7 +41,7 @@ class XYButtons(BufferedCanvas): ...@@ -41,7 +41,7 @@ class XYButtons(BufferedCanvas):
center = (124, 121) center = (124, 121)
spacer = 7 spacer = 7
def __init__(self, parent, moveCallback = None, cornerCallback = None, spacebarCallback = None, bgcolor = "#FFFFFF", ID=-1): def __init__(self, parent, moveCallback = None, cornerCallback = None, spacebarCallback = None, bgcolor = "#FFFFFF", ID=-1, zcallback=None):
self.bg_bmp = wx.Image(imagefile("control_xy.png"), wx.BITMAP_TYPE_PNG).ConvertToBitmap() self.bg_bmp = wx.Image(imagefile("control_xy.png"), wx.BITMAP_TYPE_PNG).ConvertToBitmap()
self.keypad_bmp = wx.Image(imagefile("arrow_keys.png"), wx.BITMAP_TYPE_PNG).ConvertToBitmap() self.keypad_bmp = wx.Image(imagefile("arrow_keys.png"), wx.BITMAP_TYPE_PNG).ConvertToBitmap()
self.keypad_idx = -1 self.keypad_idx = -1
...@@ -51,6 +51,7 @@ class XYButtons(BufferedCanvas): ...@@ -51,6 +51,7 @@ class XYButtons(BufferedCanvas):
self.moveCallback = moveCallback self.moveCallback = moveCallback
self.cornerCallback = cornerCallback self.cornerCallback = cornerCallback
self.spacebarCallback = spacebarCallback self.spacebarCallback = spacebarCallback
self.zCallback = zcallback
self.enabled = False self.enabled = False
# Remember the last clicked buttons, so we can repeat when spacebar pressed # Remember the last clicked buttons, so we can repeat when spacebar pressed
self.lastMove = None self.lastMove = None
...@@ -60,8 +61,7 @@ class XYButtons(BufferedCanvas): ...@@ -60,8 +61,7 @@ class XYButtons(BufferedCanvas):
self.bgcolor.SetFromName(bgcolor) self.bgcolor.SetFromName(bgcolor)
self.bgcolormask = wx.Colour(self.bgcolor.Red(), self.bgcolor.Green(), self.bgcolor.Blue(), 128) self.bgcolormask = wx.Colour(self.bgcolor.Red(), self.bgcolor.Green(), self.bgcolor.Blue(), 128)
BufferedCanvas.__init__(self, parent, ID) BufferedCanvas.__init__(self, parent, ID, size=self.bg_bmp.GetSize())
self.SetSize(self.bg_bmp.GetSize())
# Set up mouse and keyboard event capture # Set up mouse and keyboard event capture
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
...@@ -109,10 +109,13 @@ class XYButtons(BufferedCanvas): ...@@ -109,10 +109,13 @@ class XYButtons(BufferedCanvas):
self.update() self.update()
def getMovement(self): def getMovement(self):
xdir = [1, 0, -1, 0][self.quadrant] xdir = [1, 0, -1, 0, 0, 0][self.quadrant]
ydir = [0, 1, 0, -1][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-1) magnitude = math.pow(10, self.concentric-1)
return (magnitude * xdir, magnitude * ydir) if not zdir == 0:
magnitude=min(magnitude,10)
return (magnitude * xdir, magnitude * ydir, magnitude * zdir)
def lookupConcentric(self, radius): def lookupConcentric(self, radius):
idx = 0 idx = 0
...@@ -286,14 +289,21 @@ class XYButtons(BufferedCanvas): ...@@ -286,14 +289,21 @@ class XYButtons(BufferedCanvas):
self.quadrant = 2 self.quadrant = 2
elif evt.GetKeyCode() == wx.WXK_RIGHT: elif evt.GetKeyCode() == wx.WXK_RIGHT:
self.quadrant = 0 self.quadrant = 0
elif evt.GetKeyCode() == wx.WXK_PAGEUP:
self.quadrant = 4
elif evt.GetKeyCode() == wx.WXK_PAGEDOWN:
self.quadrant = 5
else: else:
evt.Skip() evt.Skip()
return return
if self.moveCallback:
self.concentric = self.keypad_idx self.concentric = self.keypad_idx
x, y = self.getMovement() x, y, z = self.getMovement()
if x!=0 or y!=0 and self.moveCallback:
self.moveCallback(x, y) self.moveCallback(x, y)
if z!=0 and self.zCallback:
self.zCallback(z)
elif evt.GetKeyCode() == wx.WXK_SPACE: elif evt.GetKeyCode() == wx.WXK_SPACE:
self.spacebarCallback() self.spacebarCallback()
...@@ -347,7 +357,7 @@ class XYButtons(BufferedCanvas): ...@@ -347,7 +357,7 @@ class XYButtons(BufferedCanvas):
if self.concentric != None: if self.concentric != None:
if self.concentric < len(XYButtons.concentric_circle_radii): if self.concentric < len(XYButtons.concentric_circle_radii):
if self.quadrant != None: if self.quadrant != None:
x, y = self.getMovement() x, y, z = self.getMovement()
if self.moveCallback: if self.moveCallback:
self.lastMove = (x, y) self.lastMove = (x, y)
self.lastCorner = None self.lastCorner = None
......
...@@ -46,9 +46,7 @@ class ZButtons(BufferedCanvas): ...@@ -46,9 +46,7 @@ class ZButtons(BufferedCanvas):
self.bgcolor.SetFromName(bgcolor) self.bgcolor.SetFromName(bgcolor)
self.bgcolormask = wx.Colour(self.bgcolor.Red(), self.bgcolor.Green(), self.bgcolor.Blue(), 128) self.bgcolormask = wx.Colour(self.bgcolor.Red(), self.bgcolor.Green(), self.bgcolor.Blue(), 128)
BufferedCanvas.__init__(self, parent, ID) BufferedCanvas.__init__(self, parent, ID, size=wx.Size(59, 244))
self.SetSize(wx.Size(59, 244))
# Set up mouse and keyboard event capture # Set up mouse and keyboard event capture
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
......
...@@ -17,15 +17,19 @@ ...@@ -17,15 +17,19 @@
import cmd, sys import cmd, sys
import glob, os, time, datetime import glob, os, time, datetime
import sys, subprocess import sys, subprocess, traceback
import math, codecs import math, codecs
import shlex
from math import sqrt from math import sqrt
from gcoder import GCode import argparse
import printcore import printcore
from printrun.printrun_utils import install_locale from printrun.printrun_utils import install_locale
from printrun import gcoder
install_locale('pronterface') install_locale('pronterface')
from functools import wraps
if os.name == "nt": if os.name == "nt":
try: try:
import _winreg import _winreg
...@@ -44,93 +48,6 @@ except: ...@@ -44,93 +48,6 @@ except:
def dosify(name): def dosify(name):
return os.path.split(name)[1].split(".")[0][:8]+".g" return os.path.split(name)[1].split(".")[0][:8]+".g"
def measurements(g):
gcode = GCode(g)
gcode.measure()
return (gcode.width, gcode.depth, gcode.height, gcode.xmin, gcode.xmax, gcode.ymin, gcode.ymax, gcode.zmin, gcode.zmax)
def totalelength(g):
gcode = GCode(g)
return gcode.filament_length()
def get_coordinate_value(axis, parts):
for i in parts:
if (axis in i):
return float(i[1:])
return None
def hypot3d(X1, Y1, Z1, X2 = 0.0, Y2 = 0.0, Z2 = 0.0):
return math.hypot(X2-X1, math.hypot(Y2-Y1, Z2-Z1))
def estimate_duration(g):
lastx = lasty = lastz = laste = lastf = 0.0
x = y = z = e = f = 0.0
currenttravel = 0.0
totaltravel = 0.0
moveduration = 0.0
totalduration = 0.0
acceleration = 1500.0 #mm/s/s ASSUMING THE DEFAULT FROM SPRINTER !!!!
layerduration = 0.0
layerbeginduration = 0.0
layercount = 0
#TODO:
# get device caps from firmware: max speed, acceleration/axis (including extruder)
# calculate the maximum move duration accounting for above ;)
# self.log(".... estimating ....")
for i in g:
i = i.split(";")[0]
if "G4" in i or "G1" in i:
if "G4" in i:
parts = i.split(" ")
moveduration = get_coordinate_value("P", parts[1:])
if moveduration is None:
continue
else:
moveduration /= 1000.0
if "G1" in i:
parts = i.split(" ")
x = get_coordinate_value("X", parts[1:])
if x is None: x = lastx
y = get_coordinate_value("Y", parts[1:])
if y is None: y = lasty
z = get_coordinate_value("Z", parts[1:])
if (z is None) or (z<lastz): z = lastz # Do not increment z if it's below the previous (Lift z on move fix)
e = get_coordinate_value("E", parts[1:])
if e is None: e = laste
f = get_coordinate_value("F", parts[1:])
if f is None: f = lastf
else: f /= 60.0 # mm/s vs mm/m
# given last feedrate and current feedrate calculate the distance needed to achieve current feedrate.
# if travel is longer than req'd distance, then subtract distance to achieve full speed, and add the time it took to get there.
# then calculate the time taken to complete the remaining distance
currenttravel = hypot3d(x, y, z, lastx, lasty, lastz)
distance = abs(2* ((lastf+f) * (f-lastf) * 0.5 ) / acceleration) #2x because we have to accelerate and decelerate
if distance <= currenttravel and ( lastf + f )!=0 and f!=0:
moveduration = 2 * distance / ( lastf + f )
currenttravel -= distance
moveduration += currenttravel/f
else:
moveduration = math.sqrt( 2 * distance / acceleration )
totalduration += moveduration
if z > lastz:
layercount +=1
#self.log("layer z: ", lastz, " will take: ", time.strftime('%H:%M:%S', time.gmtime(totalduration-layerbeginduration)))
layerbeginduration = totalduration
lastx = x
lasty = y
lastz = z
laste = e
lastf = f
#self.log("Total Duration: " #, time.strftime('%H:%M:%S', time.gmtime(totalduration)))
return "{0:d} layers, ".format(int(layercount)) + str(datetime.timedelta(seconds = int(totalduration)))
def confirm(): def confirm():
y_or_n = raw_input("y/n: ") y_or_n = raw_input("y/n: ")
if y_or_n == "y": if y_or_n == "y":
...@@ -139,27 +56,193 @@ def confirm(): ...@@ -139,27 +56,193 @@ def confirm():
return confirm() return confirm()
return False return False
class Settings: def setting_add_tooltip(func):
@wraps(func)
def decorator(self, *args, **kwargs):
widget = func(self, *args, **kwargs)
if self.help:
widget.SetToolTipString(self.help)
return widget
return decorator
class Setting(object):
DEFAULT_GROUP = "General"
hidden = False
def __init__(self, name, default, label = None, help = None, group = None):
self.name = name
self.default = default
self._value = default
self.label = label
self.help = help
self.group = group if group else Setting.DEFAULT_GROUP
def _get_value(self):
return self._value
def _set_value(self, value):
raise NotImplementedError
value = property(_get_value, _set_value)
@setting_add_tooltip
def get_label(self, parent):
import wx
widget = wx.StaticText(parent, -1, self.label or self.name)
return widget
@setting_add_tooltip
def get_widget(self, parent):
return self.get_specific_widget(parent)
def get_specific_widget(self, parent):
raise NotImplementedError
def set(self, value):
raise NotImplementedError
def get(self):
raise NotImplementedError
def update(self):
raise NotImplementedError
def __str__(self):
return self.name
def __repr__(self):
return self.name
class HiddenSetting(Setting):
hidden = True
def _set_value(self, value):
self._value = value
value = property(Setting._get_value, _set_value)
class wxSetting(Setting):
widget = None
def _set_value(self, value):
self._value = value
if self.widget:
self.widget.SetValue(value)
value = property(Setting._get_value, _set_value)
def update(self):
self.value = self.widget.GetValue()
class StringSetting(wxSetting):
def get_specific_widget(self, parent):
import wx
self.widget = wx.TextCtrl(parent, -1, str(self.value))
return self.widget
class ComboSetting(wxSetting):
def __init__(self, name, default, choices, label = None, help = None, group = None):
super(ComboSetting, self).__init__(name, default, label, help, group)
self.choices = choices
def get_specific_widget(self, parent):
import wx
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):
super(SpinSetting, self).__init__(name, default, label, help, group)
self.min = min
self.max = max
def get_specific_widget(self, parent):
import wx
self.widget = wx.SpinCtrl(parent, -1, min = self.min, max = self.max)
self.widget.SetValue(self.value)
return self.widget
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, digits = 2)
return self.widget
class BooleanSetting(wxSetting):
def _get_value(self):
return bool(self._value)
def _set_value(self, value):
self._value = value
value = property(_get_value, _set_value)
def get_specific_widget(self, parent):
import wx
self.widget = wx.CheckBox(parent, -1)
self.widget.SetValue(bool(self.value))
return self.widget
class Settings(object):
#def _temperature_alias(self): return {"pla":210, "abs":230, "off":0} #def _temperature_alias(self): return {"pla":210, "abs":230, "off":0}
#def _temperature_validate(self, v): #def _temperature_validate(self, v):
# if v < 0: raise ValueError("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0.") # if v < 0: raise ValueError("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0.")
#def _bedtemperature_alias(self): return {"pla":60, "abs":110, "off":0} #def _bedtemperature_alias(self): return {"pla":60, "abs":110, "off":0}
def _baudrate_list(self): return ["2400", "9600", "19200", "38400", "57600", "115200"] def _baudrate_list(self): return ["2400", "9600", "19200", "38400", "57600", "115200", "250000"]
def __init__(self): def __init__(self):
# defaults here. # defaults here.
# the initial value determines the type # the initial value determines the type
self.port = "" self._add(StringSetting("port", "", _("Serial port"), _("Port used to communicate with printer")))
self.baudrate = 115200 self._add(ComboSetting("baudrate", 115200, self._baudrate_list(), _("Baud rate"), _("Communications Speed (default: 115200)")))
self.bedtemp_abs = 110 self._add(SpinSetting("bedtemp_abs", 110, 0, 400, _("Bed temperature for ABS"), _("Heated Build Platform temp for ABS (default: 110 deg C)"), "Printer"))
self.bedtemp_pla = 60 self._add(SpinSetting("bedtemp_pla", 60, 0, 400, _("Bed temperature for PLA"), _("Heated Build Platform temp for PLA (default: 60 deg C)"), "Printer"))
self.temperature_abs = 230 self._add(SpinSetting("temperature_abs", 230, 0, 400, _("Extruder temperature for ABS"), _("Extruder temp for ABS (default: 230 deg C)"), "Printer"))
self.temperature_pla = 185 self._add(SpinSetting("temperature_pla", 185, 0, 400, _("Extruder temperature for PLA"), _("Extruder temp for PLA (default: 185 deg C)"), "Printer"))
self.xy_feedrate = 3000 self._add(SpinSetting("xy_feedrate", 3000, 0, 50000, _("X && Y manual feedrate"), _("Feedrate for Control Panel Moves in X and Y (default: 3000mm/min)"), "Printer"))
self.z_feedrate = 200 self._add(SpinSetting("z_feedrate", 200, 0, 50000, _("Z manual feedrate"), _("Feedrate for Control Panel Moves in Z (default: 200mm/min)"), "Printer"))
self.e_feedrate = 300 self._add(SpinSetting("e_feedrate", 100, 0, 1000, _("E manual feedrate"), _("Feedrate for Control Panel Moves in Extrusions (default: 300mm/min)"), "Printer"))
self.slicecommand = "python skeinforge/skeinforge_application/skeinforge_utilities/skeinforge_craft.py $s" self._add(StringSetting("slicecommand", "python skeinforge/skeinforge_application/skeinforge_utilities/skeinforge_craft.py $s", _("Slice command"), _("Slice command\n default:\n python skeinforge/skeinforge_application/skeinforge_utilities/skeinforge_craft.py $s)"), "External"))
self.sliceoptscommand = "python skeinforge/skeinforge_application/skeinforge.py" self._add(StringSetting("sliceoptscommand", "python skeinforge/skeinforge_application/skeinforge.py", _("Slicer options command"), _("Slice settings command\n default:\n python skeinforge/skeinforge_application/skeinforge.py"), "External"))
self.final_command = "" self._add(StringSetting("final_command", "", _("Final command"), _("Executable to run when the print is finished"), "External"))
self._add(HiddenSetting("project_offset_x", 0.0))
self._add(HiddenSetting("project_offset_y", 0.0))
self._add(HiddenSetting("project_interval", 2.0))
self._add(HiddenSetting("project_pause", 2.5))
self._add(HiddenSetting("project_scale", 1.0))
self._add(HiddenSetting("project_x", 1024.0))
self._add(HiddenSetting("project_y", 768.0))
self._add(HiddenSetting("project_projected_x", 150.0))
self._add(HiddenSetting("project_direction", "Top Down"))
self._add(HiddenSetting("project_overshoot", 3.0))
self._add(HiddenSetting("project_z_axis_rate", 200))
self._add(HiddenSetting("project_layer", 0.1))
self._add(HiddenSetting("project_prelift_gcode", ""))
self._add(HiddenSetting("project_postlift_gcode", ""))
_settings = []
def __setattr__(self, name, value):
if name.startswith("_"):
return object.__setattr__(self, name, value)
if isinstance(value, Setting):
if not value.hidden:
self._settings.append(value)
object.__setattr__(self, "_" + name, value)
elif hasattr(self, "_" + name):
getattr(self, "_" + name).value = value
else:
setattr(self, name, StringSetting(name = name, default = value))
def __getattr__(self, name):
if name.startswith("_"):
return object.__getattribute__(self, name)
return getattr(self, "_" + name).value
def _add(self, setting):
setattr(self, setting.name, setting)
def _set(self, key, value): def _set(self, key, value):
try: try:
...@@ -172,12 +255,15 @@ class Settings: ...@@ -172,12 +255,15 @@ class Settings:
getattr(self, "_%s_validate"%key)(value) getattr(self, "_%s_validate"%key)(value)
except AttributeError: except AttributeError:
pass pass
setattr(self, key, type(getattr(self, key))(value)) t = type(getattr(self, key))
if t == bool and value == "False": setattr(self, key, False)
else: setattr(self, key, t(value))
try: try:
getattr(self, "_%s_cb"%key)(key, value) getattr(self, "_%s_cb"%key)(key, value)
except AttributeError: except AttributeError:
pass pass
return value return value
def _tabcomplete(self, key): def _tabcomplete(self, key):
try: try:
return getattr(self, "_%s_list"%key)() return getattr(self, "_%s_list"%key)()
...@@ -188,8 +274,9 @@ class Settings: ...@@ -188,8 +274,9 @@ class Settings:
except AttributeError: except AttributeError:
pass pass
return [] return []
def _all_settings(self): def _all_settings(self):
return dict([(k, getattr(self, k)) for k in self.__dict__.keys() if not k.startswith("_")]) return self._settings
class Status: class Status:
...@@ -232,7 +319,7 @@ class pronsole(cmd.Cmd): ...@@ -232,7 +319,7 @@ class pronsole(cmd.Cmd):
self.recvlisteners = [] self.recvlisteners = []
self.in_macro = False self.in_macro = False
self.p.onlinecb = self.online self.p.onlinecb = self.online
self.f = None self.fgcode = None
self.listing = 0 self.listing = 0
self.sdfiles = [] self.sdfiles = []
self.paused = False self.paused = False
...@@ -253,30 +340,14 @@ class pronsole(cmd.Cmd): ...@@ -253,30 +340,14 @@ class pronsole(cmd.Cmd):
self.settings._bedtemp_pla_cb = self.set_temp_preset self.settings._bedtemp_pla_cb = self.set_temp_preset
self.monitoring = 0 self.monitoring = 0
self.silent = False self.silent = False
self.helpdict = {}
self.helpdict["baudrate"] = _("Communications Speed (default: 115200)")
self.helpdict["bedtemp_abs"] = _("Heated Build Platform temp for ABS (default: 110 deg C)")
self.helpdict["bedtemp_pla"] = _("Heated Build Platform temp for PLA (default: 60 deg C)")
self.helpdict["e_feedrate"] = _("Feedrate for Control Panel Moves in Extrusions (default: 300mm/min)")
self.helpdict["port"] = _("Port used to communicate with printer")
self.helpdict["slicecommand"] = _("Slice command\n default:\n python skeinforge/skeinforge_application/skeinforge_utilities/skeinforge_craft.py $s)")
self.helpdict["sliceoptscommand"] = _("Slice settings command\n default:\n python skeinforge/skeinforge_application/skeinforge.py")
self.helpdict["temperature_abs"] = _("Extruder temp for ABS (default: 230 deg C)")
self.helpdict["temperature_pla"] = _("Extruder temp for PLA (default: 185 deg C)")
self.helpdict["xy_feedrate"] = _("Feedrate for Control Panel Moves in X and Y (default: 3000mm/min)")
self.helpdict["z_feedrate"] = _("Feedrate for Control Panel Moves in Z (default: 200mm/min)")
self.helpdict["final_command"] = _("Executable to run when the print is finished")
self.commandprefixes='MGT$' self.commandprefixes='MGT$'
self.webrequested = False
self.web_config = None
self.web_auth_config = None
self.promptstrs = {"offline" : "%(bold)suninitialized>%(normal)s ", self.promptstrs = {"offline" : "%(bold)suninitialized>%(normal)s ",
"fallback" : "%(bold)sPC>%(normal)s ", "fallback" : "%(bold)sPC>%(normal)s ",
"macro" : "%(bold)s..>%(normal)s ", "macro" : "%(bold)s..>%(normal)s ",
"online" : "%(bold)sT:%(extruder_temp_fancy)s %(progress_fancy)s >%(normal)s "} "online" : "%(bold)sT:%(extruder_temp_fancy)s %(progress_fancy)s >%(normal)s "}
def log(self, *msg): def log(self, *msg):
print ''.join(str(i) for i in msg) print u"".join(unicode(i) for i in msg)
def promptf(self): def promptf(self):
"""A function to generate prompts so that we can do dynamic prompts. """ """A function to generate prompts so that we can do dynamic prompts. """
...@@ -555,6 +626,8 @@ class pronsole(cmd.Cmd): ...@@ -555,6 +626,8 @@ class pronsole(cmd.Cmd):
self.processing_rc = False self.processing_rc = False
def load_default_rc(self, rc_filename = ".pronsolerc"): 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"
try: try:
try: try:
self.load_rc(os.path.join(os.path.expanduser("~"), rc_filename)) self.load_rc(os.path.join(os.path.expanduser("~"), rc_filename))
...@@ -611,7 +684,7 @@ class pronsole(cmd.Cmd): ...@@ -611,7 +684,7 @@ class pronsole(cmd.Cmd):
#else: #else:
# self.log("Removed '"+key+"' from '"+self.rc_filename+"'") # self.log("Removed '"+key+"' from '"+self.rc_filename+"'")
except Exception, e: except Exception, e:
self.log("Saving failed for", key+":", str(e)) self.log("Saving failed for ", key+":", str(e))
finally: finally:
del rci, rco del rci, rco
...@@ -671,20 +744,20 @@ class pronsole(cmd.Cmd): ...@@ -671,20 +744,20 @@ class pronsole(cmd.Cmd):
def help_disconnect(self): def help_disconnect(self):
self.log("Disconnects from the printer") self.log("Disconnects from the printer")
def do_load(self,l): def do_load(self, filename):
self._do_load(l) self._do_load(filename)
def _do_load(self,l): def _do_load(self, filename):
if len(l)==0: if not filename:
self.log("No file name given.") self.log("No file name given.")
return return
self.log("Loading file:"+l) self.log("Loading file:" + filename)
if not(os.path.exists(l)): if not os.path.exists(filename):
self.log("File not found!") self.log("File not found!")
return return
self.f = [i.replace("\n", "").replace("\r", "") for i in open(l)] self.fgcode = gcoder.GCode(open(filename))
self.filename = l self.filename = filename
self.log("Loaded ", l, ", ", len(self.f)," lines.") self.log("Loaded %s, %d lines." % (filename, len(self.fgcode)))
def complete_load(self, text, line, begidx, endidx): def complete_load(self, text, line, begidx, endidx):
s = line.split() s = line.split()
...@@ -700,57 +773,45 @@ class pronsole(cmd.Cmd): ...@@ -700,57 +773,45 @@ class pronsole(cmd.Cmd):
self.log("Loads a gcode file (with tab-completion)") self.log("Loads a gcode file (with tab-completion)")
def do_upload(self, l): def do_upload(self, l):
if len(l) == 0: names = l.split()
self.log("No file name given.") if len(names) == 2:
return filename = names[0]
self.log("Loading file:"+l.split()[0]) targetname = names[1]
if not(os.path.exists(l.split()[0])): else:
self.log("File not found!") self.log(_("Please enter target name in 8.3 format."))
return return
if not self.p.online: if not self.p.online:
self.log("Not connected to printer.") self.log(_("Not connected to printer."))
return
self.f = [i.replace("\n", "") for i in open(l.split()[0])]
self.filename = l.split()[0]
self.log("Loaded ", l, ", ", len(self.f)," lines.")
tname = ""
if len(l.split())>1:
tname = l.split()[1]
else:
self.log("please enter target name in 8.3 format.")
return return
self.log("Uploading as ", tname) self._do_load(filename)
self.log(("Uploading "+self.filename)) self.log(_("Uploading as %s") % targetname)
self.p.send_now("M28 "+tname) self.log(_("Uploading %s") % self.filename)
self.log(("Press Ctrl-C to interrupt upload.")) self.p.send_now("M28 " + targetname)
self.p.startprint(self.f) self.log(_("Press Ctrl-C to interrupt upload."))
self.p.startprint(self.fgcode)
try: try:
sys.stdout.write("Progress: 00.0%") sys.stdout.write(_("Progress: ") + "00.0%")
sys.stdout.flush() sys.stdout.flush()
time.sleep(1) time.sleep(1)
while self.p.printing: while self.p.printing:
time.sleep(1) time.sleep(1)
sys.stdout.write("\b\b\b\b\b%04.1f%%" % (100*float(self.p.queueindex)/len(self.p.mainqueue),) ) sys.stdout.write("\b\b\b\b\b%04.1f%%" % (100*float(self.p.queueindex)/len(self.p.mainqueue),))
sys.stdout.flush() sys.stdout.flush()
self.p.send_now("M29 "+tname) self.p.send_now("M29 "+tname)
self.sleep(0.2) self.sleep(0.2)
self.p.clear = 1 self.p.clear = 1
self.listing = 0 self._do_ls(False)
self.sdfiles = [] self.log("\b\b\b\b\b100%.")
self.recvlisteners+=[self.listfiles] self.log(_("Upload completed. %s should now be on the card.") % targetname)
self.p.send_now("M20")
time.sleep(0.5)
self.log("\b\b\b\b\b100%. Upload completed. ", tname, " should now be on the card.")
return return
except: except:
self.log("...interrupted!") self.log(_("...interrupted!"))
self.p.pause() self.p.pause()
self.p.send_now("M29 "+tname) self.p.send_now("M29 "+targetname)
time.sleep(0.2) time.sleep(0.2)
self.p.clear = 1 self.p.clear = 1
self.p.startprint([]) self.p.startprint(None)
self.log("A partial file named ", tname, " may have been written to the sd card.") self.log(_("A partial file named %s may have been written to the sd card.") % targetname)
def complete_upload(self, text, line, begidx, endidx): def complete_upload(self, text, line, begidx, endidx):
s = line.split() s = line.split()
...@@ -766,44 +827,38 @@ class pronsole(cmd.Cmd): ...@@ -766,44 +827,38 @@ class pronsole(cmd.Cmd):
self.log("Uploads a gcode file to the sd card") self.log("Uploads a gcode file to the sd card")
def help_print(self): def help_print(self):
if self.f is None: if not self.fgcode:
self.log("Send a loaded gcode file to the printer. Load a file with the load command first.") self.log(_("Send a loaded gcode file to the printer. Load a file with the load command first."))
else: else:
self.log("Send a loaded gcode file to the printer. You have "+self.filename+" loaded right now.") self.log(_("Send a loaded gcode file to the printer. You have %s loaded right now.") % self.filename)
def do_print(self, l): def do_print(self, l):
if self.f is None: if not self.fgcode:
self.log("No file loaded. Please use load first.") self.log(_("No file loaded. Please use load first."))
return return
if not self.p.online: if not self.p.online:
self.log("Not connected to printer.") self.log(_("Not connected to printer."))
return return
self.log(("printing "+self.filename)) self.log(_("Printing %s") % self.filename)
self.log(("You can monitor the print with the monitor command.")) self.log(_("You can monitor the print with the monitor command."))
self.p.startprint(self.f) self.p.startprint(self.fgcode)
#self.p.pause()
#self.paused = True
#self.do_resume(None)
def do_pause(self, l): def do_pause(self, l):
if self.sdprinting: if self.sdprinting:
self.p.send_now("M25") self.p.send_now("M25")
else: else:
if(not self.p.printing): if not self.p.printing:
self.log("Not self.log(ing, cannot pause.") self.log(_("Not printing, cannot pause."))
return return
self.p.pause() self.p.pause()
#self.p.connect()# This seems to work, but is not a good solution.
self.paused = True self.paused = True
#self.do_resume(None)
def help_pause(self): def help_pause(self):
self.log("Pauses a running print") self.log(_("Pauses a running print"))
def do_resume(self, l): def do_resume(self, l):
if not self.paused: if not self.paused:
self.log("Not paused, unable to resume. Start a print first.") self.log(_("Not paused, unable to resume. Start a print first."))
return return
self.paused = False self.paused = False
if self.sdprinting: if self.sdprinting:
...@@ -813,7 +868,7 @@ class pronsole(cmd.Cmd): ...@@ -813,7 +868,7 @@ class pronsole(cmd.Cmd):
self.p.resume() self.p.resume()
def help_resume(self): def help_resume(self):
self.log("Resumes a paused print.") self.log(_("Resumes a paused print."))
def emptyline(self): def emptyline(self):
pass pass
...@@ -821,38 +876,43 @@ class pronsole(cmd.Cmd): ...@@ -821,38 +876,43 @@ class pronsole(cmd.Cmd):
def do_shell(self, l): def do_shell(self, l):
exec(l) exec(l)
def listfiles(self, line): def listfiles(self, line, echo = False):
if "Begin file list" in line: if "Begin file list" in line:
self.listing = 1 self.listing = 1
elif "End file list" in line: elif "End file list" in line:
self.listing = 0 self.listing = 0
self.recvlisteners.remove(self.listfiles) self.recvlisteners.remove(self.listfiles)
if echo:
self.log(_("Files on SD card:"))
self.log("\n".join(self.sdfiles))
elif self.listing: elif self.listing:
self.sdfiles+=[line.replace("\n", "").replace("\r", "").lower()] self.sdfiles.append(line.strip().lower())
def _do_ls(self, echo):
# FIXME: this was 2, but I think it should rather be 0 as in do_upload
self.listing = 0
self.sdfiles = []
self.recvlisteners.append(lambda l: self.listfiles(l, echo))
self.p.send_now("M20")
def do_ls(self, l): def do_ls(self, l):
if not self.p.online: if not self.p.online:
self.log("printer is not online. Try connect to it first.") self.log(_("Printer is not online. Please connect to it first."))
return return
self.listing = 2 self._do_ls(True)
self.sdfiles = []
self.recvlisteners+=[self.listfiles]
self.p.send_now("M20")
time.sleep(0.5)
self.log(" ".join(self.sdfiles))
def help_ls(self): def help_ls(self):
self.log("lists files on the SD card") self.log(_("Lists files on the SD card"))
def waitforsdresponse(self, l): def waitforsdresponse(self, l):
if "file.open failed" in l: if "file.open failed" in l:
self.log("Opening file failed.") self.log(_("Opening file failed."))
self.recvlisteners.remove(self.waitforsdresponse) self.recvlisteners.remove(self.waitforsdresponse)
return return
if "File opened" in l: if "File opened" in l:
self.log(l) self.log(l)
if "File selected" in l: if "File selected" in l:
self.log("Starting print") self.log(_("Starting print"))
self.p.send_now("M24") self.p.send_now("M24")
self.sdprinting = 1 self.sdprinting = 1
#self.recvlisteners.remove(self.waitforsdresponse) #self.recvlisteners.remove(self.waitforsdresponse)
...@@ -875,36 +935,33 @@ class pronsole(cmd.Cmd): ...@@ -875,36 +935,33 @@ class pronsole(cmd.Cmd):
self.p.reset() self.p.reset()
def help_reset(self): def help_reset(self):
self.log("Resets the printer.") self.log(_("Resets the printer."))
def do_sdprint(self, l): def do_sdprint(self, l):
if not self.p.online: if not self.p.online:
self.log("printer is not online. Try connect to it first.") self.log(_("Printer is not online. Please connect to it first."))
return return
self.listing = 2 self._do_ls(False)
self.sdfiles = [] while self.listfiles in self.recvlisteners:
self.recvlisteners+=[self.listfiles] time.sleep(0.1)
self.p.send_now("M20") if l.lower() not in self.sdfiles:
time.sleep(0.5) self.log(_("File is not present on card. Please upload it first."))
if not (l.lower() in self.sdfiles):
self.log("File is not present on card. Upload it first")
return return
self.recvlisteners+=[self.waitforsdresponse] self.recvlisteners.append(self.waitforsdresponse)
self.p.send_now("M23 "+l.lower()) self.p.send_now("M23 " + l.lower())
self.log("printing file: "+l.lower()+" from SD card.") self.log(_("Printing file: %s from SD card.") % l.lower())
self.log("Requesting SD print...") self.log(_("Requesting SD print..."))
time.sleep(1) time.sleep(1)
def help_sdprint(self): def help_sdprint(self):
self.log("print a file from the SD card. Tabcompletes with available file names.") self.log(_("Print a file from the SD card. Tab completes with available file names."))
self.log("sdprint filename.g") self.log(_("sdprint filename.g"))
def complete_sdprint(self, text, line, begidx, endidx): def complete_sdprint(self, text, line, begidx, endidx):
if self.sdfiles==[] and self.p.online: if not self.sdfiles and self.p.online:
self.listing = 2 self._do_ls(False)
self.recvlisteners+=[self.listfiles] while self.listfiles in self.recvlisteners:
self.p.send_now("M20") time.sleep(0.1)
time.sleep(0.5)
if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1]==" "): if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1]==" "):
return [i for i in self.sdfiles if i.startswith(text)] return [i for i in self.sdfiles if i.startswith(text)]
...@@ -913,7 +970,7 @@ class pronsole(cmd.Cmd): ...@@ -913,7 +970,7 @@ class pronsole(cmd.Cmd):
self.tempreadings = l self.tempreadings = l
self.status.update_tempreading(l) self.status.update_tempreading(l)
tstring = l.rstrip() tstring = l.rstrip()
if(tstring!="ok" and not tstring.startswith("ok T") and not tstring.startswith("T:") and not self.listing and not self.monitoring): if tstring != "ok" and not self.listing and not self.monitoring:
if tstring[:5] == "echo:": if tstring[:5] == "echo:":
tstring = tstring[5:].lstrip() tstring = tstring[5:].lstrip()
if self.silent == False: print "\r" + tstring.ljust(15) if self.silent == False: print "\r" + tstring.ljust(15)
...@@ -927,19 +984,27 @@ class pronsole(cmd.Cmd): ...@@ -927,19 +984,27 @@ class pronsole(cmd.Cmd):
self.log("! os.listdir('.')") self.log("! os.listdir('.')")
def default(self, l): def default(self, l):
if(l[0] in self.commandprefixes.upper()): if l[0] in self.commandprefixes.upper():
if(self.p and self.p.online): if self.p and self.p.online:
if(not self.p.loud): if not self.p.loud:
self.log("SENDING:"+l) self.log("SENDING:"+l)
self.p.send_now(l) self.p.send_now(l)
else: else:
self.log("printer is not online.") self.log(_("Printer is not online."))
return return
elif(l[0] in self.commandprefixes.lower()): elif l[0] in self.commandprefixes.lower():
if(self.p and self.p.online): if self.p and self.p.online:
if(not self.p.loud): if not self.p.loud:
self.log("SENDING:"+l.upper()) self.log("SENDING:"+l.upper())
self.p.send_now(l.upper()) self.p.send_now(l.upper())
else:
self.log(_("Printer is not online."))
return
elif l[0] == "@":
if self.p and self.p.online:
if not self.p.loud:
self.log("SENDING:"+l[1:])
self.p.send_now(l[1:])
else: else:
self.log("printer is not online.") self.log("printer is not online.")
return return
...@@ -951,7 +1016,7 @@ class pronsole(cmd.Cmd): ...@@ -951,7 +1016,7 @@ class pronsole(cmd.Cmd):
def tempcb(self, l): def tempcb(self, l):
if "T:" in l: if "T:" in l:
self.log(l.replace("\r", "").replace("T", "Hotend").replace("B", "Bed").replace("\n", "").replace("ok ", "")) self.log(l.strip().replace("T", "Hotend").replace("B", "Bed").replace("ok ", ""))
def do_gettemp(self, l): def do_gettemp(self, l):
if "dynamic" in l: if "dynamic" in l:
...@@ -966,7 +1031,7 @@ class pronsole(cmd.Cmd): ...@@ -966,7 +1031,7 @@ class pronsole(cmd.Cmd):
print "Bed: %s/%s" % (self.status.bed_temp, self.status.bed_temp_target) print "Bed: %s/%s" % (self.status.bed_temp, self.status.bed_temp_target)
def help_gettemp(self): def help_gettemp(self):
self.log("Read the extruder and bed temperature.") self.log(_("Read the extruder and bed temperature."))
def do_settemp(self, l): def do_settemp(self, l):
try: try:
...@@ -976,22 +1041,22 @@ class pronsole(cmd.Cmd): ...@@ -976,22 +1041,22 @@ class pronsole(cmd.Cmd):
f = float(l) f = float(l)
if f>=0: if f>=0:
if f > 250: if f > 250:
print f, " is a high temperature to set your extruder to. Are you sure you want to do that?" print _("%s is a high temperature to set your extruder to. Are you sure you want to do that?") % f
if not confirm(): if not confirm():
return return
if self.p.online: if self.p.online:
self.p.send_now("M104 S"+l) self.p.send_now("M104 S"+l)
self.log("Setting hotend temperature to ", f, " degrees Celsius.") self.log(_("Setting hotend temperature to %s degrees Celsius.") % f)
else: else:
self.log("printer is not online.") self.log(_("Printer is not online."))
else: else:
self.log("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0.") self.log(_("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0."))
except: except:
self.log("You must enter a temperature.") self.log(_("You must enter a temperature."))
def help_settemp(self): def help_settemp(self):
self.log("Sets the hotend temperature to the value entered.") 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(_("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([i+"("+self.temps[i]+")" for i in self.temps.keys()]))
def complete_settemp(self, text, line, begidx, endidx): def complete_settemp(self, text, line, begidx, endidx):
...@@ -1007,17 +1072,17 @@ class pronsole(cmd.Cmd): ...@@ -1007,17 +1072,17 @@ class pronsole(cmd.Cmd):
if f>=0: if f>=0:
if self.p.online: if self.p.online:
self.p.send_now("M140 S"+l) self.p.send_now("M140 S"+l)
self.log("Setting bed temperature to ", f, " degrees Celsius.") self.log(_("Setting bed temperature to %s degrees Celsius.") % f)
else: else:
self.log("printer is not online.") self.log(_("Printer is not online."))
else: else:
self.log("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0.") self.log(_("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0."))
except: except:
self.log("You must enter a temperature.") self.log(_("You must enter a temperature."))
def help_bedtemp(self): def help_bedtemp(self):
self.log("Sets the bed temperature to the value entered.") self.log(_("Sets the bed temperature to the value entered."))
self.log("Enter either a temperature in celsius or one of the following keywords") self.log(_("Enter either a temperature in celsius or one of the following keywords"))
self.log(", ".join([i+"("+self.bedtemps[i]+")" for i in self.bedtemps.keys()])) self.log(", ".join([i+"("+self.bedtemps[i]+")" for i in self.bedtemps.keys()]))
def complete_bedtemp(self, text, line, begidx, endidx): def complete_bedtemp(self, text, line, begidx, endidx):
...@@ -1026,13 +1091,13 @@ class pronsole(cmd.Cmd): ...@@ -1026,13 +1091,13 @@ class pronsole(cmd.Cmd):
def do_move(self, l): def do_move(self, l):
if(len(l.split())<2): if(len(l.split())<2):
self.log("No move specified.") self.log(_("No move specified."))
return return
if self.p.printing: if self.p.printing:
self.log("printer is currently printing. Please pause the print before you issue manual commands.") self.log(_("Printer is currently printing. Please pause the print before you issue manual commands."))
return return
if not self.p.online: if not self.p.online:
self.log("printer is not online. Unable to move.") self.log(_("Printer is not online. Unable to move."))
return return
l = l.split() l = l.split()
if(l[0].lower()=="x"): if(l[0].lower()=="x"):
...@@ -1048,13 +1113,13 @@ class pronsole(cmd.Cmd): ...@@ -1048,13 +1113,13 @@ class pronsole(cmd.Cmd):
feed = self.settings.e_feedrate feed = self.settings.e_feedrate
axis = "E" axis = "E"
else: else:
self.log("Unknown axis.") self.log(_("Unknown axis."))
return return
dist = 0 dist = 0
try: try:
dist = float(l[1]) dist = float(l[1])
except: except:
self.log("Invalid distance") self.log(_("Invalid distance"))
return return
try: try:
feed = int(l[2]) feed = int(l[2])
...@@ -1065,11 +1130,11 @@ class pronsole(cmd.Cmd): ...@@ -1065,11 +1130,11 @@ class pronsole(cmd.Cmd):
self.p.send_now("G90") self.p.send_now("G90")
def help_move(self): def help_move(self):
self.log("Move an axis. Specify the name of the axis and the amount. ") self.log(_("Move an axis. Specify the name of the axis and the amount. "))
self.log("move X 10 will move the X axis forward by 10mm at ", self.settings.xy_feedrate, "mm/min (default XY speed)") self.log(_("move X 10 will move the X axis forward by 10mm at %s mm/min (default XY speed)") % self.settings.xy_feedrate)
self.log("move Y 10 5000 will move the Y axis forward by 10mm at 5000mm/min") self.log(_("move Y 10 5000 will move the Y axis forward by 10mm at 5000mm/min"))
self.log("move Z -1 will move the Z axis down by 1mm at ", self.settings.z_feedrate, "mm/min (default Z speed)") self.log(_("move Z -1 will move the Z axis down by 1mm at %s mm/min (default Z speed)") % self.settings.z_feedrate)
self.log("Common amounts are in the tabcomplete list.") self.log(_("Common amounts are in the tabcomplete list."))
def complete_move(self, text, line, begidx, endidx): def complete_move(self, text, line, begidx, endidx):
if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1]==" "): if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1]==" "):
...@@ -1089,70 +1154,70 @@ class pronsole(cmd.Cmd): ...@@ -1089,70 +1154,70 @@ class pronsole(cmd.Cmd):
length = 5#default extrusion length length = 5#default extrusion length
feed = self.settings.e_feedrate#default speed feed = self.settings.e_feedrate#default speed
if not self.p.online: if not self.p.online:
self.log("printer is not online. Unable to move.") self.log("Printer is not online. Unable to extrude.")
return return
if self.p.printing: if self.p.printing:
self.log("printer is currently printing. Please pause the print before you issue manual commands.") self.log("Printer is currently printing. Please pause the print before you issue manual commands.")
return return
ls = l.split() ls = l.split()
if len(ls): if len(ls):
try: try:
length = float(ls[0]) length = float(ls[0])
except: except:
self.log("Invalid length given.") self.log(_("Invalid length given."))
if len(ls)>1: if len(ls)>1:
try: try:
feed = int(ls[1]) feed = int(ls[1])
except: except:
self.log("Invalid speed given.") self.log(_("Invalid speed given."))
if override is not None: if override is not None:
length = override length = override
feed = overridefeed feed = overridefeed
if length > 0: if length > 0:
self.log("Extruding %fmm of filament."%(length,)) self.log(_("Extruding %fmm of filament.") % (length,))
elif length <0: elif length < 0:
self.log("Reversing %fmm of filament."%(-1*length,)) self.log(_("Reversing %fmm of filament.") % (-1*length,))
else: else:
"Length is 0, not doing anything." self.log(_("Length is 0, not doing anything."))
self.p.send_now("G91") self.p.send_now("G91")
self.p.send_now("G1 E"+str(length)+" F"+str(feed)) self.p.send_now("G1 E"+str(length)+" F"+str(feed))
self.p.send_now("G90") self.p.send_now("G90")
def help_extrude(self): 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(_("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)") self.log(_("extrude - extrudes 5mm of filament at 300mm/min (5mm/s)"))
self.log("extrude 20 - extrudes 20mm of filament at 300mm/min (5mm/s)") self.log(_("extrude 20 - extrudes 20mm of filament at 300mm/min (5mm/s)"))
self.log("extrude -5 - REVERSES 5mm of filament at 300mm/min (5mm/s)") self.log(_("extrude -5 - REVERSES 5mm of filament at 300mm/min (5mm/s)"))
self.log("extrude 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)") self.log(_("extrude 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)"))
def do_reverse(self, l): def do_reverse(self, l):
length = 5#default extrusion length length = 5#default extrusion length
feed = self.settings.e_feedrate#default speed feed = self.settings.e_feedrate#default speed
if not self.p.online: if not self.p.online:
self.log("printer is not online. Unable to move.") self.log(_("Printer is not online. Unable to reverse."))
return return
if self.p.printing: if self.p.printing:
self.log("printer is currently printing. Please pause the print before you issue manual commands.") self.log(_("Printer is currently printing. Please pause the print before you issue manual commands."))
return return
ls = l.split() ls = l.split()
if len(ls): if len(ls):
try: try:
length = float(ls[0]) length = float(ls[0])
except: except:
self.log("Invalid length given.") self.log(_("Invalid length given."))
if len(ls)>1: if len(ls)>1:
try: try:
feed = int(ls[1]) feed = int(ls[1])
except: except:
self.log("Invalid speed given.") self.log(_("Invalid speed given."))
self.do_extrude("", length*-1.0, feed) self.do_extrude("", length*-1.0, feed)
def help_reverse(self): def help_reverse(self):
self.log("Reverses the extruder, 5mm by default, or the number of mm given as a parameter") self.log(_("Reverses the extruder, 5mm by default, or the number of mm given as a parameter"))
self.log("reverse - reverses 5mm of filament at 300mm/min (5mm/s)") self.log(_("reverse - reverses 5mm of filament at 300mm/min (5mm/s)"))
self.log("reverse 20 - reverses 20mm of filament at 300mm/min (5mm/s)") self.log(_("reverse 20 - reverses 20mm of filament at 300mm/min (5mm/s)"))
self.log("reverse 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)") self.log(_("reverse 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)"))
self.log("reverse -5 - EXTRUDES 5mm of filament at 300mm/min (5mm/s)") self.log(_("reverse -5 - EXTRUDES 5mm of filament at 300mm/min (5mm/s)"))
def do_exit(self, l): def do_exit(self, l):
if self.status.extruder_temp_target != 0: if self.status.extruder_temp_target != 0:
...@@ -1163,34 +1228,33 @@ class pronsole(cmd.Cmd): ...@@ -1163,34 +1228,33 @@ class pronsole(cmd.Cmd):
print "Setting bed temp to 0" print "Setting bed temp to 0"
self.p.send_now("M140 S0.0") self.p.send_now("M140 S0.0")
self.log("Disconnecting from printer...") self.log("Disconnecting from printer...")
print self.p.printing
if self.p.printing: if self.p.printing:
print "Are you sure you want to exit while printing?" print "Are you sure you want to exit while printing?"
print "(this will terminate the print)." print "(this will terminate the print)."
if not confirm(): if not confirm():
return False return False
self.log("Exiting program. Goodbye!") self.log(_("Exiting program. Goodbye!"))
self.p.disconnect() self.p.disconnect()
return True return True
def help_exit(self): def help_exit(self):
self.log("Disconnects from the printer and exits the program.") self.log(_("Disconnects from the printer and exits the program."))
def do_monitor(self, l): def do_monitor(self, l):
interval = 5 interval = 5
if not self.p.online: if not self.p.online:
self.log("printer is not online. Please connect first.") self.log(_("Printer is not online. Please connect to it first."))
return return
if not (self.p.printing or self.sdprinting): if not (self.p.printing or self.sdprinting):
self.log("Printer not printing. Please print something before monitoring.") self.log(_("Printer is not printing. Please print something before monitoring."))
return return
self.log("Monitoring printer, use ^C to interrupt.") self.log(_("Monitoring printer, use ^C to interrupt."))
if len(l): if len(l):
try: try:
interval = float(l) interval = float(l)
except: except:
self.log("Invalid period given.") self.log(_("Invalid period given."))
self.log("Updating values every %f seconds."%(interval,)) self.log(_("Updating values every %f seconds.") % (interval,))
self.monitoring = 1 self.monitoring = 1
prev_msg_len = 0 prev_msg_len = 0
try: try:
...@@ -1201,10 +1265,10 @@ class pronsole(cmd.Cmd): ...@@ -1201,10 +1265,10 @@ class pronsole(cmd.Cmd):
time.sleep(interval) time.sleep(interval)
#print (self.tempreadings.replace("\r", "").replace("T", "Hotend").replace("B", "Bed").replace("\n", "").replace("ok ", "")) #print (self.tempreadings.replace("\r", "").replace("T", "Hotend").replace("B", "Bed").replace("\n", "").replace("ok ", ""))
if self.p.printing: if self.p.printing:
preface = "Print progress: " preface = _("Print progress: ")
progress = 100*float(self.p.queueindex)/len(self.p.mainqueue) progress = 100*float(self.p.queueindex)/len(self.p.mainqueue)
elif self.sdprinting: elif self.sdprinting:
preface = "Print progress: " preface = _("Print progress: ")
progress = self.percentdone progress = self.percentdone
progress = int(progress*10)/10.0 #limit precision progress = int(progress*10)/10.0 #limit precision
prev_msg = preface + str(progress) + "%" prev_msg = preface + str(progress) + "%"
...@@ -1213,13 +1277,13 @@ class pronsole(cmd.Cmd): ...@@ -1213,13 +1277,13 @@ class pronsole(cmd.Cmd):
sys.stdout.flush() sys.stdout.flush()
prev_msg_len = len(prev_msg) prev_msg_len = len(prev_msg)
except KeyboardInterrupt: except KeyboardInterrupt:
if self.silent == False: print "Done monitoring." if self.silent == False: print _("Done monitoring.")
self.monitoring = 0 self.monitoring = 0
def help_monitor(self): def help_monitor(self):
self.log("Monitor a machine's temperatures and an SD print's status.") self.log(_("Monitor a machine's temperatures and an SD print's status."))
self.log("monitor - Reports temperature and SD print status (if SD printing) every 5 seconds") self.log(_("monitor - Reports temperature and SD print status (if SD printing) every 5 seconds"))
self.log("monitor 2 - Reports temperature and SD print status (if SD printing) every 2 seconds") self.log(_("monitor 2 - Reports temperature and SD print status (if SD printing) every 2 seconds"))
def expandcommand(self, c): def expandcommand(self, c):
return c.replace("$python", sys.executable) return c.replace("$python", sys.executable)
...@@ -1227,31 +1291,30 @@ class pronsole(cmd.Cmd): ...@@ -1227,31 +1291,30 @@ class pronsole(cmd.Cmd):
def do_skein(self, l): def do_skein(self, l):
l = l.split() l = l.split()
if len(l) == 0: if len(l) == 0:
self.log("No file name given.") self.log(_("No file name given."))
return return
settings = 0 settings = 0
if(l[0]=="set"): if(l[0]=="set"):
settings = 1 settings = 1
else: else:
self.log("Skeining file:"+l[0]) self.log(_("Skeining file: %s") % l[0])
if not(os.path.exists(l[0])): if not(os.path.exists(l[0])):
self.log("File not found!") self.log(_("File not found!"))
return return
try: try:
import shlex if settings:
if(settings):
param = self.expandcommand(self.settings.sliceoptscommand).replace("\\", "\\\\").encode() param = self.expandcommand(self.settings.sliceoptscommand).replace("\\", "\\\\").encode()
self.log("Entering slicer settings: ", param) self.log(_("Entering slicer settings: %s") % param)
subprocess.call(shlex.split(param)) subprocess.call(shlex.split(param))
else: else:
param = self.expandcommand(self.settings.slicecommand).encode() param = self.expandcommand(self.settings.slicecommand).encode()
self.log("Slicing: ", param) self.log(_("Slicing: ") % param)
params = [i.replace("$s", l[0]).replace("$o", l[0].replace(".stl", "_export.gcode").replace(".STL", "_export.gcode")).encode() for i in shlex.split(param.replace("\\", "\\\\").encode())] params = [i.replace("$s", l[0]).replace("$o", l[0].replace(".stl", "_export.gcode").replace(".STL", "_export.gcode")).encode() for i in shlex.split(param.replace("\\", "\\\\").encode())]
subprocess.call(params) subprocess.call(params)
self.log("Loading sliced file.") self.log(_("Loading sliced file."))
self.do_load(l[0].replace(".stl", "_export.gcode")) self.do_load(l[0].replace(".stl", "_export.gcode"))
except Exception, e: except Exception, e:
self.log("Skeinforge execution failed: ", e) self.log(_("Skeinforge execution failed: %s") % e)
def complete_skein(self, text, line, begidx, endidx): def complete_skein(self, text, line, begidx, endidx):
s = line.split() s = line.split()
...@@ -1264,18 +1327,17 @@ class pronsole(cmd.Cmd): ...@@ -1264,18 +1327,17 @@ class pronsole(cmd.Cmd):
return glob.glob("*/")+glob.glob("*.stl") return glob.glob("*/")+glob.glob("*.stl")
def help_skein(self): def help_skein(self):
self.log("Creates a gcode file from an stl model using the slicer (with tab-completion)") self.log(_("Creates a gcode file from an stl model using the slicer (with tab-completion)"))
self.log("skein filename.stl - create gcode file") self.log(_("skein filename.stl - create gcode file"))
self.log("skein filename.stl view - create gcode file and view using skeiniso") self.log(_("skein filename.stl view - create gcode file and view using skeiniso"))
self.log("skein set - adjust slicer settings") self.log(_("skein set - adjust slicer settings"))
def do_home(self, l): def do_home(self, l):
if not self.p.online: if not self.p.online:
self.log("printer is not online. Unable to move.") self.log(_("Printer is not online. Unable to move."))
return return
if self.p.printing: if self.p.printing:
self.log("printer is currently printing. Please pause the print before you issue manual commands.") self.log(_("Printer is currently printing. Please pause the print before you issue manual commands."))
return return
if "x" in l.lower(): if "x" in l.lower():
self.p.send_now("G28 X0") self.p.send_now("G28 X0")
...@@ -1290,35 +1352,36 @@ class pronsole(cmd.Cmd): ...@@ -1290,35 +1352,36 @@ class pronsole(cmd.Cmd):
self.p.send_now("G92 E0") self.p.send_now("G92 E0")
def help_home(self): def help_home(self):
self.log("Homes the printer") self.log(_("Homes the printer"))
self.log("home - homes all axes and zeroes the extruder(Using G28 and G92)") self.log(_("home - homes all axes and zeroes the extruder(Using G28 and G92)"))
self.log("home xy - homes x and y axes (Using G28)") self.log(_("home xy - homes x and y axes (Using G28)"))
self.log("home z - homes z axis only (Using G28)") self.log(_("home z - homes z axis only (Using G28)"))
self.log("home e - set extruder position to zero (Using G92)") self.log(_("home e - set extruder position to zero (Using G92)"))
self.log("home xyze - homes all axes and zeroes the extruder (Using G28 and G92)") self.log(_("home xyze - homes all axes and zeroes the extruder (Using G28 and G92)"))
def parse_cmdline(self, args): def add_cmdline_arguments(self, parser):
import getopt parser.add_argument('-c','--conf','--config', help = _("load this file on startup instead of .pronsolerc ; you may chain config files, if so settings auto-save will use the last specified file"), action = "append", default = [])
opts, args = getopt.getopt(args, "c:e:hw", ["conf = ", "config = ", "help"]) parser.add_argument('-e','--execute', help = _("executes command after configuration/.pronsolerc is loaded ; macros/settings from these commands are not autosaved"), action = "append", default = [])
for o, a in opts: parser.add_argument('filename', nargs='?', help = _("file to load"))
#self.log(repr((o, a)))
if o in ("-c", "--conf", "--config"): def process_cmdline_arguments(self, args):
self.load_rc(a) for config in args.conf:
elif o in ("-h", "--help"): self.load_rc(config)
self.log("Usage: "+sys.argv[0]+' [-c filename [-c filename2 ... ] ] [-e "command" ...]')
self.log(" -c | --conf | --config - override startup .pronsolerc file")
self.log(" may chain config files, settings auto-save will go into last file in the chain")
self.log(' -e <command> - executes command after configuration/.pronsolerc is loaded')
self.log(" macros/settings from these commands are not autosaved")
sys.exit()
if not self.rc_loaded: if not self.rc_loaded:
self.load_default_rc() self.load_default_rc()
for o, a in opts:
if o == "-e":
self.processing_args = True self.processing_args = True
self.onecmd(a) for command in args.execute:
self.onecmd(command)
self.processing_args = False self.processing_args = False
if args.filename:
self.do_load(args.filename)
def parse_cmdline(self, args):
parser = argparse.ArgumentParser(description = 'Printrun 3D printer interface')
self.add_cmdline_arguments(parser)
args = [arg for arg in args if not arg.startswith("-psn")]
args = parser.parse_args(args = args)
self.process_cmdline_arguments(args)
# We replace this function, defined in cmd.py . # We replace this function, defined in cmd.py .
# It's default behavior with reagrds to Ctr-C # It's default behavior with reagrds to Ctr-C
...@@ -1388,6 +1451,9 @@ if __name__ == "__main__": ...@@ -1388,6 +1451,9 @@ if __name__ == "__main__":
interp.parse_cmdline(sys.argv[1:]) interp.parse_cmdline(sys.argv[1:])
try: try:
interp.cmdloop() interp.cmdloop()
except SystemExit:
interp.p.disconnect()
except: except:
print _("Caught an exception, exiting:")
traceback.print_exc()
interp.p.disconnect() interp.p.disconnect()
#raise
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
import os, Queue, re import os, Queue, re
from printrun.printrun_utils import install_locale from printrun.printrun_utils import install_locale, RemainingTimeEstimator
install_locale('pronterface') install_locale('pronterface')
try: try:
...@@ -26,6 +26,7 @@ except: ...@@ -26,6 +26,7 @@ except:
print _("WX is not installed. This program requires WX to run.") print _("WX is not installed. This program requires WX to run.")
raise raise
import sys, glob, time, datetime, threading, traceback, cStringIO, subprocess import sys, glob, time, datetime, threading, traceback, cStringIO, subprocess
import shlex
from printrun.pronterface_widgets import * from printrun.pronterface_widgets import *
from serial import SerialException from serial import SerialException
...@@ -45,15 +46,14 @@ import printcore ...@@ -45,15 +46,14 @@ import printcore
from printrun.printrun_utils import pixmapfile, configfile from printrun.printrun_utils import pixmapfile, configfile
from printrun.gui import MainWindow from printrun.gui import MainWindow
import pronsole import pronsole
from pronsole import dosify, wxSetting, HiddenSetting, StringSetting, SpinSetting, FloatSpinSetting, BooleanSetting
from printrun import gcoder
def dosify(name): tempreport_exp = re.compile("([TB]\d*):([-+]?\d*\.?\d*)(?: \/)?([-+]?\d*\.?\d*)")
return os.path.split(name)[1].split(".")[0][:8]+".g"
def parse_temperature_report(report, key): def parse_temperature_report(report):
if key in report: matches = tempreport_exp.findall(report)
return float(filter(lambda x: x.startswith(key), report.split())[0].split(":")[1].split("/")[0]) return dict((m[0], (m[1], m[2])) for m in matches)
else:
return -1.0
def format_time(timestamp): def format_time(timestamp):
return datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") return datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M:%S")
...@@ -81,33 +81,128 @@ class Tee(object): ...@@ -81,33 +81,128 @@ class Tee(object):
def flush(self): def flush(self):
self.stdout.flush() self.stdout.flush()
def parse_build_dimensions(bdim):
# a string containing up to six numbers delimited by almost anything
# first 0-3 numbers specify the build volume, no sign, always positive
# remaining 0-3 numbers specify the coordinates of the "southwest" corner of the build platform
# "XXX,YYY"
# "XXXxYYY+xxx-yyy"
# "XXX,YYY,ZZZ+xxx+yyy-zzz"
# etc
bdl = re.findall("([-+]?[0-9]*\.?[0-9]*)", bdim)
defaults = [200, 200, 100, 0, 0, 0, 0, 0, 0]
bdl = filter(None, bdl)
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))]
for i in range(3): # Check for nonpositive dimensions for build volume
if bdl_float[i] <= 0: bdl_float[i] = 1
return bdl_float
class BuildDimensionsSetting(wxSetting):
widgets = None
def _set_value(self, value):
self._value = value
if self.widgets:
self._set_widgets_values(value)
value = property(wxSetting._get_value, _set_value)
def _set_widgets_values(self, value):
build_dimensions_list = parse_build_dimensions(value)
for i in range(len(self.widgets)):
self.widgets[i].SetValue(build_dimensions_list[i])
def get_widget(self, parent):
from wx.lib.agw.floatspin import FloatSpin
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)
self.widget = wx.GridBagSizer()
addlabel(_("Width"), (0, 0))
w(build_dimensions[0], 0, 2000)
addwidget(0, 1)
addlabel(_("Depth"), (0, 2))
w(build_dimensions[1], 0, 2000)
addwidget(0, 3)
addlabel(_("Height"), (0, 4))
w(build_dimensions[2], 0, 2000)
addwidget(0, 5)
addlabel(_("X offset"), (1, 0))
w(build_dimensions[3], -2000, 2000)
addwidget(1, 1)
addlabel(_("Y offset"), (1, 2))
w(build_dimensions[4], -2000, 2000)
addwidget(1, 3)
addlabel(_("Z offset"), (1, 4))
w(build_dimensions[5], -2000, 2000)
addwidget(1, 5)
addlabel(_("X home pos."), (2, 0))
w(build_dimensions[6], -2000, 2000)
self.widget.Add(self.widgets[-1], pos = (2, 1))
addlabel(_("Y home pos."), (2, 2))
w(build_dimensions[7], -2000, 2000)
self.widget.Add(self.widgets[-1], pos = (2, 3))
addlabel(_("Z home pos."), (2, 4))
w(build_dimensions[8], -2000, 2000)
self.widget.Add(self.widgets[-1], pos = (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 StringSetting(wxSetting):
def get_specific_widget(self, parent):
import wx
self.widget = wx.TextCtrl(parent, -1, str(self.value))
return self.widget
class ComboSetting(wxSetting):
def __init__(self, name, default, choices, label = None, help = None, group = None):
super(ComboSetting, self).__init__(name, default, label, help, group)
self.choices = choices
def get_specific_widget(self, parent):
import wx
self.widget = wx.ComboBox(parent, -1, str(self.value), choices = self.choices, style = wx.CB_DROPDOWN)
return self.widget
class PronterWindow(MainWindow, pronsole.pronsole): class PronterWindow(MainWindow, pronsole.pronsole):
def __init__(self, filename = None, size = winsize): def __init__(self, filename = None, size = winsize):
pronsole.pronsole.__init__(self) pronsole.pronsole.__init__(self)
self.settings.build_dimensions = '200x200x100+0+0+0+0+0+0' #default build dimensions are 200x200x100 with 0, 0, 0 in the corner of the bed #default build dimensions are 200x200x100 with 0, 0, 0 in the corner of the bed and endstops at 0, 0 and 0
self.settings.last_bed_temperature = 0.0 monitorsetting = BooleanSetting("monitor", False)
self.settings.last_file_path = "" monitorsetting.hidden = True
self.settings.last_temperature = 0.0 self.settings._add(monitorsetting)
self.settings.preview_extrusion_width = 0.5 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.settings.preview_grid_step1 = 10. self.settings._add(StringSetting("bgcolor", "#FFFFFF", _("Background color"), _("Pronterface background color (default: #FFFFFF)"), "UI"))
self.settings.preview_grid_step2 = 50. self.settings._add(BooleanSetting("tabbed", False, _("Use tabbed interface"), _("Use tabbed interface instead of the single window one"), "UI"))
self.settings.bgcolor = "#FFFFFF" self.settings._add(BooleanSetting("viz3d", False, _("Enable 3D viewer (requires restarting)"), _("Use 3D visualization instead of 2D layered visualization"), "UI"))
self.settings._add(ComboSetting("mainviz", "2D", ["2D", "3D", "None"], _("Main visualization"), _("Select visualization for main window."), "UI"))
self.settings._add(BooleanSetting("tempgauges", False, _("Display temperature gauges"), _("Display graphical gauges for temperatures visualization"), "UI"))
self.settings._add(HiddenSetting("last_bed_temperature", 0.0))
self.settings._add(HiddenSetting("last_file_path", ""))
self.settings._add(HiddenSetting("last_temperature", 0.0))
self.settings._add(FloatSpinSetting("preview_extrusion_width", 0.5, 0, 10, _("Preview extrusion width"), _("Width of Extrusion in Preview (default: 0.5)"), "UI"))
self.settings._add(SpinSetting("preview_grid_step1", 10., 0, 200, _("Fine grid spacing"), _("Fine Grid Spacing (default: 10)"), "UI"))
self.settings._add(SpinSetting("preview_grid_step2", 50., 0, 200, _("Coarse grid spacing"), _("Coarse Grid Spacing (default: 50)"), "UI"))
self.pauseScript = "pause.gcode" self.pauseScript = "pause.gcode"
self.endScript = "end.gcode" self.endScript = "end.gcode"
self.helpdict["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")
self.helpdict["last_bed_temperature"] = _("Last Set Temperature for the Heated Print Bed")
self.helpdict["last_file_path"] = _("Folder of last opened file")
self.helpdict["last_temperature"] = _("Last Temperature of the Hot End")
self.helpdict["preview_extrusion_width"] = _("Width of Extrusion in Preview (default: 0.5)")
self.helpdict["preview_grid_step1"] = _("Fine Grid Spacing (default: 10)")
self.helpdict["preview_grid_step2"] = _("Coarse Grid Spacing (default: 50)")
self.helpdict["bgcolor"] = _("Pronterface background color (default: #FFFFFF)")
self.filename = filename self.filename = filename
os.putenv("UBUNTU_MENUPROXY", "0") os.putenv("UBUNTU_MENUPROXY", "0")
MainWindow.__init__(self, None, title = _("Printer Interface"), size = size); MainWindow.__init__(self, None, title = _("Printer Interface"), size = size);
if hasattr(sys,"frozen") and sys.frozen=="windows_exe":
self.SetIcon(wx.Icon(sys.executable, wx.BITMAP_TYPE_ICO))
else:
self.SetIcon(wx.Icon(pixmapfile("P-face.ico"), wx.BITMAP_TYPE_ICO)) self.SetIcon(wx.Icon(pixmapfile("P-face.ico"), wx.BITMAP_TYPE_ICO))
self.panel = wx.Panel(self,-1, size = size) self.panel = wx.Panel(self,-1, size = size)
...@@ -117,11 +212,12 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -117,11 +212,12 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.capture_skip_newline = False self.capture_skip_newline = False
self.tempreport = "" self.tempreport = ""
self.monitor = 0 self.monitor = 0
self.f = None self.fgcode = None
self.skeinp = None self.skeinp = None
self.monitor_interval = 3 self.monitor_interval = 3
self.current_pos = [0, 0, 0]
self.paused = False self.paused = False
self.sentlines = Queue.Queue(30) self.sentlines = Queue.Queue(0)
self.cpbuttons = [ self.cpbuttons = [
SpecialButton(_("Motors off"), ("M84"), (250, 250, 250), None, 0, _("Switch all motors off")), SpecialButton(_("Motors off"), ("M84"), (250, 250, 250), None, 0, _("Switch all motors off")),
SpecialButton(_("Check temp"), ("M105"), (225, 200, 200), (2, 5), (1, 1), _("Check current hotend temperature")), SpecialButton(_("Check temp"), ("M105"), (225, 200, 200), (2, 5), (1, 1), _("Check current hotend temperature")),
...@@ -130,8 +226,10 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -130,8 +226,10 @@ class PronterWindow(MainWindow, pronsole.pronsole):
] ]
self.custombuttons = [] self.custombuttons = []
self.btndict = {} self.btndict = {}
self.autoconnect = False
self.parse_cmdline(sys.argv[1:]) self.parse_cmdline(sys.argv[1:])
self.build_dimensions_list = self.get_build_dimensions(self.settings.build_dimensions) self.build_dimensions_list = parse_build_dimensions(self.settings.build_dimensions)
self.display_gauges = self.settings.tempgauges
#initialize the code analyzer with the correct sizes. There must be a more general way to do so #initialize the code analyzer with the correct sizes. There must be a more general way to do so
...@@ -179,14 +277,20 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -179,14 +277,20 @@ class PronterWindow(MainWindow, pronsole.pronsole):
except: except:
pass pass
self.popmenu() self.popmenu()
if self.settings.tabbed:
self.createTabbedGui()
else:
self.createGui() self.createGui()
self.t = Tee(self.catchprint) self.t = Tee(self.catchprint)
self.stdout = sys.stdout self.stdout = sys.stdout
self.skeining = 0 self.skeining = 0
self.mini = False self.mini = False
self.p.sendcb = self.sentcb self.p.sendcb = self.sentcb
self.p.printsendcb = self.printsentcb
self.p.layerchangecb = self.layer_change_cb
self.p.startcb = self.startcb self.p.startcb = self.startcb
self.p.endcb = self.endcb self.p.endcb = self.endcb
self.compute_eta = None
self.starttime = 0 self.starttime = 0
self.extra_print_time = 0 self.extra_print_time = 0
self.curlayer = 0 self.curlayer = 0
...@@ -196,18 +300,31 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -196,18 +300,31 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.predisconnect_layer = None self.predisconnect_layer = None
self.hsetpoint = 0.0 self.hsetpoint = 0.0
self.bsetpoint = 0.0 self.bsetpoint = 0.0
if self.autoconnect:
self.connect()
if self.filename is not None: if self.filename is not None:
self.do_load(self.filename) self.do_load(self.filename)
if self.settings.monitor:
self.setmonitor(None)
def add_cmdline_arguments(self, parser):
pronsole.pronsole.add_cmdline_arguments(self, parser)
parser.add_argument('-a','--autoconnect', help = _("automatically try to connect to printer on startup"), action = "store_true")
def process_cmdline_arguments(self, args):
pronsole.pronsole.process_cmdline_arguments(self, args)
self.autoconnect = args.autoconnect
def startcb(self): def startcb(self):
self.starttime = time.time() self.starttime = time.time()
print "Print Started at: " + format_time(self.starttime) self.compute_eta = RemainingTimeEstimator(self.p.mainqueue)
print _("Print Started at: %s") % format_time(self.starttime)
def endcb(self): def endcb(self):
if self.p.queueindex == 0: if self.p.queueindex == 0:
print "Print ended at: " + format_time(time.time())
print_duration = int(time.time () - self.starttime + self.extra_print_time) print_duration = int(time.time () - self.starttime + self.extra_print_time)
print "and took: " + format_duration(print_duration) print _("Print ended at: %(end_time)s and took %(duration)s") % {"end_time": format_time(time.time()),
"duration": format_duration(print_duration)}
wx.CallAfter(self.pausebtn.Disable) wx.CallAfter(self.pausebtn.Disable)
wx.CallAfter(self.printbtn.SetLabel, _("Print")) wx.CallAfter(self.printbtn.SetLabel, _("Print"))
...@@ -216,7 +333,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -216,7 +333,6 @@ class PronterWindow(MainWindow, pronsole.pronsole):
param = self.settings.final_command param = self.settings.final_command
if not param: if not param:
return return
import shlex
pararray = [i.replace("$s", str(self.filename)).replace("$t", format_duration(print_duration)).encode() for i in shlex.split(param.replace("\\", "\\\\").encode())] pararray = [i.replace("$s", str(self.filename)).replace("$t", format_duration(print_duration)).encode() for i in shlex.split(param.replace("\\", "\\\\").encode())]
self.finalp = subprocess.Popen(pararray, stderr = subprocess.STDOUT, stdout = subprocess.PIPE) self.finalp = subprocess.Popen(pararray, stderr = subprocess.STDOUT, stdout = subprocess.PIPE)
...@@ -236,45 +352,42 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -236,45 +352,42 @@ class PronterWindow(MainWindow, pronsole.pronsole):
if self.filename: if self.filename:
wx.CallAfter(self.printbtn.Enable) wx.CallAfter(self.printbtn.Enable)
def layer_change_cb(self, newlayer):
if self.compute_eta:
secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
self.compute_eta.update_layer(newlayer, secondselapsed)
def sentcb(self, line): def sentcb(self, line):
if "G1" in line: gline = gcoder.Line(line)
if "Z" in line: gline.parse_coordinates(imperial = False)
try: if gline.is_move:
layer = float(line.split("Z")[1].split()[0]) if gline.z != None:
layer = gline.z
if layer != self.curlayer: if layer != self.curlayer:
self.curlayer = layer self.curlayer = layer
self.gviz.hilight = [] self.gviz.clearhilights()
threading.Thread(target = wx.CallAfter, args = (self.gviz.setlayer, layer)).start() wx.CallAfter(self.gviz.setlayer, layer)
except: elif gline.command in ["M104", "M109"]:
pass gline.parse_coordinates(imperial = False, force = True)
try: if gline.s != None:
self.sentlines.put_nowait(line) temp = gline.s
except: if self.display_gauges: wx.CallAfter(self.hottgauge.SetTarget, temp)
pass
#threading.Thread(target = self.gviz.addgcode, args = (line, 1)).start()
#self.gwindow.p.addgcode(line, hilight = 1)
if "M104" in line or "M109" in line:
if "S" in line:
try:
temp = float(line.split("S")[1].split("*")[0])
wx.CallAfter(self.graph.SetExtruder0TargetTemperature, temp) wx.CallAfter(self.graph.SetExtruder0TargetTemperature, temp)
except: elif gline.command == "M140":
pass gline.parse_coordinates(imperial = False, force = True)
try: if gline.s != None:
self.sentlines.put_nowait(line) temp = gline.s
except: if self.display_gauges: wx.CallAfter(self.bedtgauge.SetTarget, temp)
pass
if "M140" in line:
if "S" in line:
try:
temp = float(line.split("S")[1].split("*")[0])
wx.CallAfter(self.graph.SetBedTargetTemperature, temp) wx.CallAfter(self.graph.SetBedTargetTemperature, temp)
except: else:
pass return
try:
self.sentlines.put_nowait(line) self.sentlines.put_nowait(line)
except:
pass def printsentcb(self, gline):
if gline.is_move and hasattr(self.gwindow, "set_current_gline"):
wx.CallAfter(self.gwindow.set_current_gline, gline)
if gline.is_move and hasattr(self.gviz, "set_current_gline"):
wx.CallAfter(self.gviz.set_current_gline, gline)
def do_extrude(self, l = ""): def do_extrude(self, l = ""):
try: try:
...@@ -294,6 +407,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -294,6 +407,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
def setbedgui(self, f): def setbedgui(self, f):
self.bsetpoint = f self.bsetpoint = f
if self.display_gauges: self.bedtgauge.SetTarget(int(f))
wx.CallAfter(self.graph.SetBedTargetTemperature, int(f)) wx.CallAfter(self.graph.SetBedTargetTemperature, int(f))
if f>0: if f>0:
wx.CallAfter(self.btemp.SetValue, str(f)) wx.CallAfter(self.btemp.SetValue, str(f))
...@@ -313,6 +427,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -313,6 +427,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
def sethotendgui(self, f): def sethotendgui(self, f):
self.hsetpoint = f self.hsetpoint = f
if self.display_gauges: self.hottgauge.SetTarget(int(f))
wx.CallAfter(self.graph.SetExtruder0TargetTemperature, int(f)) wx.CallAfter(self.graph.SetExtruder0TargetTemperature, int(f))
if f > 0: if f > 0:
wx.CallAfter(self.htemp.SetValue, str(f)) wx.CallAfter(self.htemp.SetValue, str(f))
...@@ -423,10 +538,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -423,10 +538,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
def project(self,event): def project(self,event):
from printrun import projectlayer from printrun import projectlayer
if self.p.online: projectlayer.SettingsFrame(self, self.p).Show()
projectlayer.setframe(self,self.p).Show()
else:
print _("Printer is not online.")
def popmenu(self): def popmenu(self):
self.menustrip = wx.MenuBar() self.menustrip = wx.MenuBar()
...@@ -444,7 +556,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -444,7 +556,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.macros_menu = wx.Menu() self.macros_menu = wx.Menu()
m.AppendSubMenu(self.macros_menu, _("&Macros")) m.AppendSubMenu(self.macros_menu, _("&Macros"))
self.Bind(wx.EVT_MENU, self.new_macro, self.macros_menu.Append(-1, _("<&New...>"))) self.Bind(wx.EVT_MENU, self.new_macro, self.macros_menu.Append(-1, _("<&New...>")))
self.Bind(wx.EVT_MENU, lambda *e:options(self), m.Append(-1, _("&Options"), _(" Options dialog"))) self.Bind(wx.EVT_MENU, lambda *e: PronterOptions(self), m.Append(-1, _("&Options"), _(" Options dialog")))
self.Bind(wx.EVT_MENU, lambda x: threading.Thread(target = lambda:self.do_skein("set")).start(), m.Append(-1, _("Slicing Settings"), _(" Adjust slicing settings"))) self.Bind(wx.EVT_MENU, lambda x: threading.Thread(target = lambda:self.do_skein("set")).start(), m.Append(-1, _("Slicing Settings"), _(" Adjust slicing settings")))
...@@ -464,14 +576,12 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -464,14 +576,12 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.SetMenuBar(self.menustrip) self.SetMenuBar(self.menustrip)
def doneediting(self, gcode): def doneediting(self, gcode):
f = open(self.filename, "w") open(self.filename, "w").write("\n".join(gcode))
f.write("\n".join(gcode))
f.close()
wx.CallAfter(self.loadfile, None, self.filename) wx.CallAfter(self.loadfile, None, self.filename)
def do_editgcode(self, e = None): def do_editgcode(self, e = None):
if self.filename is not None: if self.filename is not None:
MacroEditor(self.filename, self.f, self.doneediting, 1) MacroEditor(self.filename, [line.raw for line in self.fgcode], self.doneediting, 1)
def new_macro(self, e = None): def new_macro(self, e = None):
dialog = wx.Dialog(self, -1, _("Enter macro name"), size = (260, 85)) dialog = wx.Dialog(self, -1, _("Enter macro name"), size = (260, 85))
...@@ -574,7 +684,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -574,7 +684,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
obj = e.GetEventObject() obj = e.GetEventObject()
popupmenu = wx.Menu() popupmenu = wx.Menu()
item = popupmenu.Append(-1, _("SD Upload")) item = popupmenu.Append(-1, _("SD Upload"))
if not self.f or not len(self.f): if not self.fgcode:
item.Enable(False) item.Enable(False)
self.Bind(wx.EVT_MENU, self.upload, id = item.GetId()) self.Bind(wx.EVT_MENU, self.upload, id = item.GetId())
item = popupmenu.Append(-1, _("SD Print")) item = popupmenu.Append(-1, _("SD Print"))
...@@ -592,7 +702,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -592,7 +702,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
wx.CallAfter(self.btemp.SetInsertionPoint, 0) wx.CallAfter(self.btemp.SetInsertionPoint, 0)
def showwin(self, event): def showwin(self, event):
if(self.f is not None): if self.fgcode:
self.gwindow.Show(True) self.gwindow.Show(True)
self.gwindow.SetToolTip(wx.ToolTip("Mousewheel zooms the display\nShift / Mousewheel scrolls layers")) self.gwindow.SetToolTip(wx.ToolTip("Mousewheel zooms the display\nShift / Mousewheel scrolls layers"))
self.gwindow.Raise() self.gwindow.Raise()
...@@ -612,21 +722,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -612,21 +722,6 @@ class PronterWindow(MainWindow, pronsole.pronsole):
except: except:
pass pass
def toggleview(self, e):
if(self.mini):
self.mini = False
self.mainsizer.Fit(self)
#self.SetSize(winsize)
wx.CallAfter(self.minibtn.SetLabel, _("Mini mode"))
else:
self.mini = True
self.uppersizer.Fit(self)
#self.SetSize(winssize)
wx.CallAfter(self.minibtn.SetLabel, _("Full mode"))
def cbuttons_reload(self): def cbuttons_reload(self):
allcbs = [] allcbs = []
ubs = self.uppersizer ubs = self.uppersizer
...@@ -649,7 +744,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -649,7 +744,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
for i in xrange(len(self.custombuttons)): for i in xrange(len(self.custombuttons)):
btndef = self.custombuttons[i] btndef = self.custombuttons[i]
try: try:
b = wx.Button(self.panel, -1, btndef.label, style = wx.BU_EXACTFIT) b = wx.Button(self.cbuttons_panel, -1, btndef.label, style = wx.BU_EXACTFIT)
b.SetToolTip(wx.ToolTip(_("Execute command: ")+btndef.command)) b.SetToolTip(wx.ToolTip(_("Execute command: ")+btndef.command))
if btndef.background: if btndef.background:
b.SetBackgroundColour(btndef.background) b.SetBackgroundColour(btndef.background)
...@@ -658,13 +753,13 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -658,13 +753,13 @@ class PronterWindow(MainWindow, pronsole.pronsole):
b.SetForegroundColour("#ffffff") b.SetForegroundColour("#ffffff")
except: except:
if i == newbuttonbuttonindex: if i == newbuttonbuttonindex:
self.newbuttonbutton = b = wx.Button(self.panel, -1, "+", size = (19, 18), style = wx.BU_EXACTFIT) self.newbuttonbutton = b = wx.Button(self.cbuttons_panel, -1, "+", size = (19, 18), style = wx.BU_EXACTFIT)
#b.SetFont(wx.Font(12, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) #b.SetFont(wx.Font(12, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))
b.SetForegroundColour("#4444ff") b.SetForegroundColour("#4444ff")
b.SetToolTip(wx.ToolTip(_("click to add new custom button"))) b.SetToolTip(wx.ToolTip(_("click to add new custom button")))
b.Bind(wx.EVT_BUTTON, self.cbutton_edit) b.Bind(wx.EVT_BUTTON, self.cbutton_edit)
else: else:
b = wx.Button(self.panel,-1, ".", size = (1, 1)) b = wx.Button(self.cbuttons_panel,-1, ".", size = (1, 1))
#b = wx.StaticText(self.panel,-1, "", size = (72, 22), style = wx.ALIGN_CENTRE+wx.ST_NO_AUTORESIZE) #+wx.SIMPLE_BORDER #b = wx.StaticText(self.panel,-1, "", size = (72, 22), style = wx.ALIGN_CENTRE+wx.ST_NO_AUTORESIZE) #+wx.SIMPLE_BORDER
b.Disable() b.Disable()
#continue #continue
...@@ -762,15 +857,15 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -762,15 +857,15 @@ class PronterWindow(MainWindow, pronsole.pronsole):
if bedit.color.GetValue().strip()!="": if bedit.color.GetValue().strip()!="":
self.custombuttons[n].background = bedit.color.GetValue() self.custombuttons[n].background = bedit.color.GetValue()
self.cbutton_save(n, self.custombuttons[n]) self.cbutton_save(n, self.custombuttons[n])
bedit.Destroy() wx.CallAfter(bedit.Destroy)
self.cbuttons_reload() wx.CallAfter(self.cbuttons_reload)
def cbutton_remove(self, e, button): def cbutton_remove(self, e, button):
n = button.custombutton n = button.custombutton
self.custombuttons[n]=None
self.cbutton_save(n, None) self.cbutton_save(n, None)
#while len(self.custombuttons) and self.custombuttons[-1] is None: del self.custombuttons[n]
# del self.custombuttons[-1] for i in range(n, len(self.custombuttons)):
self.cbutton_save(i, self.custombuttons[i])
wx.CallAfter(self.cbuttons_reload) wx.CallAfter(self.cbuttons_reload)
def cbutton_order(self, e, button, dir): def cbutton_order(self, e, button, dir):
...@@ -785,7 +880,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -785,7 +880,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.cbutton_save(n+1, self.custombuttons[n+1]) self.cbutton_save(n+1, self.custombuttons[n+1])
#if self.custombuttons[-1] is None: #if self.custombuttons[-1] is None:
# del self.custombuttons[-1] # del self.custombuttons[-1]
self.cbuttons_reload() wx.CallAfter(self.cbuttons_reload)
def editbutton(self, e): def editbutton(self, e):
if e.IsCommandEvent() or e.ButtonUp(wx.MOUSE_BTN_RIGHT): if e.IsCommandEvent() or e.ButtonUp(wx.MOUSE_BTN_RIGHT):
...@@ -932,28 +1027,35 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -932,28 +1027,35 @@ class PronterWindow(MainWindow, pronsole.pronsole):
e.Skip() e.Skip()
def homeButtonClicked(self, corner): def homeButtonClicked(self, corner):
# When user clicks on the XY control, the Z control no longer gets spacebar/repeat signals
self.zb.clearRepeat()
if corner == 0: # upper-left if corner == 0: # upper-left
self.onecmd('home X') self.onecmd('home X')
if corner == 1: # upper-right elif corner == 1: # upper-right
self.onecmd('home Y') self.onecmd('home Y')
if corner == 2: # lower-right elif corner == 2: # lower-right
self.onecmd('home Z') self.onecmd('home Z')
if corner == 3: # lower-left elif corner == 3: # lower-left
self.onecmd('home') self.onecmd('home')
# When user clicks on the XY control, the Z control no longer gets spacebar/repeat signals else:
self.zb.clearRepeat() return
self.p.send_now('M114')
def moveXY(self, x, y): def moveXY(self, x, y):
# When user clicks on the XY control, the Z control no longer gets spacebar/repeat signals
self.zb.clearRepeat()
if x != 0: if x != 0:
self.onecmd('move X %s' % x) self.onecmd('move X %s' % x)
if y != 0: elif y != 0:
self.onecmd('move Y %s' % y) self.onecmd('move Y %s' % y)
# When user clicks on the XY control, the Z control no longer gets spacebar/repeat signals else:
self.zb.clearRepeat() return
self.p.send_now('M114')
def moveZ(self, z): def moveZ(self, z):
if z != 0: if z != 0:
self.onecmd('move Z %s' % z) self.onecmd('move Z %s' % z)
self.p.send_now('M114')
# When user clicks on the Z control, the XY control no longer gets spacebar/repeat signals # When user clicks on the Z control, the XY control no longer gets spacebar/repeat signals
self.xyb.clearRepeat() self.xyb.clearRepeat()
...@@ -985,11 +1087,8 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -985,11 +1087,8 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.save_in_rc("set xy_feedrate", "set xy_feedrate %d" % self.settings.xy_feedrate) self.save_in_rc("set xy_feedrate", "set xy_feedrate %d" % self.settings.xy_feedrate)
self.save_in_rc("set z_feedrate", "set z_feedrate %d" % self.settings.z_feedrate) self.save_in_rc("set z_feedrate", "set z_feedrate %d" % self.settings.z_feedrate)
self.save_in_rc("set e_feedrate", "set e_feedrate %d" % self.settings.e_feedrate) self.save_in_rc("set e_feedrate", "set e_feedrate %d" % self.settings.e_feedrate)
try: wx.CallAfter(self.gwindow.Destroy)
self.gwindow.Destroy() wx.CallAfter(self.Destroy)
except:
pass
self.Destroy()
def do_monitor(self, l = ""): def do_monitor(self, l = ""):
if l.strip()=="": if l.strip()=="":
...@@ -1010,6 +1109,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1010,6 +1109,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
def setmonitor(self, e): def setmonitor(self, e):
self.monitor = self.monitorbox.GetValue() self.monitor = self.monitorbox.GetValue()
self.set("monitor", self.monitor)
if self.monitor: if self.monitor:
wx.CallAfter(self.graph.StartPlotting, 1000) wx.CallAfter(self.graph.StartPlotting, 1000)
else: else:
...@@ -1019,8 +1119,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1019,8 +1119,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
try: try:
self.logbox.AppendText(text) self.logbox.AppendText(text)
except: except:
print "attempted to write invalid text to console" print _("Attempted to write invalid text to console, which could be due to an invalid baudrate")
pass
def setloud(self,e): def setloud(self,e):
self.p.loud=e.IsChecked() self.p.loud=e.IsChecked()
...@@ -1032,39 +1131,74 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1032,39 +1131,74 @@ class PronterWindow(MainWindow, pronsole.pronsole):
wx.CallAfter(self.addtexttolog, ">>>" + command + "\n"); wx.CallAfter(self.addtexttolog, ">>>" + command + "\n");
self.onecmd(str(command)) self.onecmd(str(command))
self.commandbox.SetSelection(0, len(command)) self.commandbox.SetSelection(0, len(command))
self.commandbox.history+=[command] self.commandbox.history.append(command)
self.commandbox.histindex = len(self.commandbox.history) self.commandbox.histindex = len(self.commandbox.history)
def clearOutput(self, e): def clearOutput(self, e):
self.logbox.Clear() self.logbox.Clear()
def update_tempdisplay(self):
try:
# FIXME : we don't use setpoints here, we should probably exploit them
temps = parse_temperature_report(self.tempreport)
if "T0" in temps:
hotend_temp = float(temps["T0"][0])
else:
hotend_temp = float(temps["T"][0]) if "T" in temps else -1.0
wx.CallAfter(self.graph.SetExtruder0Temperature, hotend_temp)
if self.display_gauges: wx.CallAfter(self.hottgauge.SetValue, hotend_temp)
if "T1" in temps:
hotend_temp = float(temps["T1"][0])
wx.CallAfter(self.graph.SetExtruder1Temperature, hotend_temp)
bed_temp = float(temps["B"][0]) if "B" in temps else -1.0
wx.CallAfter(self.graph.SetBedTemperature, bed_temp)
if self.display_gauges: wx.CallAfter(self.bedtgauge.SetValue, bed_temp)
except:
traceback.print_exc()
def update_pos(self, l):
bits = gcoder.m114_exp.findall(l)
x = None
y = None
z = None
for bit in bits:
if x is None and bit.startswith("X"):
x = float(bit[1:].replace(":",""))
elif y is None and bit.startswith("Y"):
y = float(bit[1:].replace(":",""))
elif z is None and bit.startswith("Z"):
z = float(bit[1:].replace(":",""))
if x is not None: self.current_pos[0] = x
if y is not None: self.current_pos[1] = y
if z is not None: self.current_pos[2] = z
def statuschecker(self): def statuschecker(self):
while self.statuscheck: while self.statuscheck:
string = "" string = ""
wx.CallAfter(self.tempdisp.SetLabel, self.tempreport.strip().replace("ok ", ""))
try:
wx.CallAfter(self.graph.SetExtruder0Temperature, parse_temperature_report(self.tempreport, "T:"))
wx.CallAfter(self.graph.SetBedTemperature, parse_temperature_report(self.tempreport, "B:"))
except:
pass
fractioncomplete = 0.0 fractioncomplete = 0.0
if self.sdprinting: if self.sdprinting:
fractioncomplete = float(self.percentdone / 100.0) fractioncomplete = float(self.percentdone / 100.0)
string += _(" SD printing:%04.2f %%") % (self.percentdone,) string += _(" SD printing:%04.2f %%") % (self.percentdone,)
if fractioncomplete > 0.0:
secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
secondsestimate = secondselapsed / fractioncomplete
secondsremain = secondsestimate - secondselapsed
string += _(" Est: %s of %s remaining | ") % (format_duration(secondsremain),
format_duration(secondsestimate))
string += _(" Z: %.3f mm") % self.curlayer
if self.p.printing: if self.p.printing:
fractioncomplete = float(self.p.queueindex) / len(self.p.mainqueue) fractioncomplete = float(self.p.queueindex) / len(self.p.mainqueue)
string += _(" Printing: %04.2f%% |") % (100*float(self.p.queueindex)/len(self.p.mainqueue),) string += _(" Printing: %04.2f%% |") % (100*float(self.p.queueindex)/len(self.p.mainqueue),)
string += _(" Line# %d of %d lines |" ) % (self.p.queueindex, len(self.p.mainqueue)) string += _(" Line# %d of %d lines |" ) % (self.p.queueindex, len(self.p.mainqueue))
if fractioncomplete > 0.0: if self.p.queueindex > 0:
secondselapsed = int(time.time() - self.starttime + self.extra_print_time) secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
secondsestimate = secondselapsed / fractioncomplete secondsremain, secondsestimate = self.compute_eta(self.p.queueindex, secondselapsed)
secondsremain = secondsestimate - secondselapsed
string += _(" Est: %s of %s remaining | ") % (format_duration(secondsremain), string += _(" Est: %s of %s remaining | ") % (format_duration(secondsremain),
format_duration(secondsestimate)) format_duration(secondsestimate))
string += _(" Z: %0.2f mm") % self.curlayer string += _(" Z: %.3f mm") % self.curlayer
wx.CallAfter(self.status.SetStatusText, string) wx.CallAfter(self.status.SetStatusText, string)
wx.CallAfter(self.gviz.Refresh) wx.CallAfter(self.gviz.Refresh)
if(self.monitor and self.p.online): if self.monitor and self.p.online:
if self.sdprinting: if self.sdprinting:
self.p.send_now("M27") self.p.send_now("M27")
if not hasattr(self, "auto_monitor_pattern"): if not hasattr(self, "auto_monitor_pattern"):
...@@ -1077,11 +1211,8 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1077,11 +1211,8 @@ class PronterWindow(MainWindow, pronsole.pronsole):
break break
time.sleep(0.25) time.sleep(0.25)
while not self.sentlines.empty(): while not self.sentlines.empty():
try:
gc = self.sentlines.get_nowait() gc = self.sentlines.get_nowait()
wx.CallAfter(self.gviz.addgcode, gc, 1) wx.CallAfter(self.gviz.addgcode, gc, 1)
except:
break
wx.CallAfter(self.status.SetStatusText, _("Not connected to printer.")) wx.CallAfter(self.status.SetStatusText, _("Not connected to printer."))
def capture(self, func, *args, **kwargs): def capture(self, func, *args, **kwargs):
...@@ -1104,24 +1235,23 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1104,24 +1235,23 @@ class PronterWindow(MainWindow, pronsole.pronsole):
return retval return retval
def recvcb(self, l): def recvcb(self, l):
if "T:" in l: isreport = False
if "ok C:" in l or "Count" in l:
self.posreport = l
self.update_pos(l)
isreport = True
if "ok T:" in l:
self.tempreport = l self.tempreport = l
wx.CallAfter(self.tempdisp.SetLabel, self.tempreport.strip().replace("ok ", "")) wx.CallAfter(self.tempdisp.SetLabel, self.tempreport.strip().replace("ok ", ""))
try: self.update_tempdisplay()
wx.CallAfter(self.graph.SetExtruder0Temperature, parse_temperature_report(self.tempreport, "T:")) isreport = True
wx.CallAfter(self.graph.SetBedTemperature, parse_temperature_report(self.tempreport, "B:"))
except:
traceback.print_exc()
tstring = l.rstrip() tstring = l.rstrip()
#print tstring if self.p.loud or (tstring not in ["ok", "wait"] and not isreport):
if (tstring!="ok") and (tstring!="wait") and ("ok T:" not in tstring) and (not self.p.loud):
# print "*"+tstring+"*"
# print "[" + time.strftime('%H:%M:%S',time.localtime(time.time())) + "] " + tstring
wx.CallAfter(self.addtexttolog, tstring + "\n"); wx.CallAfter(self.addtexttolog, tstring + "\n");
for i in self.recvlisteners: for listener in self.recvlisteners:
i(l) listener(l)
def listfiles(self, line): def listfiles(self, line, ignored = False):
if "Begin file list" in line: if "Begin file list" in line:
self.listing = 1 self.listing = 1
elif "End file list" in line: elif "End file list" in line:
...@@ -1129,7 +1259,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1129,7 +1259,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.recvlisteners.remove(self.listfiles) self.recvlisteners.remove(self.listfiles)
wx.CallAfter(self.filesloaded) wx.CallAfter(self.filesloaded)
elif self.listing: elif self.listing:
self.sdfiles+=[line.replace("\n", "").replace("\r", "").lower()] self.sdfiles.append(line.strip().lower())
def waitforsdresponse(self, l): def waitforsdresponse(self, l):
if "file.open failed" in l: if "file.open failed" in l:
...@@ -1166,6 +1296,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1166,6 +1296,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
if len(target): if len(target):
self.recvlisteners+=[self.waitforsdresponse] self.recvlisteners+=[self.waitforsdresponse]
self.p.send_now("M23 "+target.lower()) self.p.send_now("M23 "+target.lower())
dlg.Destroy()
#print self.sdfiles #print self.sdfiles
def getfiles(self): def getfiles(self):
...@@ -1180,7 +1311,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1180,7 +1311,6 @@ class PronterWindow(MainWindow, pronsole.pronsole):
def skein_func(self): def skein_func(self):
try: try:
import shlex
param = self.expandcommand(self.settings.slicecommand).encode() param = self.expandcommand(self.settings.slicecommand).encode()
print "Slicing: ", param print "Slicing: ", param
pararray = [i.replace("$s", self.filename).replace("$o", self.filename.replace(".stl", "_export.gcode").replace(".STL", "_export.gcode")).encode() for i in shlex.split(param.replace("\\", "\\\\").encode())] pararray = [i.replace("$s", self.filename).replace("$o", self.filename.replace(".stl", "_export.gcode").replace(".STL", "_export.gcode")).encode() for i in shlex.split(param.replace("\\", "\\\\").encode())]
...@@ -1207,13 +1337,12 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1207,13 +1337,12 @@ class PronterWindow(MainWindow, pronsole.pronsole):
fn = self.filename fn = self.filename
try: try:
self.filename = self.filename.replace(".stl", "_export.gcode").replace(".STL", "_export.gcode").replace(".obj", "_export.gcode").replace(".OBJ", "_export.gcode") self.filename = self.filename.replace(".stl", "_export.gcode").replace(".STL", "_export.gcode").replace(".obj", "_export.gcode").replace(".OBJ", "_export.gcode")
of = open(self.filename) self.fgcode = gcoder.GCode(open(self.filename))
self.f = [i.replace("\n", "").replace("\r", "") for i in of]
of.close()
if self.p.online: if self.p.online:
wx.CallAfter(self.printbtn.Enable) wx.CallAfter(self.printbtn.Enable)
wx.CallAfter(self.status.SetStatusText, _("Loaded ")+self.filename+_(", %d lines") % (len(self.f),)) wx.CallAfter(self.status.SetStatusText, _("Loaded %s, %d lines") % (self.filename, len(self.fgcode),))
print _("Loaded %s, %d lines") % (self.filename, len(self.fgcode),)
wx.CallAfter(self.pausebtn.Disable) wx.CallAfter(self.pausebtn.Disable)
wx.CallAfter(self.printbtn.SetLabel, _("Print")) wx.CallAfter(self.printbtn.SetLabel, _("Print"))
...@@ -1251,14 +1380,17 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1251,14 +1380,17 @@ class PronterWindow(MainWindow, pronsole.pronsole):
basedir = os.path.split(self.filename)[0] basedir = os.path.split(self.filename)[0]
except: except:
pass pass
dlg = None
if filename is None:
dlg = wx.FileDialog(self, _("Open file to print"), basedir, style = wx.FD_OPEN|wx.FD_FILE_MUST_EXIST) dlg = wx.FileDialog(self, _("Open file to print"), basedir, style = wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
dlg.SetWildcard(_("OBJ, STL, and GCODE files (*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ)|*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ|All Files (*.*)|*.*")) dlg.SetWildcard(_("OBJ, STL, and GCODE files (*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ)|*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ|All Files (*.*)|*.*"))
if(filename is not None or dlg.ShowModal() == wx.ID_OK): if filename or dlg.ShowModal() == wx.ID_OK:
if filename is not None: if filename:
name = filename name = filename
else: else:
name = dlg.GetPath() name = dlg.GetPath()
if not(os.path.exists(name)): dlg.Destroy()
if not os.path.exists(name):
self.status.SetStatusText(_("File not found!")) self.status.SetStatusText(_("File not found!"))
return return
path = os.path.split(name)[0] path = os.path.split(name)[0]
...@@ -1270,10 +1402,9 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1270,10 +1402,9 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.skein(name) self.skein(name)
else: else:
self.filename = name self.filename = name
of = open(self.filename) self.fgcode = gcoder.GCode(open(self.filename))
self.f = [i.replace("\n", "").replace("\r", "") for i in of] self.status.SetStatusText(_("Loaded %s, %d lines") % (name, len(self.fgcode)))
of.close() print _("Loaded %s, %d lines") % (name, len(self.fgcode))
self.status.SetStatusText(_("Loaded %s, %d lines") % (name, len(self.f)))
wx.CallAfter(self.printbtn.SetLabel, _("Print")) wx.CallAfter(self.printbtn.SetLabel, _("Print"))
wx.CallAfter(self.pausebtn.SetLabel, _("Pause")) wx.CallAfter(self.pausebtn.SetLabel, _("Pause"))
wx.CallAfter(self.pausebtn.Disable) wx.CallAfter(self.pausebtn.Disable)
...@@ -1281,26 +1412,21 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1281,26 +1412,21 @@ class PronterWindow(MainWindow, pronsole.pronsole):
if self.p.online: if self.p.online:
wx.CallAfter(self.printbtn.Enable) wx.CallAfter(self.printbtn.Enable)
threading.Thread(target = self.loadviz).start() threading.Thread(target = self.loadviz).start()
else:
dlg.Destroy()
def loadviz(self): def loadviz(self):
Xtot, Ytot, Ztot, Xmin, Xmax, Ymin, Ymax, Zmin, Zmax = pronsole.measurements(self.f) gcode = self.fgcode
print pronsole.totalelength(self.f), _("mm of filament used in this print\n") print gcode.filament_length, _("mm of filament used in this print")
print _("the print goes from %f mm to %f mm in X\nand is %f mm wide\n") % (Xmin, Xmax, Xtot) print _("The print goes:")
print _("the print goes from %f mm to %f mm in Y\nand is %f mm wide\n") % (Ymin, Ymax, Ytot) print _("- from %.2f mm to %.2f mm in X and is %.2f mm wide") % (gcode.xmin, gcode.xmax, gcode.width)
print _("the print goes from %f mm to %f mm in Z\nand is %f mm high\n") % (Zmin, Zmax, Ztot) print _("- from %.2f mm to %.2f mm in Y and is %.2f mm deep") % (gcode.ymin, gcode.ymax, gcode.depth)
try: print _("- from %.2f mm to %.2f mm in Z and is %.2f mm high") % (gcode.zmin, gcode.zmax, gcode.height)
print _("Estimated duration (pessimistic): "), pronsole.estimate_duration(self.f) print _("Estimated duration: %s") % gcode.estimate_duration()
except:
pass
#import time
#t0 = time.time()
self.gviz.clear() self.gviz.clear()
self.gwindow.p.clear() self.gwindow.p.clear()
self.gviz.addfile(self.f) self.gviz.addfile(gcode)
#print "generated 2d view in %f s"%(time.time()-t0) self.gwindow.p.addfile(gcode)
#t0 = time.time()
self.gwindow.p.addfile(self.f)
#print "generated 3d view in %f s"%(time.time()-t0)
self.gviz.showall = 1 self.gviz.showall = 1
wx.CallAfter(self.gviz.Refresh) wx.CallAfter(self.gviz.Refresh)
...@@ -1315,14 +1441,14 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1315,14 +1441,14 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.p.send_now("M24") self.p.send_now("M24")
return return
if self.f is None or not len(self.f): if not self.fgcode:
wx.CallAfter(self.status.SetStatusText, _("No file loaded. Please use load first.")) wx.CallAfter(self.status.SetStatusText, _("No file loaded. Please use load first."))
return return
if not self.p.online: if not self.p.online:
wx.CallAfter(self.status.SetStatusText, _("Not connected to printer.")) wx.CallAfter(self.status.SetStatusText, _("Not connected to printer."))
return return
self.on_startprint() self.on_startprint()
self.p.startprint(self.f) self.p.startprint(self.fgcode)
def on_startprint(self): def on_startprint(self):
wx.CallAfter(self.pausebtn.SetLabel, _("Pause")) wx.CallAfter(self.pausebtn.SetLabel, _("Pause"))
...@@ -1339,14 +1465,14 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1339,14 +1465,14 @@ class PronterWindow(MainWindow, pronsole.pronsole):
def uploadtrigger(self, l): def uploadtrigger(self, l):
if "Writing to file" in l: if "Writing to file" in l:
self.uploading = True self.uploading = True
self.p.startprint(self.f) self.p.startprint(self.fgcode)
self.p.endcb = self.endupload self.p.endcb = self.endupload
self.recvlisteners.remove(self.uploadtrigger) self.recvlisteners.remove(self.uploadtrigger)
elif "open failed, File" in l: elif "open failed, File" in l:
self.recvlisteners.remove(self.uploadtrigger) self.recvlisteners.remove(self.uploadtrigger)
def upload(self, event): def upload(self, event):
if not self.f or not len(self.f): if not self.fgcode:
return return
if not self.p.online: if not self.p.online:
return return
...@@ -1355,6 +1481,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1355,6 +1481,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.p.send_now("M21") self.p.send_now("M21")
self.p.send_now("M28 "+str(dlg.GetValue())) self.p.send_now("M28 "+str(dlg.GetValue()))
self.recvlisteners+=[self.uploadtrigger] self.recvlisteners+=[self.uploadtrigger]
dlg.Destroy()
def pause(self, event): def pause(self, event):
print _("Paused.") print _("Paused.")
...@@ -1383,7 +1510,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1383,7 +1510,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.on_startprint() self.on_startprint()
threading.Thread(target = self.getfiles).start() threading.Thread(target = self.getfiles).start()
def connect(self, event): def connect(self, event = None):
print _("Connecting...") print _("Connecting...")
port = None port = None
try: try:
...@@ -1491,36 +1618,21 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1491,36 +1618,21 @@ class PronterWindow(MainWindow, pronsole.pronsole):
self.p.paused = 0 self.p.paused = 0
wx.CallAfter(self.pausebtn.SetLabel, _("Pause")) wx.CallAfter(self.pausebtn.SetLabel, _("Pause"))
self.paused = 0 self.paused = 0
dlg.Destroy()
def get_build_dimensions(self, bdim): class PronterApp(wx.App):
import re
# a string containing up to six numbers delimited by almost anything mainwindow = None
# first 0-3 numbers specify the build volume, no sign, always positive
# remaining 0-3 numbers specify the coordinates of the "southwest" corner of the build platform def __init__(self, *args, **kwargs):
# "XXX,YYY" super(PronterApp, self).__init__(*args, **kwargs)
# "XXXxYYY+xxx-yyy" self.mainwindow = PronterWindow()
# "XXX,YYY,ZZZ+xxx+yyy-zzz" self.mainwindow.Show()
# etc
bdl = re.match(
"[^\d+-]*(\d+)?" + # X build size
"[^\d+-]*(\d+)?" + # Y build size
"[^\d+-]*(\d+)?" + # Z build size
"[^\d+-]*([+-]\d+)?" + # X corner coordinate
"[^\d+-]*([+-]\d+)?" + # Y corner coordinate
"[^\d+-]*([+-]\d+)?" + # Z corner coordinate
"[^\d+-]*([+-]\d+)?" + # X endstop
"[^\d+-]*([+-]\d+)?" + # Y endstop
"[^\d+-]*([+-]\d+)?" # Z endstop
,bdim).groups()
defaults = [200, 200, 100, 0, 0, 0, 0, 0, 0]
bdl_float = [float(value) if value else defaults[i] for i, value in enumerate(bdl)]
return bdl_float
if __name__ == '__main__': if __name__ == '__main__':
app = wx.App(False) app = PronterApp(False)
main = PronterWindow()
main.Show()
try: try:
app.MainLoop() app.MainLoop()
except: except KeyboardInterrupt:
pass pass
del app
...@@ -145,7 +145,7 @@ setup ( ...@@ -145,7 +145,7 @@ setup (
license = "GPLv3", license = "GPLv3",
data_files = data_files, data_files = data_files,
packages = ["printrun", "printrun.svg"], packages = ["printrun", "printrun.svg"],
scripts = ["pronsole.py", "pronterface.py", "plater.py", "printcore.py", "gcoder.py"], scripts = ["pronsole.py", "pronterface.py", "plater.py", "printcore.py", "pronserve.py"],
cmdclass = {"uninstall" : uninstall, cmdclass = {"uninstall" : uninstall,
"install" : install, "install" : install,
"install_data" : install_data} "install_data" : install_data}
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" onload='init()' width="150mm" height="150mm" version="1.1">
<script type='text/ecmascript'>
<![CDATA[
function getParameterByName(name)
{
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
var results = regex.exec(window.location.search);
if(results == null)
return "";
else
return decodeURIComponent(results[1].replace(/\+/g, " "));
}
function init() {
var width = getParameterByName('width') || 150;
var height = getParameterByName('height') || 150;
var gridheight = getParameterByName('gridheight') || 10;
var gridwidth = getParameterByName('gridwidth') || 10;
var lineheight = getParameterByName('lineheight') || 0.5;
var linewidth = getParameterByName('linewidth') || 0.5;
var lineblockheight = (gridheight/2)-lineheight;
var lineblockwidth = (gridwidth/2)-linewidth;
console.log(lineblockheight,lineblockwidth)
document.getElementsByTagName('svg')[0].setAttribute('width',width+'mm');
document.getElementsByTagName('svg')[0].setAttribute('height',height+'mm');
document.getElementsByTagName('pattern')[0].setAttribute('height',gridheight+'mm');
document.getElementsByTagName('pattern')[0].setAttribute('width',gridwidth+'mm');
document.getElementsByClassName('background')[0].setAttribute('height',gridheight+'mm');
document.getElementsByClassName('background')[0].setAttribute('width',gridwidth+'mm');
var blocks = document.getElementsByClassName('block');
for (var i in blocks){
if (blocks[i] instanceof SVGRectElement){
blocks[i].setAttribute('height', lineblockheight+'mm');
blocks[i].setAttribute('width', lineblockwidth+'mm');
}
}
document.getElementsByClassName('topright')[0].setAttribute('x',lineblockwidth+'mm');
document.getElementsByClassName('bottomleft')[0].setAttribute('y',lineblockheight+'mm');
document.getElementsByClassName('bottomright')[0].setAttribute('x',lineblockwidth+'mm');
document.getElementsByClassName('bottomright')[0].setAttribute('y',lineblockheight+'mm');
}
]]>
</script>
<defs>
<pattern id="grd" patternUnits="userSpaceOnUse" width="10mm" height="10mm">
<rect class="background" width="10mm" height="10mm" fill="red"/>
<rect class="block topleft" width="4.9mm" height="4.9mm"/>
<rect class="block topright" width="4.9mm" height="4.9mm" x="4.9mm"/>
<rect class="block bottomleft" width="4.9mm" height="4.9mm" y="4.9mm"/>
<rect class="block bottomright" width="4.9mm" height="4.9mm" x="4.9mm" y="4.9mm"/>
</pattern>
</defs>
<rect height="100%" width="100%" fill="url(#grd)"/>
</svg>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment