# This python script wizard creates a mitered bend for microwave applications
# https://lists.launchpad.net/kicad-developers/msg17996.html
# Author  Henrik Forsten & easyw
# improved pads using Primitive pads, single net node 

from __future__ import division
import FootprintWizardBase

from pcbnew import *
import math

class UWMiterFootprintWizard(FootprintWizardBase.FootprintWizard):
    def GetName(self):
        return "uW Mitered Bend"

    def GetDescription(self):
        return "Mitered Bend Footprint Wizard"

    def GenerateParameterList(self):
        self.AddParam("Corner", "width", self.uMM, 1.31968)
        self.AddParam("Corner", "height", self.uMM, 1.57)
        self.AddParam("Corner", "*angle", self.uDegrees, 90)
        self.AddParam("Corner", "solder_clearance", self.uMM, 0.0)

# class UWMiterFootprintWizard(FootprintWizardBase.FootprintWizard):
#     def __init__(self):
#         FootprintWizardBase.FootprintWizard.__init__(self)
#         self.name = "uW Mitered Bend"
#         self.description = "Mitered Bend Footprint Wizard"
#         self.parameters = {
#             "Corner":{
#                      "width":           FromMM(0.34),
#                      "height":          FromMM(0.17),
#                      "*angle":          90,
#                      }
#         }
#
#        self.ClearErrors()

    # build a rectangular pad
    def smdRectPad(self, module, size, pos, name, angle, layer, solder_clearance):
            pad = D_PAD(module)
            pad.SetSize(size)
            pad.SetShape(PAD_SHAPE_RECT) #PAD_RECT)
            pad.SetAttribute(PAD_ATTRIB_SMD) #PAD_SMD)
            #Set only the copper layer without mask
            #since nothing is mounted on these pads
            pad.SetLayerSet( LSET(F_Cu) )
            pad.SetPos0(pos)
            pad.SetPosition(pos)
            pad.SetPadName(name)
            pad.Rotate(pos, angle)
            if solder_clearance > 0:
                pad.SetLocalSolderMaskMargin(solder_clearance)
                pad.SetLayerSet(pad.ConnSMDMask())
            else:
                pad.SetLayerSet( LSET(layer) )
            #Set clearance to small value, because
            #pads can be very close together.
            #If distance is smaller than clearance
            #DRC doesn't allow routing the pads
            pad.SetLocalClearance(1)
            return pad
            
    # build a custom pad
    def smdCustomPolyPad(self, module, size, pos, name, vpoints, layer, solder_clearance):
        pad = D_PAD(module)
        ## NB pads must be the same size and have the same center
        pad.SetSize(size)
        #pad.SetSize(pcbnew.wxSize(size[0]/5,size[1]/5))
        pad.SetShape(PAD_SHAPE_CUSTOM) #PAD_RECT)
        pad.SetAttribute(PAD_ATTRIB_SMD) #PAD_SMD)
        #pad.SetDrillSize (0.)
        #Set only the copper layer without mask
        #since nothing is mounted on these pads
        #pad.SetPos0(wxPoint(0,0)) #pos)
        #pad.SetPosition(wxPoint(0,0)) #pos)
        pad.SetPos0(pos)
        pad.SetPosition(pos)
        #pad.SetOffset(pos)
        pad.SetPadName(name)
        #pad.Rotate(pos, angle)
        pad.SetAnchorPadShape(PAD_SHAPE_RECT) #PAD_SHAPE_CIRCLE) #PAD_SHAPE_RECT)
        if solder_clearance > 0:
            pad.SetLocalSolderMaskMargin(solder_clearance)
            pad.SetLayerSet(pad.ConnSMDMask())
        else:
            pad.SetLayerSet( LSET(layer) )
        
        pad.AddPrimitive(vpoints,0) # (size[0]))
        return pad

    def Polygon(self, points, layer):
            """
            Draw a polygon through specified points
            """
            import pcbnew
            
            polygon = pcbnew.EDGE_MODULE(self.module)
            polygon.SetWidth(0) #Disables outline

            polygon.SetLayer(layer)
            polygon.SetShape(pcbnew.S_POLYGON)

            polygon.SetPolyPoints(points)

            self.module.Add(polygon)


    # This method checks the parameters provided to wizard and set errors
    def CheckParameters(self):
        p = self.parameters
        width = p["Corner"]["width"]
        height = p["Corner"]["height"]
        angle = p["Corner"]["*angle"]

        errors = []
        if (width<0):
            errors.append("Width has invalid value")
        if width/height < 0.25:
            errors.append("Too small width to height ratio")
        if angle > 90:
            errors.append("Too large angle")
        if angle < 0:
            errors.append("Angle must be positive")
        errors = ', '.join(errors)
        print (errors)
        return errors == ""

    def bilinear_interpolation(self, x, y, points):
        '''http://stackoverflow.com/questions/8661537/how-to-perform-bilinear-interpolation-in-python
        Interpolate (x,y) from values associated with four points.

        The four points are a list of four triplets:  (x, y, value).
        The four points can be in any order.  They should form a rectangle.

            >>> bilinear_interpolation(12, 5.5,
            ...                        [(10, 4, 100),
            ...                         (20, 4, 200),
            ...                         (10, 6, 150),
            ...                         (20, 6, 300)])
            165.0

        '''
        # See formula at:  http://en.wikipedia.org/wiki/Bilinear_interpolation

        points = sorted(points)               # order points by x, then by y
        (x1, y1, q11), (_x1, y2, q12), (x2, _y1, q21), (_x2, _y2, q22) = points

        return (q11 * (x2 - x) * (y2 - y) +
                q21 * (x - x1) * (y2 - y) +
                q12 * (x2 - x) * (y - y1) +
                q22 * (x - x1) * (y - y1)
               ) / ((x2 - x1) * (y2 - y1) + 0.0)

    def OptimalMiter(self, w, h, angle):
        """Calculate optimal miter by interpolating from table.
        https://awrcorp.com/download/faq/english/docs/Elements/MBENDA.htm
        """
        wh = w/h
        whs = [0.5, 1.0, 2.0]
        angles = [0, 30, 60, 90, 120]
        table = [
            [0, 12, 45, 75, 98],
            [0, 19, 41, 63, 92],
            [0, 7, 31, 56, 79]
        ]
        for i, x in enumerate(whs):
            if x > wh:
                break
        for j, y in enumerate(angles):
            if y > angle:
                break
        i = min(i-1,1)
        j = min(j-1,3)
        px = lambda ii,jj: (whs[ii],angles[jj],table[ii][jj])
        x1 = px(i,j)
        x2 = px(i+1,j)
        y1 = px(i,j+1)
        y2 = px(i+1,j+1)
        return self.bilinear_interpolation(wh, angle, [x1,x2,y1,y2])/100.0

    # build the footprint from parameters
    #def BuildThisFootprint(self):
    def BuildFootprint(self):

        module = MODULE(None) # create a new module
        self.module = module
        self.buildmessages = ""

        if not self.CheckParameters():
            return

        p = self.parameters
        width = p["Corner"]["width"]
        height = p["Corner"]["height"]
        angle_deg = float(p["Corner"]["*angle"])
        angle = angle_deg*0.0174532925 #To radians
        sold_clear = p["Corner"]['solder_clearance']
        #reference and value
        #text_size = self.GetTextSize()  # IPC nominal
        
        textposy = width + FromMM(1)
        size_text = wxSize( FromMM( 0.6), FromMM( 0.5) )
        
        module.name = "'uwm_{0:.2f}_{1:0.2f}_{2:.0f}'".format(ToMM(width),ToMM(height),angle_deg)
        
        #module.SetReference("uwm_{0:.2f}_{1:0.2f}_{2:.0f}".format(ToMM(width),ToMM(height),angle_deg))
        module.SetReference("uwM***")           # give it a default value
        module.Reference().SetPos0(wxPoint(0, textposy))
        module.Reference().SetPosition(module.Reference().GetPos0())
        module.Reference().SetTextSize( size_text )
        if hasattr(module.Reference(), 'SetThickness'):
            module.Reference().SetThickness(FromMM(0.125))
        else:
            module.Reference().SetTextThickness(FromMM(0.125))
        module.Reference().SetVisible(True)

        textposy = textposy + FromMM(1)
        #module.SetValue("Val***")           # give it a default value
        module.SetValue("uwM_{0:.2f}_{1:0.2f}_{2:.0f}".format(ToMM(width),ToMM(height),angle_deg))
        module.Value().SetPos0( wxPoint(0, textposy) )
        module.Value().SetPosition(module.Value().GetPos0())
        module.Value().SetTextSize( size_text )
        module.Value().SetVisible(False) #0)

        # fpid = FPID(self.module.GetReference())   #the name in library
        # module.SetFPID( fpid )

        #Calculate the miter
        w = width

        #Width of the corner from edge of the corner to inside corner
        corner_width = ToMM(w)/math.cos(angle/2)

        #Get proportion of width to cut
        cut = self.OptimalMiter(width, height, angle_deg)
        cut_pc = cut
        print ("Cut: {0:.2f}%".format(cut*100))

        #Distance from uncut outside corner point to point 7
        cut = FromMM(cut*corner_width/math.cos((math.pi-angle)/2))

        #Distance between points 2 and 3 and points 3 and 4
        #Minimum of w/2 to satisfy DRC, otherwise pads are too close
        #and track connected to other pad overlaps the other one.
        #Rounded trace end can also stick out of the cut area
        #if a is too small.
        a = max(cut-width*math.tan(angle/2),w/2)

        #Distance between points 3 and 4
        x34 = a*math.sin(angle)
        y34 = a*math.cos(angle)
        #Distance between points 4 and 5
        x45 = width*math.cos(angle)
        y45 = width*math.sin(angle)

        #  1  2
        #8 +--+
        #  |  |3
        #7 \  --+ 4
        #   \   |
        #    \--+ 5
        #    6
        
        # maui extension of polygon
        #points = [
        #        (0,0-w/2),
        #        (w,0-w/2),
        #        (w,a),
        #        (w+x34+w/2*math.sin(angle),a+y34+w/2*math.cos(angle)),
        #        (w+x34-x45+w/2*math.sin(angle),a+y34+y45+w/2*math.cos(angle)),
        #        (cut*math.sin(angle),a+width*math.tan(angle/2)+cut*math.cos(angle)),
        #        (0,a+width*math.tan(angle/2)-cut),
        #        (0,0-w/2)]
        points = [
                (0-w/2,0-w/2+w/2),
                (w-w/2,0-w/2+w/2),
                (w-w/2,a+w/2),
                (w+x34+w/2*math.sin(angle)-w/2,a+y34+w/2*math.cos(angle)+w/2),
                (w+x34-x45+w/2*math.sin(angle)-w/2,a+y34+y45+w/2*math.cos(angle)+w/2),
                (cut*math.sin(angle)-w/2,a+width*math.tan(angle/2)+cut*math.cos(angle)+w/2),
                (0-w/2,a+width*math.tan(angle/2)-cut+w/2),
                (0-w/2,0-w/2+w/2)]

        #Last two points can be equal
        if points[-2] == points[-1]:
            points = points[:-1]

        points = [wxPoint(*point) for point in points]
        vpoints = wxPoint_Vector(points)
        #self.Polygon(points, F_Cu)

        #Create pads
        pad_l = width/2 #10 allowing big track to join the fp
        size_pad = wxSize(width,pad_l)
        
        #module.Add(self.smdRectPad(module, size_pad, wxPoint(width/2,-pad_l/2), "1", 0))
        layer = F_Cu; # sold_clear = 0
        module.Add(self.smdCustomPolyPad(module, size_pad, wxPoint(width/2,-pad_l/2), "1", vpoints, layer,sold_clear))
        # smdCustomPolyPad(self, module, size, pos, name, points, layer, solder_clearance):
        size_pad = wxSize(pad_l,width)

        #Halfway between points 4 and 5
        posx = ((w+x34) + (w+x34-x45))/2
        posy = ((a+y34) + (a+y34+y45))/2

        #Position pad so that pad edge touches polygon edge
        posx += (pad_l/2)*math.sin(angle)
        posy += (pad_l/2)*math.cos(angle)+w/4
        size_pad = wxSize(pad_l, width)
        #solder clearance added only to polygon
        module.Add(self.smdRectPad(module, size_pad, wxPoint(posx,posy), "1", (angle_deg-90)*10,layer,0.0))
        # moving anchor to center of first pad
        module.MoveAnchorPosition(wxPoint(-width/2,pad_l/2))
        # set SMD attribute
        module.SetAttributes(MOD_VIRTUAL)
        self.buildmessages = (
            "Building new {name} footprint with the following parameters:\n\n"
            .format(name=module.name))
        self.buildmessages += ("Track Width: {0:.4f}mm\n".format(ToMM(width)))
        self.buildmessages += ("PCB Height: {0:.4f}mm\n".format(ToMM(height)))
        self.buildmessages += ("Angle: {:.1f}deg\n\n".format(angle_deg))
        self.buildmessages += ("Cut: {0:.2f}%\n".format(cut_pc*100))
        __version__ = 1.5
        self.buildmessages += ("version: {:.1f}".format(__version__))
        

# create our footprint wizard
uwmiter_wizard = UWMiterFootprintWizard()

# register it into pcbnew
uwmiter_wizard.register()
