#!/usr/bin/env python

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

import sys
import math
import simplestyle
import simpletransform
import inkex
from copy import deepcopy

# 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):
        """ Parses the command line options ( Base diameter, cut diameter and height of cone).
        """
        inkex.Effect.__init__(self) # Call the base class constructor.
        # Describe parameters
        self.OptionParser.add_option('-b', '--diaBase', action = 'store',
            type = 'float', dest = 'diaBase', default = 300.0,
            help = 'The diameter of the cones base.')

        self.OptionParser.add_option('-c', '--diaCut', action = 'store',
            type = 'float', dest = 'diaCut', default = 100.0,
            help = 'The diameter of cones cut (0.0 if cone is not cut.')

        self.OptionParser.add_option('-l', '--heightCone', action = 'store',
            type = 'float', dest = 'heightCone', default = 200.0,
            help = 'The height of the (cut) cone.')
        
        self.OptionParser.add_option('-u', '--units', action = 'store',
            type = 'string', dest = 'units', default = 'mm',
            help = 'The units in which the cone values are given. mm or in for real objects')
        
        self.OptionParser.add_option('-w', '--strokeWidth', action = 'store',
            type = 'float', dest = 'strokeWidth', default = 0.3,
            help = 'The line thickness in given unit. For laser cutting it should be rather small.')

        self.OptionParser.add_option('-f', '--strokeColour', action = 'store',
            type = 'string', dest = 'strokeColour', default = 896839168, # Blue
            help = 'The line colour.')

        self.OptionParser.add_option('-d', '--verbose', action = 'store',
            type = 'inkbool', dest = 'verbose', default = False,
            help = 'Enable verbose output of calculated parameters. Used for debugging or is someone needs the calculated values.')

    def getUnittouu(self, param):
        " compatibility between inkscape 0.48 and 0.91 "
        try:
            return inkex.unittouu(param)
        except AttributeError:
            return self.unittouu(param)

    def getColorString(self, longColor, verbose=False):
        """ Convert the long into a #RRGGBB color value
            - verbose=true pops up value for us in defaults
            conversion back is A + B*256^1 + G*256^2 + R*256^3
        """
        if verbose: inkex.debug("%s ="%(longColor))
        longColor = long(longColor)
        if longColor <0: longColor = long(longColor) & 0xFFFFFFFF
        hexColor = hex(longColor)[2:-3]
        hexColor = '#' + hexColor.rjust(6, '0').upper()
        if verbose: inkex.debug("  %s for color default value"%(hexColor))
        return hexColor

    # Marker arrows
    def makeMarkerstyle(self, name, rotate):
        " Markers added to defs for reuse "
        defs = self.xpathSingle('/svg:svg//svg:defs')
        if defs == None:
            defs = inkex.etree.SubElement(self.document.getroot(),inkex.addNS('defs','svg'))
        marker = inkex.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 = inkex.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' : simplestyle.formatStyle(style),
                            'd'     : self.build_arc(center, start, angle*anglefac, radius, lowside) }
            ell = inkex.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' : simplestyle.formatStyle(startstyle),
                            'd'     : self.build_arc(center, start, angle*anglefac/2-gap_angle/2*anglefac, radius, lowside) }
            ell = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
            endstyle = deepcopy(style)
            endstyle['marker-end'] = None
            line_attribs = {'style' : simplestyle.formatStyle(endstyle),
                            'd'     : self.build_arc(center, angle/2*anglefac+gap_angle/2*anglefac, angle*anglefac, radius, lowside) }
            inkex.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' : simplestyle.formatStyle(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 = inkex.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.getUnittouu("1" + self.options.units)
        # convert color
        self.options.strokeColour = self.getColorString(self.options.strokeColour)      
        # 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.view_center[0], self.view_center[1])
        grp_attribs = {inkex.addNS('label','inkscape'):'Sheet Metal Conus Group', 'transform':t}
        grp = inkex.etree.SubElement(self.current_layer, 'g', grp_attribs)

        linestyle = { 'stroke' : self.options.strokeColour, 'fill' : 'none',
                      'stroke-width': str(self.getUnittouu(str(self.options.strokeWidth) + self.options.units)) }
        line_attribs = {'style' : simplestyle.formatStyle(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'], dictCone['ptB'], convFactor) # A,B
        path += " " + self.build_arc(zeroCenter, 0.0, angle, dictCone['longRadius']*convFactor)
        path += " " + self.build_line(dictCone['ptC'], dictCone['ptD'], convFactor) # C,D
        path += self.build_arc(zeroCenter, 0.0, angle, dictCone['shortRadius']*convFactor)
        line_attribs['d'] = path
        ell = inkex.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 = inkex.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):
        " Make an arc - use degrees"
        # 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.getUnittouu(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.getUnittouu(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.debug( msg)

        # Mark center
        marker_length = max(5, longradius* unitFactor/100)
        line_attribs = {'style' : simplestyle.formatStyle(line_style),
                        inkex.addNS('label','inkscape') : 'center',
                        'd' : 'M -{0},-{0} L {0},{0}'.format(marker_length)}
        line = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        line_attribs = {'style' : simplestyle.formatStyle(line_style),
                        inkex.addNS('label','inkscape') : 'center',
                        'd' : 'M -{0},{0} L {0},-{0}'.format(marker_length)}
        line = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        # Draw tick marks
        line_attribs = {'style' : simplestyle.formatStyle(line_style), 'd' : 'M 0,-3 L 0,-30'}
        line = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        if cut_dia != 0:
            line_attribs = {'style' : simplestyle.formatStyle(line_style), 'd' : 'M {0},-3 L {0},-30'.format(shortradius * unitFactor)}
            line = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        line_attribs = {'style' : simplestyle.formatStyle(line_style), 'd' : 'M {0},-3 L {0},-30'.format(longradius * unitFactor)}
        line = inkex.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':simplestyle.formatStyle(text_style),
                         'x': str(shortradius*unitFactor/2),
                         'y': str(-15) }
            text = inkex.etree.SubElement(parent, 'text', text_atts)
            text.text = "%4.3f" %(shortradius)
        text_atts = {'style':simplestyle.formatStyle(text_style),
                     'x': str((shortradius + (longradius-shortradius)/2)*unitFactor),
                     'y': str(-15) }
        text = inkex.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' : simplestyle.formatStyle(line_style), 'd' : 'M 3,0 L %4.2f,0' % (ptA[0]*unitFactor*0.8)}
        line = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        line_attribs = {'style' : simplestyle.formatStyle(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 = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        # arc
        arc_rad = ptA[0]*unitFactor*0.50
        gap = self.getUnittouu(str(font_height*2)+"pt")
        textpos = self.drawDimArc((0,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':simplestyle.formatStyle(text_style),
                     'x': str(textpos[0]),
                     'y': str(textpos[1]) }
        text = inkex.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':simplestyle.formatStyle(text_style),
                     'transform': 'rotate(%f)' % (line_angle) }
        text = inkex.etree.SubElement(parent, 'text', text_atts)
        scale_matrix = [[1, 0.0, centerx], [0.0, 1, ypos]] # needs cos,sin corrections
        simpletransform.applyTransformToNode(scale_matrix, text)
        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 = inkex.etree.SubElement(parent, 'text', text_atts)
            scale_matrix = [[1, 0.0, centerx], [0.0, 1, ypos]]
            simpletransform.applyTransformToNode(scale_matrix, text)
            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': simplestyle.formatStyle(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 = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        simpletransform.applyTransformToNode(frustrum_repos, line)
        # ticks
        line_attribs = {'style': simplestyle.formatStyle(line_style),
                        'd': 'M %f,%f L %f,%f' %(-(5+cut_dia/2*unitFactor),0, -(5+base_dia/2*unitFactor),0 )}
        line = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs)
        simpletransform.applyTransformToNode(frustrum_repos, line)
        #
        line = self.drawDimension((-base_dia/2*unitFactor,0), (-base_dia/2*unitFactor,cone_height*unitFactor), arrow_style, parent)
        simpletransform.applyTransformToNode(frustrum_repos, line)
        # frustum text
        text_atts = {'style':simplestyle.formatStyle(text_style),
                     'x': str(-(18+base_dia/2*unitFactor)),
                     'y': str(cone_height*unitFactor/2) }
        text = inkex.etree.SubElement(parent, 'text', text_atts)
        text.text = "%4.3f" %(cone_height)
        simpletransform.applyTransformToNode(frustrum_repos, text)
        if cut_dia >= 0.001:
            text_atts = {'style':simplestyle.formatStyle(text_style),
                         'x': '0',
                         'y': str(font_height) }
            text = inkex.etree.SubElement(parent, 'text', text_atts)
            text.text = "%4.3f" %(cut_dia)
            simpletransform.applyTransformToNode(frustrum_repos, text)
        text_atts = {'style':simplestyle.formatStyle(text_style),
                     'x': '0',
                     'y': str(cone_height*unitFactor+font_height) }
        text = inkex.etree.SubElement(parent, 'text', text_atts)
        text.text = "%4.3f" %(base_dia)
        simpletransform.applyTransformToNode(frustrum_repos, text)
        
        

# Create effect instance and apply it.
effect = SheetMetalConus()
effect.affect()


