gerber 0 → 1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in manufacturing.gemspec
3
+ # Specify your gem's dependencies in gerber.gemspec
4
4
  gemspec
@@ -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
@@ -1,2 +1,9 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs.push "lib"
7
+ t.test_files = FileList['test/**/*.rb']
8
+ t.verbose = true
9
+ end
@@ -2,16 +2,18 @@
2
2
 
3
3
  Gem::Specification.new do |gem|
4
4
  gem.name = "gerber"
5
- gem.version = 0
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-274-X files}
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
@@ -1,3 +1,44 @@
1
- module Gerber
2
- # Your code goes here...
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
@@ -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