dxf 0.2 → 0.3

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,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