1 #!/usr/bin/env python
2 '''
3 This extension module can measure arbitrary path and object length
4 It adds a text to the selected path containing the length in a
5 given unit.
7 Copyright (C) 2010 Alvin Penner
8 Copyright (C) 2006 Georg Wiora
9 Copyright (C) 2006 Nathan Hurst
10 Copyright (C) 2005 Aaron Spike, aaron@ekips.org
12 This program is free software; you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation; either version 2 of the License, or
15 (at your option) any later version.
17 This program is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with this program; if not, write to the Free Software
24 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 TODO:
27 * should use the standard attributes for text
28 * Implement option to keep text orientation upright
29 1. Find text direction i.e. path tangent,
30 2. check direction >90 or <-90 Degrees
31 3. rotate by 180 degrees around text center
32 '''
33 import inkex, simplestyle, simplepath, sys, cubicsuperpath, bezmisc, locale
34 # Set current system locale
35 locale.setlocale(locale.LC_ALL, '')
37 def numsegs(csp):
38 return sum([len(p)-1 for p in csp])
39 def interpcoord(v1,v2,p):
40 return v1+((v2-v1)*p)
41 def interppoints(p1,p2,p):
42 return [interpcoord(p1[0],p2[0],p),interpcoord(p1[1],p2[1],p)]
43 def pointdistance((x1,y1),(x2,y2)):
44 return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2))
45 def bezlenapprx(sp1, sp2):
46 return pointdistance(sp1[1], sp1[2]) + pointdistance(sp1[2], sp2[0]) + pointdistance(sp2[0], sp2[1])
47 def tpoint((x1,y1), (x2,y2), t = 0.5):
48 return [x1+t*(x2-x1),y1+t*(y2-y1)]
49 def cspbezsplit(sp1, sp2, t = 0.5):
50 m1=tpoint(sp1[1],sp1[2],t)
51 m2=tpoint(sp1[2],sp2[0],t)
52 m3=tpoint(sp2[0],sp2[1],t)
53 m4=tpoint(m1,m2,t)
54 m5=tpoint(m2,m3,t)
55 m=tpoint(m4,m5,t)
56 return [[sp1[0][:],sp1[1][:],m1], [m4,m,m5], [m3,sp2[1][:],sp2[2][:]]]
57 def cspbezsplitatlength(sp1, sp2, l = 0.5, tolerance = 0.001):
58 bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])
59 t = bezmisc.beziertatlength(bez, l, tolerance)
60 return cspbezsplit(sp1, sp2, t)
61 def cspseglength(sp1,sp2, tolerance = 0.001):
62 bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])
63 return bezmisc.bezierlength(bez, tolerance)
64 def csplength(csp):
65 total = 0
66 lengths = []
67 for sp in csp:
68 lengths.append([])
69 for i in xrange(1,len(sp)):
70 l = cspseglength(sp[i-1],sp[i])
71 lengths[-1].append(l)
72 total += l
73 return lengths, total
74 def csparea(csp):
75 area = 0.0
76 n0 = 0.0
77 x0 = 0.0
78 y0 = 0.0
79 for sp in csp:
80 for i in range(len(sp)): # calculate polygon area
81 area += 0.5*sp[i-1][1][0]*(sp[i][1][1] - sp[i-2][1][1])
82 if abs(sp[i-1][1][0]-sp[i][1][0]) > 0.001 or abs(sp[i-1][1][1]-sp[i][1][1]) > 0.001:
83 n0 += 1.0
84 x0 += sp[i][1][0]
85 y0 += sp[i][1][1]
86 for i in range(1, len(sp)): # add contribution from cubic Bezier
87 bezarea = ( 0.0*sp[i-1][1][1] + 2.0*sp[i-1][2][1] + 1.0*sp[i][0][1] - 3.0*sp[i][1][1])*sp[i-1][1][0]
88 bezarea += (-2.0*sp[i-1][1][1] + 0.0*sp[i-1][2][1] + 1.0*sp[i][0][1] + 1.0*sp[i][1][1])*sp[i-1][2][0]
89 bezarea += (-1.0*sp[i-1][1][1] - 1.0*sp[i-1][2][1] + 0.0*sp[i][0][1] + 2.0*sp[i][1][1])*sp[i][0][0]
90 bezarea += ( 3.0*sp[i-1][1][1] - 1.0*sp[i-1][2][1] - 2.0*sp[i][0][1] + 0.0*sp[i][1][1])*sp[i][1][0]
91 area += 0.15*bezarea
92 return abs(area), x0/n0, y0/n0
94 class Length(inkex.Effect):
95 def __init__(self):
96 inkex.Effect.__init__(self)
97 self.OptionParser.add_option("--type",
98 action="store", type="string",
99 dest="type", default="length",
100 help="Type of measurement")
101 self.OptionParser.add_option("-f", "--fontsize",
102 action="store", type="int",
103 dest="fontsize", default=20,
104 help="Size of length lable text in px")
105 self.OptionParser.add_option("-o", "--offset",
106 action="store", type="float",
107 dest="offset", default=-6,
108 help="The distance above the curve")
109 self.OptionParser.add_option("-u", "--unit",
110 action="store", type="string",
111 dest="unit", default="mm",
112 help="The unit of the measurement")
113 self.OptionParser.add_option("-p", "--precision",
114 action="store", type="int",
115 dest="precision", default=2,
116 help="Number of significant digits after decimal point")
117 self.OptionParser.add_option("-s", "--scale",
118 action="store", type="float",
119 dest="scale", default=1,
120 help="The distance above the curve")
121 self.OptionParser.add_option("-r", "--orient",
122 action="store", type="inkbool",
123 dest="orient", default=True,
124 help="Keep orientation of text upright")
125 self.OptionParser.add_option("--tab",
126 action="store", type="string",
127 dest="tab", default="sampling",
128 help="The selected UI-tab when OK was pressed")
129 self.OptionParser.add_option("--measurehelp",
130 action="store", type="string",
131 dest="measurehelp", default="",
132 help="dummy")
134 def effect(self):
135 # get number of digits
136 prec = int(self.options.precision)
137 # loop over all selected paths
138 for id, node in self.selected.iteritems():
139 if node.tag == inkex.addNS('path','svg'):
140 self.group = inkex.etree.SubElement(node.getparent(),inkex.addNS('text','svg'))
142 t = node.get('transform')
143 # Removed to fix LP #308183
144 # (Measure Path text shifted when used with a copied object)
145 #if t:
146 # self.group.set('transform', t)
149 a =[]
150 p = cubicsuperpath.parsePath(node.get('d'))
151 num = 1
152 factor = 1.0/inkex.unittouu('1'+self.options.unit)
153 if self.options.type == "length":
154 slengths, stotal = csplength(p)
155 else:
156 stotal,x0,y0 = csparea(p)
157 stotal *= factor*self.options.scale
158 # Format the length as string
159 lenstr = locale.format("%(len)25."+str(prec)+"f",{'len':round(stotal*factor*self.options.scale,prec)}).strip()
160 if self.options.type == "length":
161 self.addTextOnPath(self.group,0, 0,lenstr+' '+self.options.unit, id, self.options.offset)
162 else:
163 self.addTextWithTspan(self.group,x0,y0,lenstr+' '+self.options.unit+'^2', id, self.options.offset)
166 def addTextOnPath(self,node,x,y,text, id,dy=0):
167 new = inkex.etree.SubElement(node,inkex.addNS('textPath','svg'))
168 s = {'text-align': 'center', 'vertical-align': 'bottom',
169 'text-anchor': 'middle', 'font-size': str(self.options.fontsize),
170 'fill-opacity': '1.0', 'stroke': 'none',
171 'font-weight': 'normal', 'font-style': 'normal', 'fill': '#000000'}
172 new.set('style', simplestyle.formatStyle(s))
173 new.set(inkex.addNS('href','xlink'), '#'+id)
174 new.set('startOffset', "50%")
175 new.set('dy', str(dy)) # dubious merit
176 #new.append(tp)
177 new.text = str(text)
178 #node.set('transform','rotate(180,'+str(-x)+','+str(-y)+')')
179 node.set('x', str(x))
180 node.set('y', str(y))
182 def addTextWithTspan(self,node,x,y,text,id,dy=0):
183 new = inkex.etree.SubElement(node,inkex.addNS('tspan','svg'), {inkex.addNS('role','sodipodi'): 'line'})
184 s = {'text-align': 'center', 'vertical-align': 'bottom',
185 'text-anchor': 'middle', 'font-size': str(self.options.fontsize),
186 'fill-opacity': '1.0', 'stroke': 'none',
187 'font-weight': 'normal', 'font-style': 'normal', 'fill': '#000000'}
188 new.set('style', simplestyle.formatStyle(s))
189 new.set(inkex.addNS('href','xlink'), '#'+id)
190 new.set('startOffset', "50%")
191 new.set('dy', str(dy))
192 new.text = str(text)
193 node.set('x', str(x))
194 node.set('y', str(y))
196 if __name__ == '__main__':
197 e = Length()
198 e.affect()
201 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99