gerber 0 → 1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -1
- data/README.markdown +42 -0
- data/Rakefile +7 -0
- data/gerber.gemspec +4 -2
- data/lib/gerber.rb +43 -2
- data/lib/gerber/aperture.rb +34 -0
- data/lib/gerber/layer.rb +35 -0
- data/lib/gerber/layer/parser.rb +207 -0
- data/lib/gerber/parser.rb +275 -0
- data/test/gerber.rb +38 -0
- data/test/gerber/example2.gerber +89 -0
- data/test/gerber/hexapod.gerber +1590 -0
- data/test/gerber/layer.rb +6 -0
- data/test/gerber/layer/parser.rb +481 -0
- data/test/gerber/m02_not_at_end.gerber +24 -0
- data/test/gerber/parser.rb +202 -0
- data/test/gerber/sample_4pcb.gerber +23 -0
- data/test/gerber/two_boxes.gerber +19 -0
- data/test/gerber/wikipedia.gerber +23 -0
- metadata +45 -5
data/Gemfile
CHANGED
data/README.markdown
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Gerber
|
2
|
+
|
3
|
+
Everything you need to read and write Gerber RS-274-D and [Extended Gerber RS-274X](http://en.wikipedia.org/wiki/Gerber_Format) files
|
4
|
+
|
5
|
+
Files created by this gem conform to the latest [Gerber format specification](http://www.ucamco.com/Portals/0/Public/The_Gerber_File_%20Format_Specification.pdf)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'gerber'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install gerber
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require 'gerber'
|
25
|
+
|
26
|
+
gerber = Gerber.read('somefile.gerber')
|
27
|
+
gerber.write('outfile.gerber')
|
28
|
+
```
|
29
|
+
|
30
|
+
License
|
31
|
+
-------
|
32
|
+
|
33
|
+
Copyright 2012-2013 Brandon Fosdick <bfoz@bfoz.net> and released under the BSD license.
|
34
|
+
|
35
|
+
|
36
|
+
## Contributing
|
37
|
+
|
38
|
+
1. Fork it
|
39
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
40
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
41
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
42
|
+
5. Create new Pull Request
|
data/Rakefile
CHANGED
data/gerber.gemspec
CHANGED
@@ -2,16 +2,18 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |gem|
|
4
4
|
gem.name = "gerber"
|
5
|
-
gem.version =
|
5
|
+
gem.version = 1
|
6
6
|
|
7
7
|
gem.authors = ["Brandon Fosdick"]
|
8
8
|
gem.email = ["bfoz@bfoz.net"]
|
9
9
|
gem.description = %q{Tools for working with Gerber and Extended Gerber files}
|
10
|
-
gem.summary = %q{Everything you need to read and write Gerber RS-274-D and Extended Gerber RS-
|
10
|
+
gem.summary = %q{Everything you need to read and write Gerber RS-274-D and Extended Gerber RS-274X files}
|
11
11
|
gem.homepage = "http://github.com/bfoz/gerber"
|
12
12
|
|
13
13
|
gem.files = `git ls-files`.split($\)
|
14
14
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
15
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
16
|
gem.require_paths = ["lib"]
|
17
|
+
|
18
|
+
gem.add_dependency 'geometry', '>= 6'
|
17
19
|
end
|
data/lib/gerber.rb
CHANGED
@@ -1,3 +1,44 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'geometry'
|
2
|
+
require 'units'
|
3
|
+
require_relative 'gerber/layer/parser'
|
4
|
+
|
5
|
+
=begin
|
6
|
+
Read and write {http://en.wikipedia.org/wiki/Gerber_Format Gerber} files (RS-274X)
|
7
|
+
=end
|
8
|
+
class Gerber
|
9
|
+
ParseError = Class.new(StandardError)
|
10
|
+
|
11
|
+
Arc = Geometry::Arc
|
12
|
+
Line = Geometry::Line
|
13
|
+
Point = Geometry::Point
|
14
|
+
|
15
|
+
attr_accessor :integer_places, :decimal_places
|
16
|
+
attr_accessor :zero_omission
|
17
|
+
|
18
|
+
attr_reader :apertures, :layers
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@apertures = []
|
22
|
+
@layers = []
|
23
|
+
@polarity = :positive
|
24
|
+
@units = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# Set the format used for coordinates
|
28
|
+
# @param [Number] integer_places The number of digits to the left of the decimal point
|
29
|
+
# @param [Number] decimal_places The number of digits to the right of the decimal point
|
30
|
+
def coordinate_format=(*args)
|
31
|
+
self.integer_places, self.decimal_places = args.flatten.map {|a| a.to_i }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Read and parse the given file into a {Gerber} object
|
35
|
+
# @return [Gerber] The resulting {Gerber} object, or nil on failure
|
36
|
+
def self.read(filename)
|
37
|
+
File.open(filename) do |f|
|
38
|
+
Gerber::Parser.new.parse(f)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.write(filename, container)
|
43
|
+
end
|
3
44
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'geometry'
|
2
|
+
|
3
|
+
=begin
|
4
|
+
An Aperture definition for an Extended {Gerber} file
|
5
|
+
=end
|
6
|
+
class Gerber
|
7
|
+
class Aperture
|
8
|
+
attr_reader :name, :shape
|
9
|
+
attr_accessor :hole, :parameters, :rotation
|
10
|
+
|
11
|
+
def initialize(parameters)
|
12
|
+
raise ArgumentError unless parameters.is_a? Hash
|
13
|
+
|
14
|
+
if parameters.has_key? :circle
|
15
|
+
@shape = Geometry::Circle.new [0,0], :diameter => parameters[:circle]
|
16
|
+
parameters.delete :circle
|
17
|
+
elsif parameters.has_key? :obround
|
18
|
+
@shape = Geometry::Obround.new [0,0], parameters[:obround]
|
19
|
+
parameters.delete :obround
|
20
|
+
elsif parameters.has_key? :polygon
|
21
|
+
@shape = Geometry::RegularPolygon.new parameters[:sides], [0,0], :diameter => parameters[:polygon]
|
22
|
+
parameters.delete :polygon
|
23
|
+
elsif parameters.has_key? :rectangle
|
24
|
+
@shape = Geometry::Rectangle.new [0,0], parameters[:rectangle]
|
25
|
+
parameters.delete :rectangle
|
26
|
+
end
|
27
|
+
parameters.each {|k| self.instance_variable_set("@#{k.first}", k.last) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
(self.hole == other.hole) && (self.parameters == other.parameters) && (self.rotation == other.rotation) && (self.shape == other.shape)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/gerber/layer.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'gerber'
|
2
|
+
require 'units'
|
3
|
+
|
4
|
+
=begin
|
5
|
+
A Gerber information layer (not to be confused with a PCB layer)
|
6
|
+
=end
|
7
|
+
class Gerber
|
8
|
+
class Layer
|
9
|
+
attr_accessor :geometry, :name, :polarity, :step, :repeat
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
super
|
13
|
+
|
14
|
+
self.geometry = []
|
15
|
+
@polarity = :dark
|
16
|
+
@repeat = Vector[1,1]
|
17
|
+
@step = Vector[0,0]
|
18
|
+
@units = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
# @group Accessors
|
22
|
+
def empty?
|
23
|
+
self.geometry.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_inches
|
27
|
+
@units = 'inch'
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_millimeters
|
31
|
+
@units = 'millimeters'
|
32
|
+
end
|
33
|
+
# @endgroup
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'gerber/layer'
|
2
|
+
|
3
|
+
class Gerber
|
4
|
+
class Layer
|
5
|
+
class Parser
|
6
|
+
DCodeError = Class.new(ParseError)
|
7
|
+
|
8
|
+
attr_reader :current_aperture
|
9
|
+
attr_reader :coordinate_mode
|
10
|
+
attr_reader :layer
|
11
|
+
attr_reader :position
|
12
|
+
attr_reader :quadrant_mode
|
13
|
+
|
14
|
+
def initialize(*args)
|
15
|
+
super
|
16
|
+
|
17
|
+
@coordinate_mode = :absolute
|
18
|
+
@dcode = 2 # off
|
19
|
+
@gcode = 1 # linear interpolation
|
20
|
+
@layer = Gerber::Layer.new
|
21
|
+
@position = Point[0,0]
|
22
|
+
@quadrant_mode = :single
|
23
|
+
@repeat = Vector[1,1]
|
24
|
+
@step = Vector[0,0]
|
25
|
+
@units = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def is_valid_geometry(arg)
|
29
|
+
arg.kind_of?(Line) || arg.kind_of?(Point) || arg.kind_of?(Arc)
|
30
|
+
end
|
31
|
+
|
32
|
+
def <<(arg)
|
33
|
+
raise ParseError, "Must set an aperture before generating geometry" unless self.current_aperture
|
34
|
+
self.layer.geometry[current_aperture] << arg if is_valid_geometry(arg)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @group Accessors
|
38
|
+
def current_aperture=(arg)
|
39
|
+
@current_aperture = arg
|
40
|
+
self.layer.geometry[arg] = [] unless self.layer.geometry[arg].is_a?(Array)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [String] The name of the current {Layer}
|
44
|
+
def name
|
45
|
+
self.layer.name
|
46
|
+
end
|
47
|
+
|
48
|
+
# Set the name of the current {Layer}
|
49
|
+
# @param [String] name An ASCII string to set the name to
|
50
|
+
def name=(name)
|
51
|
+
self.layer.name = name
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Symbol] The polarity setting of the current {Layer} (:dark or :clear)
|
55
|
+
def polarity
|
56
|
+
self.layer.polarity
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set the polarity of the current {Layer}
|
60
|
+
# @param [Symbol] polarity Set the current polarity to either :clear or :dark
|
61
|
+
def polarity=(polarity)
|
62
|
+
self.layer.polarity = polarity
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_inches
|
66
|
+
@units = 'inch'
|
67
|
+
end
|
68
|
+
|
69
|
+
def set_millimeters
|
70
|
+
@units = 'millimeters'
|
71
|
+
end
|
72
|
+
# @endgroup
|
73
|
+
|
74
|
+
def parse_dcode(s)
|
75
|
+
/D(\d{2,3})/ =~ s
|
76
|
+
dcode = $1.to_i
|
77
|
+
case dcode
|
78
|
+
when 1, 2, 3
|
79
|
+
@dcode = dcode
|
80
|
+
when 10...999
|
81
|
+
self.current_aperture = dcode
|
82
|
+
else
|
83
|
+
raise ParseError, "Invalid D Code #{dcode}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_gcode(gcode, x=nil, y=nil, i=nil, j=nil, dcode=nil)
|
88
|
+
gcode = gcode ? gcode.to_i : @gcode
|
89
|
+
dcode = dcode ? dcode.to_i : @dcode
|
90
|
+
case gcode
|
91
|
+
when 1, 55 # G55 is deprecated, but behaves like G01
|
92
|
+
parse_g1(x, y, dcode)
|
93
|
+
@dcode = dcode
|
94
|
+
@gcode = gcode
|
95
|
+
when 2
|
96
|
+
parse_g2(x, y, i, j, dcode)
|
97
|
+
@dcode = dcode
|
98
|
+
@gcode = gcode
|
99
|
+
when 3
|
100
|
+
parse_g3(x, y, i, j, dcode)
|
101
|
+
@dcode = dcode
|
102
|
+
@gcode = gcode
|
103
|
+
when 4 # G04 is used for single-line comments. Ignore the block and carry on.
|
104
|
+
when 36
|
105
|
+
p "enable outline fill"
|
106
|
+
when 37
|
107
|
+
p "disable outline fill"
|
108
|
+
when 54
|
109
|
+
raise DCodeError, "G54 requires a D code (found #{x}, #{y}, #{dcode})" unless dcode
|
110
|
+
self.current_aperture = dcode.to_i
|
111
|
+
when 70
|
112
|
+
set_inches
|
113
|
+
when 71
|
114
|
+
set_millimeters
|
115
|
+
when 74
|
116
|
+
@quadrant_mode = :single
|
117
|
+
when 75
|
118
|
+
@quadrant_mode = :multi
|
119
|
+
when 90
|
120
|
+
@coordinate_mode = :absolute
|
121
|
+
when 91
|
122
|
+
@coordinate_mode = :incremental
|
123
|
+
else
|
124
|
+
raise ParseError, "Unrecognized GCode #{gcode}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def parse_g1(x, y, dcode)
|
129
|
+
point = Point[apply_units(x) || @position.x, apply_units(y) || @position.y]
|
130
|
+
case dcode
|
131
|
+
when 1
|
132
|
+
line = Geometry::Line[@position, point]
|
133
|
+
self << line
|
134
|
+
@position = point
|
135
|
+
when 2
|
136
|
+
@position = point
|
137
|
+
when 3
|
138
|
+
self << point
|
139
|
+
@position = point
|
140
|
+
else
|
141
|
+
raise DCodeError, "Invalid D parameter (#{dcode}) in G1"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def parse_g2(x, y, i, j, dcode)
|
146
|
+
raise DCodeError, "In G2 dcode must be either 1 or 2" unless [1, 2].include? dcode
|
147
|
+
if 1 == dcode
|
148
|
+
x, y, i, j = [x, y, i, j].map {|a| apply_units(a)}
|
149
|
+
startPoint = if self.quadrant_mode == :single
|
150
|
+
# start and end are swapped in clockwise mode (Geometry::Arc defaults to counterclockwise)
|
151
|
+
# i and j should have the same signs as the x and y components of the vector from the startpoint to the endpoint
|
152
|
+
if self.coordinate_mode == :absolute
|
153
|
+
delta = Point[x, y] - @position
|
154
|
+
i = i * (delta.x<=>0)
|
155
|
+
j = j * (delta.y<=>0)
|
156
|
+
Point[x, y]
|
157
|
+
elsif self.coordinate_mode == :incremental
|
158
|
+
i = i * (x<=>0)
|
159
|
+
j = j * (y<=>0)
|
160
|
+
@position + Point[x, y]
|
161
|
+
end
|
162
|
+
elsif @quadrant_mode == :multi
|
163
|
+
Point[x, y]
|
164
|
+
else
|
165
|
+
raise ParseError, "Unrecognized quadrant mode: #{self.quadrant_mode}"
|
166
|
+
end
|
167
|
+
arc = Geometry::Arc.new(@position + Point[i, j], startPoint, @position)
|
168
|
+
self << arc
|
169
|
+
@position = arc.first
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def parse_g3(x, y, i, j, dcode)
|
174
|
+
raise DCodeError, "In G3 dcode must be either 1 or 2" unless [1, 2].include? dcode
|
175
|
+
if 1 == dcode
|
176
|
+
x, y, i, j = [x, y, i, j].map {|a| apply_units(a)}
|
177
|
+
endPoint = if self.quadrant_mode == :single
|
178
|
+
# i and j should have the same signs as the x and y components of the vector from the startpoint to the endpoint
|
179
|
+
if self.coordinate_mode == :absolute
|
180
|
+
delta = Point[x, y] - @position
|
181
|
+
i = i * (delta.x<=>0)
|
182
|
+
j = j * (delta.y<=>0)
|
183
|
+
Point[x, y]
|
184
|
+
elsif self.coordinate_mode == :incremental
|
185
|
+
i = i * (x<=>0)
|
186
|
+
j = j * (y<=>0)
|
187
|
+
@position + Point[x, y]
|
188
|
+
end
|
189
|
+
elsif @quadrant_mode == :multi
|
190
|
+
Point[x, y]
|
191
|
+
else
|
192
|
+
raise ParseError, "Unrecognized quadrant mode: #{self.quadrant_mode}"
|
193
|
+
end
|
194
|
+
arc = Geometry::Arc.new(@position + Point[i, j], @position, endPoint)
|
195
|
+
self << arc
|
196
|
+
@position = arc.last
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def apply_units(a)
|
201
|
+
raise ParseError, "Units must be set before specifying coordinates" unless @units
|
202
|
+
return nil unless a
|
203
|
+
(@units == 'inch') ? a.inch : a.mm
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'geometry'
|
2
|
+
require 'gerber'
|
3
|
+
require 'units'
|
4
|
+
require_relative 'aperture'
|
5
|
+
require_relative 'layer'
|
6
|
+
require_relative 'layer/parser'
|
7
|
+
|
8
|
+
class Gerber
|
9
|
+
=begin
|
10
|
+
Read and parse {http://en.wikipedia.org/wiki/Gerber_Format Gerber} files (RS-274X)
|
11
|
+
=end
|
12
|
+
class Parser
|
13
|
+
attr_accessor :integer_places, :decimal_places
|
14
|
+
attr_accessor :zero_omission, :absolute
|
15
|
+
|
16
|
+
attr_reader :apertures, :layers
|
17
|
+
attr_reader :eof
|
18
|
+
attr_reader :total_places
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@apertures = []
|
22
|
+
@eof = false
|
23
|
+
@layers = []
|
24
|
+
@layer_parsers = []
|
25
|
+
@axis_mirror = {:a => 1, :b => 1} # 1 => not mirrored, -1 => mirrored
|
26
|
+
@axis_select = {:a => :x, :b => :y}
|
27
|
+
@offset = Point[0,0]
|
28
|
+
@polarity = :positive
|
29
|
+
@rotation = 0.degrees
|
30
|
+
@scale = Vector[0,0]
|
31
|
+
@symbol_mirror = {:a => 1, :b => 1} # 1 => not mirrored, -1 => mirrored
|
32
|
+
@units = nil
|
33
|
+
|
34
|
+
@new_layer_polarity = :dark
|
35
|
+
end
|
36
|
+
|
37
|
+
# Apply the configured units to a number
|
38
|
+
def apply_units(a)
|
39
|
+
raise ParseError, "Units must be set before specifying dimensions" unless @units
|
40
|
+
return nil unless a
|
41
|
+
(@units == 'inch') ? a.inch : a.mm
|
42
|
+
end
|
43
|
+
private :apply_units
|
44
|
+
|
45
|
+
# Set the format used for coordinates
|
46
|
+
# @param [Number] integer_places The number of digits to the left of the decimal point
|
47
|
+
# @param [Number] decimal_places The number of digits to the right of the decimal point
|
48
|
+
def coordinate_format=(*args)
|
49
|
+
self.integer_places, self.decimal_places = args.flatten.map {|a| a.to_i }
|
50
|
+
@total_places = self.decimal_places + self.integer_places
|
51
|
+
end
|
52
|
+
|
53
|
+
# The {Layer} currently being parsed
|
54
|
+
def current_layer
|
55
|
+
@layer_parsers.last || new_layer
|
56
|
+
end
|
57
|
+
private :current_layer
|
58
|
+
|
59
|
+
# Create and return a new {Layer::Parser}
|
60
|
+
def new_layer
|
61
|
+
(@layer_parsers << Layer::Parser.new).last.polarity = @new_layer_polarity
|
62
|
+
('inch' == @units) ? @layer_parsers.last.set_inches : @layer_parsers.last.set_millimeters
|
63
|
+
@layer_parsers.last
|
64
|
+
end
|
65
|
+
|
66
|
+
# Assume that all dimensions are in inches
|
67
|
+
def set_inches
|
68
|
+
@units = 'inch'
|
69
|
+
current_layer.set_inches
|
70
|
+
end
|
71
|
+
|
72
|
+
# Assume that all dimensions are in millimeters
|
73
|
+
def set_millimeters
|
74
|
+
@units = 'millimeters'
|
75
|
+
current_layer.set_millimeters
|
76
|
+
end
|
77
|
+
|
78
|
+
# Parse the given IO stream
|
79
|
+
# @param [IO] input An IO-like object to parse
|
80
|
+
def parse(input)
|
81
|
+
input.each('*') do |block|
|
82
|
+
block.strip!
|
83
|
+
next if !block || block.empty?
|
84
|
+
raise ParseError, "Found blocks after M02" if self.eof
|
85
|
+
case block
|
86
|
+
when /^%AM/ # Special handling for aperture macros
|
87
|
+
parse_parameter((block + input.gets('%')).gsub(/[\n%]/,''))
|
88
|
+
when /^%[A-Z]{2}/
|
89
|
+
(block + input.gets('%')).gsub(/[\n%]/,'').gsub(/\* /,'').lines('*') {|b| parse_parameter(b)}
|
90
|
+
when /^D(\d{2,3})/
|
91
|
+
current_layer.parse_dcode(block)
|
92
|
+
when /^M0(0|1|2)/
|
93
|
+
mcode = $1
|
94
|
+
raise ParseError, "Invalid M code: #{m}" unless mcode
|
95
|
+
@eof = true if mcode.to_i == 2
|
96
|
+
when /^G54D(\d{2,3})/ # Deprecated G54 function code
|
97
|
+
current_layer.parse_gcode(54, nil, nil, nil, nil, $1)
|
98
|
+
when /^G70/
|
99
|
+
set_inches
|
100
|
+
when /^G71/
|
101
|
+
set_millimeters
|
102
|
+
when /^(G(\d{2}))?(X([\d\+-]+))?(Y([\d\+-]+))?(I([\d\+-]+))?(J([\d\+-]+))?(D0(1|2|3))?/
|
103
|
+
gcode, dcode = $2, $12
|
104
|
+
x, y, i, j = [$4, $6, $8, $10].map {|a| parse_coordinate(a) }
|
105
|
+
current_layer.parse_gcode(gcode, x, y, i, j, dcode)
|
106
|
+
else
|
107
|
+
raise ParseError,"Unrecognized block: \"#{block}\""
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# FIXME apply any @rotation
|
112
|
+
|
113
|
+
@layers = @layer_parsers.map {|parser| parser.layer }.select {|layer| !layer.empty? }
|
114
|
+
|
115
|
+
gerber = Gerber.new
|
116
|
+
gerber.apertures.replace @apertures
|
117
|
+
gerber.coordinate_format = self.integer_places, self.decimal_places
|
118
|
+
gerber.layers.replace @layers
|
119
|
+
gerber.zero_omission = self.zero_omission
|
120
|
+
gerber
|
121
|
+
end
|
122
|
+
|
123
|
+
# Convert a string into a {Float} using the current coordinate formating setting
|
124
|
+
# @param [String] s The string to convert
|
125
|
+
# @return [Float] The resulting {Float}, or nil
|
126
|
+
def parse_coordinate(s)
|
127
|
+
return nil unless s # Ignore nil coordinates so that they can be handled later
|
128
|
+
|
129
|
+
sign = s.start_with?('-') ? '-' : '+'
|
130
|
+
s.sub!(sign,'')
|
131
|
+
|
132
|
+
if s.length < total_places
|
133
|
+
if( zero_omission == :leading )
|
134
|
+
s = s.rjust(total_places, '0')
|
135
|
+
elsif( zero_omission == :trailing )
|
136
|
+
s = s.ljust(total_places, '0')
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
current_layer.apply_units((sign + s).insert(sign.length + integer_places, '.').to_f)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Convert a string into a {Float} and apply the appropriate {Units}
|
144
|
+
# @param [String] s The string to convert
|
145
|
+
# @return [Float] The resulting {Float} with units, or nil
|
146
|
+
def parse_float(s)
|
147
|
+
apply_units(s.to_f)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Parse a set of parameter blocks
|
151
|
+
def parse_parameter(s)
|
152
|
+
directive = s[0,2]
|
153
|
+
case directive
|
154
|
+
when 'AD' # Section 4.1
|
155
|
+
dcode, type = s.match(/ADD(\d{2,3})(\w+)/).captures
|
156
|
+
dcode = dcode.to_i
|
157
|
+
raise ParseError unless (dcode >= 10) and (dcode <= 999)
|
158
|
+
case type
|
159
|
+
when 'C'
|
160
|
+
m = s.match(/C,(?<diameter>[\d.]+)(X(?<x>[\d.]+)(X(?<y>[\d.]+))?)?/)
|
161
|
+
aperture = Aperture.new(:circle => parse_float(m[:diameter]))
|
162
|
+
if( m[:x] )
|
163
|
+
x = parse_float(m[:x])
|
164
|
+
aperture.hole = m[:y] ? {:x => x, :y => parse_float(m[:y])} : x
|
165
|
+
end
|
166
|
+
|
167
|
+
when 'R'
|
168
|
+
m = s.match(/R,(?<x>[\d.]+)X(?<y>[\d.]+)(X(?<hole_x>[\d.]+)(X(?<hole_y>[\d.]+))?)?/)
|
169
|
+
aperture = Aperture.new(:rectangle => [parse_float(m[:x]), parse_float(m[:y])])
|
170
|
+
if( m[:hole_x] )
|
171
|
+
hole_x = parse_float(m[:hole_x])
|
172
|
+
aperture.hole = m[:hole_y] ? {:x => hole_x, :y => parse_float(m[:hole_y])} : hole_x
|
173
|
+
end
|
174
|
+
|
175
|
+
when 'O'
|
176
|
+
m = s.match(/O,(?<x>[\d.]+)X(?<y>[\d.]+)(X(?<hole_x>[\d.]+)(X(?<hole_y>[\d.]+))?)?/)
|
177
|
+
aperture = Aperture.new(:obround => [parse_float(m[:x]), parse_float(m[:y])])
|
178
|
+
if( m[:hole_x] )
|
179
|
+
hole_x = parse_float(m[:hole_x])
|
180
|
+
aperture.hole = m[:hole_y] ? {:x => hole_x, :y => parse_float(m[:hole_y])} : hole_x
|
181
|
+
end
|
182
|
+
|
183
|
+
when 'P'
|
184
|
+
m = s.match(/P,(?<diameter>[\d.]+)X(?<sides>[\d.]+)(X(?<rotation>[\d.]+)(X(?<hole_x>[\d.]+)(X(?<hole_y>[\d.]+))?)?)?/)
|
185
|
+
aperture = Aperture.new(:polygon => parse_float(m[:diameter]), :sides => m[:sides].to_i)
|
186
|
+
if( m[:rotation] )
|
187
|
+
aperture.rotation = m[:rotation].to_i.degrees
|
188
|
+
if( m[:hole_x] )
|
189
|
+
hole_x = parse_float(m[:hole_x])
|
190
|
+
aperture.hole = m[:hole_y] ? {:x => hole_x, :y => parse_float(m[:hole_y])} : hole_x
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
else # Special Aperture
|
195
|
+
captures = s.match(/#{type}(,([\d.]+)(X([\d.]+))*)?/).captures
|
196
|
+
parameters = captures.values_at(* captures.each_index.select {|i| i.odd?}).select {|p| p }
|
197
|
+
aperture = Aperture.new(:name=>type)
|
198
|
+
aperture.parameters = parameters.map {|p| parse_float(p) } if( parameters && (0 != parameters.size ) )
|
199
|
+
end
|
200
|
+
self.apertures[dcode] = aperture
|
201
|
+
|
202
|
+
# Section 4.2
|
203
|
+
when 'AM'
|
204
|
+
# macro_name = block.match(/AM(\w*)\*/)[0]
|
205
|
+
primitives = s.split '*'
|
206
|
+
macro_name = primitives.shift.sub(/AM/,'')
|
207
|
+
p "Aperature Macro: #{macro_name} => #{primitives}"
|
208
|
+
when 'SM' # Deprecated
|
209
|
+
/^SM(A(0|1))?(B(0|1))?/ =~ s
|
210
|
+
@symbol_mirror[:a] = ('1' == $1) ? -1 : 1
|
211
|
+
@symbol_mirror[:b] = ('1' == $2) ? -1 : 1
|
212
|
+
|
213
|
+
# Section 4.3 - Directive Parameters
|
214
|
+
when 'AS' # Deprecated
|
215
|
+
/^ASA(X|Y)B(X|Y)/ =~ s
|
216
|
+
raise ParseError, "The AS directive requires that both axes must be specified" unless $1 && $2
|
217
|
+
raise ParseError, "Axis Select directive can't map both data axes to the same output axis" if $1 == $2
|
218
|
+
@axis_select[:a] = $1.downcase.to_sym
|
219
|
+
@axis_select[:b] = $2.downcase.to_sym
|
220
|
+
when 'FS'
|
221
|
+
/^FS(L|T)(A|I)(N\d)?(G\d)?X(\d)(\d)Y(\d)(\d)(D\d)?(M\d)?/ =~ s
|
222
|
+
self.absolute = ($2 == 'A')
|
223
|
+
self.zero_omission = ($1 == 'L') ? :leading : (($1 == 'T') ? :trailing : nil)
|
224
|
+
xn, xm, yn, ym = $5, $6, $7, $8
|
225
|
+
raise ParseError, "X and Y coordinate formats must equal" unless (xn == yn) && (xm == ym)
|
226
|
+
self.coordinate_format = xn, xm
|
227
|
+
when 'MI' # Deprecated
|
228
|
+
/^MIA(0|1)B(0|1)/ =~ s
|
229
|
+
raise ParseError, "The MI directive requires that both axes be specified" unless $1 || $2
|
230
|
+
@axis_mirror[:a] = ('0' == $1) ? 1 : -1
|
231
|
+
@axis_mirror[:b] = ('0' == $2) ? 1 : -1
|
232
|
+
when 'MO'
|
233
|
+
/^MO(IN|MM)/ =~ s
|
234
|
+
set_inches if 'IN' == $1
|
235
|
+
set_millimeters if 'MM' == $1
|
236
|
+
when 'OF' # Deprecated
|
237
|
+
/^OF(A([\d.+-]+))?(B([\d.+-]+))?/ =~ s
|
238
|
+
@offset = Point[parse_float($2) || 0.0, parse_float($4) || 0.0]
|
239
|
+
when 'SF' # Deprecated
|
240
|
+
/^SF(A([\d.+-]+))?(B([\d.+-]+))?/ =~ s
|
241
|
+
@scale = Vector[parse_float($2) || 0.0, parse_float($4) || 0.0]
|
242
|
+
|
243
|
+
# Section 4.4 - Image Parameters
|
244
|
+
when 'IJ' # Deprecated
|
245
|
+
when 'IP' # Deprecated
|
246
|
+
/^IP(POS|NEG)/ =~ s
|
247
|
+
current_layer.polarity = ('NEG' == $1) ? :negative : :positive
|
248
|
+
when 'IR' # Deprecated
|
249
|
+
/^IR(0|90|180|270)/ =~ s
|
250
|
+
@rotation = $1.to_f.degrees
|
251
|
+
|
252
|
+
# Section 4.5 - Layer Specific Parameters
|
253
|
+
when 'KO' # Deprecated
|
254
|
+
/^KO(C|D)?(X([\d.+-]+)Y([\d.+-]+)I([\d.+-]+)J([\d.+-]+))?/ =~ s
|
255
|
+
polarity, x, y, i, j = $1, $3, $4, $5, $6
|
256
|
+
raise ParseError, "KO not supported"
|
257
|
+
when 'LN'
|
258
|
+
/^LN([[:print:]]+)\*/ =~ s
|
259
|
+
new_layer.name = $1
|
260
|
+
when 'LP'
|
261
|
+
/^LP(C|D)/ =~ s
|
262
|
+
@new_layer_polarity = ('C' == $1) ? :clear : :dark
|
263
|
+
current_layer.polarity = @new_layer_polarity
|
264
|
+
when 'SR'
|
265
|
+
/^SR(X(\d+))?(Y(\d+))?(I([\d.+-]+))?(J([\d.+-]+))?/ =~ s
|
266
|
+
x, y, i, j = $2, $4, parse_float($6), parse_float($8)
|
267
|
+
layer = new_layer
|
268
|
+
layer.step = Vector[i || 0, j || 0]
|
269
|
+
layer.repeat = Vector[x || 1, y || 1]
|
270
|
+
else
|
271
|
+
raise ParseError, "Unrecognized Parameter Type: '#{directive}'"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|