svgcode 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,6 @@
1
+ module Svgcode
2
+ module GCode
3
+ class InvalidCommandError < StandardError
4
+ end
5
+ end
6
+ 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,13 @@
1
+ require 'svgcode/svg/command'
2
+
3
+ module Svgcode
4
+ module SVG
5
+ class Path
6
+ attr_reader :commands
7
+
8
+ def initialize(str)
9
+ @commands = str.split(/(?=[a-z])/i).collect { |s| Command.new(s) }
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,3 @@
1
+ module Svgcode
2
+ VERSION = '0.2.0'
3
+ end