dxf_io 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +224 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/dxf_io.gemspec +34 -0
- data/lib/dxf_io.rb +13 -0
- data/lib/dxf_io/constants.rb +24 -0
- data/lib/dxf_io/entity.rb +21 -0
- data/lib/dxf_io/entity/arc.rb +44 -0
- data/lib/dxf_io/entity/circle.rb +33 -0
- data/lib/dxf_io/entity/dimension.rb +14 -0
- data/lib/dxf_io/entity/ellipse.rb +46 -0
- data/lib/dxf_io/entity/hatch.rb +14 -0
- data/lib/dxf_io/entity/header.rb +7 -0
- data/lib/dxf_io/entity/leader.rb +14 -0
- data/lib/dxf_io/entity/line.rb +14 -0
- data/lib/dxf_io/entity/mline.rb +14 -0
- data/lib/dxf_io/entity/mtext.rb +13 -0
- data/lib/dxf_io/entity/other.rb +237 -0
- data/lib/dxf_io/entity/polyline.rb +14 -0
- data/lib/dxf_io/entity/spline.rb +13 -0
- data/lib/dxf_io/entity/support.rb +9 -0
- data/lib/dxf_io/entity/support/point.rb +114 -0
- data/lib/dxf_io/entity/text.rb +14 -0
- data/lib/dxf_io/reader.rb +139 -0
- data/lib/dxf_io/version.rb +3 -0
- data/lib/dxf_io/wrapper.rb +45 -0
- data/lib/dxf_io/writer.rb +215 -0
- metadata +122 -0
@@ -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,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,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
|