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