triangular 0.0.1

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.
@@ -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