#!/usr/bin/env python3 # Distributed under the terms of the GNU Lesser General Public License v3.0 ### Author: Neon22 - github 2016 ### import inkex import fret_scale as fs import os # for scala file filtering from math import radians, cos, sin, pi from lxml import etree ###---------------------------------------------------------------------------- ### Styles - color and size settings Black = "#000000" Font_height = 5 # factor used for marker radius marker_rad_factor = 4 Line_style = { 'stroke' : Black, 'stroke-width' : '0.2px', 'fill' : "none" } Dash_style = { 'stroke' : Black, 'stroke-width' : '0.1px', 'stroke-dasharray' : '0.9,0.9', 'fill' : "none" } Label_style = { 'font-size' : str(int(Font_height))+'px', 'font-family' : 'arial', 'text-anchor' : 'end', # middle 'fill' : Black } Centerline_style = { 'stroke' : Black, 'stroke-width' : '0.1px', 'stroke-dasharray' : '1.2,0.7,0.3,0.7', 'fill' : "none" } # Helper functions def build_line(x1, y1, x2, y2, unitFactor): path = 'M %s,%s L %s,%s' % (x1*unitFactor, y1*unitFactor, x2*unitFactor, y2*unitFactor) return path def build_notch(x,y, notch_width, unitFactor, dir=1): """ draw a notch around the x value - dir=-1 means notch is on other side """ w_2 = notch_width/2 x1 = x - w_2 x2 = x + w_2 y2 = y + notch_width*dir path = 'L %s,%s L %s,%s' % (x1*unitFactor, y*unitFactor, x1*unitFactor, y2*unitFactor) path += 'L %s,%s L %s,%s' % (x2*unitFactor, y2*unitFactor, x2*unitFactor, y*unitFactor) return path def draw_center_cross(x,y, parent, length=2, style=Line_style): " center cross for holes " d = 'M {0},{1} l {2},0 M {3},{4} l 0,{2}'.format(x-length,y, length*2, x,y-length) cross_attribs = { inkex.addNS('label','inkscape'): 'Center cross', 'style': str(inkex.Style(style)), 'd': d } etree.SubElement(parent, inkex.addNS('path','svg'), cross_attribs ) def draw_SVG_circle(cx, cy, radius, parent, name='circle', style=Line_style): " structure an SVG circle entity under parent " circ_attribs = {'style': str(inkex.Style(style)), 'cx': str(cx), 'cy': str(cy), 'r': str(radius), inkex.addNS('label','inkscape'): name} circle = etree.SubElement(parent, inkex.addNS('circle','svg'), circ_attribs ) def draw_circle_marker(x,y, radius, parent): " circle with cross at center " draw_center_cross(x, y, parent, radius/5.0) draw_SVG_circle(x, y, radius, parent) ### class FretRuler(inkex.EffectExtension): def add_arguments(self, pars): pars.add_argument('--method', default='12th Root of 2', help="Method to calculate scale") pars.add_argument('--draw_style', default='Ruler', help="How to draw the Ruler/NEck") pars.add_argument("--nth", type=int,default=12, help="For different number of notes in a scale") pars.add_argument('--scala_filename', default='12tet', help="Name of file in scales directory") pars.add_argument("--units", default="in", help="The units of entered dimensions") pars.add_argument("--length", type=float, default=25.5, help="Length of the Scale (and Ruler)") pars.add_argument("--width", type=float, default=1.5, help="Width of the Ruler (= Nut if drawing a neck)") pars.add_argument("--frets", type=int, default=18, help="number of frets on the scale") # pars.add_argument("--fanned", type=inkex.Boolean, default=False, help="Two scales on either side of the Neck") pars.add_argument("--basslength", type=float, default=25.5, help="Length of the Bass side Scale") pars.add_argument("--perpendicular", type=int, default=7, help="Fret number which is perpendicular to the Neck") # pars.add_argument("--linewidth", type=float, default=0.1, help="Width of drawn lines") pars.add_argument("--notch_width", type=float, default=0.125, help="Width of Fret notches on Router template") pars.add_argument("--annotate", type=inkex.Boolean, default=True, help="Annotate with Markers etc") pars.add_argument("--centerline", type=inkex.Boolean, default=True, help="Draw a centerline") # Neck pars.add_argument("--constant_width", type=inkex.Boolean, default=True, help="Use Bridge width as well to make Neck") pars.add_argument("--width_bridge", type=float, default=2.0, help="Width at the Bridge (drawing Neck not Ruler)") pars.add_argument("--show_markers", type=inkex.Boolean, default=False, help="Show Neck Marker Positions") pars.add_argument('--markers', default='3,5,7,10,12,12,15', help="List of frets to draw markers on") # pars.add_argument("--nutcomp", type=inkex.Boolean, default=False, help="Modify Nut position") pars.add_argument("--nutcomp_value", default="0.012in (0.30mm)", help="Preset (usual) Nut compensation values") pars.add_argument("--nutcomp_manual", type=float, default=0.014, help="Manual distance to move Nut closer to Bridge") # pars.add_argument("--show_curves", type=inkex.Boolean, default=False, help="Show a neck curvature ruler") pars.add_argument("--neck_radius", type=float, default=2.0, help="Radius of Neck curvature") pars.add_argument("--arc_length", type=float, default=2.0, help="Length of Arc") pars.add_argument("--block_mode", type=inkex.Boolean, default=False, help="Draw block or finger style") pars.add_argument("--arc_height", type=float, default=2.0, help="height of Arc") pars.add_argument("--string_spacing", type=float, default=2.0, help="Spacing between strings") # pars.add_argument("--filter_tones", type=inkex.Boolean, default=True, help="Only show Scala files with this many notes in a scale.") pars.add_argument("--scale", type=int, default=12, help="number of Notes in the scale") pars.add_argument("--filter_label", type=inkex.Boolean, default=True, help="Only show Scala files with this keyword in them.") pars.add_argument("--keywords", default="diatonic", help="Keywords to search for") # here so we can have tabs pars.add_argument("-t", "--active-tab", default='ruler', help="Active tab.") def filter_scala_files(self, parent): """ Look in the scale directory for files. - show only files matching the filters """ filter_tones = self.options.filter_tones filter_names = self.options.filter_label numtones = self.options.scale keywords = self.options.keywords keywords = keywords.strip().split(',') keywords = [k.lower() for k in keywords] # probable_dir = os.getcwd()+'/scales/' files = os.listdir(probable_dir) # inkex.utils.debug("%s"%([os.getcwd(),len(files)])) # Display filenames in document filenames = [["Searched %d files"%(len(files)), "Found no matches", 0]] for f in files: fname = probable_dir+f data = fs.read_scala(fname, False) # filter out files that don't match if filter_tones and filter_names: if numtones == data[1]: if filter_names: for k in keywords: if data[0].find(k) > -1 or f.find(k) > -1: filenames.append([f, data[0], data[1]]) elif filter_tones: if numtones == data[1]: filenames.append([f, data[0], data[1]]) elif filter_names: for k in keywords: if data[0].find(k) > -1 or f.find(k) > -1: filenames.append([f, data[0], data[1]]) # inkex.utils.debug("%s"%(filenames)) # gathered them all - display them if len(filenames) != 0: filenames[0][1] = "Found %d matches"%(len(filenames)-1) x = 0 y = 0 Label_style['text-anchor'] = 'start' for f in filenames: label = f[0] if f[2] != 0: label += " - (%d tones)"%(f[2]) self.draw_label(x, y, label, parent) self.draw_label(x+Font_height*2, y+Font_height*1.2, f[1], parent) if y ==0: y += Font_height y += Font_height*2.8 Label_style['text-anchor'] = 'end' ### def draw_label(self, x,y, label, parent, transform=False, style=Label_style): " add a text entity " text_atts = {'style':str(inkex.Style(style)), 'x': str(x), 'y': str(y) } if transform: text_atts['transform'] = transform text = etree.SubElement(parent, 'text', text_atts) text.text = "%s" %(label) ### def draw_ruler(self, neck, parent, show_numbers=False): " draw the ruler with the centre of nut at 0,0 (unless fanned)" # fanned frets have a bass side as well as the normal(treble) side scale length # assume fanned treble_length = neck.length bass_length = treble_length if not neck.fanned else neck.bass_scale y1 = neck.nut_width/2 y2 = neck.bridge_width/2 startx = 0 endx = 0 # if neck is fanned - adjust start, end if neck.fanned: if neck.fan_offset > 0: startx = neck.fan_offset else: endx = -neck.fan_offset pts = [[treble_length+startx,-y2], [bass_length+endx,y2], [endx,y1]] # Create the boundary(neck) paths path = 'M %s,%s ' % (startx*self.convFactor, -y1*self.convFactor) for i in range(3): path += " L %s,%s "%(pts[i][0]*self.convFactor, pts[i][1]*self.convFactor) path += "Z" line_attribs = {'style' : str(inkex.Style(Line_style)), inkex.addNS('label','inkscape') : 'Outline' } line_attribs['d'] = path ell = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs ) # Draw the fret lines distances = neck.frets # do the zeroth value as well for count, xt in enumerate(distances): # seq of x offsets for each fret xb = xt if not neck.fanned else neck.bass_frets[count] # if neck is not straight, calc the extra bit to draw in Y yt = yb = y1 if y1 != y2: # neck not straight yt = y1 + ((xt-startx)/float(treble_length) * (y2-y1)) yb = y1 + ((xb-endx)/float(bass_length) * (y2-y1)) path = build_line(xt, -yt, xb, yb, self.convFactor) line_attribs['d'] = path ell = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) # Fret Numbers on odd frets(+octave) if show_numbers and (count%2 == 0 or count == neck.notes_in_scale-1): # try to push the lower fret numbers to the right a little Label_style['text-anchor'] = 'start' if count < 9 else 'middle' label_pos = neck.find_mid_point(count, -neck.nut_width/3) self.draw_label(label_pos[0]*self.convFactor, label_pos[1]*self.convFactor, count+1, parent) Label_style['text-anchor'] = 'end' def draw_router_template(self, neck, parent, notch_width, show_numbers=False): " draw the ruler as a notched router template " length = neck.length y = neck.nut_width/2 startx = notch_width*6 pts = [[length,-y], [length,y], [-startx,y]] path = 'M %s,%s ' % (-startx*self.convFactor, -y*self.convFactor) # start distances = [0] distances.extend(neck.frets) # style line_attribs = {'style' : str(inkex.Style(Line_style)), inkex.addNS('label','inkscape') : 'Outline' } # draw the fret notches, lines, labels for count, x in enumerate(distances): path += build_notch(x,-y, notch_width, self.convFactor) if show_numbers and (count%2 == 1 or count == 0 or count == neck.notes_in_scale): Label_style['text-anchor'] = 'start' if count < 9 else 'middle' self.draw_label(x*self.convFactor-Font_height, -y*self.convFactor+Font_height*2.2, count, parent) Label_style['text-anchor'] = 'end' # other side markers path2 = build_line(x, y, x, notch_width*2-y, self.convFactor) line_attribs['d'] = path2 ell = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) # close other side of template for i in range(3): path += " L %s,%s "%(pts[i][0]*self.convFactor, pts[i][1]*self.convFactor) path += "Z" # Draw line_attribs['d'] = path etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs ) def draw_neck_curve_ruler(self, neck, radius, arc_length, arc_height, string_spacing, parent): " draw arcs for curved fretboards " # perfect world draw ruler and lines to curved ruler. # mode = 'block1' block_mode = self.options.block_mode tab_length = arc_height*3 * self.convFactor diam_in = radius * 2 * self.convFactor angle_d = 180*arc_length / (2*pi*radius) angle = radians(angle_d) dist = arc_height*self.convFactor path = "M%s %s L%s %s" %(diam_in + dist,0, diam_in,0) x_a = diam_in * cos(angle) y_a = diam_in * sin(angle) x_b = (diam_in + dist) * cos(angle) y_b = (diam_in + dist) * sin(angle) path += " A %s,%s 0 0 1 %s %s" % (diam_in, diam_in, x_a, y_a) path += " L%s %s" %(x_b, y_b) if block_mode: # use a solid block style # add a midpoint for users to play with path += " L%s %s" % (diam_in+dist+(x_b-diam_in-dist)/2, y_b/2) tab_length = 0 else: # tab mode # need another arc with tab sections small_angle = radians(90*string_spacing / (2*pi*radius)) angle2 = angle/2 + small_angle angle3 = angle/2 - small_angle x_c = (diam_in + dist) * cos(angle2) y_c = (diam_in + dist) * sin(angle2) x_d = (diam_in + dist + tab_length) * cos(angle2) y_d = (diam_in + dist + tab_length) * sin(angle2) x_e = (diam_in + dist + tab_length) * cos(angle3) y_e = (diam_in + dist + tab_length) * sin(angle3) x_f = (diam_in + dist) * cos(angle3) y_f = (diam_in + dist) * sin(angle3) path += " A %s,%s 0 0 0 %s %s" % (diam_in, diam_in, x_c, y_c) path += " L%s %s" %(x_d, y_d) path += " L%s %s" %(x_e, y_e) path += " L%s %s" %(x_f, y_f) path += " A %s,%s 0 0 0 %s %s" % (diam_in, diam_in, diam_in + dist, 0) # close path path += 'z' ypos = diam_in + dist + tab_length + self.options.width*self.convFactor line_attribs = {'style' : str(inkex.Style(Line_style)), inkex.addNS('label','inkscape') : 'Neck Curve', 'transform': 'rotate(%f) translate(%s,%s)' % (-angle_d/2 -90, -ypos,-dist)} line_attribs['d'] = path etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs ) # label size = "%d"%radius if radius-int(radius) == 0 else "%4.2f"%(radius) Label_style['text-anchor'] = 'start' self.draw_label(0, 0, "Radius: %s%s"% (size, neck.units), parent, transform='translate(%s,%s)'%(0,ypos-diam_in-dist/2)) Label_style['text-anchor'] = 'end' def draw_title(self, neck, parent, initial="Fret Ruler:"): " Draw list of labels far right of ruler/Neck " labels = [initial] length = "%d"%neck.length if neck.length-int(neck.length) == 0 else "%4.2f"%(neck.length) if neck.fanned: basslength = "%d"%neck.bass_scale if neck.bass_scale-int(neck.bass_scale) == 0 else "%4.2f"%(neck.bass_scale) labels.append("Scale(Fanned): %s%s - %s%s" %(length, neck.units, basslength, neck.units)) else: # not fanned labels.append("Scale: %s%s, %d frets" %(length, neck.units, len(neck.frets))) # label2 = "Method: %s" % (neck.method.title()) if neck.method == 'scala': label2 += " (%s) %d tones" %(neck.scala.split('/')[-1], len(neck.scala_notes)) labels.append(label2) labels.append('"%s"' %(neck.description)) else: labels.append(label2) # unit formatting units = self.options.units precision = 1 if units=='mm' else 2 widthN = self.options.width widthB = self.options.width_bridge label_w = "{:4.{prec}f}{}".format(widthN, units, prec=precision) if not self.options.constant_width: label_w += "(Nut) - {:4.{prec}f}{}(Bridge)".format(widthB, units, prec=precision) labels.append("Width: %s"%(label_w)) if not self.options.constant_width and len(neck.frets)>11: distance12 = neck.frets[11] # inkex.utils.debug("%s"%([distance12/float(neck.length)])) width12 = widthN + (distance12/float(neck.length) * (widthB-widthN)) labels.append("(at 12th fret: {:4.{prec}f}{})".format(width12, units, prec=precision)) # where to draw starty = widthN if self.options.constant_width else widthB y = -starty/2*self.convFactor + Font_height*1.2 x_offset = 0 if neck.fanned and self.options.draw_style != 'template': x_offset = neck.fan_offset x = neck.length*self.convFactor - Font_height*1.5 + x_offset*self.convFactor # Draw for label in labels: self.draw_label(x,y, label, parent) y += Font_height*1.2 def draw_nut_compensation(self, neck, distance, parent): " " # inkex.utils.debug("%s"%([distance])) startx = 0 endx = 0 if neck.fanned: if neck.fan_offset > 0: startx = neck.fan_offset else: endx = -neck.fan_offset y = self.options.width/2 path = build_line(startx+distance, -y, endx+distance, y, self.convFactor) line_attribs = {'style' : str(inkex.Style(Dash_style)), 'd':path, inkex.addNS('label','inkscape') : 'Nut Compensation' } etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) def draw_neck_markers(self, neck, parent): " draw symbol at fret pos. N possible " # list may contain several occurences of a fret - meaning draw dots equidistant positions = neck.frets try: # user input weirdness locations = self.options.markers.strip().split(",") counts = [[int(i),locations.count(i)] for i in locations if i] fret_counts = [] for f in counts: if f not in fret_counts: fret_counts.append(f) except: inkex.errormsg("Could not parse list of fret numbers. E.g. 3,5,7,7") fret_counts = [[3,1]] # marker radius based on thinnest of the (to be marked) fret spacings spacings = [neck.frets[f-1] - neck.frets[max(0,f-2)] for f,c in fret_counts if f < len(neck.frets)+1] thinnest = min(spacings) marker_radius = thinnest/marker_rad_factor*self.convFactor for fret, count in fret_counts: if fret <= len(positions): # ignore if > #frets on this neck # inkex.utils.debug("%s"%([fret,count,positions[fret]])) fret = fret-1 if count == 1: # if odd, draw in center markerpos = neck.find_mid_point(fret, 0) draw_circle_marker(markerpos[0]*self.convFactor, markerpos[1]*self.convFactor, marker_radius, parent) else: # draw several at that fret sep = neck.nut_width/float(count+2) for i in range(count): markerpos = neck.find_mid_point(fret, sep*i*2 - sep*(count-1)) draw_circle_marker(markerpos[0]*self.convFactor, markerpos[1]*self.convFactor, marker_radius, parent) ### def effect(self): # calc units conversion self.convFactor = self.svg.unittouu("1" + self.options.units) # fix line width Line_style['stroke-width'] = self.svg.unittouu(str(self.options.linewidth) + "mm") # Usually we want 12 tone octaves numtones = 12 if self.options.method == 'Nroot2': numtones = int(self.options.nth) self.options.method = '%droot2'%(numtones) # Usually we don't want a scala file scala_filename=False if self.options.method == 'scala': scala_filename = "scales/"+self.options.scala_filename if scala_filename[-4:] != ".scl": scala_filename += ".scl" # Create group center of view t = 'translate(%s,%s)' % (self.svg.namedview.center[0]-self.options.length*self.convFactor/2, self.svg.namedview.center[1]) grp_attribs = {inkex.addNS('label','inkscape'):'Fret Ruler', 'transform':t} grp = etree.SubElement(self.svg.get_current_layer(), 'g', grp_attribs) page = self.options.active_tab[1:-1] draw_style = self.options.draw_style # check if on Scala filters page if page == 'filters': # display filtered scala files self.filter_scala_files(grp) else: # Regular action of drawing a Ruler... # select which style to draw based on user choice and what page they're on... # if on Ruler page then use draw_style title = "Fret Ruler:" if page == 'neck': draw_style = 'neck' if page == 'ruler' and draw_style=='ruler' or draw_style=='template': # override constant width if on Ruler page self.options.constant_width = True # calc fret widths fret_width = self.options.width if (page == 'neck' or draw_style=='neck'): title = "Neck Ruler:" if not self.options.constant_width: fret_width = [self.options.width, self.options.width_bridge] # Make the Neck neck = fs.Neck(self.options.length, units=self.options.units, fret_width=fret_width) neck.calc_fret_offsets(self.options.length, self.options.frets, self.options.method, numtones=numtones, scala_filename=scala_filename) if self.options.fanned: # fanned frets so calc bass scale and adjust perpendicular = min(self.options.perpendicular, len(neck.frets)) off = neck.set_fanned(self.options.basslength, perpendicular) if draw_style=='template': notch = self.options.notch_width title = "Router Template:" self.draw_router_template(neck, grp, notch, self.options.annotate) else: self.draw_ruler(neck, grp, self.options.annotate) self.draw_title(neck, grp, title) if self.options.centerline and self.options.draw_style != 'template': path = build_line(-0.5,0, max(neck.length, neck.bass_scale)+0.5, 0, self.convFactor) line_attribs = {'style' : str(inkex.Style(Centerline_style)), 'd':path, inkex.addNS('label','inkscape') : 'Centerline' } etree.SubElement(grp, inkex.addNS('path','svg'), line_attribs) # Neck specials if page == 'neck' or draw_style=='neck': # Nut compensation if self.options.nutcomp: value = self.options.nutcomp_value try: compensation = float(value) if value != 'manual' else float(self.options.nutcomp_manual) self.draw_nut_compensation(neck, compensation, grp) except: inkex.errormsg("Could not determine Nut compensation. Use a number.") # Markers if self.options.show_markers: self.draw_neck_markers(neck, grp) # inkex.utils.debug("#%s#"%(ordered_chords)) if self.options.show_curves: # position below max height of title text self.draw_neck_curve_ruler(neck, self.options.neck_radius, self.options.arc_length, self.options.arc_height, self.options.string_spacing, grp) # Create effect instance and apply it. if __name__ == '__main__': FretRuler().run() ### TODO: # - draw option for fret0 hole to hang ruler from # - draw strings # - how many strings # - separation distance # - work out interval offsets # - calc bridge compensation # - calc stretch compensation # - draw side view with bridge, relief distances #BUGS: # # Links: # Nut compensation: http://www.lmii.com/scale-length-intonation