osut 0.2.6 → 0.2.8

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