# For Spirograph Pyrograph Instructable
'''
Created 09/10/2021
This version: 06/11/2022 for inclusion with an Instructable 

This version creates simple (circular paths) hypocycloids
on a white background for Simple Coasters

Output is controlled by setting the boolean do_print
output is jpeg image with spirograph parameters embedded in file name
and GCODE set to match my set up for 100mm coasters

Most parameters are in global variables for simple (non-GUI) use
As such it is the sort of program we used to write in the 1970s.
Set the parameters - run the program - get the output - repeat : simples!

@author: Dr Alan J Green
'''

# get some libraries
import sys
import wx
import math
import os
from pathlib import Path

#######
# Global variables!!!
#######

r_static = 200      # radius of static wheel - should be integers!
r_rotate = 50       # radius of rotating wheel
pens = [65, 50, 35]  # pen position(s) in Python list
steps = 500        # number of line segments to draw


do_print = False    #  set to false to just see pattern and decide whether to print
#do_print = True    #  uncomment to set to true to get jpeg and GCODE

my_directory = "d:/dev/Eclipse/results_op/Instruct/Laser/coasters-2/"

burn_x = 95 # size of object (mm)
burn_y = 95 # size of object (mm)

# offsets should match those used for axis drawing
burn_x_os = 11 +10 # laser offset (mm) add 10mm border 
burn_y_os = 58 +10  # laser offset (mm) add 10mm border
burn_z = 100       # focal length of laser (mm)
burn_thick = 3     # thickness of material (mm)




class drawSpiro_SC(wx.Frame):
    '''
    In this code I am just using wx to provide a Frame (a window) which 
    is set as a paint device context .
    I have not set up a GUI and there are no events or event handlers
    +++++++++
    This is not supposed to be an example of Good Programming Practice
    It is more of a scribbling block to play with the parameters prior to
    writing a full spirograph application.
    
    I have added an output function to get jpegs and GCODE
    which can be switched on and off by setting the global do_print.
    
    '''


    def __init__(self, parent, title):
        '''
        Constructor
        '''
        super(drawSpiro_SC, self).__init__(parent, title = title, size = (1000,1000))
        self.InitUI()
        
        
    def InitUI(self):
        self.Bind(wx.EVT_PAINT, self.OnDraw)
        self.Center()
        self.Show(True)
    
    ''' 
    output files
    use '/' and try to use pathlib to get Windoze to play nicely
    probably shouldn't create the directory if NOT _save_frames
    '''
    # =====================================================
    # some stuff to make GCODE files
    # =====================================================

    def GcodePreamble(self):
        lines = [
            "; gcode generated by hypocycloid generator",
            "; bounds: X116 Y163",
            "; (size of coaster + laser mount offset)",
            "; ajg 2022"
            ]
        return(lines)
    def GcodeSetup(self):
        lines = [
            "G90",      # absolute positioning
            "G21",      # work in mm
            "M106 S0",  # laser power (via fan control pwm) to 0
            f'G0 Z{burn_z + burn_thick}',  # raise head to laser focal distance
            "G28 X Y"   # home x & y axes
            ]
        return(lines)  
    def GcodeTail(self):
        lines = [
            "M106 S0",
            "; end code"
            ]
        return(lines)   
        
    def FrameSave(self, file_name):
#         print(op_file)
        '''
        output files
        use '/' and try to use pathlib to get Windoze to play nicely
        probably shouldn't create the directory if NOT _save_frames
        '''
        file_path = my_directory
        # check and make directory if necess
        if not os.path.exists(file_path):
            os.makedirs(file_path) 
        op_file = f'{file_path}{file_name}'
        
        f_path = Path(op_file)
        context = wx.ClientDC(self)
        memory = wx.MemoryDC()
        x, y = self.ClientSize
        # bitmap = wx.EmptyBitmap( x, y, -1) # generates depreciation warning
        bitmap = wx.Bitmap( x, y, -1) # is this empty? Does it matter if we are doing to blit the image into it?
        memory.SelectObject(bitmap)
        memory.Blit(0, 0, x, y, context, 0, 0)
        # memory.SelectObject(wx.NullBitmap) 
        #    from the tutorial, not sure what it does ? empty wxBitmap object
        #    possibly needed for GUI driven program to erase previous drawing
        bitmap.SaveFile(str(f_path), wx.BITMAP_TYPE_JPEG)
        
    # =====================================================
    # The drawing function
    # draws to screen, and if set, to GCODE file
    # =====================================================    
        
    def OnDraw(self, e): # e would be the event if this were event driven!
        global do_print # allows us to change its value
        '''
        hypocycloids
        http://www.mathematische-basteleien.de/spirographs.htm
        '''
        dc = wx.PaintDC(self)       
        dc.SetBackground(wx.Brush(wx.WHITE))
        dc.Clear()
        dc.SetPen(wx.Pen(wx.BLACK, 2))

        
        # general case two circles
        # each with radii r_static and r_rotate
        # (big and small in a real plastic spirograph)
        # r_static and r_rotate set in global variables
  
        # centre of static ring
        xCenter = 490 # centre needs tweaking
        yCenter = 480 # not sure why!
        
        # drawing parameters
        # steps set in global variables
        
        passes = math.lcm(r_static, r_rotate)/r_rotate #number of rotations to return to start (2pi cancelled out!)
        step = (2.0 * math.pi) / (steps/passes)
                
        # =============================
        # a quick run to get the max dimension for scaling
        # not clever programming, but
        # if it ain't broke, don't fix it
        # ============================= 
        
        max_d = -1000
        min_d = 1000
        p = max(pens)
        n = steps
        for n in range(1, steps+1):
            t = n* step
            x = ((r_static-r_rotate) * math.cos((r_rotate/r_static)*t)) + p*math.cos((t*((1-r_rotate/r_static))))                
            y = ((r_static-r_rotate) * math.sin((r_rotate/r_static)*t)) - p*math.sin((t*((1-r_rotate/r_static))))
            
            max_d = max([x, y, max_d])
            min_d = min([x, y, min_d])
        plot_size = max(max_d, (-1*min_d))
        scale = .9 * (500/plot_size) # not a big fan of hard coding this
        # =============================
        # set pyrography burning paramaters
        # =============================
        b_xSize = float(max(burn_x, burn_y)) 
        b_ySize = b_xSize
        
        b_xMin = burn_x_os+ .5 * (burn_x - b_xSize)
        b_yMin = burn_y_os + .5*(burn_y - b_ySize)
        
        b_xCent = b_xMin + b_xSize/2.0
        b_yCent = b_yMin + b_ySize/2.0
        b_Scale = (b_xSize/max_d) * 0.9
        
        # =============================
        # set up for saving files
        # =============================
        if do_print: # make a file name from the parameters                
            s_r_static = f"{r_static}"
            s_r_rotate = f"{r_rotate}"
            fn_jpeg = f"{s_r_static}-{s_r_rotate}-{len(pens)}.jpeg"
            fn_gcode = f"{my_directory}{s_r_static}-{s_r_rotate}-{len(pens)}.gcode"
            # =============================
            # create and open gcode file
            # =============================
           
            f_out_gc = open(fn_gcode, 'a')
            for s in self.GcodePreamble():
                f_out_gc.write(f'{s}\n')
            for s in self.GcodeSetup():
                f_out_gc.write(f'{s}\n')
           
        for p in pens:
            #start point
            t= 0 * step
            x = ((r_static-r_rotate) * math.cos((r_rotate/r_static)*t)) + p*math.cos((t*((1-r_rotate/r_static))))
            y = ((r_static-r_rotate) * math.sin((r_rotate/r_static)*t)) - p*math.sin((t*((1-r_rotate/r_static))))
              
            #translate and round for plotting
            x1 = int( (scale * x) + (xCenter +.5))
            y1 = int( (scale * y) + (yCenter +.5))
            '''
             Do the GCODE thing
            '''
            if do_print:
                # move head to start and turn on laser
                b_x = (x/2)*b_Scale + b_xCent
                b_y = (y/2)*b_Scale + b_yCent
                f_out_gc.write(f'G0 X{b_x:06.2f} Y{b_y:06.2f} F200 \n') 
                f_out_gc.write(f'M106 S255\n') 
    
            for n in range(1, steps+1):
                t = n* step
                x = (((r_static-r_rotate) * math.cos((r_rotate/r_static)*t)) + 
                     p*math.cos((t*((1-r_rotate/r_static)))))
                x2 = int((scale * x) + (xCenter + .5))
                y = (((r_static-r_rotate) * math.sin((r_rotate/r_static)*t)) - 
                     p*math.sin((t*((1-r_rotate/r_static)))))
                y2 = int((scale * y) + (yCenter + .5))
                  
                dc.DrawLine(x1, y1, x2, y2)
                x1 = x2
                y1 = y2
                '''
                 Do the GCODE thing
               '''
                if do_print:
                    b_x = (x/2)*b_Scale + b_xCent
                    b_y = (y/2)*b_Scale + b_yCent
                    f_out_gc.write(f'G0 X{b_x:06.2f} Y{b_y:06.2f} F200 \n')
            if do_print:
                # turn off laser
                f_out_gc.write(f'M106 S0\n') 
                             
        if do_print:
            self.FrameSave(fn_jpeg)
            #     Tail the gcode file with turn off the laser
            for s in self.GcodeTail():
                f_out_gc.write(f'{s}\n')
        
            f_out_gc.close()
            if not f_out_gc.closed:
                f_out_gc.close()
            
            do_print = False                  
     
if __name__ == "__main__" :  
    app = wx.App()
    drawSpiro_SC(None, 'hypocycloid')
    app.MainLoop()
        