mightyscape-1.2/extensions/fablabchemnitz/sheet_metal_conus/sheet_metal_conus.py

433 lines
22 KiB
Python

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