svgcode 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec_status +243 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +21 -0
- data/README/linux-cnc-reverb.png +0 -0
- data/README/rub-a-dub-reverb.svg +71 -0
- data/README.md +19 -0
- data/Rakefile +6 -0
- data/TODO.md +21 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/svgcode +43 -0
- data/lib/svgcode/gcode/command.rb +141 -0
- data/lib/svgcode/gcode/converter.rb +97 -0
- data/lib/svgcode/gcode/invalid_command_error.rb +6 -0
- data/lib/svgcode/gcode/program.rb +199 -0
- data/lib/svgcode/svg/command.rb +109 -0
- data/lib/svgcode/svg/path.rb +13 -0
- data/lib/svgcode/svg/point.rb +82 -0
- data/lib/svgcode/svg/transform.rb +52 -0
- data/lib/svgcode/version.rb +3 -0
- data/lib/svgcode.rb +37 -0
- data/pkg/svgcode-0.1.0.gem +0 -0
- data/svgcode.gemspec +31 -0
- metadata +169 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
module Svgcode
|
2
|
+
module GCode
|
3
|
+
class Command
|
4
|
+
INT_FORMAT = "%02d"
|
5
|
+
FLOAT_FORMAT = "%.3f"
|
6
|
+
INT_LETTERS = [ 'M', 'G' ]
|
7
|
+
|
8
|
+
attr_reader :letter, :number, :args
|
9
|
+
|
10
|
+
def initialize(letter_or_str = nil, number = nil, args = [])
|
11
|
+
if letter_or_str.length > 1
|
12
|
+
parts = letter_or_str.split(/\s+/)
|
13
|
+
cmd = Command.parse_single(parts.shift)
|
14
|
+
@letter = cmd.letter
|
15
|
+
@number = cmd.number
|
16
|
+
@args = parts.collect { |arg| Command.parse_single(arg) }
|
17
|
+
else
|
18
|
+
@letter = letter_or_str
|
19
|
+
@number = number
|
20
|
+
@args = args
|
21
|
+
end
|
22
|
+
|
23
|
+
@letter = @letter.to_s.upcase
|
24
|
+
@number = @number.to_f unless @number.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
num_fmt = INT_LETTERS.include?(@letter) ? INT_FORMAT : FLOAT_FORMAT
|
29
|
+
str = "#{@letter}#{num_fmt % @number}"
|
30
|
+
str += " #{@args.join(' ')}" unless @args.nil? || @args.empty?
|
31
|
+
str
|
32
|
+
end
|
33
|
+
|
34
|
+
def ==(other)
|
35
|
+
other.is_a?(self.class) &&
|
36
|
+
other.letter == @letter &&
|
37
|
+
other.number.eql?(@number) &&
|
38
|
+
other.args == @args
|
39
|
+
end
|
40
|
+
|
41
|
+
def roughly_equal?(other)
|
42
|
+
return false unless @letter == other.letter
|
43
|
+
return false unless @args.length == other.args.length
|
44
|
+
return @number == other.number unless @number.nil? || other.number.nil?
|
45
|
+
|
46
|
+
result = true
|
47
|
+
@args.each_with_index do |arg, i|
|
48
|
+
unless arg.roughly_equal?(other.args[i])
|
49
|
+
result = false
|
50
|
+
break
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
result
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.parse_single(str)
|
58
|
+
letter = str[0].to_sym
|
59
|
+
number = str.length > 1 ? str[1..(str.length - 1)] : nil
|
60
|
+
Command.new(letter, number)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.comment(str)
|
64
|
+
"\n(#{str}!!!)"
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.absolute
|
68
|
+
Command.new(:g, 90)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.relative
|
72
|
+
Command.new(:g, 91)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.metric
|
76
|
+
Command.new(:g, 21)
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.imperial
|
80
|
+
Command.new(:g, 20)
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.home
|
84
|
+
Command.new(:g, 30)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.stop
|
88
|
+
Command.new(:m, 2)
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.feedrate(rate)
|
92
|
+
Command.new(:f, rate)
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.go(x, y)
|
96
|
+
Command.new(:g, 0, [
|
97
|
+
Command.new(:x, x),
|
98
|
+
Command.new(:y, y)
|
99
|
+
])
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.cut(x, y)
|
103
|
+
Command.new(:g, 1, [
|
104
|
+
Command.new(:x, x),
|
105
|
+
Command.new(:y, y)
|
106
|
+
])
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.cubic_spline(i, j, _p, q, x, y)
|
110
|
+
Command.new(:g, 5, [
|
111
|
+
Command.new(:i, i),
|
112
|
+
Command.new(:j, j),
|
113
|
+
Command.new(:p, _p),
|
114
|
+
Command.new(:q, q),
|
115
|
+
Command.new(:x, x),
|
116
|
+
Command.new(:y, y)
|
117
|
+
])
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.clear(clearance)
|
121
|
+
Command.new(:g, 0, [
|
122
|
+
Command.new(:z, clearance)
|
123
|
+
])
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.plunge(depth)
|
127
|
+
Command.new(:g, 1, [
|
128
|
+
Command.new(:z, depth)
|
129
|
+
])
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.g(number, args = nil)
|
133
|
+
Command.new(:g, number, args)
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.m(number, args = nil)
|
137
|
+
Command.new(:m, number, args)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'svgcode/gcode/program'
|
2
|
+
require 'svgcode/svg/path'
|
3
|
+
|
4
|
+
module Svgcode
|
5
|
+
module GCode
|
6
|
+
class Converter
|
7
|
+
PX_PER_INCH = 300
|
8
|
+
PX_PER_MM = PX_PER_INCH / 25.4
|
9
|
+
|
10
|
+
attr_accessor :transforms
|
11
|
+
attr_reader :program, :finished, :metric, :max_y
|
12
|
+
|
13
|
+
def initialize(opts)
|
14
|
+
raise ArgumentError.new if opts.nil? || opts[:max_y].nil?
|
15
|
+
@finished = false
|
16
|
+
@transforms = []
|
17
|
+
@max_y = opts.delete(:max_y)
|
18
|
+
@program = Program.new(opts)
|
19
|
+
@metric = opts[:metric] != false
|
20
|
+
@metric ? @program.metric! : @program.imperial!
|
21
|
+
@program.feedrate!
|
22
|
+
end
|
23
|
+
|
24
|
+
def <<(svg_d)
|
25
|
+
svg_start = nil
|
26
|
+
path = SVG::Path.new(svg_d)
|
27
|
+
start = nil
|
28
|
+
|
29
|
+
path.commands.each do |cmd|
|
30
|
+
cmd.apply_transforms!(@transforms)
|
31
|
+
cmd.absolute? ? cmd.flip_points_y!(@max_y) : cmd.negate_points_y!
|
32
|
+
|
33
|
+
if metric?
|
34
|
+
cmd.divide_points_by!(PX_PER_MM)
|
35
|
+
else
|
36
|
+
cmd.divide_points_by!(PX_PER_INCH)
|
37
|
+
end
|
38
|
+
|
39
|
+
if (cmd.name == :close || cmd.absolute?) && @program.relative?
|
40
|
+
@program.absolute!
|
41
|
+
elsif cmd.relative? && @program.absolute?
|
42
|
+
@program.relative!
|
43
|
+
end
|
44
|
+
|
45
|
+
case cmd.name
|
46
|
+
when :move
|
47
|
+
start = cmd.relative? ? cmd.absolute(@program.pos) : cmd
|
48
|
+
@program.go!(cmd.points.first.x, cmd.points.first.y)
|
49
|
+
when :line
|
50
|
+
@program.cut!(cmd.points.first.x, cmd.points.first.y)
|
51
|
+
when :cubic
|
52
|
+
cubic!(cmd)
|
53
|
+
when :close
|
54
|
+
@program.cut!(start.points.first.x, start.points.first.y)
|
55
|
+
start = nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def metric?
|
61
|
+
@metric
|
62
|
+
end
|
63
|
+
|
64
|
+
def comment!(str)
|
65
|
+
@program.comment!(str)
|
66
|
+
end
|
67
|
+
|
68
|
+
def finish
|
69
|
+
unless @finished
|
70
|
+
@program.home!
|
71
|
+
@program.stop!
|
72
|
+
@finished = true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_s
|
77
|
+
@program.to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def cubic!(cmd)
|
83
|
+
# A relative SVG cubic bezier has all control points relative to start.
|
84
|
+
# G-code I&J are relative to start, and P&Q relative to end.
|
85
|
+
# I, J, P & Q are always relative, but SVG values can be absolute.
|
86
|
+
cmd.points[0] -= @program.pos if cmd.absolute?
|
87
|
+
cmd.points[1] -= cmd.points[2]
|
88
|
+
|
89
|
+
@program.cubic_spline!(
|
90
|
+
cmd.points[0].x, cmd.points[0].y,
|
91
|
+
cmd.points[1].x, cmd.points[1].y,
|
92
|
+
cmd.points[2].x, cmd.points[2].y,
|
93
|
+
)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'svgcode/gcode/command'
|
2
|
+
require 'svgcode/gcode/invalid_command_error'
|
3
|
+
|
4
|
+
module Svgcode
|
5
|
+
module GCode
|
6
|
+
class Program
|
7
|
+
DEFAULT_FEEDRATE = 120
|
8
|
+
DEFAULT_CLEARANCE = 5
|
9
|
+
DEFAULT_DEPTH = -0.5
|
10
|
+
|
11
|
+
attr_accessor :opts, :commands
|
12
|
+
|
13
|
+
# 3 states
|
14
|
+
# ========
|
15
|
+
# at Z home height: @is_poised = false, @is_plunged = false
|
16
|
+
# at Z clearance height: @is_poised = true, @is_plunged = false
|
17
|
+
# at Z cutting depth height: @is_poised = true, @is_plunged = true
|
18
|
+
attr_reader :is_plunged, :is_poised, :is_absolute, :x, :y
|
19
|
+
|
20
|
+
def initialize(opts = {})
|
21
|
+
@is_plunged = false
|
22
|
+
@is_poised = false
|
23
|
+
@commands = opts.delete(:commands) || []
|
24
|
+
@is_absolute = opts.delete(:absolute)
|
25
|
+
@is_absolute = true if @is_absolute.nil?
|
26
|
+
|
27
|
+
@opts = {
|
28
|
+
feedrate: DEFAULT_FEEDRATE,
|
29
|
+
clearance: DEFAULT_CLEARANCE,
|
30
|
+
depth: DEFAULT_DEPTH
|
31
|
+
}.merge(opts)
|
32
|
+
end
|
33
|
+
|
34
|
+
def feedrate
|
35
|
+
@opts[:feedrate]
|
36
|
+
end
|
37
|
+
|
38
|
+
def clearance
|
39
|
+
@opts[:clearance]
|
40
|
+
end
|
41
|
+
|
42
|
+
def depth
|
43
|
+
@opts[:depth]
|
44
|
+
end
|
45
|
+
|
46
|
+
def plunged?
|
47
|
+
@is_plunged
|
48
|
+
end
|
49
|
+
|
50
|
+
def poised?
|
51
|
+
@is_poised
|
52
|
+
end
|
53
|
+
|
54
|
+
def absolute?
|
55
|
+
@is_absolute
|
56
|
+
end
|
57
|
+
|
58
|
+
def relative?
|
59
|
+
@is_absolute.nil? ? nil : !@is_absolute
|
60
|
+
end
|
61
|
+
|
62
|
+
def <<(command)
|
63
|
+
if !command.is_a?(String) &&
|
64
|
+
(@x.nil? || @y.nil?) &&
|
65
|
+
command.letter == 'G' &&
|
66
|
+
command.number < 6 &&
|
67
|
+
command != Command.relative &&
|
68
|
+
command != Command.absolute
|
69
|
+
then
|
70
|
+
if relative?
|
71
|
+
raise InvalidCommandError.new(
|
72
|
+
'Cannot add a command when relative and @x or @y are nil'
|
73
|
+
)
|
74
|
+
else
|
75
|
+
@commands << Command.absolute
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
@commands << command
|
80
|
+
end
|
81
|
+
|
82
|
+
def comment!(str)
|
83
|
+
self << Command.comment(str)
|
84
|
+
end
|
85
|
+
|
86
|
+
def metric!
|
87
|
+
self << Command.metric
|
88
|
+
end
|
89
|
+
|
90
|
+
def imperial!
|
91
|
+
self << Command.imperial
|
92
|
+
end
|
93
|
+
|
94
|
+
def absolute!
|
95
|
+
unless absolute?
|
96
|
+
self << Command.absolute
|
97
|
+
@is_absolute = true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def relative!
|
102
|
+
unless relative?
|
103
|
+
self << Command.relative
|
104
|
+
@is_absolute = false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def feedrate!(rate = nil)
|
109
|
+
if rate.nil?
|
110
|
+
rate = feedrate
|
111
|
+
else
|
112
|
+
@opts[:feedrate] = rate
|
113
|
+
end
|
114
|
+
|
115
|
+
self << Command.feedrate(rate)
|
116
|
+
end
|
117
|
+
|
118
|
+
def stop!
|
119
|
+
self << Command.stop
|
120
|
+
end
|
121
|
+
|
122
|
+
def home!
|
123
|
+
clear! if plunged?
|
124
|
+
@commands.pop if @commands.last == Command.relative
|
125
|
+
self << Command.home if poised?
|
126
|
+
@x = nil
|
127
|
+
@y = nil
|
128
|
+
@is_poised = false
|
129
|
+
end
|
130
|
+
|
131
|
+
def clear!
|
132
|
+
temp_absolute { self << Command.clear(clearance) }
|
133
|
+
@is_plunged = false
|
134
|
+
@is_poised = true
|
135
|
+
end
|
136
|
+
|
137
|
+
def plunge!
|
138
|
+
clear! unless poised?
|
139
|
+
temp_absolute { self << Command.plunge(depth) }
|
140
|
+
@is_plunged = true
|
141
|
+
end
|
142
|
+
|
143
|
+
def go!(x, y)
|
144
|
+
clear! if plunged?
|
145
|
+
self << Command.go(x, y)
|
146
|
+
set_coords(x, y)
|
147
|
+
end
|
148
|
+
|
149
|
+
def cut!(x, y)
|
150
|
+
perform_cut(x, y) { self << Command.cut(x, y) }
|
151
|
+
end
|
152
|
+
|
153
|
+
def cubic_spline!(i, j, _p, q, x, y)
|
154
|
+
perform_cut(x, y) do
|
155
|
+
self << Command.cubic_spline(i, j, _p, q, x, y)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def pos
|
160
|
+
Svgcode::SVG::Point.new(@x, @y)
|
161
|
+
end
|
162
|
+
|
163
|
+
def to_s
|
164
|
+
@commands.join("\n")
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def perform_cut(x, y)
|
170
|
+
plunge! unless plunged?
|
171
|
+
yield
|
172
|
+
set_coords(x, y)
|
173
|
+
end
|
174
|
+
|
175
|
+
def temp_absolute
|
176
|
+
was_relative = relative?
|
177
|
+
|
178
|
+
if @commands.last == Command.relative
|
179
|
+
@commands.pop
|
180
|
+
@is_absolute = true
|
181
|
+
end
|
182
|
+
|
183
|
+
absolute! if relative?
|
184
|
+
yield
|
185
|
+
relative! if was_relative
|
186
|
+
end
|
187
|
+
|
188
|
+
def set_coords(x, y)
|
189
|
+
if absolute?
|
190
|
+
@x = x
|
191
|
+
@y = y
|
192
|
+
else
|
193
|
+
@x += x
|
194
|
+
@y += y
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'svgcode/svg/point'
|
2
|
+
|
3
|
+
module Svgcode
|
4
|
+
module SVG
|
5
|
+
class Command
|
6
|
+
attr_reader :name, :absolute, :points
|
7
|
+
|
8
|
+
NAMES = {
|
9
|
+
'm' => :move,
|
10
|
+
'l' => :line,
|
11
|
+
'c' => :cubic,
|
12
|
+
'z' => :close
|
13
|
+
}
|
14
|
+
|
15
|
+
def initialize(str_or_opts)
|
16
|
+
if str_or_opts.is_a?(Hash)
|
17
|
+
@name = str_or_opts[:name]
|
18
|
+
@absolute = !!str_or_opts[:absolute]
|
19
|
+
@points = str_or_opts[:points]
|
20
|
+
else
|
21
|
+
str = str_or_opts
|
22
|
+
@name = NAMES[str[0].to_s.downcase]
|
23
|
+
|
24
|
+
@absolute =
|
25
|
+
if @name == :close
|
26
|
+
true
|
27
|
+
else
|
28
|
+
!!str[0].match(/[A-Z]/)
|
29
|
+
end
|
30
|
+
|
31
|
+
if @name != :close && str.length > 1
|
32
|
+
@points = Point.parse(str[1..(str.length - 1)])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
@points = [] if @points.nil?
|
37
|
+
end
|
38
|
+
|
39
|
+
def absolute?
|
40
|
+
@absolute
|
41
|
+
end
|
42
|
+
|
43
|
+
def relative?
|
44
|
+
!@absolute
|
45
|
+
end
|
46
|
+
|
47
|
+
def absolute(pos)
|
48
|
+
points = @points.collect { |p| p + pos }
|
49
|
+
Command.new(name: @name, absolute: true, points: points)
|
50
|
+
end
|
51
|
+
|
52
|
+
def absolute!(pos)
|
53
|
+
if relative?
|
54
|
+
@points.collect! { |p| p + pos }
|
55
|
+
@absolute = true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def negate_points_y
|
60
|
+
points = @points.collect { |point| point.negate_y }
|
61
|
+
Command.new(name: @name, absolute: @absolute, points: points)
|
62
|
+
end
|
63
|
+
|
64
|
+
def negate_points_y!
|
65
|
+
@points.each { |point| point.negate_y! }
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def divide_points_by!(amount)
|
70
|
+
@points.each { |point| point.divide_by!(amount) }
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def flip_points_y!(max_y)
|
75
|
+
@points.each { |point| point.flip_y!(max_y) }
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def apply_transforms!(transforms)
|
80
|
+
unless transforms.empty?
|
81
|
+
transforms.reverse.each do |transform|
|
82
|
+
@points.collect! { |point| transform.apply(point) }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def name_str
|
88
|
+
Command.name_str(@name, absolute?)
|
89
|
+
end
|
90
|
+
|
91
|
+
def ==(other)
|
92
|
+
other.is_a?(self.class) &&
|
93
|
+
other.name == @name &&
|
94
|
+
other.absolute? == @absolute &&
|
95
|
+
other.points == @points
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_s
|
99
|
+
name_str + @points.collect { |p| p.to_s }.join(' ')
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.name_str(sym, absolute)
|
103
|
+
str = NAMES.key(sym).dup
|
104
|
+
str.upcase! if absolute
|
105
|
+
str
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Svgcode
|
2
|
+
module SVG
|
3
|
+
class Point
|
4
|
+
attr_reader :x, :y
|
5
|
+
|
6
|
+
VALUE_SEP = /\s?,\s?/
|
7
|
+
OBJECT_SEP = /\s+/
|
8
|
+
|
9
|
+
def initialize(str_or_x, y = nil)
|
10
|
+
if y.nil?
|
11
|
+
parts = str_or_x.split(VALUE_SEP)
|
12
|
+
@x = parts.first.to_f
|
13
|
+
@y = parts.last.to_f
|
14
|
+
else
|
15
|
+
@x = str_or_x.to_f
|
16
|
+
@y = y.to_f
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def negate_y
|
21
|
+
Point.new(@x, -@y)
|
22
|
+
end
|
23
|
+
|
24
|
+
def negate_y!
|
25
|
+
@y = -@y
|
26
|
+
end
|
27
|
+
|
28
|
+
def +(point_or_num)
|
29
|
+
if point_or_num.is_a?(Point)
|
30
|
+
Point.new(@x + point_or_num.x, @y + point_or_num.y)
|
31
|
+
else
|
32
|
+
Point.new(@x + point_or_num, @y + point_or_num)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def -(point_or_num)
|
37
|
+
if point_or_num.is_a?(Point)
|
38
|
+
Point.new(@x - point_or_num.x, @y - point_or_num.y)
|
39
|
+
else
|
40
|
+
Point.new(@x - point_or_num, @y - point_or_num)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def *(point_or_num)
|
45
|
+
if point_or_num.is_a?(Point)
|
46
|
+
Point.new(@x / point_or_num.x, @y / point_or_num.y)
|
47
|
+
else
|
48
|
+
Point.new(@x / point_or_num, @y / point_or_num)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def /(point_or_num)
|
53
|
+
if point_or_num.is_a?(Point)
|
54
|
+
Point.new(@x / point_or_num.x, @y / point_or_num.y)
|
55
|
+
else
|
56
|
+
Point.new(@x / point_or_num, @y / point_or_num)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def divide_by!(amount)
|
61
|
+
@x /= amount
|
62
|
+
@y /= amount
|
63
|
+
end
|
64
|
+
|
65
|
+
def flip_y!(max_y)
|
66
|
+
@y = max_y - @y
|
67
|
+
end
|
68
|
+
|
69
|
+
def ==(other)
|
70
|
+
other.is_a?(self.class) && other.x.eql?(@x) && other.y.eql?(@y)
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_s
|
74
|
+
"#{@x},#{@y}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.parse(str)
|
78
|
+
str.split(OBJECT_SEP).collect { |p| Point.new(p.strip) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'svgcode/svg/point'
|
2
|
+
require 'matrix'
|
3
|
+
|
4
|
+
module Svgcode
|
5
|
+
module SVG
|
6
|
+
class Transform
|
7
|
+
attr_reader :a, :b, :c, :d, :e, :f
|
8
|
+
|
9
|
+
def initialize(str_or_a, b = nil, c = nil, d = nil, e = nil, f = nil)
|
10
|
+
if str_or_a.is_a?(String)
|
11
|
+
raise ArgumentException.new unless str_or_a.start_with?('matrix')
|
12
|
+
nums = str_or_a.gsub(/.+\(/, '').gsub(/\)/, '').split(/\s*,\s*/)
|
13
|
+
|
14
|
+
nums.each_with_index do |n, i|
|
15
|
+
case i
|
16
|
+
when 0
|
17
|
+
@a = n.to_f
|
18
|
+
when 1
|
19
|
+
@b = n.to_f
|
20
|
+
when 2
|
21
|
+
@c = n.to_f
|
22
|
+
when 3
|
23
|
+
@d = n.to_f
|
24
|
+
when 4
|
25
|
+
@e = n.to_f
|
26
|
+
when 5
|
27
|
+
@f = n.to_f
|
28
|
+
end
|
29
|
+
end
|
30
|
+
else
|
31
|
+
@a = str_or_a.to_f
|
32
|
+
@b = b.to_f
|
33
|
+
@c = c.to_f
|
34
|
+
@d = d.to_f
|
35
|
+
@e = e.to_f
|
36
|
+
@f = f.to_f
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_matrix
|
41
|
+
Matrix[[@a, @c, @e], [@b, @d, @f], [0, 0, 1]]
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply(point)
|
45
|
+
point_m = Matrix[[point.x], [point.y], [1]]
|
46
|
+
transform_m = to_matrix
|
47
|
+
result = transform_m * point_m
|
48
|
+
Point.new(result[0, 0], result[1, 0])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|