svgcode 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|