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

16 17
import xml.etree.ElementTree
import wx
18
import wx.lib.agw.floatspin as floatspin
19
import os
20
import time
21 22 23
import zipfile
import tempfile
import shutil
24
from printrun.cairosvg.surface import PNGSurface
25
import cStringIO
26
import imghdr
27
import copy
28 29 30
import re
from collections import OrderedDict
import itertools
31

32
class DisplayFrame(wx.Frame):
33
    def __init__(self, parent, title, res=(1024, 768), printer=None, scale=1.0, offset=(0,0)):
34
        wx.Frame.__init__(self, parent=parent, title=title, size=res)
35 36
        self.printer = printer
        self.control_frame = parent
37 38 39
        self.pic = wx.StaticBitmap(self)
        self.bitmap = wx.EmptyBitmap(*res)
        self.bbitmap = wx.EmptyBitmap(*res)
40
        self.slicer = 'bitmap'
41
        self.dpi = 96
42
        dc = wx.MemoryDC()
43 44 45 46
        dc.SelectObject(self.bbitmap)
        dc.SetBackground(wx.Brush("black"))
        dc.Clear()
        dc.SelectObject(wx.NullBitmap)
47

48 49 50
        self.SetBackgroundColour("black")
        self.pic.Hide()
        self.SetDoubleBuffered(True)
51
        self.SetPosition((self.control_frame.GetSize().x, 0))
52
        self.Show()
53 54 55 56 57
        
        self.scale = scale
        self.index = 0
        self.size = res
        self.offset = offset
58
        self.running = False
59
        self.layer_red = False
60

61
    def clear_layer(self):
62 63 64 65 66 67 68 69 70 71
        try:
            dc = wx.MemoryDC()
            dc.SelectObject(self.bitmap)
            dc.SetBackground(wx.Brush("black"))
            dc.Clear()
            self.pic.SetBitmap(self.bitmap)
            self.pic.Show()
            self.Refresh()
        except:
            raise
72
            pass
73 74 75 76 77 78 79 80 81 82
        
    def resize(self, res=(1024, 768)):
        self.bitmap = wx.EmptyBitmap(*res)
        self.bbitmap = wx.EmptyBitmap(*res)
        dc = wx.MemoryDC()
        dc.SelectObject(self.bbitmap)
        dc.SetBackground(wx.Brush("black"))
        dc.Clear()
        dc.SelectObject(wx.NullBitmap)
        
83
    def draw_layer(self, image):
84
        try:
85
            dc = wx.MemoryDC()
86 87
            dc.SelectObject(self.bitmap)
            dc.SetBackground(wx.Brush("black"))
Gary Hodgson's avatar
Gary Hodgson committed
88
            dc.Clear()
89

90
            if self.slicer == 'Slic3r' or self.slicer == 'Skeinforge':
91 92
                
                if int(self.scale) != 1:
93 94 95
                    layercopy = copy.deepcopy(image)
                    height = float(layercopy.get('height').replace('m',''))
                    width = float(layercopy.get('width').replace('m',''))
96
                    
97 98 99
                    layercopy.set('height', str(height*self.scale) + 'mm')
                    layercopy.set('width', str(width*self.scale) + 'mm')
                    layercopy.set('viewBox', '0 0 ' + str(height*self.scale) + ' ' + str(width*self.scale))
100
                    
101
                    g = layercopy.find("{http://www.w3.org/2000/svg}g")
102
                    g.set('transform', 'scale('+str(self.scale)+')')
103 104 105
                    stream = cStringIO.StringIO(PNGSurface.convert(dpi=self.dpi, bytestring=xml.etree.ElementTree.tostring(layercopy)))
                else:    
                    stream = cStringIO.StringIO(PNGSurface.convert(dpi=self.dpi, bytestring=xml.etree.ElementTree.tostring(image)))
106 107 108 109 110 111 112 113
                    
                image = wx.ImageFromStream(stream)
                
                if self.layer_red:
                    image = image.AdjustChannels(1,0,0,1)
                
                dc.DrawBitmap(wx.BitmapFromImage(image), self.offset[0], self.offset[1], True)
                
114 115 116
            elif self.slicer == 'bitmap':
                if isinstance(image, str):
                    image = wx.Image(image)
Gary Hodgson's avatar
Gary Hodgson committed
117 118
                if self.layer_red:
                    image = image.AdjustChannels(1,0,0,1)
119
                dc.DrawBitmap(wx.BitmapFromImage(image.Scale(image.Width * self.scale, image.Height * self.scale)), self.offset[0], -self.offset[1], True)
120 121
            else:
                raise Exception(self.slicer + " is an unknown method.")
122
            
123 124
            self.pic.SetBitmap(self.bitmap)
            self.pic.Show()
125
            self.Refresh()            
126
            
127
        except:
128
            raise
129
            pass
130
            
131 132 133 134 135
    def show_img_delay(self, image):
        print "Showing "+ str(time.clock())
        self.control_frame.set_current_layer(self.index)
        self.draw_layer(image)
        wx.FutureCall(1000 * self.interval, self.hide_pic_and_rise)
136

137
    def rise(self):
138 139 140 141
        if (self.direction == "Top Down"):
            print "Lowering "+ str(time.clock())
        else:
            print "Rising "+ str(time.clock())
142
                        
143
        if self.printer != None and self.printer.online:
144
            self.printer.send_now("G91")
145 146 147 148 149 150
            
            if (self.prelift_gcode):
                for line in self.prelift_gcode.split('\n'):
                    if line:
                        self.printer.send_now(line)
            
151
            if (self.direction == "Top Down"):
152 153
                self.printer.send_now("G1 Z-%f F%g" % (self.overshoot,self.z_axis_rate,))
                self.printer.send_now("G1 Z%f F%g" % (self.overshoot-self.thickness,self.z_axis_rate,))
154
            else: # self.direction == "Bottom Up"
155 156
                self.printer.send_now("G1 Z%f F%g" % (self.overshoot,self.z_axis_rate,))
                self.printer.send_now("G1 Z-%f F%g" % (self.overshoot-self.thickness,self.z_axis_rate,))
157 158 159 160 161 162
            
            if (self.postlift_gcode):
                for line in self.postlift_gcode.split('\n'):
                    if line:
                        self.printer.send_now(line)
            
163 164 165
            self.printer.send_now("G90")
        else:
            time.sleep(self.pause)
166
        
167
        wx.FutureCall(1000 * self.pause, self.next_img)
168 169 170
        
    def hide_pic(self):
        print "Hiding "+ str(time.clock())
171
        self.pic.Hide()
172
        
173 174
    def hide_pic_and_rise(self):
        wx.CallAfter(self.hide_pic)
175
        wx.FutureCall(500, self.rise)
176
                    
177 178 179
    def next_img(self):
        if not self.running:
            return
180
        if self.index < len(self.layers):
181 182
            print self.index
            wx.CallAfter(self.show_img_delay, self.layers[self.index])
183
            self.index += 1
184 185 186 187
        else:
            print "end"
            wx.CallAfter(self.pic.Hide)
            wx.CallAfter(self.Refresh)
188
        
189 190 191 192 193 194 195 196 197 198 199 200 201 202
    def present(self, 
                layers, 
                interval=0.5, 
                pause=0.2, 
                overshoot=0.0, 
                z_axis_rate=200, 
                prelift_gcode="", 
                postlift_gcode="", 
                direction="Top Down", 
                thickness=0.4, 
                scale=1, 
                size=(1024, 768), 
                offset=(0, 0),
                layer_red=False):
203 204
        wx.CallAfter(self.pic.Hide)
        wx.CallAfter(self.Refresh)
205 206 207
        self.layers = layers
        self.scale = scale
        self.thickness = thickness
208
        self.size = size
209
        self.interval = interval
210
        self.pause = pause
211
        self.overshoot = overshoot
212
        self.z_axis_rate = z_axis_rate
213 214
        self.prelift_gcode = prelift_gcode
        self.postlift_gcode = postlift_gcode
215
        self.direction = direction 
216
        self.layer_red = layer_red
217
        self.offset = offset
218 219
        self.index = 0
        self.running = True
220
        
221
        self.next_img()
222

223
class SettingsFrame(wx.Frame):
224
    
225 226 227 228 229 230 231
    def _set_setting(self, name, value):
        if self.pronterface:
            self.pronterface.set(name,value)
    
    def _get_setting(self,name, val):
        if self.pronterface:
            try:
232
                return getattr(self.pronterface.settings, name)
233 234 235 236 237
            except AttributeError, x:
                return val
        else: 
            return val
        
238
    def __init__(self, parent, printer=None):
239 240
        wx.Frame.__init__(self, parent, title="ProjectLayer Control",style=(wx.DEFAULT_FRAME_STYLE | wx.WS_EX_CONTEXTHELP))
        self.SetExtraStyle(wx.FRAME_EX_CONTEXTHELP)
241
        self.pronterface = parent
242
        self.display_frame = DisplayFrame(self, title="ProjectLayer Display", printer=printer)
243
        
244
        self.panel = wx.Panel(self)
245
                
246 247 248 249 250
        vbox = wx.BoxSizer(wx.VERTICAL)       
        buttonbox = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Controls"), wx.HORIZONTAL) 
        
        load_button = wx.Button(self.panel, -1, "Load")
        load_button.Bind(wx.EVT_BUTTON, self.load_file)
251
        load_button.SetHelpText("Choose an SVG file created from Slic3r or Skeinforge, or a zip file of bitmap images (with extension: .3dlp.zip).")
252
        buttonbox.Add(load_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
253 254 255
        
        present_button = wx.Button(self.panel, -1, "Present")
        present_button.Bind(wx.EVT_BUTTON, self.start_present)
256
        present_button.SetHelpText("Starts the presentation of the slices.")
257
        buttonbox.Add(present_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
258 259 260
        
        self.pause_button = wx.Button(self.panel, -1, "Pause")
        self.pause_button.Bind(wx.EVT_BUTTON, self.pause_present)
261
        self.pause_button.SetHelpText("Pauses the presentation. Can be resumed afterwards by clicking this button, or restarted by clicking present again.")
262
        buttonbox.Add(self.pause_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
263 264 265
                
        stop_button = wx.Button(self.panel, -1, "Stop")
        stop_button.Bind(wx.EVT_BUTTON, self.stop_present)
266
        stop_button.SetHelpText("Stops presenting the slices.")
267 268 269 270
        buttonbox.Add(stop_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)

        self.help_button = wx.ContextHelpButton(self.panel)
        buttonbox.Add(self.help_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
271 272 273 274 275 276 277
        
        fieldboxsizer = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Settings"), wx.VERTICAL)
        fieldsizer = wx.GridBagSizer(10,10)
        
        # Left Column
        
        fieldsizer.Add(wx.StaticText(self.panel, -1, "Layer (mm):"), pos=(0, 0), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
278
        self.thickness = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_layer", "0.1")), size=(80, -1))
279 280
        self.thickness.Bind(wx.EVT_TEXT, self.update_thickness)
        self.thickness.SetHelpText("The thickness of each slice. Should match the value used to slice the model.  SVG files update this value automatically, 3dlp.zip files have to be manually entered.")        
281 282 283
        fieldsizer.Add(self.thickness, pos=(0, 1))
        
        fieldsizer.Add(wx.StaticText(self.panel, -1, "Exposure (s):"), pos=(1, 0), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
284
        self.interval = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_interval", "0.5")), size=(80,-1))
285 286
        self.interval.Bind(wx.EVT_TEXT, self.update_interval)
        self.interval.SetHelpText("How long each slice should be displayed.")
287
        fieldsizer.Add(self.interval, pos=(1, 1))
288
        
289
        fieldsizer.Add(wx.StaticText(self.panel, -1, "Blank (s):"), pos=(2,0), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
290
        self.pause = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_pause", "0.5")), size=(80,-1))
291 292
        self.pause.Bind(wx.EVT_TEXT, self.update_pause)
        self.pause.SetHelpText("The pause length between slices. This should take into account any movement of the Z axis, plus time to prepare the resin surface (sliding, tilting, sweeping, etc).")
293
        fieldsizer.Add(self.pause, pos=(2, 1))
294
        
295
        fieldsizer.Add(wx.StaticText(self.panel, -1, "Scale:"), pos=(3,0), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
296
        self.scale = floatspin.FloatSpin(self.panel, -1, value=self._get_setting('project_scale', 1.0), increment=0.1, digits=3, size=(80,-1))
297
        self.scale.Bind(floatspin.EVT_FLOATSPIN, self.update_scale)
298
        self.scale.SetHelpText("The additional scaling of each slice.")
299 300
        fieldsizer.Add(self.scale, pos=(3, 1))
        
301
        fieldsizer.Add(wx.StaticText(self.panel, -1, "Direction:"), pos=(4,0), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
302
        self.direction = wx.ComboBox(self.panel, -1, choices=["Top Down","Bottom Up"], value=self._get_setting('project_direction', "Top Down"), size=(80,-1))
303 304 305 306 307
        self.direction.Bind(wx.EVT_COMBOBOX, self.update_direction)
        self.direction.SetHelpText("The direction the Z axis should move. Top Down is where the projector is above the model, Bottom up is where the projector is below the model.")
        fieldsizer.Add(self.direction, pos=(4, 1), flag=wx.ALIGN_CENTER_VERTICAL)
        
        fieldsizer.Add(wx.StaticText(self.panel, -1, "Overshoot (mm):"), pos=(5,0), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
308
        self.overshoot= floatspin.FloatSpin(self.panel, -1, value=self._get_setting('project_overshoot', 3.0), increment=0.1, digits=1, min_val=0, size=(80,-1))
309
        self.overshoot.Bind(floatspin.EVT_FLOATSPIN, self.update_overshoot)
310 311
        self.overshoot.SetHelpText("How far the axis should move beyond the next slice position for each slice. For Top Down printers this would dunk the model under the resi and then return. For Bottom Up printers this would raise the base away from the vat and then return.")
        fieldsizer.Add(self.overshoot, pos=(5, 1))
312
        
313 314 315 316 317 318 319 320 321 322 323 324
        fieldsizer.Add(wx.StaticText(self.panel, -1, "Pre-lift Gcode:"), pos=(6, 0), flag=wx.ALIGN_CENTER_VERTICAL)
        self.prelift_gcode = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_prelift_gcode", "").replace("\\n",'\n')), size=(-1, 35), style=wx.TE_MULTILINE)
        self.prelift_gcode.SetHelpText("Additional gcode to run before raising the Z axis. Be sure to take into account any additional time needed in the pause value, and be careful what gcode is added!")
        self.prelift_gcode.Bind(wx.EVT_TEXT, self.update_prelift_gcode)
        fieldsizer.Add(self.prelift_gcode, pos=(6, 1), span=(2,1))

        fieldsizer.Add(wx.StaticText(self.panel, -1, "Post-lift Gcode:"), pos=(6, 2), flag=wx.ALIGN_CENTER_VERTICAL)
        self.postlift_gcode = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_postlift_gcode", "").replace("\\n",'\n')), size=(-1, 35), style=wx.TE_MULTILINE)
        self.postlift_gcode.SetHelpText("Additional gcode to run after raising the Z axis. Be sure to take into account any additional time needed in the pause value, and be careful what gcode is added!")
        self.postlift_gcode.Bind(wx.EVT_TEXT, self.update_postlift_gcode)
        fieldsizer.Add(self.postlift_gcode, pos=(6, 3), span=(2,1))

325 326
        # Right Column
        
327
        fieldsizer.Add(wx.StaticText(self.panel, -1, "X (px):"), pos=(0, 2), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
328
        self.X = wx.SpinCtrl(self.panel, -1, str(int(self._get_setting("project_x", 1024))), max=999999, size=(80,-1))
329
        self.X.Bind(wx.EVT_SPINCTRL, self.update_resolution)
330
        self.X.SetHelpText("The projector resolution in the X axis.")
331
        fieldsizer.Add(self.X, pos=(0, 3))
332

333
        fieldsizer.Add(wx.StaticText(self.panel, -1, "Y (px):"), pos=(1, 2), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
334
        self.Y = wx.SpinCtrl(self.panel, -1, str(int(self._get_setting("project_y", 768))), max=999999, size=(80,-1))
335
        self.Y.Bind(wx.EVT_SPINCTRL, self.update_resolution)
336
        self.Y.SetHelpText("The projector resolution in the Y axis.")
337
        fieldsizer.Add(self.Y, pos=(1, 3))
338
        
339
        fieldsizer.Add(wx.StaticText(self.panel, -1, "OffsetX (mm):"), pos=(2, 2), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
340
        self.offset_X = floatspin.FloatSpin(self.panel, -1, value=self._get_setting("project_offset_x", 0.0), increment=1, digits=1, size=(80,-1))
341
        self.offset_X.Bind(floatspin.EVT_FLOATSPIN, self.update_offset)
342
        self.offset_X.SetHelpText("How far the slice should be offset from the edge in the X axis.")
343
        fieldsizer.Add(self.offset_X, pos=(2, 3))
344

345
        fieldsizer.Add(wx.StaticText(self.panel, -1, "OffsetY (mm):"), pos=(3, 2), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
346
        self.offset_Y = floatspin.FloatSpin(self.panel, -1, value=self._get_setting("project_offset_y", 0.0), increment=1, digits=1, size=(80,-1))
347
        self.offset_Y.Bind(floatspin.EVT_FLOATSPIN, self.update_offset)
348
        self.offset_Y.SetHelpText("How far the slice should be offset from the edge in the Y axis.")
349
        fieldsizer.Add(self.offset_Y, pos=(3, 3))
350
        
351
        fieldsizer.Add(wx.StaticText(self.panel, -1, "ProjectedX (mm):"), pos=(4, 2), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
352
        self.projected_X_mm = floatspin.FloatSpin(self.panel, -1, value=self._get_setting("project_projected_x", 415.0), increment=1, digits=1, size=(80,-1))
353
        self.projected_X_mm.Bind(floatspin.EVT_FLOATSPIN, self.update_projected_Xmm)
354
        self.projected_X_mm.SetHelpText("The actual width of the entire projected image. Use the Calibrate grid to show the full size of the projected image, and measure the width at the same level where the slice will be projected onto the resin.")
355
        fieldsizer.Add(self.projected_X_mm, pos=(4, 3))
356
        
357 358
        
        fieldsizer.Add(wx.StaticText(self.panel, -1, "Z Axis Speed (mm/min):"), pos=(5, 2), flag=wx.ALIGN_CENTER_VERTICAL)
Gary Hodgson's avatar
Gary Hodgson committed
359
        self.z_axis_rate = wx.SpinCtrl(self.panel, -1, str(self._get_setting("project_z_axis_rate", 200)), max=9999, size=(80,-1))
360 361 362 363
        self.z_axis_rate.Bind(wx.EVT_SPINCTRL, self.update_z_axis_rate)
        self.z_axis_rate.SetHelpText("Speed of the Z axis in mm/minute. Take into account that slower rates may require a longer pause value.")
        fieldsizer.Add(self.z_axis_rate, pos=(5, 3))
        
364
        fieldboxsizer.Add(fieldsizer)
365
        
366
        # Display
367
        
368 369
        displayboxsizer = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Display"), wx.VERTICAL)
        displaysizer = wx.GridBagSizer(10,10)
370
        
371 372 373
        displaysizer.Add(wx.StaticText(self.panel, -1, "Fullscreen:"), pos=(0,0), flag=wx.ALIGN_CENTER_VERTICAL)
        self.fullscreen = wx.CheckBox(self.panel, -1)
        self.fullscreen.Bind(wx.EVT_CHECKBOX, self.update_fullscreen)
374
        self.fullscreen.SetHelpText("Toggles the project screen to full size.")
375 376 377 378
        displaysizer.Add(self.fullscreen, pos=(0, 1), flag=wx.ALIGN_CENTER_VERTICAL)
               
        displaysizer.Add(wx.StaticText(self.panel, -1, "Calibrate:"), pos=(0,2), flag=wx.ALIGN_CENTER_VERTICAL)
        self.calibrate = wx.CheckBox(self.panel, -1)
379
        self.calibrate.Bind(wx.EVT_CHECKBOX, self.show_calibrate)
380
        self.calibrate.SetHelpText("Toggles the calibration grid. Each grid should be 10mmx10mm in size. Use the grid to ensure the projected size is correct. See also the help for the ProjectedX field.")
381
        displaysizer.Add(self.calibrate, pos=(0,3), flag=wx.ALIGN_CENTER_VERTICAL)
382
        
383
        displaysizer.Add(wx.StaticText(self.panel, -1, "1st Layer:"), pos=(0,4), flag=wx.ALIGN_CENTER_VERTICAL)
384 385 386
        
        first_layer_boxer = wx.BoxSizer(wx.HORIZONTAL)                
        self.first_layer = wx.CheckBox(self.panel, -1)
387
        self.first_layer.Bind(wx.EVT_CHECKBOX, self.show_first_layer)
388 389
        self.first_layer.SetHelpText("Displays the first layer of the model. Use this to project the first layer for longer so it holds to the base. Note: this value does not affect the first layer when the \"Present\" run is started, it should be used manually.")

390
        first_layer_boxer.Add(self.first_layer, flag=wx.ALIGN_CENTER_VERTICAL)
391

392 393
        first_layer_boxer.Add(wx.StaticText(self.panel, -1, " (s):"), flag=wx.ALIGN_CENTER_VERTICAL)
        self.show_first_layer_timer = floatspin.FloatSpin(self.panel, -1, value=-1, increment=1, digits=1, size=(55,-1))
394
        self.show_first_layer_timer.SetHelpText("How long to display the first layer for. -1 = unlimited.")
395
        first_layer_boxer.Add(self.show_first_layer_timer, flag=wx.ALIGN_CENTER_VERTICAL)
396 397 398 399 400 401 402 403
        displaysizer.Add(first_layer_boxer, pos=(0,6), flag=wx.ALIGN_CENTER_VERTICAL)
        
        displaysizer.Add(wx.StaticText(self.panel, -1, "Red:"), pos=(0,7), flag=wx.ALIGN_CENTER_VERTICAL)
        self.layer_red = wx.CheckBox(self.panel, -1)
        self.layer_red.Bind(wx.EVT_CHECKBOX, self.show_layer_red)
        self.layer_red.SetHelpText("Toggles whether the image should be red. Useful for positioning whilst resin is in the printer as it should not cause a reaction.")
        displaysizer.Add(self.layer_red, pos=(0,8), flag=wx.ALIGN_CENTER_VERTICAL)
        
404
        
405 406 407 408 409 410
        displayboxsizer.Add(displaysizer)
                
        # Info
        infosizer = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Info"), wx.VERTICAL)
                
        infofieldsizer = wx.GridBagSizer(10,10)
411
        
412 413 414 415 416
        filelabel = wx.StaticText(self.panel, -1, "File:")
        filelabel.SetHelpText("The name of the model currently loaded.")
        infofieldsizer.Add(filelabel, pos=(0,0))
        self.filename = wx.StaticText(self.panel, -1, "")        

417 418
        infofieldsizer.Add(self.filename, pos=(0,1))
        
419 420 421
        totallayerslabel = wx.StaticText(self.panel, -1, "Total Layers:")
        totallayerslabel.SetHelpText("The total number of layers found in the model.")
        infofieldsizer.Add(totallayerslabel, pos=(1,0))
422
        self.total_layers = wx.StaticText(self.panel, -1)
423
        
424
        infofieldsizer.Add(self.total_layers, pos=(1,1))
425

426 427 428
        currentlayerlabel = wx.StaticText(self.panel, -1, "Current Layer:")
        currentlayerlabel.SetHelpText("The current layer being displayed.")
        infofieldsizer.Add(currentlayerlabel, pos=(2,0))
429 430 431
        self.current_layer = wx.StaticText(self.panel, -1, "0")
        infofieldsizer.Add(self.current_layer, pos=(2,1))
        
432 433 434
        estimatedtimelabel = wx.StaticText(self.panel, -1, "Estimated Time:")
        estimatedtimelabel.SetHelpText("An estimate of the remaining time until print completion.")
        infofieldsizer.Add(estimatedtimelabel, pos=(3,0))
435 436
        self.estimated_time = wx.StaticText(self.panel, -1, "")
        infofieldsizer.Add(self.estimated_time, pos=(3,1))
437
        
438
        infosizer.Add(infofieldsizer)
Gary Hodgson's avatar
Gary Hodgson committed
439
        
440 441 442 443 444 445 446 447
        #
        
        vbox.Add(buttonbox, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP|wx.BOTTOM, border=10)
        vbox.Add(fieldboxsizer, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10);
        vbox.Add(displayboxsizer, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10);
        vbox.Add(infosizer, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10)
        
        self.panel.SetSizer(vbox)
448 449
        self.panel.Fit() 
        self.Fit() 
450
        self.SetPosition((0, 0)) 
451
        self.Show()
452

453 454 455
    def __del__(self):
        if hasattr(self, 'image_dir') and self.image_dir != '':
            shutil.rmtree(self.image_dir)
456 457 458 459
        if self.display_frame:
            self.display_frame.Destroy()

    def set_total_layers(self, total):
Gary Hodgson's avatar
Gary Hodgson committed
460 461
        self.total_layers.SetLabel(str(total))
        self.set_estimated_time()
462 463

    def set_current_layer(self, index):
Gary Hodgson's avatar
Gary Hodgson committed
464 465
        self.current_layer.SetLabel(str(index))
        self.set_estimated_time()
466

467
    def display_filename(self,name):
468
        self.filename.SetLabel(name)
469
            
Gary Hodgson's avatar
Gary Hodgson committed
470
    def set_estimated_time(self):
471
        if not hasattr(self, 'layers'):
Gary Hodgson's avatar
Gary Hodgson committed
472 473 474 475 476 477 478 479
            return
        
        current_layer = int(self.current_layer.GetLabel())
        remaining_layers = len(self.layers[0]) - current_layer
        # 0.5 for delay between hide and rise
        estimated_time =  remaining_layers * (float(self.interval.GetValue()) + float(self.pause.GetValue()) + 0.5)  
        self.estimated_time.SetLabel(time.strftime("%H:%M:%S",time.gmtime(estimated_time)))
            
480
    def parse_svg(self, name):
481
        et = xml.etree.ElementTree.ElementTree(file=name)
482
        #xml.etree.ElementTree.dump(et)
483
        
484 485 486 487 488
        slicer = 'Slic3r' if et.getroot().find('{http://www.w3.org/2000/svg}metadata') == None else 'Skeinforge'
        zlast = 0
        zdiff = 0
        ol = []
        if (slicer == 'Slic3r'):
489 490
            height = et.getroot().get('height').replace('m','')
            width = et.getroot().get('width').replace('m','')
491
            
492 493 494 495
            for i in et.findall("{http://www.w3.org/2000/svg}g"):
                z = float(i.get('{http://slic3r.org/namespaces/slic3r}z'))
                zdiff = z - zlast
                zlast = z
496
    
497 498 499
                svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg')
                svgSnippet.set('height', height + 'mm')
                svgSnippet.set('width', width + 'mm')
500
                
501 502
                svgSnippet.set('viewBox', '0 0 ' + height + ' ' + width)
                svgSnippet.set('style','background-color:black')
503
                svgSnippet.append(i)
504
    
505 506
                ol += [svgSnippet]
        else :
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
            
            slice_layers = et.findall("{http://www.w3.org/2000/svg}metadata")[0].findall("{http://www.reprap.org/slice}layers")[0]
            minX = slice_layers.get('minX')
            maxX = slice_layers.get('maxX')
            minY = slice_layers.get('minY')
            maxY = slice_layers.get('maxY')
            
            height = str(abs(float(minY)) + abs(float(maxY)))
            width = str(abs(float(minX)) + abs(float(maxX)))
            
            for g in et.findall("{http://www.w3.org/2000/svg}g")[0].findall("{http://www.w3.org/2000/svg}g"):
                
                g.set('transform','')
                
                text_element = g.findall("{http://www.w3.org/2000/svg}text")[0]
                g.remove(text_element)
                
                path_elements = g.findall("{http://www.w3.org/2000/svg}path")
                for p in path_elements:
                    p.set('transform', 'translate('+maxX+','+maxY+')')
                    p.set('fill', 'white')

                z = float(g.get('id').split("z:")[-1])
530 531
                zdiff = z - zlast
                zlast = z
532 533 534 535 536 537 538 539 540 541
    
                svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg')
                svgSnippet.set('height', height + 'mm')
                svgSnippet.set('width', width + 'mm')
                
                svgSnippet.set('viewBox', '0 0 ' + height + ' ' + width)
                svgSnippet.set('style','background-color:black;fill:white;')
                svgSnippet.append(g)
    
                ol += [svgSnippet]
542
        return ol, zdiff, slicer
543
    
544
    def parse_3DLP_zip(self, name):
545 546
        if not zipfile.is_zipfile(name):
            raise Exception(name + " is not a zip file!")
547
        accepted_image_types = ['gif','tiff','jpg','jpeg','bmp','png']
548 549 550 551
        zipFile = zipfile.ZipFile(name, 'r')
        self.image_dir = tempfile.mkdtemp()
        zipFile.extractall(self.image_dir)
        ol = []
552 553 554 555 556 557 558 559 560 561 562
        
        # Note: the following funky code extracts any numbers from the filenames, matches
        # them with the original then sorts them. It allows for filenames of the 
        # format: abc_1.png, which would be followed by abc_10.png alphabetically.  
        os.chdir(self.image_dir)
        vals = filter(os.path.isfile, os.listdir('.'))
        keys = map(lambda p:int(re.search('\d+', p).group()), vals)
        imagefilesDict = dict(itertools.izip(keys, vals))
        imagefilesOrderedDict = OrderedDict(sorted(imagefilesDict.items(), key=lambda t: t[0]))
        
        for f in imagefilesOrderedDict.values():
563
            path = os.path.join(self.image_dir, f)
564
            if os.path.isfile(path) and imghdr.what(path) in accepted_image_types:
565
                ol.append(path)
566
        
567
        return ol, -1, "bitmap"
568
        
569
    def load_file(self, event):
570
        dlg = wx.FileDialog(self, ("Open file to print"), style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
571
        dlg.SetWildcard(("Slic3r or Skeinforge svg files (;*.svg;*.SVG;);3DLP Zip (;*.3dlp.zip;)"))
572
        if(dlg.ShowModal() == wx.ID_OK):
573
            name = dlg.GetPath()
574 575 576
            if not(os.path.exists(name)):
                self.status.SetStatusText(("File not found!"))
                return
577
            if name.endswith(".3dlp.zip"):
578
                layers = self.parse_3DLP_zip(name)
579 580
                layerHeight = float(self.thickness.GetValue())
            else:
581
                layers = self.parse_svg(name)
582 583
                layerHeight = layers[1]
                self.thickness.SetValue(str(layers[1]))
584
                print "Layer thickness detected:", layerHeight, "mm"
585 586
            print len(layers[0]), "layers found, total height", layerHeight * len(layers[0]), "mm"
            self.layers = layers
587
            self.set_total_layers(len(layers[0]))
588 589 590 591 592
            self.set_current_layer(0)
            self.current_filename = os.path.basename(name) 
            self.display_filename(self.current_filename) 
            self.slicer = layers[2]
            self.display_frame.slicer = self.slicer
593
        dlg.Destroy()
594

595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
    def show_calibrate(self, event):
        if self.calibrate.IsChecked():
            self.present_calibrate(event)
        else:
            if hasattr(self, 'layers'):
                self.display_frame.slicer = self.layers[2] 
            self.display_frame.scale = float(self.scale.GetValue())
            self.display_frame.clear_layer()

    def show_first_layer(self, event):
        if self.first_layer.IsChecked():
            self.present_first_layer(event)
        else:
            if hasattr(self, 'layers'):
                self.display_frame.slicer = self.layers[2] 
            self.display_frame.scale = float(self.scale.GetValue())
            self.display_frame.clear_layer()
    
613 614
    def show_layer_red(self, event):
        self.display_frame.layer_red = self.layer_red.IsChecked()
615 616
        
    def present_calibrate(self, event):
617
        if self.calibrate.IsChecked():
618
            self.display_frame.Raise()
619
            self.display_frame.offset = (float(self.offset_X.GetValue()), -float(self.offset_Y.GetValue()))
620 621 622
            self.display_frame.scale = 1.0
            resolution_x_pixels = int(self.X.GetValue())
            resolution_y_pixels = int(self.Y.GetValue())
623
            
624
            gridBitmap = wx.EmptyBitmap(resolution_x_pixels, resolution_y_pixels)
625 626 627 628 629
            dc = wx.MemoryDC()
            dc.SelectObject(gridBitmap)
            dc.SetBackground(wx.Brush("black"))
            dc.Clear()
            
630 631 632 633 634
            dc.SetPen(wx.Pen("red", 7))
            dc.DrawLine(0, 0, resolution_x_pixels, 0);
            dc.DrawLine(0, 0, 0, resolution_y_pixels);
            dc.DrawLine(resolution_x_pixels, 0, resolution_x_pixels, resolution_y_pixels);
            dc.DrawLine(0, resolution_y_pixels, resolution_x_pixels, resolution_y_pixels);
635
            
636 637
            dc.SetPen(wx.Pen("red", 2))
            aspectRatio = float(resolution_x_pixels) / float(resolution_y_pixels)
638
            
639 640
            projectedXmm = float(self.projected_X_mm.GetValue())            
            projectedYmm = round(projectedXmm / aspectRatio)
641
            
642 643
            pixelsXPerMM = resolution_x_pixels / projectedXmm
            pixelsYPerMM = resolution_y_pixels / projectedYmm
644
            
645 646
            gridCountX = int(projectedXmm / 10)
            gridCountY = int(projectedYmm / 10)
647
            
648 649 650 651 652
            for y in xrange(0, gridCountY + 1):
                for x in xrange(0, gridCountX + 1):
                    dc.DrawLine(0, y * (pixelsYPerMM * 10), resolution_x_pixels, y * (pixelsYPerMM * 10));
                    dc.DrawLine(x * (pixelsXPerMM * 10), 0, x * (pixelsXPerMM * 10), resolution_y_pixels);

653
            self.first_layer.SetValue(False)
654 655
            self.display_frame.slicer = 'bitmap'
            self.display_frame.draw_layer(gridBitmap.ConvertToImage())
656

657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675
    def present_first_layer(self, event):
        if (self.first_layer.GetValue()):
            if not hasattr(self, "layers"):
                print "No model loaded!"
                self.first_layer.SetValue(False)
                return
            self.display_frame.offset = (float(self.offset_X.GetValue()), float(self.offset_Y.GetValue()))
            self.display_frame.scale = float(self.scale.GetValue())

            self.display_frame.slicer = self.layers[2]
            self.display_frame.dpi = self.get_dpi()
            self.display_frame.draw_layer(copy.deepcopy(self.layers[0][0]))
            self.calibrate.SetValue(False)
            if self.show_first_layer_timer != -1.0 :
                def unpresent_first_layer():
                    self.display_frame.clear_layer()
                    self.first_layer.SetValue(False)
                wx.CallLater(self.show_first_layer_timer.GetValue() * 1000, unpresent_first_layer)

676
    def update_offset(self, event):
677 678 679 680 681 682 683 684
        
        offset_x = float(self.offset_X.GetValue())
        offset_y = float(self.offset_Y.GetValue())
        self.display_frame.offset = (offset_x, offset_y)
        
        self._set_setting('project_offset_x',offset_x)
        self._set_setting('project_offset_y',offset_y)
        
685 686 687 688 689
        self.refresh_display(event)
        
    def refresh_display(self, event):
        self.present_calibrate(event)
        self.present_first_layer(event)
690
    
691 692 693 694
    def update_thickness(self, event):
        self._set_setting('project_layer',self.thickness.GetValue())
        self.refresh_display(event)
        
695
    def update_projected_Xmm(self, event):
696
        self._set_setting('project_projected_x',self.projected_X_mm.GetValue())
697
        self.refresh_display(event)
698
        
699
    def update_scale(self, event):
700 701 702
        scale = float(self.scale.GetValue())
        self.display_frame.scale = scale
        self._set_setting('project_scale',scale)
703
        self.refresh_display(event)
704 705 706 707 708
        
    def update_interval(self, event):
        interval = float(self.interval.GetValue())
        self.display_frame.interval = interval
        self._set_setting('project_interval',interval)
Gary Hodgson's avatar
Gary Hodgson committed
709
        self.set_estimated_time()
710
        self.refresh_display(event)
711 712 713 714 715
        
    def update_pause(self, event):
        pause = float(self.pause.GetValue())
        self.display_frame.pause = pause
        self._set_setting('project_pause',pause)
Gary Hodgson's avatar
Gary Hodgson committed
716
        self.set_estimated_time()
717
        self.refresh_display(event)
718 719 720 721 722
    
    def update_overshoot(self, event):
        overshoot = float(self.overshoot.GetValue())
        self.display_frame.pause = overshoot
        self._set_setting('project_overshoot',overshoot)
723 724 725 726 727 728 729 730 731 732 733

    def update_prelift_gcode(self, event):
        prelift_gcode = self.prelift_gcode.GetValue().replace('\n', "\\n")
        self.display_frame.prelift_gcode = prelift_gcode
        self._set_setting('project_prelift_gcode',prelift_gcode)
        
    def update_postlift_gcode(self, event):
        postlift_gcode = self.postlift_gcode.GetValue().replace('\n', "\\n")
        self.display_frame.postlift_gcode = postlift_gcode
        self._set_setting('project_postlift_gcode',postlift_gcode)
        
734 735 736 737
    def update_z_axis_rate(self, event):
        z_axis_rate = int(self.z_axis_rate.GetValue())
        self.display_frame.z_axis_rate = z_axis_rate
        self._set_setting('project_z_axis_rate',z_axis_rate)
738 739 740 741 742
        
    def update_direction(self, event):
        direction = self.direction.GetValue()
        self.display_frame.direction = direction
        self._set_setting('project_direction',direction)
743
        
744
    def update_fullscreen(self, event):
745
        if (self.fullscreen.GetValue()):
746
            self.display_frame.ShowFullScreen(1)
747
        else:
748
            self.display_frame.ShowFullScreen(0)
749
        self.refresh_display(event)
750
    
751
    def update_resolution(self, event):
752 753 754 755 756
        x = float(self.X.GetValue())
        y = float(self.Y.GetValue())
        self.display_frame.resize((x,y))
        self._set_setting('project_x',x)
        self._set_setting('project_y',y)
757
        self.refresh_display(event)
758
    
759 760 761 762 763 764 765
    def get_dpi(self):
        resolution_x_pixels = int(self.X.GetValue())
        projected_x_mm = float(self.projected_X_mm.GetValue())
        projected_x_inches = projected_x_mm / 25.4
        
        return resolution_x_pixels / projected_x_inches                         
        
766 767 768 769 770
    def start_present(self, event):
        if not hasattr(self, "layers"):
            print "No model loaded!"
            return
        
771
        self.pause_button.SetLabel("Pause")
Gary Hodgson's avatar
Gary Hodgson committed
772
        self.set_current_layer(0)
773
        self.display_frame.Raise()
774
        if (self.fullscreen.GetValue()):
775 776
            self.display_frame.ShowFullScreen(1)
        self.display_frame.slicer = self.layers[2]
777
        self.display_frame.dpi = self.get_dpi()
778
        self.display_frame.present(self.layers[0][:],
779 780 781
            thickness=float(self.thickness.GetValue()),
            interval=float(self.interval.GetValue()),
            scale=float(self.scale.GetValue()),
782
            pause=float(self.pause.GetValue()),
783
            overshoot=float(self.overshoot.GetValue()),
784
            z_axis_rate=int(self.z_axis_rate.GetValue()),
785 786
            prelift_gcode=self.prelift_gcode.GetValue(),
            postlift_gcode=self.postlift_gcode.GetValue(),
787
            direction=self.direction.GetValue(),
788
            size=(float(self.X.GetValue()), float(self.Y.GetValue())),
789 790
            offset=(float(self.offset_X.GetValue()), float(self.offset_Y.GetValue())),
            layer_red=self.layer_red.IsChecked())
791 792 793
        
    def stop_present(self, event):
        print "Stop"
794
        self.pause_button.SetLabel("Pause")
Gary Hodgson's avatar
Gary Hodgson committed
795
        self.set_current_layer(0)
796
        self.display_frame.running = False
Gary Hodgson's avatar
Gary Hodgson committed
797
        
798
    def pause_present(self, event):        
799
        if self.pause_button.GetLabel() == 'Pause':
800
            print "Pause"
801
            self.pause_button.SetLabel("Continue")
802
            self.display_frame.running = False
803 804
        else:
            print "Continue"
805
            self.pause_button.SetLabel("Pause")
806 807 808
            self.display_frame.running = True
            self.display_frame.next_img()

809
if __name__ == "__main__":
810 811
    provider = wx.SimpleHelpProvider()
    wx.HelpProvider_Set(provider)
Gary Hodgson's avatar
Gary Hodgson committed
812 813
    #a = wx.App(redirect=True,filename="mylogfile.txt")
    a = wx.App()
814
    SettingsFrame(None).Show()
815
    a.MainLoop()