dxf 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2645909e4268c3367196986f86b48822419a8299
4
+ data.tar.gz: ef973d3d48e9f3cdbb0079d21e97eed21ec19706
5
+ SHA512:
6
+ metadata.gz: 3a3f3a3582ba79bfc5b8d19227dae5c3ee874cb0fbe3f30c1da21cb46de86e386b51d519578b0094183202b2967cc83d78f6753462f5c8251185f300c791f26e
7
+ data.tar.gz: 7d3307bc5e85f8e66ab9d582c1511d47ff646fddebeef222daa189f92b87d802d3014ecceb072b8972a146325698717e5f6d97db72b8f7bf9d9691118b7b4fd3
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ deploy:
5
+ provider: rubygems
6
+ api_key:
7
+ secure: S0O4Y3l92qkVXey7ZqIQnRt+81UflIXtsRLOlqONmk2eJ+OLWF8PnQf2PHLuCN39xtaNRPRmjzXxq5SqIvZdJNveAWhTMR+JerW6itGEa1pDEeqtbLsZ42uzfoUdw4kuw/tr2cB8FaNujtRndbzsqRsEinfJ/zlPO606C3CdodI=
8
+ gem: dxf
9
+ on:
10
+ tags: true
11
+ repo: bfoz/dxf-ruby
data/Gemfile CHANGED
@@ -1,4 +1,10 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in dxf.gemspec
4
3
  gemspec
4
+
5
+ gem 'sketch', github: 'bfoz/sketch'
6
+ gem 'units', github: 'bfoz/units-ruby'
7
+
8
+ group :test do
9
+ gem 'rake'
10
+ end
@@ -1,5 +1,7 @@
1
1
  # DXF
2
2
 
3
+ [![Build Status](https://travis-ci.org/bfoz/dxf-ruby.png)](https://travis-ci.org/bfoz/dxf-ruby)
4
+
3
5
  Tools for working with the popular DXF file format
4
6
 
5
7
  ## Installation
@@ -28,4 +30,4 @@ DXF.write('filename.dxf', my_sketch, :inches)
28
30
  License
29
31
  -------
30
32
 
31
- Copyright 2012-2013 Brandon Fosdick <bfoz@bfoz.net> and released under the BSD license.
33
+ Copyright 2012-2014 Brandon Fosdick <bfoz@bfoz.net> and released under the BSD license.
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
2
  require 'rake/testtask'
3
3
 
4
+ task :default => :test
5
+
4
6
  Rake::TestTask.new do |t|
5
7
  t.libs.push "lib"
6
8
  t.test_files = FileList['test/**/*.rb']
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "dxf"
7
- gem.version = '0.2'
7
+ gem.version = '0.3'
8
8
  gem.authors = ["Brandon Fosdick"]
9
9
  gem.email = ["bfoz@bfoz.net"]
10
10
  gem.description = %q{Read and write DXF files using Ruby}
@@ -16,6 +16,9 @@ Gem::Specification.new do |gem|
16
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
17
  gem.require_paths = ["lib"]
18
18
 
19
- gem.add_dependency 'sketch'
20
- gem.add_dependency 'units', '~> 2.1'
19
+ gem.add_dependency 'geometry', '~> 6.4'
20
+ gem.add_dependency 'sketch', '~> 0.4'
21
+ gem.add_dependency 'units', '~> 2.4'
22
+
23
+ gem.required_ruby_version = '>= 2.0'
21
24
  end
data/lib/dxf.rb CHANGED
@@ -1,6 +1,5 @@
1
- require 'geometry'
2
- require 'sketch'
3
- require 'units'
1
+ require_relative 'dxf/parser'
2
+ require_relative 'dxf/unparser'
4
3
 
5
4
  module DXF
6
5
  =begin
@@ -9,101 +8,18 @@ Reading and writing of files using AutoCAD's {http://en.wikipedia.org/wiki/AutoC
9
8
  {http://usa.autodesk.com/adsk/servlet/item?siteID=123112&id=12272454&linkID=10809853 DXF Specifications}
10
9
  =end
11
10
 
12
- class Builder
13
- attr_accessor :container
14
-
15
- # Initialize with a Sketch
16
- # @param [String,Symbol] units The units to convert length values to (:inches or :millimeters)
17
- def initialize(units=:mm)
18
- @units = units
19
- end
20
-
21
- # Convert the given value to the correct units and return it as a formatted string
22
- # @return [String]
23
- def format_value(value)
24
- if value.is_a? Units::Literal
25
- "%g" % value.send("to_#{@units}".to_sym)
26
- else
27
- "%g" % value
28
- end
29
- end
30
-
31
- def to_s
32
- from_sketch(container)
33
- end
34
-
35
- # Convert a {Geometry::Line} into an entity array
36
- # @overload line(Line, layer=0)
37
- # @overload line(Point, Point, layer=0)
38
- def line(*args)
39
- if args[0].is_a?(Geometry::Line)
40
- first, last = args[0].first, args[0].last
41
- layer = args[1] ||= 0
42
- else
43
- first = args[0]
44
- last = args[1]
45
- layer = args[2] ||= 0
46
- end
47
- first = Point[first] unless first.is_a?(Geometry::Point)
48
- last = Point[last] unless last.is_a?(Geometry::Point)
49
- [ 0, 'LINE',
50
- 8, layer,
51
- 10, format_value(first.x),
52
- 20, format_value(first.y),
53
- 11, format_value(last.x),
54
- 21, format_value(last.y)]
55
- end
56
-
57
- def section(name)
58
- [0, 'SECTION', 2, name]
59
- end
60
-
61
- # Build a DXF from a Sketch
62
- # @return [Array] Array of bytes to be written to a file
63
- def from_sketch(sketch)
64
- bytes = []
65
- bytes.push section('HEADER')
66
- bytes.push 0, 'ENDSEC'
67
- bytes.push section('ENTITIES')
68
-
69
- sketch.geometry.map do |element|
70
- case element
71
- when Geometry::Arc
72
- bytes.push 0, 'ARC'
73
- bytes.push 10, format_value(element.center.x)
74
- bytes.push 20, format_value(element.center.y)
75
- bytes.push 40, format_value(element.radius)
76
- bytes.push 50, format_value(element.start_angle)
77
- bytes.push 51, format_value(element.end_angle)
78
- when Geometry::Circle
79
- bytes.push 0, 'CIRCLE'
80
- bytes.push 10, format_value(element.center.x)
81
- bytes.push 20, format_value(element.center.y)
82
- bytes.push 40, format_value(element.radius)
83
- when Geometry::Line
84
- bytes.push line(element.first, element.last)
85
- when Geometry::Polyline
86
- element.edges.map {|edge| bytes.push line(edge.first, edge.last) }
87
- when Geometry::Rectangle
88
- element.edges.map {|edge| bytes.push line(edge.first, edge.last) }
89
- when Geometry::Square
90
- points = element.points
91
- points.each_cons(2) {|p1,p2| bytes.push line(p1,p2) }
92
- bytes.push line(points.last, point.first)
93
- end
94
- end
95
-
96
- bytes.push 0, 'ENDSEC'
97
- bytes.push 0, 'EOF'
98
- bytes.join "\n"
99
- end
100
- end
101
-
102
11
  # Export a {Sketch} to a DXF file
103
12
  # @param [String] filename The path to write to
104
13
  # @param [Sketch] sketch The {Sketch} to export
105
14
  # @param [Symbol] units Convert all values to the specified units (:inches or :mm)
106
15
  def self.write(filename, sketch, units=:mm)
107
- File.write(filename, Builder.new(units).from_sketch(sketch))
16
+ File.open(filename, 'w') {|f| Unparser.new(units).unparse(f, sketch)}
17
+ end
18
+
19
+ # Read a DXF file
20
+ # @param [String] filename The path to the file to read
21
+ # @return [DXF] the resulting {DXF} object
22
+ def self.read(filename)
23
+ File.open(filename, 'r') {|f| DXF::Parser.new.parse(f) }
108
24
  end
109
25
  end
@@ -0,0 +1,15 @@
1
+ # Include this module in the base class of a class cluster to handle swizzling
2
+ # of ::new
3
+ module ClusterFactory
4
+ def self.included(parent)
5
+ class << parent
6
+ alias :original_new :new
7
+
8
+ def inherited(subclass)
9
+ class << subclass
10
+ alias :new :original_new
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,169 @@
1
+ require 'geometry'
2
+
3
+ require_relative 'cluster_factory'
4
+
5
+ module DXF
6
+ Point = Geometry::Point
7
+
8
+ # {Entity} is the base class for everything that can live in the ENTITIES block
9
+ class Entity
10
+ TypeError = Class.new(StandardError)
11
+
12
+ include ClusterFactory
13
+
14
+ attr_accessor :handle
15
+ attr_accessor :layer
16
+
17
+ def self.new(type)
18
+ case type
19
+ when 'CIRCLE' then Circle.new
20
+ when 'LINE' then Line.new
21
+ when 'SPLINE' then Spline.new
22
+ else
23
+ raise TypeError, "Unrecognized entity type '#{type}'"
24
+ end
25
+ end
26
+
27
+ def parse_pair(code, value)
28
+ # Handle group codes that are common to all entities
29
+ # These are from the table that starts on page 70 of specification
30
+ case code
31
+ when '5'
32
+ handle = value
33
+ when '8'
34
+ layer = value
35
+ else
36
+ p "Unrecognized entity group code: #{code} #{value}"
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def point_from_values(*args)
43
+ Geometry::Point[args.flatten.reverse.drop_while {|a| not a }.reverse]
44
+ end
45
+ end
46
+
47
+ class Circle < Entity
48
+ attr_accessor :x, :y, :z
49
+ attr_accessor :radius
50
+
51
+ def parse_pair(code, value)
52
+ case code
53
+ when '10' then self.x = value.to_f
54
+ when '20' then self.y = value.to_f
55
+ when '30' then self.z = value.to_f
56
+ when '40' then self.radius = value.to_f
57
+ else
58
+ super # Handle common and unrecognized codes
59
+ end
60
+ end
61
+
62
+ # @!attribute [r] center
63
+ # @return [Point] the composed center of the {Circle}
64
+ def center
65
+ a = [x, y, z]
66
+ a.pop until a.last
67
+ Geometry::Point[*a]
68
+ end
69
+ end
70
+
71
+ class Line < Entity
72
+ attr_reader :first, :last
73
+ attr_accessor :x1, :y1, :z1
74
+ attr_accessor :x2, :y2, :z2
75
+
76
+ def parse_pair(code, value)
77
+ case code
78
+ when '10' then self.x1 = value.to_f
79
+ when '20' then self.y1 = value.to_f
80
+ when '30' then self.z1 = value.to_f
81
+ when '11' then self.x2 = value.to_f
82
+ when '21' then self.y2 = value.to_f
83
+ when '31' then self.z2 = value.to_f
84
+ else
85
+ super # Handle common and unrecognized codes
86
+ end
87
+ end
88
+
89
+ def initialize(*args)
90
+ @first, @last = *args
91
+ end
92
+
93
+ # @!attribute [r] first
94
+ # @return [Point] the starting point of the {Line}
95
+ def first
96
+ @first ||= point_from_values(x1, y1, z1)
97
+ end
98
+
99
+ # @!attribute [r] last
100
+ # @return [Point] the end point of the {Line}
101
+ def last
102
+ @last ||= point_from_values(x2, y2, z2)
103
+ end
104
+ end
105
+
106
+ class LWPolyline < Entity
107
+ # @!attribute points
108
+ # @return [Array<Point>] The points that make up the polyline
109
+ attr_reader :points
110
+
111
+ def initialize(*points)
112
+ @points = points.map {|a| Point[a]}
113
+ end
114
+
115
+ # Return the individual line segments
116
+ def lines
117
+ points.each_cons(2).map {|a,b| Line.new a, b}
118
+ end
119
+ end
120
+
121
+ class Spline < Entity
122
+ attr_reader :degree
123
+ attr_reader :knots
124
+ attr_reader :points
125
+
126
+ def initialize(degree:nil, knots:[], points:nil)
127
+ @degree = degree
128
+ @knots = knots || []
129
+ @points = points || []
130
+ end
131
+ end
132
+
133
+ class Bezier < Spline
134
+ # @!attribute degree
135
+ # @return [Number] The degree of the curve
136
+ def degree
137
+ points.length - 1
138
+ end
139
+
140
+ # @!attribute points
141
+ # @return [Array<Point>] The control points for the Bézier curve
142
+ attr_reader :points
143
+
144
+ def initialize(*points)
145
+ @points = points.map {|v| Geometry::Point[v]}
146
+ end
147
+
148
+ # http://en.wikipedia.org/wiki/Binomial_coefficient
149
+ # http://rosettacode.org/wiki/Evaluate_binomial_coefficients#Ruby
150
+ def binomial_coefficient(k)
151
+ (0...k).inject(1) {|m,i| (m * (degree - i)) / (i + 1) }
152
+ end
153
+
154
+ # @param t [Float] the input parameter
155
+ def [](t)
156
+ return nil unless (0..1).include?(t)
157
+ result = Geometry::Point.zero(points.first.size)
158
+ points.each_with_index do |v, i|
159
+ result += v * binomial_coefficient(i) * ((1 - t) ** (degree - i)) * (t ** i)
160
+ end
161
+ result
162
+ end
163
+
164
+ # Convert the {Bezier} into the given number of line segments
165
+ def lines(count=20)
166
+ (0..1).step(1.0/count).map {|t| self[t]}.each_cons(2).map {|a,b| Line.new a, b}
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,248 @@
1
+ require_relative 'entity'
2
+
3
+ module DXF
4
+ class Parser
5
+ ParseError = Class.new(StandardError)
6
+
7
+ # @!attribute entities
8
+ # @return [Array] the entities that comprise the drawing
9
+ attr_accessor :entities
10
+
11
+ # @!attribute header
12
+ # @return [Hash] the header variables
13
+ attr_accessor :header
14
+
15
+ def initialize(units=:mm)
16
+ @entities = []
17
+ @header = {}
18
+ end
19
+
20
+ def parse(io)
21
+ parse_pairs io do |code, value|
22
+ next if '999' == code
23
+ raise ParseError, "DXF files must begin with group code 0, not #{code}" unless '0' == code
24
+ raise ParseError, "Expecting a SECTION, not #{value}" unless 'SECTION' == value
25
+ parse_section(io)
26
+ end
27
+ self
28
+ end
29
+
30
+ private
31
+
32
+ def read_pair(io)
33
+ code = io.gets.strip
34
+ value = io.gets.strip
35
+ value = case code.to_i
36
+ when 1..9
37
+ value.to_s
38
+ when 10..18, 20..28, 30..37, 40..49
39
+ value.to_f
40
+ when 50..58
41
+ value.to_f # degrees
42
+ when 70..78, 90..99, 270..289
43
+ value.to_i
44
+ else
45
+ value
46
+ end
47
+
48
+ [code, value]
49
+ end
50
+
51
+ def parse_pairs(io, &block)
52
+ while not io.eof?
53
+ code, value = read_pair(io)
54
+ case [code, value]
55
+ when ['0', 'ENDSEC']
56
+ yield code, value # Allow the handler a chance to clean up
57
+ return
58
+ when ['0', 'EOF'] then return
59
+ else
60
+ yield code, value
61
+ end
62
+ end
63
+ end
64
+
65
+ def parse_section(io)
66
+ code, value = read_pair(io)
67
+ raise ParseError, 'SECTION must be followed by a section type' unless '2' == code
68
+
69
+ case value
70
+ when 'BLOCKS' then parse_pairs(io) {|code, value|} # Ignore until implemented
71
+ when 'CLASSES' then parse_pairs(io) {|code, value|} # Ignore until implemented
72
+ when 'ENTITIES'
73
+ parse_entities(io)
74
+ when 'HEADER'
75
+ parse_header(io)
76
+ when 'OBJECTS' then parse_pairs(io) {|code, value|} # Ignore until implemented
77
+ when 'TABLES' then parse_pairs(io) {|code, value|} # Ignore until implemented
78
+ # when 'THUMBNAILIMAGE'
79
+ else
80
+ raise ParseError, "Unrecognized section type '#{value}'"
81
+ end
82
+ end
83
+
84
+ # Parse the ENTITIES section
85
+ def parse_entities(io)
86
+ parser = nil
87
+ parse_pairs io do |code, value|
88
+ if 0 == code.to_i
89
+ if parser
90
+ entities.push parser.to_entity
91
+ parser = nil
92
+ end
93
+
94
+ # Nothing to do
95
+ next if 'ENDSEC' == value
96
+
97
+ if 'LWPOLYLINE' == value
98
+ parser = EntityParser.new(value)
99
+ elsif 'SPLINE' == value
100
+ parser = SplineParser.new
101
+ else
102
+ entities.push Entity.new(value)
103
+ end
104
+ elsif parser
105
+ parser.parse_pair(code.to_i, value)
106
+ else
107
+ entities.last.parse_pair(code, value)
108
+ end
109
+ end
110
+ end
111
+
112
+ # Parse the HEADER section
113
+ def parse_header(io)
114
+ variable_name = nil
115
+ parse_pairs io do |code, value|
116
+ case code
117
+ when '0' then next
118
+ when '9'
119
+ variable_name = value
120
+ else
121
+ header[variable_name] = value
122
+ end
123
+ end
124
+ end
125
+
126
+ # @group Helpers
127
+ def self.code_to_symbol(code)
128
+ case code
129
+ when 10..13 then :x
130
+ when 20..23 then :y
131
+ when 30..33 then :z
132
+ end
133
+ end
134
+
135
+ def self.update_point(point, x:nil, y:nil, z:nil)
136
+ a = point ? point.to_a : []
137
+ a[0] = x if x
138
+ a[1] = y if y
139
+ a[2] = z if z
140
+ Geometry::Point[a]
141
+ end
142
+ # @endgroup
143
+ end
144
+
145
+ class EntityParser
146
+ # @!attribute points
147
+ # @return [Array] points
148
+ attr_accessor :points
149
+
150
+ attr_reader :handle
151
+ attr_reader :layer
152
+
153
+ def initialize(type_name)
154
+ @flags = nil
155
+ @points = Array.new { Point.new }
156
+ @type_name = type_name
157
+
158
+ @point_index = Hash.new {|h,k| h[k] = 0}
159
+ end
160
+
161
+ def parse_pair(code, value)
162
+ case code
163
+ when 5 then @handle = value # Fixed
164
+ when 8 then @layer = value # Fixed
165
+ when 62 then @color_number = value # Fixed
166
+ when 10, 20, 30
167
+ k = Parser.code_to_symbol(code)
168
+ i = @point_index[k]
169
+ @points[i] = Parser.update_point(@points[i], k => value)
170
+ @point_index[k] += 1
171
+ when 70 then @flags = value
172
+ end
173
+ end
174
+
175
+ def to_entity
176
+ case @type_name
177
+ when 'LWPOLYLINE' then LWPolyline.new(*points)
178
+ end
179
+ end
180
+ end
181
+
182
+ class SplineParser < EntityParser
183
+ # @!attribute points
184
+ # @return [Array] points
185
+ attr_accessor :points
186
+
187
+ attr_reader :closed, :periodic, :rational, :planar, :linear
188
+ attr_reader :degree
189
+ attr_reader :knots
190
+
191
+ def initialize
192
+ super 'SPLINE'
193
+ @fit_points = []
194
+ @knots = []
195
+ @weights = []
196
+
197
+ @fit_point_index = Hash.new {|h,k| h[k] = 0}
198
+ end
199
+
200
+ def parse_pair(code, value)
201
+ case code
202
+ when 11, 21, 31
203
+ k = Parser.code_to_symbol(code)
204
+ i = @fit_point_index[k]
205
+ @fit_points[i] = Parser.update_point(@fit_points[i], k => value)
206
+ @fit_point_index[k] += 1
207
+ when 12, 22, 32 then @start_tangent = update_point(@start_tangent, Parser.code_to_symbol(code) => value)
208
+ when 13, 23, 33 then @end_tangent = update_point(@end_tangent, Parser.code_to_symbol(code) => value)
209
+ when 40 then knots.push value.to_f
210
+ when 41 then @weights.push value
211
+ when 42 then @knot_tolerance = value
212
+ when 43 then @control_tolerance = value
213
+ when 44 then @fit_tolerance = value
214
+ when 70
215
+ value = value.to_i
216
+ @closed = value[0].zero? ? nil : true
217
+ @periodic = value[1].zero? ? nil : true
218
+ @rational = value[2].zero? ? nil : true
219
+ @planar = value[3].zero? ? nil : true
220
+ @linear = value[4].zero? ? nil : true
221
+ when 71 then @degree = value
222
+ when 72 then @num_knots = value
223
+ when 73 then @num_control_points = value
224
+ when 74 then @num_fit_points = value
225
+ else
226
+ super
227
+ end
228
+ end
229
+
230
+ def to_entity
231
+ raise ParseError, "Wrong number of control points" unless points.size == @num_control_points
232
+
233
+ # If all of the points lie in the XY plane, remove the Z component from each point
234
+ if planar && points.all? {|a| a.z.zero?}
235
+ @points.map! {|a| Geometry::Point[a[0, 2]]}
236
+ end
237
+
238
+ if knots.size == 2*points.size
239
+ # Bezier?
240
+ if knots[0,points.size].all?(&:zero?) && (knots[-points.size,points.size].uniq.size==1)
241
+ Bezier.new *points
242
+ end
243
+ else
244
+ Spline.new degree:degree, knots:knots, points:points
245
+ end
246
+ end
247
+ end
248
+ end