#!/usr/bin/env python3

# Distributed under the terms of the GNU Lesser General Public License v3.0

import math
import inkex
from copy import deepcopy
from lxml import etree
from inkex.transforms import Transform
from inkex import Color

# Helper functions
def calc_angle_between_points(p1, p2):
    xDiff = p2[0] - p1[0]
    yDiff = p2[1] - p1[1]
    return math.degrees(math.atan2(yDiff, xDiff))
	
def calc_dist_between_points(p1, p2):
    xDiff = p2[0] - p1[0]
    yDiff = p2[1] - p1[1]
    return math.sqrt(yDiff*yDiff + xDiff*xDiff)
	
def normalize(p1, p2):
    " p1,p2 defines a vector return normalized "
    xDiff = p2[0] - p1[0]
    yDiff = p2[1] - p1[1]
    magn = calc_dist_between_points(p1,p2)
    return (xDiff/magn, yDiff/magn)
	
def polar_to_cartesian(cx, cy, radius, angle_degrees):
    " So we can make arcs in the 'A' svg syntax. "
    angle_radians = math.radians(angle_degrees)
    return [
	          cx + (radius * math.cos(angle_radians)), 
			  cy + (radius * math.sin(angle_radians))
		    ]
	
def point_on_circle(radius, angle):
    " return xy coord of the point at distance radius from origin at angle "
    x = radius * math.cos(angle)
    y = radius * math.sin(angle)
    return [x, y]

class SheetMetalConus(inkex.Effect):
    """ Program to unfold a frustum of a cone or a cone 
        (if parameter diaCut=0) and generate a sheet cutting layout
        or flat pattern projection that can be rolled or bend up into a (truncated) cone shape.
    """
    color_marker_dim = '#703cd6'    # purple
    color_marker_chords = '#9d2222' # red
    color_marker_base = '#36ba36'   # green
    # Arrowed lines
    dimline_style = {'stroke'        : '#000000',
                     'stroke-width'  : '0.75px',
                     'fill'          : 'none',
                     'marker-start'  : 'url(#ArrowDIN-start)',
                     'marker-end'    : 'url(#ArrowDIN-end)'    }

    def __init__(self):
        inkex.Effect.__init__(self) # Call the base class constructor.
        self.arg_parser.add_argument('-b', '--diaBase', type = float, dest = 'diaBase', default = 300.0, help = 'The diameter of the cones base.')
        self.arg_parser.add_argument('-c', '--diaCut',  type = float, default = 100.0, help = 'The diameter of cones cut (0.0 if cone is not cut.')
        self.arg_parser.add_argument('-l', '--heightCone',  type = float, default = 200.0, help = 'The height of the (cut) cone.')
        self.arg_parser.add_argument('-u', '--units', default = 'mm', help = 'The units in which the cone values are given. mm or in for real objects')
        self.arg_parser.add_argument('-w', '--strokeWidth', type = float, default = 0.3, help = 'The line thickness in given unit. For laser cutting it should be rather small.')
        self.arg_parser.add_argument('-f', '--strokeColour', type=Color, default = 255, help = 'The line colour.')
        self.arg_parser.add_argument('-d', '--verbose', type = inkex.Boolean, default = False, help = 'Enable verbose output of calculated parameters. Used for debugging or is someone needs the calculated values.')

    # Marker arrows
    def makeMarkerstyle(self, name, rotate):
        " Markers added to defs for reuse "
        defs = self.svg.getElement('/svg:svg//svg:defs')
        if defs == None:
            defs = etree.SubElement(self.document.getroot(),inkex.addNS('defs','svg'))
        marker = etree.SubElement(defs ,inkex.addNS('marker','svg'))
        marker.set('id', name)
        marker.set('orient', 'auto')
        marker.set('refX', '0.0')
        marker.set('refY', '0.0')
        marker.set('style', 'overflow:visible')
        marker.set(inkex.addNS('stockid','inkscape'), name)

        arrow = etree.Element("path")
        # definition of arrows in beautiful DIN-shapes:
        if name.startswith('ArrowDIN-'):
            if rotate:
                arrow.set('d', 'M 8,0 -8,2.11 -8,-2.11 z')
            else:
                arrow.set('d', 'M -8,0 8,-2.11 8,2.11 z')
        if name.startswith('ArrowDINout-'):
            if rotate:
                arrow.set('d', 'M 0,0 16,2.11 16,0.5 26,0.5 26,-0.5 16,-0.5 16,-2.11 z')
            else:
                arrow.set('d', 'M 0,0 -16,2.11 -16,0.5 -26,0.5 -26,-0.5 -16,-0.5 -16,-2.11 z')
        arrow.set('style', 'fill:#000000;stroke:none')
        marker.append(arrow)

    def set_arrow_dir(self, option, style):
        if option=='inside':
            # inside
            self.arrowlen = 6.0
            style['marker-start'] = 'url(#ArrowDIN-start)'
            style['marker-end'] = 'url(#ArrowDIN-end)'
            self.makeMarkerstyle('ArrowDIN-start', False)
            self.makeMarkerstyle('ArrowDIN-end', True)
        else:
            # outside
            self.arrowlen = 0
            style['marker-start'] = 'url(#ArrowDINout-start)'
            style['marker-end'] = 'url(#ArrowDINout-end)'
            self.makeMarkerstyle('ArrowDINout-start', False)
            self.makeMarkerstyle('ArrowDINout-end', True)

    def drawDimArc(self, center, start, end, radius, style, parent, gap=0, lowside=True):
        " just the arrowed arc line "
        angle = abs(end-start)
        # inside or outside 
        inside = True
        critical_length = 35
        dist = calc_dist_between_points(point_on_circle(radius, start), point_on_circle(radius, end))
        if angle < 45 and dist > critical_length: inside = False
        # change start and end angles to make room for arrow markers
        arrow_angle = math.degrees(math.sin(self.arrowlen/radius))
        if lowside:
            start += arrow_angle
            angle -= arrow_angle
            anglefac = 1
        else:
            start -= arrow_angle
            angle -= arrow_angle
            anglefac = -1
        #
        if gap == 0:
            line_attribs = {'style' : str(inkex.Style(style)),
                            'd'     : self.build_arc(center, start, angle*anglefac, radius, lowside) }
            ell = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
        else: # leave a gap for label
            gap_angle = math.degrees(math.sin(gap/radius))
            startstyle = deepcopy(style)
            startstyle['marker-start'] = None
            line_attribs = {'style' : str(inkex.Style(startstyle)),
                            'd'     : self.build_arc(center, start, angle*anglefac/2-gap_angle/2*anglefac, radius, lowside) }
            ell = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
            endstyle = deepcopy(style)
            endstyle['marker-end'] = None
            line_attribs = {'style' : str(inkex.Style(endstyle)),
                            'd'     : self.build_arc(center, angle/2*anglefac+gap_angle/2*anglefac, angle*anglefac, radius, lowside) }
            etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
        # return pos in center of gap (or arc)
        textposangle = angle/2*anglefac
        return (point_on_circle(radius, math.radians(textposangle)))
        
    def drawDimension(self, a, b, style, parent):
        " draw arrowed dimensions using markers "
        # draw arrows as inside or outside dimension
        critical_length = 35.
        if calc_dist_between_points(a,b) > critical_length:
            self.set_arrow_dir('inside', style)
        else:
            self.set_arrow_dir('outside', style)
        attribs = {'style' : str(inkex.Style(style))}
        # account for length change so arrows fit
        norm = normalize(a, b)
        dim_start_x = a[0] + self.arrowlen*norm[0]
        dim_start_y = a[1] + self.arrowlen*norm[1]
        dim_end_x = b[0] - self.arrowlen*norm[0]
        dim_end_y = b[1] - self.arrowlen*norm[1]
        #
        attribs['d'] = 'M %f,%f %f,%f' % (dim_start_x, dim_start_y, dim_end_x, dim_end_y)
        dimline = etree.SubElement(parent, inkex.addNS('path', 'svg'), attribs)
        return dimline
    
    def calculateCone(self, dictCone):
        """ Calculates all relevant values in order to construct a cone.
            These values are:
            - short radius
            - long radius
            - angle of cone layout
            - chord of base diameter
            - chord of cut diameter
            - coordinates of points A, B, C and D
        """        
        dBase = dictCone['diaBase']
        dCut =  dictCone['diaCut']
        hCone = dictCone['heightCone']
        base = dBase - dCut
        # radius from top of cone to cut
        if dCut > 0:
            shortRadius = math.sqrt( dCut*dCut/4 + (dCut*hCone)/base * (dCut*hCone)/base )
        else:
            shortRadius=0.0
        dictCone['shortRadius'] = shortRadius
        ## radius from top of cone to base of cone
        longRadius=math.sqrt( dBase*dBase/4 + (dBase*hCone)/base * (dBase*hCone)/base )
        dictCone['longRadius'] = longRadius

        ## angle of circle sector
        angle=(math.pi * dBase) / longRadius
        dictCone['angle'] = angle
        # chord is the straight line between the 2 endpoints of an arc. 
        # Not used directly, but available in verbose output.
        chordBase = longRadius * math.sqrt( 2* (1-math.cos(angle)) )
        dictCone['chordBase'] = chordBase
        chordCut = shortRadius * math.sqrt( 2* (1-math.cos(angle)) )
        dictCone['chordCut'] = chordCut

        # calculate coordinates of points A, B, C and D
        # center M is at (0,0) and points A and B are on the x-axis:
        ptA = (shortRadius, 0.0)
        ptB = (longRadius,  0.0)
        # we can calculate points C and D with the given radii and the calculated angle
        ptC=(longRadius * math.cos(angle),  longRadius *  math.sin(angle))
        ptD=(shortRadius * math.cos(angle), shortRadius * math.sin(angle))
        dictCone['ptA'] = ptA
        dictCone['ptB'] = ptB
        dictCone['ptC'] = ptC
        dictCone['ptD'] = ptD

    def effect(self):
        """ Effect behaviour.
            - Overrides base class' method and draws rolled out sheet metal cone into SVG document.
        """
        # calc scene scale
        convFactor = self.svg.unittouu("1" + self.options.units)   
        # Store all the relevants values in a dictionary for easy access
        dictCone={'diaBase':    self.options.diaBase,
                  'diaCut':     self.options.diaCut,
                  'heightCone': self.options.heightCone }
        # Get all values needed in order to draw cone layout:
        self.calculateCone(dictCone)
        
        # Draw the cone layout:
        # Make top level group
        t = 'translate(%s,%s)' % (self.svg.namedview.center[0], self.svg.namedview.center[1])
        grp_attribs = {inkex.addNS('label','inkscape'):'Sheet Metal Conus Group', 'transform':t}
        grp = etree.SubElement(self.svg.get_current_layer(), 'g', grp_attribs)

        linestyle = { 'stroke' : self.options.strokeColour, 'fill' : 'none',
                      'stroke-width': str(self.svg.unittouu(str(self.options.strokeWidth) + self.options.units)) }
        line_attribs = {'style' : str(inkex.Style(linestyle)), inkex.addNS('label','inkscape') : 'Cone' }
        
        # Connect the points into a single path of lines and arcs
        zeroCenter=(0.0, 0.0)
        angle = math.degrees(dictCone['angle'])
        path = ""
        path += self.build_line(dictCone['ptA'][0], dictCone['ptA'][1], dictCone['ptB'][0], dictCone['ptB'][1], convFactor) # A,B
        path += " " + self.build_arc(zeroCenter[0], zeroCenter[1], 0.0, angle, dictCone.get('longRadius')*convFactor)
        path += " " + self.build_line(dictCone['ptC'][0], dictCone['ptC'][1],dictCone['ptD'][0], dictCone['ptD'][1], convFactor) # C,D
        path += self.build_arc(zeroCenter[0], zeroCenter[1], 0.0, angle, dictCone['shortRadius']*convFactor)
        line_attribs['d'] = path
        ell = etree.SubElement(grp, inkex.addNS('path','svg'), line_attribs )
        
        # Draw Dimensions Markup
        if self.options.verbose == True:
            grp_attribs = {inkex.addNS('label','inkscape'):'markup'}
            markup_group = etree.SubElement(grp, 'g', grp_attribs)
            self.beVerbose(dictCone, convFactor, markup_group)
                
    def build_arc(self, x, y, start_angle, end_angle, radius, reverse=True, swap=False):
        # Not using internal arc rep - instead construct path A in svg style directly
        # so we can append lines to make single path
        start = polar_to_cartesian(x, y, radius, end_angle)
        end = polar_to_cartesian(x, y, radius, start_angle)
        arc_flag = 0 if reverse else 1
        sweep = 0 if (end_angle-start_angle) <=180 else 1
        if swap: sweep = 1-sweep
        path = 'M %s,%s' % (start[0], start[1])
        path += " A %s,%s 0 %d %d %s %s" % (radius, radius, sweep, arc_flag, end[0], end[1])
        return path
    
    def build_line(self, x1, y1, x2, y2, unitFactor):
        path = 'M %s,%s L %s,%s' % (x1*unitFactor, y1*unitFactor, x2*unitFactor, y2*unitFactor)
        return path

    def beVerbose(self, dictCone, unitFactor, parent):
        """ Verbose output of calculated values. 
            Can be used for debugging purposes or if calculated values needed.
        """
        # unpack
        base_dia = dictCone['diaBase']
        cut_dia = dictCone['diaCut']
        cone_height = dictCone['heightCone']
        shortradius = dictCone['shortRadius']
        longradius = dictCone['longRadius']
        angle = dictCone['angle']
        chord_base = dictCone['chordBase']
        chord_cut = dictCone['chordCut']
        ptA = dictCone['ptA']
        ptB = dictCone['ptB']
        ptC = dictCone['ptC']
        ptD = dictCone['ptD']

        # styles for markup
        stroke_width = max(0.1, self.svg.unittouu(str(self.options.strokeWidth/2) + self.options.units))
        line_style = { 'stroke': self.color_marker_dim, 'stroke-width': str(stroke_width), 'fill':'none' }
        arrow_style = self.dimline_style
        font_height = min(32, max( 8, int(self.svg.unittouu(str(longradius/40) + self.options.units))))
        text_style = { 'font-size': str(font_height),
                       'font-family': 'arial',
                       'text-anchor': 'middle',
                       'text-align': 'center',
                       'fill': self.color_marker_dim }
        # verbose message for debug window
        msg = "Base diameter: " + str(base_dia) + "Cut diameter: " + str(cut_dia) + \
              "\nCone height: " + str(cone_height) + "\nShort radius: " + str(shortradius) + \
              "\nLong radius: " + str(longradius) + "\nAngle of circle sector: " + str(angle) + \
              " radians (= " + str(math.degrees(angle)) + " degrees)" + \
              "\nChord length of base arc: " + str(chord_base) + \
              "\nChord length of cut arc: " + str(chord_cut)
        #inkex.utils.debug( msg)

        # Mark center
        marker_length = max(5, longradius* unitFactor/100)
        line_attribs = {'style' : str(inkex.Style(line_style)),
                        inkex.addNS('label','inkscape') : 'center',
                        'd' : 'M -{0},-{0} L {0},{0}'.format(marker_length)}
        line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        line_attribs = {'style' : str(inkex.Style(line_style)),
                        inkex.addNS('label','inkscape') : 'center',
                        'd' : 'M -{0},{0} L {0},-{0}'.format(marker_length)}
        line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        # Draw tick marks
        line_attribs = {'style' : str(inkex.Style(line_style)), 'd' : 'M 0,-3 L 0,-30'}
        line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        if cut_dia != 0:
            line_attribs = {'style' : str(inkex.Style(line_style)), 'd' : 'M {0},-3 L {0},-30'.format(shortradius * unitFactor)}
            line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        line_attribs = {'style' : str(inkex.Style(line_style)), 'd' : 'M {0},-3 L {0},-30'.format(longradius * unitFactor)}
        line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        # span line
        arrow_style['stroke'] = self.color_marker_dim
        self.drawDimension((0,-10), (shortradius * unitFactor, -10), arrow_style, parent)
        self.drawDimension((shortradius * unitFactor,-10), (longradius * unitFactor, -10), arrow_style, parent)
        # labels for short, long radii
        if cut_dia >= 0.001:
            text_atts = {'style':str(inkex.Style(text_style)),
                         'x': str(shortradius*unitFactor/2),
                         'y': str(-15) }
            text = etree.SubElement(parent, 'text', text_atts)
            text.text = "%4.3f" %(shortradius)
        text_atts = {'style':str(inkex.Style(text_style)),
                     'x': str((shortradius + (longradius-shortradius)/2)*unitFactor),
                     'y': str(-15) }
        text = etree.SubElement(parent, 'text', text_atts)
        text.text = "%4.3f" %(longradius)
        # Draw angle
        lowside = math.degrees(angle) < 180
        value = math.degrees(angle) if lowside else 360-math.degrees(angle)
        # radial limit lines
        line_attribs = {'style' : str(inkex.Style(line_style)), 'd' : 'M 3,0 L %4.2f,0' % (ptA[0]*unitFactor*0.8)}
        line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        line_attribs = {'style' : str(inkex.Style(line_style)), 'd' : 'M %4.2f,%4.2f L %4.2f,%4.2f' % (ptD[0]*unitFactor*0.02, ptD[1]*unitFactor*0.02,ptD[0]*unitFactor*0.8, ptD[1]*unitFactor*0.8)}
        line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        # arc
        arc_rad = ptA[0]*unitFactor*0.50
        gap = self.svg.unittouu(str(font_height*2)+"pt")
        textpos = self.drawDimArc(0, 0, value, arc_rad, arrow_style, parent, gap, lowside)
        # angle label
        textpos[1] += font_height/4 if lowside else font_height/2
        text_atts = {'style':str(inkex.Style(text_style)),
                     'x': str(textpos[0]),
                     'y': str(textpos[1]) }
        text = etree.SubElement(parent, 'text', text_atts)
        text.text = "%4.2f deg" %(value)
        # chord lines
        dash_style = deepcopy(arrow_style)
        dash_style['stroke'] = self.color_marker_chords
        dash_style['stroke-dasharray'] = '4, 2, 1, 2'
        line = self.drawDimension((ptA[0]*unitFactor, ptA[1]*unitFactor), (ptD[0]*unitFactor, ptD[1]*unitFactor), dash_style, parent)
        line = self.drawDimension((ptB[0]*unitFactor, ptB[1]*unitFactor), (ptC[0]*unitFactor, ptC[1]*unitFactor), dash_style, parent)
        # chord labels
        centerx = ptB[0]*unitFactor + (ptC[0]-ptB[0])*unitFactor/2
        centery = ptB[1]*unitFactor + (ptC[1]-ptB[1])*unitFactor/2
        line_angle = calc_angle_between_points(ptC, ptB)
        ypos = centery+font_height+2 if line_angle<0 else centery-2
        text_style['fill'] = self.color_marker_chords
        text_atts = {'style':str(inkex.Style(text_style)),
                     'transform': 'rotate(%f)' % (line_angle) }
        text = etree.SubElement(parent, 'text', text_atts)
        scale_matrix = [[1, 0.0, centerx], [0.0, 1, ypos]] # needs cos,sin corrections
        text.transform = Transform(scale_matrix) * text.transform
        text.text = "%4.2f" % (chord_base)
        if cut_dia >= 0.001:
            centerx = ptA[0]*unitFactor + (ptD[0]-ptA[0])*unitFactor/2
            centery = ptA[1]*unitFactor + (ptD[1]-ptA[1])*unitFactor/2
            xpos = centerx - font_height*math.sin(math.radians(abs(line_angle)))
            ypos = centery-2 if line_angle>0 else centery+font_height+2
            text = etree.SubElement(parent, 'text', text_atts)
            scale_matrix = [[1, 0.0, centerx], [0.0, 1, ypos]]
            text.transform = Transform(scale_matrix) * text.transform
            text.text = "%4.2f" % (chord_cut)
        # frustum lines
        frustrum_repos = [[1, 0.0, 1], [0.0, 1, math.sqrt(pow(shortradius*unitFactor,2)-pow(cut_dia*unitFactor/2,2))]]
        text_style['fill'] = self.color_marker_base
        line_style['stroke'] = self.color_marker_base
        arrow_style['stroke'] = self.color_marker_base
        line_attribs = {'style': str(inkex.Style(line_style)),
                        'd': 'M %f,%f L %f,%f %f,%f %f,%f z' %(-cut_dia/2*unitFactor,0, cut_dia/2*unitFactor,0, base_dia/2*unitFactor,cone_height*unitFactor, -base_dia/2*unitFactor,cone_height*unitFactor)}
        line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        line.transform = Transform(frustrum_repos) * line.transform
        # ticks
        line_attribs = {'style': str(inkex.Style(line_style)),
                        'd': 'M %f,%f L %f,%f' %(-(5+cut_dia/2*unitFactor),0, -(5+base_dia/2*unitFactor),0 )}
        line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        line.transform = Transform(frustrum_repos) * line.transform
        #
        line = self.drawDimension((-base_dia/2*unitFactor,0), (-base_dia/2*unitFactor,cone_height*unitFactor), arrow_style, parent)
        line.transform = Transform(frustrum_repos) * line.transform
        # frustum text
        text_atts = {'style':str(inkex.Style(text_style)),
                     'x': str(-(18+base_dia/2*unitFactor)),
                     'y': str(cone_height*unitFactor/2) }
        text = etree.SubElement(parent, 'text', text_atts)
        text.text = "%4.3f" %(cone_height)
        text.transform = Transform(frustrum_repos) * text.transform
        if cut_dia >= 0.001:
            text_atts = {'style':str(inkex.Style(text_style)),
                         'x': '0',
                         'y': str(font_height) }
            text = etree.SubElement(parent, 'text', text_atts)
            text.text = "%4.3f" %(cut_dia)
            text.transform = Transform(frustrum_repos) * text.transform
        text_atts = {'style':str(inkex.Style(text_style)),
                     'x': '0',
                     'y': str(cone_height*unitFactor+font_height) }
        text = etree.SubElement(parent, 'text', text_atts)
        text.text = "%4.3f" %(base_dia)
        text.transform = Transform(frustrum_repos) * text.transform
        
if __name__ == '__main__':
    SheetMetalConus().run()