Code

Ruby port of simplepath.py
[inkscape.git] / share / extensions / simplepath.rb
1 #!/usr/bin/env ruby
3 # simplepath.rb
4 # functions for digesting paths into a simple list structure
5 #
6 # Ruby port by MenTaLguY
7 #
8 # Copyright (C) 2005 Aaron Spike  <aaron@ekips.org>
9 # Copyright (C) 2006 MenTaLguY  <mental@rydia.net>
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25 require 'strscan'
27 def lexPath(d)
28     # iterator which breaks path data 
29     # identifies command and parameter tokens
31     scanner = StringScanner.new(d)
33     delim = /[ \t\r\n,]+/
34     command = /[MLHVCSQTAZmlhvcsqtaz]/
35     parameter = /(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/
37     until scanner.eos?
38         scanner.skip(delim)
39         if m = scanner.scan(command)
40             yield m, true
41         elsif m = scanner.scan(parameter)
42             yield m, false
43         else
44             raise 'Invalid path data!'
45         end
46     end
47 end
49 PathDef = Struct.new :implicit_next, :param_count, :casts, :coord_types
50 PATHDEFS = {
51     'M' => PathDef['L', 2, [:to_f, :to_f], [:x,:y]], 
52     'L' => PathDef['L', 2, [:to_f, :to_f], [:x,:y]], 
53     'H' => PathDef['H', 1, [:to_f], [:x]], 
54     'V' => PathDef['V', 1, [:to_f], [:y]], 
55     'C' => PathDef['C', 6, [:to_f, :to_f, :to_f, :to_f, :to_f, :to_f], [:x,:y,:x,:y,:x,:y]], 
56     'S' => PathDef['S', 4, [:to_f, :to_f, :to_f, :to_f], [:x,:y,:x,:y]], 
57     'Q' => PathDef['Q', 4, [:to_f, :to_f, :to_f, :to_f], [:x,:y,:x,:y]], 
58     'T' => PathDef['T', 2, [:to_f, :to_f], [:x,:y]], 
59     'A' => PathDef['A', 7, [:to_f, :to_f, :to_f, :to_i, :to_i, :to_f, :to_f], [0,0,0,0,0,:x,:y]], 
60     'Z' => PathDef['L', 0, [], []]
61 }
63 def parsePath(d)
64     # Parse SVG path and return an array of segments.
65     # Removes all shorthand notation.
66     # Converts coordinates to absolute.
68     retval = []
70     command = nil
71     outputCommand = nil
72     params = []
74     pen = [0.0,0.0]
75     subPathStart = pen
76     lastControl = pen
77     lastCommand = nil
79     lexPath(d) do |token, isCommand|
80         unless command
81             if isCommand
82                 if lastCommand or token.upcase == 'M'
83                     command = token
84                 else
85                     raise 'Invalid path, must begin with moveto.'
86                 end
87             else
88                 #command was omited
89                 #use last command's implicit next command
90                 if lastCommand
91                     if lastCommand =~ /[A-Z]/
92                         command = PATHDEFS[lastCommand].implicit_next
93                     else
94                         command = PATHDEFS[lastCommand.upcase].implicit_next.downcase
95                     end
96                 else
97                     raise 'Invalid path, no initial command.'    
98                 end
99             end
100             outputCommand = command.upcase
101         else
102             raise 'Invalid number of parameters' if isCommand
103             param = token.send PATHDEFS[outputCommand].casts[params.length]
104             if command =~ /[a-z]/
105                 case PATHDEFS[outputCommand].coord_types[params.length]
106                 when :x: param += pen[0]
107                 when :y: param += pen[1]
108                 end
109             end
110             params.push param
111         end
113         if params.length == PATHDEFS[outputCommand].param_count
115             #Flesh out shortcut notation
116             case outputCommand
117             when 'H','V'
118                 case outputCommand
119                 when 'H': params.push pen[1]
120                 when 'V': params.unshift pen[0]
121                 end
122                 outputCommand = 'L'
123             when 'S','T'
124                 params.unshift(pen[1]+(pen[1]-lastControl[1]))
125                 params.unshift(pen[0]+(pen[0]-lastControl[0]))
126                 case outputCommand
127                 when 'S': outputCommand = 'C'
128                 when 'T': outputCommand = 'Q'
129                 end
130             end
132             #current values become "last" values
133             case outputCommand
134             when 'M'
135                 subPathStart = params[0,2]
136                 pen = subPathStart
137             when 'Z'
138                 pen = subPathStart
139             else
140                 pen = params[-2,2]
141             end
143             case outputCommand
144             when 'Q','C'
145                 lastControl = params[-4,2]
146             else
147                 lastControl = pen
148             end
150             lastCommand = command
151             retval.push [outputCommand,params]
152             command = nil
153             params = []
154         end
155     end
157     raise 'Unexpected end of path' if command
159     return retval
160 end
162 def formatPath(a)
163     # Format SVG path data from an array
164     a.map { |cmd,params| "#{cmd} #{params.join(' ')}" }.join
165 end
167 def translatePath(p, x, y)
168     p.each do |cmd,params|
169         coord_types = PATHDEFS[cmd].coord_types
170         for i in 0...(params.length)
171             case coord_types[i]
172             when :x: params[i] += x
173             when :y: params[i] += y
174             end
175         end
176     end
177 end
179 def scalePath(p, x, y)
180     p.each do |cmd,params|
181         coord_types = PATHDEFS[cmd].coord_types
182         for i in 0...(params.length)
183             case coord_types[i]
184             when :x: params[i] *= x
185             when :y: params[i] *= y
186             end
187         end
188     end
189 end
191 def rotatePath(p, a, cx = 0, cy = 0)
192     return p if a == 0
193     p.each do |cmd,params|
194         coord_types = PATHDEFS[cmd].coord_types
195         for i in 0...(params.length)
196             if coord_types[i] == :x
197                 x = params[i] - cx
198                 y = params[i + 1] - cy
199                 r = Math.sqrt((x**2) + (y**2))
200                 unless r.zero?
201                     theta = Math.atan2(y, x) + a
202                     params[i] = (r * Math.cos(theta)) + cx
203                     params[i + 1] = (r * Math.sin(theta)) + cy
204                 end
205             end
206         end
207     end
208 end