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.
@@ -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