tbd 3.0.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/.gitattributes +3 -0
- data/.github/workflows/pull_request.yml +72 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/Gemfile +3 -0
- data/LICENSE.md +21 -0
- data/README.md +154 -0
- data/Rakefile +60 -0
- data/json/midrise.json +64 -0
- data/json/tbd_5ZoneNoHVAC.json +19 -0
- data/json/tbd_5ZoneNoHVAC_btap.json +91 -0
- data/json/tbd_seb_n2.json +41 -0
- data/json/tbd_seb_n4.json +57 -0
- data/json/tbd_warehouse10.json +24 -0
- data/json/tbd_warehouse5.json +37 -0
- data/lib/measures/tbd/LICENSE.md +21 -0
- data/lib/measures/tbd/README.md +136 -0
- data/lib/measures/tbd/README.md.erb +42 -0
- data/lib/measures/tbd/docs/.gitkeep +1 -0
- data/lib/measures/tbd/measure.rb +327 -0
- data/lib/measures/tbd/measure.xml +460 -0
- data/lib/measures/tbd/resources/geo.rb +714 -0
- data/lib/measures/tbd/resources/geometry.rb +351 -0
- data/lib/measures/tbd/resources/model.rb +1431 -0
- data/lib/measures/tbd/resources/oslog.rb +381 -0
- data/lib/measures/tbd/resources/psi.rb +2229 -0
- data/lib/measures/tbd/resources/tbd.rb +55 -0
- data/lib/measures/tbd/resources/transformation.rb +121 -0
- data/lib/measures/tbd/resources/ua.rb +986 -0
- data/lib/measures/tbd/resources/utils.rb +1636 -0
- data/lib/measures/tbd/resources/version.rb +3 -0
- data/lib/measures/tbd/tests/tbd_full_PSI.json +17 -0
- data/lib/measures/tbd/tests/tbd_tests.rb +222 -0
- data/lib/tbd/geo.rb +714 -0
- data/lib/tbd/psi.rb +2229 -0
- data/lib/tbd/ua.rb +986 -0
- data/lib/tbd/version.rb +25 -0
- data/lib/tbd.rb +93 -0
- data/sponsors/canada.png +0 -0
- data/sponsors/quebec.png +0 -0
- data/tbd.gemspec +43 -0
- data/tbd.schema.json +571 -0
- data/v291_MacOS.md +110 -0
- metadata +191 -0
@@ -0,0 +1,1431 @@
|
|
1
|
+
begin
|
2
|
+
require "topolys/version"
|
3
|
+
rescue LoadError
|
4
|
+
require File.join(File.dirname(__FILE__), 'version.rb')
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'json'
|
8
|
+
require 'securerandom'
|
9
|
+
require 'set'
|
10
|
+
|
11
|
+
# Topology represents connections between geometry in a model.
|
12
|
+
#
|
13
|
+
# Class structure inspired from Topologic's RCH (Rigorous Class Hierarchy)
|
14
|
+
# (excerpts from https://topologic.app/Software/)
|
15
|
+
# Topology: Abstract superclass holding constructors, properties and
|
16
|
+
# methods used by other subclasses that extend it.
|
17
|
+
# Vertex: 1D entity equivalent to a geometry point.
|
18
|
+
# Edge: 1D entity defined by two vertices.
|
19
|
+
# Wire: Contiguous collection of Edges where adjacent Edges are
|
20
|
+
# connected by shared Vertices.
|
21
|
+
# Face: 2D region defined by a collection of closed Wires.
|
22
|
+
# Shell: Contiguous collection of Faces, where adjacent Faces are
|
23
|
+
# connected by shared Edges.
|
24
|
+
# Cell: 3D region defined by a collection of closed Shells.
|
25
|
+
# CellComplex: Contiguous collection of Cells where adjacent Cells are
|
26
|
+
# connected by shared Faces.
|
27
|
+
# Cluster: Collection of any topologic entities.
|
28
|
+
|
29
|
+
# TODO : start integrating warning logs à la raise ...
|
30
|
+
module Topolys
|
31
|
+
|
32
|
+
@@normal_tol = 0.000001
|
33
|
+
@@planar_tol = 0.01
|
34
|
+
|
35
|
+
## Tolerance for normal vector checks
|
36
|
+
def Topolys.normal_tol
|
37
|
+
@@normal_tol
|
38
|
+
end
|
39
|
+
|
40
|
+
## Tolerance for planarity checks
|
41
|
+
def Topolys.planar_tol
|
42
|
+
@@planar_tol
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Checks if one array of objects is the same as another array of objects.
|
47
|
+
# The order of objects must be the same but the two arrays may start at different indices.
|
48
|
+
#
|
49
|
+
# @param [Array] objects1 Array
|
50
|
+
# @param [Array] objects2 Array
|
51
|
+
#
|
52
|
+
# @return [Integer] Returns offset between objects2 and objects1 or nil
|
53
|
+
def Topolys.find_offset(objects1, objects2)
|
54
|
+
|
55
|
+
n = objects1.size
|
56
|
+
return nil if objects2.size != n
|
57
|
+
|
58
|
+
offset = objects2.index{|obj| objects1[0].id == obj.id}
|
59
|
+
return nil if !offset
|
60
|
+
|
61
|
+
objects1.each_index do |i|
|
62
|
+
return nil if objects1[i].id != objects2[(offset+i)%n].id
|
63
|
+
end
|
64
|
+
|
65
|
+
return offset
|
66
|
+
end
|
67
|
+
|
68
|
+
# The Topolys Model contains many Topolys Objects, a Topolys Object can only be
|
69
|
+
# connected to other Topolys Objects in the same Topolys Model. To enforce this
|
70
|
+
# Topolys Objects should not be constructed directly, they should be retrieved using
|
71
|
+
# the Topolys Model get_* object methods.
|
72
|
+
class Model
|
73
|
+
attr_reader :vertices, :edges, :directed_edges, :wires, :faces, :shells, :cells
|
74
|
+
attr_reader :tol, :tol2
|
75
|
+
|
76
|
+
def initialize(tol=nil)
|
77
|
+
|
78
|
+
# changing tolerance on a model after construction would be very complicated
|
79
|
+
# you would have to go through and regroup points, etc
|
80
|
+
if !tol.is_a?(Numeric)
|
81
|
+
tol = 0.01
|
82
|
+
end
|
83
|
+
@tol = tol
|
84
|
+
@tol2 = @tol**2
|
85
|
+
|
86
|
+
@vertices = []
|
87
|
+
@edges = []
|
88
|
+
@directed_edges = []
|
89
|
+
@wires = []
|
90
|
+
@faces = []
|
91
|
+
@shells = []
|
92
|
+
@cells = []
|
93
|
+
end
|
94
|
+
|
95
|
+
def all_objects
|
96
|
+
@vertices + @edges + @directed_edges + @wires + @faces + @shells + @cells
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_json
|
100
|
+
result= {
|
101
|
+
vertices: @vertices.map { |v| v.to_json },
|
102
|
+
edges: @edges.map { |e| e.to_json },
|
103
|
+
directed_edges: @directed_edges.map { |de| de.to_json },
|
104
|
+
wires: @wires.map { |w| w.to_json },
|
105
|
+
faces: @faces.map { |f| f.to_json },
|
106
|
+
shells: @shells.map { |s| s.to_json },
|
107
|
+
cells: @cells.map { |c| c.to_json }
|
108
|
+
}
|
109
|
+
return result
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.from_json(obj)
|
113
|
+
model = Model.new
|
114
|
+
id_map = {}
|
115
|
+
|
116
|
+
obj[:vertices].each do |v|
|
117
|
+
p = v[:point]
|
118
|
+
point = Point3D.new(p[:x], p[:y], p[:z])
|
119
|
+
vertex = model.get_vertex(point)
|
120
|
+
set_id(vertex, v[:id])
|
121
|
+
vertex.attributes = v[:attributes] if v[:attributes]
|
122
|
+
id_map[v[:id]] = vertex
|
123
|
+
end
|
124
|
+
|
125
|
+
obj[:edges].each do |e|
|
126
|
+
v0 = id_map[e[:v0]]
|
127
|
+
v1 = id_map[e[:v1]]
|
128
|
+
edge = model.get_edge(v0, v1)
|
129
|
+
set_id(edge, e[:id])
|
130
|
+
edge.attributes = e[:attributes] if e[:attributes]
|
131
|
+
id_map[e[:id]] = edge
|
132
|
+
end
|
133
|
+
|
134
|
+
obj[:directed_edges].each do |de|
|
135
|
+
edge = id_map[de[:edge]]
|
136
|
+
inverted = de[:inverted]
|
137
|
+
directed_edge = nil
|
138
|
+
if inverted
|
139
|
+
directed_edge = model.get_directed_edge(edge.v1, edge.v0)
|
140
|
+
else
|
141
|
+
directed_edge = model.get_directed_edge(edge.v0, edge.v1)
|
142
|
+
end
|
143
|
+
set_id(directed_edge, de[:id])
|
144
|
+
directed_edge.attributes = de[:attributes] if de[:attributes]
|
145
|
+
id_map[de[:id]] = directed_edge
|
146
|
+
end
|
147
|
+
|
148
|
+
obj[:wires].each do |w|
|
149
|
+
vertices = []
|
150
|
+
w[:directed_edges].each do |id|
|
151
|
+
directed_edge = id_map[id]
|
152
|
+
vertices << directed_edge.v0
|
153
|
+
end
|
154
|
+
wire = model.get_wire(vertices)
|
155
|
+
set_id(wire, w[:id])
|
156
|
+
wire.attributes = w[:attributes] if w[:attributes]
|
157
|
+
id_map[w[:id]] = wire
|
158
|
+
end
|
159
|
+
|
160
|
+
obj[:faces].each do |f|
|
161
|
+
outer = id_map[f[:outer]]
|
162
|
+
holes = []
|
163
|
+
f[:holes].each do |id|
|
164
|
+
holes << id_map[id]
|
165
|
+
end
|
166
|
+
face = model.get_face(outer, holes)
|
167
|
+
set_id(face, f[:id])
|
168
|
+
face.attributes = f[:attributes] if f[:attributes]
|
169
|
+
id_map[f[:id]] = face
|
170
|
+
end
|
171
|
+
|
172
|
+
obj[:shells].each do |s|
|
173
|
+
faces = []
|
174
|
+
s[:faces].each do |id|
|
175
|
+
faces << id_map[id]
|
176
|
+
end
|
177
|
+
shell = model.get_shell(faces)
|
178
|
+
set_id(shell, s[:id])
|
179
|
+
shell.attributes = s[:attributes] if s[:attributes]
|
180
|
+
id_map[s[:id]] = shell
|
181
|
+
end
|
182
|
+
|
183
|
+
return model
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.schema_file
|
187
|
+
return File.join(File.dirname(__FILE__), "./schema/topolys.json")
|
188
|
+
end
|
189
|
+
|
190
|
+
def self.schema
|
191
|
+
s = File.read(schema_file)
|
192
|
+
return JSON.parse(s)
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_s
|
196
|
+
JSON.pretty_generate(to_json)
|
197
|
+
end
|
198
|
+
|
199
|
+
def save(file)
|
200
|
+
File.open(file, 'w') do |file|
|
201
|
+
file.puts self.to_s
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def to_graphviz
|
206
|
+
result = "digraph model {\n"
|
207
|
+
result += " rankdir=LR\n"
|
208
|
+
all_objects.each do |obj|
|
209
|
+
obj.children.each { |child| result += " #{child.short_name} -> #{obj.short_name}\n" }
|
210
|
+
#obj.parents.each { |parent| result += " #{parent.short_name} -> #{obj.short_name}\n" }
|
211
|
+
end
|
212
|
+
result += " }"
|
213
|
+
|
214
|
+
return result
|
215
|
+
end
|
216
|
+
|
217
|
+
def save_graphviz(file)
|
218
|
+
File.open(file, 'w') do |file|
|
219
|
+
file.puts to_graphviz
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# @param [Vertex] vertex
|
224
|
+
# @param [Edge] edge
|
225
|
+
# @return [Point3D] Point3D of vertex projected on edge or nil
|
226
|
+
def vertex_intersect_edge(vertex, edge)
|
227
|
+
if vertex.id == edge.v0.id || vertex.id == edge.v1.id
|
228
|
+
return nil
|
229
|
+
end
|
230
|
+
|
231
|
+
# new_point, length - length unused here
|
232
|
+
new_point, _ = project_point_on_edge(edge.v0.point, edge.v1.point, vertex.point)
|
233
|
+
|
234
|
+
return new_point
|
235
|
+
end
|
236
|
+
|
237
|
+
# @param [Point3D] point
|
238
|
+
# @return [Vertex] Vertex
|
239
|
+
def get_vertex(point)
|
240
|
+
# search for point and return corresponding vertex if it exists
|
241
|
+
v = find_existing_vertex(point)
|
242
|
+
return v if v
|
243
|
+
|
244
|
+
# otherwise create new vertex
|
245
|
+
v = Vertex.new(point)
|
246
|
+
@vertices << v
|
247
|
+
|
248
|
+
# check if this vertex needs to be inserted on any edge
|
249
|
+
updated = false
|
250
|
+
@edges.each do |edge|
|
251
|
+
if new_point = vertex_intersect_edge(v, edge)
|
252
|
+
|
253
|
+
# point might need to be added to multiple edges
|
254
|
+
# point can be updated to project it onto edge, don't update multiple times
|
255
|
+
if !updated
|
256
|
+
# simulate friend access to set point on vertex
|
257
|
+
v.instance_variable_set(:@point, new_point)
|
258
|
+
v.recalculate
|
259
|
+
updated = true
|
260
|
+
end
|
261
|
+
|
262
|
+
# now split the edge with this vertex
|
263
|
+
split_edge(edge, v)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
return v
|
268
|
+
end
|
269
|
+
|
270
|
+
# @param [Point3D] point
|
271
|
+
# @return [Vertex] Vertex or nil
|
272
|
+
def find_existing_vertex(point)
|
273
|
+
# search for point and return corresponding vertex if it exists
|
274
|
+
# otherwise create new vertex
|
275
|
+
@vertices.each do |v|
|
276
|
+
p = v.point
|
277
|
+
|
278
|
+
## L1 norm
|
279
|
+
#if ((p.x-point.x).abs < @tol) &&
|
280
|
+
# (p.y-point.y).abs < @tol) &&
|
281
|
+
# (p.z-point.z).abs < @tol))
|
282
|
+
# return v
|
283
|
+
#end
|
284
|
+
|
285
|
+
# L2 norm
|
286
|
+
if ((p.x-point.x)**2 + (p.y-point.y)**2 + (p.z-point.z)**2) < @tol2
|
287
|
+
return v
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
return nil
|
292
|
+
end
|
293
|
+
|
294
|
+
# @param [Array] points Array of Point3D
|
295
|
+
# @return [Array] Array of Vertex
|
296
|
+
def get_vertices(points)
|
297
|
+
points.map {|p| get_vertex(p)}
|
298
|
+
end
|
299
|
+
|
300
|
+
# @param [Vertex] v0
|
301
|
+
# @param [Vertex] v1
|
302
|
+
# @return [Edge] Edge
|
303
|
+
def get_edge(v0, v1)
|
304
|
+
# search for edge and return if it exists
|
305
|
+
e = find_existing_edge(v0, v1)
|
306
|
+
return e if e
|
307
|
+
|
308
|
+
# otherwise create new edge
|
309
|
+
@vertices << v0 if !@vertices.find {|v| v.id == v0.id}
|
310
|
+
@vertices << v1 if !@vertices.find {|v| v.id == v1.id}
|
311
|
+
|
312
|
+
edge = Edge.new(v0, v1)
|
313
|
+
@edges << edge
|
314
|
+
return edge
|
315
|
+
end
|
316
|
+
|
317
|
+
# @param [Vertex] v0
|
318
|
+
# @param [Vertex] v1
|
319
|
+
# @return [Edge] Edge or nil
|
320
|
+
def find_existing_edge(v0, v1)
|
321
|
+
@edges.each do |e|
|
322
|
+
if (e.v0.id == v0.id) && (e.v1.id == v1.id)
|
323
|
+
return e
|
324
|
+
elsif (e.v0.id == v1.id) && (e.v1.id == v0.id)
|
325
|
+
return e
|
326
|
+
end
|
327
|
+
end
|
328
|
+
return nil
|
329
|
+
end
|
330
|
+
|
331
|
+
# @param [Vertex] v0
|
332
|
+
# @param [Vertex] v1
|
333
|
+
# @return [DirectedEdge] DirectedEdge
|
334
|
+
def get_directed_edge(v0, v1)
|
335
|
+
# search for directed edge and return if it exists
|
336
|
+
de = find_existing_directed_edge(v0, v1)
|
337
|
+
return de if de
|
338
|
+
|
339
|
+
# otherwise create new directed edge
|
340
|
+
edge = get_edge(v0, v1)
|
341
|
+
|
342
|
+
inverted = false
|
343
|
+
if (edge.v0.id != v0.id)
|
344
|
+
inverted = true
|
345
|
+
end
|
346
|
+
|
347
|
+
directed_edge = DirectedEdge.new(edge, inverted)
|
348
|
+
@directed_edges << directed_edge
|
349
|
+
return directed_edge
|
350
|
+
end
|
351
|
+
|
352
|
+
# @param [Vertex] v0
|
353
|
+
# @param [Vertex] v1
|
354
|
+
# @return [DirectedEdge] DirectedEdge
|
355
|
+
def find_existing_directed_edge(v0, v1)
|
356
|
+
# search for directed edge and return if it exists
|
357
|
+
@directed_edges.each do |de|
|
358
|
+
if (de.v0.id == v0.id) && (de.v1.id == v1.id)
|
359
|
+
return de
|
360
|
+
end
|
361
|
+
end
|
362
|
+
return nil
|
363
|
+
end
|
364
|
+
|
365
|
+
# @param [Array] vertices Array of Vertex, assumes closed wire (e.g. first vertex is also last vertex)
|
366
|
+
# @return [Wire] Wire
|
367
|
+
def get_wire(vertices)
|
368
|
+
# search for wire and return if exists
|
369
|
+
# otherwise create new wire
|
370
|
+
|
371
|
+
# insert any existing model vertices that should be inserted on the edges in vertices
|
372
|
+
vertices = insert_vertices_on_edges(vertices)
|
373
|
+
|
374
|
+
n = vertices.size
|
375
|
+
directed_edges = []
|
376
|
+
vertices.each_index do |i|
|
377
|
+
directed_edges << get_directed_edge(vertices[i], vertices[(i+1)%n])
|
378
|
+
end
|
379
|
+
|
380
|
+
# see if we already have this wire
|
381
|
+
@wires.each do |wire|
|
382
|
+
if wire.circular_equal?(directed_edges)
|
383
|
+
return wire
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
wire = nil
|
388
|
+
begin
|
389
|
+
wire = Wire.new(directed_edges)
|
390
|
+
@wires << wire
|
391
|
+
rescue => exception
|
392
|
+
puts exception
|
393
|
+
end
|
394
|
+
|
395
|
+
return wire
|
396
|
+
end
|
397
|
+
|
398
|
+
# @param [Wire] outer Outer wire
|
399
|
+
# @param [Array] holes Array of Wire
|
400
|
+
# @return [Face] Face Returns Face or nil if wires are not in model
|
401
|
+
def get_face(outer, holes)
|
402
|
+
# search for face and return if exists
|
403
|
+
# otherwise create new face
|
404
|
+
|
405
|
+
hole_ids = holes.map{|h| h.id}.sort
|
406
|
+
@faces.each do |face|
|
407
|
+
if face.outer.id == outer.id
|
408
|
+
if face.holes.map{|h| h.id}.sort == hole_ids
|
409
|
+
return face
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# all the wires have to be in the model
|
415
|
+
return nil if @wires.index{|w| w.id == outer.id}.nil?
|
416
|
+
holes.each do |hole|
|
417
|
+
return nil if @wires.index{|w| w.id == outer.id}.nil?
|
418
|
+
end
|
419
|
+
|
420
|
+
face = nil
|
421
|
+
begin
|
422
|
+
face = Face.new(outer, holes)
|
423
|
+
@faces << face
|
424
|
+
rescue => exception
|
425
|
+
puts exception
|
426
|
+
end
|
427
|
+
|
428
|
+
return face
|
429
|
+
end
|
430
|
+
|
431
|
+
# @param [Array] faces Array of Face
|
432
|
+
# @return [Shell] Returns Shell or nil if faces are not in model or not connected
|
433
|
+
def get_shell(faces)
|
434
|
+
|
435
|
+
# all the faces have to be in the model
|
436
|
+
faces.each do |face|
|
437
|
+
return nil if @faces.index{|f| f.id == face.id}.nil?
|
438
|
+
end
|
439
|
+
|
440
|
+
# check if we already have this shell
|
441
|
+
face_ids = faces.map{|f| f.id}.sort
|
442
|
+
@shells.each do |shell|
|
443
|
+
if shell.faces.map{|f| f.id}.sort == face_ids
|
444
|
+
return shell
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
# create a new shell
|
449
|
+
shell = nil
|
450
|
+
begin
|
451
|
+
shell = Shell.new(faces)
|
452
|
+
@shells << shell
|
453
|
+
rescue => exception
|
454
|
+
puts exception
|
455
|
+
end
|
456
|
+
|
457
|
+
return shell
|
458
|
+
end
|
459
|
+
|
460
|
+
# @param [Object] object Object
|
461
|
+
# @return [Object] Returns reversed object
|
462
|
+
def get_reverse(object)
|
463
|
+
if object.is_a?(Vertex)
|
464
|
+
return object
|
465
|
+
elsif object.is_a?(Edge)
|
466
|
+
return object
|
467
|
+
elsif object.is_a?(DirectedEdge)
|
468
|
+
return get_directed_edge(object.v1, object.v0)
|
469
|
+
elsif object.is_a?(Wire)
|
470
|
+
return get_wire(object.vertices.reverse)
|
471
|
+
elsif object.is_a?(Face)
|
472
|
+
reverse_outer = get_wire(object.outer.vertices.reverse)
|
473
|
+
reverse_holes = []
|
474
|
+
object.holes.each do |hole|
|
475
|
+
reverse_holes << get_wire(hole.vertices.reverse)
|
476
|
+
end
|
477
|
+
return get_face(reverse_outer, reverse_holes)
|
478
|
+
elsif object.is_a?(Shell)
|
479
|
+
# can't reverse a shell
|
480
|
+
return nil
|
481
|
+
end
|
482
|
+
|
483
|
+
return nil
|
484
|
+
end
|
485
|
+
|
486
|
+
private
|
487
|
+
|
488
|
+
##
|
489
|
+
# Set id on an object, used in deserialization
|
490
|
+
#
|
491
|
+
# @param [Object] obj Object to modify
|
492
|
+
# @param [String] id New id
|
493
|
+
def self.set_id(obj, id)
|
494
|
+
# simulate friend access to set id on object
|
495
|
+
obj.instance_variable_set(:@id, id)
|
496
|
+
end
|
497
|
+
|
498
|
+
##
|
499
|
+
# Inserts existing model vertices that should be included in vertices
|
500
|
+
#
|
501
|
+
# @param [Array] Array of original vertices
|
502
|
+
# @return [Array] Array with inserted model vertices
|
503
|
+
def insert_vertices_on_edges(vertices)
|
504
|
+
|
505
|
+
bb = BoundingBox.new
|
506
|
+
ids = ::Set.new
|
507
|
+
vertices.each do |vertex|
|
508
|
+
bb.add_point(vertex.point)
|
509
|
+
ids.add(vertex.id)
|
510
|
+
end
|
511
|
+
|
512
|
+
# find vertices that might be inserted
|
513
|
+
vertices_to_check = []
|
514
|
+
@vertices.each do |vertex|
|
515
|
+
next if ids.include?(vertex.id)
|
516
|
+
|
517
|
+
if bb.include?(vertex.point)
|
518
|
+
vertices_to_check << vertex
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
# temporarily close vertices
|
523
|
+
vertices << vertices[0]
|
524
|
+
|
525
|
+
# check if any vertices need to be inserted on this edge
|
526
|
+
new_vertices = []
|
527
|
+
(0...vertices.size-1).each do |i|
|
528
|
+
v_this = vertices[i]
|
529
|
+
v_next = vertices[i+1]
|
530
|
+
|
531
|
+
new_vertices << v_this
|
532
|
+
|
533
|
+
vertices_to_add = []
|
534
|
+
vertices_to_check.each do |vertex|
|
535
|
+
new_point, length = project_point_on_edge(v_this.point, v_next.point, vertex.point)
|
536
|
+
if new_point
|
537
|
+
vertices_to_add << {vertex: vertex, new_point: new_point, length: length}
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
vertices_to_add.sort! { |x, y| x[:length] <=> y[:length] }
|
542
|
+
|
543
|
+
vertices_to_add.each { |vs| new_vertices << vs[:vertex] }
|
544
|
+
end
|
545
|
+
|
546
|
+
new_vertices << vertices[-1]
|
547
|
+
|
548
|
+
# pop the last vertex
|
549
|
+
vertices.pop
|
550
|
+
new_vertices.pop
|
551
|
+
|
552
|
+
# DLM: it's possible that inserting the vertices on the edge would make the face non-planar
|
553
|
+
# but if we move the vertices that could break other surfaces
|
554
|
+
|
555
|
+
#if vertices.size != new_vertices.size
|
556
|
+
# puts "old vertices"
|
557
|
+
# puts vertices.map {|v| v.point.to_s }
|
558
|
+
# puts "new vertices"
|
559
|
+
# puts new_vertices.map {|v| v.point.to_s }
|
560
|
+
#end
|
561
|
+
|
562
|
+
return new_vertices
|
563
|
+
end
|
564
|
+
|
565
|
+
# @param [Point3D] p0 Point3D at beginning of edge
|
566
|
+
# @param [Point3D] p1 Point3D at end of edge
|
567
|
+
# @param [Point3D] p Point3D to project onto edge
|
568
|
+
# @return [Point3D] new point projected onto edge or nil
|
569
|
+
# @return [Numeric] length of new point projected along edge or nil
|
570
|
+
def project_point_on_edge(p0, p1, p)
|
571
|
+
vector1 = (p1 - p0)
|
572
|
+
edge_length = vector1.magnitude
|
573
|
+
vector1.normalize!
|
574
|
+
|
575
|
+
vector2 = (p - p0)
|
576
|
+
|
577
|
+
length = vector1.dot(vector2)
|
578
|
+
if length < 0 || length > edge_length
|
579
|
+
return nil, nil
|
580
|
+
end
|
581
|
+
|
582
|
+
new_point = p0 + (vector1*length)
|
583
|
+
|
584
|
+
distance = (p - new_point).magnitude
|
585
|
+
if distance > @tol
|
586
|
+
return nil, nil
|
587
|
+
end
|
588
|
+
|
589
|
+
return new_point, length
|
590
|
+
end
|
591
|
+
|
592
|
+
##
|
593
|
+
# Adds new vertex between edges's v0 and v1, edge now goes from
|
594
|
+
# v0 to new vertex and a new_edge goes from new vertex to v1.
|
595
|
+
# Updates directed edges which reference edge.
|
596
|
+
#
|
597
|
+
# @param [Edge] edge Edge to modify
|
598
|
+
# @param [Vertex] new_vertex Vertex to add
|
599
|
+
def split_edge(edge, new_vertex)
|
600
|
+
v1 = edge.v1
|
601
|
+
|
602
|
+
# simulate friend access to set v1 on edge
|
603
|
+
edge.instance_variable_set(:@v1, new_vertex)
|
604
|
+
edge.recalculate
|
605
|
+
|
606
|
+
# make a new edge
|
607
|
+
new_edge = get_edge(new_vertex, v1)
|
608
|
+
|
609
|
+
# update the directed edges referencing this edge
|
610
|
+
parents = edge.parents.dup
|
611
|
+
parents.each do |directed_edge|
|
612
|
+
split_directed_edge(directed_edge, new_edge)
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
##
|
617
|
+
# Creates a new directed edge in same direction for the new edge.
|
618
|
+
# Updates wires which reference directed edge.
|
619
|
+
#
|
620
|
+
# @param [DirectedEdge] directed_edge Existing directed edge
|
621
|
+
# @param [Edge] new_edge New edge
|
622
|
+
def split_directed_edge(directed_edge, new_edge)
|
623
|
+
|
624
|
+
# directed edge is pointing to the new updated edge
|
625
|
+
directed_edge.recalculate
|
626
|
+
|
627
|
+
# make a new directed edge for the new edge
|
628
|
+
offset = nil
|
629
|
+
new_directed_edge = nil
|
630
|
+
if directed_edge.inverted
|
631
|
+
offset = 0
|
632
|
+
new_directed_edge = get_directed_edge(new_edge.v1, new_edge.v0)
|
633
|
+
else
|
634
|
+
offset = 1
|
635
|
+
new_directed_edge = get_directed_edge(new_edge.v0, new_edge.v1)
|
636
|
+
end
|
637
|
+
|
638
|
+
# update the wires referencing this directed edge
|
639
|
+
parents = directed_edge.parents.dup
|
640
|
+
parents.each do |wire|
|
641
|
+
split_wire(wire, directed_edge, offset, new_directed_edge)
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
##
|
646
|
+
# Inserts new directed edge after directed edge in wire
|
647
|
+
#
|
648
|
+
# @param [Wire] wire Existing wire
|
649
|
+
# @param [DirectedEdge] directed_edge Existing directed edge
|
650
|
+
# @param [Integer] offset 0 to insert new_directed_edge edge before directed_edge, 1 to insert after
|
651
|
+
# @param [DirectedEdge] directed_edge New directed edge to insert
|
652
|
+
def split_wire(wire, directed_edge, offset, new_directed_edge)
|
653
|
+
|
654
|
+
directed_edges = wire.directed_edges
|
655
|
+
|
656
|
+
index = directed_edges.index{|de| de.id == directed_edge.id}
|
657
|
+
return nil if !index
|
658
|
+
|
659
|
+
directed_edges.insert(index + offset, new_directed_edge)
|
660
|
+
|
661
|
+
# simulate friend access to set directed_edges on wire
|
662
|
+
wire.instance_variable_set(:@directed_edges, directed_edges)
|
663
|
+
wire.recalculate
|
664
|
+
|
665
|
+
# no need to update faces referencing this wire
|
666
|
+
end
|
667
|
+
|
668
|
+
end # Model
|
669
|
+
|
670
|
+
class Object
|
671
|
+
|
672
|
+
# @return [-] attribute linked to a pre-speficied key (e.g. keyword)
|
673
|
+
attr_accessor :attributes # a [k,v] hash of properties required by a parent
|
674
|
+
# app, but of no intrinsic utility to Topolys.
|
675
|
+
# e.g. a thermal bridge PSI type
|
676
|
+
# "attribute[:bridge] = :balcony
|
677
|
+
# e.g. air leakage crack type (ASHRAE Fund's)
|
678
|
+
# "attribute[:crack] = :sliding
|
679
|
+
# e.g. LCA $ element type
|
680
|
+
# "attribute[$lca] = :parapet"
|
681
|
+
|
682
|
+
# @return [String] Unique string id
|
683
|
+
attr_reader :id
|
684
|
+
|
685
|
+
# @return [Array] Array of parent Objects
|
686
|
+
attr_reader :parents
|
687
|
+
|
688
|
+
# @return [Array] Array of child Objects
|
689
|
+
attr_reader :children
|
690
|
+
|
691
|
+
##
|
692
|
+
# Initialize the object with read only attributes.
|
693
|
+
# If read only attributes are changed externally, must call recalculate.
|
694
|
+
#
|
695
|
+
def initialize
|
696
|
+
@attributes = {}
|
697
|
+
@id = SecureRandom.uuid
|
698
|
+
@parents = []
|
699
|
+
@children = []
|
700
|
+
end
|
701
|
+
|
702
|
+
##
|
703
|
+
# Must be called when read only attributes are updated externally.
|
704
|
+
# Recalculates cached attribute values and links children with parents.
|
705
|
+
# Throws if a class invariant is violated.
|
706
|
+
#
|
707
|
+
def recalculate
|
708
|
+
end
|
709
|
+
|
710
|
+
# @return [String] Unique string id
|
711
|
+
def hash
|
712
|
+
@id
|
713
|
+
end
|
714
|
+
|
715
|
+
# @return [Hash] Hash containing JSON serialized fields
|
716
|
+
def to_json
|
717
|
+
result = { id: @id}
|
718
|
+
result[:attributes] = @attributes if !@attributes.empty?
|
719
|
+
return result
|
720
|
+
end
|
721
|
+
|
722
|
+
# @return [String] Short id used for Graphviz
|
723
|
+
def short_id
|
724
|
+
@id.slice(0,6)
|
725
|
+
end
|
726
|
+
|
727
|
+
# @return [String] Short name used for Graphviz
|
728
|
+
def short_name
|
729
|
+
"#{self.class.to_s.gsub('Topolys::','').gsub('DirectedEdge', 'DEdge')}_#{short_id}"
|
730
|
+
end
|
731
|
+
|
732
|
+
# @return [String] To string
|
733
|
+
def to_s
|
734
|
+
short_name
|
735
|
+
end
|
736
|
+
|
737
|
+
def debug(str)
|
738
|
+
#puts "#{str}#{self.class} #{short_id} has [#{@parents.map{|p| p.short_id}.join(', ')}] parents and [#{@children.map{|c| c.short_id}.join(', ')}] children"
|
739
|
+
end
|
740
|
+
|
741
|
+
# @return [Class] Class of Parent objects
|
742
|
+
def parent_class
|
743
|
+
NilClass
|
744
|
+
end
|
745
|
+
|
746
|
+
# @return [Class] Class of Child objects
|
747
|
+
def child_class
|
748
|
+
NilClass
|
749
|
+
end
|
750
|
+
|
751
|
+
##
|
752
|
+
# Links a parent with a child object
|
753
|
+
#
|
754
|
+
# @param [Object] parent A parent object to link
|
755
|
+
# @param [Object] child A child object to link
|
756
|
+
def Object.link(parent, child)
|
757
|
+
child.link_parent(parent)
|
758
|
+
parent.link_child(child)
|
759
|
+
end
|
760
|
+
|
761
|
+
##
|
762
|
+
# Unlinks a parent from a child object
|
763
|
+
#
|
764
|
+
# @param [Object] parent A parent object to unlink
|
765
|
+
# @param [Object] child A child object to unlink
|
766
|
+
def Object.unlink(parent, child)
|
767
|
+
child.unlink_parent(parent)
|
768
|
+
parent.unlink_child(child)
|
769
|
+
end
|
770
|
+
|
771
|
+
##
|
772
|
+
# Links a parent object
|
773
|
+
#
|
774
|
+
# @param [Object] object A parent object to link
|
775
|
+
def link_parent(object)
|
776
|
+
#puts "link parent #{object.short_id} with child #{self.short_id}"
|
777
|
+
if object && object.is_a?(parent_class)
|
778
|
+
@parents << object if !@parents.find {|obj| obj.id == object.id }
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
##
|
783
|
+
# Unlinks a parent object
|
784
|
+
#
|
785
|
+
# @param [Object] object A parent object to unlink
|
786
|
+
def unlink_parent(object)
|
787
|
+
#puts "unlink parent #{object.short_id} from child #{self.short_id}"
|
788
|
+
@parents.reject!{ |obj| obj.id == object.id }
|
789
|
+
end
|
790
|
+
|
791
|
+
##
|
792
|
+
# Links a child object
|
793
|
+
#
|
794
|
+
# @param [Object] object A child object to link
|
795
|
+
def link_child(object)
|
796
|
+
#puts "link child #{object.short_id} with parent #{self.short_id}"
|
797
|
+
if object && object.is_a?(child_class)
|
798
|
+
@children << object if !@children.find {|obj| obj.id == object.id }
|
799
|
+
end
|
800
|
+
end
|
801
|
+
|
802
|
+
##
|
803
|
+
# Unlinks a child object
|
804
|
+
#
|
805
|
+
# @param [Object] object A child object to unlink
|
806
|
+
def unlink_child(object)
|
807
|
+
#puts "unlink child #{object.short_id} from parent #{self.short_id}"
|
808
|
+
@children.reject!{ |obj| obj.id == object.id }
|
809
|
+
end
|
810
|
+
|
811
|
+
end # Object
|
812
|
+
|
813
|
+
class Vertex < Object
|
814
|
+
|
815
|
+
# @return [Point3D] Point3D geometry
|
816
|
+
attr_reader :point
|
817
|
+
|
818
|
+
##
|
819
|
+
# Initializes a Vertex object, use Model.get_point instead
|
820
|
+
#
|
821
|
+
# Throws if point is incorrect type
|
822
|
+
#
|
823
|
+
# @param [Point3D] point
|
824
|
+
def initialize(point)
|
825
|
+
super()
|
826
|
+
@point = point
|
827
|
+
|
828
|
+
recalculate
|
829
|
+
end
|
830
|
+
|
831
|
+
def to_json
|
832
|
+
result = super
|
833
|
+
result[:point] = { x: @point.x, y: @point.y, z: @point.z }
|
834
|
+
return result
|
835
|
+
end
|
836
|
+
|
837
|
+
def recalculate
|
838
|
+
super()
|
839
|
+
end
|
840
|
+
|
841
|
+
def parent_class
|
842
|
+
Edge
|
843
|
+
end
|
844
|
+
|
845
|
+
def child_class
|
846
|
+
NilClass
|
847
|
+
end
|
848
|
+
|
849
|
+
def edges
|
850
|
+
@parents
|
851
|
+
end
|
852
|
+
|
853
|
+
end # Vertex
|
854
|
+
|
855
|
+
class Edge < Object
|
856
|
+
|
857
|
+
# @return [Vertex] the initial vertex, the edge origin
|
858
|
+
attr_reader :v0
|
859
|
+
|
860
|
+
# @return [Vertex] the second vertex, the edge terminal point
|
861
|
+
attr_reader :v1
|
862
|
+
|
863
|
+
# @return [Numeric] the length of this edge
|
864
|
+
attr_reader :length
|
865
|
+
alias magnitude length
|
866
|
+
|
867
|
+
##
|
868
|
+
# Initializes an Edge object, use Model.get_edge instead
|
869
|
+
#
|
870
|
+
# Throws if v0 or v1 are incorrect type or refer to same vertex
|
871
|
+
#
|
872
|
+
# @param [Vertex] v0 The origin Vertex
|
873
|
+
# @param [Vertex] v1 The terminal Vertex
|
874
|
+
def initialize(v0, v1)
|
875
|
+
super()
|
876
|
+
@v0 = v0
|
877
|
+
@v1 = v1
|
878
|
+
|
879
|
+
recalculate
|
880
|
+
end
|
881
|
+
|
882
|
+
def to_json
|
883
|
+
result = super
|
884
|
+
result[:v0] = @v0.id
|
885
|
+
result[:v1] = @v1.id
|
886
|
+
return result
|
887
|
+
end
|
888
|
+
|
889
|
+
def recalculate
|
890
|
+
super()
|
891
|
+
|
892
|
+
# TODO: should catch if 'origin' or 'terminal' are not Vertex objects
|
893
|
+
# TODO: should also catch if 'origin' or 'terminal' refer to same object or are within tol of each other
|
894
|
+
|
895
|
+
debug("before: ")
|
896
|
+
|
897
|
+
# unlink from any previous vertices
|
898
|
+
@children.reverse_each {|child| Object.unlink(self, child)}
|
899
|
+
|
900
|
+
# link to current vertices
|
901
|
+
Object.link(self, @v0)
|
902
|
+
Object.link(self, @v1)
|
903
|
+
|
904
|
+
debug("after: ")
|
905
|
+
|
906
|
+
# recompute cached properties and check invariants
|
907
|
+
vector = @v1.point - @v0.point
|
908
|
+
@length = vector.magnitude
|
909
|
+
end
|
910
|
+
|
911
|
+
def parent_class
|
912
|
+
DirectedEdge
|
913
|
+
end
|
914
|
+
|
915
|
+
def child_class
|
916
|
+
Vertex
|
917
|
+
end
|
918
|
+
|
919
|
+
def forward_edge
|
920
|
+
@parents.first{|de| !de.inverted}
|
921
|
+
end
|
922
|
+
|
923
|
+
def reverse_edge
|
924
|
+
@parents.first{|de| de.inverted}
|
925
|
+
end
|
926
|
+
|
927
|
+
def directed_edges
|
928
|
+
@parents
|
929
|
+
end
|
930
|
+
|
931
|
+
def vertices
|
932
|
+
@children
|
933
|
+
end
|
934
|
+
|
935
|
+
end # Edge
|
936
|
+
|
937
|
+
class DirectedEdge < Object
|
938
|
+
|
939
|
+
# @return [Vertex] the initial vertex, the directed edge origin
|
940
|
+
attr_reader :v0
|
941
|
+
|
942
|
+
# @return [Vertex] the second vertex, the directed edge terminal point
|
943
|
+
attr_reader :v1
|
944
|
+
|
945
|
+
# @return [Edge] the edge this directed edge points to
|
946
|
+
attr_reader :edge
|
947
|
+
|
948
|
+
# @return [Boolean] true if this is a forward directed edge, false otherwise
|
949
|
+
attr_reader :inverted
|
950
|
+
|
951
|
+
# @return [Numeric] the length of this edge
|
952
|
+
attr_reader :length
|
953
|
+
|
954
|
+
# @return [Vector3D] the vector of this directed edge
|
955
|
+
attr_reader :vector
|
956
|
+
|
957
|
+
##
|
958
|
+
# Initializes a DirectedEdge object, use Model.get_directed_edge instead
|
959
|
+
#
|
960
|
+
# Throws if edge or inverted are incorrect type
|
961
|
+
#
|
962
|
+
# @param [Edge] edge The underlying edge
|
963
|
+
# @param [Boolean] inverted True if this is a forward DirectedEdge, false otherwise
|
964
|
+
def initialize(edge, inverted)
|
965
|
+
super()
|
966
|
+
@edge = edge
|
967
|
+
@inverted = inverted
|
968
|
+
|
969
|
+
recalculate
|
970
|
+
end
|
971
|
+
|
972
|
+
def to_json
|
973
|
+
result = super
|
974
|
+
result[:edge] = @edge.id
|
975
|
+
result[:inverted] = @inverted
|
976
|
+
return result
|
977
|
+
end
|
978
|
+
|
979
|
+
def recalculate
|
980
|
+
super()
|
981
|
+
|
982
|
+
debug("before: ")
|
983
|
+
|
984
|
+
# unlink from any previous edges
|
985
|
+
@children.reverse_each {|child| Object.unlink(self, child)}
|
986
|
+
|
987
|
+
# link with current edge
|
988
|
+
Object.link(self, @edge)
|
989
|
+
|
990
|
+
debug("after: ")
|
991
|
+
|
992
|
+
# recompute cached properties and check invariants
|
993
|
+
if @inverted
|
994
|
+
@v0 = edge.v1
|
995
|
+
@v1 = edge.v0
|
996
|
+
else
|
997
|
+
@v0 = edge.v0
|
998
|
+
@v1 = edge.v1
|
999
|
+
end
|
1000
|
+
|
1001
|
+
@vector = @v1.point - @v0.point
|
1002
|
+
@length = @vector.magnitude
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
def parent_class
|
1006
|
+
Wire
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
def child_class
|
1010
|
+
Edge
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
def wires
|
1014
|
+
@parents
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
def edges
|
1018
|
+
@children
|
1019
|
+
end
|
1020
|
+
|
1021
|
+
end # DirectedEdge
|
1022
|
+
|
1023
|
+
class Wire < Object
|
1024
|
+
|
1025
|
+
# @return [Array] array of directed edges
|
1026
|
+
attr_reader :directed_edges
|
1027
|
+
|
1028
|
+
# @return [Plane3D] plane of this wire
|
1029
|
+
attr_reader :plane
|
1030
|
+
|
1031
|
+
# @return [Vector3D] outward normal of this wire's plane
|
1032
|
+
attr_reader :normal
|
1033
|
+
|
1034
|
+
##
|
1035
|
+
# Initializes a Wire object
|
1036
|
+
#
|
1037
|
+
# Throws if directed_edges is incorrect type or if not sequential, not planar, or not closed
|
1038
|
+
#
|
1039
|
+
# @param [Edge] edge The underlying edge
|
1040
|
+
# @param [Boolean] inverted True if this is a forward DirectedEdge, false otherwise
|
1041
|
+
def initialize(directed_edges)
|
1042
|
+
super()
|
1043
|
+
@directed_edges = directed_edges
|
1044
|
+
|
1045
|
+
recalculate
|
1046
|
+
end
|
1047
|
+
|
1048
|
+
def to_json
|
1049
|
+
result = super
|
1050
|
+
result[:directed_edges] = @directed_edges.map { |de| de.id }
|
1051
|
+
return result
|
1052
|
+
end
|
1053
|
+
|
1054
|
+
def recalculate
|
1055
|
+
super()
|
1056
|
+
|
1057
|
+
# unlink from any previous directed edges
|
1058
|
+
@children.reverse_each {|child| Object.unlink(self, child)}
|
1059
|
+
|
1060
|
+
# link with current directed edges
|
1061
|
+
@directed_edges.each {|de| Object.link(self, de)}
|
1062
|
+
|
1063
|
+
# recompute cached properties and check invariants
|
1064
|
+
|
1065
|
+
raise "Empty edges" if @directed_edges.empty?
|
1066
|
+
raise "Not sequential" if !sequential?
|
1067
|
+
raise "Not closed" if !closed?
|
1068
|
+
|
1069
|
+
@normal = nil
|
1070
|
+
largest = 0
|
1071
|
+
(0...@directed_edges.size-1).each do |i|
|
1072
|
+
temp = @directed_edges[i].vector.cross(@directed_edges[i+1].vector)
|
1073
|
+
if temp.magnitude > largest
|
1074
|
+
largest = temp.magnitude
|
1075
|
+
@normal = temp
|
1076
|
+
end
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
raise "Cannot compute normal" if @normal.nil?
|
1080
|
+
raise "Normal has 0 length" if largest == 0
|
1081
|
+
|
1082
|
+
@normal.normalize!
|
1083
|
+
|
1084
|
+
@plane = Topolys::Plane3D.new(@directed_edges[0].v0.point, @normal)
|
1085
|
+
|
1086
|
+
@directed_edges.each do |de|
|
1087
|
+
raise "Point not on plane" if (de.v0.point - @plane.project(de.v0.point)).magnitude > Topolys.planar_tol
|
1088
|
+
raise "Point not on plane" if (de.v1.point - @plane.project(de.v1.point)).magnitude > Topolys.planar_tol
|
1089
|
+
end
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
def parent_class
|
1093
|
+
Face
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
def child_class
|
1097
|
+
DirectedEdge
|
1098
|
+
end
|
1099
|
+
|
1100
|
+
def faces
|
1101
|
+
@parents
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
##
|
1105
|
+
# @return [Array] Array of Edge
|
1106
|
+
def edges
|
1107
|
+
@directed_edges.map {|de| de.edge}
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
##
|
1111
|
+
# @return [Array] Array of Vertex
|
1112
|
+
def vertices
|
1113
|
+
@directed_edges.map {|de| de.v0}
|
1114
|
+
end
|
1115
|
+
|
1116
|
+
##
|
1117
|
+
# @return [Array] Array of Point3D
|
1118
|
+
def points
|
1119
|
+
vertices.map {|v| v.point}
|
1120
|
+
end
|
1121
|
+
|
1122
|
+
##
|
1123
|
+
# Validates if directed edges are sequential
|
1124
|
+
#
|
1125
|
+
# @return [Bool] Returns true if sequential
|
1126
|
+
def sequential?
|
1127
|
+
n = @directed_edges.size
|
1128
|
+
@directed_edges.each_index do |i|
|
1129
|
+
break if i == n-1
|
1130
|
+
|
1131
|
+
# e.g. check if first edge v0 == last edge v
|
1132
|
+
# check if each intermediate, nieghbouring v0 & v are equal
|
1133
|
+
# e.g. by relying on 'inverted?'
|
1134
|
+
# 'answer = true' if all checks out
|
1135
|
+
return false if @directed_edges[i].v1.id != @directed_edges[i+1].v0.id
|
1136
|
+
end
|
1137
|
+
return true
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
##
|
1141
|
+
# Validates if directed edges are closed
|
1142
|
+
#
|
1143
|
+
# @return [Bool] Returns true if closed
|
1144
|
+
def closed?
|
1145
|
+
n = @directed_edges.size
|
1146
|
+
return false if n < 3
|
1147
|
+
return @directed_edges[n-1].v1.id == @directed_edges[0].v0.id
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
##
|
1151
|
+
# Checks if this Wire's directed edges are the same as another array of directed edges.
|
1152
|
+
# The order of directed edges must be the same but the two arrays may start at different indices.
|
1153
|
+
#
|
1154
|
+
# @param [Array] directed_edges Array of DirectedEdge
|
1155
|
+
#
|
1156
|
+
# @return [Bool] Returns true if the wires are circular_equal, false otherwise
|
1157
|
+
def circular_equal?(directed_edges)
|
1158
|
+
|
1159
|
+
if !Topolys::find_offset(@directed_edges, directed_edges).nil?
|
1160
|
+
return true
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
return false
|
1164
|
+
end
|
1165
|
+
|
1166
|
+
##
|
1167
|
+
# Checks if this Wire is reverse equal to another Wire.
|
1168
|
+
# The order of directed edges must be the same but the two arrays may start at different indices.
|
1169
|
+
#
|
1170
|
+
# @param [Wire] other Other Wire
|
1171
|
+
#
|
1172
|
+
# @return [Bool] Returns true if the wires are reverse_equal, false otherwise
|
1173
|
+
def reverse_equal?(other)
|
1174
|
+
# TODO: implement
|
1175
|
+
return false
|
1176
|
+
end
|
1177
|
+
|
1178
|
+
# TODO : deleting an edge, inserting a sequential edge, etc.
|
1179
|
+
|
1180
|
+
##
|
1181
|
+
# Gets 3D wire perimeter length
|
1182
|
+
#
|
1183
|
+
# @return [Float] Returns perimeter of 3D wire
|
1184
|
+
def perimeter
|
1185
|
+
@directed_edges.inject(0){|sum, de| sum + de.length }
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
##
|
1189
|
+
# Gets shared edges with another wire
|
1190
|
+
#
|
1191
|
+
# @return [Array] Returns array of shared edges
|
1192
|
+
def shared_edges(other)
|
1193
|
+
return nil unless other.is_a?(Wire)
|
1194
|
+
|
1195
|
+
result = []
|
1196
|
+
@directed_edges.each do |de|
|
1197
|
+
other.directed_edges.each do |other_de|
|
1198
|
+
result << de.edge if de.edge.id == other_de.edge.id
|
1199
|
+
end
|
1200
|
+
end
|
1201
|
+
|
1202
|
+
return result
|
1203
|
+
end
|
1204
|
+
|
1205
|
+
end # Wire
|
1206
|
+
|
1207
|
+
class Face < Object
|
1208
|
+
|
1209
|
+
# @return [Wire] outer polygon
|
1210
|
+
attr_reader :outer
|
1211
|
+
|
1212
|
+
# @return [Array] Array of Wire
|
1213
|
+
attr_reader :holes
|
1214
|
+
|
1215
|
+
##
|
1216
|
+
# Initializes a Face object
|
1217
|
+
#
|
1218
|
+
# Throws if outer or holes are incorrect type or if holes have incorrect winding
|
1219
|
+
#
|
1220
|
+
# @param [Wire] outer The outer boundary
|
1221
|
+
# @param [Array] holes Array of inner wires
|
1222
|
+
def initialize(outer, holes)
|
1223
|
+
super()
|
1224
|
+
@outer = outer
|
1225
|
+
@holes = holes
|
1226
|
+
|
1227
|
+
recalculate
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
def to_json
|
1231
|
+
result = super
|
1232
|
+
result[:outer] = @outer.id
|
1233
|
+
result[:holes] = @holes.map { |h| h.id }
|
1234
|
+
return result
|
1235
|
+
end
|
1236
|
+
|
1237
|
+
def recalculate
|
1238
|
+
super()
|
1239
|
+
|
1240
|
+
# unlink from any previous wires
|
1241
|
+
@children.reverse_each {|child| Object.unlink(self, child)}
|
1242
|
+
|
1243
|
+
# link with current wires
|
1244
|
+
Object.link(self, outer)
|
1245
|
+
@holes.each {|hole| Object.link(self, hole)}
|
1246
|
+
|
1247
|
+
# recompute cached properties and check invariants
|
1248
|
+
|
1249
|
+
# check that holes have same normal as outer
|
1250
|
+
normal = @outer.normal
|
1251
|
+
@holes.each do |hole|
|
1252
|
+
raise "Hole does not have correct winding, #{hole.normal.dot(normal)}" if hole.normal.dot(normal) < 1 - Topolys.normal_tol
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
# check that holes are on same plane as outer
|
1256
|
+
plane = @outer.plane
|
1257
|
+
@holes.each do |hole|
|
1258
|
+
hole.points.each do |point|
|
1259
|
+
raise "Point not on plane" if (point - plane.project(point)).magnitude > Topolys.planar_tol
|
1260
|
+
end
|
1261
|
+
end
|
1262
|
+
|
1263
|
+
# TODO: check that holes are contained within outer
|
1264
|
+
|
1265
|
+
end
|
1266
|
+
|
1267
|
+
def parent_class
|
1268
|
+
Shell
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
def child_class
|
1272
|
+
Wire
|
1273
|
+
end
|
1274
|
+
|
1275
|
+
def shells
|
1276
|
+
@parents
|
1277
|
+
end
|
1278
|
+
|
1279
|
+
def wires
|
1280
|
+
@children
|
1281
|
+
end
|
1282
|
+
|
1283
|
+
def shared_outer_edges(other)
|
1284
|
+
return nil unless other.is_a?(Face)
|
1285
|
+
|
1286
|
+
result = []
|
1287
|
+
@outer.directed_edges.each do |de|
|
1288
|
+
other.outer.directed_edges.each do |other_de|
|
1289
|
+
# next if de.id == other.de
|
1290
|
+
result << de.edge if de.edge.id == other_de.edge.id
|
1291
|
+
end
|
1292
|
+
end
|
1293
|
+
|
1294
|
+
return result
|
1295
|
+
end
|
1296
|
+
|
1297
|
+
end # Face
|
1298
|
+
|
1299
|
+
class Shell < Object
|
1300
|
+
|
1301
|
+
# @return [Array] Array of all edges from outer faces
|
1302
|
+
attr_reader :all_edges
|
1303
|
+
|
1304
|
+
# @return [Array] Array of shared edges from outer faces
|
1305
|
+
attr_reader :shared_edges
|
1306
|
+
|
1307
|
+
# @return [Hash] Map edges to array of outer faces
|
1308
|
+
attr_reader :edge_to_face_map
|
1309
|
+
|
1310
|
+
# @return [Matrix] Matrix of level 1 face to face connections
|
1311
|
+
attr_reader :connection_matrix
|
1312
|
+
|
1313
|
+
##
|
1314
|
+
# Initializes a Shell object
|
1315
|
+
#
|
1316
|
+
# Throws if faces are not connected
|
1317
|
+
#
|
1318
|
+
# @param [Array] faces Array of Face
|
1319
|
+
def initialize(faces)
|
1320
|
+
super()
|
1321
|
+
@faces = faces
|
1322
|
+
@all_edges = []
|
1323
|
+
@shared_edges = []
|
1324
|
+
@edge_to_face_map = {}
|
1325
|
+
@connection_matrix = Matrix.identity(faces.size)
|
1326
|
+
|
1327
|
+
recalculate
|
1328
|
+
end
|
1329
|
+
|
1330
|
+
def to_json
|
1331
|
+
result = super
|
1332
|
+
result[:faces] = @faces.map { |h| h.id }
|
1333
|
+
return result
|
1334
|
+
end
|
1335
|
+
|
1336
|
+
def recalculate
|
1337
|
+
|
1338
|
+
# unlink from any previous faces
|
1339
|
+
@children.reverse_each {|child| Object.unlink(self, child)}
|
1340
|
+
|
1341
|
+
# link with current faces
|
1342
|
+
@faces.each {|face| Object.link(self, face)}
|
1343
|
+
|
1344
|
+
# recompute cached properties and check invariants
|
1345
|
+
n = @faces.size
|
1346
|
+
|
1347
|
+
# can't have duplicate faces
|
1348
|
+
face_ids = @faces.map{|face| face.id}.uniq.sort
|
1349
|
+
raise "Duplicate faces in shell" if face_ids.size != n
|
1350
|
+
|
1351
|
+
@all_edges = []
|
1352
|
+
@shared_edges = []
|
1353
|
+
@edge_to_face_map = {}
|
1354
|
+
@connection_matrix = Matrix.identity(faces.size)
|
1355
|
+
(0...n).each do |i|
|
1356
|
+
|
1357
|
+
# populate edge_to_face_map and all_edges
|
1358
|
+
@faces[i].outer.edges.each do |edge|
|
1359
|
+
@edge_to_face_map[edge.id] = [] if @edge_to_face_map[edge.id].nil?
|
1360
|
+
@edge_to_face_map[edge.id] << @faces[i]
|
1361
|
+
@all_edges << edge
|
1362
|
+
end
|
1363
|
+
|
1364
|
+
# loop over other edges
|
1365
|
+
(i+1...n).each do |j|
|
1366
|
+
shared_edges = @faces[i].shared_outer_edges(@faces[j])
|
1367
|
+
#puts "#{i}, #{j}, [#{shared_edges.map{|e| e.short_name}.join(', ')}]"
|
1368
|
+
@shared_edges.concat(shared_edges)
|
1369
|
+
if !shared_edges.empty?
|
1370
|
+
@connection_matrix[i,j] = @connection_matrix[j,i] = 1
|
1371
|
+
end
|
1372
|
+
end
|
1373
|
+
end
|
1374
|
+
@shared_edges.uniq! {|e| e.id}
|
1375
|
+
@shared_edges.sort_by! {|e| e.id}
|
1376
|
+
@all_edges.uniq! {|e| e.id}
|
1377
|
+
@all_edges.sort_by! {|e| e.id}
|
1378
|
+
|
1379
|
+
temp_last = @connection_matrix
|
1380
|
+
temp = normalize_connection_matrix(temp_last * @connection_matrix)
|
1381
|
+
i = 0
|
1382
|
+
while temp_last != temp
|
1383
|
+
temp_last = temp
|
1384
|
+
temp = normalize_connection_matrix(temp * @connection_matrix)
|
1385
|
+
i += 1
|
1386
|
+
break if i > 100
|
1387
|
+
end
|
1388
|
+
|
1389
|
+
# check that every face is connected to every other faces
|
1390
|
+
temp.each {|connection| raise "Faces not connected in shell" if connection == 0}
|
1391
|
+
|
1392
|
+
end
|
1393
|
+
|
1394
|
+
def normalize_connection_matrix(m)
|
1395
|
+
n = faces.size
|
1396
|
+
result = Matrix.identity(n)
|
1397
|
+
(0...n).each do |i|
|
1398
|
+
(i+1...n).each do |j|
|
1399
|
+
result[i,j] = result[j,i] = (m[i,j] > 0 ? 1 : 0)
|
1400
|
+
end
|
1401
|
+
end
|
1402
|
+
return result
|
1403
|
+
end
|
1404
|
+
|
1405
|
+
##
|
1406
|
+
# Checks if faces form a closed Shell
|
1407
|
+
#
|
1408
|
+
# @return [Bool] Returns true if closed
|
1409
|
+
def closed?
|
1410
|
+
@edge_to_face_map.each_value do |faces|
|
1411
|
+
return false if faces.size != 2
|
1412
|
+
end
|
1413
|
+
|
1414
|
+
return @all_edges == @shared_edges
|
1415
|
+
end
|
1416
|
+
|
1417
|
+
def parent_class
|
1418
|
+
NilClass
|
1419
|
+
end
|
1420
|
+
|
1421
|
+
def child_class
|
1422
|
+
Face
|
1423
|
+
end
|
1424
|
+
|
1425
|
+
def faces
|
1426
|
+
@children
|
1427
|
+
end
|
1428
|
+
|
1429
|
+
end # Shell
|
1430
|
+
|
1431
|
+
end # TOPOLYS
|