#!/usr/bin/env python # 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 os # Set up Internationalization using gettext # searching for installed locales on /usr/share; uses relative folder if not found (windows) from printrun.printrun_utils import install_locale install_locale('pronterface') import wx import time import threading import math import sys import re import traceback import subprocess from copy import copy from printrun import stltool from printrun.objectplater import Plater glview = False if "-nogl" not in sys.argv: try: from printrun import stlview glview = True except: print "Could not load 3D viewer for plater:" traceback.print_exc() def evalme(s): return eval(s[s.find("(") + 1:s.find(")")]) class showstl(wx.Window): def __init__(self, parent, size, pos): wx.Window.__init__(self, parent, size = size, pos = pos) self.i = 0 self.parent = parent self.previ = 0 self.Bind(wx.EVT_MOUSEWHEEL, self.rot) self.Bind(wx.EVT_MOUSE_EVENTS, self.move) self.Bind(wx.EVT_PAINT, self.repaint) self.Bind(wx.EVT_KEY_DOWN, self.keypress) self.triggered = 0 self.initpos = None self.prevsel = -1 def drawmodel(self, m, scale): m.bitmap = wx.EmptyBitmap(800, 800, 32) dc = wx.MemoryDC() dc.SelectObject(m.bitmap) dc.SetBackground(wx.Brush((0, 0, 0, 0))) dc.SetBrush(wx.Brush((0, 0, 0, 255))) dc.SetBrush(wx.Brush(wx.Colour(128, 255, 128))) dc.SetPen(wx.Pen(wx.Colour(128, 128, 128))) for i in m.facets: dc.DrawPolygon([wx.Point(400 + scale * p[0], (400 - scale * p[1])) for p in i[1]]) dc.SelectObject(wx.NullBitmap) m.bitmap.SetMask(wx.Mask(m.bitmap, wx.Colour(0, 0, 0, 255))) def move_shape(self, delta): """moves shape (selected in l, which is list ListBox of shapes) by an offset specified in tuple delta. Positive numbers move to (rigt, down)""" name = self.parent.l.GetSelection() if name == wx.NOT_FOUND: return False name = self.parent.l.GetString(name) model = self.parent.models[name] model.offsets = [model.offsets[0] + delta[0], model.offsets[1] + delta[1], model.offsets[2] ] self.Refresh() return True def move(self, event): if event.ButtonUp(wx.MOUSE_BTN_LEFT): if(self.initpos is not None): currentpos = event.GetPositionTuple() delta = (0.5 * (currentpos[0] - self.initpos[0]), -0.5 * (currentpos[1] - self.initpos[1]) ) self.move_shape(delta) self.Refresh() self.initpos = None elif event.ButtonDown(wx.MOUSE_BTN_RIGHT): self.parent.right(event) elif event.Dragging(): if self.initpos is None: self.initpos = event.GetPositionTuple() self.Refresh() dc = wx.ClientDC(self) p = event.GetPositionTuple() dc.DrawLine(self.initpos[0], self.initpos[1], p[0], p[1]) del dc else: event.Skip() def rotate_shape(self, angle): """rotates acive shape positive angle is clockwise """ self.i += angle if not self.triggered: self.triggered = 1 threading.Thread(target = self.cr).start() def keypress(self, event): """gets keypress events and moves/rotates acive shape""" keycode = event.GetKeyCode() #print keycode step = 5 angle = 18 if event.ControlDown(): step = 1 angle = 1 #h if keycode == 72: self.move_shape((-step, 0)) #l if keycode == 76: self.move_shape((step, 0)) #j if keycode == 75: self.move_shape((0, step)) #k if keycode == 74: self.move_shape((0, -step)) #[ if keycode == 91: self.rotate_shape(-angle) #] if keycode == 93: self.rotate_shape(angle) event.Skip() def rotateafter(self): if self.i != self.previ: i = self.parent.l.GetSelection() if i != wx.NOT_FOUND: self.parent.models[self.parent.l.GetString(i)].rot -= 5 * (self.i - self.previ) self.previ = self.i self.Refresh() def cr(self): time.sleep(0.01) wx.CallAfter(self.rotateafter) self.triggered = 0 def rot(self, event): z = event.GetWheelRotation() s = self.parent.l.GetSelection() if self.prevsel != s: self.i = 0 self.prevsel = s if z < 0: self.rotate_shape(-1) else: self.rotate_shape(1) def repaint(self, event): dc = wx.PaintDC(self) self.paint(dc = dc) def paint(self, coord1 = "x", coord2 = "y", dc = None): if dc is None: dc = wx.ClientDC(self) scale = 2 dc.SetPen(wx.Pen(wx.Colour(100, 100, 100))) for i in xrange(20): dc.DrawLine(0, i * scale * 10, 400, i * scale * 10) dc.DrawLine(i * scale * 10, 0, i * scale * 10, 400) dc.SetPen(wx.Pen(wx.Colour(0, 0, 0))) for i in xrange(4): dc.DrawLine(0, i * scale * 50, 400, i * scale * 50) dc.DrawLine(i * scale * 50, 0, i * scale * 50, 400) dc.SetBrush(wx.Brush(wx.Colour(128, 255, 128))) dc.SetPen(wx.Pen(wx.Colour(128, 128, 128))) dcs = wx.MemoryDC() for m in self.parent.models.values(): b = m.bitmap im = b.ConvertToImage() imgc = wx.Point(im.GetWidth() / 2, im.GetHeight() / 2) im = im.Rotate(math.radians(m.rot), imgc, 0) bm = wx.BitmapFromImage(im) dcs.SelectObject(bm) bsz = bm.GetSize() dc.Blit(scale * m.offsets[0] - bsz[0] / 2, 400 - (scale * m.offsets[1] + bsz[1] / 2), bsz[0], bsz[1], dcs, 0, 0, useMask = 1) del dc class StlPlater(Plater): load_wildcard = _("STL files (*.stl;*.STL)|*.stl;*.STL|OpenSCAD files (*.scad)|*.scad") save_wildcard = _("STL files (*.stl;*.STL)|*.stl;*.STL") def __init__(self, filenames = [], size = (800, 580), callback = None, parent = None, build_dimensions = None, circular_platform = False, simarrange_path = None): super(StlPlater, self).__init__(filenames, size, callback, parent, build_dimensions) if glview: viewer = stlview.StlViewPanel(self, (580, 580), build_dimensions = self.build_dimensions, circular = circular_platform) else: viewer = showstl(self, (580, 580), (0, 0)) self.simarrange_path = simarrange_path if simarrange_path else "./simarrange/sa" self.set_viewer(viewer) def done(self, event, cb): try: os.mkdir("tempstl") except: pass name = "tempstl/" + str(int(time.time()) % 10000) + ".stl" self.export_to(name) if cb is not None: cb(name) self.Destroy() def load_file(self, filename): if filename.lower().endswith(".stl"): try: self.load_stl(filename) except: dlg = wx.MessageDialog(self, _("Loading STL file failed"), _("Error"), wx.OK) dlg.ShowModal() traceback.print_exc(file = sys.stdout) elif filename.lower().endswith(".scad"): try: self.load_scad(filename) except: dlg = wx.MessageDialog(self, _("Loading OpenSCAD file failed"), _("Error"), wx.OK) dlg.ShowModal() traceback.print_exc(file = sys.stdout) def load_scad(self, name): lf = open(name) s = [i.replace("\n", "").replace("\r", "").replace(";", "") for i in lf if "stl" in i] lf.close() for i in s: parts = i.split() for part in parts: if 'translate' in part: translate_list = evalme(part) for part in parts: if 'rotate' in part: rotate_list = evalme(part) for part in parts: if 'import' in part: stl_file = evalme(part) newname = os.path.split(stl_file.lower())[1] c = 1 while newname in self.models: newname = os.path.split(stl_file.lower())[1] newname = newname + "(%d)" % c c += 1 stl_path = os.path.join(os.path.split(name)[0:len(os.path.split(stl_file)) - 1]) stl_full_path = os.path.join(stl_path[0], str(stl_file)) self.load_stl_into_model(stl_full_path, stl_file, translate_list, rotate_list[2]) def load_stl(self, name): if not os.path.exists(name): print _("Couldn't load non-existing file %s") % name return path = os.path.split(name)[0] self.basedir = path if name.lower().endswith(".stl"): for model in self.models.values(): if model.filename == name: newmodel = copy(model) newmodel.offsets = list(model.offsets) newmodel.rot = model.rot newmodel.scale = list(model.scale) self.add_model(name, newmodel) self.s.drawmodel(newmodel, 2) break else: #Filter out the path, just show the STL filename. self.load_stl_into_model(name, name) self.Refresh() def load_stl_into_model(self, path, name, offset = [0, 0, 0], rotation = 0, scale = [1.0, 1.0, 1.0]): model = stltool.stl(path) model.offsets = list(offset) model.rot = rotation model.scale = list(scale) model.filename = name minx = float("inf") miny = float("inf") minz = float("inf") maxx = float("-inf") maxy = float("-inf") maxz = float("-inf") for i in model.facets: for j in i[1]: if j[0] < minx: minx = j[0] if j[1] < miny: miny = j[1] if j[2] < minz: minz = j[2] if j[0] > maxx: maxx = j[0] if j[1] > maxy: maxy = j[1] if j[2] > maxz: maxz = j[2] model.dims = [minx, maxx, miny, maxy, minz, maxz] self.add_model(name, model) model.centeroffset = [-(model.dims[1] + model.dims[0]) / 2, -(model.dims[3] + model.dims[2]) / 2, 0] self.s.drawmodel(model, 2) def export_to(self, name): sf = open(name.replace(".", "_") + ".scad", "w") facets = [] for model in self.models.values(): r = model.rot rot = [0, 0, r] if r else None o = model.offsets co = model.centeroffset sf.write("translate([%s, %s, %s])" "rotate([0, 0, %s])" "translate([%s, %s, %s])" "import(\"%s\");\n" % (o[0], o[1], o[2], r, co[0], co[1], co[2], model.filename)) if any(co): model = model.translate(co) if rot: model = model.rotate(rot) if any(o): model = model.translate(o) facets += model.facets sf.close() stltool.emitstl(name, facets, "plater_export") print _("Wrote plate to %s") % name def autoplate(self, event = None): try: self.autoplate_simarrange() except: traceback.print_exc(file = sys.stdout) print _("Failed to use simarrange for plating, " "falling back to the standard method") super(StlPlater, self).autoplate() def autoplate_simarrange(self): print _("Autoplating using simarrange") models = dict(self.models) files = [model.filename for model in models.values()] command = [self.simarrange_path, "--dryrun", "-m", # Pack around center "-x", str(int(self.build_dimensions[0])), "-y", str(int(self.build_dimensions[1]))] + files p = subprocess.Popen(command, stdout = subprocess.PIPE) pos_regexp = re.compile("File: (.*) minx: ([0-9]+), miny: ([0-9]+), minrot: ([0-9]+)") for line in p.stdout: line = line.rstrip() if "Generating plate" in line: plateid = int(line.split()[-1]) if plateid > 0: print _("Plate full, please remove some objects") break if "File:" in line: bits = pos_regexp.match(line).groups() filename = bits[0] x = float(bits[1]) y = float(bits[2]) rot = -float(bits[3]) for name, model in models.items(): # FIXME: not sure this is going to work superwell with utf8 if model.filename == filename: model.offsets[0] = x model.offsets[1] = y model.rot = rot del models[name] break if p.wait() != 0: raise RuntimeError, _("simarrange failed")