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
33 import gettext
34 _ = gettext.gettext
36 ####################################################################
37 ##-- zOrder computation...
38 ##-- this should be shipped out in a separate file. inkex.py?
40 def zSort(inNode,idList):
41 sortedList=[]
42 theid = inNode.get("id")
43 if theid in idList:
44 sortedList.append(theid)
45 for child in inNode:
46 if len(sortedList)==len(idList):
47 break
48 sortedList+=zSort(child,idList)
49 return sortedList
52 class PathModifier(inkex.Effect):
53 def __init__(self):
54 inkex.Effect.__init__(self)
56 ##################################
57 #-- Selectionlists manipulation --
58 ##################################
60 def duplicateNodes(self, aList):
61 clones={}
62 for id,node in aList.iteritems():
63 clone=copy.deepcopy(node)
64 #!!!--> should it be given an id?
65 #seems to work without this!?!
66 myid = node.tag.split('}')[-1]
67 clone.set("id", self.uniqueId(myid))
68 node.getparent().append(clone)
69 clones[clone.get("id")]=clone
70 return(clones)
72 def uniqueId(self, prefix):
73 id="%s%04i"%(prefix,random.randint(0,9999))
74 while len(self.document.getroot().xpath('//*[@id="%s"]' % id,namespaces=inkex.NSS)):
75 id="%s%04i"%(prefix,random.randint(0,9999))
76 return(id)
78 def expandGroups(self,aList,transferTransform=True):
79 for id, node in aList.items():
80 if node.tag == inkex.addNS('g','svg') or node.tag=='g':
81 mat=parseTransform(node.get("transform"))
82 for child in node:
83 if transferTransform:
84 applyTransformToNode(mat,child)
85 aList.update(self.expandGroups({child.get('id'):child}))
86 if transferTransform and node.get("transform"):
87 del node.attrib["transform"]
88 del aList[id]
89 return(aList)
91 def expandGroupsUnlinkClones(self,aList,transferTransform=True,doReplace=True):
92 for id in aList.keys()[:]:
93 node=aList[id]
94 if node.tag == inkex.addNS('g','svg') or node.tag=='g':
95 self.expandGroups(aList,transferTransform)
96 self.expandGroupsUnlinkClones(aList,transferTransform,doReplace)
97 #Hum... not very efficient if there are many clones of groups...
99 elif node.tag == inkex.addNS('use','svg') or node.tag=='use':
100 refnode=self.refNode(node)
101 newnode=self.unlinkClone(node,doReplace)
102 del aList[id]
104 style = simplestyle.parseStyle(node.get('style') or "")
105 refstyle=simplestyle.parseStyle(refnode.get('style') or "")
106 style.update(refstyle)
107 newnode.set('style',simplestyle.formatStyle(style))
109 newid=newnode.get('id')
110 aList.update(self.expandGroupsUnlinkClones({newid:newnode},transferTransform,doReplace))
111 return aList
113 def recursNewIds(self,node):
114 if node.get('id'):
115 node.set('id',self.uniqueId(node.tag))
116 for child in node:
117 self.recursNewIds(child)
119 def refNode(self,node):
120 if node.get(inkex.addNS('href','xlink')):
121 refid=node.get(inkex.addNS('href','xlink'))
122 path = '//*[@id="%s"]' % refid[1:]
123 newNode = self.document.getroot().xpath(path, namespaces=inkex.NSS)[0]
124 return newNode
125 else:
126 raise AssertionError, "Trying to follow empty xlink.href attribute."
128 def unlinkClone(self,node,doReplace):
129 if node.tag == inkex.addNS('use','svg') or node.tag=='use':
130 newNode = copy.deepcopy(self.refNode(node))
131 self.recursNewIds(newNode)
132 applyTransformToNode(parseTransform(node.get('transform')),newNode)
134 if doReplace:
135 parent=node.getparent()
136 parent.insert(parent.index(node),newNode)
137 parent.remove(node)
139 return newNode
140 else:
141 raise AssertionError, "Only clones can be unlinked..."
145 ################################
146 #-- Object conversion ----------
147 ################################
149 def rectToPath(self,node,doReplace=True):
150 if node.tag == inkex.addNS('rect','svg'):
151 x =float(node.get('x'))
152 y =float(node.get('y'))
153 #FIXME: no exception anymore and sometimes just one
154 try:
155 rx=float(node.get('rx'))
156 ry=float(node.get('ry'))
157 except:
158 rx=0
159 ry=0
160 w =float(node.get('width' ))
161 h =float(node.get('height'))
162 d ='M %f,%f '%(x+rx,y)
163 d+='L %f,%f '%(x+w-rx,y)
164 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w,y+ry)
165 d+='L %f,%f '%(x+w,y+h-ry)
166 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w-rx,y+h)
167 d+='L %f,%f '%(x+rx,y+h)
168 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x,y+h-ry)
169 d+='L %f,%f '%(x,y+ry)
170 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+rx,y)
172 newnode=inkex.etree.Element('path')
173 newnode.set('d',d)
174 newnode.set('id', self.uniqueId('path'))
175 newnode.set('style',node.get('style'))
176 nnt = node.get('transform')
177 if nnt:
178 newnode.set('transform',nnt)
179 fuseTransform(newnode)
180 if doReplace:
181 parent=node.getparent()
182 parent.insert(parent.index(node),newnode)
183 parent.remove(node)
184 return newnode
186 def groupToPath(self,node,doReplace=True):
187 if node.tag == inkex.addNS('g','svg'):
188 newNode = inkex.etree.SubElement(self.current_layer,inkex.addNS('path','svg'))
190 newstyle = simplestyle.parseStyle(node.get('style') or "")
191 newp = []
192 for child in node:
193 childstyle = simplestyle.parseStyle(child.get('style') or "")
194 childstyle.update(newstyle)
195 newstyle.update(childstyle)
196 childAsPath = self.objectToPath(child,False)
197 newp += cubicsuperpath.parsePath(childAsPath.get('d'))
198 newNode.set('d',cubicsuperpath.formatPath(newp))
199 newNode.set('style',simplestyle.formatStyle(newstyle))
201 self.current_layer.remove(newNode)
202 if doReplace:
203 parent=node.getparent()
204 parent.insert(parent.index(node),newNode)
205 parent.remove(node)
207 return newNode
208 else:
209 raise AssertionError
211 def objectToPath(self,node,doReplace=True):
212 #--TODO: support other object types!!!!
213 #--TODO: make sure cubicsuperpath supports A and Q commands...
214 if node.tag == inkex.addNS('rect','svg'):
215 return(self.rectToPath(node,doReplace))
216 if node.tag == inkex.addNS('g','svg'):
217 return(self.groupToPath(node,doReplace))
218 elif node.tag == inkex.addNS('path','svg') or node.tag == 'path':
219 #remove inkscape attributes, otherwise any modif of 'd' will be discarded!
220 for attName in node.attrib.keys():
221 if ("sodipodi" in attName) or ("inkscape" in attName):
222 del node.attrib[attName]
223 fuseTransform(node)
224 return node
225 elif node.tag == inkex.addNS('use','svg') or node.tag == 'use':
226 newNode = self.unlinkClone(node,doReplace)
227 return self.objectToPath(newNode,doReplace)
228 else:
229 inkex.errormsg(_("Please first convert objects to paths! (Got [%s].)") % node.tag)
230 return None
232 def objectsToPaths(self,aList,doReplace=True):
233 newSelection={}
234 for id,node in aList.items():
235 newnode=self.objectToPath(node,doReplace)
236 del aList[id]
237 aList[newnode.get('id')]=newnode
240 ################################
241 #-- Action ----------
242 ################################
244 #-- overwrite this method in subclasses...
245 def effect(self):
246 #self.duplicateNodes(self.selected)
247 #self.expandGroupsUnlinkClones(self.selected, True)
248 self.objectsToPaths(self.selected, True)
249 self.bbox=computeBBox(self.selected.values())
250 for id, node in self.selected.iteritems():
251 if node.tag == inkex.addNS('path','svg'):
252 d = node.get('d')
253 p = cubicsuperpath.parsePath(d)
255 #do what ever you want with p!
257 node.set('d',cubicsuperpath.formatPath(p))
260 class Diffeo(PathModifier):
261 def __init__(self):
262 inkex.Effect.__init__(self)
264 def applyDiffeo(self,bpt,vects=()):
265 '''
266 bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
267 Defaults to identity!
268 '''
269 for v in vects:
270 v[0]-=bpt[0]
271 v[1]-=bpt[1]
273 #-- your transformations go here:
274 #x,y=bpt
275 #bpt[0]=f(x,y)
276 #bpt[1]=g(x,y)
277 #for v in vects:
278 # vx,vy=v
279 # v[0]=df/dx(x,y)*vx+df/dy(x,y)*vy
280 # v[1]=dg/dx(x,y)*vx+dg/dy(x,y)*vy
281 #
282 #-- !caution! y-axis is pointing downward!
284 for v in vects:
285 v[0]+=bpt[0]
286 v[1]+=bpt[1]
289 def effect(self):
290 #self.duplicateNodes(self.selected)
291 self.expandGroupsUnlinkClones(self.selected, True)
292 self.expandGroups(self.selected, True)
293 self.objectsToPaths(self.selected, True)
294 self.bbox=computeBBox(self.selected.values())
295 for id, node in self.selected.iteritems():
296 if node.tag == inkex.addNS('path','svg') or node.tag=='path':
297 d = node.get('d')
298 p = cubicsuperpath.parsePath(d)
300 for sub in p:
301 for ctlpt in sub:
302 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))
304 node.set('d',cubicsuperpath.formatPath(p))
306 #e = Diffeo()
307 #e.affect()
310 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99