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