printrun-src/printrun/objectplater.py

changeset 15
0bbb006204fc
child 46
cce0af6351f0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/printrun-src/printrun/objectplater.py	Fri Jun 03 09:16:07 2016 +0200
@@ -0,0 +1,300 @@
+#!/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/>.
+
+from .utils import install_locale, iconfile
+install_locale('plater')
+
+import logging
+import os
+import types
+import wx
+
+def patch_method(obj, method, replacement):
+    orig_handler = getattr(obj, method)
+
+    def wrapped(*a, **kwargs):
+        kwargs['orig_handler'] = orig_handler
+        return replacement(*a, **kwargs)
+    setattr(obj, method, types.MethodType(wrapped, obj))
+
+class PlaterPanel(wx.Panel):
+    def __init__(self, **kwargs):
+        self.destroy_on_done = False
+        parent = kwargs.get("parent", None)
+        super(PlaterPanel, self).__init__(parent = parent)
+        self.prepare_ui(**kwargs)
+
+    def prepare_ui(self, filenames = [], callback = None, parent = None, build_dimensions = None):
+        self.filenames = filenames
+        self.mainsizer = wx.BoxSizer(wx.HORIZONTAL)
+        panel = self.menupanel = wx.Panel(self, -1)
+        sizer = self.menusizer = wx.GridBagSizer()
+        self.l = wx.ListBox(panel)
+        sizer.Add(self.l, pos = (1, 0), span = (1, 2), flag = wx.EXPAND)
+        sizer.AddGrowableRow(1, 1)
+        # Clear button
+        clearbutton = wx.Button(panel, label = _("Clear"))
+        clearbutton.Bind(wx.EVT_BUTTON, self.clear)
+        sizer.Add(clearbutton, pos = (2, 0), span = (1, 2), flag = wx.EXPAND)
+        # Load button
+        loadbutton = wx.Button(panel, label = _("Load"))
+        loadbutton.Bind(wx.EVT_BUTTON, self.load)
+        sizer.Add(loadbutton, pos = (0, 0), span = (1, 1), flag = wx.EXPAND)
+        # Snap to Z = 0 button
+        snapbutton = wx.Button(panel, label = _("Snap to Z = 0"))
+        snapbutton.Bind(wx.EVT_BUTTON, self.snap)
+        sizer.Add(snapbutton, pos = (3, 0), span = (1, 1), flag = wx.EXPAND)
+        # Put at center button
+        centerbutton = wx.Button(panel, label = _("Put at center"))
+        centerbutton.Bind(wx.EVT_BUTTON, self.center)
+        sizer.Add(centerbutton, pos = (3, 1), span = (1, 1), flag = wx.EXPAND)
+        # Delete button
+        deletebutton = wx.Button(panel, label = _("Delete"))
+        deletebutton.Bind(wx.EVT_BUTTON, self.delete)
+        sizer.Add(deletebutton, pos = (4, 0), span = (1, 1), flag = wx.EXPAND)
+        # Auto arrange button
+        autobutton = wx.Button(panel, label = _("Auto arrange"))
+        autobutton.Bind(wx.EVT_BUTTON, self.autoplate)
+        sizer.Add(autobutton, pos = (5, 0), span = (1, 2), flag = wx.EXPAND)
+        # Export button
+        exportbutton = wx.Button(panel, label = _("Export"))
+        exportbutton.Bind(wx.EVT_BUTTON, self.export)
+        sizer.Add(exportbutton, pos = (0, 1), span = (1, 1), flag = wx.EXPAND)
+        if callback is not None:
+            donebutton = wx.Button(panel, label = _("Done"))
+            donebutton.Bind(wx.EVT_BUTTON, lambda e: self.done(e, callback))
+            sizer.Add(donebutton, pos = (6, 0), span = (1, 1), flag = wx.EXPAND)
+            cancelbutton = wx.Button(panel, label = _("Cancel"))
+            cancelbutton.Bind(wx.EVT_BUTTON, lambda e: self.Destroy())
+            sizer.Add(cancelbutton, pos = (6, 1), span = (1, 1), flag = wx.EXPAND)
+        self.basedir = "."
+        self.models = {}
+        panel.SetSizerAndFit(sizer)
+        self.mainsizer.Add(panel, flag = wx.EXPAND)
+        self.SetSizer(self.mainsizer)
+        if build_dimensions:
+            self.build_dimensions = build_dimensions
+        else:
+            self.build_dimensions = [200, 200, 100, 0, 0, 0]
+
+    def set_viewer(self, viewer):
+        # Patch handle_rotation on the fly
+        if hasattr(viewer, "handle_rotation"):
+            def handle_rotation(self, event, orig_handler):
+                if self.initpos is None:
+                    self.initpos = event.GetPositionTuple()
+                else:
+                    if event.ShiftDown():
+                        p1 = self.initpos
+                        p2 = event.GetPositionTuple()
+                        x1, y1, _ = self.mouse_to_3d(p1[0], p1[1])
+                        x2, y2, _ = self.mouse_to_3d(p2[0], p2[1])
+                        self.parent.move_shape((x2 - x1, y2 - y1))
+                        self.initpos = p2
+                    else:
+                        orig_handler(event)
+            patch_method(viewer, "handle_rotation", handle_rotation)
+        # Patch handle_wheel on the fly
+        if hasattr(viewer, "handle_wheel"):
+            def handle_wheel(self, event, orig_handler):
+                if event.ShiftDown():
+                    delta = event.GetWheelRotation()
+                    angle = 10
+                    if delta > 0:
+                        self.parent.rotate_shape(angle / 2)
+                    else:
+                        self.parent.rotate_shape(-angle / 2)
+                else:
+                    orig_handler(event)
+            patch_method(viewer, "handle_wheel", handle_wheel)
+        self.s = viewer
+        self.mainsizer.Add(self.s, 1, wx.EXPAND)
+
+    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.l.GetSelection()
+        if name == wx.NOT_FOUND:
+            return False
+
+        name = self.l.GetString(name)
+
+        model = self.models[name]
+        model.offsets = [model.offsets[0] + delta[0],
+                         model.offsets[1] + delta[1],
+                         model.offsets[2]
+                         ]
+        return True
+
+    def rotate_shape(self, angle):
+        """rotates acive shape
+        positive angle is clockwise
+        """
+        name = self.l.GetSelection()
+        if name == wx.NOT_FOUND:
+            return False
+        name = self.l.GetString(name)
+        model = self.models[name]
+        model.rot += angle
+
+    def autoplate(self, event = None):
+        logging.info(_("Autoplating"))
+        separation = 2
+        try:
+            from printrun import packer
+            p = packer.Packer()
+            for i in self.models:
+                width = abs(self.models[i].dims[0] - self.models[i].dims[1])
+                height = abs(self.models[i].dims[2] - self.models[i].dims[3])
+                p.add_rect(width, height, data = i)
+            centerx = self.build_dimensions[0] / 2 + self.build_dimensions[3]
+            centery = self.build_dimensions[1] / 2 + self.build_dimensions[4]
+            rects = p.pack(padding = separation,
+                           center = packer.Vector2(centerx, centery))
+            for rect in rects:
+                i = rect.data
+                position = rect.center()
+                self.models[i].offsets[0] = position.x
+                self.models[i].offsets[1] = position.y
+        except ImportError:
+            bedsize = self.build_dimensions[0:3]
+            cursor = [0, 0, 0]
+            newrow = 0
+            max = [0, 0]
+            for i in self.models:
+                self.models[i].offsets[2] = -1.0 * self.models[i].dims[4]
+                x = abs(self.models[i].dims[0] - self.models[i].dims[1])
+                y = abs(self.models[i].dims[2] - self.models[i].dims[3])
+                centre = [x / 2, y / 2]
+                centreoffset = [self.models[i].dims[0] + centre[0],
+                                self.models[i].dims[2] + centre[1]]
+                if (cursor[0] + x + separation) >= bedsize[0]:
+                    cursor[0] = 0
+                    cursor[1] += newrow + separation
+                    newrow = 0
+                if (newrow == 0) or (newrow < y):
+                    newrow = y
+                # To the person who works out why the offsets are applied
+                # differently here:
+                #    Good job, it confused the hell out of me.
+                self.models[i].offsets[0] = cursor[0] + centre[0] - centreoffset[0]
+                self.models[i].offsets[1] = cursor[1] + centre[1] - centreoffset[1]
+                if (max[0] == 0) or (max[0] < (cursor[0] + x)):
+                    max[0] = cursor[0] + x
+                if (max[1] == 0) or (max[1] < (cursor[1] + x)):
+                    max[1] = cursor[1] + x
+                cursor[0] += x + separation
+                if (cursor[1] + y) >= bedsize[1]:
+                    logging.info(_("Bed full, sorry sir :("))
+                    self.Refresh()
+                    return
+            centerx = self.build_dimensions[0] / 2 + self.build_dimensions[3]
+            centery = self.build_dimensions[1] / 2 + self.build_dimensions[4]
+            centreoffset = [centerx - max[0] / 2, centery - max[1] / 2]
+            for i in self.models:
+                self.models[i].offsets[0] += centreoffset[0]
+                self.models[i].offsets[1] += centreoffset[1]
+        self.Refresh()
+
+    def clear(self, event):
+        result = wx.MessageBox(_('Are you sure you want to clear the grid? All unsaved changes will be lost.'),
+                               _('Clear the grid?'),
+                               wx.YES_NO | wx.ICON_QUESTION)
+        if result == 2:
+            self.models = {}
+            self.l.Clear()
+            self.Refresh()
+
+    def center(self, event):
+        i = self.l.GetSelection()
+        if i != -1:
+            m = self.models[self.l.GetString(i)]
+            centerx = self.build_dimensions[0] / 2 + self.build_dimensions[3]
+            centery = self.build_dimensions[1] / 2 + self.build_dimensions[4]
+            m.offsets = [centerx, centery, m.offsets[2]]
+            self.Refresh()
+
+    def snap(self, event):
+        i = self.l.GetSelection()
+        if i != -1:
+            m = self.models[self.l.GetString(i)]
+            m.offsets[2] = -m.dims[4]
+            self.Refresh()
+
+    def delete(self, event):
+        i = self.l.GetSelection()
+        if i != -1:
+            del self.models[self.l.GetString(i)]
+            self.l.Delete(i)
+            self.l.Select(self.l.GetCount() - 1)
+            self.Refresh()
+
+    def add_model(self, name, model):
+        newname = os.path.split(name.lower())[1]
+        if not isinstance(newname, unicode):
+            newname = unicode(newname, "utf-8")
+        c = 1
+        while newname in self.models:
+            newname = os.path.split(name.lower())[1]
+            newname = newname + "(%d)" % c
+            c += 1
+        self.models[newname] = model
+
+        self.l.Append(newname)
+        i = self.l.GetSelection()
+        if i == wx.NOT_FOUND:
+            self.l.Select(0)
+
+        self.l.Select(self.l.GetCount() - 1)
+
+    def load(self, event):
+        dlg = wx.FileDialog(self, _("Pick file to load"), self.basedir, style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
+        dlg.SetWildcard(self.load_wildcard)
+        if dlg.ShowModal() == wx.ID_OK:
+            name = dlg.GetPath()
+            self.load_file(name)
+        dlg.Destroy()
+
+    def load_file(self, filename):
+        raise NotImplementedError
+
+    def export(self, event):
+        dlg = wx.FileDialog(self, _("Pick file to save to"), self.basedir, style = wx.FD_SAVE)
+        dlg.SetWildcard(self.save_wildcard)
+        if dlg.ShowModal() == wx.ID_OK:
+            name = dlg.GetPath()
+            self.export_to(name)
+        dlg.Destroy()
+
+    def export_to(self, name):
+        raise NotImplementedError
+
+class Plater(wx.Frame):
+    def __init__(self, **kwargs):
+        self.destroy_on_done = True
+        parent = kwargs.get("parent", None)
+        size = kwargs.get("size", (800, 580))
+        if "size" in kwargs:
+            del kwargs["size"]
+        wx.Frame.__init__(self, parent, title = _("Plate building tool"), size = size)
+        self.SetIcon(wx.Icon(iconfile("plater.png"), wx.BITMAP_TYPE_PNG))
+        self.prepare_ui(**kwargs)
+
+def make_plater(panel_class):
+    name = panel_class.__name__.replace("Panel", "")
+    return type(name, (Plater, panel_class), {})

mercurial