"""
Recursive tree drawing program modeled after Dan Garcia's BYOB code
from his course the Beauty and Joy of Computing.
Authors: Adam Moran and Dave Touretzky
Course: 15-294: Rapid Prototyping Technologies

--README--

Usage: 'treegen.py -s [A|B]

treegen.py is a recursive tree drawing program that outputs to both
Tkinter (for previewing) and DXF (for laser cutting).

The program flow is as follows:
1. main() is called to interpret the arguments and call start()
2. start(type) draws the stand base and a vertical branch, and initiates the recursion.
3. branch(l,r) draws the two parallel lines to form a branch, adjusting their
   lengths as needed when used to form a vee shape.
4. vee() recursively calls itself to make a forked branch or call a terminal function.
5. square() is a terminal function that draws a square terminal node.
"""

import math
import random
import getopt,sys
from tkinter import *
from dxfwrite import DXFEngine as dxf

### Initialize Globals ###
(x,y) = (400,600) # Current drawing point
theta = 0        # Current heading
pen = True        # Pen is down (will draw) when True
branch_width = 10 # Must be an even number of pixels
branch_length = 60
branch_angle = 70 # in degrees
dxf_scale = 1 / 3.0   # scale factor to convert pixels to mm
material_thickness = 3.175 # this value in mm
slot_unit = (material_thickness)/dxf_scale # divide out the scale to cancel
stand_unit = slot_unit+(branch_width/2) # varies stand with branch and material
total_objects = 0 # in case we want to count terminal nodes

# This is a hairy computation to make well-formed vee junctions
s = branch_width*math.cos(math.radians(branch_angle/2)) - branch_width/2
mitre_length = s / math.sin(math.radians(branch_angle/2))

### Setup Tkinter ###
master = Tk()
w = Canvas(master, width=1000, height=900)

### Setup DXF Outputs ###
drawing = dxf.drawing('output.dxf')
drawing.header['$LUNITS'] = 4   # units in millimeters
drawing.add_layer('LINES')


""" Description: Lifts up the virtual pen that draws the lines
                 in Tkinter or in the DXF.  If the pen is up when you call
                 forward(), nothing will be drawn, however, the global (x,y)
                 will be updated accordingly.
"""
def pen_up():
    global pen
    pen = False


""" Description: Lowers the virtual pen that draws the lines
                 in Tkinter or in the DXF.  If the pen is down when you call
                 forward(), a line will be written to Tkinter and the DXF.
"""
def pen_down():
    global pen
    pen = True


""" Description:  This is a 'turtle graphics' function that advances the
                  turtle in the forward direction.  If the pen is down, it
                  draws a line, both in Tkinter and in the DXF.
    Inputs: dist [int] -> the distance to move forward in pixels
                          (in the direction of the global, theta)
"""
def forward(dist):
    global x, y, dxf_scale
    new_x = x - dist * math.sin(math.radians(theta-90))
    new_y = y - dist * math.cos(math.radians(theta-90))
    if pen:
        w.create_line(int(x), int(y), int(new_x), int(new_y), fill='blue')
        drawing.add(dxf.line((float(x)*dxf_scale, float(y)*dxf_scale),
                             (float(new_x)*dxf_scale, float(new_y)*dxf_scale),
                             color = 5, #blue
                             layer = 'LINES'))
    (x,y) = (new_x, new_y)


""" Description:  This is a 'turtle graphics' function that changes the turtle's
                  current heading, held in the global variable theta.
    Inputs: angle [int] -> a number in degrees that gets added to the global theta
                           to change the current heading.
"""
def turn(angle):
    global theta
    theta += (-1*angle)


""" Description:  This is a helper function that takes an angle you would provide
                  to Tkinter and corrects for the DXF angle system.
                  IMPORTANT: In order for this function to work correctly, you
                             must not change theta in your circle function!
    Inputs: angle [int] -> the angle you passed to Tkinter
"""
def dxf_angle(angle):
    return angle - 2*theta


""" Description:  This 'turtle graphics' function draws two parallel lines in the
                  forward direction to be used for the branch edges.  The mitre
                  values are used to shorten one line so it forms a properly-fitted vee.
    Inputs: left_mitre [int] -> the distance to skip ahead in the left line
            right_mitre [int] -> the distance to skip ahead in the right line
"""
def branch(left_mitre,right_mitre):
    global x, y, theta
    (oldx,oldy,oldtheta) = (x,y,theta)
    # draw the left line
    pen_up()
    turn(-90)
    forward(branch_width/2)
    turn(90)
    forward(left_mitre)
    pen_down()
    forward(branch_length-left_mitre)
    # draw the right line
    (x,y,theta) = (oldx,oldy,oldtheta)
    pen_up()
    turn(90)
    forward(branch_width/2)
    turn(-90)
    forward(right_mitre)
    pen_down()
    forward(branch_length-right_mitre)
    # advance to endpoint
    (x,y,theta) = (oldx,oldy,oldtheta)
    pen_up()
    forward(branch_length)

""" Description: This is the recursive function that either draws a
    terminal node, or a fork with two recursive calls.
"""
def vee():
    choices = (square, square, square, vee, vee)
    global x, y, theta
    (oldx,oldy,oldtheta) = (x,y,theta)
    pen_up()
    turn(90)
    forward(branch_width/2)
    turn(-90)
    turn(-(90-branch_angle/2))
    forward(branch_width/2)
    turn(90)
    branch(mitre_length,0)
    random.choice(choices)()
    (x,y,theta) = (oldx,oldy,oldtheta)
    pen_up()
    turn(-90)
    forward(branch_width/2)
    turn(90)
    turn(90-branch_angle/2)
    forward(branch_width/2)
    turn(-90)
    branch(0,mitre_length)
    random.choice(choices)()


""" Description:  This function draws a square at the current global (x,y)
                  position.
"""
def square():
    global total_objects
    total_objects += 1
    pen_up()
    turn(-90)
    forward(branch_width/2)
    pen_down()
    forward(branch_width*2)
    for i in range(3):
        turn(90)
        forward(branch_width*5)
    turn(90)
    forward(branch_width*2)


""" Description:  This function draws one of the 2 base stand types.
    Inputs: standtype [string] -> possibilities are 'A' or 'B'.
"""
def drawbase(standtype):
    print(" -> Drawing base " + standtype)
    if standtype == "A":
        pen_down()
        turn(-180)
        forward(stand_unit*9 + slot_unit)
        turn(-90)
        forward(stand_unit*3)
        turn(90)
        forward(stand_unit*5)
        turn(-90)
        forward(stand_unit*2)
        turn(-90)
        forward(stand_unit*8)
        turn(-90)
        forward(stand_unit*3)
        turn(90)
        forward(slot_unit)
        turn(90)
        forward(stand_unit*3)
        turn(-90)
        forward(stand_unit*6)
        forward(branch_width)
        forward(stand_unit*6)
        turn(-90)
        forward(stand_unit*3)
        turn(90)
        forward(slot_unit)
        turn(90)
        forward(stand_unit*3)
        turn(-90)
        forward(stand_unit*8)
        turn(-90)
        forward(stand_unit*2)
        turn(-90)
        forward(stand_unit*5)
        turn(90)
        forward(stand_unit*3)
        turn(-90)
        forward(stand_unit*9 + slot_unit)
        pen_up()
        forward(branch_width/2)
        turn(90)
    else:
        pen_down()
        turn(-180)
        forward(stand_unit*6)
        turn(-90)
        forward(stand_unit*3)
        turn(90)
        forward(slot_unit)
        turn(90)
        forward(stand_unit*3)
        turn(-90)
        forward(stand_unit*3)
        turn(-90)
        forward(stand_unit*3)
        turn(90)
        forward(stand_unit*5)
        turn(-90)
        forward(stand_unit*2)
        turn(-90)
        forward(stand_unit*14)
        forward(branch_width + 2*slot_unit)
        forward(stand_unit*14)
        turn(-90)
        forward(stand_unit*2)
        turn(-90)
        forward(stand_unit*5)
        turn(90)
        forward(stand_unit*3)
        turn(-90)
        forward(stand_unit*3)
        turn(-90)
        forward(stand_unit*3)
        turn(90)
        forward(slot_unit)
        turn(90)
        forward(stand_unit*3)
        turn(-90)
        forward(stand_unit*6)
        pen_up()
        forward(branch_width/2)
        turn(90)



""" Description:  The start function draws the stand type specified in the argument
                  to the python call, then draws a vertical branch, and then starts
                  the recursion.
    Inputs: standtype [string] -> possibilities are 'A' or 'B'.
"""
def start(standtype):
    drawbase(standtype)
    branch(0,0)
    vee()


def main(argv):
    standtype = 'A'
    try:
        opts, args = getopt.getopt(argv,"hs:",["standtype="])
    except getopt.GetoptError:
        print('Usage: treegen.py -s [A|B]')
        sys.exit(2)

    for opt, arg in opts:
        if opt == '-h':
            print('Usage: treegen.py -s [A|B]')
            sys.exit()
        elif opt in ("-s", "--standtype"):
            if arg != "A" and arg != "B":
                print('Standtype must be "A" or "B"')
                sys.exit(2)
            else:
                standtype = arg

    print(' -> Making a tree with stand type ' + standtype)
    w.pack()
    start(standtype)



if __name__ == "__main__":
    main(sys.argv[1:])
    drawing.save()
    print(' -> A DXF file output.dxf was placed in the current directory.')
    print(' -> Starting Tkinter preview')
    mainloop()
