1 #!/usr/bin/env python
2 '''
3 Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 barraud@math.univ-lille1.fr
20 This code defines a basic class (PathModifier) of effects whose purpose is
21 to somehow deform given objects: one common tasks for all such effect is to
22 convert shapes, groups, clones to paths. The class has several functions to
23 make this (more or less!) easy.
24 As an exemple, a second class (Diffeo) is derived from it,
25 to implement deformations of the form X=f(x,y), Y=g(x,y)...
27 TODO: Several handy functions are defined, that might in fact be of general
28 interest and that should be shipped out in separate files...
29 '''
30 import inkex, cubicsuperpath, bezmisc, simplestyle
31 from simpletransform import *
32 import copy, math, re, random
34 ####################################################################
35 ##-- zOrder computation...
36 ##-- this should be shipped out in a separate file. inkex.py?
38 def zSort(inNode,idList):
39 sortedList=[]
40 theid = inNode.get("id")
41 if theid in idList:
42 sortedList.append(theid)
43 for child in inNode:
44 if len(sortedList)==len(idList):
45 break
46 sortedList+=zSort(child,idList)
47 return sortedList
50 class PathModifier(inkex.Effect):
51 def __init__(self):
52 inkex.Effect.__init__(self)
54 ##################################
55 #-- Selectionlists manipulation --
56 ##################################
58 def duplicateNodes(self, aList):
59 clones={}
60 for id,node in aList.iteritems():
61 clone=copy.deepcopy(node)
62 #!!!--> should it be given an id?
63 #seems to work without this!?!
64 myid = node.tag.split('}')[-1]
65 clone.set("id", self.uniqueId(myid))
66 node.getparent().append(clone)
67 clones[clone.get("id")]=clone
68 return(clones)
70 def uniqueId(self, prefix):
71 id="%s%04i"%(prefix,random.randint(0,9999))
72 while len(self.document.getroot().xpath('//*[@id="%s"]' % id,namespaces=inkex.NSS)):
73 id="%s%04i"%(prefix,random.randint(0,9999))
74 return(id)
76 def expandGroups(self,aList,transferTransform=True):
77 for id, node in aList.items():
78 if node.tag == inkex.addNS('g','svg') or node.tag=='g':
79 mat=parseTransform(node.get("transform"))
80 for child in node:
81 if transferTransform:
82 applyTransformToNode(mat,child)
83 aList.update(self.expandGroups({child.get('id'):child}))
84 if transferTransform and node.get("transform"):
85 del node.attrib["transform"]
86 del aList[id]
87 return(aList)
89 def expandGroupsUnlinkClones(self,aList,transferTransform=True,doReplace=True):
90 for id in aList.keys()[:]:
91 node=aList[id]
92 if node.tag == inkex.addNS('g','svg') or node.tag=='g':
93 self.expandGroups(aList,transferTransform)
94 self.expandGroupsUnlinkClones(aList,transferTransform,doReplace)
95 #Hum... not very efficient if there are many clones of groups...
97 elif node.tag == inkex.addNS('use','svg') or node.tag=='use':
98 refnode=self.refNode(node)
99 newnode=self.unlinkClone(node,doReplace)
100 del aList[id]
102 style = simplestyle.parseStyle(node.get('style') or "")
103 refstyle=simplestyle.parseStyle(refnode.get('style') or "")
104 style.update(refstyle)
105 newnode.set('style',simplestyle.formatStyle(style))
107 newid=newnode.get('id')
108 aList.update(self.expandGroupsUnlinkClones({newid:newnode},transferTransform,doReplace))
109 return aList
111 def recursNewIds(self,node):
112 if node.get('id'):
113 node.set('id',self.uniqueId(node.tag))
114 for child in node:
115 self.recursNewIds(child)
117 def refNode(self,node):
118 if node.get(inkex.addNS('href','xlink')):
119 refid=node.get(inkex.addNS('href','xlink'))
120 path = '//*[@id="%s"]' % refid[1:]
121 newNode = self.document.getroot().xpath(path, namespaces=inkex.NSS)[0]
122 return newNode
123 else:
124 raise AssertionError, "Trying to follow empty xlink.href attribute."
126 def unlinkClone(self,node,doReplace):
127 if node.tag == inkex.addNS('use','svg') or node.tag=='use':
128 newNode = copy.deepcopy(self.refNode(node))
129 self.recursNewIds(newNode)
130 applyTransformToNode(parseTransform(node.get('transform')),newNode)
132 if doReplace:
133 parent=node.getparent()
134 parent.insert(parent.index(node),newNode)
135 parent.remove(node)
137 return newNode
138 else:
139 raise AssertionError, "Only clones can be unlinked..."
143 ################################
144 #-- Object conversion ----------
145 ################################
147 def rectToPath(self,node,doReplace=True):
148 if node.tag == inkex.addNS('rect','svg'):
149 x =float(node.get('x'))
150 y =float(node.get('y'))
151 #FIXME: no exception anymore and sometimes just one
152 try:
153 rx=float(node.get('rx'))
154 ry=float(node.get('ry'))
155 except:
156 rx=0
157 ry=0
158 w =float(node.get('width' ))
159 h =float(node.get('height'))
160 d ='M %f,%f '%(x+rx,y)
161 d+='L %f,%f '%(x+w-rx,y)
162 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w,y+ry)
163 d+='L %f,%f '%(x+w,y+h-ry)
164 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w-rx,y+h)
165 d+='L %f,%f '%(x+rx,y+h)
166 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x,y+h-ry)
167 d+='L %f,%f '%(x,y+ry)
168 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+rx,y)
170 newnode=inkex.etree.Element('path')
171 newnode.set('d',d)
172 newnode.set('id', self.uniqueId('path'))
173 newnode.set('style',node.get('style'))
174 nnt = node.get('transform')
175 if nnt:
176 newnode.set('transform',nnt)
177 fuseTransform(newnode)
178 if doReplace:
179 parent=node.getparent()
180 parent.insert(parent.index(node),newnode)
181 parent.remove(node)
182 return newnode
184 def groupToPath(self,node,doReplace=True):
185 if node.tag == inkex.addNS('g','svg'):
186 newNode = inkex.etree.SubElement(self.current_layer,inkex.addNS('path','svg'))
188 newstyle = simplestyle.parseStyle(node.get('style') or "")
189 newp = []
190 for child in node:
191 childstyle = simplestyle.parseStyle(child.get('style') or "")
192 childstyle.update(newstyle)
193 newstyle.update(childstyle)
194 childAsPath = self.objectToPath(child,False)
195 newp += cubicsuperpath.parsePath(childAsPath.get('d'))
196 newNode.set('d',cubicsuperpath.formatPath(newp))
197 newNode.set('style',simplestyle.formatStyle(newstyle))
199 self.current_layer.remove(newNode)
200 if doReplace:
201 parent=node.getparent()
202 parent.insert(parent.index(node),newNode)
203 parent.remove(node)
205 return newNode
206 else:
207 raise AssertionError
209 def objectToPath(self,node,doReplace=True):
210 #--TODO: support other object types!!!!
211 #--TODO: make sure cubicsuperpath supports A and Q commands...
212 if node.tag == inkex.addNS('rect','svg'):
213 return(self.rectToPath(node,doReplace))
214 if node.tag == inkex.addNS('g','svg'):
215 return(self.groupToPath(node,doReplace))
216 elif node.tag == inkex.addNS('path','svg') or node.tag == 'path':
217 #remove inkscape attributes, otherwise any modif of 'd' will be discarded!
218 for attName in node.attrib.keys():
219 if ("sodipodi" in attName) or ("inkscape" in attName):
220 del node.attrib[attName]
221 fuseTransform(node)
222 return node
223 elif node.tag == inkex.addNS('use','svg') or node.tag == 'use':
224 newNode = self.unlinkClone(node,doReplace)
225 return self.objectToPath(newNode,doReplace)
226 else:
227 inkex.debug("Please first convert objects to paths!...(got '%s')"%node.tag)
228 return None
230 def objectsToPaths(self,aList,doReplace=True):
231 newSelection={}
232 for id,node in aList.items():
233 newnode=self.objectToPath(node,doReplace)
234 del aList[id]
235 aList[newnode.get('id')]=newnode
238 ################################
239 #-- Action ----------
240 ################################
242 #-- overwrite this method in subclasses...
243 def effect(self):
244 #self.duplicateNodes(self.selected)
245 #self.expandGroupsUnlinkClones(self.selected, True)
246 self.objectsToPaths(self.selected, True)
247 self.bbox=computeBBox(self.selected.values())
248 for id, node in self.selected.iteritems():
249 if node.tag == inkex.addNS('path','svg'):
250 d = node.get('d')
251 p = cubicsuperpath.parsePath(d)
253 #do what ever you want with p!
255 node.set('d',cubicsuperpath.formatPath(p))
258 class Diffeo(PathModifier):
259 def __init__(self):
260 inkex.Effect.__init__(self)
262 def applyDiffeo(self,bpt,vects=()):
263 '''
264 bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
265 Defaults to identity!
266 '''
267 for v in vects:
268 v[0]-=bpt[0]
269 v[1]-=bpt[1]
271 #-- your transformations go here:
272 #x,y=bpt
273 #bpt[0]=f(x,y)
274 #bpt[1]=g(x,y)
275 #for v in vects:
276 # vx,vy=v
277 # v[0]=df/dx(x,y)*vx+df/dy(x,y)*vy
278 # v[1]=dg/dx(x,y)*vx+dg/dy(x,y)*vy
279 #
280 #-- !caution! y-axis is pointing downward!
282 for v in vects:
283 v[0]+=bpt[0]
284 v[1]+=bpt[1]
287 def effect(self):
288 #self.duplicateNodes(self.selected)
289 self.expandGroupsUnlinkClones(self.selected, True)
290 self.expandGroups(self.selected, True)
291 self.objectsToPaths(self.selected, True)
292 self.bbox=computeBBox(self.selected.values())
293 for id, node in self.selected.iteritems():
294 if node.tag == inkex.addNS('path','svg') or node.tag=='path':
295 d = node.get('d')
296 p = cubicsuperpath.parsePath(d)
298 for sub in p:
299 for ctlpt in sub:
300 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))
302 node.set('d',cubicsuperpath.formatPath(p))
304 #e = Diffeo()
305 #e.affect()