osut 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04db3007d81904287c0c36c677270a1e1bc5e45375d3b37bbac9068d51c43754
4
- data.tar.gz: aafa2bb60bdfaf5b5df03e99aca0cb3181f6270468a761652d2b6d0438037daa
3
+ metadata.gz: bec97227aeda04a933080c621650e72003d97816d01d2a4716f146e6501b7571
4
+ data.tar.gz: 82c0d1c211238120894a1bfaffed802b09461f22713957ed5c4a6b9bcb0ef757
5
5
  SHA512:
6
- metadata.gz: ca60fd088aa106b52bfe15fe4fd352c55a2c79083f95212d7e9e37250f1832b3b94f0dc415e664f5289e837600aad2ab936eff304e9d5cdba0c3a977a56cd7d0
7
- data.tar.gz: 8aa96937a55520f99a9c122d0fe98f69ec97d45cbe0cac1e0db7626b48cacee5d65f801eb4a9791e3a6953d64473555b981b3bd9646d7948b6b5c26b2d02e041
6
+ metadata.gz: e3897a390ef6f90524503ce63ff70a3e5275b1d3abf6148ac7752eb82ec64d5bfa2eedbf56898accdd87c7f394adf435e20b30dec457d3ebe4b3fc92e9130be5
7
+ data.tar.gz: 51529559490ab5f48e16a696ce657b867fe538ade32d07237b9fffc28e48b6da98905998c7819e019cdf8232fc2e16d8bc9cdb673280c2cc64861289f24370a5
@@ -7,7 +7,7 @@ on:
7
7
 
8
8
  jobs:
9
9
  test_300x:
10
- runs-on: ubuntu-18.04
10
+ runs-on: ubuntu-22.04
11
11
  steps:
12
12
  - name: Check out repository
13
13
  uses: actions/checkout@v2
@@ -23,7 +23,7 @@ jobs:
23
23
  docker exec -t test bundle exec rake
24
24
  docker kill test
25
25
  test_321x:
26
- runs-on: ubuntu-18.04
26
+ runs-on: ubuntu-22.04
27
27
  steps:
28
28
  - name: Check out repository
29
29
  uses: actions/checkout@v2
@@ -39,7 +39,7 @@ jobs:
39
39
  docker exec -t test bundle exec rake
40
40
  docker kill test
41
41
  test_330x:
42
- runs-on: ubuntu-20.04
42
+ runs-on: ubuntu-22.04
43
43
  steps:
44
44
  - name: Check out repository
45
45
  uses: actions/checkout@v2
@@ -55,7 +55,7 @@ jobs:
55
55
  docker exec -t test bundle exec rake
56
56
  docker kill test
57
57
  test_340x:
58
- runs-on: ubuntu-20.04
58
+ runs-on: ubuntu-22.04
59
59
  steps:
60
60
  - name: Check out repository
61
61
  uses: actions/checkout@v2
@@ -70,3 +70,19 @@ jobs:
70
70
  docker exec -t test bundle update
71
71
  docker exec -t test bundle exec rake
72
72
  docker kill test
73
+ test_351x:
74
+ runs-on: ubuntu-22.04
75
+ steps:
76
+ - name: Check out repository
77
+ uses: actions/checkout@v2
78
+ - name: Run Tests
79
+ run: |
80
+ echo $(pwd)
81
+ echo $(ls)
82
+ docker pull nrel/openstudio:3.5.1
83
+ docker run --name test --rm -d -t -v $(pwd):/work -w /work nrel/openstudio:3.5.1
84
+ docker exec -t test pwd
85
+ docker exec -t test ls
86
+ docker exec -t test bundle update
87
+ docker exec -t test bundle exec rake
88
+ docker kill test
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  BSD 3-Clause License
2
2
 
3
- Copyright (c) 2022, Denis Bourgeois
3
+ Copyright (c) 2022-2023, Denis Bourgeois
4
4
  All rights reserved.
5
5
 
6
6
  Redistribution and use in source and binary forms, with or without
data/lib/osut/utils.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # BSD 3-Clause License
2
2
  #
3
- # Copyright (c) 2022, Denis Bourgeois
3
+ # Copyright (c) 2022-2023, Denis Bourgeois
4
4
  # All rights reserved.
5
5
  #
6
6
  # Redistribution and use in source and binary forms, with or without
@@ -59,7 +59,7 @@ module OSut
59
59
  # cooling system of sufficient size to maintain temperatures suitable
60
60
  # for HUMAN COMFORT:
61
61
  # - COOLED: cooled by a system >= 10 W/m2
62
- # - HEATED: heated by a system e.g., >= 50 W/m2 in Climate Zone CZ-7
62
+ # - HEATED: heated by a system, e.g. >= 50 W/m2 in Climate Zone CZ-7
63
63
  # - INDIRECTLY: heated or cooled via adjacent space(s) provided:
64
64
  # - UA of adjacent surfaces > UA of other surfaces
65
65
  # or
@@ -89,7 +89,7 @@ module OSut
89
89
  # response to the exterior ambient temperature by the provision, either
90
90
  # DIRECTLY or INDIRECTLY, of heating or cooling [...]". Although criteria
91
91
  # differ (e.g., not sizing-based), the general idea is sufficiently similar
92
- # to ASHRAE 90.1 (e.g., heating and/or cooling based, no distinction for
92
+ # to ASHRAE 90.1 (e.g. heating and/or cooling based, no distinction for
93
93
  # INDIRECTLY conditioned spaces like plenums).
94
94
  #
95
95
  # SEMI-HEATED spaces are also a defined NECB term, but again the distinction
@@ -109,16 +109,16 @@ module OSut
109
109
  # processes. As discussed in greater detail elswhere, methods are developed to
110
110
  # rely on zoning info and/or "intended" temperature setpoints.
111
111
  #
112
- # For an OpenStudio model (OSM) in an incomplete or preliminary state, e.g.
113
- # holding fully-formed ENCLOSED spaces without thermal zoning information or
114
- # setpoint temperatures (early design stage assessments of form, porosity or
115
- # envelope), all OSM spaces will be considered CONDITIONED, presuming
116
- # setpoints of ~21°C (heating) and ~24°C (cooling).
112
+ # For an OpenStudio model in an incomplete or preliminary state, e.g. holding
113
+ # fully-formed ENCLOSED spaces without thermal zoning information or setpoint
114
+ # temperatures (early design stage assessments of form, porosity or envelope),
115
+ # all OpenStudio spaces will be considered CONDITIONED, presuming setpoints of
116
+ # ~21°C (heating) and ~24°C (cooling).
117
117
  #
118
- # If ANY valid space/zone-specific temperature setpoints are found in the OSM,
119
- # spaces/zones WITHOUT valid heating or cooling setpoints are considered as
120
- # UNCONDITIONED or UNENCLOSED spaces (like attics), or INDIRECTLY CONDITIONED
121
- # spaces (like plenums), see "plenum?" method.
118
+ # If ANY valid space/zone-specific temperature setpoints are found in the
119
+ # OpenStudio model, spaces/zones WITHOUT valid heating or cooling setpoints
120
+ # are considered as UNCONDITIONED or UNENCLOSED spaces (like attics), or
121
+ # INDIRECTLY CONDITIONED spaces (like plenums), see "plenum?" method.
122
122
 
123
123
  ##
124
124
  # Return min & max values of a schedule (ruleset).
@@ -139,6 +139,7 @@ module OSut
139
139
  res = { min: nil, max: nil }
140
140
 
141
141
  return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
142
+
142
143
  id = sched.nameString
143
144
  return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
144
145
 
@@ -186,6 +187,7 @@ module OSut
186
187
  res = { min: nil, max: nil }
187
188
 
188
189
  return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
190
+
189
191
  id = sched.nameString
190
192
  return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
191
193
 
@@ -218,6 +220,7 @@ module OSut
218
220
  res = { min: nil, max: nil }
219
221
 
220
222
  return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
223
+
221
224
  id = sched.nameString
222
225
  return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
223
226
 
@@ -231,9 +234,11 @@ module OSut
231
234
  end
232
235
 
233
236
  return empty("'#{id}' values", mth, ERR, res) if vals.empty?
237
+
234
238
  ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
235
239
  log(ERR, "Non-numeric values in '#{id}' (#{mth})") unless ok
236
240
  return res unless ok
241
+
237
242
  res[:min] = vals.min
238
243
  res[:max] = vals.max
239
244
 
@@ -248,19 +253,21 @@ module OSut
248
253
  # @return [Hash] min: (Float), max: (Float)
249
254
  # @return [Hash] min: nil, max: nil (if invalid input)
250
255
  def scheduleIntervalMinMax(sched = nil)
251
- mth = "OSut::#{__callee__}"
252
- cl = OpenStudio::Model::ScheduleInterval
253
- vals = []
254
- prev_str = ""
255
- res = { min: nil, max: nil }
256
+ mth = "OSut::#{__callee__}"
257
+ cl = OpenStudio::Model::ScheduleInterval
258
+ vals = []
259
+ res = { min: nil, max: nil }
256
260
 
257
261
  return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
262
+
258
263
  id = sched.nameString
259
264
  return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
265
+
260
266
  vals = sched.timeSeries.values
261
- ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
267
+ ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
262
268
  log(ERR, "Non-numeric values in '#{id}' (#{mth})") unless ok
263
269
  return res unless ok
270
+
264
271
  res[:min] = vals.min
265
272
  res[:max] = vals.max
266
273
 
@@ -289,6 +296,7 @@ module OSut
289
296
  res = { spt: nil, dual: false }
290
297
 
291
298
  return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
299
+
292
300
  id = zone.nameString
293
301
  return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
294
302
 
@@ -369,6 +377,7 @@ module OSut
369
377
  end
370
378
 
371
379
  return res if zone.thermostat.empty?
380
+
372
381
  tstat = zone.thermostat.get
373
382
  res[:spt] = nil
374
383
 
@@ -427,6 +436,7 @@ module OSut
427
436
 
428
437
  sched.getScheduleWeeks.each do |week|
429
438
  next if week.winterDesignDaySchedule.empty?
439
+
430
440
  dd = week.winterDesignDaySchedule.get
431
441
  next unless dd.values.empty?
432
442
 
@@ -479,6 +489,7 @@ module OSut
479
489
  res = { spt: nil, dual: false }
480
490
 
481
491
  return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
492
+
482
493
  id = zone.nameString
483
494
  return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
484
495
 
@@ -546,6 +557,7 @@ module OSut
546
557
  end
547
558
 
548
559
  return res if zone.thermostat.empty?
560
+
549
561
  tstat = zone.thermostat.get
550
562
  res[:spt] = nil
551
563
 
@@ -604,6 +616,7 @@ module OSut
604
616
 
605
617
  sched.getScheduleWeeks.each do |week|
606
618
  next if week.summerDesignDaySchedule.empty?
619
+
607
620
  dd = week.summerDesignDaySchedule.get
608
621
  next unless dd.values.empty?
609
622
 
@@ -628,7 +641,7 @@ module OSut
628
641
  mth = "OSut::#{__callee__}"
629
642
  cl = OpenStudio::Model::Model
630
643
 
631
- return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
644
+ return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
632
645
 
633
646
  model.getThermalZones.each do |zone|
634
647
  return true if minCoolScheduledSetpoint(zone)[:spt]
@@ -648,12 +661,12 @@ module OSut
648
661
  mth = "OSut::#{__callee__}"
649
662
  cl = OpenStudio::Model::Model
650
663
 
651
- return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
664
+ return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
652
665
 
653
666
  model.getThermalZones.each do |zone|
654
- next if zone.canBePlenum
655
- return true unless zone.airLoopHVACs.empty?
656
- return true if zone.isPlenum
667
+ next if zone.canBePlenum
668
+ return true unless zone.airLoopHVACs.empty?
669
+ return true if zone.isPlenum
657
670
  end
658
671
 
659
672
  false
@@ -678,7 +691,7 @@ module OSut
678
691
  # A space may be tagged as a plenum if:
679
692
  #
680
693
  # CASE A: its zone's "isPlenum" == true (SDK method) for a fully-developed
681
- # OpenStudio model (complete with HVAC air loops);
694
+ # OpenStudio model (complete with HVAC air loops); OR
682
695
  #
683
696
  # CASE B: (IN ABSENCE OF HVAC AIRLOOPS) if it's excluded from a building's
684
697
  # total floor area yet linked to a zone holding an 'inactive'
@@ -691,10 +704,13 @@ module OSut
691
704
  cl = OpenStudio::Model::Space
692
705
 
693
706
  return invalid("space", mth, 1, DBG, false) unless space.respond_to?(NS)
707
+
694
708
  id = space.nameString
695
709
  return mismatch(id, space, cl, mth, DBG, false) unless space.is_a?(cl)
710
+
696
711
  valid = loops == true || loops == false
697
712
  return invalid("loops", mth, 2, DBG, false) unless valid
713
+
698
714
  valid = setpoints == true || setpoints == false
699
715
  return invalid("setpoints", mth, 3, DBG, false) unless valid
700
716
 
@@ -714,11 +730,11 @@ module OSut
714
730
  unless space.spaceType.empty?
715
731
  type = space.spaceType.get
716
732
  return type.nameString.downcase == "plenum" # C
733
+ end
717
734
 
718
- unless type.standardsSpaceType.empty?
719
- type = type.standardsSpaceType.get
720
- return type.downcase == "plenum" # C
721
- end
735
+ unless type.standardsSpaceType.empty?
736
+ type = type.standardsSpaceType.get
737
+ return type.downcase == "plenum" # C
722
738
  end
723
739
 
724
740
  false
@@ -737,8 +753,8 @@ module OSut
737
753
  cl = OpenStudio::Model::Model
738
754
  limits = nil
739
755
 
740
- return mismatch("model", model, cl, mth) unless model.is_a?(cl)
741
- return invalid("availability", avl, 2, mth) unless avl.respond_to?(:to_s)
756
+ return mismatch("model", model, cl, mth) unless model.is_a?(cl)
757
+ return invalid("availability", avl, 2, mth) unless avl.respond_to?(:to_s)
742
758
 
743
759
  # Either fetch availability ScheduleTypeLimits object, or create one.
744
760
  model.getScheduleTypeLimitss.each do |l|
@@ -751,6 +767,7 @@ module OSut
751
767
  next unless l.numericType.get.downcase == "discrete"
752
768
  next unless l.unitType.downcase == "availability"
753
769
  next unless l.nameString.downcase == "hvac operation scheduletypelimits"
770
+
754
771
  limits = l
755
772
  end
756
773
 
@@ -769,9 +786,10 @@ module OSut
769
786
  off = OpenStudio::Model::ScheduleDay.new(model, 0)
770
787
 
771
788
  # Seasonal availability start/end dates.
772
- year = model.yearDescription
773
- return empty("yearDescription", mth, ERR) if year.empty?
774
- year = year.get
789
+ year = model.yearDescription
790
+ return empty("yearDescription", mth, ERR) if year.empty?
791
+
792
+ year = year.get
775
793
  may01 = year.makeDate(OpenStudio::MonthOfYear.new("May"), 1)
776
794
  oct31 = year.makeDate(OpenStudio::MonthOfYear.new("Oct"), 31)
777
795
 
@@ -850,9 +868,11 @@ module OSut
850
868
  ok = schedule.setScheduleTypeLimits(limits)
851
869
  log(ERR, "'#{nom}': Can't set schedule type limits (#{mth})") unless ok
852
870
  return nil unless ok
871
+
853
872
  ok = schedule.defaultDaySchedule.addValue(time, val)
854
873
  log(ERR, "'#{nom}': Can't set default day schedule (#{mth})") unless ok
855
874
  return nil unless ok
875
+
856
876
  schedule.defaultDaySchedule.setName(dft)
857
877
 
858
878
  unless tag.empty?
@@ -861,12 +881,15 @@ module OSut
861
881
  ok = rule.setStartDate(may01)
862
882
  log(ERR, "'#{tag}': Can't set start date (#{mth})") unless ok
863
883
  return nil unless ok
884
+
864
885
  ok = rule.setEndDate(oct31)
865
886
  log(ERR, "'#{tag}': Can't set end date (#{mth})") unless ok
866
887
  return nil unless ok
888
+
867
889
  ok = rule.setApplyAllDays(true)
868
890
  log(ERR, "'#{tag}': Can't apply to all days (#{mth})") unless ok
869
891
  return nil unless ok
892
+
870
893
  rule.daySchedule.setName(day)
871
894
  end
872
895
 
@@ -874,7 +897,7 @@ module OSut
874
897
  end
875
898
 
876
899
  ##
877
- # Validate if default construction set holds a base ground construction.
900
+ # Validate if default construction set holds a base construction.
878
901
  #
879
902
  # @param set [OpenStudio::Model::DefaultConstructionSet] a default set
880
903
  # @param bse [OpensStudio::Model::ConstructionBase] a construction base
@@ -890,17 +913,23 @@ module OSut
890
913
  cl2 = OpenStudio::Model::ConstructionBase
891
914
 
892
915
  return invalid("set", mth, 1, DBG, false) unless set.respond_to?(NS)
916
+
893
917
  id = set.nameString
894
918
  return mismatch(id, set, cl1, mth, DBG, false) unless set.is_a?(cl1)
895
919
  return invalid("base", mth, 2, DBG, false) unless bse.respond_to?(NS)
920
+
896
921
  id = bse.nameString
897
922
  return mismatch(id, bse, cl2, mth, DBG, false) unless bse.is_a?(cl2)
923
+
898
924
  valid = gr == true || gr == false
899
925
  return invalid("ground", mth, 3, DBG, false) unless valid
926
+
900
927
  valid = ex == true || ex == false
901
928
  return invalid("exterior", mth, 4, DBG, false) unless valid
929
+
902
930
  valid = typ.respond_to?(:to_s)
903
931
  return invalid("surface typ", mth, 4, DBG, false) unless valid
932
+
904
933
  type = typ.to_s.downcase
905
934
  valid = type == "floor" || type == "wall" || type == "roofceiling"
906
935
  return invalid("surface type", mth, 5, DBG, false) unless valid
@@ -957,17 +986,20 @@ module OSut
957
986
  cl1 = OpenStudio::Model::Model
958
987
  cl2 = OpenStudio::Model::Surface
959
988
 
960
- return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
961
- return invalid("s", mth, 2) unless s.respond_to?(NS)
962
- id = s.nameString
963
- return mismatch(id, s, cl2, mth) unless s.is_a?(cl2)
989
+ return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
990
+ return invalid("s", mth, 2) unless s.respond_to?(NS)
991
+
992
+ id = s.nameString
993
+ return mismatch(id, s, cl2, mth) unless s.is_a?(cl2)
964
994
 
965
995
  ok = s.isConstructionDefaulted
966
996
  log(ERR, "'#{id}' construction not defaulted (#{mth})") unless ok
967
997
  return nil unless ok
968
- return empty("'#{id}' construction", mth, ERR) if s.construction.empty?
998
+ return empty("'#{id}' construction", mth, ERR) if s.construction.empty?
999
+
969
1000
  base = s.construction.get
970
- return empty("'#{id}' space", mth, ERR) if s.space.empty?
1001
+ return empty("'#{id}' space", mth, ERR) if s.space.empty?
1002
+
971
1003
  space = s.space.get
972
1004
  type = s.surfaceType
973
1005
  ground = false
@@ -981,7 +1013,7 @@ module OSut
981
1013
 
982
1014
  unless space.defaultConstructionSet.empty?
983
1015
  set = space.defaultConstructionSet.get
984
- return set if holdsConstruction?(set, base, ground, exterior, type)
1016
+ return set if holdsConstruction?(set, base, ground, exterior, type)
985
1017
  end
986
1018
 
987
1019
  unless space.spaceType.empty?
@@ -989,7 +1021,7 @@ module OSut
989
1021
 
990
1022
  unless spacetype.defaultConstructionSet.empty?
991
1023
  set = spacetype.defaultConstructionSet.get
992
- return set if holdsConstruction?(set, base, ground, exterior, type)
1024
+ return set if holdsConstruction?(set, base, ground, exterior, type)
993
1025
  end
994
1026
  end
995
1027
 
@@ -998,7 +1030,7 @@ module OSut
998
1030
 
999
1031
  unless story.defaultConstructionSet.empty?
1000
1032
  set = story.defaultConstructionSet.get
1001
- return set if holdsConstruction?(set, base, ground, exterior, type)
1033
+ return set if holdsConstruction?(set, base, ground, exterior, type)
1002
1034
  end
1003
1035
  end
1004
1036
 
@@ -1006,7 +1038,7 @@ module OSut
1006
1038
 
1007
1039
  unless building.defaultConstructionSet.empty?
1008
1040
  set = building.defaultConstructionSet.get
1009
- return set if holdsConstruction?(set, base, ground, exterior, type)
1041
+ return set if holdsConstruction?(set, base, ground, exterior, type)
1010
1042
  end
1011
1043
 
1012
1044
  nil
@@ -1036,19 +1068,21 @@ module OSut
1036
1068
  #
1037
1069
  # @param lc [OpenStudio::LayeredConstruction] a layered construction
1038
1070
  #
1039
- # @return [Double] total layered construction thickness
1040
- # @return [Double] 0 if invalid input
1071
+ # @return [Float] total layered construction thickness
1072
+ # @return [Float] 0 if invalid input
1041
1073
  def thickness(lc = nil)
1042
1074
  mth = "OSut::#{__callee__}"
1043
1075
  cl = OpenStudio::Model::LayeredConstruction
1044
1076
 
1045
- return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
1077
+ return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
1078
+
1046
1079
  id = lc.nameString
1047
- return mismatch(id, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
1080
+ return mismatch(id, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
1048
1081
 
1049
1082
  ok = standardOpaqueLayers?(lc)
1050
1083
  log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})") unless ok
1051
1084
  return 0.0 unless ok
1085
+
1052
1086
  thickness = 0.0
1053
1087
  lc.layers.each { |m| thickness += m.thickness }
1054
1088
 
@@ -1113,11 +1147,14 @@ module OSut
1113
1147
  cl2 = Numeric
1114
1148
 
1115
1149
  return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
1150
+
1116
1151
  id = lc.nameString
1152
+
1117
1153
  return mismatch(id, lc, cl1, mth, DBG, 0.0) unless lc.is_a?(cl1)
1118
1154
  return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2)
1119
1155
  return mismatch("temp K", t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2)
1120
- t += 273.0 # °C to K
1156
+
1157
+ t += 273.0 # °C to K
1121
1158
  return negative("temp K", mth, DBG, 0.0) if t < 0
1122
1159
  return negative("film", mth, DBG, 0.0) if film < 0
1123
1160
 
@@ -1127,6 +1164,7 @@ module OSut
1127
1164
  # Fenestration materials first (ignoring shades, screens, etc.)
1128
1165
  empty = m.to_SimpleGlazing.empty?
1129
1166
  return 1 / m.to_SimpleGlazing.get.uFactor unless empty
1167
+
1130
1168
  empty = m.to_StandardGlazing.empty?
1131
1169
  rsi += m.to_StandardGlazing.get.thermalResistance unless empty
1132
1170
  empty = m.to_RefractionExtinctionGlazing.empty?
@@ -1166,9 +1204,10 @@ module OSut
1166
1204
  res = { index: nil, type: nil, r: 0.0 }
1167
1205
  i = 0 # iterator
1168
1206
 
1169
- return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
1170
- id = lc.nameString
1171
- return mismatch(id, lc, cl1, mth, DBG, res) unless lc.is_a?(cl)
1207
+ return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
1208
+
1209
+ id = lc.nameString
1210
+ return mismatch(id, lc, cl1, mth, DBG, res) unless lc.is_a?(cl)
1172
1211
 
1173
1212
  lc.layers.each do |m|
1174
1213
  unless m.to_MasslessOpaqueMaterial.empty?
@@ -1178,9 +1217,9 @@ module OSut
1178
1217
  i += 1
1179
1218
  next
1180
1219
  else
1181
- res[:r] = m.thermalResistance
1220
+ res[:r ] = m.thermalResistance
1182
1221
  res[:index] = i
1183
- res[:type] = :massless
1222
+ res[:type ] = :massless
1184
1223
  end
1185
1224
  end
1186
1225
 
@@ -1193,9 +1232,9 @@ module OSut
1193
1232
  i += 1
1194
1233
  next
1195
1234
  else
1196
- res[:r] = d / k
1235
+ res[:r ] = d / k
1197
1236
  res[:index] = i
1198
- res[:type] = :standard
1237
+ res[:type ] = :standard
1199
1238
  end
1200
1239
  end
1201
1240
 
@@ -1221,6 +1260,7 @@ module OSut
1221
1260
 
1222
1261
  return mismatch("model", model, cl1, mth, DBG, res) unless model.is_a?(cl1)
1223
1262
  return invalid("group", mth, 2, DBG, res) unless group.respond_to?(NS)
1263
+
1224
1264
  id = group.nameString
1225
1265
  return mismatch(id, group, cl2, mth, DBG, res) unless group.is_a?(cl2)
1226
1266
 
@@ -1230,6 +1270,28 @@ module OSut
1230
1270
  res
1231
1271
  end
1232
1272
 
1273
+ ##
1274
+ # Return a scalar product of an OpenStudio Vector3d.
1275
+ #
1276
+ # @param v [OpenStudio::Vector3d] a vector
1277
+ # @param m [Float] a scalar
1278
+ #
1279
+ # @return [OpenStudio::Vector3d] modified vector
1280
+ # @return [OpenStudio::Vector3d] provided (or empty) vector if invalid input
1281
+ def scalar(v = OpenStudio::Vector3d.new(0,0,0), m = 0)
1282
+ mth = "OSut::#{__callee__}"
1283
+ cl1 = OpenStudio::Vector3d
1284
+ cl2 = Numeric
1285
+
1286
+ return mismatch("vector", v, cl1, mth, DBG, v) unless v.is_a?(cl1)
1287
+ return mismatch("x", v.x, cl2, mth, DBG, v) unless v.x.respond_to?(:to_f)
1288
+ return mismatch("y", v.y, cl2, mth, DBG, v) unless v.y.respond_to?(:to_f)
1289
+ return mismatch("z", v.z, cl2, mth, DBG, v) unless v.z.respond_to?(:to_f)
1290
+ return mismatch("m", m, cl2, mth, DBG, v) unless m.respond_to?(:to_f)
1291
+
1292
+ OpenStudio::Vector3d.new(m * v.x, m * v.y, m * v.z)
1293
+ end
1294
+
1233
1295
  ##
1234
1296
  # Flatten OpenStudio 3D points vs Z-axis (Z=0).
1235
1297
  #
@@ -1243,8 +1305,9 @@ module OSut
1243
1305
  v = OpenStudio::Point3dVector.new
1244
1306
 
1245
1307
  valid = pts.is_a?(cl1) || pts.is_a?(Array)
1246
- return mismatch("points", pts, cl1, mth, DBG, v) unless valid
1247
- pts.each { |pt| mismatch("pt", pt, cl2, mth, ERR, v) unless pt.is_a?(cl2) }
1308
+ return mismatch("points", pts, cl1, mth, DBG, v) unless valid
1309
+
1310
+ pts.each { |pt| mismatch("pt", pt, cl2, mth, ERR, v) unless pt.is_a?(cl2) }
1248
1311
  pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, 0) }
1249
1312
 
1250
1313
  v
@@ -1268,40 +1331,53 @@ module OSut
1268
1331
 
1269
1332
  return invalid("id1", mth, 3, DBG, a) unless id1.respond_to?(:to_s)
1270
1333
  return invalid("id2", mth, 4, DBG, a) unless id2.respond_to?(:to_s)
1334
+
1271
1335
  i1 = id1.to_s
1272
1336
  i2 = id2.to_s
1273
1337
  i1 = "poly1" if i1.empty?
1274
1338
  i2 = "poly2" if i2.empty?
1339
+
1275
1340
  valid1 = p1.is_a?(cl1) || p1.is_a?(Array)
1276
1341
  valid2 = p2.is_a?(cl1) || p2.is_a?(Array)
1342
+
1277
1343
  return mismatch(i1, p1, cl1, mth, DBG, a) unless valid1
1278
1344
  return mismatch(i2, p2, cl1, mth, DBG, a) unless valid2
1279
- return empty(i1, mth, ERR, a) if p1.empty?
1280
- return empty(i2, mth, ERR, a) if p2.empty?
1345
+ return empty(i1, mth, ERR, a) if p1.empty?
1346
+ return empty(i2, mth, ERR, a) if p2.empty?
1347
+
1281
1348
  p1.each { |v| return mismatch(i1, v, cl2, mth, ERR, a) unless v.is_a?(cl2) }
1282
1349
  p2.each { |v| return mismatch(i2, v, cl2, mth, ERR, a) unless v.is_a?(cl2) }
1283
1350
 
1284
- ft = OpenStudio::Transformation::alignFace(p1).inverse
1285
- ft_p1 = flatZ( (ft * p1).reverse )
1286
- return false if ft_p1.empty?
1287
- area1 = OpenStudio::getArea(ft_p1)
1288
- return empty("#{i1} area", mth, ERR, a) if area1.empty?
1351
+ # XY-plane transformation matrix ... needs to be clockwise for boost.
1352
+ ft = OpenStudio::Transformation.alignFace(p1)
1353
+ ft_p1 = flatZ( (ft.inverse * p1) )
1354
+ return false if ft_p1.empty?
1355
+
1356
+ cw = OpenStudio.pointInPolygon(ft_p1.first, ft_p1, TOL)
1357
+ ft_p1 = flatZ( (ft.inverse * p1).reverse ) unless cw
1358
+ ft_p2 = flatZ( (ft.inverse * p2).reverse ) unless cw
1359
+ ft_p2 = flatZ( (ft.inverse * p2) ) if cw
1360
+ return false if ft_p2.empty?
1361
+
1362
+ area1 = OpenStudio.getArea(ft_p1)
1363
+ area2 = OpenStudio.getArea(ft_p2)
1364
+ return empty("#{i1} area", mth, ERR, a) if area1.empty?
1365
+ return empty("#{i2} area", mth, ERR, a) if area2.empty?
1366
+
1289
1367
  area1 = area1.get
1290
- ft_p2 = flatZ( (ft * p2).reverse )
1291
- return false if ft_p2.empty?
1292
- area2 = OpenStudio::getArea(ft_p2)
1293
- return empty("#{i2} area", mth, ERR, a) if area2.empty?
1294
1368
  area2 = area2.get
1295
- union = OpenStudio::join(ft_p1, ft_p2, TOL2)
1296
- return false if union.empty?
1369
+ union = OpenStudio.join(ft_p1, ft_p2, TOL2)
1370
+ return false if union.empty?
1371
+
1297
1372
  union = union.get
1298
- area = OpenStudio::getArea(union)
1299
- return empty("#{i1}:#{i2} union area", mth, ERR, a) if area.empty?
1373
+ area = OpenStudio.getArea(union)
1374
+ return empty("#{i1}:#{i2} union area", mth, ERR, a) if area.empty?
1375
+
1300
1376
  area = area.get
1301
1377
 
1302
- return false if area < TOL
1303
- return true if (area - area2).abs < TOL
1304
- return false if (area - area2).abs > TOL
1378
+ return false if area < TOL
1379
+ return true if (area - area2).abs < TOL
1380
+ return false if (area - area2).abs > TOL
1305
1381
 
1306
1382
  true
1307
1383
  end
@@ -1324,42 +1400,290 @@ module OSut
1324
1400
 
1325
1401
  return invalid("id1", mth, 3, DBG, a) unless id1.respond_to?(:to_s)
1326
1402
  return invalid("id2", mth, 4, DBG, a) unless id2.respond_to?(:to_s)
1403
+
1327
1404
  i1 = id1.to_s
1328
1405
  i2 = id2.to_s
1329
1406
  i1 = "poly1" if i1.empty?
1330
1407
  i2 = "poly2" if i2.empty?
1408
+
1331
1409
  valid1 = p1.is_a?(cl1) || p1.is_a?(Array)
1332
1410
  valid2 = p2.is_a?(cl1) || p2.is_a?(Array)
1411
+
1333
1412
  return mismatch(i1, p1, cl1, mth, DBG, a) unless valid1
1334
1413
  return mismatch(i2, p2, cl1, mth, DBG, a) unless valid2
1335
- return empty(i1, mth, ERR, a) if p1.empty?
1336
- return empty(i2, mth, ERR, a) if p2.empty?
1414
+ return empty(i1, mth, ERR, a) if p1.empty?
1415
+ return empty(i2, mth, ERR, a) if p2.empty?
1416
+
1337
1417
  p1.each { |v| return mismatch(i1, v, cl2, mth, ERR, a) unless v.is_a?(cl2) }
1338
1418
  p2.each { |v| return mismatch(i2, v, cl2, mth, ERR, a) unless v.is_a?(cl2) }
1339
1419
 
1340
- ft = OpenStudio::Transformation::alignFace(p1).inverse
1341
- ft_p1 = flatZ( (ft * p1).reverse )
1342
- return false if ft_p1.empty?
1343
- area1 = OpenStudio::getArea(ft_p1)
1344
- return empty("#{i1} area", mth, ERR, a) if area1.empty?
1420
+ # XY-plane transformation matrix ... needs to be clockwise for boost.
1421
+ ft = OpenStudio::Transformation.alignFace(p1)
1422
+ ft_p1 = flatZ( (ft.inverse * p1) )
1423
+ ft_p2 = flatZ( (ft.inverse * p2) )
1424
+ return false if ft_p1.empty?
1425
+ return false if ft_p2.empty?
1426
+
1427
+ cw = OpenStudio.pointInPolygon(ft_p1.first, ft_p1, TOL)
1428
+ ft_p1 = flatZ( (ft.inverse * p1).reverse ) unless cw
1429
+ ft_p2 = flatZ( (ft.inverse * p2).reverse ) unless cw
1430
+ return false if ft_p1.empty?
1431
+ return false if ft_p2.empty?
1432
+
1433
+ area1 = OpenStudio.getArea(ft_p1)
1434
+ area2 = OpenStudio.getArea(ft_p2)
1435
+ return empty("#{i1} area", mth, ERR, a) if area1.empty?
1436
+ return empty("#{i2} area", mth, ERR, a) if area2.empty?
1437
+
1345
1438
  area1 = area1.get
1346
- ft_p2 = flatZ( (ft * p2).reverse )
1347
- return false if ft_p2.empty?
1348
- area2 = OpenStudio::getArea(ft_p2)
1349
- return empty("#{i2} area", mth, ERR, a) if area2.empty?
1350
1439
  area2 = area2.get
1351
- union = OpenStudio::join(ft_p1, ft_p2, TOL2)
1352
- return false if union.empty?
1440
+ union = OpenStudio.join(ft_p1, ft_p2, TOL2)
1441
+ return false if union.empty?
1442
+
1353
1443
  union = union.get
1354
- area = OpenStudio::getArea(union)
1355
- return empty("#{i1}:#{i2} union area", mth, ERR, a) if area.empty?
1356
- area = area.get
1444
+ area = OpenStudio.getArea(union)
1445
+ return empty("#{i1}:#{i2} union area", mth, ERR, a) if area.empty?
1357
1446
 
1358
- return false if area < TOL
1447
+ area = area.get
1448
+ return false if area < TOL
1359
1449
 
1360
1450
  true
1361
1451
  end
1362
1452
 
1453
+ ##
1454
+ # Generate offset vertices (by width) for a 3- or 4-sided, convex polygon.
1455
+ #
1456
+ # @param p1 [OpenStudio::Point3dVector] OpenStudio Point3D vector/array
1457
+ # @param w [Float] offset width (min: 0.0254m)
1458
+ # @param v [Integer] OpenStudio SDK version, eg '321' for 'v3.2.1' (optional)
1459
+ #
1460
+ # @return [OpenStudio::Point3dVector] offset points if successful
1461
+ # @return [OpenStudio::Point3dVector] original points if invalid input
1462
+ def offset(p1 = [], w = 0, v = 0)
1463
+ mth = "TBD::#{__callee__}"
1464
+ cl = OpenStudio::Point3d
1465
+ vrsn = OpenStudio.openStudioVersion.split(".").map(&:to_i).join.to_i
1466
+
1467
+ valid = p1.is_a?(OpenStudio::Point3dVector) || p1.is_a?(Array)
1468
+ return mismatch("pts", p1, cl1, mth, DBG, p1) unless valid
1469
+ return empty("pts", mth, ERR, p1) if p1.empty?
1470
+
1471
+ valid = p1.size == 3 || p1.size == 4
1472
+ iv = true if p1.size == 4
1473
+ return invalid("pts", mth, 1, DBG, p1) unless valid
1474
+ return invalid("width", mth, 2, DBG, p1) unless w.respond_to?(:to_f)
1475
+
1476
+ w = w.to_f
1477
+ return p1 if w < 0.0254
1478
+
1479
+ v = v.to_i if v.respond_to?(:to_i)
1480
+ v = 0 unless v.respond_to?(:to_i)
1481
+ v = vrsn if v.zero?
1482
+
1483
+ p1.each { |x| return mismatch("p", x, cl, mth, ERR, p1) unless x.is_a?(cl) }
1484
+
1485
+ unless v < 340
1486
+ # XY-plane transformation matrix ... needs to be clockwise for boost.
1487
+ ft = OpenStudio::Transformation::alignFace(p1)
1488
+ ft_pts = flatZ( (ft.inverse * p1) )
1489
+ return p1 if ft_pts.empty?
1490
+
1491
+ cw = OpenStudio::pointInPolygon(ft_pts.first, ft_pts, TOL)
1492
+ ft_pts = flatZ( (ft.inverse * p1).reverse ) unless cw
1493
+ offset = OpenStudio.buffer(ft_pts, w, TOL)
1494
+ return p1 if offset.empty?
1495
+
1496
+ offset = offset.get
1497
+ offset = ft * offset if cw
1498
+ offset = (ft * offset).reverse unless cw
1499
+
1500
+ pz = OpenStudio::Point3dVector.new
1501
+ offset.each { |o| pz << OpenStudio::Point3d.new(o.x, o.y, o.z ) }
1502
+
1503
+ return pz
1504
+ else # brute force approach
1505
+ pz = {}
1506
+ pz[:A] = {}
1507
+ pz[:B] = {}
1508
+ pz[:C] = {}
1509
+ pz[:D] = {} if iv
1510
+
1511
+ pz[:A][:p] = OpenStudio::Point3d.new(p1[0].x, p1[0].y, p1[0].z)
1512
+ pz[:B][:p] = OpenStudio::Point3d.new(p1[1].x, p1[1].y, p1[1].z)
1513
+ pz[:C][:p] = OpenStudio::Point3d.new(p1[2].x, p1[2].y, p1[2].z)
1514
+ pz[:D][:p] = OpenStudio::Point3d.new(p1[3].x, p1[3].y, p1[3].z) if iv
1515
+
1516
+ pzAp = pz[:A][:p]
1517
+ pzBp = pz[:B][:p]
1518
+ pzCp = pz[:C][:p]
1519
+ pzDp = pz[:D][:p] if iv
1520
+
1521
+ # Generate vector pairs, from next point & from previous point.
1522
+ # :f_n : "from next"
1523
+ # :f_p : "from previous"
1524
+ #
1525
+ #
1526
+ #
1527
+ #
1528
+ #
1529
+ #
1530
+ # A <---------- B
1531
+ # ^
1532
+ # \
1533
+ # \
1534
+ # C (or D)
1535
+ #
1536
+ pz[:A][:f_n] = pzAp - pzBp
1537
+ pz[:A][:f_p] = pzAp - pzCp unless iv
1538
+ pz[:A][:f_p] = pzAp - pzDp if iv
1539
+
1540
+ pz[:B][:f_n] = pzBp - pzCp
1541
+ pz[:B][:f_p] = pzBp - pzAp
1542
+
1543
+ pz[:C][:f_n] = pzCp - pzAp unless iv
1544
+ pz[:C][:f_n] = pzCp - pzDp if iv
1545
+ pz[:C][:f_p] = pzCp - pzBp
1546
+
1547
+ pz[:D][:f_n] = pzDp - pzAp if iv
1548
+ pz[:D][:f_p] = pzDp - pzCp if iv
1549
+
1550
+ # Generate 3D plane from vectors.
1551
+ #
1552
+ #
1553
+ # | <<< 3D plane ... from point A, with normal B>A
1554
+ # |
1555
+ # |
1556
+ # |
1557
+ # <---------- A <---------- B
1558
+ # |\
1559
+ # | \
1560
+ # | \
1561
+ # | C (or D)
1562
+ #
1563
+ pz[:A][:pl_f_n] = OpenStudio::Plane.new(pzAp, pz[:A][:f_n])
1564
+ pz[:A][:pl_f_p] = OpenStudio::Plane.new(pzAp, pz[:A][:f_p])
1565
+
1566
+ pz[:B][:pl_f_n] = OpenStudio::Plane.new(pzBp, pz[:B][:f_n])
1567
+ pz[:B][:pl_f_p] = OpenStudio::Plane.new(pzBp, pz[:B][:f_p])
1568
+
1569
+ pz[:C][:pl_f_n] = OpenStudio::Plane.new(pzCp, pz[:C][:f_n])
1570
+ pz[:C][:pl_f_p] = OpenStudio::Plane.new(pzCp, pz[:C][:f_p])
1571
+
1572
+ pz[:D][:pl_f_n] = OpenStudio::Plane.new(pzDp, pz[:D][:f_n]) if iv
1573
+ pz[:D][:pl_f_p] = OpenStudio::Plane.new(pzDp, pz[:D][:f_p]) if iv
1574
+
1575
+ # Project an extended point (pC) unto 3D plane.
1576
+ #
1577
+ # pC <<< projected unto extended B>A 3D plane
1578
+ # eC |
1579
+ # \ |
1580
+ # \ |
1581
+ # \|
1582
+ # <---------- A <---------- B
1583
+ # |\
1584
+ # | \
1585
+ # | \
1586
+ # | C (or D)
1587
+ #
1588
+ pz[:A][:p_n_pl] = pz[:A][:pl_f_n].project(pz[:A][:p] + pz[:A][:f_p])
1589
+ pz[:A][:n_p_pl] = pz[:A][:pl_f_p].project(pz[:A][:p] + pz[:A][:f_n])
1590
+
1591
+ pz[:B][:p_n_pl] = pz[:B][:pl_f_n].project(pz[:B][:p] + pz[:B][:f_p])
1592
+ pz[:B][:n_p_pl] = pz[:B][:pl_f_p].project(pz[:B][:p] + pz[:B][:f_n])
1593
+
1594
+ pz[:C][:p_n_pl] = pz[:C][:pl_f_n].project(pz[:C][:p] + pz[:C][:f_p])
1595
+ pz[:C][:n_p_pl] = pz[:C][:pl_f_p].project(pz[:C][:p] + pz[:C][:f_n])
1596
+
1597
+ pz[:D][:p_n_pl] = pz[:D][:pl_f_n].project(pz[:D][:p] + pz[:D][:f_p]) if iv
1598
+ pz[:D][:n_p_pl] = pz[:D][:pl_f_p].project(pz[:D][:p] + pz[:D][:f_n]) if iv
1599
+
1600
+ # Generate vector from point (e.g. A) to projected extended point (pC).
1601
+ #
1602
+ # pC
1603
+ # eC ^
1604
+ # \ |
1605
+ # \ |
1606
+ # \|
1607
+ # <---------- A <---------- B
1608
+ # |\
1609
+ # | \
1610
+ # | \
1611
+ # | C (or D)
1612
+ #
1613
+ pz[:A][:n_p_n_pl] = pz[:A][:p_n_pl] - pzAp
1614
+ pz[:A][:n_n_p_pl] = pz[:A][:n_p_pl] - pzAp
1615
+
1616
+ pz[:B][:n_p_n_pl] = pz[:B][:p_n_pl] - pzBp
1617
+ pz[:B][:n_n_p_pl] = pz[:B][:n_p_pl] - pzBp
1618
+
1619
+ pz[:C][:n_p_n_pl] = pz[:C][:p_n_pl] - pzCp
1620
+ pz[:C][:n_n_p_pl] = pz[:C][:n_p_pl] - pzCp
1621
+
1622
+ pz[:D][:n_p_n_pl] = pz[:D][:p_n_pl] - pzDp if iv
1623
+ pz[:D][:n_n_p_pl] = pz[:D][:n_p_pl] - pzDp if iv
1624
+
1625
+ # Fetch angle between both extended vectors (A>pC & A>pB),
1626
+ # ... then normalize (Cn).
1627
+ #
1628
+ # pC
1629
+ # eC ^
1630
+ # \ |
1631
+ # \ Cn
1632
+ # \|
1633
+ # <---------- A <---------- B
1634
+ # |\
1635
+ # | \
1636
+ # | \
1637
+ # | C (or D)
1638
+ #
1639
+ a1 = OpenStudio.getAngle(pz[:A][:n_p_n_pl], pz[:A][:n_n_p_pl])
1640
+ a2 = OpenStudio.getAngle(pz[:B][:n_p_n_pl], pz[:B][:n_n_p_pl])
1641
+ a3 = OpenStudio.getAngle(pz[:C][:n_p_n_pl], pz[:C][:n_n_p_pl])
1642
+ a4 = OpenStudio.getAngle(pz[:D][:n_p_n_pl], pz[:D][:n_n_p_pl]) if iv
1643
+
1644
+ # Generate new 3D points A', B', C' (and D') ... zigzag.
1645
+ #
1646
+ #
1647
+ #
1648
+ #
1649
+ # A' ---------------------- B'
1650
+ # \
1651
+ # \ A <---------- B
1652
+ # \ \
1653
+ # \ \
1654
+ # \ \
1655
+ # C' C
1656
+ pz[:A][:f_n].normalize
1657
+ pz[:A][:n_p_n_pl].normalize
1658
+ pzAp = pzAp + scalar(pz[:A][:n_p_n_pl], w)
1659
+ pzAp = pzAp + scalar(pz[:A][:f_n], w * Math.tan(a1/2))
1660
+
1661
+ pz[:B][:f_n].normalize
1662
+ pz[:B][:n_p_n_pl].normalize
1663
+ pzBp = pzBp + scalar(pz[:B][:n_p_n_pl], w)
1664
+ pzBp = pzBp + scalar(pz[:B][:f_n], w * Math.tan(a2/2))
1665
+
1666
+ pz[:C][:f_n].normalize
1667
+ pz[:C][:n_p_n_pl].normalize
1668
+ pzCp = pzCp + scalar(pz[:C][:n_p_n_pl], w)
1669
+ pzCp = pzCp + scalar(pz[:C][:f_n], w * Math.tan(a3/2))
1670
+
1671
+ pz[:D][:f_n].normalize if iv
1672
+ pz[:D][:n_p_n_pl].normalize if iv
1673
+ pzDp = pzDp + scalar(pz[:D][:n_p_n_pl], w) if iv
1674
+ pzDp = pzDp + scalar(pz[:D][:f_n], w * Math.tan(a4/2)) if iv
1675
+
1676
+ # Re-convert to OpenStudio 3D points.
1677
+ vec = OpenStudio::Point3dVector.new
1678
+ vec << OpenStudio::Point3d.new(pzAp.x, pzAp.y, pzAp.z)
1679
+ vec << OpenStudio::Point3d.new(pzBp.x, pzBp.y, pzBp.z)
1680
+ vec << OpenStudio::Point3d.new(pzCp.x, pzCp.y, pzCp.z)
1681
+ vec << OpenStudio::Point3d.new(pzDp.x, pzDp.y, pzDp.z) if iv
1682
+
1683
+ return vec
1684
+ end
1685
+ end
1686
+
1363
1687
  ##
1364
1688
  # Callback when other modules extend OSlg
1365
1689
  #
data/lib/osut/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # BSD 3-Clause License
2
2
  #
3
- # Copyright (c) 2022, Denis Bourgeois
3
+ # Copyright (c) 2022-2023, Denis Bourgeois
4
4
  # All rights reserved.
5
5
  #
6
6
  # Redistribution and use in source and binary forms, with or without
@@ -29,5 +29,5 @@
29
29
  # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
30
 
31
31
  module OSut
32
- VERSION = "0.2.6".freeze
32
+ VERSION = "0.2.8".freeze
33
33
  end
data/lib/osut.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # BSD 3-Clause License
2
2
  #
3
- # Copyright (c) 2022, Denis Bourgeois
3
+ # Copyright (c) 2022-2023, Denis Bourgeois
4
4
  # All rights reserved.
5
5
  #
6
6
  # Redistribution and use in source and binary forms, with or without
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: osut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Bourgeois
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-15 00:00:00.000000000 Z
11
+ date: 2023-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oslg
@@ -90,7 +90,7 @@ licenses:
90
90
  - BSD-3-Clause
91
91
  metadata:
92
92
  homepage_uri: https://github.com/rd2/osut
93
- source_code_uri: https://github.com/rd2/osut/tree/v0.2.6
93
+ source_code_uri: https://github.com/rd2/osut/tree/v0.2.8
94
94
  bug_tracker_uri: https://github.com/rd2/osut/issues
95
95
  post_install_message:
96
96
  rdoc_options: []