dxf 0.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +11 -0
- data/Gemfile +7 -1
- data/README.markdown +3 -1
- data/Rakefile +2 -0
- data/dxf.gemspec +6 -3
- data/lib/dxf.rb +10 -94
- data/lib/dxf/cluster_factory.rb +15 -0
- data/lib/dxf/entity.rb +169 -0
- data/lib/dxf/parser.rb +248 -0
- data/lib/dxf/unparser.rb +109 -0
- data/test/dxf/parser.rb +48 -0
- data/test/dxf/{builder.rb → unparser.rb} +84 -15
- data/test/fixtures/circle.dxf +24 -0
- data/test/fixtures/circle_translate.dxf +24 -0
- data/test/fixtures/spline.dxf +5098 -0
- data/test/fixtures/square_lwpolyline_inches.dxf +38 -0
- data/test/fixtures/square_lwpolyline_millimeters.dxf +38 -0
- metadata +50 -26
checksums.yaml
ADDED
@@ -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
|
data/.travis.yml
ADDED
@@ -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
data/README.markdown
CHANGED
@@ -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-
|
33
|
+
Copyright 2012-2014 Brandon Fosdick <bfoz@bfoz.net> and released under the BSD license.
|
data/Rakefile
CHANGED
data/dxf.gemspec
CHANGED
@@ -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.
|
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
|
-
|
20
|
-
gem.add_dependency
|
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
|
-
|
2
|
-
|
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.
|
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
|
data/lib/dxf/entity.rb
ADDED
@@ -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
|
data/lib/dxf/parser.rb
ADDED
@@ -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
|