topolys 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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