tbd 3.4.4 → 3.4.5

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,6 +1,6 @@
1
1
  # BSD 3-Clause License
2
2
  #
3
- # Copyright (c) 2022-2024, Denis Bourgeois
3
+ # Copyright (c) 2022-2025, Denis Bourgeois
4
4
  # All rights reserved.
5
5
  #
6
6
  # Redistribution and use in source and binary forms, with or without
@@ -220,13 +220,14 @@ module OSut
220
220
  chk = @@uo.keys.include?(specs[:type])
221
221
  return invalid("surface type", mth, 2, ERR) unless chk
222
222
 
223
- specs[:uo] = @@uo[ specs[:type] ] unless specs.key?(:uo)
223
+ specs[:uo] = @@uo[ specs[:type] ] unless specs.key?(:uo) # can be nil
224
224
  u = specs[:uo]
225
225
 
226
226
  if u
227
- return mismatch("#{id} Uo", u, Numeric, mth) unless u.is_a?(Numeric)
228
- return invalid("#{id} Uo (> 5.678)", mth, 2, ERR) if u > 5.678
229
- return negative("#{id} Uo" , mth, ERR) if u < 0
227
+ return mismatch("#{id} Uo", u, Numeric, mth) unless u.is_a?(Numeric)
228
+ return invalid("#{id} Uo (> 5.678)", mth, 2, ERR) if u > 5.678
229
+ return zero("#{id} Uo", mth, ERR) if u.round(2) == 0.00
230
+ return negative("#{id} Uo", mth, ERR) if u < 0
230
231
  end
231
232
 
232
233
  # Optional specs. Log/reset if invalid.
@@ -466,14 +467,14 @@ module OSut
466
467
  a[:compo ][:d ] = d
467
468
  a[:compo ][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
468
469
  when :window
469
- a[:glazing][:u ] = specs[:uo ]
470
+ a[:glazing][:u ] = u ? u : @@uo[:window]
470
471
  a[:glazing][:shgc] = 0.450
471
472
  a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
472
473
  a[:glazing][:id ] = "OSut|window"
473
474
  a[:glazing][:id ] += "|U#{format('%.1f', a[:glazing][:u])}"
474
475
  a[:glazing][:id ] += "|SHGC#{format('%d', a[:glazing][:shgc]*100)}"
475
476
  when :skylight
476
- a[:glazing][:u ] = specs[:uo ]
477
+ a[:glazing][:u ] = u ? u : @@uo[:skylight]
477
478
  a[:glazing][:shgc] = 0.450
478
479
  a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
479
480
  a[:glazing][:id ] = "OSut|skylight"
@@ -482,25 +483,11 @@ module OSut
482
483
  end
483
484
 
484
485
  # Initiate layers.
485
- glazed = true
486
- glazed = false if a[:glazing].empty?
487
- layers = OpenStudio::Model::OpaqueMaterialVector.new unless glazed
488
- layers = OpenStudio::Model::FenestrationMaterialVector.new if glazed
486
+ unglazed = a[:glazing].empty? ? true : false
489
487
 
490
- if glazed
491
- u = a[:glazing][:u ]
492
- shgc = a[:glazing][:shgc]
493
- lyr = model.getSimpleGlazingByName(a[:glazing][:id])
494
-
495
- if lyr.empty?
496
- lyr = OpenStudio::Model::SimpleGlazing.new(model, u, shgc)
497
- lyr.setName(a[:glazing][:id])
498
- else
499
- lyr = lyr.get
500
- end
488
+ if unglazed
489
+ layers = OpenStudio::Model::OpaqueMaterialVector.new
501
490
 
502
- layers << lyr
503
- else
504
491
  # Loop through each layer spec, and generate construction.
505
492
  a.each do |i, l|
506
493
  next if l.empty?
@@ -524,44 +511,68 @@ module OSut
524
511
 
525
512
  layers << lyr
526
513
  end
514
+ else
515
+ layers = OpenStudio::Model::FenestrationMaterialVector.new
516
+
517
+ u0 = a[:glazing][:u ]
518
+ shgc = a[:glazing][:shgc]
519
+ lyr = model.getSimpleGlazingByName(a[:glazing][:id])
520
+
521
+ if lyr.empty?
522
+ lyr = OpenStudio::Model::SimpleGlazing.new(model, u0, shgc)
523
+ lyr.setName(a[:glazing][:id])
524
+ else
525
+ lyr = lyr.get
526
+ end
527
+
528
+ layers << lyr
527
529
  end
528
530
 
529
531
  c = OpenStudio::Model::Construction.new(layers)
530
532
  c.setName(id)
531
533
 
532
534
  # Adjust insulating layer thickness or conductivity to match requested Uo.
533
- unless glazed
534
- ro = 0
535
- ro = 1 / specs[:uo] - @@film[ specs[:type] ] if specs[:uo]
536
-
537
- if specs[:type] == :door # 1x layer, adjust conductivity
538
- layer = c.getLayer(0).to_StandardOpaqueMaterial
539
- return invalid("#{id} standard material?", mth, 0) if layer.empty?
540
-
541
- layer = layer.get
542
- k = layer.thickness / ro
543
- layer.setConductivity(k)
544
- elsif ro > 0 # multiple layers, adjust insulating layer thickness
545
- lyr = insulatingLayer(c)
546
- return invalid("#{id} construction", mth, 0) if lyr[:index].nil?
547
- return invalid("#{id} construction", mth, 0) if lyr[:type ].nil?
548
- return invalid("#{id} construction", mth, 0) if lyr[:r ].zero?
549
-
550
- index = lyr[:index]
551
- layer = c.getLayer(index).to_StandardOpaqueMaterial
552
- return invalid("#{id} material @#{index}", mth, 0) if layer.empty?
553
-
554
- layer = layer.get
555
- k = layer.conductivity
556
- d = (ro - rsi(c) + lyr[:r]) * k
557
- return invalid("#{id} adjusted m", mth, 0) if d < 0.03
558
-
559
- nom = "OSut|"
560
- nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "")
561
- nom += "|"
562
- nom += format("%03d", d*1000)[-3..-1]
563
- layer.setName(nom) if model.getStandardOpaqueMaterialByName(nom).empty?
564
- layer.setThickness(d)
535
+ if u and unglazed
536
+ ro = 1 / u - film
537
+
538
+ if ro > 0
539
+ if specs[:type] == :door # 1x layer, adjust conductivity
540
+ layer = c.getLayer(0).to_StandardOpaqueMaterial
541
+ return invalid("#{id} standard material?", mth, 0) if layer.empty?
542
+
543
+ layer = layer.get
544
+ k = layer.thickness / ro
545
+ layer.setConductivity(k)
546
+ else # multiple layers, adjust insulating layer thickness
547
+ lyr = insulatingLayer(c)
548
+ return invalid("#{id} construction", mth, 0) if lyr[:index].nil?
549
+ return invalid("#{id} construction", mth, 0) if lyr[:type ].nil?
550
+ return invalid("#{id} construction", mth, 0) if lyr[:r ].zero?
551
+
552
+ index = lyr[:index]
553
+ layer = c.getLayer(index).to_StandardOpaqueMaterial
554
+ return invalid("#{id} material @#{index}", mth, 0) if layer.empty?
555
+
556
+ layer = layer.get
557
+ k = layer.conductivity
558
+ d = (ro - rsi(c) + lyr[:r]) * k
559
+ return invalid("#{id} adjusted m", mth, 0) if d < 0.03
560
+
561
+ nom = "OSut|"
562
+ nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "")
563
+ nom += "|"
564
+ nom += format("%03d", d*1000)[-3..-1]
565
+
566
+ lyr = model.getStandardOpaqueMaterialByName(nom)
567
+
568
+ if lyr.empty?
569
+ layer.setName(nom)
570
+ layer.setThickness(d)
571
+ else
572
+ omat = lyr.get
573
+ c.setLayer(index, omat)
574
+ end
575
+ end
565
576
  end
566
577
  end
567
578
 
@@ -746,7 +757,7 @@ module OSut
746
757
  # Validates if a default construction set holds a base construction.
747
758
  #
748
759
  # @param set [OpenStudio::Model::DefaultConstructionSet] a default set
749
- # @param bse [OpensStudio::Model::ConstructionBase] a construction base
760
+ # @param bse [OpenStudio::Model::ConstructionBase] a construction base
750
761
  # @param gr [Bool] if ground-facing surface
751
762
  # @param ex [Bool] if exterior-facing surface
752
763
  # @param tp [#to_s] a surface type
@@ -1048,7 +1059,7 @@ module OSut
1048
1059
  return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
1049
1060
 
1050
1061
  id = lc.nameString
1051
- return mismatch(id, lc, cl1, mth, DBG, res) unless lc.is_a?(cl)
1062
+ return mismatch(id, lc, cl, mth, DBG, res) unless lc.is_a?(cl)
1052
1063
 
1053
1064
  lc.layers.each do |m|
1054
1065
  unless m.to_MasslessOpaqueMaterial.empty?
@@ -1102,7 +1113,7 @@ module OSut
1102
1113
  id = s.nameString
1103
1114
  m1 = "#{id}:spandrel"
1104
1115
  m2 = "#{id}:spandrel:boolean"
1105
- return mismatch(id, s, cl, mth) unless s.is_a?(cl)
1116
+ return mismatch(id, s, cl, mth, false) unless s.is_a?(cl)
1106
1117
 
1107
1118
  if s.additionalProperties.hasFeature("spandrel")
1108
1119
  val = s.additionalProperties.getFeatureAsBoolean("spandrel")
@@ -1129,7 +1140,7 @@ module OSut
1129
1140
  return invalid("subsurface", mth, 1, DBG, false) unless s.respond_to?(NS)
1130
1141
 
1131
1142
  id = s.nameString
1132
- return mismatch(id, s, cl, mth, false) unless s.is_a?(cl)
1143
+ return mismatch(id, s, cl, mth, DBG, false) unless s.is_a?(cl)
1133
1144
 
1134
1145
  # OpenStudio::Model::SubSurface.validSubSurfaceTypeValues
1135
1146
  # "FixedWindow" : fenestration
@@ -1422,7 +1433,15 @@ module OSut
1422
1433
  id = sched.nameString
1423
1434
  return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
1424
1435
 
1425
- vals = sched.timeSeries.values
1436
+ values = sched.timeSeries.values
1437
+
1438
+ values.each do |value|
1439
+ if value.respond_to?(:to_f)
1440
+ vals << value.to_f
1441
+ else
1442
+ invalid("numerical at #{i}", mth, 1, ERR)
1443
+ end
1444
+ end
1426
1445
 
1427
1446
  res[:min] = vals.min.is_a?(Numeric) ? vals.min : nil
1428
1447
  res[:max] = vals.max.is_a?(Numeric) ? vals.min : nil
@@ -1529,6 +1548,16 @@ module OSut
1529
1548
  res[:spt] = max if res[:spt] < max
1530
1549
  end
1531
1550
  end
1551
+
1552
+ unless sched.to_ScheduleInterval.empty?
1553
+ sched = sched.to_ScheduleInterval.get
1554
+ max = scheduleIntervalMinMax(sched)[:max]
1555
+
1556
+ if max
1557
+ res[:spt] = max unless res[:spt]
1558
+ res[:spt] = max if res[:spt] < max
1559
+ end
1560
+ end
1532
1561
  end
1533
1562
 
1534
1563
  return res if zone.thermostat.empty?
@@ -1586,6 +1615,16 @@ module OSut
1586
1615
  end
1587
1616
  end
1588
1617
 
1618
+ unless sched.to_ScheduleInterval.empty?
1619
+ sched = sched.to_ScheduleInterval.get
1620
+ max = scheduleIntervalMinMax(sched)[:max]
1621
+
1622
+ if max
1623
+ res[:spt] = max unless res[:spt]
1624
+ res[:spt] = max if res[:spt] < max
1625
+ end
1626
+ end
1627
+
1589
1628
  unless sched.to_ScheduleYear.empty?
1590
1629
  sched = sched.to_ScheduleYear.get
1591
1630
 
@@ -1707,6 +1746,16 @@ module OSut
1707
1746
  res[:spt] = min if res[:spt] > min
1708
1747
  end
1709
1748
  end
1749
+
1750
+ unless sched.to_ScheduleInterval.empty?
1751
+ sched = sched.to_ScheduleInterval.get
1752
+ min = scheduleIntervalMinMax(sched)[:min]
1753
+
1754
+ if min
1755
+ res[:spt] = min unless res[:spt]
1756
+ res[:spt] = min if res[:spt] > min
1757
+ end
1758
+ end
1710
1759
  end
1711
1760
 
1712
1761
  return res if zone.thermostat.empty?
@@ -1764,6 +1813,16 @@ module OSut
1764
1813
  end
1765
1814
  end
1766
1815
 
1816
+ unless sched.to_ScheduleInterval.empty?
1817
+ sched = sched.to_ScheduleInerval.get
1818
+ min = scheduleIntervalMinMax(sched)[:min]
1819
+
1820
+ if min
1821
+ res[:spt] = min unless res[:spt]
1822
+ res[:spt] = min if res[:spt] > min
1823
+ end
1824
+ end
1825
+
1767
1826
  unless sched.to_ScheduleYear.empty?
1768
1827
  sched = sched.to_ScheduleYear.get
1769
1828
 
@@ -2351,7 +2410,7 @@ module OSut
2351
2410
  # @return [OpenStudio::Vector3d] true normal vector
2352
2411
  # @return [nil] if invalid input (see logs)
2353
2412
  def trueNormal(s = nil, r = 0)
2354
- mth = "TBD::#{__callee__}"
2413
+ mth = "OSut::#{__callee__}"
2355
2414
  cl = OpenStudio::Model::PlanarSurface
2356
2415
  return mismatch("surface", s, cl, mth) unless s.is_a?(cl)
2357
2416
  return invalid("rotation angle", mth, 2) unless r.respond_to?(:to_f)
@@ -2384,31 +2443,31 @@ module OSut
2384
2443
 
2385
2444
  ##
2386
2445
  # Returns OpenStudio 3D points as an OpenStudio point vector, validating
2387
- # points in the process (if Array).
2446
+ # points in the process (e.g. if Array).
2388
2447
  #
2389
2448
  # @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
2390
2449
  #
2391
2450
  # @return [OpenStudio::Point3dVector] 3D vector (see logs if empty)
2392
2451
  def to_p3Dv(pts = nil)
2393
2452
  mth = "OSut::#{__callee__}"
2394
- cl1 = OpenStudio::Point3d
2395
- cl2 = OpenStudio::Point3dVector
2396
- cl3 = OpenStudio::Model::PlanarSurface
2397
- cl4 = Array
2398
2453
  v = OpenStudio::Point3dVector.new
2399
2454
 
2400
- if pts.is_a?(cl1)
2455
+ if pts.is_a?(OpenStudio::Point3d)
2401
2456
  v << pts
2402
2457
  return v
2458
+ elsif pts.is_a?(OpenStudio::Point3dVector)
2459
+ return pts
2460
+ elsif pts.is_a?(OpenStudio::Model::PlanarSurface)
2461
+ pts.vertices.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, pt.z) }
2462
+ return v
2403
2463
  end
2404
2464
 
2405
- return pts if pts.is_a?(cl2)
2406
- return pts.vertices if pts.is_a?(cl3)
2407
-
2408
- return mismatch("points", pts, cl1, mth, DBG, v) unless pts.is_a?(cl4)
2465
+ return mismatch("points", pts, Array, mth, DBG, v) unless pts.is_a?(Array)
2409
2466
 
2410
2467
  pts.each do |pt|
2411
- return mismatch("point", pt, cl4, mth, DBG, v) unless pt.is_a?(cl1)
2468
+ unless pt.is_a?(OpenStudio::Point3d)
2469
+ return mismatch("point", pt, OpenStudio::Point3d, mth, DBG, v)
2470
+ end
2412
2471
  end
2413
2472
 
2414
2473
  pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, pt.z) }
@@ -2684,7 +2743,7 @@ module OSut
2684
2743
 
2685
2744
  pair = pts.each_cons(2).find { |p1, _| same?(p1, pt) }
2686
2745
 
2687
- pair.nil? ? pts.first : pair.last
2746
+ pair.nil? ? pts[0] : pair[-1]
2688
2747
  end
2689
2748
 
2690
2749
  ##
@@ -2775,21 +2834,25 @@ module OSut
2775
2834
  # @param pts [Set<OpenStudio::Point3d] 3D points
2776
2835
  # @param n [#to_i] requested number of unique points (0 returns all)
2777
2836
  #
2778
- # @return [OpenStudio::Point3dVector] unique points (see logs if empty)
2779
- def getUniques(pts = nil, n = 0)
2837
+ # @return [OpenStudio::Point3dVector] unique points (see logs)
2838
+ def uniques(pts = nil, n = 0)
2780
2839
  mth = "OSut::#{__callee__}"
2781
2840
  pts = to_p3Dv(pts)
2782
- ok = n.respond_to?(:to_i)
2783
2841
  v = OpenStudio::Point3dVector.new
2784
2842
  return v if pts.empty?
2785
- return mismatch("n unique points", n, Integer, mth, DBG, v) unless ok
2843
+
2844
+ if n.is_a?(Numeric)
2845
+ n = n.to_i
2846
+ else
2847
+ mismatch("n points", n, Integer, mth, DBG)
2848
+ n = 0
2849
+ end
2786
2850
 
2787
2851
  pts.each { |pt| v << pt unless holds?(v, pt) }
2788
2852
 
2789
- n = n.to_i
2790
- n = 0 unless n.abs < v.size
2791
- v = v[0..n] if n > 0
2792
- v = v[n..-1] if n < 0
2853
+ n = 0 if n.abs > v.size
2854
+ v = v[0..n-1] if n > 0
2855
+ v = v[n..-1] if n < 0
2793
2856
 
2794
2857
  v
2795
2858
  end
@@ -2803,10 +2866,10 @@ module OSut
2803
2866
  # @param pts [Set<OpenStudio::Point3d>] 3D points
2804
2867
  #
2805
2868
  # @return [OpenStudio::Point3dVectorVector] line segments (see logs if empty)
2806
- def getSegments(pts = nil)
2869
+ def segments(pts = nil)
2807
2870
  mth = "OSut::#{__callee__}"
2808
2871
  vv = OpenStudio::Point3dVectorVector.new
2809
- pts = getUniques(pts)
2872
+ pts = uniques(pts)
2810
2873
  return vv if pts.size < 2
2811
2874
 
2812
2875
  pts.each_with_index do |p1, i1|
@@ -2833,7 +2896,6 @@ module OSut
2833
2896
  # @return [false] if invalid input (see logs)
2834
2897
  def segment?(pts = nil)
2835
2898
  pts = to_p3Dv(pts)
2836
- return false if pts.empty?
2837
2899
  return false unless pts.size == 2
2838
2900
  return false if same?(pts[0], pts[1])
2839
2901
 
@@ -2850,10 +2912,10 @@ module OSut
2850
2912
  # @param pts [OpenStudio::Point3dVector] 3D points
2851
2913
  #
2852
2914
  # @return [OpenStudio::Point3dVectorVector] triads (see logs if empty)
2853
- def getTriads(pts = nil, co = false)
2915
+ def triads(pts = nil, co = false)
2854
2916
  mth = "OSut::#{__callee__}"
2855
2917
  vv = OpenStudio::Point3dVectorVector.new
2856
- pts = getUniques(pts)
2918
+ pts = uniques(pts)
2857
2919
  return vv if pts.size < 2
2858
2920
 
2859
2921
  pts.each_with_index do |p1, i1|
@@ -2880,7 +2942,7 @@ module OSut
2880
2942
  # @param pts [Set<OpenStudio::Point3d>] 3D points
2881
2943
  #
2882
2944
  # @return [Bool] whether set is a valid triad (i.e. a trio of 3D points)
2883
- # @return [false] if invalid input (see logs)
2945
+ # @return [false] if invalid input (see 'to_p3Dv' logs)
2884
2946
  def triad?(pts = nil)
2885
2947
  pts = to_p3Dv(pts)
2886
2948
  return false if pts.empty?
@@ -2905,18 +2967,17 @@ module OSut
2905
2967
  mth = "OSut::#{__callee__}"
2906
2968
  cl1 = OpenStudio::Point3d
2907
2969
  cl2 = OpenStudio::Point3dVector
2908
- return mismatch( "point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1)
2909
- return mismatch("segment", sg, cl2, mth, DBG, false) unless segment?(sg)
2910
-
2970
+ return mismatch("point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1)
2971
+ return false unless segment?(sg)
2911
2972
  return true if holds?(sg, p0)
2912
2973
 
2913
- a = sg.first
2914
- b = sg.last
2974
+ a = sg[ 0]
2975
+ b = sg[-1]
2915
2976
  ab = b - a
2916
2977
  abn = b - a
2917
2978
  abn.normalize
2918
2979
  ap = p0 - a
2919
- sp = ap.dot(abn)
2980
+ sp = ap.dot(abn)
2920
2981
  return false if sp < 0
2921
2982
 
2922
2983
  apd = scalar(abn, sp)
@@ -2941,9 +3002,9 @@ module OSut
2941
3002
  mth = "OSut::#{__callee__}"
2942
3003
  cl1 = OpenStudio::Point3d
2943
3004
  cl2 = OpenStudio::Point3dVectorVector
2944
- sgs = sgs.is_a?(cl2) ? sgs : getSegments(sgs)
2945
- return empty("segments", mth, DBG, false) if sgs.empty?
2946
- return mismatch("point", p0, cl, mth, DBG, false) unless p0.is_a?(cl1)
3005
+ sgs = sgs.is_a?(cl2) ? sgs : segments(sgs)
3006
+ return empty("segments", mth, DBG, false) if sgs.empty?
3007
+ return mismatch("point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1)
2947
3008
 
2948
3009
  sgs.each { |sg| return true if pointAlongSegment?(p0, sg) }
2949
3010
 
@@ -2958,9 +3019,9 @@ module OSut
2958
3019
  #
2959
3020
  # @return [OpenStudio::Point3d] point of intersection of both lines
2960
3021
  # @return [nil] if no intersection, equal, or invalid input (see logs)
2961
- def getLineIntersection(s1 = [], s2 = [])
2962
- s1 = getSegments(s1)
2963
- s2 = getSegments(s2)
3022
+ def lineIntersection(s1 = [], s2 = [])
3023
+ s1 = segments(s1)
3024
+ s2 = segments(s2)
2964
3025
  return nil if s1.empty?
2965
3026
  return nil if s2.empty?
2966
3027
 
@@ -2971,10 +3032,10 @@ module OSut
2971
3032
  return nil if same?(s1, s2)
2972
3033
  return nil if same?(s1, s2.to_a.reverse)
2973
3034
 
2974
- a1 = s1[0]
2975
- a2 = s1[1]
2976
- b1 = s2[0]
2977
- b2 = s2[1]
3035
+ a1 = s1.first
3036
+ b1 = s2.first
3037
+ a2 = s1.last
3038
+ b2 = s2.last
2978
3039
 
2979
3040
  # Matching segment endpoints?
2980
3041
  return a1 if same?(a1, b1)
@@ -2983,18 +3044,18 @@ module OSut
2983
3044
  return a2 if same?(a2, b2)
2984
3045
 
2985
3046
  # Segment endpoint along opposite segment?
2986
- return a1 if pointAlongSegments?(a1, s2)
2987
- return a2 if pointAlongSegments?(a2, s2)
2988
- return b1 if pointAlongSegments?(b1, s1)
2989
- return b2 if pointAlongSegments?(b2, s1)
3047
+ return a1 if pointAlongSegment?(a1, s2)
3048
+ return a2 if pointAlongSegment?(a2, s2)
3049
+ return b1 if pointAlongSegment?(b1, s1)
3050
+ return b2 if pointAlongSegment?(b2, s1)
2990
3051
 
2991
- # Line segments as vectors. Skip if colinear.
3052
+ # Line segments as vectors. Skip if collinear or parallel.
2992
3053
  a = a2 - a1
2993
3054
  b = b2 - b1
2994
3055
  xab = a.cross(b)
2995
3056
  return nil if xab.length.round(4) < TOL2
2996
3057
 
2997
- # Link 1st point to other segment endpoints as vectors. Must be coplanar.
3058
+ # Link 1st point to other segment endpoints, as vectors. Must be coplanar.
2998
3059
  a1b1 = b1 - a1
2999
3060
  a1b2 = b2 - a1
3000
3061
  xa1b1 = a.cross(a1b1)
@@ -3035,7 +3096,7 @@ module OSut
3035
3096
  return nil if a.dot(p0 - a1) < 0
3036
3097
 
3037
3098
  # Ensure intersection is sandwiched between endpoints.
3038
- return nil unless pointAlongSegments?(p0, s2) && pointAlongSegments?(p0, s1)
3099
+ return nil unless pointAlongSegment?(p0, s2) && pointAlongSegment?(p0, s1)
3039
3100
 
3040
3101
  p0
3041
3102
  end
@@ -3049,14 +3110,14 @@ module OSut
3049
3110
  # @return [Bool] whether 3D line intersects 3D segments
3050
3111
  # @return [false] if invalid input (see logs)
3051
3112
  def lineIntersects?(l = [], s = [])
3052
- l = getSegments(l)
3053
- s = getSegments(s)
3113
+ l = segments(l)
3114
+ s = segments(s)
3054
3115
  return nil if l.empty?
3055
3116
  return nil if s.empty?
3056
3117
 
3057
3118
  l = l.first
3058
3119
 
3059
- s.each { |segment| return true if getLineIntersection(l, segment) }
3120
+ s.each { |segment| return true if lineIntersection(l, segment) }
3060
3121
 
3061
3122
  false
3062
3123
  end
@@ -3142,28 +3203,33 @@ module OSut
3142
3203
  end
3143
3204
 
3144
3205
  ##
3145
- # Returns sequential non-collinear points in an OpenStudio 3D point vector.
3206
+ # Returns non-collinear points in an OpenStudio 3D point vector.
3146
3207
  #
3147
3208
  # @param pts [Set<OpenStudio::Point3d] 3D points
3148
3209
  # @param n [#to_i] requested number of non-collinears (0 returns all)
3149
3210
  #
3150
- # @return [OpenStudio::Point3dVector] non-collinears (see logs if empty)
3151
- def getNonCollinears(pts = nil, n = 0)
3211
+ # @return [OpenStudio::Point3dVector] non-collinears (see logs)
3212
+ def nonCollinears(pts = nil, n = 0)
3152
3213
  mth = "OSut::#{__callee__}"
3153
- pts = getUniques(pts)
3154
- ok = n.respond_to?(:to_i)
3155
- v = OpenStudio::Point3dVector.new
3156
3214
  a = []
3215
+ pts = uniques(pts)
3157
3216
  return pts if pts.size < 3
3158
- return mismatch("n non-collinears", n, Integer, mth, DBG, v) unless ok
3217
+
3218
+ if n.is_a?(Numeric)
3219
+ n = n.to_i
3220
+ else
3221
+ mismatch("n points", n, Integer, mth, DBG)
3222
+ n = 0
3223
+ end
3159
3224
 
3160
3225
  # Evaluate cross product of vectors of 3x sequential points.
3161
3226
  pts.each_with_index do |p2, i2|
3162
- i1 = i2 - 1
3163
- i3 = i2 + 1
3164
- i3 = 0 if i3 == pts.size
3165
- p1 = pts[i1]
3166
- p3 = pts[i3]
3227
+ i1 = i2 - 1
3228
+ i3 = i2 + 1
3229
+ i3 = 0 if i3 == pts.size
3230
+ p1 = pts[i1]
3231
+ p3 = pts[i3]
3232
+
3167
3233
  v13 = p3 - p1
3168
3234
  v12 = p2 - p1
3169
3235
  next if v12.cross(v13).length < TOL2
@@ -3171,36 +3237,47 @@ module OSut
3171
3237
  a << p2
3172
3238
  end
3173
3239
 
3174
- if holds?(a, pts[0])
3240
+ if a.include?(pts[0])
3175
3241
  a = a.rotate(-1) unless same?(a[0], pts[0])
3176
3242
  end
3177
3243
 
3178
- n = n.to_i
3179
- a = a[0..n-1] if n > 0
3180
- a = a[n-1..-1] if n < 0
3244
+ n = 0 if n.abs > a.size
3245
+ a = a[0..n-1] if n > 0
3246
+ a = a[n..-1] if n < 0
3181
3247
 
3182
3248
  to_p3Dv(a)
3183
3249
  end
3184
3250
 
3185
3251
  ##
3186
- # Returns sequential collinear points in an OpenStudio 3D point vector.
3252
+ # Returns collinear points in an OpenStudio 3D point vector.
3187
3253
  #
3188
3254
  # @param pts [Set<OpenStudio::Point3d] 3D points
3189
3255
  # @param n [#to_i] requested number of collinears (0 returns all)
3190
3256
  #
3191
- # @return [OpenStudio::Point3dVector] collinears (see logs if empty)
3192
- def getCollinears(pts = nil, n = 0)
3257
+ # @return [OpenStudio::Point3dVector] collinears (see logs)
3258
+ def collinears(pts = nil, n = 0)
3193
3259
  mth = "OSut::#{__callee__}"
3194
- pts = getUniques(pts)
3195
- ok = n.respond_to?(:to_i)
3196
- v = OpenStudio::Point3dVector.new
3260
+ a = OpenStudio::Point3dVector.new
3261
+ pts = uniques(pts)
3197
3262
  return pts if pts.size < 3
3198
- return mismatch("n collinears", n, Integer, mth, DBG, v) unless ok
3199
3263
 
3200
- ncolls = getNonCollinears(pts)
3201
- return pts if ncolls.empty?
3264
+ if n.is_a?(Numeric)
3265
+ n = n.to_i
3266
+ else
3267
+ mismatch("n points", n, Integer, mth, DBG, pts)
3268
+ n = 0
3269
+ end
3270
+
3271
+ ncolls = nonCollinears(pts)
3272
+ return a if ncolls.empty?
3273
+
3274
+ a = pts.delete_if { |pt| holds?(ncolls, pt) }
3202
3275
 
3203
- to_p3Dv( pts.delete_if { |pt| holds?(ncolls, pt) } )
3276
+ n = 0 if n.abs > a.size
3277
+ a = a[0..n-1] if n > 0
3278
+ a = a[n..-1] if n < 0
3279
+
3280
+ a
3204
3281
  end
3205
3282
 
3206
3283
  ##
@@ -3237,7 +3314,7 @@ module OSut
3237
3314
 
3238
3315
  # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
3239
3316
  # Minimum 3 points?
3240
- p3 = getNonCollinears(pts, 3)
3317
+ p3 = nonCollinears(pts, 3)
3241
3318
  return empty("polygon (non-collinears < 3)", mth, ERR, v) if p3.size < 3
3242
3319
 
3243
3320
  # Coplanar?
@@ -3268,8 +3345,8 @@ module OSut
3268
3345
  # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
3269
3346
  # Ensure uniqueness and/or non-collinearity. Preserve original sequence.
3270
3347
  p0 = a.first
3271
- a = getUniques(a).to_a if uq
3272
- a = getNonCollinears(a).to_a if co
3348
+ a = uniques(a).to_a if uq
3349
+ a = nonCollinears(a).to_a if co
3273
3350
  i0 = a.index { |pt| same?(pt, p0) }
3274
3351
  a = a.rotate(i0) unless i0.nil?
3275
3352
 
@@ -3278,7 +3355,7 @@ module OSut
3278
3355
  if vx && a.size > 3
3279
3356
  zen = OpenStudio::Point3d.new(0, 0, 1000)
3280
3357
 
3281
- getTriads(a).each do |trio|
3358
+ triads(a).each do |trio|
3282
3359
  p1 = trio[0]
3283
3360
  p2 = trio[1]
3284
3361
  p3 = trio[2]
@@ -3344,31 +3421,31 @@ module OSut
3344
3421
  return false unless pl.pointOnPlane(p0)
3345
3422
 
3346
3423
  entirely = false unless [true, false].include?(entirely)
3347
- segments = getSegments(s)
3424
+ sgments = segments(s)
3348
3425
 
3349
3426
  # Along polygon edges, or near vertices?
3350
- if pointAlongSegments?(p0, segments)
3427
+ if pointAlongSegments?(p0, sgments)
3351
3428
  return false if entirely
3352
3429
  return true unless entirely
3353
3430
  end
3354
3431
 
3355
- segments.each do |segment|
3432
+ sgments.each do |sgment|
3356
3433
  # - draw vector from segment midpoint to point
3357
3434
  # - scale 1000x (assuming no building surface would be 1km wide)
3358
3435
  # - convert vector to an independent line segment
3359
3436
  # - loop through polygon segments, tally the number of intersections
3360
3437
  # - avoid double-counting polygon vertices as intersections
3361
3438
  # - return false if number of intersections is even
3362
- mid = midpoint(segment.first, segment.last)
3439
+ mid = midpoint(sgment.first, sgment.last)
3363
3440
  mpV = scalar(mid - p0, 1000)
3364
3441
  p1 = p0 + mpV
3365
3442
  ctr = 0
3366
3443
 
3367
3444
  # Skip if ~collinear.
3368
- next if mpV.cross(segment.last - segment.first).length.round(4) < TOL2
3445
+ next if mpV.cross(sgment.last - sgment.first).length.round(4) < TOL2
3369
3446
 
3370
- segments.each do |sg|
3371
- intersect = getLineIntersection([p0, p1], sg)
3447
+ sgments.each do |sg|
3448
+ intersect = lineIntersection([p0, p1], sg)
3372
3449
  next unless intersect
3373
3450
 
3374
3451
  # Skip test altogether if one of the polygon vertices.
@@ -3518,7 +3595,7 @@ module OSut
3518
3595
  return false if pts.empty?
3519
3596
  return false unless rectangular?(pts)
3520
3597
 
3521
- getSegments(pts).each do |pt|
3598
+ segments(pts).each do |pt|
3522
3599
  l = (pt[1] - pt[0]).length
3523
3600
  d = l unless d
3524
3601
  return false unless l.round(2) == d.round(2)
@@ -3552,7 +3629,7 @@ module OSut
3552
3629
  p2.each { |p0| return false if pointWithinPolygon?(p0, p1, true) }
3553
3630
 
3554
3631
  # p1 segment mid-points must not lie OUTSIDE of p2.
3555
- getSegments(p1).each do |sg|
3632
+ segments(p1).each do |sg|
3556
3633
  mp = midpoint(sg.first, sg.last)
3557
3634
  return false unless pointWithinPolygon?(mp, p2)
3558
3635
  end
@@ -3595,22 +3672,17 @@ module OSut
3595
3672
  cw1 = clockwise?(p01)
3596
3673
  a1 = cw1 ? p01.to_a.reverse : p01.to_a
3597
3674
  a2 = p02.to_a
3598
- a2 = flatten(a2).to_a if flat
3599
- return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z)
3600
-
3601
- cw2 = clockwise?(a2)
3602
- a2 = a2.reverse if cw2
3603
3675
  else
3604
3676
  t = OpenStudio::Transformation.alignFace(p01)
3605
3677
  a1 = t.inverse * p01
3606
3678
  a2 = t.inverse * p02
3607
- a2 = flatten(a2).to_a if flat
3608
- return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z)
3609
-
3610
- cw2 = clockwise?(a2)
3611
- a2 = a2.reverse if cw2
3612
3679
  end
3613
3680
 
3681
+ a2 = flatten(a2).to_a if flat
3682
+ cw2 = clockwise?(a2)
3683
+ a2 = a2.reverse if cw2
3684
+ return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z)
3685
+
3614
3686
  # Return either (transformed) polygon if one fits into the other.
3615
3687
  p1t = p01
3616
3688
 
@@ -3690,7 +3762,7 @@ module OSut
3690
3762
  p2 = poly(p2)
3691
3763
  return face if p1.empty?
3692
3764
  return face if p2.empty?
3693
- return mismatch("ray", ray, cl, mth) unless ray.is_a?(cl)
3765
+ return mismatch("ray", ray, cl, mth, face) unless ray.is_a?(cl)
3694
3766
 
3695
3767
  # From OpenStudio SDK v3.7.0 onwards, one could/should rely on:
3696
3768
  #
@@ -4023,12 +4095,12 @@ module OSut
4023
4095
  #
4024
4096
  # @param [Set<OpenStudio::Point3d>] a triad (3D points)
4025
4097
  #
4026
- # @return [Set<OpenStudio::Point3D>] a rectangular ULC box (see logs if empty)
4098
+ # @return [Set<OpenStudio::Point3D>] a rectangular BLC box (see logs if empty)
4027
4099
  def triadBox(pts = nil)
4028
4100
  mth = "OSut::#{__callee__}"
4029
4101
  bkp = OpenStudio::Point3dVector.new
4030
4102
  box = []
4031
- pts = getNonCollinears(pts)
4103
+ pts = nonCollinears(pts)
4032
4104
  return bkp if pts.empty?
4033
4105
 
4034
4106
  t = xyz?(pts, :z) ? nil : OpenStudio::Transformation.alignFace(pts)
@@ -4065,7 +4137,7 @@ module OSut
4065
4137
  box << OpenStudio::Point3d.new(p1.x, p1.y, p1.z)
4066
4138
  box << OpenStudio::Point3d.new(p2.x, p2.y, p2.z)
4067
4139
  box << OpenStudio::Point3d.new(p3.x, p3.y, p3.z)
4068
- box = getNonCollinears(box, 4)
4140
+ box = nonCollinears(box, 4)
4069
4141
  return bkp unless box.size == 4
4070
4142
 
4071
4143
  box = blc(box)
@@ -4098,7 +4170,7 @@ module OSut
4098
4170
 
4099
4171
  # Generate vertical plane along longest segment.
4100
4172
  mpoints = []
4101
- sgs = getSegments(pts)
4173
+ sgs = segments(pts)
4102
4174
  longest = sgs.max_by { |s| OpenStudio.getDistanceSquared(s.first, s.last) }
4103
4175
  plane = verticalPlane(longest.first, longest.last)
4104
4176
 
@@ -4112,7 +4184,7 @@ module OSut
4112
4184
  box << mpoints.first
4113
4185
  box << mpoints.last
4114
4186
  box << plane.project(mpoints.last)
4115
- box = getNonCollinears(box).to_a
4187
+ box = nonCollinears(box).to_a
4116
4188
  return bkp unless box.size == 4
4117
4189
 
4118
4190
  box = clockwise?(box) ? blc(box.reverse) : blc(box)
@@ -4165,16 +4237,16 @@ module OSut
4165
4237
  aire = 0
4166
4238
 
4167
4239
  # PATH C : Right-angle, midpoint triad approach.
4168
- getSegments(pts).each do |sg|
4240
+ segments(pts).each do |sg|
4169
4241
  m0 = midpoint(sg.first, sg.last)
4170
4242
 
4171
- getSegments(pts).each do |seg|
4243
+ segments(pts).each do |seg|
4172
4244
  p1 = seg.first
4173
4245
  p2 = seg.last
4174
4246
  next if same?(p1, sg.first)
4175
4247
  next if same?(p1, sg.last)
4176
4248
  next if same?(p2, sg.first)
4177
- next if same?(p2, sg.first)
4249
+ next if same?(p2, sg.last)
4178
4250
 
4179
4251
  out = triadBox(OpenStudio::Point3dVector.new([m0, p1, p2]))
4180
4252
  next if out.empty?
@@ -4194,7 +4266,7 @@ module OSut
4194
4266
  end
4195
4267
 
4196
4268
  # PATH D : Right-angle triad approach, may override PATH C boxes.
4197
- getSegments(pts).each do |sg|
4269
+ segments(pts).each do |sg|
4198
4270
  p0 = sg.first
4199
4271
  p1 = sg.last
4200
4272
 
@@ -4227,7 +4299,7 @@ module OSut
4227
4299
  # PATH E : Medial box, segment approach.
4228
4300
  aire = 0
4229
4301
 
4230
- getSegments(pts).each do |sg|
4302
+ segments(pts).each do |sg|
4231
4303
  p0 = sg.first
4232
4304
  p1 = sg.last
4233
4305
 
@@ -4260,7 +4332,7 @@ module OSut
4260
4332
  # PATH F : Medial box, triad approach.
4261
4333
  aire = 0
4262
4334
 
4263
- getTriads(pts).each do |sg|
4335
+ triads(pts).each do |sg|
4264
4336
  p0 = sg[0]
4265
4337
  p1 = sg[1]
4266
4338
  p2 = sg[2]
@@ -4292,7 +4364,7 @@ module OSut
4292
4364
  holes = OpenStudio::Point3dVectorVector.new
4293
4365
 
4294
4366
  OpenStudio.computeTriangulation(outer, holes).each do |triangle|
4295
- getSegments(triangle).each do |sg|
4367
+ segments(triangle).each do |sg|
4296
4368
  p0 = sg.first
4297
4369
  p1 = sg.last
4298
4370
 
@@ -4349,7 +4421,7 @@ module OSut
4349
4421
  #
4350
4422
  # @return [Hash] :set, :box, :bbox, :t, :r & :o
4351
4423
  # @return [Hash] :set, :box, :bbox, :t, :r & :o (nil) if invalid (see logs)
4352
- def getRealignedFace(pts = nil, force = false)
4424
+ def realignedFace(pts = nil, force = false)
4353
4425
  mth = "OSut::#{__callee__}"
4354
4426
  out = { set: nil, box: nil, bbox: nil, t: nil, r: nil, o: nil }
4355
4427
  pts = poly(pts, false, true)
@@ -4372,11 +4444,11 @@ module OSut
4372
4444
  box = boundedBox(pts)
4373
4445
  return invalid("bounded box", mth, 0, DBG, out) if box.empty?
4374
4446
 
4375
- segments = getSegments(box)
4376
- return invalid("bounded box segments", mth, 0, DBG, out) if segments.empty?
4447
+ sgments = segments(box)
4448
+ return invalid("bounded box segments", mth, 0, DBG, out) if sgments.empty?
4377
4449
 
4378
4450
  # Deterministic ID of box rotation/translation 'origin'.
4379
- segments.each_with_index do |sg, idx|
4451
+ sgments.each_with_index do |sg, idx|
4380
4452
  sgs[sg] = {}
4381
4453
  sgs[sg][:idx] = idx
4382
4454
  sgs[sg][:mid] = midpoint(sg[0], sg[1])
@@ -4396,10 +4468,10 @@ module OSut
4396
4468
  i = sg0[:idx]
4397
4469
  end
4398
4470
 
4399
- k = i + 2 < segments.size ? i + 2 : i - 2
4471
+ k = i + 2 < sgments.size ? i + 2 : i - 2
4400
4472
 
4401
- origin = midpoint(segments[i][0], segments[i][1])
4402
- terminal = midpoint(segments[k][0], segments[k][1])
4473
+ origin = midpoint(sgments[i][0], sgments[i][1])
4474
+ terminal = midpoint(sgments[k][0], sgments[k][1])
4403
4475
  seg = terminal - origin
4404
4476
  right = OpenStudio::Point3d.new(origin.x + d, origin.y , 0) - origin
4405
4477
  north = OpenStudio::Point3d.new(origin.x, origin.y + d, 0) - origin
@@ -4441,7 +4513,7 @@ module OSut
4441
4513
  # @param pts [Set<OpenStudio::Point3d>] 3D points, once re/aligned
4442
4514
  # @param force [Bool] whether to force rotation of (narrow) bounded box
4443
4515
  #
4444
- # @return [Float] width al©ong X-axis, once re/aligned
4516
+ # @return [Float] width along X-axis, once re/aligned
4445
4517
  # @return [0.0] if invalid inputs
4446
4518
  def alignedWidth(pts = nil, force = false)
4447
4519
  mth = "OSut::#{__callee__}"
@@ -4453,7 +4525,7 @@ module OSut
4453
4525
  force = false
4454
4526
  end
4455
4527
 
4456
- pts = getRealignedFace(pts, force)[:set]
4528
+ pts = realignedFace(pts, force)[:set]
4457
4529
  return 0 if pts.size < 2
4458
4530
 
4459
4531
  pts.max_by(&:x).x - pts.min_by(&:x).x
@@ -4477,12 +4549,84 @@ module OSut
4477
4549
  force = false
4478
4550
  end
4479
4551
 
4480
- pts = getRealignedFace(pts, force)[:set]
4552
+ pts = realignedFace(pts, force)[:set]
4481
4553
  return 0 if pts.size < 2
4482
4554
 
4483
4555
  pts.max_by(&:y).y - pts.min_by(&:y).y
4484
4556
  end
4485
4557
 
4558
+ ##
4559
+ # Fetch a space's full height (in space coordinates). The solution considers
4560
+ # all surface types ("Floor" vs "Wall" vs "RoofCeiling").
4561
+ #
4562
+ # @param space [OpenStudio::Model::Space] a space
4563
+ #
4564
+ # @return [Float] full height of space (0 if invalid input)
4565
+ def spaceHeight(space = nil)
4566
+ return 0 unless space.is_a?(OpenStudio::Model::Space)
4567
+
4568
+ minZ = 10000
4569
+ maxZ = -10000
4570
+
4571
+ space.surfaces.each do |surface|
4572
+ minZ = [surface.vertices.min_by(&:z).z, minZ].min
4573
+ maxZ = [surface.vertices.max_by(&:z).z, maxZ].max
4574
+ end
4575
+
4576
+ maxZ < minZ ? 0 : maxZ - minZ
4577
+ end
4578
+
4579
+ ##
4580
+ # Fetch a space's width, based on the geometry of space floors.
4581
+ #
4582
+ # @param space [OpenStudio::Model::Space] a space
4583
+ #
4584
+ # @return [Float] width of a space (0 if invalid input)
4585
+ def spaceWidth(space = nil)
4586
+ return 0 unless space.is_a?(OpenStudio::Model::Space)
4587
+
4588
+ floors = facets(space, "all", "Floor")
4589
+ return 0 if floors.empty?
4590
+
4591
+ # Automatically determining a space's "width" is not straightforward:
4592
+ # - a space may hold multiple floor surfaces at various Z-axis levels
4593
+ # - a space may hold multiple floor surfaces, with unique "widths"
4594
+ # - a floor surface may expand/contract (in "width") along its length.
4595
+ #
4596
+ # First, attempt to merge all floor surfaces together as 1x polygon:
4597
+ # - select largest floor surface (in area)
4598
+ # - determine its 3D plane
4599
+ # - retain only other floor surfaces sharing same 3D plane
4600
+ # - recover potential union between floor surfaces
4601
+ # - fall back to largest floor surface if invalid union
4602
+ # - return width of largest bounded box
4603
+ floors = floors.sort_by(&:grossArea).reverse
4604
+ floor = floors.first
4605
+ plane = floor.plane
4606
+ t = OpenStudio::Transformation.alignFace(floor.vertices)
4607
+ polyg = poly(floor, false, true, true, t, :ulc).to_a.reverse
4608
+ return 0 if polyg.empty?
4609
+
4610
+ if floors.size > 1
4611
+ floors = floors.select { |flr| plane.equal(flr.plane, 0.001) }
4612
+
4613
+ if floors.size > 1
4614
+ polygs = floors.map { |flr| poly(flr, false, true, true, t, :ulc) }
4615
+ polygs = polygs.reject { |plg| plg.empty? }
4616
+ polygs = polygs.map { |plg| plg.to_a.reverse }
4617
+ union = OpenStudio.joinAll(polygs, 0.01).first
4618
+ polyg = poly(union, false, true, true)
4619
+ return 0 if polyg.empty?
4620
+ end
4621
+ end
4622
+
4623
+ res = realignedFace(polyg.to_a.reverse)
4624
+ return 0 if res[:box].nil?
4625
+
4626
+ # A bounded box's 'height', at its narrowest, is its 'width'.
4627
+ height(res[:box])
4628
+ end
4629
+
4486
4630
  ##
4487
4631
  # Identifies 'leader line anchors', i.e. specific 3D points of a (larger) set
4488
4632
  # (e.g. delineating a larger, parent polygon), each anchor linking the BLC
@@ -4576,7 +4720,7 @@ module OSut
4576
4720
  else
4577
4721
  st[:t] = OpenStudio::Transformation.alignFace(pts) unless st.key?(:t)
4578
4722
  tpts = st[:t].inverse * st[tag]
4579
- o = getRealignedFace(tpts, true)
4723
+ o = realignedFace(tpts, true)
4580
4724
  tpts = st[:t] * (o[:r] * (o[:t] * o[:set]))
4581
4725
 
4582
4726
  st[:out] = o
@@ -4594,7 +4738,7 @@ module OSut
4594
4738
  nb = 0
4595
4739
 
4596
4740
  # Check for intersections between leader line and larger polygon edges.
4597
- getSegments(pts).each do |sg|
4741
+ segments(pts).each do |sg|
4598
4742
  break unless nb.zero?
4599
4743
  next if holds?(sg, pt)
4600
4744
 
@@ -4608,7 +4752,7 @@ module OSut
4608
4752
 
4609
4753
  ost = other[tag]
4610
4754
 
4611
- getSegments(ost).each { |sg| nb += 1 if lineIntersects?(ld, sg) }
4755
+ segments(ost).each { |sg| nb += 1 if lineIntersects?(ld, sg) }
4612
4756
  end
4613
4757
 
4614
4758
  # ... and previous leader lines (first come, first serve basis).
@@ -4626,7 +4770,7 @@ module OSut
4626
4770
  end
4627
4771
 
4628
4772
  # Finally, check for self-intersections.
4629
- getSegments(tpts).each do |sg|
4773
+ segments(tpts).each do |sg|
4630
4774
  break unless nb.zero?
4631
4775
  next if holds?(sg, tpts.first)
4632
4776
 
@@ -4686,8 +4830,9 @@ module OSut
4686
4830
  set.each_with_index do |st, i|
4687
4831
  str1 = id + "subset ##{i+1}"
4688
4832
  str2 = str1 + " #{tag.to_s}"
4689
- next if st.key?(:void) && st[:void]
4690
4833
  return mismatch(str1, st, Hash, mth, DBG, a) unless st.respond_to?(:key?)
4834
+ next if st.key?(:void) && st[:void]
4835
+
4691
4836
  return hashkey( str1, st, tag, mth, DBG, a) unless st.key?(tag)
4692
4837
  return empty("#{str2} vertices", mth, DBG, a) if st[tag].empty?
4693
4838
  return hashkey( str1, st, :ld, mth, DBG, a) unless st.key?(:ld)
@@ -4696,9 +4841,9 @@ module OSut
4696
4841
  return invalid("#{str2} polygon", mth, 0, DBG, a) if stt.empty?
4697
4842
 
4698
4843
  ld = st[:ld]
4699
- return mismatch(str, ld, Hash, mth, DBG, a) unless ld.is_a?(Hash)
4700
- return hashkey( str, ld, s, mth, DBG, a) unless ld.key?(s)
4701
- return mismatch(str, ld[s], cl, mth, DBG, a) unless ld[s].is_a?(cl)
4844
+ return mismatch(str2, ld, Hash, mth, DBG, a) unless ld.is_a?(Hash)
4845
+ return hashkey( str2, ld, s, mth, DBG, a) unless ld.key?(s)
4846
+ return mismatch(str2, ld[s], cl, mth, DBG, a) unless ld[s].is_a?(cl)
4702
4847
  end
4703
4848
 
4704
4849
  # Re-sequence polygon vertices.
@@ -5091,7 +5236,7 @@ module OSut
5091
5236
  pltz.each_with_index do |plt, i|
5092
5237
  id = "plate # #{i+1} (index #{i})"
5093
5238
 
5094
- return mismatch(id, plt, cl1, mth, DBG, slb) unless plt.is_a?(cl2)
5239
+ return mismatch(id, plt, cl2, mth, DBG, slb) unless plt.is_a?(cl2)
5095
5240
  return hashkey( id, plt, :x, mth, DBG, slb) unless plt.key?(:x )
5096
5241
  return hashkey( id, plt, :y, mth, DBG, slb) unless plt.key?(:y )
5097
5242
  return hashkey( id, plt, :dx, mth, DBG, slb) unless plt.key?(:dx)
@@ -5142,7 +5287,7 @@ module OSut
5142
5287
  end
5143
5288
 
5144
5289
  # Once joined, re-adjust Z-axis coordinates.
5145
- unless z.zero?
5290
+ unless z.round(2) == 0.00
5146
5291
  vtx = OpenStudio::Point3dVector.new
5147
5292
  slb.each { |pt| vtx << OpenStudio::Point3d.new(pt.x, pt.y, z) }
5148
5293
  slb = vtx
@@ -5162,18 +5307,18 @@ module OSut
5162
5307
  # @param spaces [Set<OpenStudio::Model::Space>] target spaces
5163
5308
  #
5164
5309
  # @return [Array<OpenStudio::Model::Surface>] roofs (may be empty)
5165
- def getRoofs(spaces = [])
5310
+ def roofs(spaces = [])
5166
5311
  mth = "OSut::#{__callee__}"
5167
5312
  up = OpenStudio::Point3d.new(0,0,1) - OpenStudio::Point3d.new(0,0,0)
5168
- roofs = []
5313
+ rfs = []
5169
5314
  spaces = spaces.is_a?(OpenStudio::Model::Space) ? [spaces] : spaces
5170
5315
  spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
5171
5316
 
5172
5317
  spaces = spaces.select { |space| space.is_a?(OpenStudio::Model::Space) }
5173
5318
 
5174
5319
  # Space-specific outdoor-facing roof surfaces.
5175
- roofs = facets(spaces, "Outdoors", "RoofCeiling")
5176
- roofs = roofs.select { |roof| roof?(roof) }
5320
+ rfs = facets(spaces, "Outdoors", "RoofCeiling")
5321
+ rfs = rfs.select { |rf| roof?(rf) }
5177
5322
 
5178
5323
  spaces.each do |space|
5179
5324
  # When unoccupied spaces are involved (e.g. plenums, attics), the target
@@ -5209,12 +5354,12 @@ module OSut
5209
5354
  cst = cast(cv0, rvi, up)
5210
5355
  next unless overlaps?(cst, rvi, false)
5211
5356
 
5212
- roofs << ruf unless roofs.include?(ruf)
5357
+ rfs << ruf unless rfs.include?(ruf)
5213
5358
  end
5214
5359
  end
5215
5360
  end
5216
5361
 
5217
- roofs
5362
+ rfs
5218
5363
  end
5219
5364
 
5220
5365
  ##
@@ -5240,10 +5385,10 @@ module OSut
5240
5385
  return invalid("baselit" , mth, 4, DBG, false) unless ck4
5241
5386
 
5242
5387
  walls = sidelit ? facets(space, "Outdoors", "Wall") : []
5243
- roofs = toplit ? facets(space, "Outdoors", "RoofCeiling") : []
5388
+ rufs = toplit ? facets(space, "Outdoors", "RoofCeiling") : []
5244
5389
  floors = baselit ? facets(space, "Outdoors", "Floor") : []
5245
5390
 
5246
- (walls + roofs + floors).each do |surface|
5391
+ (walls + rufs + floors).each do |surface|
5247
5392
  surface.subSurfaces.each do |sub|
5248
5393
  # All fenestrated subsurface types are considered, as user can set these
5249
5394
  # explicitly (e.g. skylight in a wall) in OpenStudio.
@@ -5370,11 +5515,11 @@ module OSut
5370
5515
  box = boundedBox(s0)
5371
5516
 
5372
5517
  if realign
5373
- s00 = getRealignedFace(box, true)
5518
+ s00 = realignedFace(box, true)
5374
5519
  return invalid("bound realignment", mth, 0, DBG, false) unless s00[:set]
5375
5520
  end
5376
5521
  elsif realign
5377
- s00 = getRealignedFace(s0, false)
5522
+ s00 = realignedFace(s0, false)
5378
5523
  return invalid("unbound realignment", mth, 0, DBG, false) unless s00[:set]
5379
5524
  end
5380
5525
 
@@ -5983,7 +6128,6 @@ module OSut
5983
6128
  # previously-added leader lines.
5984
6129
  #
5985
6130
  # @todo: revise approach for attics ONCE skylight wells have been added.
5986
- olap = nil
5987
6131
  olap = overlap(cst, rvi, false)
5988
6132
  next if olap.empty?
5989
6133
 
@@ -6013,24 +6157,24 @@ module OSut
6013
6157
  # (Array of 2x linked surfaces). Each surface may be linked to more than one
6014
6158
  # horizontal ridge.
6015
6159
  #
6016
- # @param roofs [Array<OpenStudio::Model::Surface>] target surfaces
6160
+ # @param rfs [Array<OpenStudio::Model::Surface>] target surfaces
6017
6161
  #
6018
6162
  # @return [Array] horizontal ridges (see logs if empty)
6019
- def getHorizontalRidges(roofs = [])
6163
+ def horizontalRidges(rfs = [])
6020
6164
  mth = "OSut::#{__callee__}"
6021
6165
  ridges = []
6022
- return ridges unless roofs.is_a?(Array)
6166
+ return ridges unless rfs.is_a?(Array)
6023
6167
 
6024
- roofs = roofs.select { |s| s.is_a?(OpenStudio::Model::Surface) }
6025
- roofs = roofs.select { |s| sloped?(s) }
6168
+ rfs = rfs.select { |s| s.is_a?(OpenStudio::Model::Surface) }
6169
+ rfs = rfs.select { |s| sloped?(s) }
6026
6170
 
6027
- roofs.each do |roof|
6028
- maxZ = roof.vertices.max_by(&:z).z
6029
- next if roof.space.empty?
6171
+ rfs.each do |rf|
6172
+ maxZ = rf.vertices.max_by(&:z).z
6173
+ next if rf.space.empty?
6030
6174
 
6031
- space = roof.space.get
6175
+ space = rf.space.get
6032
6176
 
6033
- getSegments(roof).each do |edge|
6177
+ segments(rf).each do |edge|
6034
6178
  next unless xyz?(edge, :z, maxZ)
6035
6179
 
6036
6180
  # Skip if already tracked.
@@ -6045,18 +6189,18 @@ module OSut
6045
6189
 
6046
6190
  next if match
6047
6191
 
6048
- ridge = { edge: edge, length: (edge[1] - edge[0]).length, roofs: [roof] }
6192
+ ridge = { edge: edge, length: (edge[1] - edge[0]).length, roofs: [rf] }
6049
6193
 
6050
6194
  # Links another roof (same space)?
6051
6195
  match = false
6052
6196
 
6053
- roofs.each do |ruf|
6197
+ rfs.each do |ruf|
6054
6198
  break if match
6055
- next if ruf == roof
6199
+ next if ruf == rf
6056
6200
  next if ruf.space.empty?
6057
6201
  next unless ruf.space.get == space
6058
6202
 
6059
- getSegments(ruf).each do |edg|
6203
+ segments(ruf).each do |edg|
6060
6204
  break if match
6061
6205
  next unless same?(edge, edg) || same?(edge, edg.reverse)
6062
6206
 
@@ -6100,7 +6244,7 @@ module OSut
6100
6244
  if opts[:size].respond_to?(:to_f)
6101
6245
  w = opts[:size].to_f
6102
6246
  w2 = w * w
6103
- return invalid(size, mth, 0, ERR, []) if w.round(2) < gap4
6247
+ return invalid("size", mth, 0, ERR, []) if w.round(2) < gap4
6104
6248
  else
6105
6249
  return mismatch("size", opts[:size], Numeric, mth, DBG, [])
6106
6250
  end
@@ -6123,12 +6267,12 @@ module OSut
6123
6267
  spaces = spaces.select { |sp| sp.partofTotalFloorArea }
6124
6268
  spaces = spaces.reject { |sp| unconditioned?(sp) }
6125
6269
  spaces = spaces.reject { |sp| vestibule?(sp) }
6126
- spaces = spaces.reject { |sp| getRoofs(sp).empty? }
6270
+ spaces = spaces.reject { |sp| roofs(sp).empty? }
6127
6271
  spaces = spaces.reject { |sp| sp.floorArea < 4 * w2 }
6128
6272
  spaces = spaces.sort_by(&:floorArea).reverse
6129
- return empty("spaces", mth, WRN, 0) if spaces.empty?
6273
+ return empty("spaces", mth, WRN, []) if spaces.empty?
6130
6274
  else
6131
- return mismatch("spaces", spaces, Array, mth, DBG, 0)
6275
+ return mismatch("spaces", spaces, Array, mth, DBG, [])
6132
6276
  end
6133
6277
 
6134
6278
  # Unfenestrated spaces have no windows, glazed doors or skylights. By
@@ -6169,7 +6313,7 @@ module OSut
6169
6313
 
6170
6314
  # Gather roof surfaces - possibly those of attics or plenums above.
6171
6315
  spaces.each do |sp|
6172
- getRoofs(sp).each do |rf|
6316
+ roofs(sp).each do |rf|
6173
6317
  espaces[sp] = {roofs: []} unless espaces.key?(sp)
6174
6318
  espaces[sp][:roofs] << rf unless espaces[sp][:roofs].include?(rf)
6175
6319
  end
@@ -6244,6 +6388,7 @@ module OSut
6244
6388
  bfr = 0.005 # minimum array perimeter buffer (no wells)
6245
6389
  w = 1.22 # default 48" x 48" skylight base
6246
6390
  w2 = w * w # m2
6391
+ v = OpenStudio.openStudioVersion.split(".").join.to_i
6247
6392
 
6248
6393
  # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
6249
6394
  # Excerpts of ASHRAE 90.1 2022 definitions:
@@ -6360,10 +6505,10 @@ module OSut
6360
6505
 
6361
6506
  if frame.respond_to?(:frameWidth)
6362
6507
  frame = nil if v < 321
6363
- frame = nil if f.frameWidth.round(2) < 0
6364
- frame = nil if f.frameWidth.round(2) > gap
6508
+ frame = nil if frame.frameWidth.round(2) < 0
6509
+ frame = nil if frame.frameWidth.round(2) > gap
6365
6510
 
6366
- f = f.frameWidth if frame
6511
+ f = frame.frameWidth if frame
6367
6512
  log(WRN, "Skip Frame&Divider (#{mth})") unless frame
6368
6513
  else
6369
6514
  frame = nil
@@ -6420,7 +6565,7 @@ module OSut
6420
6565
  end
6421
6566
 
6422
6567
  # Purge if requested.
6423
- getRoofs(spaces).each { |s| s.subSurfaces.map(&:remove) } if clear
6568
+ roofs(spaces).each { |s| s.subSurfaces.map(&:remove) } if clear
6424
6569
 
6425
6570
  # Safely exit, e.g. if strictly called to purge existing roof subsurfaces.
6426
6571
  return 0 if area && area.round(2) == 0
@@ -6588,14 +6733,14 @@ module OSut
6588
6733
  next unless opts[opt] == false
6589
6734
 
6590
6735
  case opt
6591
- when :sidelit then filters.map! { |f| f.include?("b") ? f.delete("b") : f }
6592
- when :sloped then filters.map! { |f| f.include?("c") ? f.delete("c") : f }
6593
- when :plenum then filters.map! { |f| f.include?("d") ? f.delete("d") : f }
6594
- when :attic then filters.map! { |f| f.include?("e") ? f.delete("e") : f }
6736
+ when :sidelit then filters.map! { |fl| fl.include?("b") ? fl.delete("b") : fl }
6737
+ when :sloped then filters.map! { |fl| fl.include?("c") ? fl.delete("c") : fl }
6738
+ when :plenum then filters.map! { |fl| fl.include?("d") ? fl.delete("d") : fl }
6739
+ when :attic then filters.map! { |fl| fl.include?("e") ? fl.delete("e") : fl }
6595
6740
  end
6596
6741
  end
6597
6742
 
6598
- filters.reject! { |f| f.empty? }
6743
+ filters.reject! { |fl| fl.empty? }
6599
6744
  filters.uniq!
6600
6745
 
6601
6746
  # Remaining filters may be further pruned automatically after space/roof
@@ -6698,7 +6843,7 @@ module OSut
6698
6843
  # Process outdoor-facing roof surfaces of plenums and attics above.
6699
6844
  rooms.each do |space, room|
6700
6845
  t0 = room[:t0]
6701
- rufs = getRoofs(space) - room[:roofs]
6846
+ rufs = roofs(space) - room[:roofs]
6702
6847
 
6703
6848
  rufs.each do |ruf|
6704
6849
  next unless roof?(ruf)
@@ -6860,12 +7005,12 @@ module OSut
6860
7005
  # Ensure uniqueness of plenum roofs.
6861
7006
  attics.values.each do |attic|
6862
7007
  attic[:roofs ].uniq!
6863
- attic[:ridges] = getHorizontalRidges(attic[:roofs]) # @todo
7008
+ attic[:ridges] = horizontalRidges(attic[:roofs]) # @todo
6864
7009
  end
6865
7010
 
6866
7011
  plenums.values.each do |plenum|
6867
7012
  plenum[:roofs ].uniq!
6868
- plenum[:ridges] = getHorizontalRidges(plenum[:roofs]) # @todo
7013
+ plenum[:ridges] = horizontalRidges(plenum[:roofs]) # @todo
6869
7014
  end
6870
7015
 
6871
7016
  # Regardless of the selected skylight arrangement pattern, the solution only
@@ -7388,7 +7533,7 @@ module OSut
7388
7533
  pattern = "array"
7389
7534
  elsif fpm2.keys.include?("strips")
7390
7535
  pattern = "strips"
7391
- else fpm2.keys.include?("strip")
7536
+ else # fpm2.keys.include?("strip")
7392
7537
  pattern = "strip"
7393
7538
  end
7394
7539
  else
@@ -7399,7 +7544,7 @@ module OSut
7399
7544
  pattern = "strip"
7400
7545
  elsif fpm2.keys.include?("strips")
7401
7546
  pattern = "strips"
7402
- else fpm2.keys.include?("array")
7547
+ else # fpm2.keys.include?("array")
7403
7548
  pattern = "array"
7404
7549
  end
7405
7550
  end
@@ -7502,7 +7647,7 @@ module OSut
7502
7647
  # Size contraction: round 2: prioritize larger sets.
7503
7648
  adm2 = 0
7504
7649
 
7505
- sets.each_with_index do |set, i|
7650
+ sets.each_with_index do |set|
7506
7651
  next if set[:w].round(2) <= w0
7507
7652
  next if set[:d].round(2) <= w0
7508
7653
 
@@ -7674,12 +7819,12 @@ module OSut
7674
7819
 
7675
7820
  # Generate well walls.
7676
7821
  vX = cast(roof, tile, ray)
7677
- s0 = getSegments(t0 * roof.vertices)
7678
- sX = getSegments(t0 * vX)
7822
+ s0 = segments(t0 * roof.vertices)
7823
+ sX = segments(t0 * vX)
7679
7824
 
7680
7825
  s0.each_with_index do |sg, j|
7681
- sg0 = sg.to_a
7682
- sgX = sX[j].to_a
7826
+ sg0 = sg
7827
+ sgX = sX[j]
7683
7828
  vec = OpenStudio::Point3dVector.new
7684
7829
  vec << sg0.first
7685
7830
  vec << sg0.last