dxf_io 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ module DxfIO
2
+ module Entity
3
+ module Support
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Point
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,114 @@
1
+ module DxfIO
2
+ module Entity
3
+ module Support
4
+ class Point
5
+
6
+ START_POINT_GROUP_NUMS = DxfIO::Constants::START_POINT_GROUP_NUMS
7
+ END_POINT_GROUP_NUMS = DxfIO::Constants::END_POINT_GROUP_NUMS
8
+ TYPES = %i(start end).freeze
9
+
10
+ attr_reader :x, :y, :type
11
+
12
+ def initialize(x, y, options = {})
13
+ @x, @y = x, y
14
+ @type = TYPES.include?(options[:type]) ? options[:type] : TYPES.first
15
+ end
16
+
17
+ def start?
18
+ @type == :start
19
+ end
20
+
21
+ def end?
22
+ @type == :end
23
+ end
24
+
25
+ def to_a
26
+ [@x, @y]
27
+ end
28
+
29
+ def to_h
30
+ {x: @x, y: @y}
31
+ end
32
+
33
+ def to_dxf_array
34
+ if start?
35
+ [{START_POINT_GROUP_NUMS.first => @x}, {START_POINT_GROUP_NUMS.last => @y}]
36
+ elsif end?
37
+ [{END_POINT_GROUP_NUMS.first => @x}, {END_POINT_GROUP_NUMS.last => @y}]
38
+ end
39
+ end
40
+
41
+ # eq operations
42
+
43
+ def ==(point)
44
+ to_a == point.to_a
45
+ end
46
+
47
+ # math operations
48
+
49
+ # unary
50
+
51
+ def -@
52
+ self.class.new(-@x, -@y, type: @type)
53
+ end
54
+
55
+ # binary
56
+
57
+ def +(point)
58
+ if point.is_a? self.class
59
+ self.class.new(@x + point.x,
60
+ @y + point.y,
61
+ type: @type == point.type ? @type : :start)
62
+ elsif point.is_a? Array
63
+ self.class.new(@x + point[0],
64
+ @y + point[1],
65
+ type: @type)
66
+ else
67
+ raise ArgumentError, 'point must be an Array or a DxfIO::Entity::Support::Point'
68
+ end
69
+ end
70
+
71
+ def -(point)
72
+ if point.is_a? self.class
73
+ self - point
74
+ elsif point.is_a? Array
75
+ self + point.map(&:-@)
76
+ else
77
+ raise ArgumentError, 'point must be an Array or a DxfIO::Entity::Support::Point'
78
+ end
79
+ end
80
+
81
+ def *(num)
82
+ if num.is_a? Numeric
83
+ self.class.new(@x * num,
84
+ @y * num,
85
+ type: @type)
86
+ else
87
+ raise ArgumentError, 'argument must be Numeric'
88
+ end
89
+ end
90
+
91
+ def /(num)
92
+ if num.is_a? Numeric
93
+ self.class.new(@x / num,
94
+ @y / num,
95
+ type: @type)
96
+ else
97
+ raise ArgumentError, 'argument must be Numeric'
98
+ end
99
+ end
100
+
101
+ # geometrical operation (supposed what point is a vector from zero)
102
+
103
+ def rotate_90
104
+ self.class.new(@y, -@x, type: @type)
105
+ end
106
+
107
+ def rotate_180
108
+ -self
109
+ end
110
+
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,14 @@
1
+ module DxfIO
2
+ module Entity
3
+ class Text < Other
4
+
5
+ protected
6
+
7
+ def initialize(groups, representation_hash)
8
+ @representation_hash = representation_hash
9
+ super(groups)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,139 @@
1
+ module DxfIO
2
+ # based on DXF AutoCAD 2008 documentation (http://images.autodesk.com/adsk/files/acad_dxf0.pdf)
3
+ class Reader
4
+
5
+ SECTIONS_LIST = DxfIO::Constants::SECTIONS_LIST
6
+ HEADER_NAME = DxfIO::Constants::HEADER_NAME
7
+
8
+ def initialize(options)
9
+ if options.is_a? String
10
+ @filename = options
11
+ elsif options.is_a? Hash
12
+ if options[:path].present?
13
+ @filename = options[:path]
14
+ else
15
+ raise ArgumentError, 'options must contain a :path key'
16
+ end
17
+ @encoding = options[:encoding] || 'Windows-1251'
18
+ end
19
+ end
20
+
21
+ class << self
22
+ def open(options)
23
+ self.new(options).tap do |reader_instance|
24
+ if block_given?
25
+ yield reader_instance
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ([HEADER_NAME] + SECTIONS_LIST).each do |method|
32
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
33
+ def #{method.downcase} # def classes
34
+ run['#{method}'] # run['CLASSES']
35
+ end # end
36
+ EOT
37
+ end
38
+
39
+ def run
40
+ @result_hash ||= parse
41
+ end
42
+
43
+ alias to_hash run
44
+ alias to_h run
45
+
46
+ def rerun
47
+ @result_hash = parse
48
+ end
49
+
50
+ def parse(filename = @filename, encoding = @encoding)
51
+ read_flag = "r:#{encoding}:UTF-8"
52
+ fp = File.open(filename, read_flag)
53
+ dxf = {HEADER_NAME => {}}
54
+ SECTIONS_LIST.each do |section_name|
55
+ dxf[section_name] = []
56
+ end
57
+ #
58
+ # main loop
59
+ #
60
+ begin
61
+ while true
62
+ c, v = read_codes(fp)
63
+ break if v == 'EOF'
64
+ if v == 'SECTION'
65
+ c, v = read_codes(fp)
66
+ if v == HEADER_NAME
67
+ hdr = dxf[HEADER_NAME]
68
+ while true
69
+ c, v = read_codes(fp)
70
+ break if v == 'ENDSEC' # or v == "BLOCKS" or v == "ENTITIES" or v == "EOF"
71
+ if c == 9
72
+ key = v
73
+ hdr[key] = {}
74
+ else
75
+ add_att(hdr[key], c, v)
76
+ end
77
+ end # while
78
+ elsif SECTIONS_LIST.include?(v)
79
+ section = dxf[v]
80
+ parse_entities(section, fp)
81
+ end
82
+ end # if in SECTION
83
+ end # main loop
84
+ ensure
85
+ fp.close unless fp.nil?
86
+ end
87
+
88
+ dxf
89
+ end
90
+
91
+ private
92
+
93
+ def parse_entities(section, fp)
94
+ while true
95
+ c, v = read_codes(fp)
96
+ break if v == 'ENDSEC' || v == 'EOF'
97
+ next if c == 999
98
+
99
+ if c == 0
100
+ section << [c => v]
101
+ else
102
+ section[-1] << {c => v}
103
+ end
104
+ end # while
105
+ end
106
+
107
+ def read_codes(fp)
108
+ c = fp.gets
109
+ return [0, 'EOF'] if c.nil?
110
+ v = fp.gets
111
+ return [0, 'EOF'] if v.nil?
112
+ c = c.to_i
113
+ v.strip!
114
+ v.upcase! if c == 0
115
+ case c
116
+ when 10..59, 110..119, 120..129, 130..139, 140..149, 140..147, 210..239, 460..469, 1010..1059
117
+ v = v.to_f
118
+ when 60..79, 90..99, 170..175, 280..289, 370..379, 380..389, 400..409, 420..429, 440..449, 450..459, 500..409, 1060..1070, 1071
119
+ v = v.to_i
120
+ end
121
+
122
+ [c, v]
123
+ end
124
+
125
+ def add_att(ent, code, value)
126
+ if ent[code].nil?
127
+ ent[code] = value
128
+ elsif ent[code].is_a? Array
129
+ ent[code] << value
130
+ else
131
+ t = ent[code]
132
+ ent[code] = []
133
+ ent[code] << t
134
+ ent[code] << value
135
+ end
136
+ end
137
+
138
+ end
139
+ end
@@ -0,0 +1,3 @@
1
+ module DxfIO
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,45 @@
1
+ module DxfIO
2
+ class Wrapper
3
+
4
+ SECTIONS_LIST = DxfIO::Constants::SECTIONS_LIST
5
+ HEADER_NAME = DxfIO::Constants::HEADER_NAME
6
+ ENTITIES_TYPE_NAME_VALUE_MAPPING = DxfIO::Constants::ENTITIES_TYPE_NAME_VALUE_MAPPING
7
+
8
+ def initialize(options)
9
+ if options.is_a? Hash
10
+ if options[:dxf_hash].nil?
11
+ @dxf_hash = options
12
+ else
13
+ @dxf_hash = options[:dxf_hash]
14
+ end
15
+ else
16
+ raise ArgumentError, 'options must be a Hash'
17
+ end
18
+ end
19
+
20
+ SECTIONS_LIST.each do |method|
21
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
22
+ def #{method.downcase} # def classes
23
+ fetch_entities('#{method}') # fetch_entities('CLASSES')
24
+ end # end
25
+ EOT
26
+ end
27
+
28
+ def fetch_entities(group_name)
29
+ @dxf_hash[group_name.upcase].collect do |entity_groups|
30
+ to_proper_class(DxfIO::Entity::Other.new(entity_groups))
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def to_proper_class(entity)
37
+ type_name = ENTITIES_TYPE_NAME_VALUE_MAPPING.invert[entity.type.upcase]
38
+ if type_name.nil? || !DxfIO::Entity.constants.include?(type_name.capitalize.to_sym)
39
+ entity
40
+ else
41
+ entity.public_send("to_#{type_name}")
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,215 @@
1
+ module DxfIO
2
+ class Writer
3
+ require 'fileutils'
4
+
5
+ SECTIONS_LIST = DxfIO::Constants::SECTIONS_LIST
6
+ HEADER_NAME = DxfIO::Constants::HEADER_NAME
7
+ STRATEGY = DxfIO::Constants::WRITER_STRATEGY
8
+
9
+ def initialize(options)
10
+ # TODO: replace instance variables to hash with options
11
+ # default options
12
+ @encoding = 'Windows-1251'
13
+ @delimiter = "\r\n"
14
+ @strategy = STRATEGY.first
15
+ @dxf_hash = {}
16
+
17
+ if options.is_a? String
18
+ @filename = options
19
+ elsif options.is_a? Hash
20
+ @dxf_hash = options[:dxf_hash] if options.has_key? :dxf_hash
21
+ @filename = options[:path] if options.has_key? :path
22
+ @encoding = options[:encoding] if options.has_key? :encoding
23
+ @delimiter = options[:delimiter] if options.has_key? :delimiter
24
+ @strategy = options[:strategy] if options.has_key? :strategy && STRATEGY.include?(options[:strategy])
25
+ else
26
+ raise ArgumentError, 'options must be String or Hash with :path key'
27
+ end
28
+ end
29
+
30
+ class << self
31
+ def open(options)
32
+ self.new(options).tap do |writer_instance|
33
+ if block_given?
34
+ yield writer_instance
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ # construct dxf content in memory and write all in file at once
41
+ def write_through_memory(dxf_hash = @dxf_hash)
42
+ file_stream do |fp|
43
+ fp.write(
44
+ file_content do
45
+ dxf_hash.inject('') do |sections_content, (section_name, section_content)|
46
+ sections_content << section_wrapper_content(section_name) do
47
+ if header_section?(section_name)
48
+ header_content(section_content)
49
+ else
50
+ other_section_content(section_content)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ )
56
+ end
57
+ end
58
+
59
+ # write dxf file directly on disk without temporary usage of memory for content
60
+ def write_through_disk(dxf_hash = @dxf_hash)
61
+ file_stream do |fp|
62
+ file_wrap(fp) do
63
+ dxf_hash.each_pair do |section_name, section_content|
64
+ section_wrap(fp, section_name) do
65
+ if header_section?(section_name)
66
+ header_wrap(fp, section_content)
67
+ else
68
+ other_section_wrap(fp, section_content)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def run(dxf_hash = @dxf_hash)
77
+ if @strategy == :memory
78
+ write_through_memory(dxf_hash)
79
+ elsif @strategy == :disk
80
+ write_through_disk(dxf_hash)
81
+ else
82
+ raise ArgumentError, ':strategy has invalid value; allowed only [:memory, :disk]'
83
+ end
84
+ end
85
+
86
+ alias write_hash run
87
+
88
+ private
89
+
90
+ # work with file
91
+
92
+ def file_stream(&block)
93
+ folder_path = @filename.split('/')[0..-2].join('/')
94
+ FileUtils.mkdir_p(folder_path)
95
+ fp = File.open(@filename, "w:#{@encoding}")
96
+ begin
97
+ block.call(fp)
98
+ ensure
99
+ fp.close unless fp.nil?
100
+ end
101
+ end
102
+
103
+ # helpers
104
+
105
+ def header_section?(section_name)
106
+ section_name.upcase == HEADER_NAME
107
+ end
108
+
109
+ # wrappers
110
+
111
+ # file format:
112
+ # ... file content ...
113
+ # 0
114
+ # EOF
115
+ def file_wrap(fp, &block)
116
+ file_end = "0#{@delimiter}EOF#{@delimiter}"
117
+
118
+ block.call
119
+ fp.write(file_end)
120
+ end
121
+
122
+ # section format:
123
+ # 0
124
+ # SECTION
125
+ # 2
126
+ # <section_name>
127
+ # ... section content ...
128
+ # 0
129
+ # ENDSEC
130
+ def section_wrap(fp, section_name, &block)
131
+ section_begin = "0#{@delimiter}SECTION#{@delimiter}2#{@delimiter}#{section_name.upcase}#{@delimiter}"
132
+ section_end = "0#{@delimiter}ENDSEC#{@delimiter}"
133
+
134
+ fp.write(section_begin)
135
+ block.call
136
+ fp.write(section_end)
137
+ end
138
+
139
+ # header format:
140
+ # 9
141
+ # $<variable>
142
+ # <group code>
143
+ # <value>
144
+ def header_wrap(fp, variables)
145
+ variables.each_pair do |variable, groups|
146
+ fp.write("9#{@delimiter}#{'$' if variable[0] != '$'}#{variable}#{@delimiter}")
147
+ groups.each_pair do |group_code, value|
148
+ fp.write("#{group_code}#{@delimiter}#{try_to_upcase_exponent(value)}#{@delimiter}")
149
+ end
150
+ end
151
+ end
152
+
153
+ # other section format:
154
+ # <group code>
155
+ # <value>
156
+ def other_section_wrap(fp, variables)
157
+ variables.each do |groups|
158
+ groups.each do |group|
159
+ fp.write("#{group.keys.first}#{@delimiter}#{try_to_upcase_exponent(group.values.first)}#{@delimiter}")
160
+ end
161
+ end
162
+ end
163
+
164
+ # content constructors
165
+
166
+ def file_content(&block)
167
+ file_end = "0#{@delimiter}EOF#{@delimiter}"
168
+
169
+ "#{block.call}#{file_end}"
170
+ end
171
+
172
+ def section_wrapper_content(section_name, &block)
173
+ section_begin = "0#{@delimiter}SECTION#{@delimiter}2#{@delimiter}#{section_name.upcase}#{@delimiter}"
174
+ section_end = "0#{@delimiter}ENDSEC#{@delimiter}"
175
+
176
+ "#{section_begin}#{block.call}#{section_end}"
177
+ end
178
+
179
+ def header_content(variables)
180
+ variables.inject('') do |result, (variable, groups)|
181
+ variable_part = "9#{@delimiter}#{'$' if variable[0] != '$'}#{variable}#{@delimiter}"
182
+ result << groups.inject(variable_part) do |group_result, (group_code, value)|
183
+ group_result << "#{group_code}#{@delimiter}#{try_to_upcase_exponent(value)}#{@delimiter}"
184
+ end
185
+ end
186
+ end
187
+
188
+ def other_section_content(variables)
189
+ variables.inject('') do |result, groups|
190
+ result << groups.inject('') do |group_result, group|
191
+ group_result << "#{group.keys.first}#{@delimiter}#{try_to_upcase_exponent(group.values.first)}#{@delimiter}"
192
+ end
193
+ end
194
+ end
195
+
196
+ # formatting
197
+
198
+ # replace exponential notation by decimal notation and remove redundant zeros in the end
199
+ def try_to_decimal_fraction(num)
200
+ if num.is_a? Float
201
+ ('%.25f' % num).to_s.sub(/0+$/, '')
202
+ else
203
+ num
204
+ end
205
+ end
206
+
207
+ def try_to_upcase_exponent(num)
208
+ if num.is_a? Float
209
+ num.to_s.sub('e', 'E')
210
+ else
211
+ num
212
+ end
213
+ end
214
+ end
215
+ end