triangular 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ require "bundler/setup"
2
+ require "triangular"
3
+
4
+ solid = Triangular.parse_file("#{File.dirname(__FILE__)}/example_files/y-axis-spacer.stl")
5
+ polyline = solid.slice_at_z(-0.5)
6
+
7
+ File.open(File.expand_path("~/Desktop/slice.svg"), "w+") do |file|
8
+ file.puts polyline.to_svg(10, 10)
9
+ end
@@ -0,0 +1,19 @@
1
+ require 'triangular/point'
2
+ require 'triangular/vertex'
3
+ require 'triangular/vector'
4
+ require 'triangular/line'
5
+ require 'triangular/polyline'
6
+ require 'triangular/facet'
7
+ require 'triangular/solid'
8
+
9
+ module Triangular
10
+ def self.parse(string)
11
+ Solid.parse(string)
12
+ end
13
+
14
+ def self.parse_file(path)
15
+ File.open(path) do |file|
16
+ Solid.parse(file.read)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,81 @@
1
+ module Triangular
2
+ class Facet
3
+
4
+ attr_accessor :normal, :vertices
5
+
6
+ def initialize(normal = nil, *args)
7
+ @normal = normal
8
+ @vertices = args
9
+ end
10
+
11
+ def to_s
12
+ output = "facet normal #{@normal.to_s}\n"
13
+ output += "outer loop\n"
14
+ @vertices.each do |vertex|
15
+ output += vertex.to_s + "\n"
16
+ end
17
+ output += "endloop\n"
18
+ output += "endfacet\n"
19
+
20
+ output
21
+ end
22
+
23
+ def lines
24
+ [
25
+ Line.new(@vertices[0], @vertices[1]),
26
+ Line.new(@vertices[1], @vertices[2]),
27
+ Line.new(@vertices[2], @vertices[0])
28
+ ]
29
+ end
30
+
31
+ def intersection_at_z(z_plane)
32
+ return nil if @vertices.count{|vertex| vertex.z == z_plane} > 2
33
+
34
+ intersection_points = []
35
+ lines.each do |line|
36
+ intersection_points << line.intersection_at_z(z_plane) unless line.start.z == z_plane && line.end.z == z_plane
37
+ end
38
+
39
+ intersection_points.compact!
40
+ if intersection_points.empty?
41
+ nil
42
+ elsif intersection_points.count == 2
43
+ Line.new(intersection_points[0], intersection_points[1])
44
+ end
45
+ end
46
+
47
+ def self.parse(string)
48
+ facets = []
49
+
50
+ string.scan(self.pattern) do |match_data|
51
+ facet = self.new
52
+
53
+ facet.vertices << Vertex.parse(match_data[4])
54
+ facet.vertices << Vertex.parse(match_data[9])
55
+ facet.vertices << Vertex.parse(match_data[14])
56
+
57
+ facet.normal = Vector.parse(match_data[0])
58
+
59
+ facets << facet
60
+ end
61
+
62
+ if facets.length == 1
63
+ facets.first
64
+ else
65
+ facets
66
+ end
67
+ end
68
+
69
+ def self.pattern
70
+ /
71
+ \s* facet\snormal\s (?<normal> #{Point.pattern})\s
72
+ \s* outer\sloop\s
73
+ \s* (?<vertex1> #{Vertex.pattern})
74
+ \s* (?<vertex2> #{Vertex.pattern})
75
+ \s* (?<vertex3> #{Vertex.pattern})
76
+ \s* endloop\s
77
+ \s* endfacet\s
78
+ /x
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,38 @@
1
+ module Triangular
2
+ class Line
3
+
4
+ attr_accessor :start, :end
5
+
6
+ def initialize(line_start, line_end)
7
+ @start = line_start
8
+ @end = line_end
9
+ end
10
+
11
+ def ==(other)
12
+ return false unless other.is_a?(Line)
13
+ self.start == other.start && self.end == other.end
14
+ end
15
+
16
+ def intersects_z?(z_plane)
17
+ if (@start.z >= z_plane && @end.z <= z_plane) || (@start.z <= z_plane && @end.z >= z_plane)
18
+ true
19
+ else
20
+ false
21
+ end
22
+ end
23
+
24
+ def intersection_at_z(z_plane)
25
+ return nil if !self.intersects_z?(z_plane)
26
+ raise "Cannot calculate intersection for line that lies on the target Z plane" if @start.z == z_plane && @end.z == z_plane
27
+
28
+ x_intersect = (@end.x - @start.x) / (@end.z - @start.z) * (z_plane - @start.z) + @start.x
29
+ y_intersect = (@end.y - @start.y) / (@end.z - @start.z) * (z_plane - @start.z) + @start.y
30
+
31
+ Point.new(x_intersect, y_intersect, z_plane)
32
+ end
33
+
34
+ def to_svg_path
35
+ "<path d=\"M #{@start.x} #{@start.y} L #{@end.x} #{@end.y}\" fill=\"none\" stroke=\"black\" stroke-width=\"1\" />"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ module Triangular
2
+ class Point
3
+
4
+ attr_accessor :x, :y, :z
5
+
6
+ def initialize(x, y, z)
7
+ @x = x
8
+ @y = y
9
+ @z = z
10
+ end
11
+
12
+ def to_s
13
+ "#{@x.to_f} #{@y.to_f} #{@z.to_f}"
14
+ end
15
+
16
+ def ==(other)
17
+ return false unless other.is_a?(Point)
18
+ self.x == other.x && self.y == other.y && self.z == other.z
19
+ end
20
+
21
+ def self.parse(string)
22
+ string.strip!
23
+ match_data = string.match(self.pattern)
24
+
25
+ self.new(match_data[:x].to_f, match_data[:y].to_f, match_data[:z].to_f)
26
+ end
27
+
28
+ def self.pattern
29
+ /(?<x>-?\d+.\d+(e\-?\d+)?)\s(?<y>-?\d+.\d+(e\-?\d+)?)\s(?<z>-?\d+.\d+(e\-?\d+)?)/
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ module Triangular
2
+ class Polyline
3
+
4
+ attr_accessor :lines
5
+
6
+ def initialize(lines)
7
+ @lines = lines
8
+ end
9
+
10
+ def to_svg(x_offset = 20, y_offset = 20)
11
+ output = '<?xml version="1.0" standalone="no"?>' + "\n"
12
+ output << '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' + "\n"
13
+ output << '<svg xmlns="http://www.w3.org/2000/svg" version="1.1">' + "\n"
14
+ output << " <g transform=\"translate(#{x_offset},#{y_offset})\">\n"
15
+
16
+ @lines.each do |line|
17
+ output << " " + line.to_svg_path + "\n"
18
+ end
19
+
20
+ output << ' </g>' + "\n"
21
+ output << '</svg>'
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ module Triangular
2
+ class Solid
3
+
4
+ attr_accessor :name, :facets
5
+
6
+ def initialize(name, *args)
7
+ @name = name
8
+ @facets = args
9
+ end
10
+
11
+ def to_s
12
+ output = "solid #{@name || ""}\n"
13
+ @facets.each do |facet|
14
+ output << "facet normal #{facet.normal.x.to_f} #{facet.normal.y.to_f} #{facet.normal.z.to_f}\n"
15
+ output << "outer loop\n"
16
+ facet.vertices.each do |vertex|
17
+ output <<"vertex #{vertex.x.to_f} #{vertex.y.to_f} #{vertex.z.to_f}\n"
18
+ end
19
+ output << "endloop\n"
20
+ output << "endfacet\n"
21
+ end
22
+ output << "endsolid #{@name || ""}\n"
23
+
24
+ output
25
+ end
26
+
27
+ def slice_at_z(z_plane)
28
+ lines = @facets.map {|facet| facet.intersection_at_z(z_plane) }
29
+ lines.compact!
30
+
31
+ Polyline.new(lines)
32
+ end
33
+
34
+ def self.parse(string)
35
+ partial_pattern = /\s* solid\s+ (?<name> [a-zA-Z0-9\-\_\.]+)?/x
36
+ match_data = string.match(partial_pattern)
37
+
38
+ solid = self.new(match_data[:name])
39
+
40
+ solid.facets = Facet.parse(string.gsub(partial_pattern, ""))
41
+
42
+ solid
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,4 @@
1
+ module Triangular
2
+ class Vector < Point
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module Triangular
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,44 @@
1
+ require 'forwardable'
2
+
3
+ module Triangular
4
+ class Vertex
5
+ extend Forwardable
6
+
7
+ def_delegator :@point, :x, :x
8
+ def_delegator :@point, :y, :y
9
+ def_delegator :@point, :z, :z
10
+
11
+ attr_accessor :point
12
+
13
+ def initialize(*args)
14
+ if args.length == 1 && args.first.is_a?(Point)
15
+ @point = args.first
16
+ elsif args.length == 3
17
+ @point = Point.new(args[0], args[1], args[2])
18
+ else
19
+ raise "You must either supply the XYZ coordinates or a Point object to create a Vertex"
20
+ end
21
+ end
22
+
23
+ def to_s
24
+ "vertex #{@point.to_s}"
25
+ end
26
+
27
+ def ==(other)
28
+ return false unless other.is_a?(Vertex)
29
+ self.x == other.x && self.y == other.y && self.z == other.z
30
+ end
31
+
32
+ def self.parse(string)
33
+ string.strip!
34
+ match_data = string.match(self.pattern)
35
+
36
+ self.new(Point.parse(match_data[:point]))
37
+ end
38
+
39
+ def self.pattern
40
+ /vertex\s+ (?<point>#{Point.pattern})/x
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,257 @@
1
+ require 'spec_helper'
2
+
3
+ describe Facet do
4
+ describe ".parse" do
5
+ context "with a correctly formatted facet" do
6
+ before do
7
+ @result = Facet.parse(<<-EOD)
8
+ facet normal 0.0 0.0 -1.0
9
+ outer loop
10
+ vertex 16.5 0.0 -0.75
11
+ vertex 0.0 -9.5 -0.75
12
+ vertex 0.0 0.0 -0.75
13
+ endloop
14
+ endfacet
15
+ EOD
16
+ end
17
+
18
+ it "should return a facet object" do
19
+ @result.should be_a Facet
20
+ end
21
+
22
+ it "should return a facet with 3 vertices" do
23
+ @result.vertices.length.should == 3
24
+ end
25
+
26
+ it "should return a facet with vertices of type Vertex" do
27
+ @result.vertices.each do |vertex|
28
+ vertex.should be_a Vertex
29
+ end
30
+ end
31
+
32
+ it "should return a facet with a normal of type Vector" do
33
+ @result.normal.should be_a Vector
34
+ end
35
+
36
+ it "should correctly set the normal values" do
37
+ @result.normal.x.should == 0
38
+ @result.normal.y.should == 0
39
+ @result.normal.z.should == -1
40
+ end
41
+
42
+ it "should correctly set the values for the first vertex" do
43
+ @result.vertices[0].x.should == 16.5
44
+ @result.vertices[0].y.should == 0
45
+ @result.vertices[0].z.should == -0.75
46
+ end
47
+
48
+ it "should correctly set the values for the second vertex" do
49
+ @result.vertices[1].x.should == 0
50
+ @result.vertices[1].y.should == -9.5
51
+ @result.vertices[1].z.should == -0.75
52
+ end
53
+
54
+ it "should correctly set the values for the third vertex" do
55
+ @result.vertices[2].x.should == 0
56
+ @result.vertices[2].y.should == 0
57
+ @result.vertices[2].z.should == -0.75
58
+ end
59
+ end
60
+
61
+ context "when passed multiple facets" do
62
+ before do
63
+ @result = Facet.parse(<<-EOD)
64
+ facet normal 0.0 0.0 -1.0
65
+ outer loop
66
+ vertex 16.5 0.0 -0.75
67
+ vertex 0.0 -9.5 -0.75
68
+ vertex 0.0 0.0 -0.75
69
+ endloop
70
+ endfacet
71
+ facet normal 0.0 0.0 -1.0
72
+ outer loop
73
+ vertex 16.5 0.0 -0.75
74
+ vertex 0.0 -9.5 -0.75
75
+ vertex 0.0 0.0 -0.75
76
+ endloop
77
+ endfacet
78
+ EOD
79
+ end
80
+
81
+ it "should return multiple facet objects" do
82
+ @result.should be_a Array
83
+ @result.each do |item|
84
+ item.should be_a Facet
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ describe "#to_s" do
91
+ it "should return the string representation for a facet" do
92
+ facet = Facet.new
93
+ facet.normal = Vector.new(0, 0, 1)
94
+ facet.vertices << Point.new(1, 2, 3)
95
+ facet.vertices << Point.new(1, 2, 3)
96
+ facet.vertices << Point.new(1, 2, 3)
97
+
98
+ expected_string = "facet normal 0.0 0.0 1.0\n"
99
+ expected_string += "outer loop\n"
100
+ expected_string += facet.vertices[0].to_s + "\n"
101
+ expected_string += facet.vertices[1].to_s + "\n"
102
+ expected_string += facet.vertices[2].to_s + "\n"
103
+ expected_string += "endloop\n"
104
+ expected_string += "endfacet\n"
105
+
106
+ facet.to_s.should == expected_string
107
+ end
108
+ end
109
+
110
+ describe "#intersection_at_z" do
111
+ context "for a facet that intersects the target Z plane" do
112
+ before do
113
+ vertex1 = Vertex.new(0.0, 0.0, 0.0)
114
+ vertex2 = Vertex.new(0.0, 0.0, 6.0)
115
+ vertex3 = Vertex.new(6.0, 0.0, 6.0)
116
+
117
+ @facet = Facet.new(nil, vertex1, vertex2, vertex3)
118
+ end
119
+
120
+ context "when the target Z plane is 3.0" do
121
+ it "should return a line object" do
122
+ @facet.intersection_at_z(3.0).should be_a Line
123
+ end
124
+
125
+ it "should return a line with the correct start value" do
126
+ @facet.intersection_at_z(3.0).start.x.should == 0.0
127
+ @facet.intersection_at_z(3.0).start.y.should == 0.0
128
+ @facet.intersection_at_z(3.0).start.z.should == 3.0
129
+ end
130
+
131
+ it "should return a line with the correct end value" do
132
+ @facet.intersection_at_z(3.0).end.x.should == 3.0
133
+ @facet.intersection_at_z(3.0).end.y.should == 0.0
134
+ @facet.intersection_at_z(3.0).end.z.should == 3.0
135
+ end
136
+ end
137
+
138
+ context "when the target Z plane is 6.0" do
139
+ it "should return a line object" do
140
+ @facet.intersection_at_z(6.0).should be_a Line
141
+ end
142
+
143
+ it "should return a line with the correct start value" do
144
+ @facet.intersection_at_z(6.0).start.x.should == 0.0
145
+ @facet.intersection_at_z(6.0).start.y.should == 0.0
146
+ @facet.intersection_at_z(6.0).start.z.should == 6.0
147
+ end
148
+
149
+ it "should return a line with the correct end value" do
150
+ @facet.intersection_at_z(6.0).end.x.should == 6.0
151
+ @facet.intersection_at_z(6.0).end.y.should == 0.0
152
+ @facet.intersection_at_z(6.0).end.z.should == 6.0
153
+ end
154
+ end
155
+ end
156
+
157
+ context "for a facet that intersects the target Z plane at an angle" do
158
+ before do
159
+ vertex1 = Vertex.new(0.0, 0.0, 0.0)
160
+ vertex2 = Vertex.new(0.0, 6.0, 6.0)
161
+ vertex3 = Vertex.new(6.0, 6.0, 6.0)
162
+
163
+ @facet = Facet.new(nil, vertex1, vertex2, vertex3)
164
+ end
165
+
166
+ context "when the target Z plane is 3.0" do
167
+ it "should return a line object" do
168
+ @facet.intersection_at_z(3.0).should be_a Line
169
+ end
170
+
171
+ it "should return a line with the correct start value" do
172
+ @facet.intersection_at_z(3.0).start.x.should == 0.0
173
+ @facet.intersection_at_z(3.0).start.y.should == 3.0
174
+ @facet.intersection_at_z(3.0).start.z.should == 3.0
175
+ end
176
+
177
+ it "should return a line with the correct end value" do
178
+ @facet.intersection_at_z(3.0).end.x.should == 3.0
179
+ @facet.intersection_at_z(3.0).end.y.should == 3.0
180
+ @facet.intersection_at_z(3.0).end.z.should == 3.0
181
+ end
182
+ end
183
+
184
+ context "when the target Z plane is 6.0" do
185
+ it "should return a line object" do
186
+ @facet.intersection_at_z(6.0).should be_a Line
187
+ end
188
+
189
+ it "should return a line with the correct start value" do
190
+ @facet.intersection_at_z(6.0).start.x.should == 0.0
191
+ @facet.intersection_at_z(6.0).start.y.should == 6.0
192
+ @facet.intersection_at_z(6.0).start.z.should == 6.0
193
+ end
194
+
195
+ it "should return a line with the correct end value" do
196
+ @facet.intersection_at_z(6.0).end.x.should == 6.0
197
+ @facet.intersection_at_z(6.0).end.y.should == 6.0
198
+ @facet.intersection_at_z(6.0).end.z.should == 6.0
199
+ end
200
+ end
201
+ end
202
+
203
+ context "with vertices in both positive and negative space" do
204
+ before do
205
+ @facet = Facet.parse(<<-EOD)
206
+ facet normal -0.0 1.0 -0.0
207
+ outer loop
208
+ vertex -1.0 1.0 1.0
209
+ vertex 1.0 1.0 -1.0
210
+ vertex -1.0 1.0 -1.0
211
+ endloop
212
+ endfacet
213
+ EOD
214
+ end
215
+
216
+ it "should return a line with the correct start value" do
217
+ @facet.intersection_at_z(0.0).start.x.should == 0.0
218
+ @facet.intersection_at_z(0.0).start.y.should == 1.0
219
+ @facet.intersection_at_z(0.0).start.z.should == 0.0
220
+ end
221
+
222
+ it "should return a line with the correct end value" do
223
+ @facet.intersection_at_z(0.0).end.x.should == -1.0
224
+ @facet.intersection_at_z(0.0).end.y.should == 1.0
225
+ @facet.intersection_at_z(0.0).end.z.should == 0.0
226
+ end
227
+ end
228
+
229
+ context "for a facet that lies on the target Z plane" do
230
+ before do
231
+ vertex1 = Vertex.new(0.0, 0.0, 1.0)
232
+ vertex2 = Vertex.new(2.0, 0.0, 1.0)
233
+ vertex3 = Vertex.new(2.0, 2.0, 1.0)
234
+
235
+ @facet = Facet.new(nil, vertex1, vertex2, vertex3)
236
+ end
237
+
238
+ it "should return nil" do
239
+ @facet.intersection_at_z(1.0).should == nil
240
+ end
241
+ end
242
+
243
+ context "for a facet that does not intersect the target Z plane" do
244
+ before do
245
+ vertex1 = Vertex.new(0.0, 0.0, 0.0)
246
+ vertex2 = Vertex.new(2.0, 0.0, 0.0)
247
+ vertex3 = Vertex.new(2.0, 2.0, 0.0)
248
+
249
+ @facet = Facet.new(nil, vertex1, vertex2, vertex3)
250
+ end
251
+
252
+ it "should return nil" do
253
+ @facet.intersection_at_z(1.0).should == nil
254
+ end
255
+ end
256
+ end
257
+ end