dxf_io 0.1.0
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.
- 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
|