|
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 import os |
|
19 |
|
20 # Set up Internationalization using gettext |
|
21 # searching for installed locales on /usr/share; uses relative folder if not found (windows) |
|
22 from .utils import install_locale |
|
23 install_locale('pronterface') |
|
24 |
|
25 import wx |
|
26 import time |
|
27 import logging |
|
28 import threading |
|
29 import math |
|
30 import sys |
|
31 import re |
|
32 import traceback |
|
33 import subprocess |
|
34 from copy import copy |
|
35 |
|
36 from printrun import stltool |
|
37 from printrun.objectplater import make_plater, PlaterPanel |
|
38 |
|
39 glview = False |
|
40 if "-nogl" not in sys.argv: |
|
41 try: |
|
42 from printrun import stlview |
|
43 glview = True |
|
44 except: |
|
45 logging.warning("Could not load 3D viewer for plater:" |
|
46 + "\n" + traceback.format_exc()) |
|
47 |
|
48 |
|
49 def evalme(s): |
|
50 return eval(s[s.find("(") + 1:s.find(")")]) |
|
51 |
|
52 def transformation_matrix(model): |
|
53 matrix = stltool.I |
|
54 if any(model.centeroffset): |
|
55 matrix = model.translation_matrix(model.centeroffset).dot(matrix) |
|
56 if model.rot: |
|
57 matrix = model.rotation_matrix([0, 0, model.rot]).dot(matrix) |
|
58 if any(model.offsets): |
|
59 matrix = model.translation_matrix(model.offsets).dot(matrix) |
|
60 return matrix |
|
61 |
|
62 class showstl(wx.Window): |
|
63 def __init__(self, parent, size, pos): |
|
64 wx.Window.__init__(self, parent, size = size, pos = pos) |
|
65 self.i = 0 |
|
66 self.parent = parent |
|
67 self.previ = 0 |
|
68 self.Bind(wx.EVT_MOUSEWHEEL, self.rot) |
|
69 self.Bind(wx.EVT_MOUSE_EVENTS, self.move) |
|
70 self.Bind(wx.EVT_PAINT, self.repaint) |
|
71 self.Bind(wx.EVT_KEY_DOWN, self.keypress) |
|
72 self.triggered = 0 |
|
73 self.initpos = None |
|
74 self.prevsel = -1 |
|
75 |
|
76 def prepare_model(self, m, scale): |
|
77 m.bitmap = wx.EmptyBitmap(800, 800, 32) |
|
78 dc = wx.MemoryDC() |
|
79 dc.SelectObject(m.bitmap) |
|
80 dc.SetBackground(wx.Brush((0, 0, 0, 0))) |
|
81 dc.SetBrush(wx.Brush((0, 0, 0, 255))) |
|
82 dc.SetBrush(wx.Brush(wx.Colour(128, 255, 128))) |
|
83 dc.SetPen(wx.Pen(wx.Colour(128, 128, 128))) |
|
84 for i in m.facets: |
|
85 dc.DrawPolygon([wx.Point(400 + scale * p[0], (400 - scale * p[1])) for p in i[1]]) |
|
86 dc.SelectObject(wx.NullBitmap) |
|
87 m.bitmap.SetMask(wx.Mask(m.bitmap, wx.Colour(0, 0, 0, 255))) |
|
88 |
|
89 def move_shape(self, delta): |
|
90 """moves shape (selected in l, which is list ListBox of shapes) |
|
91 by an offset specified in tuple delta. |
|
92 Positive numbers move to (rigt, down)""" |
|
93 name = self.parent.l.GetSelection() |
|
94 if name == wx.NOT_FOUND: |
|
95 return False |
|
96 name = self.parent.l.GetString(name) |
|
97 model = self.parent.models[name] |
|
98 model.offsets = [model.offsets[0] + delta[0], |
|
99 model.offsets[1] + delta[1], |
|
100 model.offsets[2] |
|
101 ] |
|
102 self.Refresh() |
|
103 return True |
|
104 |
|
105 def move(self, event): |
|
106 if event.ButtonUp(wx.MOUSE_BTN_LEFT): |
|
107 if self.initpos is not None: |
|
108 currentpos = event.GetPositionTuple() |
|
109 delta = (0.5 * (currentpos[0] - self.initpos[0]), |
|
110 -0.5 * (currentpos[1] - self.initpos[1]) |
|
111 ) |
|
112 self.move_shape(delta) |
|
113 self.Refresh() |
|
114 self.initpos = None |
|
115 elif event.ButtonDown(wx.MOUSE_BTN_RIGHT): |
|
116 self.parent.right(event) |
|
117 elif event.Dragging(): |
|
118 if self.initpos is None: |
|
119 self.initpos = event.GetPositionTuple() |
|
120 self.Refresh() |
|
121 dc = wx.ClientDC(self) |
|
122 p = event.GetPositionTuple() |
|
123 dc.DrawLine(self.initpos[0], self.initpos[1], p[0], p[1]) |
|
124 del dc |
|
125 else: |
|
126 event.Skip() |
|
127 |
|
128 def rotate_shape(self, angle): |
|
129 """rotates acive shape |
|
130 positive angle is clockwise |
|
131 """ |
|
132 self.i += angle |
|
133 if not self.triggered: |
|
134 self.triggered = 1 |
|
135 threading.Thread(target = self.cr).start() |
|
136 |
|
137 def keypress(self, event): |
|
138 """gets keypress events and moves/rotates acive shape""" |
|
139 keycode = event.GetKeyCode() |
|
140 step = 5 |
|
141 angle = 18 |
|
142 if event.ControlDown(): |
|
143 step = 1 |
|
144 angle = 1 |
|
145 # h |
|
146 if keycode == 72: |
|
147 self.move_shape((-step, 0)) |
|
148 # l |
|
149 if keycode == 76: |
|
150 self.move_shape((step, 0)) |
|
151 # j |
|
152 if keycode == 75: |
|
153 self.move_shape((0, step)) |
|
154 # k |
|
155 if keycode == 74: |
|
156 self.move_shape((0, -step)) |
|
157 # [ |
|
158 if keycode == 91: |
|
159 self.rotate_shape(-angle) |
|
160 # ] |
|
161 if keycode == 93: |
|
162 self.rotate_shape(angle) |
|
163 event.Skip() |
|
164 |
|
165 def rotateafter(self): |
|
166 if self.i != self.previ: |
|
167 i = self.parent.l.GetSelection() |
|
168 if i != wx.NOT_FOUND: |
|
169 self.parent.models[self.parent.l.GetString(i)].rot -= 5 * (self.i - self.previ) |
|
170 self.previ = self.i |
|
171 self.Refresh() |
|
172 |
|
173 def cr(self): |
|
174 time.sleep(0.01) |
|
175 wx.CallAfter(self.rotateafter) |
|
176 self.triggered = 0 |
|
177 |
|
178 def rot(self, event): |
|
179 z = event.GetWheelRotation() |
|
180 s = self.parent.l.GetSelection() |
|
181 if self.prevsel != s: |
|
182 self.i = 0 |
|
183 self.prevsel = s |
|
184 if z < 0: |
|
185 self.rotate_shape(-1) |
|
186 else: |
|
187 self.rotate_shape(1) |
|
188 |
|
189 def repaint(self, event): |
|
190 dc = wx.PaintDC(self) |
|
191 self.paint(dc = dc) |
|
192 |
|
193 def paint(self, coord1 = "x", coord2 = "y", dc = None): |
|
194 if dc is None: |
|
195 dc = wx.ClientDC(self) |
|
196 scale = 2 |
|
197 dc.SetPen(wx.Pen(wx.Colour(100, 100, 100))) |
|
198 for i in xrange(20): |
|
199 dc.DrawLine(0, i * scale * 10, 400, i * scale * 10) |
|
200 dc.DrawLine(i * scale * 10, 0, i * scale * 10, 400) |
|
201 dc.SetPen(wx.Pen(wx.Colour(0, 0, 0))) |
|
202 for i in xrange(4): |
|
203 dc.DrawLine(0, i * scale * 50, 400, i * scale * 50) |
|
204 dc.DrawLine(i * scale * 50, 0, i * scale * 50, 400) |
|
205 dc.SetBrush(wx.Brush(wx.Colour(128, 255, 128))) |
|
206 dc.SetPen(wx.Pen(wx.Colour(128, 128, 128))) |
|
207 dcs = wx.MemoryDC() |
|
208 for m in self.parent.models.values(): |
|
209 b = m.bitmap |
|
210 im = b.ConvertToImage() |
|
211 imgc = wx.Point(im.GetWidth() / 2, im.GetHeight() / 2) |
|
212 im = im.Rotate(math.radians(m.rot), imgc, 0) |
|
213 bm = wx.BitmapFromImage(im) |
|
214 dcs.SelectObject(bm) |
|
215 bsz = bm.GetSize() |
|
216 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) |
|
217 del dc |
|
218 |
|
219 class StlPlaterPanel(PlaterPanel): |
|
220 |
|
221 load_wildcard = _("STL files (*.stl;*.STL)|*.stl;*.STL|OpenSCAD files (*.scad)|*.scad") |
|
222 save_wildcard = _("STL files (*.stl;*.STL)|*.stl;*.STL") |
|
223 |
|
224 def prepare_ui(self, filenames = [], callback = None, |
|
225 parent = None, build_dimensions = None, circular_platform = False, |
|
226 simarrange_path = None, antialias_samples = 0): |
|
227 super(StlPlaterPanel, self).prepare_ui(filenames, callback, parent, build_dimensions) |
|
228 self.cutting = False |
|
229 self.cutting_axis = None |
|
230 self.cutting_dist = None |
|
231 if glview: |
|
232 viewer = stlview.StlViewPanel(self, (580, 580), |
|
233 build_dimensions = self.build_dimensions, |
|
234 circular = circular_platform, |
|
235 antialias_samples = antialias_samples) |
|
236 # Cutting tool |
|
237 nrows = self.menusizer.GetRows() |
|
238 self.menusizer.Add(wx.StaticText(self.menupanel, -1, _("Cut along:")), |
|
239 pos = (nrows, 0), span = (1, 1), flag = wx.ALIGN_CENTER) |
|
240 cutconfirmbutton = wx.Button(self.menupanel, label = _("Confirm cut")) |
|
241 cutconfirmbutton.Bind(wx.EVT_BUTTON, self.cut_confirm) |
|
242 cutconfirmbutton.Disable() |
|
243 self.cutconfirmbutton = cutconfirmbutton |
|
244 self.menusizer.Add(cutconfirmbutton, pos = (nrows, 1), span = (1, 1), flag = wx.EXPAND) |
|
245 cutpanel = wx.Panel(self.menupanel, -1) |
|
246 cutsizer = self.cutsizer = wx.BoxSizer(wx.HORIZONTAL) |
|
247 cutpanel.SetSizer(cutsizer) |
|
248 cutxplusbutton = wx.ToggleButton(cutpanel, label = _(">X"), style = wx.BU_EXACTFIT) |
|
249 cutxplusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "x", 1)) |
|
250 cutsizer.Add(cutxplusbutton, 1, flag = wx.EXPAND) |
|
251 cutzplusbutton = wx.ToggleButton(cutpanel, label = _(">Y"), style = wx.BU_EXACTFIT) |
|
252 cutzplusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "y", 1)) |
|
253 cutsizer.Add(cutzplusbutton, 1, flag = wx.EXPAND) |
|
254 cutzplusbutton = wx.ToggleButton(cutpanel, label = _(">Z"), style = wx.BU_EXACTFIT) |
|
255 cutzplusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "z", 1)) |
|
256 cutsizer.Add(cutzplusbutton, 1, flag = wx.EXPAND) |
|
257 cutxminusbutton = wx.ToggleButton(cutpanel, label = _("<X"), style = wx.BU_EXACTFIT) |
|
258 cutxminusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "x", -1)) |
|
259 cutsizer.Add(cutxminusbutton, 1, flag = wx.EXPAND) |
|
260 cutzminusbutton = wx.ToggleButton(cutpanel, label = _("<Y"), style = wx.BU_EXACTFIT) |
|
261 cutzminusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "y", -1)) |
|
262 cutsizer.Add(cutzminusbutton, 1, flag = wx.EXPAND) |
|
263 cutzminusbutton = wx.ToggleButton(cutpanel, label = _("<Z"), style = wx.BU_EXACTFIT) |
|
264 cutzminusbutton.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.start_cutting_tool(event, "z", -1)) |
|
265 cutsizer.Add(cutzminusbutton, 1, flag = wx.EXPAND) |
|
266 self.menusizer.Add(cutpanel, pos = (nrows + 1, 0), span = (1, 2), flag = wx.EXPAND) |
|
267 else: |
|
268 viewer = showstl(self, (580, 580), (0, 0)) |
|
269 self.simarrange_path = simarrange_path |
|
270 self.set_viewer(viewer) |
|
271 |
|
272 def start_cutting_tool(self, event, axis, direction): |
|
273 toggle = event.GetEventObject() |
|
274 if toggle.GetValue(): |
|
275 # Disable the other toggles |
|
276 for child in self.cutsizer.GetChildren(): |
|
277 child = child.GetWindow() |
|
278 if child != toggle: |
|
279 child.SetValue(False) |
|
280 self.cutting = True |
|
281 self.cutting_axis = axis |
|
282 self.cutting_dist = None |
|
283 self.cutting_direction = direction |
|
284 else: |
|
285 self.cutting = False |
|
286 self.cutting_axis = None |
|
287 self.cutting_dist = None |
|
288 self.cutting_direction = None |
|
289 |
|
290 def cut_confirm(self, event): |
|
291 name = self.l.GetSelection() |
|
292 name = self.l.GetString(name) |
|
293 model = self.models[name] |
|
294 transformation = transformation_matrix(model) |
|
295 transformed = model.transform(transformation) |
|
296 logging.info(_("Cutting %s alongside %s axis") % (name, self.cutting_axis)) |
|
297 axes = ["x", "y", "z"] |
|
298 cut = transformed.cut(axes.index(self.cutting_axis), |
|
299 self.cutting_direction, |
|
300 self.cutting_dist) |
|
301 cut.offsets = [0, 0, 0] |
|
302 cut.rot = 0 |
|
303 cut.scale = model.scale |
|
304 cut.filename = model.filename |
|
305 cut.centeroffset = [0, 0, 0] |
|
306 self.s.prepare_model(cut, 2) |
|
307 self.models[name] = cut |
|
308 self.cutconfirmbutton.Disable() |
|
309 self.cutting = False |
|
310 self.cutting_axis = None |
|
311 self.cutting_dist = None |
|
312 self.cutting_direction = None |
|
313 for child in self.cutsizer.GetChildren(): |
|
314 child = child.GetWindow() |
|
315 child.SetValue(False) |
|
316 |
|
317 def clickcb(self, event, single = False): |
|
318 if not isinstance(self.s, stlview.StlViewPanel): |
|
319 return |
|
320 if self.cutting: |
|
321 self.clickcb_cut(event) |
|
322 else: |
|
323 self.clickcb_rebase(event) |
|
324 |
|
325 def clickcb_cut(self, event): |
|
326 axis = self.cutting_axis |
|
327 self.cutting_dist, _, _ = self.s.get_cutting_plane(axis, None, |
|
328 local_transform = True) |
|
329 if self.cutting_dist is not None: |
|
330 self.cutconfirmbutton.Enable() |
|
331 |
|
332 def clickcb_rebase(self, event): |
|
333 x, y = event.GetPosition() |
|
334 ray_near, ray_far = self.s.mouse_to_ray(x, y, local_transform = True) |
|
335 best_match = None |
|
336 best_facet = None |
|
337 best_dist = float("inf") |
|
338 for key, model in self.models.iteritems(): |
|
339 transformation = transformation_matrix(model) |
|
340 transformed = model.transform(transformation) |
|
341 if not transformed.intersect_box(ray_near, ray_far): |
|
342 logging.debug("Skipping %s for rebase search" % key) |
|
343 continue |
|
344 facet, facet_dist = transformed.intersect(ray_near, ray_far) |
|
345 if facet is not None and facet_dist < best_dist: |
|
346 best_match = key |
|
347 best_facet = facet |
|
348 best_dist = facet_dist |
|
349 if best_match is not None: |
|
350 logging.info("Rebasing %s" % best_match) |
|
351 model = self.models[best_match] |
|
352 newmodel = model.rebase(best_facet) |
|
353 newmodel.offsets = list(model.offsets) |
|
354 newmodel.rot = 0 |
|
355 newmodel.scale = model.scale |
|
356 newmodel.filename = model.filename |
|
357 newmodel.centeroffset = [-(newmodel.dims[1] + newmodel.dims[0]) / 2, |
|
358 -(newmodel.dims[3] + newmodel.dims[2]) / 2, |
|
359 0] |
|
360 self.s.prepare_model(newmodel, 2) |
|
361 self.models[best_match] = newmodel |
|
362 wx.CallAfter(self.Refresh) |
|
363 |
|
364 def done(self, event, cb): |
|
365 if not os.path.exists("tempstl"): |
|
366 os.mkdir("tempstl") |
|
367 name = "tempstl/" + str(int(time.time()) % 10000) + ".stl" |
|
368 self.export_to(name) |
|
369 if cb is not None: |
|
370 cb(name) |
|
371 if self.destroy_on_done: |
|
372 self.Destroy() |
|
373 |
|
374 def load_file(self, filename): |
|
375 if filename.lower().endswith(".stl"): |
|
376 try: |
|
377 self.load_stl(filename) |
|
378 except: |
|
379 dlg = wx.MessageDialog(self, _("Loading STL file failed"), |
|
380 _("Error:") + traceback.format_exc(), |
|
381 wx.OK) |
|
382 dlg.ShowModal() |
|
383 logging.error(_("Loading STL file failed:") |
|
384 + "\n" + traceback.format_exc()) |
|
385 elif filename.lower().endswith(".scad"): |
|
386 try: |
|
387 self.load_scad(filename) |
|
388 except: |
|
389 dlg = wx.MessageDialog(self, _("Loading OpenSCAD file failed"), |
|
390 _("Error:") + traceback.format_exc(), |
|
391 wx.OK) |
|
392 dlg.ShowModal() |
|
393 logging.error(_("Loading OpenSCAD file failed:") |
|
394 + "\n" + traceback.format_exc()) |
|
395 |
|
396 def load_scad(self, name): |
|
397 lf = open(name) |
|
398 s = [i.replace("\n", "").replace("\r", "").replace(";", "") for i in lf if "stl" in i] |
|
399 lf.close() |
|
400 |
|
401 for i in s: |
|
402 parts = i.split() |
|
403 for part in parts: |
|
404 if 'translate' in part: |
|
405 translate_list = evalme(part) |
|
406 for part in parts: |
|
407 if 'rotate' in part: |
|
408 rotate_list = evalme(part) |
|
409 for part in parts: |
|
410 if 'import' in part: |
|
411 stl_file = evalme(part) |
|
412 |
|
413 newname = os.path.split(stl_file.lower())[1] |
|
414 c = 1 |
|
415 while newname in self.models: |
|
416 newname = os.path.split(stl_file.lower())[1] |
|
417 newname = newname + "(%d)" % c |
|
418 c += 1 |
|
419 stl_path = os.path.join(os.path.split(name)[0:len(os.path.split(stl_file)) - 1]) |
|
420 stl_full_path = os.path.join(stl_path[0], str(stl_file)) |
|
421 self.load_stl_into_model(stl_full_path, stl_file, translate_list, rotate_list[2]) |
|
422 |
|
423 def load_stl(self, name): |
|
424 if not os.path.exists(name): |
|
425 logging.error(_("Couldn't load non-existing file %s") % name) |
|
426 return |
|
427 path = os.path.split(name)[0] |
|
428 self.basedir = path |
|
429 if name.lower().endswith(".stl"): |
|
430 for model in self.models.values(): |
|
431 if model.filename == name: |
|
432 newmodel = copy(model) |
|
433 newmodel.offsets = list(model.offsets) |
|
434 newmodel.rot = model.rot |
|
435 newmodel.scale = list(model.scale) |
|
436 self.add_model(name, newmodel) |
|
437 self.s.prepare_model(newmodel, 2) |
|
438 break |
|
439 else: |
|
440 # Filter out the path, just show the STL filename. |
|
441 self.load_stl_into_model(name, name) |
|
442 wx.CallAfter(self.Refresh) |
|
443 |
|
444 def load_stl_into_model(self, path, name, offset = None, rotation = 0, scale = [1.0, 1.0, 1.0]): |
|
445 model = stltool.stl(path) |
|
446 if offset is None: |
|
447 offset = [self.build_dimensions[3], self.build_dimensions[4], 0] |
|
448 model.offsets = list(offset) |
|
449 model.rot = rotation |
|
450 model.scale = list(scale) |
|
451 model.filename = name |
|
452 self.add_model(name, model) |
|
453 model.centeroffset = [-(model.dims[1] + model.dims[0]) / 2, |
|
454 -(model.dims[3] + model.dims[2]) / 2, |
|
455 0] |
|
456 self.s.prepare_model(model, 2) |
|
457 |
|
458 def export_to(self, name): |
|
459 with open(name.replace(".", "_") + ".scad", "w") as sf: |
|
460 facets = [] |
|
461 for model in self.models.values(): |
|
462 r = model.rot |
|
463 o = model.offsets |
|
464 co = model.centeroffset |
|
465 sf.write("translate([%s, %s, %s])" |
|
466 "rotate([0, 0, %s])" |
|
467 "translate([%s, %s, %s])" |
|
468 "import(\"%s\");\n" % (o[0], o[1], o[2], |
|
469 r, |
|
470 co[0], co[1], co[2], |
|
471 model.filename)) |
|
472 model = model.transform(transformation_matrix(model)) |
|
473 facets += model.facets |
|
474 stltool.emitstl(name, facets, "plater_export") |
|
475 logging.info(_("Wrote plate to %s") % name) |
|
476 |
|
477 def autoplate(self, event = None): |
|
478 if self.simarrange_path: |
|
479 try: |
|
480 self.autoplate_simarrange() |
|
481 except Exception, e: |
|
482 logging.warning(_("Failed to use simarrange for plating, " |
|
483 "falling back to the standard method. " |
|
484 "The error was: ") + e) |
|
485 super(StlPlaterPanel, self).autoplate() |
|
486 else: |
|
487 super(StlPlaterPanel, self).autoplate() |
|
488 |
|
489 def autoplate_simarrange(self): |
|
490 logging.info(_("Autoplating using simarrange")) |
|
491 models = dict(self.models) |
|
492 files = [model.filename for model in models.values()] |
|
493 command = [self.simarrange_path, "--dryrun", |
|
494 "-m", # Pack around center |
|
495 "-x", str(int(self.build_dimensions[0])), |
|
496 "-y", str(int(self.build_dimensions[1]))] + files |
|
497 p = subprocess.Popen(command, stdout = subprocess.PIPE) |
|
498 |
|
499 pos_regexp = re.compile("File: (.*) minx: ([0-9]+), miny: ([0-9]+), minrot: ([0-9]+)") |
|
500 for line in p.stdout: |
|
501 line = line.rstrip() |
|
502 if "Generating plate" in line: |
|
503 plateid = int(line.split()[-1]) |
|
504 if plateid > 0: |
|
505 logging.error(_("Plate full, please remove some objects")) |
|
506 break |
|
507 if "File:" in line: |
|
508 bits = pos_regexp.match(line).groups() |
|
509 filename = bits[0] |
|
510 x = float(bits[1]) |
|
511 y = float(bits[2]) |
|
512 rot = -float(bits[3]) |
|
513 for name, model in models.items(): |
|
514 # FIXME: not sure this is going to work superwell with utf8 |
|
515 if model.filename == filename: |
|
516 model.offsets[0] = x + self.build_dimensions[3] |
|
517 model.offsets[1] = y + self.build_dimensions[4] |
|
518 model.rot = rot |
|
519 del models[name] |
|
520 break |
|
521 if p.wait() != 0: |
|
522 raise RuntimeError(_("simarrange failed")) |
|
523 |
|
524 StlPlater = make_plater(StlPlaterPanel) |