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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +3 -0
  3. data/.github/workflows/pull_request.yml +72 -0
  4. data/.gitignore +23 -0
  5. data/.rspec +3 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.md +21 -0
  8. data/README.md +154 -0
  9. data/Rakefile +60 -0
  10. data/json/midrise.json +64 -0
  11. data/json/tbd_5ZoneNoHVAC.json +19 -0
  12. data/json/tbd_5ZoneNoHVAC_btap.json +91 -0
  13. data/json/tbd_seb_n2.json +41 -0
  14. data/json/tbd_seb_n4.json +57 -0
  15. data/json/tbd_warehouse10.json +24 -0
  16. data/json/tbd_warehouse5.json +37 -0
  17. data/lib/measures/tbd/LICENSE.md +21 -0
  18. data/lib/measures/tbd/README.md +136 -0
  19. data/lib/measures/tbd/README.md.erb +42 -0
  20. data/lib/measures/tbd/docs/.gitkeep +1 -0
  21. data/lib/measures/tbd/measure.rb +327 -0
  22. data/lib/measures/tbd/measure.xml +460 -0
  23. data/lib/measures/tbd/resources/geo.rb +714 -0
  24. data/lib/measures/tbd/resources/geometry.rb +351 -0
  25. data/lib/measures/tbd/resources/model.rb +1431 -0
  26. data/lib/measures/tbd/resources/oslog.rb +381 -0
  27. data/lib/measures/tbd/resources/psi.rb +2229 -0
  28. data/lib/measures/tbd/resources/tbd.rb +55 -0
  29. data/lib/measures/tbd/resources/transformation.rb +121 -0
  30. data/lib/measures/tbd/resources/ua.rb +986 -0
  31. data/lib/measures/tbd/resources/utils.rb +1636 -0
  32. data/lib/measures/tbd/resources/version.rb +3 -0
  33. data/lib/measures/tbd/tests/tbd_full_PSI.json +17 -0
  34. data/lib/measures/tbd/tests/tbd_tests.rb +222 -0
  35. data/lib/tbd/geo.rb +714 -0
  36. data/lib/tbd/psi.rb +2229 -0
  37. data/lib/tbd/ua.rb +986 -0
  38. data/lib/tbd/version.rb +25 -0
  39. data/lib/tbd.rb +93 -0
  40. data/sponsors/canada.png +0 -0
  41. data/sponsors/quebec.png +0 -0
  42. data/tbd.gemspec +43 -0
  43. data/tbd.schema.json +571 -0
  44. data/v291_MacOS.md +110 -0
  45. 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