Code

made it callable by other extensions. the workaround is sorta ugly, but it should...
[inkscape.git] / share / extensions / pathmodifier.py
1 #!/usr/bin/env python\r
2 '''\r
3 Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr\r
4 \r
5 This program is free software; you can redistribute it and/or modify\r
6 it under the terms of the GNU General Public License as published by\r
7 the Free Software Foundation; either version 2 of the License, or\r
8 (at your option) any later version.\r
9 \r
10 This program is distributed in the hope that it will be useful,\r
11 but WITHOUT ANY WARRANTY; without even the implied warranty of\r
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
13 GNU General Public License for more details.\r
14 \r
15 You should have received a copy of the GNU General Public License\r
16 along with this program; if not, write to the Free Software\r
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
18 barraud@math.univ-lille1.fr\r
19 '''\r
20 import inkex, cubicsuperpath, bezmisc, simplestyle\r
21 import copy, math, re, random, xml.xpath\r
22 \r
23 def parseTransform(transf,mat=[[1.0,0.0,0.0],[0.0,1.0,0.0]]):\r
24     if transf=="":\r
25         return(mat)\r
26     result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\(([^)]*)\)",transf)\r
27 #-- translate --\r
28     if result.group(1)=="translate":\r
29         args=result.group(2).split(",")\r
30         dx=float(args[0])\r
31         if len(args)==1:\r
32             dy=0.0\r
33         else:\r
34             dy=float(args[1])\r
35         matrix=[[1,0,dx],[0,1,dy]]\r
36 #-- scale --\r
37     if result.groups(1)=="scale":\r
38         args=result.group(2).split(",")\r
39         sx=float(args[0])\r
40         if len(args)==1:\r
41             sy=sx\r
42         else:\r
43             sy=float(args[1])\r
44         matrix=[[sx,0,0],[0,sy,0]]\r
45 #-- rotate --\r
46     if result.groups(1)=="rotate":\r
47         args=result.group(2).split(",")\r
48         a=float(args[0])*math.pi/180\r
49         if len(args)==1:\r
50             cx,cy=(0.0,0.0)\r
51         else:\r
52             cx,cy=args[1:]\r
53         matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]]\r
54 #-- skewX --\r
55     if result.groups(1)=="skewX":\r
56         a=float(result.group(2))*math.pi/180\r
57         matrix=[[1,math.tan(a),0],[0,1,0]]\r
58 #-- skewX --\r
59     if result.groups(1)=="skewX":\r
60         a=float(result.group(2))*math.pi/180\r
61         matrix=[[1,0,0],[math.tan(a),1,0]]\r
62 #-- matrix --\r
63     if result.group(1)=="matrix":\r
64         a11,a21,a12,a22,v1,v2=result.group(2).split(",")\r
65         matrix=[[float(a11),float(a12),float(v1)],[float(a21),float(a22),float(v2)]]\r
66     \r
67     matrix=composeTransform(mat,matrix)\r
68     if result.end()<len(transf):\r
69         return(parseTransform(transf[result.end():],matrix))\r
70     else:\r
71         return matrix\r
72 \r
73 def formatTransform(mat):\r
74     return("matrix(%f,%f,%f,%f,%f,%f)"%(mat[0][0],mat[1][0],mat[0][1],mat[1][1],mat[0][2],mat[1][2]))\r
75 \r
76 def composeTransform(M1,M2):\r
77     a11=M1[0][0]*M2[0][0]+M1[0][1]*M2[1][0]\r
78     a12=M1[0][0]*M2[0][1]+M1[0][1]*M2[1][1]\r
79     a21=M1[1][0]*M2[0][0]+M1[1][1]*M2[1][0]\r
80     a22=M1[1][0]*M2[0][1]+M1[1][1]*M2[1][1]\r
81 \r
82     v1=M1[0][0]*M2[0][2]+M1[0][1]*M2[1][2]+M1[0][2]\r
83     v2=M1[1][0]*M2[0][2]+M1[1][1]*M2[1][2]+M1[1][2]\r
84     return [[a11,a12,v1],[a21,a22,v2]]\r
85 \r
86 def applyTransformToNode(mat,node):\r
87     m=parseTransform(node.getAttributeNS(None,"transform"))\r
88     newtransf=formatTransform(composeTransform(mat,m))\r
89     node.setAttributeNS(None,"transform", newtransf)\r
90 \r
91 def applyTransformToPoint(mat,pt):\r
92     x=mat[0][0]*pt[0]+mat[0][1]*pt[1]+mat[0][2]\r
93     y=mat[1][0]*pt[0]+mat[1][1]*pt[1]+mat[1][2]\r
94     pt[0]=x\r
95     pt[1]=y\r
96 \r
97 def fuseTransform(node):\r
98     m=parseTransform(node.getAttributeNS(None,"transform"))\r
99     d = node.getAttributeNS(None,'d')\r
100     p=cubicsuperpath.parsePath(d)\r
101     for comp in p:\r
102         for ctl in comp:\r
103             for pt in ctl:\r
104                 applyTransformToPoint(m,pt)\r
105     node.setAttributeNS(None,'d', cubicsuperpath.formatPath(p))\r
106     node.removeAttributeNS(None,"transform")\r
107 \r
108 \r
109 def boxunion(b1,b2):\r
110     if b1 is None:\r
111         return b2\r
112     elif b2 is None:\r
113         return b1    \r
114     else:\r
115         return((min(b1[0],b2[0]),max(b1[1],b2[1]),min(b1[2],b2[2]),max(b1[3],b2[3])))\r
116 \r
117 def roughBBox(path):\r
118     xmin,xMax,ymin,yMax=path[0][0][0][0],path[0][0][0][0],path[0][0][0][1],path[0][0][0][1]\r
119     for pathcomp in path:\r
120         for ctl in pathcomp:\r
121            for pt in ctl:\r
122                xmin=min(xmin,pt[0])\r
123                xMax=max(xMax,pt[0])\r
124                ymin=min(ymin,pt[1])\r
125                yMax=max(yMax,pt[1])\r
126     return xmin,xMax,ymin,yMax\r
127 \r
128 \r
129 class PathModifier(inkex.Effect):\r
130     def __init__(self):\r
131         inkex.Effect.__init__(self)\r
132 \r
133 ##################################\r
134 #-- Selectionlists manipulation --\r
135 ##################################\r
136     def computeBBox(self, aList):\r
137         bbox=None\r
138         for id, node in aList.iteritems():\r
139             if node.tagName == 'path':\r
140                 d = node.attributes.getNamedItem('d')\r
141                 p = cubicsuperpath.parsePath(d.value)\r
142                 bbox=boxunion(roughBBox(p),bbox)\r
143         return bbox\r
144 \r
145     def duplicateNodes(self, aList):\r
146         clones={}\r
147         for id,node in aList.iteritems():\r
148             clone=node.cloneNode(True)\r
149     #!!!--> should it be given an id?\r
150     #seems to work without this!?!\r
151             clone.setAttributeNS(None,"id", self.uniqueId(node.tagName))\r
152             node.parentNode.appendChild(clone)\r
153             clones[clone.getAttributeNS(None,"id")]=clone\r
154         return(clones)\r
155 \r
156     def uniqueId(self, prefix):\r
157         id="%s%04i"%(prefix,random.randint(0,9999))\r
158         while len(xml.xpath.Evaluate('//*[@id="%s"]' % id,self.document)):\r
159             id="%s%04i"%(prefix,random.randint(0,9999))\r
160         return(id)\r
161 \r
162     def expandGroups(self,aList,transferTransform=True):\r
163         for id, node in aList.items():      \r
164             if node.tagName == 'g':\r
165                 mat=parseTransform(node.getAttributeNS(None,"transform"))\r
166                 for child in node.childNodes:\r
167                     if child.nodeType==child.ELEMENT_NODE:\r
168                         if transferTransform:\r
169                             applyTransformToNode(mat,child)\r
170                         aList.update(self.expandGroups({child.getAttribute('id'):child}))\r
171                 if transferTransform:\r
172                     node.removeAttribute("transform")\r
173                 del aList[id]\r
174         return(aList)\r
175 \r
176     def expandGroupsUnlinkClones(self,aList,transferTransform=True,doReplace=True):\r
177         for id in aList.keys()[:]:     \r
178             node=aList[id]\r
179             if node.tagName == 'g':\r
180                 self.expandGroups(aList,transferTransform)\r
181                 self.expandGroupsUnlinkClones(aList,transferTransform,doReplace)\r
182                 #Hum... not very efficient if there are many clones of groups...\r
183             elif node.tagName == 'use':\r
184                 refid=node.getAttributeNS(inkex.NSS[u'xlink'],'href')\r
185                 path = '//*[@id="%s"]' % refid[1:]\r
186                 refnode = xml.xpath.Evaluate(path,self.document)[0]\r
187                 newnode=refnode.cloneNode(True)\r
188                 self.recursNewIds(newnode)\r
189 \r
190                 if node.hasAttributeNS(None,u'style'):\r
191                     style=simplestyle.parseStyle(node.getAttributeNS(None,u'style'))\r
192                     refstyle=simplestyle.parseStyle(refnode.getAttributeNS(None,u'style'))\r
193                     style.update(refstyle)\r
194                     newnode.setAttributeNS(None,'style',simplestyle.formatStyle(style))\r
195                 applyTransformToNode(parseTransform(node.getAttributeNS(None,'transform')),newnode)\r
196                 if doReplace:\r
197                     parent=node.parentNode\r
198                     parent.insertBefore(newnode,node)\r
199                     parent.removeChild(node)\r
200                 del aList[id]\r
201                 newid=newnode.getAttributeNS(None,'id')\r
202                 aList.update(self.expandGroupsUnlinkClones({newid:newnode},transferTransform,doReplace))\r
203         return aList\r
204     \r
205     def recursNewIds(self,node):\r
206         if node.nodeType==node.ELEMENT_NODE and node.hasAttributeNS(None,u'id'):\r
207             node.setAttributeNS(None,u'id',self.uniqueId(node.tagName))\r
208         for child in node.childNodes:\r
209             self.recursNewIds(child)\r
210                 \r
211 \r
212         \r
213 \r
214 #     def makeClonesReal(self,aList,doReplace=True,recursivelytransferTransform=True):\r
215 #         for id in aList.keys():     \r
216 #             node=aList[id]\r
217 #             if node.tagName == 'g':\r
218 #                 childs={}\r
219 #                 for child in node.childNodes:\r
220 #                     if child.nodeType==child.ELEMENT_NODE:\r
221 #                       childid=child.getAttributeNS(None,'id')\r
222 #                       del aList[childid]\r
223 #                         aList.update(self.makeClonesReal({childid:child},doReplace))\r
224 #             elif node.tagName == 'use':\r
225 #                 refid=node.getAttributeNS(inkex.NSS[u'xlink'],'href')\r
226 #                 path = '//*[@id="%s"]' % refid[1:]\r
227 #                 refnode = xml.xpath.Evaluate(path,document)[0]\r
228 #                 clone=refnode.cloneNode(True)\r
229 #               cloneid=self.uniqueId(clone.tagName)\r
230 #                 clone.setAttributeNS(None,'id', cloneid)\r
231 #                 style=simplestyle.parseStyle(node.getAttributeNS(None,u'style'))\r
232 #                 refstyle=simplestyle.parseStyle(refnode.getAttributeNS(None,u'style'))\r
233 #                 style.update(refstyle)\r
234 #                 clone.setAttributeNS(None,'style',simplestyle.formatStyle(style))\r
235 #                 applyTransformToNode(parseTransform(node.getAttributeNS(None,'transform')),clone)\r
236 #                 if doReplace:\r
237 #                     parent=node.parentNode\r
238 #                     parent.insertBefore(clone,node)\r
239 #                     parent.removeChild(node)\r
240 #                 del aList[id]\r
241 #                 aList.update(self.expandGroupsUnlinkClones({cloneid:clone}))\r
242 #         return aList\r
243 \r
244 ################################\r
245 #-- Object conversion ----------\r
246 ################################\r
247 \r
248     def rectToPath(self,node,doReplace=True):\r
249         if node.tagName == 'rect':\r
250             x =float(node.getAttributeNS(None,u'x'))\r
251             y =float(node.getAttributeNS(None,u'y'))\r
252             try:\r
253                 rx=float(node.getAttributeNS(None,u'rx'))\r
254                 ry=float(node.getAttributeNS(None,u'ry'))\r
255             except:\r
256                 rx=0\r
257                 ry=0\r
258             w =float(node.getAttributeNS(None,u'width' ))\r
259             h =float(node.getAttributeNS(None,u'height'))\r
260             d ='M %f,%f '%(x+rx,y)\r
261             d+='L %f,%f '%(x+w-rx,y)\r
262             d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w,y+ry)\r
263             d+='L %f,%f '%(x+w,y+h-ry)\r
264             d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w-rx,y+h)\r
265             d+='L %f,%f '%(x+rx,y+h)\r
266             d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x,y+h-ry)\r
267             d+='L %f,%f '%(x,y+ry)\r
268             d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+rx,y)\r
269 \r
270             newnode=self.document.createElement('path')\r
271             newnode.setAttributeNS(None,'d',d)\r
272             newnode.setAttributeNS(None,'id', self.uniqueId('path'))\r
273             newnode.setAttributeNS(None,'style',node.getAttributeNS(None,u'style'))\r
274             newnode.setAttributeNS(None,'transform',node.getAttributeNS(None,u'transform'))\r
275             fuseTransform(newnode)\r
276             if doReplace:\r
277                 parent=node.parentNode\r
278                 parent.insertBefore(newnode,node)\r
279                 parent.removeChild(node)\r
280             return newnode\r
281 \r
282     def objectToPath(self,node,doReplace=True):\r
283     #--TODO: support other object types!!!!\r
284     #--TODO: make sure cubicsuperpath supports A and Q commands... \r
285         if node.tagName == 'rect':\r
286             return(self.rectToPath(node,doReplace))\r
287         elif node.tagName == 'path':\r
288             attributes = node.attributes.keys()\r
289             for uri,attName in attributes:\r
290                 if uri in [inkex.NSS[u'sodipodi'],inkex.NSS[u'inkscape']]:\r
291 #                if attName not in ["d","id","style","transform"]:\r
292                     node.removeAttributeNS(uri,attName)\r
293             fuseTransform(node)\r
294             return node\r
295         else:\r
296             inkex.debug("Please first convert objects to paths!...(got '%s')"%node.tagName)\r
297             return None\r
298 \r
299     def objectsToPaths(self,aList,doReplace=True):\r
300         newSelection={}\r
301         for id,node in aList.items():\r
302             newnode=self.objectToPath(node,self.document)\r
303             del aList[id]\r
304             aList[newnode.getAttributeNS(None,u'id')]=newnode\r
305 \r
306 \r
307 ################################\r
308 #-- Action ----------\r
309 ################################\r
310             \r
311     #-- overwrite this method in subclasses...\r
312     def effect(self):\r
313         #self.duplicateNodes(self.selected)\r
314         self.expandGroupsUnlinkClones(self.selected, True)\r
315         self.objectsToPaths(self.selected, True)\r
316         self.bbox=self.computeBBox(self.selected)\r
317         for id, node in self.selected.iteritems():\r
318             if node.tagName == 'path':\r
319                 d = node.attributes.getNamedItem('d')\r
320                 p = cubicsuperpath.parsePath(d.value)\r
321 \r
322                 #do what ever you want with p!\r
323 \r
324                 d.value = cubicsuperpath.formatPath(p)\r
325 \r
326 \r
327 class Diffeo(PathModifier):\r
328     def __init__(self):\r
329         inkex.Effect.__init__(self)\r
330 \r
331     def applyDiffeo(self,bpt,vects=()):\r
332         '''\r
333         bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt. \r
334         Defaults to identity!\r
335         '''\r
336         for v in vects:\r
337             v[0]-=bpt[0]\r
338             v[1]-=bpt[1]\r
339 \r
340         #-- your transformations go here:\r
341         #x,y=bpt\r
342         #bpt[0]=f(x,y)\r
343         #bpt[1]=g(x,y)\r
344         #for v in vects:\r
345         #    vx,vy=v\r
346         #    v[0]=df/dx(x,y)*vx+df/dy(x,y)*vy\r
347         #    v[1]=dg/dx(x,y)*vx+dg/dy(x,y)*vy\r
348         #\r
349         #-- !caution! y-axis is pointing downward!\r
350 \r
351         for v in vects:\r
352             v[0]+=bpt[0]\r
353             v[1]+=bpt[1]\r
354 \r
355 \r
356     def effect(self):\r
357         #self.duplicateNodes(self.selected)\r
358         self.expandGroupsUnlinkClones(self.selected, True)\r
359         self.expandGroups(self.selected, True)\r
360         self.objectsToPaths(self.selected, True)\r
361         self.bbox=self.computeBBox(self.selected)\r
362         for id, node in self.selected.iteritems():\r
363             if node.tagName == 'path':\r
364                 d = node.attributes.getNamedItem('d')\r
365                 p = cubicsuperpath.parsePath(d.value)\r
366 \r
367                 for sub in p:\r
368                     for ctlpt in sub:\r
369                         self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))\r
370 \r
371                 d.value = cubicsuperpath.formatPath(p)\r
372 \r
373 #e = Diffeo()\r
374 #e.affect()\r
376