#!/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()