328 lines
14 KiB
Python
328 lines
14 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
'''
|
|
Tool for drawing beautiful DIN-conform dimensioning arrows
|
|
(c) 2012 by Johannes B. Rutzmoser, johannes.rutzmoser (at) googlemail (dot) com
|
|
|
|
Please contact me, if you know a way how the extension module accepts mouse input; this would help to improve the tool
|
|
|
|
Add this file and the dimensioning.inx file into the following folder to get the feature run:
|
|
UNIX:
|
|
$HOME/.config/inkscape/extensions/
|
|
|
|
Mac OS X (when using the binary):
|
|
/Applications/Inkscape.app/Contents/Resources/extensions/
|
|
or
|
|
/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions
|
|
|
|
WINDOWS (Filepath may differ, depending where the program was installed):
|
|
C:\Program Files\Inkscape\share\extensions
|
|
|
|
License:
|
|
GNU GENERAL PUBLIC LICENSE
|
|
|
|
'''
|
|
|
|
import inkex, cubicsuperpath
|
|
import simplestyle
|
|
import numpy as np
|
|
import gettext
|
|
_ = gettext.gettext
|
|
|
|
def norm(a):
|
|
return a/np.sqrt(np.dot(a, a))
|
|
|
|
def rotate(tangentvec, point):
|
|
if tangentvec[0] == 0:
|
|
angle = - np.pi/2
|
|
else:
|
|
angle = np.arctan(tangentvec[1]/tangentvec[0])
|
|
return 'rotate(' + str(angle/np.pi*180) + ',' + str(point[0]) + ',' + str(point[1]) + ')'
|
|
|
|
|
|
class Dimensioning(inkex.Effect):
|
|
def __init__(self):
|
|
inkex.Effect.__init__(self)
|
|
# the options given in the dialouge
|
|
self.OptionParser.add_option("--orientation",
|
|
action="store", type="string",
|
|
dest="orientation", default='horizontal',
|
|
help="The type of orientation of the dimensioning (horizontal, vertical or parallel)")
|
|
self.OptionParser.add_option("--arrow_orientation",
|
|
action="store", type="string",
|
|
dest="arrow_orientation", default='auto',
|
|
help="The type of orientation of the arrows")
|
|
self.OptionParser.add_option("--line_scale",
|
|
action="store", type="float",
|
|
dest="line_scale", default=1.0,
|
|
help="Scale factor for the line thickness")
|
|
self.OptionParser.add_option("--overlap",
|
|
action="store", type="float",
|
|
dest="overlap", default=1.0,
|
|
help="Overlap of the helpline over the dimensioning line")
|
|
self.OptionParser.add_option("--distance",
|
|
action="store", type="float",
|
|
dest="distance", default=1.0,
|
|
help="Distance of the helpline to the object")
|
|
self.OptionParser.add_option("--position",
|
|
action="store", type="float",
|
|
dest="position", default=1.0,
|
|
help="position of the dimensioning line")
|
|
self.OptionParser.add_option("--flip",
|
|
action="store", type="inkbool",
|
|
dest="flip", default=False,
|
|
help="flip side")
|
|
self.OptionParser.add_option("--scale_factor",
|
|
action="store", type="float",
|
|
dest="scale_factor", default=1.0,
|
|
help="scale factor for the dimensioning text")
|
|
self.OptionParser.add_option("--unit",
|
|
action="store", type="string",
|
|
dest="unit", default='px',
|
|
help="The unit that should be used for the dimensioning")
|
|
self.OptionParser.add_option("--rotate",
|
|
action="store", type="inkbool",
|
|
dest="rotate", default=True,
|
|
help="Rotate the annotation?")
|
|
self.OptionParser.add_option("--digit",
|
|
action="store", type="int",
|
|
dest="digit", default=0,
|
|
help="number of digits after the point")
|
|
self.OptionParser.add_option("--tab",
|
|
action="store", type="string",
|
|
dest="tab", default="sampling",
|
|
help="The selected UI-tab when OK was pressed")
|
|
def create_linestyles(self):
|
|
'''
|
|
Create the line styles for the drawings.
|
|
'''
|
|
|
|
self.helpline_style = {
|
|
'stroke' : '#000000',
|
|
'stroke-width' : '{}px'.format(0.5*self.options.line_scale),
|
|
'fill' : 'none'
|
|
}
|
|
self.dimline_style = {
|
|
'stroke' : '#000000',
|
|
'stroke-width' : '{}px'.format(0.75*self.options.line_scale),
|
|
'fill' : 'none',
|
|
'marker-start' : 'url(#ArrowDIN-start)',
|
|
'marker-end' : 'url(#ArrowDIN-end)'
|
|
}
|
|
self.text_style = {
|
|
'font-size' : '{}px'.format(12*self.options.line_scale),
|
|
'font-family' : 'Sans',
|
|
'font-style' : 'normal',
|
|
'text-anchor' : 'middle'
|
|
}
|
|
self.helpline_attribs = {'style' : simplestyle.formatStyle(self.helpline_style),
|
|
inkex.addNS('label', 'inkscape') : 'helpline',
|
|
'd' : 'm 0,0 100,0'
|
|
}
|
|
self.text_attribs = {'style' : simplestyle.formatStyle(self.text_style),
|
|
'x' : '100',
|
|
'y' : '100'
|
|
}
|
|
self.dimline_attribs = {'style' : simplestyle.formatStyle(self.dimline_style),
|
|
inkex.addNS('label','inkscape') : 'dimline',
|
|
'd' : 'm 0,0 200,0'
|
|
}
|
|
|
|
def getUnittouu(self, param):
|
|
try:
|
|
return inkex.unittouu(param)
|
|
except AttributeError:
|
|
return self.unittouu(param)
|
|
|
|
def effect(self):
|
|
# will be executed when feature is activated
|
|
self.create_linestyles()
|
|
self.makeGroup()
|
|
self.getPoints()
|
|
self.calcab()
|
|
self.drawHelpline()
|
|
self.drawDimension()
|
|
self.drawText()
|
|
|
|
def makeMarkerstyle(self, name, rotate):
|
|
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")
|
|
# messy but works; 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 makeGroup(self):
|
|
'''puts everything of the dimensioning in a group'''
|
|
layer = self.current_layer
|
|
# Group in which the object should be put into
|
|
grp_name = 'dimensioning'
|
|
grp_attributes = {inkex.addNS('label', 'inkscape') : grp_name}
|
|
self.grp = inkex.etree.SubElement(layer, 'g', grp_attributes)
|
|
|
|
def getPoints(self):
|
|
self.p1 = np.array([0.,100.])
|
|
self.p1 = np.array([100.,100.])
|
|
# Get variables of a selected object
|
|
for id, node in self.selected.iteritems():
|
|
# if it is a path:
|
|
if node.tag == inkex.addNS('path', 'svg'):
|
|
d = node.get('d')
|
|
p = cubicsuperpath.parsePath(d)
|
|
# p has all nodes with the anchor points in a list;
|
|
# the rule is [anchorpoint, node, anchorpoint]
|
|
# the points are lists with x and y coordinate
|
|
self.p1 = np.array(p[0][0][1])
|
|
self.p2 = np.array(p[0][-1][1])
|
|
|
|
|
|
def calcab(self):
|
|
# get p1,p2 ordered for correct dimension direction
|
|
# determine quadrant
|
|
if self.p1[0] <= self.p2[0]:
|
|
if self.p1[1] <= self.p2[1]:
|
|
quad = 1 # p1 is left,up of p2
|
|
else: quad = 2 # p1 is left,down of p2
|
|
elif self.p1[1] <= self.p2[1]:
|
|
quad = 3 # p1 is right,up of p2
|
|
else: quad = 4 # p1 is right,down of p2
|
|
swap = False if quad ==1 else True
|
|
minp = self.p2 if swap else self.p1
|
|
maxp = self.p1 if swap else self.p2
|
|
# distance between points
|
|
delta = maxp - minp
|
|
# rotation matrix
|
|
rotateMat = np.array([[0,-1],[1,0]])
|
|
# compute the unit vectors e1 and e2 along the cartesian coordinates of the dimension
|
|
if self.options.orientation == 'horizontal':
|
|
if quad == 3: self.e1 = np.array([1.0, 0.0])
|
|
else: self.e1 = np.array([-1.0, 0.0])
|
|
if self.options.orientation == 'vertical':
|
|
if quad == 2:
|
|
self.e1 = np.array([0.0, -1.0])
|
|
else: self.e1 = np.array([0.0, 1.0])
|
|
if self.options.orientation == 'parallel':
|
|
self.e1 = norm(delta)
|
|
#if quad==2 or quad==3: self.e1 *= -1
|
|
self.e2 = np.dot(rotateMat, self.e1)
|
|
if self.options.flip:
|
|
self.e2 *= -1.
|
|
# compute the points a and b, where the dimension line arrow spikes start and end
|
|
dist = self.options.position*self.e2
|
|
if self.options.flip:
|
|
outpt = maxp
|
|
delta *= -1
|
|
if swap:
|
|
self.a = outpt + dist
|
|
self.b = self.a + self.e1*np.dot(self.e1,delta)
|
|
else:
|
|
self.b = outpt + dist
|
|
self.a = self.b + self.e1*np.dot(self.e1,delta)
|
|
else:
|
|
outpt = minp
|
|
if swap:
|
|
self.b = outpt + dist
|
|
self.a = self.b + self.e1*np.dot(self.e1,delta)
|
|
else:
|
|
self.a = outpt + dist
|
|
self.b = self.a + self.e1*np.dot(self.e1,delta)
|
|
|
|
|
|
def drawHelpline(self):
|
|
# manipulate the start- and endpoints with distance and overlap
|
|
h1_start = self.p1 + norm(self.a - self.p1)*self.options.distance
|
|
h1_end = self.a + norm(self.a - self.p1)*self.options.overlap
|
|
h2_start = self.p2 + norm(self.b - self.p2)*self.options.distance
|
|
h2_end = self.b + norm(self.b - self.p2)*self.options.overlap
|
|
|
|
# print the lines
|
|
hline1 = inkex.etree.SubElement(self.grp, inkex.addNS('path', 'svg'), self.helpline_attribs)
|
|
hline1.set('d', 'M %f,%f %f,%f' % (h1_start[0], h1_start[1],h1_end[0],h1_end[1],))
|
|
|
|
hline2 = inkex.etree.SubElement(self.grp, inkex.addNS('path', 'svg'), self.helpline_attribs)
|
|
hline2.set('d', 'M %f,%f %f,%f' % (h2_start[0], h2_start[1],h2_end[0],h2_end[1],))
|
|
|
|
def setMarker(self, option):
|
|
if option=='inside':
|
|
# inside
|
|
self.arrowlen = 6.0 * self.options.line_scale
|
|
self.dimline_style['marker-start'] = 'url(#ArrowDIN-start)'
|
|
self.dimline_style['marker-end'] = 'url(#ArrowDIN-end)'
|
|
self.makeMarkerstyle('ArrowDIN-start', False)
|
|
self.makeMarkerstyle('ArrowDIN-end', True)
|
|
else:
|
|
# outside
|
|
self.arrowlen = 0
|
|
self.dimline_style['marker-start'] = 'url(#ArrowDINout-start)'
|
|
self.dimline_style['marker-end'] = 'url(#ArrowDINout-end)'
|
|
self.makeMarkerstyle('ArrowDINout-start', False)
|
|
self.makeMarkerstyle('ArrowDINout-end', True)
|
|
self.dimline_attribs['style'] = simplestyle.formatStyle(self.dimline_style)
|
|
|
|
def drawDimension(self):
|
|
# critical length, when inside snaps to outside
|
|
critical_length = 35 * self.options.line_scale
|
|
if self.options.arrow_orientation == 'auto':
|
|
if np.abs(np.dot(self.e1, self.b - self.a)) > critical_length:
|
|
self.setMarker('inside')
|
|
else:
|
|
self.setMarker('outside')
|
|
else:
|
|
self.setMarker(self.options.arrow_orientation)
|
|
# start- and endpoint of the dimension line
|
|
dim_start = self.a + self.arrowlen*norm(self.b - self.a)
|
|
dim_end = self.b - self.arrowlen*norm(self.b - self.a)
|
|
# print
|
|
dimline = inkex.etree.SubElement(self.grp, inkex.addNS('path', 'svg'), self.dimline_attribs)
|
|
dimline.set('d', 'M %f,%f %f,%f' % (dim_start[0], dim_start[1], dim_end[0], dim_end[1]))
|
|
|
|
def drawText(self):
|
|
# distance of text to the dimension line
|
|
self.textdistance = 5.0 * self.options.line_scale
|
|
if self.e2[1] > 0:
|
|
textpoint = (self.a + self.b)/2 - self.e2*self.textdistance
|
|
elif self.e2[1] == 0:
|
|
textpoint = (self.a + self.b)/2 - np.array([1,0])*self.textdistance
|
|
else:
|
|
textpoint = (self.a + self.b)/2 + self.e2*self.textdistance
|
|
|
|
value = np.abs(np.dot(self.e1, self.b - self.a)) / (self.getUnittouu(str(self.options.scale_factor)+self.options.unit))
|
|
string_value = str(round(value, self.options.digit))
|
|
# chop off last characters if digit is zero or negative
|
|
if self.options.digit <=0:
|
|
string_value = string_value[:-2]
|
|
text = inkex.etree.SubElement(self.grp, inkex.addNS('text', 'svg'), self.text_attribs)
|
|
# The alternative for framing with dollars, when LATEX Math export is seeked
|
|
# text.text = '$' + string_value + '$'
|
|
text.text = string_value
|
|
text.set('x', str(textpoint[0]))
|
|
text.set('y', str(textpoint[1]))
|
|
if self.options.rotate:
|
|
text.set('transform', rotate(self.e1, textpoint))
|
|
|
|
# call the object function
|
|
if __name__ == '__main__':
|
|
a = Dimensioning()
|
|
a.affect()
|