Sat, 23 Sep 2017 08:51:58 +0200
Added support for multiple cutting passes with automatic Z refocusing
15 | 1 | #!/usr/bin/env python |
2 | ||
3 | # This file is part of the Printrun suite. | |
4 | # | |
5 | # Printrun is free software: you can redistribute it and/or modify | |
6 | # it under the terms of the GNU General Public License as published by | |
7 | # the Free Software Foundation, either version 3 of the License, or | |
8 | # (at your option) any later version. | |
9 | # | |
10 | # Printrun is distributed in the hope that it will be useful, | |
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | # GNU General Public License for more details. | |
14 | # | |
15 | # You should have received a copy of the GNU General Public License | |
16 | # along with Printrun. If not, see <http://www.gnu.org/licenses/>. | |
17 | ||
18 | from .utils import install_locale, iconfile | |
19 | install_locale('plater') | |
20 | ||
21 | import logging | |
22 | import os | |
23 | import types | |
24 | import wx | |
25 | ||
26 | def patch_method(obj, method, replacement): | |
27 | orig_handler = getattr(obj, method) | |
28 | ||
29 | def wrapped(*a, **kwargs): | |
30 | kwargs['orig_handler'] = orig_handler | |
31 | return replacement(*a, **kwargs) | |
32 | setattr(obj, method, types.MethodType(wrapped, obj)) | |
33 | ||
34 | class PlaterPanel(wx.Panel): | |
35 | def __init__(self, **kwargs): | |
36 | self.destroy_on_done = False | |
37 | parent = kwargs.get("parent", None) | |
38 | super(PlaterPanel, self).__init__(parent = parent) | |
39 | self.prepare_ui(**kwargs) | |
40 | ||
41 | def prepare_ui(self, filenames = [], callback = None, parent = None, build_dimensions = None): | |
42 | self.filenames = filenames | |
43 | self.mainsizer = wx.BoxSizer(wx.HORIZONTAL) | |
44 | panel = self.menupanel = wx.Panel(self, -1) | |
45 | sizer = self.menusizer = wx.GridBagSizer() | |
46 | self.l = wx.ListBox(panel) | |
47 | sizer.Add(self.l, pos = (1, 0), span = (1, 2), flag = wx.EXPAND) | |
48 | sizer.AddGrowableRow(1, 1) | |
49 | # Clear button | |
50 | clearbutton = wx.Button(panel, label = _("Clear")) | |
51 | clearbutton.Bind(wx.EVT_BUTTON, self.clear) | |
52 | sizer.Add(clearbutton, pos = (2, 0), span = (1, 2), flag = wx.EXPAND) | |
53 | # Load button | |
54 | loadbutton = wx.Button(panel, label = _("Load")) | |
55 | loadbutton.Bind(wx.EVT_BUTTON, self.load) | |
56 | sizer.Add(loadbutton, pos = (0, 0), span = (1, 1), flag = wx.EXPAND) | |
57 | # Snap to Z = 0 button | |
58 | snapbutton = wx.Button(panel, label = _("Snap to Z = 0")) | |
59 | snapbutton.Bind(wx.EVT_BUTTON, self.snap) | |
60 | sizer.Add(snapbutton, pos = (3, 0), span = (1, 1), flag = wx.EXPAND) | |
61 | # Put at center button | |
62 | centerbutton = wx.Button(panel, label = _("Put at center")) | |
63 | centerbutton.Bind(wx.EVT_BUTTON, self.center) | |
64 | sizer.Add(centerbutton, pos = (3, 1), span = (1, 1), flag = wx.EXPAND) | |
65 | # Delete button | |
66 | deletebutton = wx.Button(panel, label = _("Delete")) | |
67 | deletebutton.Bind(wx.EVT_BUTTON, self.delete) | |
68 | sizer.Add(deletebutton, pos = (4, 0), span = (1, 1), flag = wx.EXPAND) | |
69 | # Auto arrange button | |
70 | autobutton = wx.Button(panel, label = _("Auto arrange")) | |
71 | autobutton.Bind(wx.EVT_BUTTON, self.autoplate) | |
72 | sizer.Add(autobutton, pos = (5, 0), span = (1, 2), flag = wx.EXPAND) | |
73 | # Export button | |
74 | exportbutton = wx.Button(panel, label = _("Export")) | |
75 | exportbutton.Bind(wx.EVT_BUTTON, self.export) | |
76 | sizer.Add(exportbutton, pos = (0, 1), span = (1, 1), flag = wx.EXPAND) | |
77 | if callback is not None: | |
78 | donebutton = wx.Button(panel, label = _("Done")) | |
79 | donebutton.Bind(wx.EVT_BUTTON, lambda e: self.done(e, callback)) | |
80 | sizer.Add(donebutton, pos = (6, 0), span = (1, 1), flag = wx.EXPAND) | |
81 | cancelbutton = wx.Button(panel, label = _("Cancel")) | |
82 | cancelbutton.Bind(wx.EVT_BUTTON, lambda e: self.Destroy()) | |
83 | sizer.Add(cancelbutton, pos = (6, 1), span = (1, 1), flag = wx.EXPAND) | |
84 | self.basedir = "." | |
85 | self.models = {} | |
86 | panel.SetSizerAndFit(sizer) | |
87 | self.mainsizer.Add(panel, flag = wx.EXPAND) | |
88 | self.SetSizer(self.mainsizer) | |
89 | if build_dimensions: | |
90 | self.build_dimensions = build_dimensions | |
91 | else: | |
92 | self.build_dimensions = [200, 200, 100, 0, 0, 0] | |
93 | ||
94 | def set_viewer(self, viewer): | |
95 | # Patch handle_rotation on the fly | |
96 | if hasattr(viewer, "handle_rotation"): | |
97 | def handle_rotation(self, event, orig_handler): | |
98 | if self.initpos is None: | |
99 | self.initpos = event.GetPositionTuple() | |
100 | else: | |
101 | if event.ShiftDown(): | |
102 | p1 = self.initpos | |
103 | p2 = event.GetPositionTuple() | |
104 | x1, y1, _ = self.mouse_to_3d(p1[0], p1[1]) | |
105 | x2, y2, _ = self.mouse_to_3d(p2[0], p2[1]) | |
106 | self.parent.move_shape((x2 - x1, y2 - y1)) | |
107 | self.initpos = p2 | |
108 | else: | |
109 | orig_handler(event) | |
110 | patch_method(viewer, "handle_rotation", handle_rotation) | |
111 | # Patch handle_wheel on the fly | |
112 | if hasattr(viewer, "handle_wheel"): | |
113 | def handle_wheel(self, event, orig_handler): | |
114 | if event.ShiftDown(): | |
115 | delta = event.GetWheelRotation() | |
116 | angle = 10 | |
117 | if delta > 0: | |
118 | self.parent.rotate_shape(angle / 2) | |
119 | else: | |
120 | self.parent.rotate_shape(-angle / 2) | |
121 | else: | |
122 | orig_handler(event) | |
123 | patch_method(viewer, "handle_wheel", handle_wheel) | |
124 | self.s = viewer | |
125 | self.mainsizer.Add(self.s, 1, wx.EXPAND) | |
126 | ||
127 | def move_shape(self, delta): | |
128 | """moves shape (selected in l, which is list ListBox of shapes) | |
129 | by an offset specified in tuple delta. | |
130 | Positive numbers move to (rigt, down)""" | |
131 | name = self.l.GetSelection() | |
132 | if name == wx.NOT_FOUND: | |
133 | return False | |
134 | ||
135 | name = self.l.GetString(name) | |
136 | ||
137 | model = self.models[name] | |
138 | model.offsets = [model.offsets[0] + delta[0], | |
139 | model.offsets[1] + delta[1], | |
140 | model.offsets[2] | |
141 | ] | |
142 | return True | |
143 | ||
144 | def rotate_shape(self, angle): | |
145 | """rotates acive shape | |
146 | positive angle is clockwise | |
147 | """ | |
148 | name = self.l.GetSelection() | |
149 | if name == wx.NOT_FOUND: | |
150 | return False | |
151 | name = self.l.GetString(name) | |
152 | model = self.models[name] | |
153 | model.rot += angle | |
154 | ||
155 | def autoplate(self, event = None): | |
156 | logging.info(_("Autoplating")) | |
157 | separation = 2 | |
158 | try: | |
159 | from printrun import packer | |
160 | p = packer.Packer() | |
161 | for i in self.models: | |
162 | width = abs(self.models[i].dims[0] - self.models[i].dims[1]) | |
163 | height = abs(self.models[i].dims[2] - self.models[i].dims[3]) | |
164 | p.add_rect(width, height, data = i) | |
165 | centerx = self.build_dimensions[0] / 2 + self.build_dimensions[3] | |
166 | centery = self.build_dimensions[1] / 2 + self.build_dimensions[4] | |
167 | rects = p.pack(padding = separation, | |
168 | center = packer.Vector2(centerx, centery)) | |
169 | for rect in rects: | |
170 | i = rect.data | |
171 | position = rect.center() | |
172 | self.models[i].offsets[0] = position.x | |
173 | self.models[i].offsets[1] = position.y | |
174 | except ImportError: | |
175 | bedsize = self.build_dimensions[0:3] | |
176 | cursor = [0, 0, 0] | |
177 | newrow = 0 | |
178 | max = [0, 0] | |
179 | for i in self.models: | |
180 | self.models[i].offsets[2] = -1.0 * self.models[i].dims[4] | |
181 | x = abs(self.models[i].dims[0] - self.models[i].dims[1]) | |
182 | y = abs(self.models[i].dims[2] - self.models[i].dims[3]) | |
183 | centre = [x / 2, y / 2] | |
184 | centreoffset = [self.models[i].dims[0] + centre[0], | |
185 | self.models[i].dims[2] + centre[1]] | |
186 | if (cursor[0] + x + separation) >= bedsize[0]: | |
187 | cursor[0] = 0 | |
188 | cursor[1] += newrow + separation | |
189 | newrow = 0 | |
190 | if (newrow == 0) or (newrow < y): | |
191 | newrow = y | |
192 | # To the person who works out why the offsets are applied | |
193 | # differently here: | |
194 | # Good job, it confused the hell out of me. | |
195 | self.models[i].offsets[0] = cursor[0] + centre[0] - centreoffset[0] | |
196 | self.models[i].offsets[1] = cursor[1] + centre[1] - centreoffset[1] | |
197 | if (max[0] == 0) or (max[0] < (cursor[0] + x)): | |
198 | max[0] = cursor[0] + x | |
199 | if (max[1] == 0) or (max[1] < (cursor[1] + x)): | |
200 | max[1] = cursor[1] + x | |
201 | cursor[0] += x + separation | |
202 | if (cursor[1] + y) >= bedsize[1]: | |
203 | logging.info(_("Bed full, sorry sir :(")) | |
204 | self.Refresh() | |
205 | return | |
206 | centerx = self.build_dimensions[0] / 2 + self.build_dimensions[3] | |
207 | centery = self.build_dimensions[1] / 2 + self.build_dimensions[4] | |
208 | centreoffset = [centerx - max[0] / 2, centery - max[1] / 2] | |
209 | for i in self.models: | |
210 | self.models[i].offsets[0] += centreoffset[0] | |
211 | self.models[i].offsets[1] += centreoffset[1] | |
212 | self.Refresh() | |
213 | ||
214 | def clear(self, event): | |
215 | result = wx.MessageBox(_('Are you sure you want to clear the grid? All unsaved changes will be lost.'), | |
216 | _('Clear the grid?'), | |
217 | wx.YES_NO | wx.ICON_QUESTION) | |
218 | if result == 2: | |
219 | self.models = {} | |
220 | self.l.Clear() | |
221 | self.Refresh() | |
222 | ||
223 | def center(self, event): | |
224 | i = self.l.GetSelection() | |
225 | if i != -1: | |
226 | m = self.models[self.l.GetString(i)] | |
227 | centerx = self.build_dimensions[0] / 2 + self.build_dimensions[3] | |
228 | centery = self.build_dimensions[1] / 2 + self.build_dimensions[4] | |
229 | m.offsets = [centerx, centery, m.offsets[2]] | |
230 | self.Refresh() | |
231 | ||
232 | def snap(self, event): | |
233 | i = self.l.GetSelection() | |
234 | if i != -1: | |
235 | m = self.models[self.l.GetString(i)] | |
236 | m.offsets[2] = -m.dims[4] | |
237 | self.Refresh() | |
238 | ||
239 | def delete(self, event): | |
240 | i = self.l.GetSelection() | |
241 | if i != -1: | |
242 | del self.models[self.l.GetString(i)] | |
243 | self.l.Delete(i) | |
244 | self.l.Select(self.l.GetCount() - 1) | |
245 | self.Refresh() | |
246 | ||
247 | def add_model(self, name, model): | |
248 | newname = os.path.split(name.lower())[1] | |
249 | if not isinstance(newname, unicode): | |
250 | newname = unicode(newname, "utf-8") | |
251 | c = 1 | |
252 | while newname in self.models: | |
253 | newname = os.path.split(name.lower())[1] | |
254 | newname = newname + "(%d)" % c | |
255 | c += 1 | |
256 | self.models[newname] = model | |
257 | ||
258 | self.l.Append(newname) | |
259 | i = self.l.GetSelection() | |
260 | if i == wx.NOT_FOUND: | |
261 | self.l.Select(0) | |
262 | ||
263 | self.l.Select(self.l.GetCount() - 1) | |
264 | ||
265 | def load(self, event): | |
266 | dlg = wx.FileDialog(self, _("Pick file to load"), self.basedir, style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) | |
267 | dlg.SetWildcard(self.load_wildcard) | |
268 | if dlg.ShowModal() == wx.ID_OK: | |
269 | name = dlg.GetPath() | |
270 | self.load_file(name) | |
271 | dlg.Destroy() | |
272 | ||
273 | def load_file(self, filename): | |
274 | raise NotImplementedError | |
275 | ||
276 | def export(self, event): | |
277 | dlg = wx.FileDialog(self, _("Pick file to save to"), self.basedir, style = wx.FD_SAVE) | |
278 | dlg.SetWildcard(self.save_wildcard) | |
279 | if dlg.ShowModal() == wx.ID_OK: | |
280 | name = dlg.GetPath() | |
281 | self.export_to(name) | |
282 | dlg.Destroy() | |
283 | ||
284 | def export_to(self, name): | |
285 | raise NotImplementedError | |
286 | ||
287 | class Plater(wx.Frame): | |
288 | def __init__(self, **kwargs): | |
289 | self.destroy_on_done = True | |
290 | parent = kwargs.get("parent", None) | |
291 | size = kwargs.get("size", (800, 580)) | |
292 | if "size" in kwargs: | |
293 | del kwargs["size"] | |
294 | wx.Frame.__init__(self, parent, title = _("Plate building tool"), size = size) | |
295 | self.SetIcon(wx.Icon(iconfile("plater.png"), wx.BITMAP_TYPE_PNG)) | |
296 | self.prepare_ui(**kwargs) | |
297 | ||
298 | def make_plater(panel_class): | |
299 | name = panel_class.__name__.replace("Panel", "") | |
300 | return type(name, (Plater, panel_class), {}) |