#!/usr/bin/env python3
'''
Copyright (C) 2007 Aaron Spike  (aaron @ ekips.org)
Copyright (C) 2007 Tavmjong Bah (tavmjong @ free.fr)
Copyright (C) http://cnc-club.ru/forum/viewtopic.php?f=33&t=434&p=2594#p2500
Copyright (C) 2014 Jürgen Weigert (juewei@fabmail.org)
Copyright (C) 2020 Spadino (spada.andrea @ gmail DOT com)

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

2014-03-20 jw@suse.de 0.2  Option --accuracy=0 for automatic added.
2014-03-21                 sent upstream: https://bugs.launchpad.net/inkscape/+bug/1295641
2014-03-21 jw@suse.de 0.3  Fixed center of rotation for gears with odd number of teeth.
2014-04-04 juewei     0.7  Revamped calc_unit_factor(). 
2014-04-05 juewei    0.7a  Correctly positioned rack gear.
                       The geometry above the meshing line is wrong.
2014-04-06 juewei    0.7b  Undercut detection added. Reference:
               http://nptel.ac.in/courses/IIT-MADRAS/Machine_Design_II/pdf/2_2.pdf
               Manually merged https://github.com/jnweiger/inkscape-gears-dev/pull/15
2014-04-07 juewei    0.7c  Manually merged https://github.com/jnweiger/inkscape-gears-dev/pull/17
2014-04-09 juewei    0.8   Fixed https://github.com/jnweiger/inkscape-gears-dev/issues/19
			   Ring gears are ready for production now. Thanks neon22 for driving this.
			   Profile shift implemented (Advanced Tab), fixing 
			   https://github.com/jnweiger/inkscape-gears-dev/issues/9
2015-05-29 juewei 0.9 	ported to inkscape 0.91
			AttributeError: 'module' object inkex has no attribute 'uutounit
			Fixed https://github.com/jnweiger/inkscape-gears-dev
2020-7-4   spadino 1.0 ported to inkscape 1.0
'''

import inkex
from lxml import etree
from os import devnull # for debugging
from math import pi, cos, sin, tan, radians, degrees, ceil, asin, acos, sqrt
two_pi = 2 * pi


__version__ = '1.0'

def uutounit(self,nn,uu):
    return self.svg.uutounit(nn,uu)

def linspace(a,b,n):
    """ return list of linear interp of a to b in n steps
        - if a and b are ints - you'll get an int result.
        - n must be an integer
    """
    return [a+x*(b-a)/(n-1) for x in range(0,n)]

def involute_intersect_angle(Rb, R):
    " "
    Rb, R = float(Rb), float(R)
    return (sqrt(R**2 - Rb**2) / (Rb)) - (acos(Rb / R))

def point_on_circle(radius, angle):
    " return xy coord of the point at distance radius from origin at angle "
    x = radius * cos(angle)
    y = radius * sin(angle)
    return (x, y)
    
def points_to_bbox(p):
    """ from a list of points (x,y pairs)
        - return the lower-left xy and upper-right xy
    """
    llx = urx = p[0][0]
    lly = ury = p[0][1]
    for x in p[1:]:
        if   x[0] < llx: llx = x[0]
        elif x[0] > urx: urx = x[0]
        if   x[1] < lly: lly = x[1]
        elif x[1] > ury: ury = x[1]
    return (llx, lly, urx, ury)

def points_to_bbox_center(p):
    """ from a list of points (x,y pairs)
        - find midpoint of bounding box around all points
        - return (x,y)
    """
    bbox = points_to_bbox(p)
    return ((bbox[0]+bbox[2])/2.0, (bbox[1]+bbox[3])/2.0)
                
def points_to_svgd(p):
    " convert list of points into a closed SVG path list"
    f = p[0]
    p = p[1:]
    svgd = 'M%.4f,%.4f' % f
    for x in p:
        svgd += 'L%.4f,%.4f' % x
    svgd += 'z'
    return svgd

def draw_SVG_circle(parent, r, cx, cy, name, style):
    " add an SVG circle entity to parent "
    circ_attribs = {'style': str(inkex.Style(style)),
                    'cx': str(cx), 'cy': str(cy), 
                    'r': str(r),
                    inkex.addNS('label','inkscape'):name}
    circle = etree.SubElement(parent, inkex.addNS('circle','svg'), circ_attribs )


### Undercut support functions
def undercut_min_teeth(pitch_angle, k=1.0):
    """ computes the minimum tooth count for a 
        spur gear so that no undercut with the given pitch_angle (in deg) 
        and an addendum = k * metric_module, where 0 < k < 1
    Note:
    The return value should be rounded upwards for perfect safety. E.g.
    min_teeth = int(math.ceil(undercut_min_teeth(20.0)))    # 18, not 17
    """
    x = sin(radians(pitch_angle))
    return 2*k /(x*x)

def undercut_max_k(teeth, pitch_angle=20.0):
    """ computes the maximum k value for a given teeth count and pitch_angle
        so that no undercut occurs.
    """
    x = sin(radians(pitch_angle))
    return 0.5 * teeth * x * x

def undercut_min_angle(teeth, k=1.0):
    """ computes the minimum pitch angle, to that the given teeth count (and
        profile shift) cause no undercut.
    """
    return degrees(asin(min(0.856, sqrt(2.0*k/teeth))))    # max 59.9 deg


def have_undercut(teeth, pitch_angle=20.0, k=1.0):
    """ returns true if the specified number of teeth would
        cause an undercut.
    """
    return (teeth < undercut_min_teeth(pitch_angle, k))


## gather all basic gear calculations in one place
def gear_calculations(num_teeth, circular_pitch, pressure_angle, clearance=0, ring_gear=False, profile_shift=0.):
    """ Put base calcs for spur/ring gears in one place.
        - negative profile shifting helps against undercut. 
    """
    diametral_pitch = pi / circular_pitch
    pitch_diameter = num_teeth / diametral_pitch
    pitch_radius = pitch_diameter / 2.0
    addendum = 1 / diametral_pitch
    #dedendum = 1.157 / diametral_pitch # auto calc clearance
    dedendum = addendum
    dedendum *= 1+profile_shift
    addendum *= 1-profile_shift
    if ring_gear:
        addendum = addendum + clearance # our method
    else:
        dedendum = dedendum + clearance # our method
    #
    #
    base_radius = pitch_diameter * cos(radians(pressure_angle)) / 2.0
    outer_radius = pitch_radius + addendum
    root_radius =  pitch_radius - dedendum
    # Tooth thickness: Tooth width along pitch circle.
    tooth_thickness  = ( pi * pitch_diameter ) / ( 2.0 * num_teeth )
    # we don't use these
    working_depth = 2 / diametral_pitch
    whole_depth = 2.157 / diametral_pitch
    #outside_diameter = (num_teeth + 2) / diametral_pitch
    #
    return (pitch_radius, base_radius,
            addendum, dedendum, outer_radius, root_radius,
            tooth_thickness
            )

 
def generate_rack_points(tooth_count, pitch, addendum, pressure_angle,
                       rack_base_height, tab_length, clearance=0, draw_guides=False):
        """ Return path (suitable for svg) of the Rack gear.
            - rack gear uses straight sides
                - involute on a circle of infinite radius is a simple linear ramp
            - the meshing circle touches at y = 0, 
            - the highest elevation of the teeth is at y = +addendum
            - the lowest elevation of the teeth is at y = -addendum-clearance
            - the rack_base_height extends downwards from the lowest elevation.
            - we generate this middle tooth exactly centered on the y=0 line.
              (one extra tooth on the right hand side, if number of teeth is even)
        """
        spacing = 0.5 * pitch # rolling one pitch distance on the spur gear pitch_diameter.
        # roughly center rack in drawing, exact position is so that it meshes
        # nicely with the spur gear.
        # -0.5*spacing has a gap in the center.
        # +0.5*spacing has a tooth in the center.
        fudge = +0.5 * spacing

        tas  = tan(radians(pressure_angle)) * addendum
        tasc = tan(radians(pressure_angle)) * (addendum+clearance)
        base_top = addendum+clearance
        base_bot = addendum+clearance+rack_base_height

        x_lhs = -pitch * int(0.5*tooth_count-.5) - spacing - tab_length - tasc + fudge
        #inkex.utils.debug("angle=%s spacing=%s"%(pressure_angle, spacing))
        # Start with base tab on LHS
        points = [] # make list of points
        points.append((x_lhs, base_bot))
        points.append((x_lhs, base_top))
        x = x_lhs + tab_length+tasc

        # An involute on a circle of infinite radius is a simple linear ramp.
        # We need to add curve at bottom and use clearance.
        for i in range(tooth_count):
            # move along path, generating the next 'tooth'
            # pitch line is at y=0. the left edge hits the pitch line at x
            points.append((x-tasc, base_top))
            points.append((x+tas, -addendum))
            points.append((x+spacing-tas, -addendum))
            points.append((x+spacing+tasc, base_top)) 
            x += pitch
        x -= spacing # remove last adjustment
        # add base on RHS
        x_rhs = x+tasc+tab_length
        points.append((x_rhs, base_top))
        points.append((x_rhs, base_bot))
        # We don't close the path here. Caller does it.
        # points.append((x_lhs, base_bot))

        # Draw line representing the pitch circle of infinite diameter
        guide_path = None
        if draw_guides:
            p = []
            p.append( (x_lhs + 0.5 * tab_length, 0) )
            p.append( (x_rhs - 0.5 * tab_length, 0) )
            guide_path = points_to_svgd(p)
        # return points ready for use in an SVG 'path'
        return (points, guide_path)
    

def generate_spur_points(teeth, base_radius, pitch_radius, outer_radius, root_radius, accuracy_involute, accuracy_circular):
    """ given a set of core gear params
        - generate the svg path for the gear
    """
    half_thick_angle = two_pi / (4.0 * teeth ) #?? = pi / (2.0 * teeth)
    pitch_to_base_angle  = involute_intersect_angle( base_radius, pitch_radius )
    pitch_to_outer_angle = involute_intersect_angle( base_radius, outer_radius ) - pitch_to_base_angle

    start_involute_radius = max(base_radius, root_radius)
    radii = linspace(start_involute_radius, outer_radius, accuracy_involute)
    angles = [involute_intersect_angle(base_radius, r) for r in radii]

    centers = [(x * two_pi / float( teeth) ) for x in range( teeth ) ]
    points = []

    for c in centers:
        # Angles
        pitch1 = c - half_thick_angle
        base1  = pitch1 - pitch_to_base_angle
        offsetangles1 = [ base1 + x for x in angles]
        points1 = [ point_on_circle( radii[i], offsetangles1[i]) for i in range(0,len(radii)) ]

        pitch2 = c + half_thick_angle
        base2  = pitch2 + pitch_to_base_angle
        offsetangles2 = [ base2 - x for x in angles] 
        points2 = [ point_on_circle( radii[i], offsetangles2[i]) for i in range(0,len(radii)) ]

        points_on_outer_radius = [ point_on_circle(outer_radius, x) for x in linspace(offsetangles1[-1], offsetangles2[-1], accuracy_circular) ]

        if root_radius > base_radius:
            pitch_to_root_angle = pitch_to_base_angle - involute_intersect_angle(base_radius, root_radius )
            root1 = pitch1 - pitch_to_root_angle
            root2 = pitch2 + pitch_to_root_angle
            points_on_root = [point_on_circle (root_radius, x) for x in linspace(root2, root1+(two_pi/float(teeth)), accuracy_circular) ]
            p_tmp = points1 + points_on_outer_radius[1:-1] + points2[::-1] + points_on_root[1:-1] # [::-1] reverses list; [1:-1] removes first and last element
        else:
            points_on_root = [point_on_circle (root_radius, x) for x in linspace(base2, base1+(two_pi/float(teeth)), accuracy_circular) ]
            p_tmp = points1 + points_on_outer_radius[1:-1] + points2[::-1] + points_on_root # [::-1] reverses list

        points.extend( p_tmp )
    return (points)


def generate_spokes_path(root_radius, spoke_width, spoke_count, mount_radius, mount_hole,
                         unit_factor, unit_label):
    """ given a set of constraints
        - generate the svg path for the gear spokes
        - lies between mount_radius (inner hole) and root_radius (bottom of the teeth)
        - spoke width also defines the spacing at the root_radius
        - mount_radius is adjusted so that spokes fit if there is room
        - if no room (collision) then spokes not drawn
    """
    # Spokes
    collision = False # assume we draw spokes
    messages = []     # messages to send back about changes.
    path = ''
    r_outer = root_radius - spoke_width
    # checks for collision with spokes
    # check for mount hole collision with inner spokes
    if mount_radius <= mount_hole/2:
        adj_factor = (r_outer - mount_hole/2) / 5
        if adj_factor < 0.1:
            # not enough reasonable room
            collision = True
        else:
            mount_radius = mount_hole/2 + adj_factor # small fix
            messages.append("Mount support too small. Auto increased to %2.2f%s." % (mount_radius/unit_factor*2, unit_label))
            
    # then check to see if cross-over on spoke width
    if spoke_width * spoke_count +0.5 >= two_pi * mount_radius:
        adj_factor = 1.2 # wrong value. its probably one of the points distances calculated below
        mount_radius += adj_factor
        messages.append("Too many spokes. Increased Mount support by %2.3f%s" % (adj_factor/unit_factor, unit_label))
    
    # check for collision with outer rim
    if r_outer <= mount_radius:
        # not enough room to draw spokes so cancel
        collision = True
    if collision: # don't draw spokes if no room.
        messages.append("Not enough room for Spokes. Decrease Spoke width.")
    else: # draw spokes
        for i in range(spoke_count):
            points = []
            start_a, end_a = i * two_pi / spoke_count, (i+1) * two_pi / spoke_count
            # inner circle around mount
            asin_factor = spoke_width/mount_radius/2
            # check if need to clamp radius
            asin_factor = max(-1.0, min(1.0, asin_factor)) # no longer needed - resized above
            a = asin(asin_factor)
            points += [ point_on_circle(mount_radius, start_a + a), point_on_circle(mount_radius, end_a - a)]
            # is inner circle too small
            asin_factor = spoke_width/r_outer/2
            # check if need to clamp radius
            asin_factor = max(-1.0, min(1.0, asin_factor)) # no longer needed - resized above
            a = asin(asin_factor)
            points += [point_on_circle(r_outer, end_a - a), point_on_circle(r_outer, start_a + a) ]

            path += (
                    "M %f,%f" % points[0] +
                    "A  %f,%f %s %s %s %f,%f" % tuple((mount_radius, mount_radius, 0, 0 if spoke_count!=1 else 1, 1 ) + points[1]) +
                    "L %f,%f" % points[2] +
                    "A  %f,%f %s %s %s %f,%f" % tuple((r_outer, r_outer, 0, 0 if spoke_count!=1 else 1, 0 ) + points[3]) +
                    "Z"
                    )
    return (path, messages)


class Gears(inkex.EffectExtension):
    def __init__(self):
        inkex.Effect.__init__(self)
        # an alternate way to get debug info:
        # could use inkex.utils.debug(string) instead...
        # try:
        #     self.tty = open("/dev/tty", 'w')
        # except:
        #     self.tty = open(devnull, 'w')  # '/dev/null' for POSIX, 'nul' for Windows.
        #     # print >>self.tty, "gears-dev " + __version__

        self.arg_parser.add_argument("-t", "--teeth", type=int, default=24, help="Number of teeth")
        self.arg_parser.add_argument("-s", "--system", default='CP', help="Select system: 'CP' (Cyclic Pitch (default)), 'DP' (Diametral Pitch), 'MM' (Metric Module)")
        self.arg_parser.add_argument("-d", "--dimension", type=float, default=1.0, help="Tooth size, depending on system (which defaults to CP)")
        self.arg_parser.add_argument("-a", "--angle", type=float, default=20.0, help="Pressure Angle (common values: 14.5, 20, 25 degrees)")
        self.arg_parser.add_argument("-p", "--profile_shift", type=float, default=20.0, help="Profile shift [in percent of the module]. Negative values help against undercut")
        self.arg_parser.add_argument("-u", "--units", default='mm', help="Units this dialog is using")
        self.arg_parser.add_argument("-A", "--accuracy", type=int, default=0, help="Accuracy of involute: automatic: 5..20 (default), best: 20(default), medium 10, low: 5; good acuracy is important with a low tooth count")
        # Clearance: Radial distance between top of tooth on one gear to bottom of gap on another.
        self.arg_parser.add_argument("-cl", "--clearance", type=float, default=0.0, help="Clearance between bottom of gap of this gear and top of tooth of another")
        self.arg_parser.add_argument("-an", "--annotation", type=inkex.Boolean, default=False, help="Draw annotation text")
        self.arg_parser.add_argument("-i", "--internal_ring", type=inkex.Boolean, default=False, help="Ring (or Internal) gear style (default: normal spur gear)")
        self.arg_parser.add_argument("-mh", "--mount_hole", type=float, default=5, help="Mount hole diameter")
        self.arg_parser.add_argument("-md", "--mount_diameter", type=float, default=15, help="Mount support diameter")
        self.arg_parser.add_argument("-sc", "--spoke_count", type=int, default=3, help="Spokes count")
        self.arg_parser.add_argument("-sw", "--spoke_width", type=float, default=5, help="Spoke width")
        self.arg_parser.add_argument("-hr", "--holes_rounding", type=float, default=5, help="Holes rounding")
        self.arg_parser.add_argument("-at", "--active_tab", default='', help="Active tab. Not used now.")
        self.arg_parser.add_argument("-x", "--centercross", type=inkex.Boolean, default=False, help="Draw cross in center")
        self.arg_parser.add_argument("-c", "--pitchcircle", type=inkex.Boolean, default=False, help="Draw pitch circle (for mating)")
        self.arg_parser.add_argument("-r", "--draw_rack", type=inkex.Boolean, default=False, help="Draw rack gear instead of spur gear")
        self.arg_parser.add_argument("-rl", "--rack_teeth_length", type=int, default=12, help="Length (in teeth) of rack")
        self.arg_parser.add_argument("-rh", "--rack_base_height", type=float, default=8, help="Height of base of rack")
        self.arg_parser.add_argument("-rt", "--rack_base_tab", type=float, default=14, help="Length of tabs on ends of rack")
        self.arg_parser.add_argument("-ua", "--undercut_alert", type=inkex.Boolean, default=False, help="Let the user confirm a warning dialog if undercut occurs. This dialog also shows helpful hints against undercut")
  
    def add_text(self, node, text, position, text_height=12):
        """ Create and insert a single line of text into the svg under node.
            - use 'text' type and label as anootation
            - where color is Ponoko Orange - so ignored when lasercutting
        """
        line_style = {'font-size': '%dpx' % text_height, 'font-style':'normal', 'font-weight': 'normal',
                     'fill': '#F6921E', 'font-family': 'Bitstream Vera Sans,sans-serif',
                     'text-anchor': 'middle', 'text-align': 'center'}
        line_attribs = {inkex.addNS('label','inkscape'): 'Annotation',
                       'style':  str(inkex.Style(line_style)),
                       'x': str(position[0]),
                       'y': str((position[1] + text_height) * 1.2)
                       }
        line = etree.SubElement(node, inkex.addNS('text','svg'), line_attribs)
        line.text = text

           
    def calc_unit_factor(self):
        """ return the scale factor for all dimension conversions.
            - The document units are always irrelevant as
              everything in inkscape is expected to be in 90dpi pixel units
        """
        # namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi'))
        # doc_units = uutounit(self, 1.0, namedView.get(inkex.addNS('document-units', 'inkscape')))
        dialog_units = uutounit(self, 1.0, self.options.units)
        unit_factor = 1.0 / dialog_units
        return unit_factor

    def calc_circular_pitch(self):
        """ We use math based on circular pitch.
            Expressed in inkscape units which is 90dpi 'pixel' units.
        """
        dimension = self.options.dimension
        # print >> self.tty, "unit_factor=%s, doc_units=%s, dialog_units=%s (%s), system=%s" % (unit_factor, doc_units, dialog_units, self.options.units, self.options.system)
        if   self.options.system == 'CP': # circular pitch
            circular_pitch = dimension
        elif self.options.system == 'DP': # diametral pitch 
            circular_pitch = pi / dimension
        elif self.options.system == 'MM': # module (metric)
            circular_pitch = dimension * pi / 25.4
        else:
            inkex.utils.debug("unknown system '%s', try CP, DP, MM" % self.options.system)
        # circular_pitch defines the size in inches.
        # We divide the internal inch factor (px = 90dpi), to remove the inch 
        # unit.
        # The internal inkscape unit is always px, 
        # it is independent of the doc_units!
        return circular_pitch / uutounit(self, 1.0, 'in')



    def effect(self):
        """ Calculate Gear factors from inputs.
            - Make list of radii, angles, and centers for each tooth and 
              iterate through them
            - Turn on other visual features e.g. cross, rack, annotations, etc
        """
        path_stroke = '#000000'  # might expose one day
        path_fill   = 'none'     # no fill - just a line
        path_stroke_width  = uutounit(self, 0.1, 'mm') # might expose one day
        path_stroke_light  = uutounit(self, 0.05, 'mm') # guides are thinner
        #
        warnings = [] # list of extra messages to be shown in annotations
        # calculate unit factor for units defined in dialog. 
        unit_factor = self.calc_unit_factor()
        # User defined options
        teeth = self.options.teeth
        # Angle of tangent to tooth at circular pitch wrt radial line.
        angle = self.options.angle 
        # Clearance: Radial distance between top of tooth on one gear to 
        # bottom of gap on another.
        clearance = self.options.clearance * unit_factor
        mount_hole = self.options.mount_hole * unit_factor
        # for spokes
        mount_radius = self.options.mount_diameter * 0.5 * unit_factor
        spoke_count = self.options.spoke_count
        spoke_width = self.options.spoke_width * unit_factor
        holes_rounding = self.options.holes_rounding * unit_factor # unused
        # visible guide lines
        centercross = self.options.centercross # draw center or not (boolean)
        pitchcircle = self.options.pitchcircle # draw pitch circle or not (boolean)
        # Accuracy of teeth curves
        accuracy_involute = 20 # Number of points of the involute curve
        accuracy_circular = 9  # Number of points on circular parts
        if self.options.accuracy is not None:
            if self.options.accuracy == 0:  
                # automatic
                if   teeth < 10: accuracy_involute = 20
                elif teeth < 30: accuracy_involute = 12
                else:            accuracy_involute = 6
            else:
                accuracy_involute = self.options.accuracy
            accuracy_circular = max(3, int(accuracy_involute/2) - 1) # never less than three
        # print >>self.tty, "accuracy_circular=%s accuracy_involute=%s" % (accuracy_circular, accuracy_involute)
        # Pitch (circular pitch): Length of the arc from one tooth to the next)
        # Pitch diameter: Diameter of pitch circle.
        pitch = self.calc_circular_pitch()
        # Replace section below with this call to get the combined gear_calculations() above
        (pitch_radius, base_radius, addendum, dedendum,
         outer_radius, root_radius, tooth) = gear_calculations(teeth, pitch, angle, clearance, self.options.internal_ring, self.options.profile_shift*0.01)

        # Detect Undercut of teeth
##        undercut = int(ceil(undercut_min_teeth( angle )))
##        needs_undercut = teeth < undercut #? no longer needed ?
        if have_undercut(teeth, angle, 1.0):
            min_teeth = int(ceil(undercut_min_teeth(angle, 1.0)))
            min_angle = undercut_min_angle(teeth, 1.0) + .1
            max_k = undercut_max_k(teeth, angle)
            msg = "Undercut Warning: This gear (%d teeth) will not work well.\nTry tooth count of %d or more,\nor a pressure angle of %.1f [deg] or more,\nor try a profile shift of %d %%.\nOr other decent combinations." % (teeth, min_teeth, min_angle, int(100. * max_k) - 100.)
            # alas annotation cannot handle the degree symbol.  Also it ignore
            # newlines.
            # so split and make a list
            warnings.extend(msg.split("\n"))
	    #if self.options.undercut_alert: 
     #           inkex.utils.debug(msg)
	    #else:
     #           print >> self.tty, msg

        # All base calcs done. Start building gear
        points = generate_spur_points(teeth, base_radius, pitch_radius, outer_radius, root_radius, accuracy_involute, accuracy_circular)
        
##        half_thick_angle = two_pi / (4.0 * teeth ) #?? = pi / (2.0 * teeth)
##        pitch_to_base_angle  = involute_intersect_angle( base_radius, pitch_radius )
##        pitch_to_outer_angle = involute_intersect_angle( base_radius, outer_radius ) - pitch_to_base_angle
##
##        start_involute_radius = max(base_radius, root_radius)
##        radii = linspace(start_involute_radius, outer_radius, accuracy_involute)
##        angles = [involute_intersect_angle(base_radius, r) for r in radii]
##
##        centers = [(x * two_pi / float( teeth) ) for x in range( teeth ) ]
##        points = []
##
##        for c in centers:
##            # Angles
##            pitch1 = c - half_thick_angle
##            base1  = pitch1 - pitch_to_base_angle
##            offsetangles1 = [ base1 + x for x in angles]
##            points1 = [ point_on_circle( radii[i], offsetangles1[i]) for i in range(0,len(radii)) ]
##
##            pitch2 = c + half_thick_angle
##            base2  = pitch2 + pitch_to_base_angle
##            offsetangles2 = [ base2 - x for x in angles] 
##            points2 = [ point_on_circle( radii[i], offsetangles2[i]) for i in range(0,len(radii)) ]
##
##            points_on_outer_radius = [ point_on_circle(outer_radius, x) for x in linspace(offsetangles1[-1], offsetangles2[-1], accuracy_circular) ]
##
##            if root_radius > base_radius:
##                pitch_to_root_angle = pitch_to_base_angle - involute_intersect_angle(base_radius, root_radius )
##                root1 = pitch1 - pitch_to_root_angle
##                root2 = pitch2 + pitch_to_root_angle
##                points_on_root = [point_on_circle (root_radius, x) for x in linspace(root2, root1+(two_pi/float(teeth)), accuracy_circular) ]
##                p_tmp = points1 + points_on_outer_radius[1:-1] + points2[::-1] + points_on_root[1:-1] # [::-1] reverses list; [1:-1] removes first and last element
##            else:
##                points_on_root = [point_on_circle (root_radius, x) for x in linspace(base2, base1+(two_pi/float(teeth)), accuracy_circular) ]
##                p_tmp = points1 + points_on_outer_radius[1:-1] + points2[::-1] + points_on_root # [::-1] reverses list
##
##            points.extend( p_tmp )

        path = points_to_svgd( points )
        bbox_center = points_to_bbox_center( points )

        # Spokes (add to current path)
        if not self.options.internal_ring:  # only draw internals if spur gear
            spokes_path, msg = generate_spokes_path(root_radius, spoke_width, spoke_count, mount_radius, mount_hole,
                                                    unit_factor, self.options.units)
            warnings.extend(msg)
            path += spokes_path

            # Draw mount hole
            # A : rx,ry  x-axis-rotation, large-arch-flag, sweepflag  x,y
            r = mount_hole / 2
            path += (
                    "M %f,%f" % (0,r) +
                    "A  %f,%f %s %s %s %f,%f" % (r,r, 0,0,0, 0,-r) +
                    "A  %f,%f %s %s %s %f,%f" % (r,r, 0,0,0, 0,r) 
                    )
        else:
            # its a ring gear
            # which only has an outer ring where width = spoke width
            r = outer_radius + spoke_width
            path += (
                    "M %f,%f" % (0,r) +
                    "A  %f,%f %s %s %s %f,%f" % (r,r, 0,0,0, 0,-r) +
                    "A  %f,%f %s %s %s %f,%f" % (r,r, 0,0,0, 0,r) 
                    )
        
        # Embed gear in group to make animation easier:
        #  Translate group, Rotate path.
        t = 'translate(' + str( self.svg.namedview.center[0] ) + ',' + str( self.svg.namedview.center[1] ) + ')'
        g_attribs = { inkex.addNS('label','inkscape'):'Gear' + str( teeth ),
                      inkex.addNS('transform-center-x','inkscape'): str(-bbox_center[0]),
                      inkex.addNS('transform-center-y','inkscape'): str(-bbox_center[1]),
                      'transform':t,
                      'info':'N:'+str(teeth)+'; Pitch:'+ str(pitch) + '; Pressure Angle: '+str(angle) }
        # add the group to the current layer
        g = etree.SubElement(self.svg.get_current_layer(), 'g', g_attribs )

        # Create gear path under top level group
        style = { 'stroke': path_stroke, 'fill': path_fill, 'stroke-width': path_stroke_width }
        gear_attribs = { 'style': str(inkex.Style(style)), 'd': path }
        gear = etree.SubElement(g, inkex.addNS('path','svg'), gear_attribs )

        # Add center
        if centercross:
            style = {'stroke': path_stroke, 'fill': path_fill,
                     'stroke-width': path_stroke_light}
            cs = str(pitch / 3)  # centercross length
            d = 'M-'+cs+',0L'+cs+',0M0,-'+cs+'L0,'+cs  # 'M-10,0L10,0M0,-10L0,10'
            center_attribs = {inkex.addNS('label', 'inkscape'): 'Center cross',
                              'style': str(inkex.Style(style)), 'd': d}
            center = etree.SubElement(
                g, inkex.addNS('path', 'svg'), center_attribs)

        # Add pitch circle (for mating)
        if pitchcircle:
            style = { 'stroke': path_stroke, 'fill': path_fill, 'stroke-width': path_stroke_light }
            draw_SVG_circle(g, pitch_radius, 0, 0, 'Pitch circle', style)

        # Add Rack (below)
        if self.options.draw_rack:
            rack_base_height = self.options.rack_base_height * unit_factor
            tab_width = self.options.rack_base_tab * unit_factor
            tooth_count = self.options.rack_teeth_length
            (points, guide_path) = generate_rack_points(tooth_count, pitch, addendum, angle,
                                                        rack_base_height, tab_width, clearance, pitchcircle)
            path = points_to_svgd(points)
            # position below Gear, so that it meshes nicely
            # xoff = 0          ## if teeth % 4 == 2.
            # xoff = -0.5*pitch     ## if teeth % 4 == 0.
            # xoff = -0.75*pitch    ## if teeth % 4 == 3.
            # xoff = -0.25*pitch    ## if teeth % 4 == 1.
            xoff = (-0.5, -0.25, 0, -0.75)[teeth % 4] * pitch
            t = 'translate(' + str( xoff ) + ',' + str( pitch_radius ) + ')'
            g_attribs = { inkex.addNS('label', 'inkscape'): 'RackGear' + str(tooth_count),
                          'transform': t }
            rack = etree.SubElement(g, 'g', g_attribs)

            # Create SVG Path for gear
            style = {'stroke': path_stroke, 'fill': 'none', 'stroke-width': path_stroke_width }
            gear_attribs = { 'style': str(inkex.Style(style)), 'd': path }
            gear = etree.SubElement(
                rack, inkex.addNS('path', 'svg'), gear_attribs)
            if guide_path is not None:
                style2 = { 'stroke': path_stroke, 'fill': 'none', 'stroke-width': path_stroke_light }
                gear_attribs2 = { 'style':  str(inkex.Style(style2)), 'd': guide_path }
                gear = etree.SubElement(
                    rack, inkex.addNS('path', 'svg'), gear_attribs2)


        # Add Annotations (above)
        if self.options.annotation:
            outer_dia = outer_radius * 2
            if self.options.internal_ring:
                outer_dia += 2 * spoke_width
            notes = []
            notes.extend(warnings)
            #notes.append('Document (%s) scale conversion = %2.4f' % (self.document.getroot().find(inkex.addNS('namedview', 'sodipodi')).get(inkex.addNS('document-units', 'inkscape')), unit_factor))
            notes.extend(['Teeth: %d   CP: %2.4f(%s) ' % (teeth, pitch / unit_factor, self.options.units),
                          'DP: %2.3f Module: %2.4f' % (pi / pitch * unit_factor, pitch / pi * 25.4),
                          'Pressure Angle: %2.2f degrees' % (angle),
                          'Pitch diameter: %2.3f %s' % (pitch_radius * 2 / unit_factor, self.options.units),
                          'Outer diameter: %2.3f %s' % (outer_dia / unit_factor, self.options.units),
                          'Base diameter:  %2.3f %s' % (base_radius * 2 / unit_factor, self.options.units)#,
                          #'Addendum:      %2.4f %s'  % (addendum / unit_factor, self.options.units),
                          #'Dedendum:      %2.4f %s'  % (dedendum / unit_factor, self.options.units)
                          ])
            # text height relative to gear size.
            # ranges from 10 to 22 over outer radius size 60 to 360
            text_height = max(10, min(10+(outer_dia-60)/24, 22))
            # position above
            y = - outer_radius - (len(notes)+1) * text_height * 1.2
            for note in notes:
                self.add_text(g, note, [0,y], text_height)
                y += text_height * 1.2

if __name__ == '__main__':
    Gears().run()