#!/usr/bin/env python3 # Distributed under the terms of the GNU Lesser General Public License v3.0 import math import inkex from copy import deepcopy from lxml import etree from inkex.transforms import Transform from inkex import Color # Helper functions def calc_angle_between_points(p1, p2): xDiff = p2[0] - p1[0] yDiff = p2[1] - p1[1] return math.degrees(math.atan2(yDiff, xDiff)) def calc_dist_between_points(p1, p2): xDiff = p2[0] - p1[0] yDiff = p2[1] - p1[1] return math.sqrt(yDiff*yDiff + xDiff*xDiff) def normalize(p1, p2): " p1,p2 defines a vector return normalized " xDiff = p2[0] - p1[0] yDiff = p2[1] - p1[1] magn = calc_dist_between_points(p1,p2) return (xDiff/magn, yDiff/magn) def polar_to_cartesian(cx, cy, radius, angle_degrees): " So we can make arcs in the 'A' svg syntax. " angle_radians = math.radians(angle_degrees) return [ cx + (radius * math.cos(angle_radians)), cy + (radius * math.sin(angle_radians)) ] def point_on_circle(radius, angle): " return xy coord of the point at distance radius from origin at angle " x = radius * math.cos(angle) y = radius * math.sin(angle) return [x, y] class SheetMetalConus(inkex.EffectExtension): """ 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 add_arguments(self, pars): pars.add_argument('-b', '--diaBase', type = float, dest = 'diaBase', default = 300.0, help = 'The diameter of the cones base.') pars.add_argument('-c', '--diaCut', type = float, default = 100.0, help = 'The diameter of cones cut (0.0 if cone is not cut.') pars.add_argument('-l', '--heightCone', type = float, default = 200.0, help = 'The height of the (cut) cone.') pars.add_argument('-u', '--units', default = 'mm', help = 'The units in which the cone values are given. mm or in for real objects') pars.add_argument('-w', '--strokeWidth', type = float, default = 0.3, help = 'The line thickness in given unit. For laser cutting it should be rather small.') pars.add_argument('-f', '--strokeColour', type=Color, default = 255, help = 'The line colour.') pars.add_argument('-d', '--verbose', type = inkex.Boolean, default = False, help = 'Enable verbose output of calculated parameters. Used for debugging or is someone needs the calculated values.') # Marker arrows def makeMarkerstyle(self, name, rotate): " Markers added to defs for reuse " defs = self.svg.getElement('/svg:svg//svg:defs') if defs == None: defs = etree.SubElement(self.document.getroot(),inkex.addNS('defs','svg')) marker = etree.SubElement(defs ,inkex.addNS('marker','svg')) marker.set('id', name) marker.set('orient', 'auto') marker.set('refX', '0.0') marker.set('refY', '0.0') marker.set('style', 'overflow:visible') marker.set(inkex.addNS('stockid','inkscape'), name) arrow = etree.Element("path") # definition of arrows in beautiful DIN-shapes: if name.startswith('ArrowDIN-'): if rotate: arrow.set('d', 'M 8,0 -8,2.11 -8,-2.11 z') else: arrow.set('d', 'M -8,0 8,-2.11 8,2.11 z') if name.startswith('ArrowDINout-'): if rotate: arrow.set('d', 'M 0,0 16,2.11 16,0.5 26,0.5 26,-0.5 16,-0.5 16,-2.11 z') else: arrow.set('d', 'M 0,0 -16,2.11 -16,0.5 -26,0.5 -26,-0.5 -16,-0.5 -16,-2.11 z') arrow.set('style', 'fill:#000000;stroke:none') marker.append(arrow) def set_arrow_dir(self, option, style): if option=='inside': # inside self.arrowlen = 6.0 style['marker-start'] = 'url(#ArrowDIN-start)' style['marker-end'] = 'url(#ArrowDIN-end)' self.makeMarkerstyle('ArrowDIN-start', False) self.makeMarkerstyle('ArrowDIN-end', True) else: # outside self.arrowlen = 0 style['marker-start'] = 'url(#ArrowDINout-start)' style['marker-end'] = 'url(#ArrowDINout-end)' self.makeMarkerstyle('ArrowDINout-start', False) self.makeMarkerstyle('ArrowDINout-end', True) def drawDimArc(self, start, end, radius, style, parent, gap=0, lowside=True): " just the arrowed arc line " angle = abs(end-start) # inside or outside inside = True critical_length = 35 dist = calc_dist_between_points(point_on_circle(radius, start), point_on_circle(radius, end)) if angle < 45 and dist > critical_length: inside = False # change start and end angles to make room for arrow markers arrow_angle = math.degrees(math.sin(self.arrowlen/radius)) if lowside: start += arrow_angle angle -= arrow_angle anglefac = 1 else: start -= arrow_angle angle -= arrow_angle anglefac = -1 if gap == 0: line_attribs = {'style' : str(inkex.Style(style)), 'd' : self.build_arc(0, 0, start, angle*anglefac, radius, lowside) } ell = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs ) else: # leave a gap for label gap_angle = math.degrees(math.sin(gap/radius)) startstyle = deepcopy(style) startstyle['marker-start'] = None line_attribs = {'style' : str(inkex.Style(startstyle)), 'd' : self.build_arc(0, 0, start, angle*anglefac/2-gap_angle/2*anglefac, radius, lowside) } ell = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs ) endstyle = deepcopy(style) endstyle['marker-end'] = None line_attribs = {'style' : str(inkex.Style(endstyle)), 'd' : self.build_arc(0, 0, angle/2*anglefac+gap_angle/2*anglefac, angle*anglefac, radius, lowside) } etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs ) # return pos in center of gap (or arc) textposangle = angle/2*anglefac return (point_on_circle(radius, math.radians(textposangle))) def drawDimension(self, a, b, style, parent): " draw arrowed dimensions using markers " # draw arrows as inside or outside dimension critical_length = 35. if calc_dist_between_points(a,b) > critical_length: self.set_arrow_dir('inside', style) else: self.set_arrow_dir('outside', style) attribs = {'style' : str(inkex.Style(style))} # account for length change so arrows fit norm = normalize(a, b) dim_start_x = a[0] + self.arrowlen*norm[0] dim_start_y = a[1] + self.arrowlen*norm[1] dim_end_x = b[0] - self.arrowlen*norm[0] dim_end_y = b[1] - self.arrowlen*norm[1] # attribs['d'] = 'M %f,%f %f,%f' % (dim_start_x, dim_start_y, dim_end_x, dim_end_y) dimline = etree.SubElement(parent, inkex.addNS('path', 'svg'), attribs) return dimline def calculateCone(self, dictCone): """ Calculates all relevant values in order to construct a cone. These values are: - short radius - long radius - angle of cone layout - chord of base diameter - chord of cut diameter - coordinates of points A, B, C and D """ dBase = dictCone['diaBase'] dCut = dictCone['diaCut'] hCone = dictCone['heightCone'] base = dBase - dCut # radius from top of cone to cut if dCut > 0: shortRadius = math.sqrt( dCut*dCut/4 + (dCut*hCone)/base * (dCut*hCone)/base ) else: shortRadius=0.0 dictCone['shortRadius'] = shortRadius ## radius from top of cone to base of cone longRadius=math.sqrt( dBase*dBase/4 + (dBase*hCone)/base * (dBase*hCone)/base ) dictCone['longRadius'] = longRadius ## angle of circle sector angle=(math.pi * dBase) / longRadius dictCone['angle'] = angle # chord is the straight line between the 2 endpoints of an arc. # Not used directly, but available in verbose output. chordBase = longRadius * math.sqrt( 2* (1-math.cos(angle)) ) dictCone['chordBase'] = chordBase chordCut = shortRadius * math.sqrt( 2* (1-math.cos(angle)) ) dictCone['chordCut'] = chordCut # calculate coordinates of points A, B, C and D # center M is at (0,0) and points A and B are on the x-axis: ptA = (shortRadius, 0.0) ptB = (longRadius, 0.0) # we can calculate points C and D with the given radii and the calculated angle ptC=(longRadius * math.cos(angle), longRadius * math.sin(angle)) ptD=(shortRadius * math.cos(angle), shortRadius * math.sin(angle)) dictCone['ptA'] = ptA dictCone['ptB'] = ptB dictCone['ptC'] = ptC dictCone['ptD'] = ptD def effect(self): if self.options.diaBase == self.options.diaCut: inkex.utils.debug("Warning. Cut diameter may not be equal to base diameter.") exit(1) # calc scene scale convFactor = self.svg.unittouu("1" + self.options.units) # Store all the relevants values in a dictionary for easy access dictCone={'diaBase': self.options.diaBase, 'diaCut': self.options.diaCut, 'heightCone': self.options.heightCone } # Get all values needed in order to draw cone layout: self.calculateCone(dictCone) # Draw the cone layout: # Make top level group t = 'translate(%s,%s)' % (self.svg.namedview.center[0], self.svg.namedview.center[1]) grp_attribs = {inkex.addNS('label','inkscape'):'Sheet Metal Conus Group', 'transform':t} grp = etree.SubElement(self.svg.get_current_layer(), 'g', grp_attribs) linestyle = { 'stroke' : self.options.strokeColour, 'fill' : 'none', 'stroke-width': str(self.svg.unittouu(str(self.options.strokeWidth) + self.options.units)) } line_attribs = {'style' : str(inkex.Style(linestyle)), inkex.addNS('label','inkscape') : 'Cone' } # Connect the points into a single path of lines and arcs zeroCenter=(0.0, 0.0) angle = math.degrees(dictCone['angle']) path = "" path += self.build_line(dictCone['ptA'][0], dictCone['ptA'][1], dictCone['ptB'][0], dictCone['ptB'][1], convFactor) # A,B path += " " + self.build_arc(zeroCenter[0], zeroCenter[1], 0.0, angle, dictCone.get('longRadius')*convFactor) path += " " + self.build_line(dictCone['ptC'][0], dictCone['ptC'][1],dictCone['ptD'][0], dictCone['ptD'][1], convFactor) # C,D path += self.build_arc(zeroCenter[0], zeroCenter[1], 0.0, angle, dictCone['shortRadius']*convFactor) line_attribs['d'] = path ell = etree.SubElement(grp, inkex.addNS('path','svg'), line_attribs ) # Draw Dimensions Markup if self.options.verbose == True: grp_attribs = {inkex.addNS('label','inkscape'):'markup'} markup_group = etree.SubElement(grp, 'g', grp_attribs) self.beVerbose(dictCone, convFactor, markup_group) def build_arc(self, x, y, start_angle, end_angle, radius, reverse=True, swap=False): # Not using internal arc rep - instead construct path A in svg style directly # so we can append lines to make single path start = polar_to_cartesian(x, y, radius, end_angle) end = polar_to_cartesian(x, y, radius, start_angle) arc_flag = 0 if reverse else 1 sweep = 0 if (end_angle-start_angle) <=180 else 1 if swap: sweep = 1-sweep path = 'M %s,%s' % (start[0], start[1]) path += " A %s,%s 0 %d %d %s %s" % (radius, radius, sweep, arc_flag, end[0], end[1]) return path def build_line(self, x1, y1, x2, y2, unitFactor): path = 'M %s,%s L %s,%s' % (x1*unitFactor, y1*unitFactor, x2*unitFactor, y2*unitFactor) return path def beVerbose(self, dictCone, unitFactor, parent): """ Verbose output of calculated values. Can be used for debugging purposes or if calculated values needed. """ # unpack base_dia = dictCone['diaBase'] cut_dia = dictCone['diaCut'] cone_height = dictCone['heightCone'] shortradius = dictCone['shortRadius'] longradius = dictCone['longRadius'] angle = dictCone['angle'] chord_base = dictCone['chordBase'] chord_cut = dictCone['chordCut'] ptA = dictCone['ptA'] ptB = dictCone['ptB'] ptC = dictCone['ptC'] ptD = dictCone['ptD'] # styles for markup stroke_width = max(0.1, self.svg.unittouu(str(self.options.strokeWidth/2) + self.options.units)) line_style = { 'stroke': self.color_marker_dim, 'stroke-width': str(stroke_width), 'fill':'none' } arrow_style = self.dimline_style font_height = min(32, max( 8, int(self.svg.unittouu(str(longradius/40) + self.options.units)))) text_style = { 'font-size': str(font_height), 'font-family': 'arial', 'text-anchor': 'middle', 'text-align': 'center', 'fill': self.color_marker_dim } # verbose message for debug window msg = "Base diameter: " + str(base_dia) + "Cut diameter: " + str(cut_dia) + \ "\nCone height: " + str(cone_height) + "\nShort radius: " + str(shortradius) + \ "\nLong radius: " + str(longradius) + "\nAngle of circle sector: " + str(angle) + \ " radians (= " + str(math.degrees(angle)) + " degrees)" + \ "\nChord length of base arc: " + str(chord_base) + \ "\nChord length of cut arc: " + str(chord_cut) #inkex.utils.debug( msg) # Mark center marker_length = max(5, longradius* unitFactor/100) line_attribs = {'style' : str(inkex.Style(line_style)), inkex.addNS('label','inkscape') : 'center', 'd' : 'M -{0},-{0} L {0},{0}'.format(marker_length)} line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) line_attribs = {'style' : str(inkex.Style(line_style)), inkex.addNS('label','inkscape') : 'center', 'd' : 'M -{0},{0} L {0},-{0}'.format(marker_length)} line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) # Draw tick marks line_attribs = {'style' : str(inkex.Style(line_style)), 'd' : 'M 0,-3 L 0,-30'} line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) if cut_dia != 0: line_attribs = {'style' : str(inkex.Style(line_style)), 'd' : 'M {0},-3 L {0},-30'.format(shortradius * unitFactor)} line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) line_attribs = {'style' : str(inkex.Style(line_style)), 'd' : 'M {0},-3 L {0},-30'.format(longradius * unitFactor)} line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) # span line arrow_style['stroke'] = self.color_marker_dim self.drawDimension((0,-10), (shortradius * unitFactor, -10), arrow_style, parent) self.drawDimension((shortradius * unitFactor,-10), (longradius * unitFactor, -10), arrow_style, parent) # labels for short, long radii if cut_dia >= 0.001: text_atts = {'style':str(inkex.Style(text_style)), 'x': str(shortradius*unitFactor/2), 'y': str(-15) } text = etree.SubElement(parent, 'text', text_atts) text.text = "%4.3f" %(shortradius) text_atts = {'style':str(inkex.Style(text_style)), 'x': str((shortradius + (longradius-shortradius)/2)*unitFactor), 'y': str(-15) } text = etree.SubElement(parent, 'text', text_atts) text.text = "%4.3f" %(longradius) # Draw angle lowside = math.degrees(angle) < 180 value = math.degrees(angle) if lowside else 360-math.degrees(angle) # radial limit lines line_attribs = {'style' : str(inkex.Style(line_style)), 'd' : 'M 3,0 L %4.2f,0' % (ptA[0]*unitFactor*0.8)} line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) line_attribs = {'style' : str(inkex.Style(line_style)), 'd' : 'M %4.2f,%4.2f L %4.2f,%4.2f' % (ptD[0]*unitFactor*0.02, ptD[1]*unitFactor*0.02,ptD[0]*unitFactor*0.8, ptD[1]*unitFactor*0.8)} line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) # arc arc_rad = ptA[0]*unitFactor*0.50 gap = self.svg.unittouu(str(font_height*2)+"pt") textpos = self.drawDimArc(0, value, arc_rad, arrow_style, parent, gap, lowside) # angle label textpos[1] += font_height/4 if lowside else font_height/2 text_atts = {'style':str(inkex.Style(text_style)), 'x': str(textpos[0]), 'y': str(textpos[1]) } text = etree.SubElement(parent, 'text', text_atts) text.text = "%4.2f deg" %(value) # chord lines dash_style = deepcopy(arrow_style) dash_style['stroke'] = self.color_marker_chords dash_style['stroke-dasharray'] = '4, 2, 1, 2' line = self.drawDimension((ptA[0]*unitFactor, ptA[1]*unitFactor), (ptD[0]*unitFactor, ptD[1]*unitFactor), dash_style, parent) line = self.drawDimension((ptB[0]*unitFactor, ptB[1]*unitFactor), (ptC[0]*unitFactor, ptC[1]*unitFactor), dash_style, parent) # chord labels centerx = ptB[0]*unitFactor + (ptC[0]-ptB[0])*unitFactor/2 centery = ptB[1]*unitFactor + (ptC[1]-ptB[1])*unitFactor/2 line_angle = calc_angle_between_points(ptC, ptB) ypos = centery+font_height+2 if line_angle<0 else centery-2 text_style['fill'] = self.color_marker_chords text_atts = {'style':str(inkex.Style(text_style)), 'transform': 'rotate(%f)' % (line_angle) } text = etree.SubElement(parent, 'text', text_atts) scale_matrix = [[1, 0.0, centerx], [0.0, 1, ypos]] # needs cos,sin corrections text.transform = Transform(scale_matrix) * text.transform text.text = "%4.2f" % (chord_base) if cut_dia >= 0.001: centerx = ptA[0]*unitFactor + (ptD[0]-ptA[0])*unitFactor/2 centery = ptA[1]*unitFactor + (ptD[1]-ptA[1])*unitFactor/2 xpos = centerx - font_height*math.sin(math.radians(abs(line_angle))) ypos = centery-2 if line_angle>0 else centery+font_height+2 text = etree.SubElement(parent, 'text', text_atts) scale_matrix = [[1, 0.0, centerx], [0.0, 1, ypos]] text.transform = Transform(scale_matrix) * text.transform text.text = "%4.2f" % (chord_cut) # frustum lines frustrum_repos = [[1, 0.0, 1], [0.0, 1, math.sqrt(pow(shortradius*unitFactor,2)-pow(cut_dia*unitFactor/2,2))]] text_style['fill'] = self.color_marker_base line_style['stroke'] = self.color_marker_base arrow_style['stroke'] = self.color_marker_base line_attribs = {'style': str(inkex.Style(line_style)), 'd': 'M %f,%f L %f,%f %f,%f %f,%f z' %(-cut_dia/2*unitFactor,0, cut_dia/2*unitFactor,0, base_dia/2*unitFactor,cone_height*unitFactor, -base_dia/2*unitFactor,cone_height*unitFactor)} line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) line.transform = Transform(frustrum_repos) * line.transform # ticks line_attribs = {'style': str(inkex.Style(line_style)), 'd': 'M %f,%f L %f,%f' %(-(5+cut_dia/2*unitFactor),0, -(5+base_dia/2*unitFactor),0 )} line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs) line.transform = Transform(frustrum_repos) * line.transform # line = self.drawDimension((-base_dia/2*unitFactor,0), (-base_dia/2*unitFactor,cone_height*unitFactor), arrow_style, parent) line.transform = Transform(frustrum_repos) * line.transform # frustum text text_atts = {'style':str(inkex.Style(text_style)), 'x': str(-(18+base_dia/2*unitFactor)), 'y': str(cone_height*unitFactor/2) } text = etree.SubElement(parent, 'text', text_atts) text.text = "%4.3f" %(cone_height) text.transform = Transform(frustrum_repos) * text.transform if cut_dia >= 0.001: text_atts = {'style':str(inkex.Style(text_style)), 'x': '0', 'y': str(font_height) } text = etree.SubElement(parent, 'text', text_atts) text.text = "%4.3f" %(cut_dia) text.transform = Transform(frustrum_repos) * text.transform text_atts = {'style':str(inkex.Style(text_style)), 'x': '0', 'y': str(cone_height*unitFactor+font_height) } text = etree.SubElement(parent, 'text', text_atts) text.text = "%4.3f" %(base_dia) text.transform = Transform(frustrum_repos) * text.transform if __name__ == '__main__': SheetMetalConus().run()