plater.py 17.8 KB
Newer Older
Kliment's avatar
Kliment committed
1
#!/usr/bin/env python
2 3

# This file is part of the Printrun suite.
4
#
5 6 7 8
# 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.
9
#
10 11 12 13
# 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.
14
#
15 16
# You should have received a copy of the GNU General Public License
# along with Printrun.  If not, see <http://www.gnu.org/licenses/>.
17 18 19

# Set up Internationalization using gettext
# searching for installed locales on /usr/share; uses relative folder if not found (windows)
20
import os, Queue, re
21

22 23
from printrun.printrun_utils import install_locale
install_locale('plater')
24

25 26 27 28 29
import wx
import time
import random
import threading
import math
30 31
import sys

32 33
from printrun import stltool
from printrun.printrun_utils import pixmapfile
34

35
glview = False
36 37
if "-nogl" not in sys.argv:
    try:
38
        from printrun import stlview
39
        glview = True
40 41 42
    except:
        pass

43

44
def evalme(s):
45 46
    return eval(s[s.find("(") + 1:s.find(")")])

47

48
class stlwrap:
49
    def __init__(self, obj, name = None):
50 51
        self.obj = obj
        self.name = name
Travis Howse's avatar
Travis Howse committed
52
        if name is None:
53 54
            self.name = obj.name

Travis Howse's avatar
Travis Howse committed
55 56
    def __repr__(self):
        return self.name
57

58

59
class showstl(wx.Window):
60
    def __init__(self, parent, size, pos):
61
        wx.Window.__init__(self, parent, size = size, pos = pos)
62 63 64 65 66 67 68 69
        #self.SetBackgroundColour((0, 0, 0))
        #wx.FutureCall(200, self.paint)
        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)
70
        self.Bind(wx.EVT_KEY_DOWN, self.keypress)
71 72 73 74 75 76 77 78
        #self.s = stltool.stl("sphere.stl").scale([2, 1, 1])
        self.triggered = 0
        self.initpos = None
        self.prevsel = -1

    def drawmodel(self, m, scale):
        m.bitmap = wx.EmptyBitmap(800, 800, 32)
        dc = wx.MemoryDC()
79
        dc.SelectObject(m.bitmap)
80 81 82 83 84 85 86 87 88
        dc.SetBackground(wx.Brush((0, 0, 0, 0)))
        dc.SetBrush(wx.Brush((0, 0, 0, 255)))
        #dc.DrawRectangle(-1, -1, 10000, 10000)
        dc.SetBrush(wx.Brush(wx.Colour(128, 255, 128)))
        dc.SetPen(wx.Pen(wx.Colour(128, 128, 128)))
        #m.offsets = [10, 10, 0]
        #print m.offsets, m.dims
        for i in m.facets:  # random.sample(m.facets, min(100000, len(m.facets))):
            dc.DrawPolygon([wx.Point(400 + scale * p[0], (400 - scale * p[1])) for p in i[1]])
89 90 91
            #if(time.time()-t)>5:
            #    break
        dc.SelectObject(wx.NullBitmap)
92 93
        m.bitmap.SetMask(wx.Mask(m.bitmap, wx.Colour(0, 0, 0, 255)))

94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
    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):
112 113
        if event.ButtonUp(wx.MOUSE_BTN_LEFT):
            if(self.initpos is not None):
114 115 116 117 118 119
                currentpos = event.GetPositionTuple()
                delta = (
                        0.5 * (currentpos[0] - self.initpos[0]),
                        - 0.5 * (currentpos[1] - self.initpos[1])
                    )
                self.move_shape(delta)
120
                self.Refresh()
121
                self.initpos = None
122 123 124 125
        elif event.ButtonDown(wx.MOUSE_BTN_RIGHT):
            self.parent.right(event)
        elif event.Dragging():
            if self.initpos is None:
126
                self.initpos = event.GetPositionTuple()
127
            self.Refresh()
128 129 130
            dc = wx.ClientDC(self)
            p = event.GetPositionTuple()
            dc.DrawLine(self.initpos[0], self.initpos[1], p[0], p[1])
131
            #print math.sqrt((p[0]-self.initpos[0])**2+(p[1]-self.initpos[1])**2)
132

133 134 135
            del dc
        else:
            event.Skip()
136

137 138 139 140 141 142 143
    def rotate_shape(self, angle):
        """rotates acive shape
        positive angle is clockwise
        """
        self.i += angle
        if not self.triggered:
            self.triggered = 1
144
            threading.Thread(target = self.cr).start()
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174

    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()

175
    def rotateafter(self):
176 177
        if(self.i != self.previ):
            i = self.parent.l.GetSelection()
178
            if i != wx.NOT_FOUND:
179 180 181 182
                #o = self.models[self.l.GetItemText(i)].offsets
                self.parent.models[self.parent.l.GetString(i)].rot -= 5 * (self.i - self.previ)
                #self.models[self.l.GetItemText(i)].offsets = o
            self.previ = self.i
183
            self.Refresh()
184

185 186 187
    def cr(self):
        time.sleep(0.01)
        wx.CallAfter(self.rotateafter)
188 189
        self.triggered = 0

190
    def rot(self, event):
191 192 193 194 195
        z = event.GetWheelRotation()
        s = self.parent.l.GetSelection()
        if self.prevsel != s:
            self.i = 0
            self.prevsel = s
Kliment Yanev's avatar
Kliment Yanev committed
196
        if z < 0:
197
            self.rotate_shape(-1)
198
        else:
199
            self.rotate_shape(1)
200 201 202

    def repaint(self, event):
        dc = wx.PaintDC(self)
203
        self.paint(dc = dc)
204

205
    def paint(self, coord1 = "x", coord2 = "y", dc = None):
206
        coords = {"x": 0, "y": 1, "z": 2}
207
        if dc is None:
208 209 210 211
            dc = wx.ClientDC(self)
        offset = [0, 0]
        scale = 2
        dc.SetPen(wx.Pen(wx.Colour(100, 100, 100)))
212
        for i in xrange(20):
213 214 215
            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)))
216
        for i in xrange(4):
217 218 219 220 221 222
            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)))
        t = time.time()
        dcs = wx.MemoryDC()
223
        for m in self.parent.models.values():
224
            b = m.bitmap
225
                #print b
226
            im = b.ConvertToImage()
227
                #print im
228
            imgc = wx.Point(im.GetWidth() / 2, im.GetHeight() / 2)
229
                #print math.radians(5*(self.i-self.previ))
230 231
            im = im.Rotate(math.radians(m.rot), imgc, 0)
            bm = wx.BitmapFromImage(im)
232
            dcs.SelectObject(bm)
233
            bsz = bm.GetSize()
234
            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)
235 236
            #for i in m.facets:#random.sample(m.facets, min(100000, len(m.facets))):
            #    dc.DrawPolygon([wx.Point(offset[0]+scale*m.offsets[0]+scale*p[0], 400-(offset[1]+scale*m.offsets[1]+scale*p[1])) for p in i[1]])
237 238 239 240 241
                #if(time.time()-t)>5:
                #    break
        del dc
        #print time.time()-t
        #s.export()
242 243


244
class stlwin(wx.Frame):
245 246
    def __init__(self, size = (800, 580), callback = None, parent = None):
        wx.Frame.__init__(self, parent, title = _("Plate building tool"), size = size)
247
        self.SetIcon(wx.Icon(pixmapfile("plater.ico"), wx.BITMAP_TYPE_ICO))
248
        self.mainsizer = wx.BoxSizer(wx.HORIZONTAL)
249
        self.panel = wx.Panel(self, -1, size = (150, 600), pos = (0, 0))
250
        #self.panel.SetBackgroundColour((10, 10, 10))
251 252 253
        self.l = wx.ListBox(self.panel, size = (300, 180), pos = (0, 30))
        self.cl = wx.Button(self.panel, label = _("Clear"), pos = (0, 205))
        self.lb = wx.Button(self.panel, label = _("Load"), pos = (0, 0))
254
        if(callback is None):
255
            self.eb = wx.Button(self.panel, label = _("Export"), pos = (100, 0))
256
            self.eb.Bind(wx.EVT_BUTTON, self.export)
257
        else:
258
            self.eb = wx.Button(self.panel, label = _("Export"), pos = (200, 205))
259
            self.eb.Bind(wx.EVT_BUTTON, self.export)
260
            self.edb = wx.Button(self.panel, label = _("Done"), pos = (100, 0))
261
            self.edb.Bind(wx.EVT_BUTTON, lambda e: self.done(e, callback))
262
            self.eb = wx.Button(self.panel, label = _("Cancel"), pos = (200, 0))
263
            self.eb.Bind(wx.EVT_BUTTON, lambda e: self.Destroy())
264 265 266 267
        self.sb = wx.Button(self.panel, label = _("Snap to Z = 0"), pos = (00, 255))
        self.cb = wx.Button(self.panel, label = _("Put at 100, 100"), pos = (0, 280))
        self.db = wx.Button(self.panel, label = _("Delete"), pos = (0, 305))
        self.ab = wx.Button(self.panel, label = _("Auto"), pos = (0, 330))
268 269 270 271 272 273 274 275
        self.cl.Bind(wx.EVT_BUTTON, self.clear)
        self.lb.Bind(wx.EVT_BUTTON, self.right)
        self.sb.Bind(wx.EVT_BUTTON, self.snap)
        self.cb.Bind(wx.EVT_BUTTON, self.center)
        self.db.Bind(wx.EVT_BUTTON, self.delete)
        self.ab.Bind(wx.EVT_BUTTON, self.autoplate)
        self.basedir = "."
        self.models = {}
276
        #self.SetBackgroundColour((10, 10, 10))
277 278 279
        self.mainsizer.Add(self.panel)
        #self.mainsizer.AddSpacer(10)
        if glview:
280
            self.s = stlview.TestGlPanel(self, (580, 580))
281
        else:
282
            self.s = showstl(self, (580, 580), (0, 0))
283 284 285 286
        self.mainsizer.Add(self.s, 1, wx.EXPAND)
        self.SetSizer(self.mainsizer)
        #self.mainsizer.Fit(self)
        self.Layout()
287

288
        #self.SetClientSize(size)
289 290

    def autoplate(self, event):
291
        print _("Autoplating")
292
        separation = 2
293 294
        bedsize = [200, 200, 100]
        cursor = [0, 0, 0]
295
        newrow = 0
296
        max = [0, 0]
297
        for i in self.models:
298
            self.models[i].offsets[2] = -1.0 * self.models[i].dims[4]
299 300
            x = abs(self.models[i].dims[0] - self.models[i].dims[1])
            y = abs(self.models[i].dims[2] - self.models[i].dims[3])
301
            centre = [x / 2, y / 2]
302
            centreoffset = [self.models[i].dims[0] + centre[0], self.models[i].dims[2] + centre[1]]
303
            if (cursor[0] + x + separation) >= bedsize[0]:
304
                cursor[0] = 0
305
                cursor[1] += newrow + separation
306 307 308 309 310 311 312
                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]
313 314 315 316 317 318
            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]:
319
                print _("Bed full, sorry sir :(")
320 321
                self.Refresh()
                return
322
        centreoffset = [(bedsize[0] - max[0]) / 2, (bedsize[1] - max[1]) / 2]
323 324 325 326
        for i in self.models:
            self.models[i].offsets[0] += centreoffset[0]
            self.models[i].offsets[1] += centreoffset[1]
        self.Refresh()
327 328

    def clear(self, event):
329
        result = wx.MessageBox(_('Are you sure you want to clear the grid? All unsaved changes will be lost.'), _('Clear the grid?'),
330 331
            wx.YES_NO | wx.ICON_QUESTION)
        if (result == 2):
332
            self.models = {}
333 334
            self.l.Clear()
            self.Refresh()
335 336 337

    def center(self, event):
        i = self.l.GetSelection()
Travis Howse's avatar
Travis Howse committed
338
        if i != -1:
339 340 341
            m = self.models[self.l.GetString(i)]
            m.offsets = [100, 100, m.offsets[2]]
            self.Refresh()
342

343 344
    def snap(self, event):
        i = self.l.GetSelection()
Travis Howse's avatar
Travis Howse committed
345
        if i != -1:
346 347 348 349
            m = self.models[self.l.GetString(i)]
            m.offsets[2] = -1.0 * min(m.facetsminz)[0]
            #print m.offsets[2]
            self.Refresh()
350

351 352
    def delete(self, event):
        i = self.l.GetSelection()
Travis Howse's avatar
Travis Howse committed
353
        if i != -1:
354 355 356 357
            del self.models[self.l.GetString(i)]
            self.l.Delete(i)
            self.l.Select(self.l.GetCount() - 1)
            self.Refresh()
358

359
    def done(self, event, cb):
360 361 362 363
        try:
            os.mkdir("tempstl")
        except:
            pass
364
        name = "tempstl/" + str(int(time.time()) % 10000) + ".stl"
365 366 367 368
        self.writefiles(name)
        if cb is not None:
            cb(name)
        self.Destroy()
369 370

    def export(self, event):
371
        dlg = wx.FileDialog(self, _("Pick file to save to"), self.basedir, style = wx.FD_SAVE)
372
        dlg.SetWildcard(_("STL files (;*.stl;*.STL;)"))
Travis Howse's avatar
Travis Howse committed
373
        if(dlg.ShowModal() == wx.ID_OK):
374
            name = dlg.GetPath()
375
            self.writefiles(name)
376
        dlg.Destroy()
377 378 379 380

    def writefiles(self, name):
        sf = open(name.replace(".", "_") + ".scad", "w")
        facets = []
381
        for i in self.models.values():
382 383 384 385

            r = i.rot
            o = i.offsets
            sf.write('translate([%s, %s, %s]) rotate([0, 0, %s]) import_stl("%s");\n' % (str(o[0]), str(o[1]), str(o[2]), r, os.path.split(i.filename)[1]))
386
            if r != 0:
387 388 389 390
                i = i.rotate([0, 0, r])
            if o != [0, 0, 0]:
                i = i.translate([o[0], o[1], o[2]])
            facets += i.facets
391
        sf.close()
392
        stltool.emitstl(name, facets, "plater_export")
393
        print _("wrote %s") % name
394 395

    def right(self, event):
396
        dlg = wx.FileDialog(self, _("Pick file to load"), self.basedir, style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
397
        dlg.SetWildcard(_("STL files (;*.stl;*.STL;)|*.stl|OpenSCAD files (;*.scad;)|*.scad"))
Travis Howse's avatar
Travis Howse committed
398
        if(dlg.ShowModal() == wx.ID_OK):
399
            name = dlg.GetPath()
400
            if (name.lower().endswith(".stl")):
401
                self.load_stl(event, name)
402
            elif (name.lower().endswith(".scad")):
403
                self.load_scad(event, name)
404
        dlg.Destroy()
405 406 407 408

    def load_scad(self, event, name):
        lf = open(name)
        s = [i.replace("\n", "").replace("\r", "").replace(";", "") for i in lf if "stl" in i]
409 410 411 412
        lf.close()

        for i in s:
            parts = i.split()
413 414 415 416 417 418 419 420 421
            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)
422 423 424

            newname = os.path.split(stl_file.lower())[1]
            c = 1
425
            while newname in self.models:
426 427 428 429 430 431 432 433
                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, event, name):
434 435 436
        if not(os.path.exists(name)):
            return
        path = os.path.split(name)[0]
437 438
        self.basedir = path
        t = time.time()
439 440 441
        #print name
        if name.lower().endswith(".stl"):
            #Filter out the path, just show the STL filename.
442
            self.load_stl_into_model(name, name)
443 444
        self.Refresh()
        #print time.time()-t
445

446
    def load_stl_into_model(self, path, name, offset = [0, 0, 0], rotation = 0, scale = [1.0, 1.0, 1.0]):
447 448
        newname = os.path.split(name.lower())[1]
        c = 1
449
        while newname in self.models:
450 451 452 453 454 455 456 457 458
            newname = os.path.split(name.lower())[1]
            newname = newname + "(%d)" % c
            c += 1
        self.models[newname] = stltool.stl(path)
        self.models[newname].offsets = offset
        self.models[newname].rot = rotation
        self.models[newname].scale = scale
        self.models[newname].filename = name
        minx, miny, minz, maxx, maxy, maxz = (10000, 10000, 10000, 0, 0, 0)
459 460
        for i in self.models[newname].facets:
            for j in i[1]:
461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
                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]
        self.models[newname].dims = [minx, maxx, miny, maxy, minz, maxz]
        #if minx < 0:
        #    self.models[newname].offsets[0] = -minx
        #if miny < 0:
        #    self.models[newname].offsets[1] = -miny
        self.s.drawmodel(self.models[newname], 2)

        #print time.time() - t
481
        self.l.Append(newname)
482 483
        i = self.l.GetSelection()
        if i == wx.NOT_FOUND:
484
            self.l.Select(0)
485 486 487 488

        self.l.Select(self.l.GetCount() - 1)


489
if __name__ == '__main__':
Travis Howse's avatar
Travis Howse committed
490 491 492
    app = wx.App(False)
    main = stlwin()
    main.Show()
493
    app.MainLoop()