osut 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/pull_request.yml +21 -21
- data/.gitignore +2 -0
- data/LICENSE +1 -1
- data/lib/osut/utils.rb +1676 -1072
- data/lib/osut/version.rb +2 -2
- data/lib/osut.rb +1 -1
- data/osut.gemspec +1 -1
- metadata +6 -9
data/lib/osut/utils.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# BSD 3-Clause License
|
2
2
|
#
|
3
|
-
# Copyright (c) 2022-
|
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
|
@@ -34,20 +34,20 @@ module OSut
|
|
34
34
|
# DEBUG for devs; WARN/ERROR for users (bad OS input), see OSlg
|
35
35
|
extend OSlg
|
36
36
|
|
37
|
-
TOL = 0.01
|
38
|
-
TOL2 = TOL * TOL
|
39
|
-
DBG = OSlg::DEBUG # see github.com/rd2/oslg
|
40
|
-
INF = OSlg::INFO # see github.com/rd2/oslg
|
41
|
-
WRN = OSlg::WARN # see github.com/rd2/oslg
|
42
|
-
ERR = OSlg::ERROR # see github.com/rd2/oslg
|
43
|
-
FTL = OSlg::FATAL # see github.com/rd2/oslg
|
44
|
-
NS = "nameString"
|
37
|
+
TOL = 0.01 # default distance tolerance (m)
|
38
|
+
TOL2 = TOL * TOL # default area tolerance (m2)
|
39
|
+
DBG = OSlg::DEBUG.dup # see github.com/rd2/oslg
|
40
|
+
INF = OSlg::INFO.dup # see github.com/rd2/oslg
|
41
|
+
WRN = OSlg::WARN.dup # see github.com/rd2/oslg
|
42
|
+
ERR = OSlg::ERROR.dup # see github.com/rd2/oslg
|
43
|
+
FTL = OSlg::FATAL.dup # see github.com/rd2/oslg
|
44
|
+
NS = "nameString" # OpenStudio object identifier method
|
45
45
|
|
46
46
|
HEAD = 2.032 # standard 80" door
|
47
47
|
SILL = 0.762 # standard 30" window sill
|
48
48
|
|
49
49
|
# General surface orientations (see facets method)
|
50
|
-
SIDZ = [:bottom, # e.g. ground-facing, exposed
|
50
|
+
SIDZ = [:bottom, # e.g. ground-facing, exposed floors
|
51
51
|
:top, # e.g. roof/ceiling
|
52
52
|
:north, # NORTH
|
53
53
|
:east, # EAST
|
@@ -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)
|
228
|
-
return invalid("#{id} Uo (> 5.678)",
|
229
|
-
return
|
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 ] =
|
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 ] =
|
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
|
-
|
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
|
491
|
-
|
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
|
-
|
534
|
-
ro =
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
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 [
|
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,
|
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
|
-
|
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
|
|
@@ -1924,7 +1983,7 @@ module OSut
|
|
1924
1983
|
# isolation) to determine whether an UNOCCUPIED space should have its
|
1925
1984
|
# envelope insulated ("plenum") or not ("attic").
|
1926
1985
|
#
|
1927
|
-
# In contrast to OpenStudio-Standards' "space_plenum?",
|
1986
|
+
# In contrast to OpenStudio-Standards' "space_plenum?", the method below
|
1928
1987
|
# strictly returns FALSE if a space is indeed "partofTotalFloorArea". It
|
1929
1988
|
# also returns FALSE if the space is a vestibule. Otherwise, it needs more
|
1930
1989
|
# information to determine if such an UNOCCUPIED space is indeed a
|
@@ -1933,7 +1992,7 @@ module OSut
|
|
1933
1992
|
# CASE A: it includes the substring "plenum" (case insensitive) in its
|
1934
1993
|
# spaceType's name, or in the latter's standardsSpaceType string;
|
1935
1994
|
#
|
1936
|
-
# CASE B: "isPlenum" == TRUE in an OpenStudio model WITH HVAC airloops
|
1995
|
+
# CASE B: "isPlenum" == TRUE in an OpenStudio model WITH HVAC airloops; OR
|
1937
1996
|
#
|
1938
1997
|
# CASE C: its zone holds an 'inactive' thermostat (i.e. can't extract valid
|
1939
1998
|
# setpoints) in an OpenStudio model with setpoint temperatures.
|
@@ -2054,7 +2113,7 @@ module OSut
|
|
2054
2113
|
res[:heating] = nil
|
2055
2114
|
res[:cooling] = nil
|
2056
2115
|
elsif cnd.downcase == "semiheated"
|
2057
|
-
res[:heating] =
|
2116
|
+
res[:heating] = 14.0 if res[:heating].nil?
|
2058
2117
|
res[:cooling] = nil
|
2059
2118
|
elsif cnd.downcase.include?("conditioned")
|
2060
2119
|
# "nonresconditioned", "resconditioned" or "indirectlyconditioned"
|
@@ -2090,6 +2149,60 @@ module OSut
|
|
2090
2149
|
ok
|
2091
2150
|
end
|
2092
2151
|
|
2152
|
+
##
|
2153
|
+
# Validates whether a space can be considered as REFRIGERATED.
|
2154
|
+
#
|
2155
|
+
# @param space [OpenStudio::Model::Space] a space
|
2156
|
+
#
|
2157
|
+
# @return [Bool] whether space is considered REFRIGERATED
|
2158
|
+
# @return [false] if invalid input (see logs)
|
2159
|
+
def refrigerated?(space = nil)
|
2160
|
+
mth = "OSut::#{__callee__}"
|
2161
|
+
cl = OpenStudio::Model::Space
|
2162
|
+
tg0 = "refrigerated"
|
2163
|
+
return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
|
2164
|
+
|
2165
|
+
# 1. First check OSut's REFRIGERATED status.
|
2166
|
+
status = space.additionalProperties.getFeatureAsString(tg0)
|
2167
|
+
|
2168
|
+
unless status.empty?
|
2169
|
+
status = status.get
|
2170
|
+
return status if [true, false].include?(status)
|
2171
|
+
|
2172
|
+
log(ERR, "Unknown #{space.nameString} REFRIGERATED #{status} (#{mth})")
|
2173
|
+
end
|
2174
|
+
|
2175
|
+
# 2. Else, compare design heating/cooling setpoints.
|
2176
|
+
stps = setpoints(space)
|
2177
|
+
return false unless stps[:heating].nil?
|
2178
|
+
return false if stps[:cooling].nil?
|
2179
|
+
return true if stps[:cooling] < 15
|
2180
|
+
|
2181
|
+
false
|
2182
|
+
end
|
2183
|
+
|
2184
|
+
##
|
2185
|
+
# Validates whether a space can be considered as SEMIHEATED as per NECB 2020
|
2186
|
+
# 1.2.1.2. 2): design heating setpoint < 15°C (and non-REFRIGERATED).
|
2187
|
+
#
|
2188
|
+
# @param space [OpenStudio::Model::Space] a space
|
2189
|
+
#
|
2190
|
+
# @return [Bool] whether space is considered SEMIHEATED
|
2191
|
+
# @return [false] if invalid input (see logs)
|
2192
|
+
def semiheated?(space = nil)
|
2193
|
+
mth = "OSut::#{__callee__}"
|
2194
|
+
cl = OpenStudio::Model::Space
|
2195
|
+
return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
|
2196
|
+
return false if refrigerated?(space)
|
2197
|
+
|
2198
|
+
stps = setpoints(space)
|
2199
|
+
return false unless stps[:cooling].nil?
|
2200
|
+
return false if stps[:heating].nil?
|
2201
|
+
return true if stps[:heating] < 15
|
2202
|
+
|
2203
|
+
false
|
2204
|
+
end
|
2205
|
+
|
2093
2206
|
##
|
2094
2207
|
# Generates an HVAC availability schedule.
|
2095
2208
|
#
|
@@ -2256,9 +2369,9 @@ module OSut
|
|
2256
2369
|
# ---- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---- #
|
2257
2370
|
# This final set of utilities targets OpenStudio geometry. Many of the
|
2258
2371
|
# following geometry methods rely on Boost as an OpenStudio dependency.
|
2259
|
-
# As per Boost requirements, points (e.g.
|
2372
|
+
# As per Boost requirements, points (e.g. vertical polygon) must be 'aligned':
|
2260
2373
|
# - first rotated/tilted as to lay flat along XY plane (Z-axis ~= 0)
|
2261
|
-
# - initial Z-axis values
|
2374
|
+
# - initial Z-axis values now become Y-axis values
|
2262
2375
|
# - points with the lowest X-axis values are 'aligned' along X-axis (0)
|
2263
2376
|
# - points with the lowest Z-axis values are 'aligned' along Y-axis (0)
|
2264
2377
|
# - for several Boost methods, points must be clockwise in sequence
|
@@ -2297,7 +2410,7 @@ module OSut
|
|
2297
2410
|
# @return [OpenStudio::Vector3d] true normal vector
|
2298
2411
|
# @return [nil] if invalid input (see logs)
|
2299
2412
|
def trueNormal(s = nil, r = 0)
|
2300
|
-
mth = "
|
2413
|
+
mth = "OSut::#{__callee__}"
|
2301
2414
|
cl = OpenStudio::Model::PlanarSurface
|
2302
2415
|
return mismatch("surface", s, cl, mth) unless s.is_a?(cl)
|
2303
2416
|
return invalid("rotation angle", mth, 2) unless r.respond_to?(:to_f)
|
@@ -2330,31 +2443,31 @@ module OSut
|
|
2330
2443
|
|
2331
2444
|
##
|
2332
2445
|
# Returns OpenStudio 3D points as an OpenStudio point vector, validating
|
2333
|
-
# points in the process (if Array).
|
2446
|
+
# points in the process (e.g. if Array).
|
2334
2447
|
#
|
2335
2448
|
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
2336
2449
|
#
|
2337
2450
|
# @return [OpenStudio::Point3dVector] 3D vector (see logs if empty)
|
2338
2451
|
def to_p3Dv(pts = nil)
|
2339
2452
|
mth = "OSut::#{__callee__}"
|
2340
|
-
cl1 = OpenStudio::Point3d
|
2341
|
-
cl2 = OpenStudio::Point3dVector
|
2342
|
-
cl3 = OpenStudio::Model::PlanarSurface
|
2343
|
-
cl4 = Array
|
2344
2453
|
v = OpenStudio::Point3dVector.new
|
2345
2454
|
|
2346
|
-
if pts.is_a?(
|
2455
|
+
if pts.is_a?(OpenStudio::Point3d)
|
2347
2456
|
v << pts
|
2348
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
|
2349
2463
|
end
|
2350
2464
|
|
2351
|
-
return pts
|
2352
|
-
return pts.vertices if pts.is_a?(cl3)
|
2353
|
-
|
2354
|
-
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)
|
2355
2466
|
|
2356
2467
|
pts.each do |pt|
|
2357
|
-
|
2468
|
+
unless pt.is_a?(OpenStudio::Point3d)
|
2469
|
+
return mismatch("point", pt, OpenStudio::Point3d, mth, DBG, v)
|
2470
|
+
end
|
2358
2471
|
end
|
2359
2472
|
|
2360
2473
|
pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, pt.z) }
|
@@ -2630,7 +2743,7 @@ module OSut
|
|
2630
2743
|
|
2631
2744
|
pair = pts.each_cons(2).find { |p1, _| same?(p1, pt) }
|
2632
2745
|
|
2633
|
-
pair.nil? ? pts
|
2746
|
+
pair.nil? ? pts[0] : pair[-1]
|
2634
2747
|
end
|
2635
2748
|
|
2636
2749
|
##
|
@@ -2721,21 +2834,25 @@ module OSut
|
|
2721
2834
|
# @param pts [Set<OpenStudio::Point3d] 3D points
|
2722
2835
|
# @param n [#to_i] requested number of unique points (0 returns all)
|
2723
2836
|
#
|
2724
|
-
# @return [OpenStudio::Point3dVector] unique points (see logs
|
2725
|
-
def
|
2837
|
+
# @return [OpenStudio::Point3dVector] unique points (see logs)
|
2838
|
+
def uniques(pts = nil, n = 0)
|
2726
2839
|
mth = "OSut::#{__callee__}"
|
2727
2840
|
pts = to_p3Dv(pts)
|
2728
|
-
ok = n.respond_to?(:to_i)
|
2729
2841
|
v = OpenStudio::Point3dVector.new
|
2730
2842
|
return v if pts.empty?
|
2731
|
-
|
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
|
2732
2850
|
|
2733
2851
|
pts.each { |pt| v << pt unless holds?(v, pt) }
|
2734
2852
|
|
2735
|
-
n = n.
|
2736
|
-
|
2737
|
-
v = v[
|
2738
|
-
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
|
2739
2856
|
|
2740
2857
|
v
|
2741
2858
|
end
|
@@ -2749,10 +2866,10 @@ module OSut
|
|
2749
2866
|
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
2750
2867
|
#
|
2751
2868
|
# @return [OpenStudio::Point3dVectorVector] line segments (see logs if empty)
|
2752
|
-
def
|
2869
|
+
def segments(pts = nil)
|
2753
2870
|
mth = "OSut::#{__callee__}"
|
2754
2871
|
vv = OpenStudio::Point3dVectorVector.new
|
2755
|
-
pts =
|
2872
|
+
pts = uniques(pts)
|
2756
2873
|
return vv if pts.size < 2
|
2757
2874
|
|
2758
2875
|
pts.each_with_index do |p1, i1|
|
@@ -2779,7 +2896,6 @@ module OSut
|
|
2779
2896
|
# @return [false] if invalid input (see logs)
|
2780
2897
|
def segment?(pts = nil)
|
2781
2898
|
pts = to_p3Dv(pts)
|
2782
|
-
return false if pts.empty?
|
2783
2899
|
return false unless pts.size == 2
|
2784
2900
|
return false if same?(pts[0], pts[1])
|
2785
2901
|
|
@@ -2796,10 +2912,10 @@ module OSut
|
|
2796
2912
|
# @param pts [OpenStudio::Point3dVector] 3D points
|
2797
2913
|
#
|
2798
2914
|
# @return [OpenStudio::Point3dVectorVector] triads (see logs if empty)
|
2799
|
-
def
|
2915
|
+
def triads(pts = nil, co = false)
|
2800
2916
|
mth = "OSut::#{__callee__}"
|
2801
2917
|
vv = OpenStudio::Point3dVectorVector.new
|
2802
|
-
pts =
|
2918
|
+
pts = uniques(pts)
|
2803
2919
|
return vv if pts.size < 2
|
2804
2920
|
|
2805
2921
|
pts.each_with_index do |p1, i1|
|
@@ -2826,7 +2942,7 @@ module OSut
|
|
2826
2942
|
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
2827
2943
|
#
|
2828
2944
|
# @return [Bool] whether set is a valid triad (i.e. a trio of 3D points)
|
2829
|
-
# @return [false] if invalid input (see logs)
|
2945
|
+
# @return [false] if invalid input (see 'to_p3Dv' logs)
|
2830
2946
|
def triad?(pts = nil)
|
2831
2947
|
pts = to_p3Dv(pts)
|
2832
2948
|
return false if pts.empty?
|
@@ -2851,18 +2967,17 @@ module OSut
|
|
2851
2967
|
mth = "OSut::#{__callee__}"
|
2852
2968
|
cl1 = OpenStudio::Point3d
|
2853
2969
|
cl2 = OpenStudio::Point3dVector
|
2854
|
-
return mismatch(
|
2855
|
-
return
|
2856
|
-
|
2970
|
+
return mismatch("point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1)
|
2971
|
+
return false unless segment?(sg)
|
2857
2972
|
return true if holds?(sg, p0)
|
2858
2973
|
|
2859
|
-
a = sg
|
2860
|
-
b = sg
|
2974
|
+
a = sg[ 0]
|
2975
|
+
b = sg[-1]
|
2861
2976
|
ab = b - a
|
2862
2977
|
abn = b - a
|
2863
2978
|
abn.normalize
|
2864
2979
|
ap = p0 - a
|
2865
|
-
sp
|
2980
|
+
sp = ap.dot(abn)
|
2866
2981
|
return false if sp < 0
|
2867
2982
|
|
2868
2983
|
apd = scalar(abn, sp)
|
@@ -2887,9 +3002,9 @@ module OSut
|
|
2887
3002
|
mth = "OSut::#{__callee__}"
|
2888
3003
|
cl1 = OpenStudio::Point3d
|
2889
3004
|
cl2 = OpenStudio::Point3dVectorVector
|
2890
|
-
sgs = sgs.is_a?(cl2) ? sgs :
|
2891
|
-
return empty("segments",
|
2892
|
-
return mismatch("point", p0,
|
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)
|
2893
3008
|
|
2894
3009
|
sgs.each { |sg| return true if pointAlongSegment?(p0, sg) }
|
2895
3010
|
|
@@ -2904,9 +3019,9 @@ module OSut
|
|
2904
3019
|
#
|
2905
3020
|
# @return [OpenStudio::Point3d] point of intersection of both lines
|
2906
3021
|
# @return [nil] if no intersection, equal, or invalid input (see logs)
|
2907
|
-
def
|
2908
|
-
s1
|
2909
|
-
s2
|
3022
|
+
def lineIntersection(s1 = [], s2 = [])
|
3023
|
+
s1 = segments(s1)
|
3024
|
+
s2 = segments(s2)
|
2910
3025
|
return nil if s1.empty?
|
2911
3026
|
return nil if s2.empty?
|
2912
3027
|
|
@@ -2917,10 +3032,10 @@ module OSut
|
|
2917
3032
|
return nil if same?(s1, s2)
|
2918
3033
|
return nil if same?(s1, s2.to_a.reverse)
|
2919
3034
|
|
2920
|
-
a1 = s1
|
2921
|
-
|
2922
|
-
|
2923
|
-
b2 = s2
|
3035
|
+
a1 = s1.first
|
3036
|
+
b1 = s2.first
|
3037
|
+
a2 = s1.last
|
3038
|
+
b2 = s2.last
|
2924
3039
|
|
2925
3040
|
# Matching segment endpoints?
|
2926
3041
|
return a1 if same?(a1, b1)
|
@@ -2929,25 +3044,32 @@ module OSut
|
|
2929
3044
|
return a2 if same?(a2, b2)
|
2930
3045
|
|
2931
3046
|
# Segment endpoint along opposite segment?
|
2932
|
-
return a1 if
|
2933
|
-
return a2 if
|
2934
|
-
return b1 if
|
2935
|
-
return b2 if
|
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)
|
2936
3051
|
|
2937
|
-
# Line segments as vectors. Skip if
|
3052
|
+
# Line segments as vectors. Skip if collinear or parallel.
|
2938
3053
|
a = a2 - a1
|
2939
3054
|
b = b2 - b1
|
2940
3055
|
xab = a.cross(b)
|
2941
3056
|
return nil if xab.length.round(4) < TOL2
|
2942
3057
|
|
2943
|
-
# 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.
|
2944
3059
|
a1b1 = b1 - a1
|
2945
3060
|
a1b2 = b2 - a1
|
2946
3061
|
xa1b1 = a.cross(a1b1)
|
2947
3062
|
xa1b2 = a.cross(a1b2)
|
3063
|
+
xa1b1.normalize
|
3064
|
+
xa1b2.normalize
|
3065
|
+
xab.normalize
|
2948
3066
|
return nil unless xab.cross(xa1b1).length.round(4) < TOL2
|
2949
3067
|
return nil unless xab.cross(xa1b2).length.round(4) < TOL2
|
2950
3068
|
|
3069
|
+
# Reset.
|
3070
|
+
xa1b1 = a.cross(a1b1)
|
3071
|
+
xa1b2 = a.cross(a1b2)
|
3072
|
+
|
2951
3073
|
# Both segment endpoints can't be 'behind' point.
|
2952
3074
|
return nil if a.dot(a1b1) < 0 && a.dot(a1b2) < 0
|
2953
3075
|
|
@@ -2974,7 +3096,7 @@ module OSut
|
|
2974
3096
|
return nil if a.dot(p0 - a1) < 0
|
2975
3097
|
|
2976
3098
|
# Ensure intersection is sandwiched between endpoints.
|
2977
|
-
return nil unless
|
3099
|
+
return nil unless pointAlongSegment?(p0, s2) && pointAlongSegment?(p0, s1)
|
2978
3100
|
|
2979
3101
|
p0
|
2980
3102
|
end
|
@@ -2988,33 +3110,36 @@ module OSut
|
|
2988
3110
|
# @return [Bool] whether 3D line intersects 3D segments
|
2989
3111
|
# @return [false] if invalid input (see logs)
|
2990
3112
|
def lineIntersects?(l = [], s = [])
|
2991
|
-
l
|
2992
|
-
s
|
3113
|
+
l = segments(l)
|
3114
|
+
s = segments(s)
|
2993
3115
|
return nil if l.empty?
|
2994
3116
|
return nil if s.empty?
|
2995
3117
|
|
2996
3118
|
l = l.first
|
2997
3119
|
|
2998
|
-
s.each { |segment| return true if
|
3120
|
+
s.each { |segment| return true if lineIntersection(l, segment) }
|
2999
3121
|
|
3000
3122
|
false
|
3001
3123
|
end
|
3002
3124
|
|
3003
3125
|
##
|
3004
|
-
#
|
3126
|
+
# Validates whether OpenStudio 3D points are listed clockwise, assuming points
|
3127
|
+
# have been pre-'aligned' - not just flattened along XY (i.e. Z = 0).
|
3005
3128
|
#
|
3006
|
-
# @param pts [OpenStudio::Point3dVector] 3D points
|
3129
|
+
# @param pts [OpenStudio::Point3dVector] pre-aligned 3D points
|
3007
3130
|
#
|
3008
3131
|
# @return [Bool] whether sequence is clockwise
|
3009
3132
|
# @return [false] if invalid input (see logs)
|
3010
3133
|
def clockwise?(pts = nil)
|
3011
3134
|
mth = "OSut::#{__callee__}"
|
3012
3135
|
pts = to_p3Dv(pts)
|
3013
|
-
|
3014
|
-
return invalid("
|
3015
|
-
|
3136
|
+
return invalid("3+ points" , mth, 1, DBG, false) if pts.size < 3
|
3137
|
+
return invalid("flat points", mth, 1, DBG, false) unless xyz?(pts, :z)
|
3138
|
+
|
3139
|
+
n = OpenStudio.getOutwardNormal(pts)
|
3140
|
+
return invalid("polygon", mth, 1, DBG, false) if n.empty?
|
3016
3141
|
|
3017
|
-
|
3142
|
+
n.get.z > 0 ? false : true
|
3018
3143
|
end
|
3019
3144
|
|
3020
3145
|
##
|
@@ -3078,28 +3203,33 @@ module OSut
|
|
3078
3203
|
end
|
3079
3204
|
|
3080
3205
|
##
|
3081
|
-
# Returns
|
3206
|
+
# Returns non-collinear points in an OpenStudio 3D point vector.
|
3082
3207
|
#
|
3083
3208
|
# @param pts [Set<OpenStudio::Point3d] 3D points
|
3084
3209
|
# @param n [#to_i] requested number of non-collinears (0 returns all)
|
3085
3210
|
#
|
3086
|
-
# @return [OpenStudio::Point3dVector] non-collinears (see logs
|
3087
|
-
def
|
3211
|
+
# @return [OpenStudio::Point3dVector] non-collinears (see logs)
|
3212
|
+
def nonCollinears(pts = nil, n = 0)
|
3088
3213
|
mth = "OSut::#{__callee__}"
|
3089
|
-
pts = getUniques(pts)
|
3090
|
-
ok = n.respond_to?(:to_i)
|
3091
|
-
v = OpenStudio::Point3dVector.new
|
3092
3214
|
a = []
|
3215
|
+
pts = uniques(pts)
|
3093
3216
|
return pts if pts.size < 3
|
3094
|
-
|
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
|
3095
3224
|
|
3096
3225
|
# Evaluate cross product of vectors of 3x sequential points.
|
3097
3226
|
pts.each_with_index do |p2, i2|
|
3098
|
-
i1
|
3099
|
-
i3
|
3100
|
-
i3
|
3101
|
-
p1
|
3102
|
-
p3
|
3227
|
+
i1 = i2 - 1
|
3228
|
+
i3 = i2 + 1
|
3229
|
+
i3 = 0 if i3 == pts.size
|
3230
|
+
p1 = pts[i1]
|
3231
|
+
p3 = pts[i3]
|
3232
|
+
|
3103
3233
|
v13 = p3 - p1
|
3104
3234
|
v12 = p2 - p1
|
3105
3235
|
next if v12.cross(v13).length < TOL2
|
@@ -3107,36 +3237,47 @@ module OSut
|
|
3107
3237
|
a << p2
|
3108
3238
|
end
|
3109
3239
|
|
3110
|
-
if
|
3240
|
+
if a.include?(pts[0])
|
3111
3241
|
a = a.rotate(-1) unless same?(a[0], pts[0])
|
3112
3242
|
end
|
3113
3243
|
|
3114
|
-
n = n.
|
3115
|
-
a = a[0..n-1]
|
3116
|
-
a = a[n
|
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
|
3117
3247
|
|
3118
3248
|
to_p3Dv(a)
|
3119
3249
|
end
|
3120
3250
|
|
3121
3251
|
##
|
3122
|
-
# Returns
|
3252
|
+
# Returns collinear points in an OpenStudio 3D point vector.
|
3123
3253
|
#
|
3124
3254
|
# @param pts [Set<OpenStudio::Point3d] 3D points
|
3125
3255
|
# @param n [#to_i] requested number of collinears (0 returns all)
|
3126
3256
|
#
|
3127
|
-
# @return [OpenStudio::Point3dVector] collinears (see logs
|
3128
|
-
def
|
3257
|
+
# @return [OpenStudio::Point3dVector] collinears (see logs)
|
3258
|
+
def collinears(pts = nil, n = 0)
|
3129
3259
|
mth = "OSut::#{__callee__}"
|
3130
|
-
|
3131
|
-
|
3132
|
-
v = OpenStudio::Point3dVector.new
|
3260
|
+
a = OpenStudio::Point3dVector.new
|
3261
|
+
pts = uniques(pts)
|
3133
3262
|
return pts if pts.size < 3
|
3134
|
-
return mismatch("n collinears", n, Integer, mth, DBG, v) unless ok
|
3135
3263
|
|
3136
|
-
|
3137
|
-
|
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) }
|
3138
3275
|
|
3139
|
-
|
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
|
3140
3281
|
end
|
3141
3282
|
|
3142
3283
|
##
|
@@ -3144,8 +3285,8 @@ module OSut
|
|
3144
3285
|
# polygon. In addition to basic OpenStudio polygon tests (e.g. all points
|
3145
3286
|
# sharing the same 3D plane, non-self-intersecting), the method can
|
3146
3287
|
# optionally check for convexity, or ensure uniqueness and/or non-collinearity.
|
3147
|
-
# Returned vector can also be 'aligned', as well as in UpperLeftCorner (ULC)
|
3148
|
-
#
|
3288
|
+
# Returned vector can also be 'aligned', as well as in UpperLeftCorner (ULC),
|
3289
|
+
# BottomLeftCorner (BLC), in clockwise (or counterclockwise) sequence.
|
3149
3290
|
#
|
3150
3291
|
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
3151
3292
|
# @param vx [Bool] whether to check for convexity
|
@@ -3162,7 +3303,7 @@ module OSut
|
|
3162
3303
|
v = OpenStudio::Point3dVector.new
|
3163
3304
|
vx = false unless [true, false].include?(vx)
|
3164
3305
|
uq = false unless [true, false].include?(uq)
|
3165
|
-
co =
|
3306
|
+
co = false unless [true, false].include?(co)
|
3166
3307
|
|
3167
3308
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3168
3309
|
# Exit if mismatched/invalid arguments.
|
@@ -3173,8 +3314,8 @@ module OSut
|
|
3173
3314
|
|
3174
3315
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3175
3316
|
# Minimum 3 points?
|
3176
|
-
p3 =
|
3177
|
-
return empty("polygon", mth, ERR, v) if p3.size < 3
|
3317
|
+
p3 = nonCollinears(pts, 3)
|
3318
|
+
return empty("polygon (non-collinears < 3)", mth, ERR, v) if p3.size < 3
|
3178
3319
|
|
3179
3320
|
# Coplanar?
|
3180
3321
|
pln = OpenStudio::Plane.new(p3)
|
@@ -3204,8 +3345,8 @@ module OSut
|
|
3204
3345
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3205
3346
|
# Ensure uniqueness and/or non-collinearity. Preserve original sequence.
|
3206
3347
|
p0 = a.first
|
3207
|
-
a =
|
3208
|
-
a =
|
3348
|
+
a = uniques(a).to_a if uq
|
3349
|
+
a = nonCollinears(a).to_a if co
|
3209
3350
|
i0 = a.index { |pt| same?(pt, p0) }
|
3210
3351
|
a = a.rotate(i0) unless i0.nil?
|
3211
3352
|
|
@@ -3214,7 +3355,7 @@ module OSut
|
|
3214
3355
|
if vx && a.size > 3
|
3215
3356
|
zen = OpenStudio::Point3d.new(0, 0, 1000)
|
3216
3357
|
|
3217
|
-
|
3358
|
+
triads(a).each do |trio|
|
3218
3359
|
p1 = trio[0]
|
3219
3360
|
p2 = trio[1]
|
3220
3361
|
p3 = trio[2]
|
@@ -3273,49 +3414,47 @@ module OSut
|
|
3273
3414
|
return mismatch("point", p0, cl, mth, DBG, false) unless p0.is_a?(cl)
|
3274
3415
|
|
3275
3416
|
n = OpenStudio.getOutwardNormal(s)
|
3276
|
-
return false if n.empty?
|
3417
|
+
return invalid("plane/normal", mth, 2, DBG, false) if n.empty?
|
3277
3418
|
|
3278
3419
|
n = n.get
|
3279
3420
|
pl = OpenStudio::Plane.new(s.first, n)
|
3280
3421
|
return false unless pl.pointOnPlane(p0)
|
3281
3422
|
|
3282
3423
|
entirely = false unless [true, false].include?(entirely)
|
3283
|
-
|
3424
|
+
sgments = segments(s)
|
3284
3425
|
|
3285
3426
|
# Along polygon edges, or near vertices?
|
3286
|
-
if pointAlongSegments?(p0,
|
3427
|
+
if pointAlongSegments?(p0, sgments)
|
3287
3428
|
return false if entirely
|
3288
3429
|
return true unless entirely
|
3289
3430
|
end
|
3290
3431
|
|
3291
|
-
|
3432
|
+
sgments.each do |sgment|
|
3292
3433
|
# - draw vector from segment midpoint to point
|
3293
3434
|
# - scale 1000x (assuming no building surface would be 1km wide)
|
3294
3435
|
# - convert vector to an independent line segment
|
3295
3436
|
# - loop through polygon segments, tally the number of intersections
|
3296
3437
|
# - avoid double-counting polygon vertices as intersections
|
3297
3438
|
# - return false if number of intersections is even
|
3298
|
-
mid = midpoint(
|
3439
|
+
mid = midpoint(sgment.first, sgment.last)
|
3299
3440
|
mpV = scalar(mid - p0, 1000)
|
3300
3441
|
p1 = p0 + mpV
|
3301
3442
|
ctr = 0
|
3302
|
-
pts = []
|
3303
3443
|
|
3304
3444
|
# Skip if ~collinear.
|
3305
|
-
next if
|
3445
|
+
next if mpV.cross(sgment.last - sgment.first).length.round(4) < TOL2
|
3306
3446
|
|
3307
|
-
|
3308
|
-
intersect =
|
3447
|
+
sgments.each do |sg|
|
3448
|
+
intersect = lineIntersection([p0, p1], sg)
|
3309
3449
|
next unless intersect
|
3310
3450
|
|
3311
|
-
#
|
3451
|
+
# Skip test altogether if one of the polygon vertices.
|
3312
3452
|
if holds?(s, intersect)
|
3313
|
-
|
3314
|
-
|
3315
|
-
|
3453
|
+
ctr = 0
|
3454
|
+
break
|
3455
|
+
else
|
3456
|
+
ctr += 1
|
3316
3457
|
end
|
3317
|
-
|
3318
|
-
ctr += 1
|
3319
3458
|
end
|
3320
3459
|
|
3321
3460
|
next if ctr.zero?
|
@@ -3334,56 +3473,88 @@ module OSut
|
|
3334
3473
|
# @return [Bool] whether 2 polygons are parallel
|
3335
3474
|
# @return [false] if invalid input (see logs)
|
3336
3475
|
def parallel?(p1 = nil, p2 = nil)
|
3337
|
-
p1
|
3338
|
-
p2
|
3476
|
+
p1 = poly(p1, false, true)
|
3477
|
+
p2 = poly(p2, false, true)
|
3339
3478
|
return false if p1.empty?
|
3340
3479
|
return false if p2.empty?
|
3341
3480
|
|
3342
|
-
|
3343
|
-
|
3344
|
-
return false if
|
3345
|
-
return false if
|
3481
|
+
n1 = OpenStudio.getOutwardNormal(p1)
|
3482
|
+
n2 = OpenStudio.getOutwardNormal(p2)
|
3483
|
+
return false if n1.empty?
|
3484
|
+
return false if n2.empty?
|
3485
|
+
|
3486
|
+
n1.get.dot(n2.get).abs > 0.99
|
3487
|
+
end
|
3488
|
+
|
3489
|
+
##
|
3490
|
+
# Validates whether a polygon can be considered a valid 'roof' surface, as per
|
3491
|
+
# ASHRAE 90.1 & Canadian NECBs, i.e. outward normal within 60° from vertical
|
3492
|
+
#
|
3493
|
+
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
3494
|
+
#
|
3495
|
+
# @return [Bool] if considered a roof surface
|
3496
|
+
# @return [false] if invalid input (see logs)
|
3497
|
+
def roof?(pts = nil)
|
3498
|
+
ray = OpenStudio::Point3d.new(0,0,1) - OpenStudio::Point3d.new(0,0,0)
|
3499
|
+
dut = Math.cos(60 * Math::PI / 180)
|
3500
|
+
pts = poly(pts, false, true, true)
|
3501
|
+
return false if pts.empty?
|
3346
3502
|
|
3347
|
-
|
3348
|
-
|
3503
|
+
dot = ray.dot(OpenStudio.getOutwardNormal(pts).get)
|
3504
|
+
return false if dot.round(2) <= 0
|
3505
|
+
return true if dot.round(2) == 1
|
3349
3506
|
|
3350
|
-
|
3507
|
+
dot.round(4) >= dut.round(4)
|
3351
3508
|
end
|
3352
3509
|
|
3353
3510
|
##
|
3354
|
-
# Validates whether a polygon faces upwards
|
3511
|
+
# Validates whether a polygon faces upwards, harmonized with OpenStudio
|
3512
|
+
# Utilities' "alignZPrime" function.
|
3355
3513
|
#
|
3356
3514
|
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
3357
3515
|
#
|
3358
3516
|
# @return [Bool] if facing upwards
|
3359
3517
|
# @return [false] if invalid input (see logs)
|
3360
3518
|
def facingUp?(pts = nil)
|
3361
|
-
|
3362
|
-
pts = poly(pts, false, true,
|
3363
|
-
return false if pts.empty?
|
3364
|
-
|
3365
|
-
pts = getNonCollinears(pts, 3)
|
3519
|
+
ray = OpenStudio::Point3d.new(0,0,1) - OpenStudio::Point3d.new(0,0,0)
|
3520
|
+
pts = poly(pts, false, true, true)
|
3366
3521
|
return false if pts.empty?
|
3367
3522
|
|
3368
|
-
OpenStudio
|
3523
|
+
OpenStudio.getOutwardNormal(pts).get.dot(ray) > 0.99
|
3369
3524
|
end
|
3370
3525
|
|
3371
3526
|
##
|
3372
|
-
# Validates whether a polygon faces downwards
|
3527
|
+
# Validates whether a polygon faces downwards, harmonized with OpenStudio
|
3528
|
+
# Utilities' "alignZPrime" function.
|
3373
3529
|
#
|
3374
3530
|
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
3375
3531
|
#
|
3376
3532
|
# @return [Bool] if facing downwards
|
3377
3533
|
# @return [false] if invalid input (see logs)
|
3378
3534
|
def facingDown?(pts = nil)
|
3379
|
-
|
3380
|
-
pts = poly(pts, false, true,
|
3535
|
+
ray = OpenStudio::Point3d.new(0,0,-1) - OpenStudio::Point3d.new(0,0,0)
|
3536
|
+
pts = poly(pts, false, true, true)
|
3381
3537
|
return false if pts.empty?
|
3382
3538
|
|
3383
|
-
pts
|
3539
|
+
OpenStudio.getOutwardNormal(pts).get.dot(ray) > 0.99
|
3540
|
+
end
|
3541
|
+
|
3542
|
+
##
|
3543
|
+
# Validates whether surface can be considered 'sloped' (i.e. not ~flat, as per
|
3544
|
+
# OpenStudio Utilities' "alignZPrime"). A vertical polygon returns true.
|
3545
|
+
#
|
3546
|
+
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
3547
|
+
#
|
3548
|
+
# @return [Bool] whether surface is sloped
|
3549
|
+
# @return [false] if invalid input (see logs)
|
3550
|
+
def sloped?(pts = nil)
|
3551
|
+
mth = "OSut::#{__callee__}"
|
3552
|
+
pts = poly(pts, false, true, true)
|
3384
3553
|
return false if pts.empty?
|
3554
|
+
return false if facingUp?(pts)
|
3555
|
+
return false if facingDown?(pts)
|
3385
3556
|
|
3386
|
-
|
3557
|
+
true
|
3387
3558
|
end
|
3388
3559
|
|
3389
3560
|
##
|
@@ -3424,7 +3595,7 @@ module OSut
|
|
3424
3595
|
return false if pts.empty?
|
3425
3596
|
return false unless rectangular?(pts)
|
3426
3597
|
|
3427
|
-
|
3598
|
+
segments(pts).each do |pt|
|
3428
3599
|
l = (pt[1] - pt[0]).length
|
3429
3600
|
d = l unless d
|
3430
3601
|
return false unless l.round(2) == d.round(2)
|
@@ -3454,6 +3625,15 @@ module OSut
|
|
3454
3625
|
|
3455
3626
|
p1.each { |p0| return false unless pointWithinPolygon?(p0, p2) }
|
3456
3627
|
|
3628
|
+
# Although p2 points may lie ALONG p1, none may lie entirely WITHIN p1.
|
3629
|
+
p2.each { |p0| return false if pointWithinPolygon?(p0, p1, true) }
|
3630
|
+
|
3631
|
+
# p1 segment mid-points must not lie OUTSIDE of p2.
|
3632
|
+
segments(p1).each do |sg|
|
3633
|
+
mp = midpoint(sg.first, sg.last)
|
3634
|
+
return false unless pointWithinPolygon?(mp, p2)
|
3635
|
+
end
|
3636
|
+
|
3457
3637
|
entirely = false unless [true, false].include?(entirely)
|
3458
3638
|
return true unless entirely
|
3459
3639
|
|
@@ -3492,22 +3672,17 @@ module OSut
|
|
3492
3672
|
cw1 = clockwise?(p01)
|
3493
3673
|
a1 = cw1 ? p01.to_a.reverse : p01.to_a
|
3494
3674
|
a2 = p02.to_a
|
3495
|
-
a2 = flatten(a2).to_a if flat
|
3496
|
-
return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z)
|
3497
|
-
|
3498
|
-
cw2 = clockwise?(a2)
|
3499
|
-
a2 = a2.reverse if cw2
|
3500
3675
|
else
|
3501
3676
|
t = OpenStudio::Transformation.alignFace(p01)
|
3502
3677
|
a1 = t.inverse * p01
|
3503
3678
|
a2 = t.inverse * p02
|
3504
|
-
a2 = flatten(a2).to_a if flat
|
3505
|
-
return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z)
|
3506
|
-
|
3507
|
-
cw2 = clockwise?(a2)
|
3508
|
-
a2 = a2.reverse if cw2
|
3509
3679
|
end
|
3510
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
|
+
|
3511
3686
|
# Return either (transformed) polygon if one fits into the other.
|
3512
3687
|
p1t = p01
|
3513
3688
|
|
@@ -3587,7 +3762,7 @@ module OSut
|
|
3587
3762
|
p2 = poly(p2)
|
3588
3763
|
return face if p1.empty?
|
3589
3764
|
return face if p2.empty?
|
3590
|
-
return mismatch("ray", ray, cl, mth) unless ray.is_a?(cl)
|
3765
|
+
return mismatch("ray", ray, cl, mth, face) unless ray.is_a?(cl)
|
3591
3766
|
|
3592
3767
|
# From OpenStudio SDK v3.7.0 onwards, one could/should rely on:
|
3593
3768
|
#
|
@@ -3600,7 +3775,7 @@ module OSut
|
|
3600
3775
|
# The following +/- replicates the same solution, based on:
|
3601
3776
|
# https://stackoverflow.com/a/65832417
|
3602
3777
|
p0 = p2.first
|
3603
|
-
pl = OpenStudio::Plane.new(
|
3778
|
+
pl = OpenStudio::Plane.new(p2)
|
3604
3779
|
n = pl.outwardNormal
|
3605
3780
|
return face if n.dot(ray).abs < TOL
|
3606
3781
|
|
@@ -3920,12 +4095,12 @@ module OSut
|
|
3920
4095
|
#
|
3921
4096
|
# @param [Set<OpenStudio::Point3d>] a triad (3D points)
|
3922
4097
|
#
|
3923
|
-
# @return [Set<OpenStudio::Point3D>] a rectangular
|
4098
|
+
# @return [Set<OpenStudio::Point3D>] a rectangular BLC box (see logs if empty)
|
3924
4099
|
def triadBox(pts = nil)
|
3925
4100
|
mth = "OSut::#{__callee__}"
|
3926
4101
|
bkp = OpenStudio::Point3dVector.new
|
3927
4102
|
box = []
|
3928
|
-
pts =
|
4103
|
+
pts = nonCollinears(pts)
|
3929
4104
|
return bkp if pts.empty?
|
3930
4105
|
|
3931
4106
|
t = xyz?(pts, :z) ? nil : OpenStudio::Transformation.alignFace(pts)
|
@@ -3962,6 +4137,8 @@ module OSut
|
|
3962
4137
|
box << OpenStudio::Point3d.new(p1.x, p1.y, p1.z)
|
3963
4138
|
box << OpenStudio::Point3d.new(p2.x, p2.y, p2.z)
|
3964
4139
|
box << OpenStudio::Point3d.new(p3.x, p3.y, p3.z)
|
4140
|
+
box = nonCollinears(box, 4)
|
4141
|
+
return bkp unless box.size == 4
|
3965
4142
|
|
3966
4143
|
box = blc(box)
|
3967
4144
|
return bkp unless rectangular?(box)
|
@@ -3993,7 +4170,7 @@ module OSut
|
|
3993
4170
|
|
3994
4171
|
# Generate vertical plane along longest segment.
|
3995
4172
|
mpoints = []
|
3996
|
-
sgs =
|
4173
|
+
sgs = segments(pts)
|
3997
4174
|
longest = sgs.max_by { |s| OpenStudio.getDistanceSquared(s.first, s.last) }
|
3998
4175
|
plane = verticalPlane(longest.first, longest.last)
|
3999
4176
|
|
@@ -4007,6 +4184,9 @@ module OSut
|
|
4007
4184
|
box << mpoints.first
|
4008
4185
|
box << mpoints.last
|
4009
4186
|
box << plane.project(mpoints.last)
|
4187
|
+
box = nonCollinears(box).to_a
|
4188
|
+
return bkp unless box.size == 4
|
4189
|
+
|
4010
4190
|
box = clockwise?(box) ? blc(box.reverse) : blc(box)
|
4011
4191
|
return bkp unless rectangular?(box)
|
4012
4192
|
return bkp unless fits?(box, pts)
|
@@ -4057,20 +4237,21 @@ module OSut
|
|
4057
4237
|
aire = 0
|
4058
4238
|
|
4059
4239
|
# PATH C : Right-angle, midpoint triad approach.
|
4060
|
-
|
4240
|
+
segments(pts).each do |sg|
|
4061
4241
|
m0 = midpoint(sg.first, sg.last)
|
4062
4242
|
|
4063
|
-
|
4243
|
+
segments(pts).each do |seg|
|
4064
4244
|
p1 = seg.first
|
4065
4245
|
p2 = seg.last
|
4066
4246
|
next if same?(p1, sg.first)
|
4067
4247
|
next if same?(p1, sg.last)
|
4068
4248
|
next if same?(p2, sg.first)
|
4069
|
-
next if same?(p2, sg.
|
4249
|
+
next if same?(p2, sg.last)
|
4070
4250
|
|
4071
4251
|
out = triadBox(OpenStudio::Point3dVector.new([m0, p1, p2]))
|
4072
4252
|
next if out.empty?
|
4073
4253
|
next unless fits?(out, pts)
|
4254
|
+
next if fits?(pts, out)
|
4074
4255
|
|
4075
4256
|
area = OpenStudio.getArea(out)
|
4076
4257
|
next if area.empty?
|
@@ -4085,7 +4266,7 @@ module OSut
|
|
4085
4266
|
end
|
4086
4267
|
|
4087
4268
|
# PATH D : Right-angle triad approach, may override PATH C boxes.
|
4088
|
-
|
4269
|
+
segments(pts).each do |sg|
|
4089
4270
|
p0 = sg.first
|
4090
4271
|
p1 = sg.last
|
4091
4272
|
|
@@ -4096,6 +4277,7 @@ module OSut
|
|
4096
4277
|
out = triadBox(OpenStudio::Point3dVector.new([p0, p1, p2]))
|
4097
4278
|
next if out.empty?
|
4098
4279
|
next unless fits?(out, pts)
|
4280
|
+
next if fits?(pts, out)
|
4099
4281
|
|
4100
4282
|
area = OpenStudio.getArea(out)
|
4101
4283
|
next if area.empty?
|
@@ -4117,7 +4299,7 @@ module OSut
|
|
4117
4299
|
# PATH E : Medial box, segment approach.
|
4118
4300
|
aire = 0
|
4119
4301
|
|
4120
|
-
|
4302
|
+
segments(pts).each do |sg|
|
4121
4303
|
p0 = sg.first
|
4122
4304
|
p1 = sg.last
|
4123
4305
|
|
@@ -4128,6 +4310,7 @@ module OSut
|
|
4128
4310
|
out = medialBox(OpenStudio::Point3dVector.new([p0, p1, p2]))
|
4129
4311
|
next if out.empty?
|
4130
4312
|
next unless fits?(out, pts)
|
4313
|
+
next if fits?(pts, out)
|
4131
4314
|
|
4132
4315
|
area = OpenStudio.getArea(box)
|
4133
4316
|
next if area.empty?
|
@@ -4149,7 +4332,7 @@ module OSut
|
|
4149
4332
|
# PATH F : Medial box, triad approach.
|
4150
4333
|
aire = 0
|
4151
4334
|
|
4152
|
-
|
4335
|
+
triads(pts).each do |sg|
|
4153
4336
|
p0 = sg[0]
|
4154
4337
|
p1 = sg[1]
|
4155
4338
|
p2 = sg[2]
|
@@ -4157,6 +4340,7 @@ module OSut
|
|
4157
4340
|
out = medialBox(OpenStudio::Point3dVector.new([p0, p1, p2]))
|
4158
4341
|
next if out.empty?
|
4159
4342
|
next unless fits?(out, pts)
|
4343
|
+
next if fits?(pts, out)
|
4160
4344
|
|
4161
4345
|
area = OpenStudio.getArea(box)
|
4162
4346
|
next if area.empty?
|
@@ -4180,7 +4364,7 @@ module OSut
|
|
4180
4364
|
holes = OpenStudio::Point3dVectorVector.new
|
4181
4365
|
|
4182
4366
|
OpenStudio.computeTriangulation(outer, holes).each do |triangle|
|
4183
|
-
|
4367
|
+
segments(triangle).each do |sg|
|
4184
4368
|
p0 = sg.first
|
4185
4369
|
p1 = sg.last
|
4186
4370
|
|
@@ -4191,6 +4375,7 @@ module OSut
|
|
4191
4375
|
out = medialBox(OpenStudio::Point3dVector.new([p0, p1, p2]))
|
4192
4376
|
next if out.empty?
|
4193
4377
|
next unless fits?(out, pts)
|
4378
|
+
next if fits?(pts, out)
|
4194
4379
|
|
4195
4380
|
area = OpenStudio.getArea(out)
|
4196
4381
|
next if area.empty?
|
@@ -4214,25 +4399,29 @@ module OSut
|
|
4214
4399
|
|
4215
4400
|
##
|
4216
4401
|
# Generates re-'aligned' polygon vertices wrt main axis of symmetry of its
|
4217
|
-
# largest bounded box.
|
4402
|
+
# largest bounded box. Input polygon vertex Z-axis values must equal 0, and be
|
4403
|
+
# counterclockwise. A Hash is returned with 6x key:value pairs ...
|
4218
4404
|
# set: realigned (cloned) polygon vertices, box: its bounded box (wrt to :set),
|
4219
4405
|
# bbox: its bounding box, t: its translation transformation, r: its rotation
|
4220
4406
|
# transformation, and o: the origin coordinates of its axis of rotation. First,
|
4221
4407
|
# cloned polygon vertices are rotated so the longest axis of symmetry of its
|
4222
4408
|
# bounded box lies parallel to the X-axis; :o being the midpoint of the narrow
|
4223
|
-
# side (of the bounded box) nearest to grid origin (0,0,0).
|
4409
|
+
# side (of the bounded box) nearest to grid origin (0,0,0). If the axis of
|
4410
|
+
# symmetry of the bounded box is already parallel to the X-axis, then the
|
4411
|
+
# rotation step is skipped (unless force == true). Whether rotated or not,
|
4224
4412
|
# polygon vertices are then translated as to ensure one or more vertices are
|
4225
4413
|
# aligned along the X-axis and one or more vertices are aligned along the
|
4226
|
-
# Y-axis (no vertices with negative X or Y coordinate values). To unalign
|
4227
|
-
# returned set of vertices (or its bounded box, or its bounding box),
|
4228
|
-
# inverse the translation transformation, then inverse the rotation
|
4414
|
+
# Y-axis (no vertices with negative X or Y coordinate values). To unalign
|
4415
|
+
# the returned set of vertices (or its bounded box, or its bounding box),
|
4416
|
+
# first inverse the translation transformation, then inverse the rotation
|
4229
4417
|
# transformation.
|
4230
4418
|
#
|
4231
4419
|
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
4420
|
+
# @param force [Bool] whether to force rotation for aligned yet narrow boxes
|
4232
4421
|
#
|
4233
4422
|
# @return [Hash] :set, :box, :bbox, :t, :r & :o
|
4234
4423
|
# @return [Hash] :set, :box, :bbox, :t, :r & :o (nil) if invalid (see logs)
|
4235
|
-
def
|
4424
|
+
def realignedFace(pts = nil, force = false)
|
4236
4425
|
mth = "OSut::#{__callee__}"
|
4237
4426
|
out = { set: nil, box: nil, bbox: nil, t: nil, r: nil, o: nil }
|
4238
4427
|
pts = poly(pts, false, true)
|
@@ -4240,6 +4429,13 @@ module OSut
|
|
4240
4429
|
return invalid("aligned plane", mth, 1, DBG, out) unless xyz?(pts, :z)
|
4241
4430
|
return invalid("clockwise pts", mth, 1, DBG, out) if clockwise?(pts)
|
4242
4431
|
|
4432
|
+
# Optionally force rotation so bounded box ends up wider than taller.
|
4433
|
+
# Strongly suggested for flat surfaces like roofs (see 'sloped?').
|
4434
|
+
unless [true, false].include?(force)
|
4435
|
+
log(DBG, "Ignoring force input (#{mth})")
|
4436
|
+
force = false
|
4437
|
+
end
|
4438
|
+
|
4243
4439
|
o = OpenStudio::Point3d.new(0, 0, 0)
|
4244
4440
|
w = width(pts)
|
4245
4441
|
h = height(pts)
|
@@ -4248,11 +4444,11 @@ module OSut
|
|
4248
4444
|
box = boundedBox(pts)
|
4249
4445
|
return invalid("bounded box", mth, 0, DBG, out) if box.empty?
|
4250
4446
|
|
4251
|
-
|
4252
|
-
return invalid("bounded box segments", mth, 0, DBG, out) if
|
4447
|
+
sgments = segments(box)
|
4448
|
+
return invalid("bounded box segments", mth, 0, DBG, out) if sgments.empty?
|
4253
4449
|
|
4254
4450
|
# Deterministic ID of box rotation/translation 'origin'.
|
4255
|
-
|
4451
|
+
sgments.each_with_index do |sg, idx|
|
4256
4452
|
sgs[sg] = {}
|
4257
4453
|
sgs[sg][:idx] = idx
|
4258
4454
|
sgs[sg][:mid] = midpoint(sg[0], sg[1])
|
@@ -4263,7 +4459,6 @@ module OSut
|
|
4263
4459
|
sgs = sgs.sort_by { |sg, s| s[:mo] }.first(2).to_h if square?(box)
|
4264
4460
|
sgs = sgs.sort_by { |sg, s| s[:l ] }.first(2).to_h unless square?(box)
|
4265
4461
|
sgs = sgs.sort_by { |sg, s| s[:mo] }.first(2).to_h unless square?(box)
|
4266
|
-
|
4267
4462
|
sg0 = sgs.values[0]
|
4268
4463
|
sg1 = sgs.values[1]
|
4269
4464
|
|
@@ -4273,16 +4468,23 @@ module OSut
|
|
4273
4468
|
i = sg0[:idx]
|
4274
4469
|
end
|
4275
4470
|
|
4276
|
-
k = i + 2 <
|
4471
|
+
k = i + 2 < sgments.size ? i + 2 : i - 2
|
4277
4472
|
|
4278
|
-
origin = midpoint(
|
4279
|
-
terminal = midpoint(
|
4473
|
+
origin = midpoint(sgments[i][0], sgments[i][1])
|
4474
|
+
terminal = midpoint(sgments[k][0], sgments[k][1])
|
4280
4475
|
seg = terminal - origin
|
4281
4476
|
right = OpenStudio::Point3d.new(origin.x + d, origin.y , 0) - origin
|
4282
4477
|
north = OpenStudio::Point3d.new(origin.x, origin.y + d, 0) - origin
|
4283
4478
|
axis = OpenStudio::Point3d.new(origin.x, origin.y , d) - origin
|
4284
4479
|
angle = OpenStudio::getAngle(right, seg)
|
4285
4480
|
angle = -angle if north.dot(seg) < 0
|
4481
|
+
|
4482
|
+
# Skip rotation if bounded box is already aligned along XY grid (albeit
|
4483
|
+
# 'narrow'), i.e. if the angle is 90°.
|
4484
|
+
if angle.round(3) == (Math::PI/2).round(3)
|
4485
|
+
angle = 0 unless force
|
4486
|
+
end
|
4487
|
+
|
4286
4488
|
r = OpenStudio.createRotation(origin, axis, angle)
|
4287
4489
|
pts = to_p3Dv(r.inverse * pts)
|
4288
4490
|
box = to_p3Dv(r.inverse * box)
|
@@ -4291,13 +4493,13 @@ module OSut
|
|
4291
4493
|
xy = OpenStudio::Point3d.new(origin.x + dX, origin.y + dY, 0)
|
4292
4494
|
origin2 = xy - origin
|
4293
4495
|
t = OpenStudio.createTranslation(origin2)
|
4294
|
-
set = t.inverse * pts
|
4295
|
-
box = t.inverse * box
|
4496
|
+
set = to_p3Dv(t.inverse * pts)
|
4497
|
+
box = to_p3Dv(t.inverse * box)
|
4296
4498
|
bbox = outline([set])
|
4297
4499
|
|
4298
|
-
out[:set ] = set
|
4299
|
-
out[:box ] = box
|
4300
|
-
out[:bbox] = bbox
|
4500
|
+
out[:set ] = blc(set)
|
4501
|
+
out[:box ] = blc(box)
|
4502
|
+
out[:bbox] = blc(bbox)
|
4301
4503
|
out[:t ] = t
|
4302
4504
|
out[:r ] = r
|
4303
4505
|
out[:o ] = origin
|
@@ -4309,14 +4511,21 @@ module OSut
|
|
4309
4511
|
# Returns 'width' of a set of OpenStudio 3D points, once re/aligned.
|
4310
4512
|
#
|
4311
4513
|
# @param pts [Set<OpenStudio::Point3d>] 3D points, once re/aligned
|
4514
|
+
# @param force [Bool] whether to force rotation of (narrow) bounded box
|
4312
4515
|
#
|
4313
4516
|
# @return [Float] width along X-axis, once re/aligned
|
4314
4517
|
# @return [0.0] if invalid inputs
|
4315
|
-
def alignedWidth(pts = nil)
|
4518
|
+
def alignedWidth(pts = nil, force = false)
|
4519
|
+
mth = "OSut::#{__callee__}"
|
4316
4520
|
pts = poly(pts, false, true, true, true)
|
4317
4521
|
return 0 if pts.size < 2
|
4318
4522
|
|
4319
|
-
|
4523
|
+
unless [true, false].include?(force)
|
4524
|
+
log(DBG, "Ignoring force input (#{mth})")
|
4525
|
+
force = false
|
4526
|
+
end
|
4527
|
+
|
4528
|
+
pts = realignedFace(pts, force)[:set]
|
4320
4529
|
return 0 if pts.size < 2
|
4321
4530
|
|
4322
4531
|
pts.max_by(&:x).x - pts.min_by(&:x).x
|
@@ -4326,57 +4535,149 @@ module OSut
|
|
4326
4535
|
# Returns 'height' of a set of OpenStudio 3D points, once re/aligned.
|
4327
4536
|
#
|
4328
4537
|
# @param pts [Set<OpenStudio::Point3d>] 3D points, once re/aligned
|
4538
|
+
# @param force [Bool] whether to force rotation of (narrow) bounded box
|
4329
4539
|
#
|
4330
4540
|
# @return [Float] height along Y-axis, once re/aligned
|
4331
4541
|
# @return [0.0] if invalid inputs
|
4332
|
-
def alignedHeight(pts = nil)
|
4333
|
-
|
4542
|
+
def alignedHeight(pts = nil, force = false)
|
4543
|
+
mth = "OSut::#{__callee__}"
|
4544
|
+
pts = poly(pts, false, true, true, true)
|
4334
4545
|
return 0 if pts.size < 2
|
4335
4546
|
|
4336
|
-
|
4547
|
+
unless [true, false].include?(force)
|
4548
|
+
log(DBG, "Ignoring force input (#{mth})")
|
4549
|
+
force = false
|
4550
|
+
end
|
4551
|
+
|
4552
|
+
pts = realignedFace(pts, force)[:set]
|
4337
4553
|
return 0 if pts.size < 2
|
4338
4554
|
|
4339
4555
|
pts.max_by(&:y).y - pts.min_by(&:y).y
|
4340
4556
|
end
|
4341
4557
|
|
4342
4558
|
##
|
4343
|
-
#
|
4344
|
-
#
|
4345
|
-
# :vtx (key) vertices (users can select another collection of vertices, e.g.
|
4346
|
-
# tag == :box). The method minimally validates individual sets of vertices
|
4347
|
-
# (e.g. coplanarity, non-self-intersecting, no inter-set conflicts). Potential
|
4348
|
-
# leader lines cannot intersect each other, other 'tagged' set vertices or
|
4349
|
-
# original polygon edges. For highly-articulated cases (e.g. a narrow polygon
|
4350
|
-
# with multiple concavities, holding multiple sets), such leader line
|
4351
|
-
# conflicts will surely occur. The method relies on a 'first-come-first-served'
|
4352
|
-
# approach: sets without leader lines are ignored (check for set :void keys,
|
4353
|
-
# see error logs). It is recommended to sort sets prior to calling the method.
|
4559
|
+
# Fetch a space's full height (in space coordinates). The solution considers
|
4560
|
+
# all surface types ("Floor" vs "Wall" vs "RoofCeiling").
|
4354
4561
|
#
|
4355
|
-
# @param
|
4356
|
-
#
|
4357
|
-
# @
|
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
|
4358
4583
|
#
|
4359
|
-
# @return [
|
4360
|
-
def
|
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
|
+
|
4630
|
+
##
|
4631
|
+
# Identifies 'leader line anchors', i.e. specific 3D points of a (larger) set
|
4632
|
+
# (e.g. delineating a larger, parent polygon), each anchor linking the BLC
|
4633
|
+
# corner of one or more (smaller) subsets (free-floating within the parent)
|
4634
|
+
# - see follow-up 'genInserts'. Subsets may hold several 'tagged' vertices
|
4635
|
+
# (e.g. :box, :cbox). By default, the solution seeks to anchor subset :box
|
4636
|
+
# vertices. Users can select other tags, e.g. tag == :cbox. The solution
|
4637
|
+
# minimally validates individual subsets (e.g. no self-intersecting polygons,
|
4638
|
+
# coplanarity, no inter-subset conflicts, must fit within larger set).
|
4639
|
+
# Potential leader lines cannot intersect each other, similarly tagged subsets
|
4640
|
+
# or (parent) polygon edges. For highly-articulated cases (e.g. a narrow
|
4641
|
+
# parent polygon with multiple concavities, holding multiple subsets), such
|
4642
|
+
# leader line conflicts are likely unavoidable. It is recommended to first
|
4643
|
+
# sort subsets (e.g. areas), given the solution's 'first-come-first-served'
|
4644
|
+
# policy. Subsets without valid leader lines are ultimately ignored (check
|
4645
|
+
# for new set :void keys, see error logs). The larger set of points is
|
4646
|
+
# expected to be in space coordinates - not building or site coordinates,
|
4647
|
+
# while subset points are expected to 'fit?' in the larger set.
|
4648
|
+
#
|
4649
|
+
# @param s [Set<OpenStudio::Point3d>] a (larger) parent set of points
|
4650
|
+
# @param [Array<Hash>] set a collection of (smaller) sequenced points
|
4651
|
+
# @option [Symbol] tag sequence of subset vertices to target
|
4652
|
+
#
|
4653
|
+
# @return [Integer] number of successfully anchored subsets (see logs)
|
4654
|
+
def genAnchors(s = nil, set = [], tag = :box)
|
4361
4655
|
mth = "OSut::#{__callee__}"
|
4362
|
-
|
4363
|
-
t = nil
|
4656
|
+
n = 0
|
4364
4657
|
id = s.respond_to?(:nameString) ? "#{s.nameString}: " : ""
|
4365
4658
|
pts = poly(s)
|
4366
|
-
n
|
4367
|
-
return n if pts.empty?
|
4659
|
+
return invalid("#{id} polygon", mth, 1, DBG, n) if pts.empty?
|
4368
4660
|
return mismatch("set", set, Array, mth, DBG, n) unless set.respond_to?(:to_a)
|
4369
4661
|
|
4370
|
-
|
4662
|
+
origin = OpenStudio::Point3d.new(0,0,0)
|
4663
|
+
zenith = OpenStudio::Point3d.new(0,0,1)
|
4664
|
+
ray = zenith - origin
|
4665
|
+
set = set.to_a
|
4371
4666
|
|
4372
|
-
# Validate individual
|
4667
|
+
# Validate individual subsets. Purge surface-specific leader line anchors.
|
4373
4668
|
set.each_with_index do |st, i|
|
4374
|
-
str1 = id + "
|
4669
|
+
str1 = id + "subset ##{i+1}"
|
4375
4670
|
str2 = str1 + " #{tag.to_s}"
|
4376
4671
|
return mismatch(str1, st, Hash, mth, DBG, n) unless st.respond_to?(:key?)
|
4377
4672
|
return hashkey( str1, st, tag, mth, DBG, n) unless st.key?(tag)
|
4378
4673
|
return empty("#{str2} vertices", mth, DBG, n) if st[tag].empty?
|
4379
4674
|
|
4675
|
+
if st.key?(:out)
|
4676
|
+
return hashkey( str1, st, :t, mth, DBG, n) unless st.key?(:t)
|
4677
|
+
return hashkey( str1, st, :ti, mth, DBG, n) unless st.key?(:ti)
|
4678
|
+
return hashkey( str1, st, :t0, mth, DBG, n) unless st.key?(:t0)
|
4679
|
+
end
|
4680
|
+
|
4380
4681
|
stt = poly(st[tag])
|
4381
4682
|
return invalid("#{str2} polygon", mth, 0, DBG, n) if stt.empty?
|
4382
4683
|
return invalid("#{str2} gap", mth, 0, DBG, n) unless fits?(stt, pts, true)
|
@@ -4391,76 +4692,87 @@ module OSut
|
|
4391
4692
|
end
|
4392
4693
|
end
|
4393
4694
|
|
4394
|
-
|
4395
|
-
|
4396
|
-
|
4695
|
+
set.each_with_index do |st, i|
|
4696
|
+
# When a subset already holds a leader line anchor (from an initial call
|
4697
|
+
# to 'genAnchors'), it inherits key :out - a Hash holding (among others) a
|
4698
|
+
# 'realigned' set of points (by default a 'realigned' :box). The latter is
|
4699
|
+
# typically generated from an outdoor-facing roof (e.g. when called from
|
4700
|
+
# 'lights'). Subsequent calls to 'genAnchors' may send (as first
|
4701
|
+
# argument) a corresponding ceiling tile below (also from 'addSkylights').
|
4702
|
+
# Roof vs ceiling may neither share alignment transformation nor space
|
4703
|
+
# site transformation identities. All subsequent calls to 'genAnchors'
|
4704
|
+
# shall recover the :out points, apply a succession of de/alignments and
|
4705
|
+
# transformations in sync , and overwrite tagged points.
|
4706
|
+
#
|
4707
|
+
# Although 'genAnchors' and 'genInserts' have both been developed to
|
4708
|
+
# support anchor insertions in other cases (e.g. bay window in a wall),
|
4709
|
+
# variables and terminology here continue pertain to roofs, ceilings,
|
4710
|
+
# skylights and wells - less abstract, simpler to follow.
|
4711
|
+
if st.key?(:out)
|
4712
|
+
ti = st[:ti ] # unoccupied attic/plenum space site transformation
|
4713
|
+
t0 = st[:t0 ] # occupied space site transformation
|
4714
|
+
t = st[:t ] # initial alignment transformation of roof surface
|
4715
|
+
o = st[:out]
|
4716
|
+
tpts = t0.inverse * (ti * (t * (o[:r] * (o[:t] * o[:set]))))
|
4717
|
+
tpts = cast(tpts, pts, ray)
|
4718
|
+
|
4719
|
+
st[tag] = tpts
|
4397
4720
|
else
|
4398
|
-
|
4399
|
-
|
4721
|
+
st[:t] = OpenStudio::Transformation.alignFace(pts) unless st.key?(:t)
|
4722
|
+
tpts = st[:t].inverse * st[tag]
|
4723
|
+
o = realignedFace(tpts, true)
|
4724
|
+
tpts = st[:t] * (o[:r] * (o[:t] * o[:set]))
|
4725
|
+
|
4726
|
+
st[:out] = o
|
4727
|
+
st[tag ] = tpts
|
4400
4728
|
end
|
4401
|
-
else
|
4402
|
-
t = OpenStudio::Transformation.alignFace(pts)
|
4403
|
-
pts = t.inverse * pts
|
4404
4729
|
end
|
4405
4730
|
|
4406
|
-
#
|
4407
|
-
# anchor with shortest distance to first vertex of 'tagged' set.
|
4731
|
+
# Identify candidate leader line anchors for each subset.
|
4408
4732
|
set.each_with_index do |st, i|
|
4409
4733
|
candidates = []
|
4410
|
-
|
4734
|
+
tpts = st[tag]
|
4411
4735
|
|
4412
|
-
|
4413
|
-
|
4736
|
+
pts.each do |pt|
|
4737
|
+
ld = [pt, tpts.first]
|
4738
|
+
nb = 0
|
4414
4739
|
|
4415
|
-
|
4416
|
-
|
4417
|
-
nb = 0
|
4418
|
-
|
4419
|
-
# Check for intersections between leader line and polygon edges.
|
4420
|
-
getSegments(pts).each do |sg|
|
4740
|
+
# Check for intersections between leader line and larger polygon edges.
|
4741
|
+
segments(pts).each do |sg|
|
4421
4742
|
break unless nb.zero?
|
4422
4743
|
next if holds?(sg, pt)
|
4423
4744
|
|
4424
4745
|
nb += 1 if lineIntersects?(sg, ld)
|
4425
4746
|
end
|
4426
4747
|
|
4427
|
-
|
4428
|
-
|
4429
|
-
# Check for intersections between candidate leader line and other sets.
|
4430
|
-
set.each_with_index do |other, j|
|
4748
|
+
# Check for intersections between candidate leader line vs other subsets.
|
4749
|
+
set.each do |other|
|
4431
4750
|
break unless nb.zero?
|
4432
|
-
next if
|
4751
|
+
next if st == other
|
4433
4752
|
|
4434
|
-
ost =
|
4435
|
-
sgj = getSegments(ost)
|
4753
|
+
ost = other[tag]
|
4436
4754
|
|
4437
|
-
|
4755
|
+
segments(ost).each { |sg| nb += 1 if lineIntersects?(ld, sg) }
|
4438
4756
|
end
|
4439
4757
|
|
4440
|
-
next unless nb.zero?
|
4441
|
-
|
4442
4758
|
# ... and previous leader lines (first come, first serve basis).
|
4443
|
-
set.
|
4759
|
+
set.each do |other|
|
4444
4760
|
break unless nb.zero?
|
4445
|
-
next if
|
4761
|
+
next if st == other
|
4762
|
+
next unless other.key?(:ld)
|
4446
4763
|
next unless other[:ld].key?(s)
|
4447
4764
|
|
4448
4765
|
ost = other[tag]
|
4449
|
-
|
4450
|
-
|
4451
|
-
ldj = dZ ? flatten([ old, pj ]) : t.inverse * [ old, pj ]
|
4766
|
+
pld = other[:ld][s]
|
4767
|
+
next if same?(pld, pt)
|
4452
4768
|
|
4453
|
-
|
4454
|
-
nb += 1 if lineIntersects?(ld, ldj)
|
4455
|
-
end
|
4769
|
+
nb += 1 if lineIntersects?(ld, [pld, ost.first])
|
4456
4770
|
end
|
4457
4771
|
|
4458
|
-
next unless nb.zero?
|
4459
|
-
|
4460
4772
|
# Finally, check for self-intersections.
|
4461
|
-
|
4773
|
+
segments(tpts).each do |sg|
|
4462
4774
|
break unless nb.zero?
|
4463
|
-
next if holds?(sg,
|
4775
|
+
next if holds?(sg, tpts.first)
|
4464
4776
|
|
4465
4777
|
nb += 1 if lineIntersects?(sg, ld)
|
4466
4778
|
nb += 1 if (sg.first - sg.last).cross(ld.first - ld.last).length < TOL
|
@@ -4471,18 +4783,13 @@ module OSut
|
|
4471
4783
|
|
4472
4784
|
if candidates.empty?
|
4473
4785
|
str = id + "set ##{i+1}"
|
4474
|
-
log(
|
4786
|
+
log(WRN, "#{str}: unable to anchor #{tag} leader line (#{mth})")
|
4475
4787
|
st[:void] = true
|
4476
4788
|
else
|
4477
|
-
p0 = candidates.sort_by
|
4478
|
-
|
4479
|
-
if dZ
|
4480
|
-
st[:ld][s] = OpenStudio::Point3d.new(p0.x, p0.y, p0.z + dZ)
|
4481
|
-
else
|
4482
|
-
st[:ld][s] = t * p0
|
4483
|
-
end
|
4484
|
-
|
4789
|
+
p0 = candidates.sort_by { |pt| (pt - tpts.first).length }.first
|
4485
4790
|
n += 1
|
4791
|
+
|
4792
|
+
st[:ld][s] = p0
|
4486
4793
|
end
|
4487
4794
|
end
|
4488
4795
|
|
@@ -4490,17 +4797,19 @@ module OSut
|
|
4490
4797
|
end
|
4491
4798
|
|
4492
4799
|
##
|
4493
|
-
#
|
4494
|
-
#
|
4495
|
-
#
|
4496
|
-
#
|
4497
|
-
#
|
4498
|
-
#
|
4499
|
-
#
|
4800
|
+
# Extends (larger) polygon vertices to circumscribe one or more (smaller)
|
4801
|
+
# subsets of vertices, based on previously-generated 'leader line' anchors.
|
4802
|
+
# The solution minimally validates individual subsets (e.g. no
|
4803
|
+
# self-intersecting polygons, coplanarity, no inter-subset conflicts, must fit
|
4804
|
+
# within larger set). Valid leader line anchors (set key :ld) need to be
|
4805
|
+
# generated prior to calling the method - see 'genAnchors'. Subsets may hold
|
4806
|
+
# several 'tag'ged vertices (e.g. :box, :vtx). By default, the solution
|
4807
|
+
# seeks to anchor subset :vtx vertices. Users can select other tags, e.g.
|
4808
|
+
# tag == :box).
|
4500
4809
|
#
|
4501
4810
|
# @param s [Set<OpenStudio::Point3d>] a larger (parent) set of points
|
4502
|
-
# @param [Array<Hash>] set a collection of sequenced vertices
|
4503
|
-
# @option set [Hash] :ld a
|
4811
|
+
# @param [Array<Hash>] set a collection of (smaller) sequenced vertices
|
4812
|
+
# @option set [Hash] :ld a polygon-specific leader line anchors
|
4504
4813
|
# @option [Symbol] tag sequence of set vertices to target
|
4505
4814
|
#
|
4506
4815
|
# @return [OpenStudio::Point3dVector] extended vertices (see logs if empty)
|
@@ -4519,9 +4828,11 @@ module OSut
|
|
4519
4828
|
|
4520
4829
|
# Validate individual sets.
|
4521
4830
|
set.each_with_index do |st, i|
|
4522
|
-
str1 = id + "
|
4831
|
+
str1 = id + "subset ##{i+1}"
|
4523
4832
|
str2 = str1 + " #{tag.to_s}"
|
4524
4833
|
return mismatch(str1, st, Hash, mth, DBG, a) unless st.respond_to?(:key?)
|
4834
|
+
next if st.key?(:void) && st[:void]
|
4835
|
+
|
4525
4836
|
return hashkey( str1, st, tag, mth, DBG, a) unless st.key?(tag)
|
4526
4837
|
return empty("#{str2} vertices", mth, DBG, a) if st[tag].empty?
|
4527
4838
|
return hashkey( str1, st, :ld, mth, DBG, a) unless st.key?(:ld)
|
@@ -4530,9 +4841,9 @@ module OSut
|
|
4530
4841
|
return invalid("#{str2} polygon", mth, 0, DBG, a) if stt.empty?
|
4531
4842
|
|
4532
4843
|
ld = st[:ld]
|
4533
|
-
return mismatch(
|
4534
|
-
return hashkey(
|
4535
|
-
return mismatch(
|
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)
|
4536
4847
|
end
|
4537
4848
|
|
4538
4849
|
# Re-sequence polygon vertices.
|
@@ -4540,7 +4851,8 @@ module OSut
|
|
4540
4851
|
v << pt
|
4541
4852
|
|
4542
4853
|
# Loop through each valid set; concatenate circumscribing vertices.
|
4543
|
-
set.
|
4854
|
+
set.each do |st|
|
4855
|
+
next if st.key?(:void) && st[:void]
|
4544
4856
|
next unless same?(st[:ld][s], pt)
|
4545
4857
|
next unless st.key?(tag)
|
4546
4858
|
|
@@ -4553,21 +4865,22 @@ module OSut
|
|
4553
4865
|
end
|
4554
4866
|
|
4555
4867
|
##
|
4556
|
-
# Generates
|
4557
|
-
#
|
4558
|
-
# (
|
4559
|
-
#
|
4560
|
-
#
|
4561
|
-
#
|
4562
|
-
#
|
4563
|
-
#
|
4564
|
-
# @param [
|
4565
|
-
# @
|
4566
|
-
# @option set [
|
4868
|
+
# Generates (1D or 2D) arrays of (smaller) rectangular collection of points,
|
4869
|
+
# (e.g. arrays of polygon inserts) from subset parameters, within a (larger)
|
4870
|
+
# set (e.g. parent polygon). If successful, each subset inherits additional
|
4871
|
+
# key:value pairs: namely :vtx (collection of circumscribing vertices), and
|
4872
|
+
# :vts (collection of individual insert vertices). Valid leader line anchors
|
4873
|
+
# (set key :ld) need to be generated prior to calling the solution
|
4874
|
+
# - see 'genAnchors'.
|
4875
|
+
#
|
4876
|
+
# @param s [Set<OpenStudio::Point3d>] a larger (parent) set of points
|
4877
|
+
# @param [Array<Hash>] set a collection of (smaller) sequenced vertices
|
4878
|
+
# @option set [Set<OpenStudio::Point3d>] :box bounding box of each subset
|
4879
|
+
# @option set [Hash] :ld a collection of leader line anchors
|
4567
4880
|
# @option set [Integer] :rows (1) number of rows of inserts
|
4568
4881
|
# @option set [Integer] :cols (1) number of columns of inserts
|
4569
|
-
# @option set [Numeric] :w0
|
4570
|
-
# @option set [Numeric] :d0
|
4882
|
+
# @option set [Numeric] :w0 width of individual inserts (wrt cols) min 0.4
|
4883
|
+
# @option set [Numeric] :d0 depth of individual inserts (wrt rows) min 0.4
|
4571
4884
|
# @option set [Numeric] :dX (0) optional left/right X-axis buffer
|
4572
4885
|
# @option set [Numeric] :dY (0) optional top/bottom Y-axis buffer
|
4573
4886
|
#
|
@@ -4587,10 +4900,12 @@ module OSut
|
|
4587
4900
|
|
4588
4901
|
# Validate/reset individual set collections.
|
4589
4902
|
set.each_with_index do |st, i|
|
4590
|
-
str1 = id + "
|
4903
|
+
str1 = id + "subset ##{i+1}"
|
4904
|
+
next if st.key?(:void) && st[:void]
|
4591
4905
|
return mismatch(str1, st, Hash, mth, DBG, a) unless st.respond_to?(:key?)
|
4592
4906
|
return hashkey( str1, st, :box, mth, DBG, a) unless st.key?(:box)
|
4593
4907
|
return hashkey( str1, st, :ld, mth, DBG, a) unless st.key?(:ld)
|
4908
|
+
return hashkey( str1, st, :out, mth, DBG, a) unless st.key?(:out)
|
4594
4909
|
|
4595
4910
|
str2 = str1 + " anchor"
|
4596
4911
|
ld = st[:ld]
|
@@ -4599,7 +4914,7 @@ module OSut
|
|
4599
4914
|
return mismatch(str2, ld[s], cl, mth, DBG, a) unless ld[s].is_a?(cl)
|
4600
4915
|
|
4601
4916
|
# Ensure each set bounding box is safely within larger polygon boundaries.
|
4602
|
-
#
|
4917
|
+
# @todo: In line with related addSkylights' @todo, expand solution to
|
4603
4918
|
# safely handle 'side' cutouts (i.e. no need for leader lines). In
|
4604
4919
|
# so doing, boxes could eventually align along surface edges.
|
4605
4920
|
str3 = str1 + " box"
|
@@ -4659,9 +4974,10 @@ module OSut
|
|
4659
4974
|
end
|
4660
4975
|
end
|
4661
4976
|
|
4662
|
-
# Flag conflicts between set bounding boxes.
|
4977
|
+
# Flag conflicts between set bounding boxes. @todo: ease up for ridges.
|
4663
4978
|
set.each_with_index do |st, i|
|
4664
4979
|
bx = st[:box]
|
4980
|
+
next if st.key?(:void) && st[:void]
|
4665
4981
|
|
4666
4982
|
set.each_with_index do |other, j|
|
4667
4983
|
next if i == j
|
@@ -4673,37 +4989,18 @@ module OSut
|
|
4673
4989
|
end
|
4674
4990
|
end
|
4675
4991
|
|
4676
|
-
|
4677
|
-
|
4678
|
-
# coordinates once completed.
|
4679
|
-
set.each_with_index do |st, i|
|
4680
|
-
str = id + "set ##{i+1}"
|
4681
|
-
dZ = nil
|
4682
|
-
t = nil
|
4683
|
-
bx = st[:box]
|
4684
|
-
|
4685
|
-
if facingUp?(bx)
|
4686
|
-
if xyz?(bx, :z)
|
4687
|
-
dZ = 0
|
4688
|
-
else
|
4689
|
-
dZ = bx.first.z
|
4690
|
-
bx = flatten(bx).to_a
|
4691
|
-
end
|
4692
|
-
else
|
4693
|
-
t = OpenStudio::Transformation.alignFace(bx)
|
4694
|
-
bx = t.inverse * bx
|
4695
|
-
end
|
4696
|
-
|
4697
|
-
o = getRealignedFace(bx)
|
4698
|
-
next unless o[:set]
|
4699
|
-
|
4700
|
-
st[:out] = o
|
4701
|
-
st[:bx ] = blc(o[:r] * (o[:t] * o[:set]))
|
4992
|
+
t = OpenStudio::Transformation.alignFace(pts)
|
4993
|
+
rpts = t.inverse * pts
|
4702
4994
|
|
4995
|
+
# Loop through each 'valid' subset (i.e. linking a valid leader line anchor),
|
4996
|
+
# generate subset vertex array based on user-provided specs.
|
4997
|
+
set.each_with_index do |st, i|
|
4998
|
+
str = id + "subset ##{i+1}"
|
4999
|
+
next if st.key?(:void) && st[:void]
|
4703
5000
|
|
5001
|
+
o = st[:out]
|
4704
5002
|
vts = {} # collection of individual (named) polygon insert vertices
|
4705
5003
|
vtx = [] # sequence of circumscribing polygon vertices
|
4706
|
-
|
4707
5004
|
bx = o[:set]
|
4708
5005
|
w = width(bx) # overall sandbox width
|
4709
5006
|
d = height(bx) # overall sandbox depth
|
@@ -4712,7 +5009,7 @@ module OSut
|
|
4712
5009
|
cols = st[:cols] # number of array columns
|
4713
5010
|
rows = st[:rows] # number of array rows
|
4714
5011
|
x = st[:w0 ] # width of individual insert
|
4715
|
-
y = st[:d0 ] # depth of
|
5012
|
+
y = st[:d0 ] # depth of individual insert
|
4716
5013
|
gX = 0 # gap between insert columns
|
4717
5014
|
gY = 0 # gap between insert rows
|
4718
5015
|
|
@@ -4790,15 +5087,7 @@ module OSut
|
|
4790
5087
|
vec << OpenStudio::Point3d.new(xC + x, yC , 0)
|
4791
5088
|
|
4792
5089
|
# Store.
|
4793
|
-
|
4794
|
-
|
4795
|
-
if dZ
|
4796
|
-
vz = OpenStudio::Point3dVector.new
|
4797
|
-
vtz.each { |v| vz << OpenStudio::Point3d.new(v.x, v.y, v.z + dZ) }
|
4798
|
-
vts[nom] = vz
|
4799
|
-
else
|
4800
|
-
vts[nom] = to_p3Dv(t * vtz)
|
4801
|
-
end
|
5090
|
+
vts[nom] = to_p3Dv(t * ulc(o[:r] * (o[:t] * vec)))
|
4802
5091
|
|
4803
5092
|
# Add reverse vertices, circumscribing each insert.
|
4804
5093
|
vec.reverse!
|
@@ -4814,18 +5103,8 @@ module OSut
|
|
4814
5103
|
end
|
4815
5104
|
end
|
4816
5105
|
|
4817
|
-
vtx = o[:r] * (o[:t] * vtx)
|
4818
|
-
|
4819
|
-
if dZ
|
4820
|
-
vz = OpenStudio::Point3dVector.new
|
4821
|
-
vtx.each { |v| vz << OpenStudio::Point3d.new(v.x, v.y, v.z + dZ) }
|
4822
|
-
vtx = vz
|
4823
|
-
else
|
4824
|
-
vtx = to_p3Dv(t * vtx)
|
4825
|
-
end
|
4826
|
-
|
4827
5106
|
st[:vts] = vts
|
4828
|
-
st[:vtx] = vtx
|
5107
|
+
st[:vtx] = to_p3Dv(t * (o[:r] * (o[:t] * vtx)))
|
4829
5108
|
end
|
4830
5109
|
|
4831
5110
|
# Extended vertex sequence of the larger polygon.
|
@@ -4835,9 +5114,11 @@ module OSut
|
|
4835
5114
|
##
|
4836
5115
|
# Returns an array of OpenStudio space surfaces or subsurfaces that match
|
4837
5116
|
# criteria, e.g. exterior, north-east facing walls in hotel "lobby". Note that
|
4838
|
-
# 'sides' rely on space coordinates (not
|
5117
|
+
# 'sides' rely on space coordinates (not building or site coordinates). Also,
|
4839
5118
|
# 'sides' are exclusive (not inclusive), e.g. walls strictly north-facing or
|
4840
5119
|
# strictly east-facing would not be returned if 'sides' holds [:north, :east].
|
5120
|
+
# No outside boundary condition filters if 'boundary' argument == "all". No
|
5121
|
+
# surface type filters if 'type' argument == "all".
|
4841
5122
|
#
|
4842
5123
|
# @param spaces [Set<OpenStudio::Model::Space>] target spaces
|
4843
5124
|
# @param boundary [#to_s] OpenStudio outside boundary condition
|
@@ -4845,7 +5126,7 @@ module OSut
|
|
4845
5126
|
# @param sides [Set<Symbols>] direction keys, e.g. :north (see OSut::SIDZ)
|
4846
5127
|
#
|
4847
5128
|
# @return [Array<OpenStudio::Model::Surface>] surfaces (may be empty, no logs)
|
4848
|
-
def facets(spaces = [], boundary = "
|
5129
|
+
def facets(spaces = [], boundary = "all", type = "all", sides = [])
|
4849
5130
|
spaces = spaces.is_a?(OpenStudio::Model::Space) ? [spaces] : spaces
|
4850
5131
|
spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
|
4851
5132
|
return [] if spaces.empty?
|
@@ -4859,8 +5140,8 @@ module OSut
|
|
4859
5140
|
return [] if boundary.empty?
|
4860
5141
|
return [] if type.empty?
|
4861
5142
|
|
4862
|
-
# Filter sides. If sides is initially empty, return all surfaces of
|
4863
|
-
# type and outside boundary condition.
|
5143
|
+
# Filter sides. If 'sides' is initially empty, return all surfaces of
|
5144
|
+
# matching type and outside boundary condition.
|
4864
5145
|
unless sides.empty?
|
4865
5146
|
sides = sides.select { |side| SIDZ.include?(side) }
|
4866
5147
|
return [] if sides.empty?
|
@@ -4870,8 +5151,13 @@ module OSut
|
|
4870
5151
|
return [] unless space.respond_to?(:setSpaceType)
|
4871
5152
|
|
4872
5153
|
space.surfaces.each do |s|
|
4873
|
-
|
4874
|
-
|
5154
|
+
unless boundary == "all"
|
5155
|
+
next unless s.outsideBoundaryCondition.downcase == boundary
|
5156
|
+
end
|
5157
|
+
|
5158
|
+
unless type == "all"
|
5159
|
+
next unless s.surfaceType.downcase == type
|
5160
|
+
end
|
4875
5161
|
|
4876
5162
|
if sides.empty?
|
4877
5163
|
faces << s
|
@@ -4891,13 +5177,15 @@ module OSut
|
|
4891
5177
|
|
4892
5178
|
# SubSurfaces?
|
4893
5179
|
spaces.each do |space|
|
4894
|
-
break unless faces.empty?
|
4895
|
-
|
4896
5180
|
space.surfaces.each do |s|
|
4897
|
-
|
5181
|
+
unless boundary == "all"
|
5182
|
+
next unless s.outsideBoundaryCondition.downcase == boundary
|
5183
|
+
end
|
4898
5184
|
|
4899
5185
|
s.subSurfaces.each do |sub|
|
4900
|
-
|
5186
|
+
unless type == "all"
|
5187
|
+
next unless sub.subSurfaceType.downcase == type
|
5188
|
+
end
|
4901
5189
|
|
4902
5190
|
if sides.empty?
|
4903
5191
|
faces << sub
|
@@ -4948,7 +5236,7 @@ module OSut
|
|
4948
5236
|
pltz.each_with_index do |plt, i|
|
4949
5237
|
id = "plate # #{i+1} (index #{i})"
|
4950
5238
|
|
4951
|
-
return mismatch(id, plt,
|
5239
|
+
return mismatch(id, plt, cl2, mth, DBG, slb) unless plt.is_a?(cl2)
|
4952
5240
|
return hashkey( id, plt, :x, mth, DBG, slb) unless plt.key?(:x )
|
4953
5241
|
return hashkey( id, plt, :y, mth, DBG, slb) unless plt.key?(:y )
|
4954
5242
|
return hashkey( id, plt, :dx, mth, DBG, slb) unless plt.key?(:dx)
|
@@ -4999,7 +5287,7 @@ module OSut
|
|
4999
5287
|
end
|
5000
5288
|
|
5001
5289
|
# Once joined, re-adjust Z-axis coordinates.
|
5002
|
-
unless z.
|
5290
|
+
unless z.round(2) == 0.00
|
5003
5291
|
vtx = OpenStudio::Point3dVector.new
|
5004
5292
|
slb.each { |pt| vtx << OpenStudio::Point3d.new(pt.x, pt.y, z) }
|
5005
5293
|
slb = vtx
|
@@ -5012,28 +5300,30 @@ module OSut
|
|
5012
5300
|
# Returns outdoor-facing, space-related roof surfaces. These include
|
5013
5301
|
# outdoor-facing roofs of each space per se, as well as any outdoor-facing
|
5014
5302
|
# roof surface of unoccupied spaces immediately above (e.g. plenums, attics)
|
5015
|
-
# overlapping any of the ceiling surfaces of each space.
|
5303
|
+
# overlapping any of the ceiling surfaces of each space. It does not include
|
5304
|
+
# surfaces labelled as 'RoofCeiling', which do not comply with ASHRAE 90.1 or
|
5305
|
+
# NECB tilt criteria - see 'roof?'.
|
5016
5306
|
#
|
5017
5307
|
# @param spaces [Set<OpenStudio::Model::Space>] target spaces
|
5018
5308
|
#
|
5019
5309
|
# @return [Array<OpenStudio::Model::Surface>] roofs (may be empty)
|
5020
|
-
def
|
5310
|
+
def roofs(spaces = [])
|
5021
5311
|
mth = "OSut::#{__callee__}"
|
5022
5312
|
up = OpenStudio::Point3d.new(0,0,1) - OpenStudio::Point3d.new(0,0,0)
|
5023
|
-
|
5313
|
+
rfs = []
|
5024
5314
|
spaces = spaces.is_a?(OpenStudio::Model::Space) ? [spaces] : spaces
|
5025
5315
|
spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
|
5026
5316
|
|
5027
5317
|
spaces = spaces.select { |space| space.is_a?(OpenStudio::Model::Space) }
|
5028
5318
|
|
5029
5319
|
# Space-specific outdoor-facing roof surfaces.
|
5030
|
-
|
5320
|
+
rfs = facets(spaces, "Outdoors", "RoofCeiling")
|
5321
|
+
rfs = rfs.select { |rf| roof?(rf) }
|
5031
5322
|
|
5032
|
-
# Outdoor-facing roof surfaces of unoccupied plenums or attics above?
|
5033
5323
|
spaces.each do |space|
|
5034
|
-
# When
|
5324
|
+
# When unoccupied spaces are involved (e.g. plenums, attics), the target
|
5035
5325
|
# space may not share the same local transformation as the space(s) above.
|
5036
|
-
# Fetching
|
5326
|
+
# Fetching site transformation.
|
5037
5327
|
t0 = transforms(space)
|
5038
5328
|
next unless t0[:t]
|
5039
5329
|
|
@@ -5056,18 +5346,20 @@ module OSut
|
|
5056
5346
|
|
5057
5347
|
ti = ti[:t]
|
5058
5348
|
|
5059
|
-
#
|
5349
|
+
# @todo: recursive call for stacked spaces as atria (via AirBoundaries).
|
5060
5350
|
facets(other, "Outdoors", "RoofCeiling").each do |ruf|
|
5351
|
+
next unless roof?(ruf)
|
5352
|
+
|
5061
5353
|
rvi = ti * ruf.vertices
|
5062
5354
|
cst = cast(cv0, rvi, up)
|
5063
5355
|
next unless overlaps?(cst, rvi, false)
|
5064
5356
|
|
5065
|
-
|
5357
|
+
rfs << ruf unless rfs.include?(ruf)
|
5066
5358
|
end
|
5067
5359
|
end
|
5068
5360
|
end
|
5069
5361
|
|
5070
|
-
|
5362
|
+
rfs
|
5071
5363
|
end
|
5072
5364
|
|
5073
5365
|
##
|
@@ -5093,10 +5385,10 @@ module OSut
|
|
5093
5385
|
return invalid("baselit" , mth, 4, DBG, false) unless ck4
|
5094
5386
|
|
5095
5387
|
walls = sidelit ? facets(space, "Outdoors", "Wall") : []
|
5096
|
-
|
5388
|
+
rufs = toplit ? facets(space, "Outdoors", "RoofCeiling") : []
|
5097
5389
|
floors = baselit ? facets(space, "Outdoors", "Floor") : []
|
5098
5390
|
|
5099
|
-
(walls +
|
5391
|
+
(walls + rufs + floors).each do |surface|
|
5100
5392
|
surface.subSurfaces.each do |sub|
|
5101
5393
|
# All fenestrated subsurface types are considered, as user can set these
|
5102
5394
|
# explicitly (e.g. skylight in a wall) in OpenStudio.
|
@@ -5128,11 +5420,13 @@ module OSut
|
|
5128
5420
|
# @option subs [#to_f] :r_buffer gap between sub/array and right corner
|
5129
5421
|
# @option subs [#to_f] :l_buffer gap between sub/array and left corner
|
5130
5422
|
# @param clear [Bool] whether to remove current sub surfaces
|
5423
|
+
# @param bound [Bool] whether to add subs wrt surface's bounded box
|
5424
|
+
# @param realign [Bool] whether to first realign bounded box
|
5131
5425
|
# @param bfr [#to_f] safety buffer, to maintain near other edges
|
5132
5426
|
#
|
5133
5427
|
# @return [Bool] whether addition is successful
|
5134
5428
|
# @return [false] if invalid input (see logs)
|
5135
|
-
def addSubs(s = nil, subs = [], clear = false, bfr = 0.005)
|
5429
|
+
def addSubs(s = nil, subs = [], clear = false, bound = false, realign = false, bfr = 0.005)
|
5136
5430
|
mth = "OSut::#{__callee__}"
|
5137
5431
|
v = OpenStudio.openStudioVersion.split(".").join.to_i
|
5138
5432
|
cl1 = OpenStudio::Model::Surface
|
@@ -5144,15 +5438,18 @@ module OSut
|
|
5144
5438
|
|
5145
5439
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5146
5440
|
# Exit if mismatched or invalid argument classes.
|
5147
|
-
|
5148
|
-
|
5441
|
+
sbs = subs.is_a?(cl3) ? [subs] : subs
|
5442
|
+
sbs = sbs.respond_to?(:to_a) ? sbs.to_a : []
|
5443
|
+
return mismatch("surface", s, cl1, mth, DBG, no) unless s.is_a?(cl1)
|
5444
|
+
return mismatch("subs", subs, cl2, mth, DBG, no) if sbs.empty?
|
5149
5445
|
return empty("surface points", mth, DBG, no) if poly(s).empty?
|
5150
5446
|
|
5151
|
-
|
5152
|
-
|
5153
|
-
|
5154
|
-
mdl = s.model
|
5447
|
+
subs = sbs
|
5448
|
+
nom = s.nameString
|
5449
|
+
mdl = s.model
|
5155
5450
|
|
5451
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5452
|
+
# Purge existing sub surfaces?
|
5156
5453
|
unless [true, false].include?(clear)
|
5157
5454
|
log(WRN, "#{nom}: Keeping existing sub surfaces (#{mth})")
|
5158
5455
|
clear = false
|
@@ -5161,9 +5458,30 @@ module OSut
|
|
5161
5458
|
s.subSurfaces.map(&:remove) if clear
|
5162
5459
|
|
5163
5460
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5164
|
-
#
|
5165
|
-
|
5166
|
-
|
5461
|
+
# Add sub surfaces with respect to base surface's bounded box? This is
|
5462
|
+
# often useful (in some cases necessary) with irregular or concave surfaces.
|
5463
|
+
# If true, sub surface parameters (e.g. height, offset, centreline) no
|
5464
|
+
# longer apply to the original surface 'bounding' box, but instead to its
|
5465
|
+
# largest 'bounded' box. This can be combined with the 'realign' parameter.
|
5466
|
+
unless [true, false].include?(bound)
|
5467
|
+
log(WRN, "#{nom}: Ignoring bounded box (#{mth})")
|
5468
|
+
bound = false
|
5469
|
+
end
|
5470
|
+
|
5471
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5472
|
+
# Force re-alignment of base surface (or its 'bounded' box)? False by
|
5473
|
+
# default (ideal for vertical/tilted walls & sloped roofs). If set to true
|
5474
|
+
# for a narrow wall for instance, an array of sub surfaces will be added
|
5475
|
+
# from bottom to top (rather from left to right).
|
5476
|
+
unless [true, false].include?(realign)
|
5477
|
+
log(WRN, "#{nom}: Ignoring realignment (#{mth})")
|
5478
|
+
realign = false
|
5479
|
+
end
|
5480
|
+
|
5481
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5482
|
+
# Ensure minimum safety buffer.
|
5483
|
+
if bfr.respond_to?(:to_f)
|
5484
|
+
bfr = bfr.to_f
|
5167
5485
|
return negative("safety buffer", mth, ERR, no) if bfr.round(2) < 0
|
5168
5486
|
|
5169
5487
|
msg = "Safety buffer < 5mm may generate invalid geometry (#{mth})"
|
@@ -5192,15 +5510,21 @@ module OSut
|
|
5192
5510
|
s0 = poly(s, false, false, false, t, :ulc)
|
5193
5511
|
s00 = nil
|
5194
5512
|
|
5195
|
-
if
|
5196
|
-
|
5197
|
-
|
5513
|
+
# Adapt sandbox if user selects to 'bound' and/or 'realign'.
|
5514
|
+
if bound
|
5515
|
+
box = boundedBox(s0)
|
5198
5516
|
|
5199
|
-
|
5517
|
+
if realign
|
5518
|
+
s00 = realignedFace(box, true)
|
5519
|
+
return invalid("bound realignment", mth, 0, DBG, false) unless s00[:set]
|
5520
|
+
end
|
5521
|
+
elsif realign
|
5522
|
+
s00 = realignedFace(s0, false)
|
5523
|
+
return invalid("unbound realignment", mth, 0, DBG, false) unless s00[:set]
|
5200
5524
|
end
|
5201
5525
|
|
5202
|
-
max_x = width(s0)
|
5203
|
-
max_y = height(s0)
|
5526
|
+
max_x = s00 ? width( s00[:set]) : width(s0)
|
5527
|
+
max_y = s00 ? height(s00[:set]) : height(s0)
|
5204
5528
|
mid_x = max_x / 2
|
5205
5529
|
mid_y = max_y / 2
|
5206
5530
|
|
@@ -5219,7 +5543,7 @@ module OSut
|
|
5219
5543
|
sub[:type ] = trim(sub[:type])
|
5220
5544
|
sub[:id ] = trim(sub[:id])
|
5221
5545
|
sub[:type ] = type if sub[:type].empty?
|
5222
|
-
sub[:id ] = "OSut
|
5546
|
+
sub[:id ] = "OSut:#{nom}:#{index}" if sub[:id ].empty?
|
5223
5547
|
sub[:count ] = 1 unless sub[:count ].respond_to?(:to_i)
|
5224
5548
|
sub[:multiplier] = 1 unless sub[:multiplier].respond_to?(:to_i)
|
5225
5549
|
sub[:count ] = sub[:count ].to_i
|
@@ -5271,8 +5595,10 @@ module OSut
|
|
5271
5595
|
next if key == :frame
|
5272
5596
|
next if key == :assembly
|
5273
5597
|
|
5274
|
-
|
5275
|
-
|
5598
|
+
unless value.respond_to?(:to_f)
|
5599
|
+
return mismatch(key, value, Float, mth, DBG, no)
|
5600
|
+
end
|
5601
|
+
|
5276
5602
|
next if key == :centreline
|
5277
5603
|
|
5278
5604
|
negative(key, mth, WRN) if value < 0
|
@@ -5329,27 +5655,27 @@ module OSut
|
|
5329
5655
|
# Log/reset "height" if beyond min/max.
|
5330
5656
|
if sub.key?(:height)
|
5331
5657
|
unless sub[:height].between?(glass - TOL2, max_height + TOL2)
|
5332
|
-
|
5333
|
-
sub[:height] =
|
5334
|
-
log(WRN, "
|
5658
|
+
log(WRN, "Reset '#{id}' height #{sub[:height].round(3)}m (#{mth})")
|
5659
|
+
sub[:height] = sub[:height].clamp(glass, max_height)
|
5660
|
+
log(WRN, "Height '#{id}' reset to #{sub[:height].round(3)}m (#{mth})")
|
5335
5661
|
end
|
5336
5662
|
end
|
5337
5663
|
|
5338
5664
|
# Log/reset "head" height if beyond min/max.
|
5339
5665
|
if sub.key?(:head)
|
5340
5666
|
unless sub[:head].between?(min_head - TOL2, max_head + TOL2)
|
5341
|
-
|
5342
|
-
sub[:head] =
|
5343
|
-
log(WRN, "
|
5667
|
+
log(WRN, "Reset '#{id}' head #{sub[:head].round(3)}m (#{mth})")
|
5668
|
+
sub[:head] = sub[:head].clamp(min_head, max_head)
|
5669
|
+
log(WRN, "Head '#{id}' reset to #{sub[:head].round(3)}m (#{mth})")
|
5344
5670
|
end
|
5345
5671
|
end
|
5346
5672
|
|
5347
5673
|
# Log/reset "sill" height if beyond min/max.
|
5348
5674
|
if sub.key?(:sill)
|
5349
5675
|
unless sub[:sill].between?(min_sill - TOL2, max_sill + TOL2)
|
5350
|
-
|
5351
|
-
sub[:sill] =
|
5352
|
-
log(WRN, "
|
5676
|
+
log(WRN, "Reset '#{id}' sill #{sub[:sill].round(3)}m (#{mth})")
|
5677
|
+
sub[:sill] = sub[:sill].clamp(min_sill, max_sill)
|
5678
|
+
log(WRN, "Sill '#{id}' reset to #{sub[:sill].round(3)}m (#{mth})")
|
5353
5679
|
end
|
5354
5680
|
end
|
5355
5681
|
|
@@ -5368,8 +5694,9 @@ module OSut
|
|
5368
5694
|
log(ERR, "Skip: invalid '#{id}' head/sill combo (#{mth})")
|
5369
5695
|
next
|
5370
5696
|
else
|
5697
|
+
log(WRN, "(Re)set '#{id}' sill #{sub[:sill].round(3)}m (#{mth})")
|
5371
5698
|
sub[:sill] = sill
|
5372
|
-
log(WRN, "
|
5699
|
+
log(WRN, "Sill '#{id}' (re)set to #{sub[:sill].round(3)}m (#{mth})")
|
5373
5700
|
end
|
5374
5701
|
end
|
5375
5702
|
|
@@ -5379,7 +5706,8 @@ module OSut
|
|
5379
5706
|
height = sub[:head] - sub[:sill]
|
5380
5707
|
|
5381
5708
|
if sub.key?(:height) && (sub[:height] - height).abs > TOL2
|
5382
|
-
log(WRN, "(Re)set '#{id}' height
|
5709
|
+
log(WRN, "(Re)set '#{id}' height #{sub[:height].round(3)}m (#{mth})")
|
5710
|
+
log(WRN, "Height '#{id}' (re)set to #{height.round(3)}m (#{mth})")
|
5383
5711
|
end
|
5384
5712
|
|
5385
5713
|
sub[:height] = height
|
@@ -5400,9 +5728,10 @@ module OSut
|
|
5400
5728
|
log(ERR, "Skip: invalid '#{id}' head/height combo (#{mth})")
|
5401
5729
|
next
|
5402
5730
|
else
|
5731
|
+
log(WRN, "(Re)set '#{id}' height #{sub[:height].round(3)}m (#{mth})")
|
5403
5732
|
sub[:sill ] = sill
|
5404
5733
|
sub[:height] = height
|
5405
|
-
log(WRN, "
|
5734
|
+
log(WRN, "Height '#{id}' re(set) #{sub[:height].round(3)}m (#{mth})")
|
5406
5735
|
end
|
5407
5736
|
else
|
5408
5737
|
sub[:sill] = sill
|
@@ -5428,9 +5757,10 @@ module OSut
|
|
5428
5757
|
log(ERR, "Skip: invalid '#{id}' sill/height combo (#{mth})")
|
5429
5758
|
next
|
5430
5759
|
else
|
5760
|
+
log(WRN, "(Re)set '#{id}' height #{sub[:height].round(3)}m (#{mth})")
|
5431
5761
|
sub[:head ] = head
|
5432
5762
|
sub[:height] = height
|
5433
|
-
log(WRN, "
|
5763
|
+
log(WRN, "Height '#{id}' reset to #{sub[:height].round(3)}m (#{mth})")
|
5434
5764
|
end
|
5435
5765
|
else
|
5436
5766
|
sub[:head] = head
|
@@ -5459,9 +5789,9 @@ module OSut
|
|
5459
5789
|
# Log/reset "width" if beyond min/max.
|
5460
5790
|
if sub.key?(:width)
|
5461
5791
|
unless sub[:width].between?(glass - TOL2, max_width + TOL2)
|
5462
|
-
|
5463
|
-
sub[:width] =
|
5464
|
-
log(WRN, "
|
5792
|
+
log(WRN, "Reset '#{id}' width #{sub[:width].round(3)}m (#{mth})")
|
5793
|
+
sub[:width] = sub[:width].clamp(glass, max_width)
|
5794
|
+
log(WRN, "Width '#{id}' reset to #{sub[:width].round(3)}m (#{mth})")
|
5465
5795
|
end
|
5466
5796
|
end
|
5467
5797
|
|
@@ -5471,7 +5801,7 @@ module OSut
|
|
5471
5801
|
|
5472
5802
|
if sub[:count] < 1
|
5473
5803
|
sub[:count] = 1
|
5474
|
-
log(WRN, "Reset '#{id}' count to
|
5804
|
+
log(WRN, "Reset '#{id}' count to min 1 (#{mth})")
|
5475
5805
|
end
|
5476
5806
|
else
|
5477
5807
|
sub[:count] = 1
|
@@ -5482,16 +5812,18 @@ module OSut
|
|
5482
5812
|
# Log/reset if left-sided buffer under min jamb position.
|
5483
5813
|
if sub.key?(:l_buffer)
|
5484
5814
|
if sub[:l_buffer] < min_ljamb - TOL
|
5815
|
+
log(WRN, "Reset '#{id}' left buffer #{sub[:l_buffer].round(3)}m (#{mth})")
|
5485
5816
|
sub[:l_buffer] = min_ljamb
|
5486
|
-
log(WRN, "
|
5817
|
+
log(WRN, "Left buffer '#{id}' reset to #{sub[:l_buffer].round(3)}m (#{mth})")
|
5487
5818
|
end
|
5488
5819
|
end
|
5489
5820
|
|
5490
5821
|
# Log/reset if right-sided buffer beyond max jamb position.
|
5491
5822
|
if sub.key?(:r_buffer)
|
5492
5823
|
if sub[:r_buffer] > max_rjamb - TOL
|
5824
|
+
log(WRN, "Reset '#{id}' right buffer #{sub[:r_buffer].round(3)}m (#{mth})")
|
5493
5825
|
sub[:r_buffer] = min_rjamb
|
5494
|
-
log(WRN, "
|
5826
|
+
log(WRN, "Right buffer '#{id}' reset to #{sub[:r_buffer].round(3)}m (#{mth})")
|
5495
5827
|
end
|
5496
5828
|
end
|
5497
5829
|
|
@@ -5511,15 +5843,15 @@ module OSut
|
|
5511
5843
|
sub[:multiplier] = 0
|
5512
5844
|
sub[:height ] = 0 if sub.key?(:height)
|
5513
5845
|
sub[:width ] = 0 if sub.key?(:width)
|
5514
|
-
log(ERR, "Skip:
|
5846
|
+
log(ERR, "Skip: ratio ~0 (#{mth})")
|
5515
5847
|
next
|
5516
5848
|
end
|
5517
5849
|
|
5518
5850
|
# Log/reset if "ratio" beyond min/max?
|
5519
5851
|
unless sub[:ratio].between?(min, max)
|
5520
|
-
|
5521
|
-
sub[:ratio] =
|
5522
|
-
log(WRN, "
|
5852
|
+
log(WRN, "Reset ratio #{sub[:ratio].round(3)} (#{mth})")
|
5853
|
+
sub[:ratio] = sub[:ratio].clamp(min, max)
|
5854
|
+
log(WRN, "Ratio reset to #{sub[:ratio].round(3)} (#{mth})")
|
5523
5855
|
end
|
5524
5856
|
|
5525
5857
|
# Log/reset "count" unless 1.
|
@@ -5536,7 +5868,7 @@ module OSut
|
|
5536
5868
|
|
5537
5869
|
if sub.key?(:l_buffer)
|
5538
5870
|
if sub.key?(:centreline)
|
5539
|
-
log(WRN, "Skip #{id} left buffer (vs centreline) (#{mth})")
|
5871
|
+
log(WRN, "Skip '#{id}' left buffer (vs centreline) (#{mth})")
|
5540
5872
|
else
|
5541
5873
|
x0 = sub[:l_buffer] - frame
|
5542
5874
|
xf = x0 + w
|
@@ -5544,7 +5876,7 @@ module OSut
|
|
5544
5876
|
end
|
5545
5877
|
elsif sub.key?(:r_buffer)
|
5546
5878
|
if sub.key?(:centreline)
|
5547
|
-
log(WRN, "Skip #{id} right buffer (vs centreline) (#{mth})")
|
5879
|
+
log(WRN, "Skip '#{id}' right buffer (vs centreline) (#{mth})")
|
5548
5880
|
else
|
5549
5881
|
xf = max_x - sub[:r_buffer] + frame
|
5550
5882
|
x0 = xf - w
|
@@ -5559,13 +5891,14 @@ module OSut
|
|
5559
5891
|
sub[:multiplier] = 0
|
5560
5892
|
sub[:height ] = 0 if sub.key?(:height)
|
5561
5893
|
sub[:width ] = 0 if sub.key?(:width)
|
5562
|
-
log(ERR, "Skip: invalid (ratio) width/centreline (#{mth})")
|
5894
|
+
log(ERR, "Skip '#{id}': invalid (ratio) width/centreline (#{mth})")
|
5563
5895
|
next
|
5564
5896
|
end
|
5565
5897
|
|
5566
5898
|
if sub.key?(:width) && (sub[:width] - width).abs > TOL
|
5899
|
+
log(WRN, "Reset '#{id}' width (ratio) #{sub[:width].round(2)}m (#{mth})")
|
5567
5900
|
sub[:width] = width
|
5568
|
-
log(WRN, "
|
5901
|
+
log(WRN, "Width (ratio) '#{id}' reset to #{sub[:width].round(2)}m (#{mth})")
|
5569
5902
|
end
|
5570
5903
|
|
5571
5904
|
sub[:width] = width unless sub.key?(:width)
|
@@ -5583,12 +5916,13 @@ module OSut
|
|
5583
5916
|
width = sub[:width] + frames
|
5584
5917
|
gap = (max_x - n * width) / (n + 1)
|
5585
5918
|
gap = sub[:offset] - width if sub.key?(:offset)
|
5586
|
-
gap = 0 if gap <
|
5919
|
+
gap = 0 if gap < buffer
|
5587
5920
|
offset = gap + width
|
5588
5921
|
|
5589
5922
|
if sub.key?(:offset) && (offset - sub[:offset]).abs > TOL
|
5923
|
+
log(WRN, "Reset '#{id}' sub offset #{sub[:offset].round(2)}m (#{mth})")
|
5590
5924
|
sub[:offset] = offset
|
5591
|
-
log(WRN, "
|
5925
|
+
log(WRN, "Sub offset (#{id}) reset to #{sub[:offset].round(2)}m (#{mth})")
|
5592
5926
|
end
|
5593
5927
|
|
5594
5928
|
sub[:offset] = offset unless sub.key?(:offset)
|
@@ -5600,7 +5934,7 @@ module OSut
|
|
5600
5934
|
|
5601
5935
|
if sub.key?(:l_buffer)
|
5602
5936
|
if sub.key?(:centreline)
|
5603
|
-
log(WRN, "Skip #{id} left buffer (vs centreline) (#{mth})")
|
5937
|
+
log(WRN, "Skip '#{id}' left buffer (vs centreline) (#{mth})")
|
5604
5938
|
else
|
5605
5939
|
x0 = sub[:l_buffer] - frame
|
5606
5940
|
xf = x0 + w
|
@@ -5608,7 +5942,7 @@ module OSut
|
|
5608
5942
|
end
|
5609
5943
|
elsif sub.key?(:r_buffer)
|
5610
5944
|
if sub.key?(:centreline)
|
5611
|
-
log(WRN, "Skip #{id} right buffer (vs centreline) (#{mth})")
|
5945
|
+
log(WRN, "Skip '#{id}' right buffer (vs centreline) (#{mth})")
|
5612
5946
|
else
|
5613
5947
|
xf = max_x - sub[:r_buffer] + frame
|
5614
5948
|
x0 = xf - w
|
@@ -5617,7 +5951,7 @@ module OSut
|
|
5617
5951
|
end
|
5618
5952
|
|
5619
5953
|
# Too wide?
|
5620
|
-
if x0 <
|
5954
|
+
if x0 < buffer - TOL2 || xf > max_x - buffer - TOL2
|
5621
5955
|
sub[:ratio ] = 0 if sub.key?(:ratio)
|
5622
5956
|
sub[:count ] = 0
|
5623
5957
|
sub[:multiplier] = 0
|
@@ -5633,10 +5967,9 @@ module OSut
|
|
5633
5967
|
|
5634
5968
|
# Generate sub(s).
|
5635
5969
|
sub[:count].times do |i|
|
5636
|
-
name = "#{id}
|
5970
|
+
name = "#{id}:#{i}"
|
5637
5971
|
fr = 0
|
5638
5972
|
fr = sub[:frame].frameWidth if sub[:frame]
|
5639
|
-
|
5640
5973
|
vec = OpenStudio::Point3dVector.new
|
5641
5974
|
vec << OpenStudio::Point3d.new(pos, sub[:head], 0)
|
5642
5975
|
vec << OpenStudio::Point3d.new(pos, sub[:sill], 0)
|
@@ -5647,34 +5980,41 @@ module OSut
|
|
5647
5980
|
# Log/skip if conflict between individual sub and base surface.
|
5648
5981
|
vc = vec
|
5649
5982
|
vc = offset(vc, fr, 300) if fr > 0
|
5650
|
-
ok = fits?(vc, s)
|
5651
5983
|
|
5652
|
-
|
5653
|
-
|
5984
|
+
unless fits?(vc, s)
|
5985
|
+
log(ERR, "Skip '#{name}': won't fit in '#{nom}' (#{mth})")
|
5986
|
+
break
|
5987
|
+
end
|
5654
5988
|
|
5655
5989
|
# Log/skip if conflicts with existing subs (even if same array).
|
5990
|
+
conflict = false
|
5991
|
+
|
5656
5992
|
s.subSurfaces.each do |sb|
|
5657
5993
|
nome = sb.nameString
|
5658
5994
|
fd = sb.windowPropertyFrameAndDivider
|
5659
|
-
fr = 0
|
5660
|
-
fr = fd.get.frameWidth unless fd.empty?
|
5995
|
+
fr = fd.empty? ? 0 : fd.get.frameWidth
|
5661
5996
|
vk = sb.vertices
|
5662
5997
|
vk = offset(vk, fr, 300) if fr > 0
|
5663
|
-
|
5664
|
-
|
5665
|
-
|
5666
|
-
|
5998
|
+
|
5999
|
+
if overlaps?(vc, vk)
|
6000
|
+
log(ERR, "Skip '#{name}': overlaps '#{nome}' (#{mth})")
|
6001
|
+
conflict = true
|
6002
|
+
break
|
6003
|
+
end
|
5667
6004
|
end
|
5668
6005
|
|
5669
|
-
break
|
6006
|
+
break if conflict
|
5670
6007
|
|
5671
6008
|
sb = OpenStudio::Model::SubSurface.new(vec, mdl)
|
5672
6009
|
sb.setName(name)
|
5673
6010
|
sb.setSubSurfaceType(sub[:type])
|
5674
|
-
sb.setConstruction(sub[:assembly])
|
5675
|
-
|
5676
|
-
|
5677
|
-
|
6011
|
+
sb.setConstruction(sub[:assembly]) if sub[:assembly]
|
6012
|
+
sb.setMultiplier(sub[:multiplier]) if sub[:multiplier] > 1
|
6013
|
+
|
6014
|
+
if sub[:frame] && sb.allowWindowPropertyFrameAndDivider
|
6015
|
+
sb.setWindowPropertyFrameAndDivider(sub[:frame])
|
6016
|
+
end
|
6017
|
+
|
5678
6018
|
sb.setSurface(s)
|
5679
6019
|
|
5680
6020
|
# Reset "pos" if array.
|
@@ -5685,32 +6025,15 @@ module OSut
|
|
5685
6025
|
true
|
5686
6026
|
end
|
5687
6027
|
|
5688
|
-
##
|
5689
|
-
# Validates whether surface is considered a sloped roof (outdoor-facing,
|
5690
|
-
# 10% < tilt < 90%).
|
5691
|
-
#
|
5692
|
-
# @param s [OpenStudio::Model::Surface] a model surface
|
5693
|
-
#
|
5694
|
-
# @return [Bool] whether surface is a sloped roof
|
5695
|
-
# @return [false] if invalid input (see logs)
|
5696
|
-
def slopedRoof?(s = nil)
|
5697
|
-
mth = "OSut::#{__callee__}"
|
5698
|
-
cl = OpenStudio::Model::Surface
|
5699
|
-
return mismatch("surface", s, cl, mth, DBG, false) unless s.is_a?(cl)
|
5700
|
-
|
5701
|
-
return false if facingUp?(s)
|
5702
|
-
return false if facingDown?(s)
|
5703
|
-
|
5704
|
-
true
|
5705
|
-
end
|
5706
|
-
|
5707
6028
|
##
|
5708
6029
|
# Returns the "gross roof area" above selected conditioned, occupied spaces.
|
5709
6030
|
# This includes all roof surfaces of indirectly-conditioned, unoccupied spaces
|
5710
6031
|
# like plenums (if located above any of the selected spaces). This also
|
5711
6032
|
# includes roof surfaces of unconditioned or unenclosed spaces like attics, if
|
5712
6033
|
# vertically-overlapping any ceiling of occupied spaces below; attic roof
|
5713
|
-
# sections above uninsulated soffits are excluded, for instance.
|
6034
|
+
# sections above uninsulated soffits are excluded, for instance. It does not
|
6035
|
+
# include surfaces labelled as 'RoofCeiling', which do not comply with ASHRAE
|
6036
|
+
# 90.1 or NECB tilt criteria - see 'roof?'.
|
5714
6037
|
def grossRoofArea(spaces = [])
|
5715
6038
|
mth = "OSut::#{__callee__}"
|
5716
6039
|
up = OpenStudio::Point3d.new(0,0,1) - OpenStudio::Point3d.new(0,0,0)
|
@@ -5721,6 +6044,7 @@ module OSut
|
|
5721
6044
|
spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
|
5722
6045
|
spaces = spaces.select { |space| space.is_a?(OpenStudio::Model::Space) }
|
5723
6046
|
spaces = spaces.select { |space| space.partofTotalFloorArea }
|
6047
|
+
spaces = spaces.reject { |space| unconditioned?(space) }
|
5724
6048
|
return invalid("spaces", mth, 1, DBG, 0) if spaces.empty?
|
5725
6049
|
|
5726
6050
|
# The method is very similar to OpenStudio-Standards' :
|
@@ -5732,17 +6056,18 @@ module OSut
|
|
5732
6056
|
#
|
5733
6057
|
# ... yet differs with regards to attics with overhangs/soffits.
|
5734
6058
|
|
5735
|
-
# Start with roof surfaces of occupied spaces.
|
6059
|
+
# Start with roof surfaces of occupied, conditioned spaces.
|
5736
6060
|
spaces.each do |space|
|
5737
6061
|
facets(space, "Outdoors", "RoofCeiling").each do |roof|
|
5738
6062
|
next if rfs.key?(roof)
|
6063
|
+
next unless roof?(roof)
|
5739
6064
|
|
5740
6065
|
rfs[roof] = {m2: roof.grossArea, m: space.multiplier}
|
5741
6066
|
end
|
5742
6067
|
end
|
5743
6068
|
|
5744
6069
|
# Roof surfaces of unoccupied, conditioned spaces above (e.g. plenums)?
|
5745
|
-
#
|
6070
|
+
# @todo: recursive call for stacked spaces as atria (via AirBoundaries).
|
5746
6071
|
spaces.each do |space|
|
5747
6072
|
facets(space, "Surface", "RoofCeiling").each do |ceiling|
|
5748
6073
|
floor = ceiling.adjacentSurface
|
@@ -5757,6 +6082,7 @@ module OSut
|
|
5757
6082
|
|
5758
6083
|
facets(other, "Outdoors", "RoofCeiling").each do |roof|
|
5759
6084
|
next if rfs.key?(roof)
|
6085
|
+
next unless roof?(roof)
|
5760
6086
|
|
5761
6087
|
rfs[roof] = {m2: roof.grossArea, m: other.multiplier}
|
5762
6088
|
end
|
@@ -5764,7 +6090,7 @@ module OSut
|
|
5764
6090
|
end
|
5765
6091
|
|
5766
6092
|
# Roof surfaces of unoccupied, unconditioned spaces above (e.g. attics)?
|
5767
|
-
#
|
6093
|
+
# @todo: recursive call for stacked spaces as atria (via AirBoundaries).
|
5768
6094
|
spaces.each do |space|
|
5769
6095
|
# When taking overlaps into account, the target space may not share the
|
5770
6096
|
# same local transformation as the space(s) above.
|
@@ -5792,6 +6118,8 @@ module OSut
|
|
5792
6118
|
ti = ti[:t]
|
5793
6119
|
|
5794
6120
|
facets(other, "Outdoors", "RoofCeiling").each do |roof|
|
6121
|
+
next unless roof?(roof)
|
6122
|
+
|
5795
6123
|
rvi = ti * roof.vertices
|
5796
6124
|
cst = cast(cv0, rvi, up)
|
5797
6125
|
next if cst.empty?
|
@@ -5799,8 +6127,7 @@ module OSut
|
|
5799
6127
|
# The overlap calculation fails for roof and ceiling surfaces with
|
5800
6128
|
# previously-added leader lines.
|
5801
6129
|
#
|
5802
|
-
#
|
5803
|
-
olap = nil
|
6130
|
+
# @todo: revise approach for attics ONCE skylight wells have been added.
|
5804
6131
|
olap = overlap(cst, rvi, false)
|
5805
6132
|
next if olap.empty?
|
5806
6133
|
|
@@ -5823,30 +6150,31 @@ module OSut
|
|
5823
6150
|
end
|
5824
6151
|
|
5825
6152
|
##
|
5826
|
-
# Identifies horizontal ridges
|
5827
|
-
#
|
6153
|
+
# Identifies horizontal ridges along 2x sloped (roof?) surfaces (same space).
|
6154
|
+
# The concept of 'sloped' is harmonized with OpenStudio's "alignZPrime". If
|
6155
|
+
# successful, the returned Array holds 'ridge' Hashes. Each Hash holds: an
|
5828
6156
|
# :edge (OpenStudio::Point3dVector), the edge :length (Numeric), and :roofs
|
5829
|
-
# (Array of 2x linked
|
5830
|
-
#
|
6157
|
+
# (Array of 2x linked surfaces). Each surface may be linked to more than one
|
6158
|
+
# horizontal ridge.
|
5831
6159
|
#
|
5832
|
-
# @param
|
6160
|
+
# @param rfs [Array<OpenStudio::Model::Surface>] target surfaces
|
5833
6161
|
#
|
5834
6162
|
# @return [Array] horizontal ridges (see logs if empty)
|
5835
|
-
def
|
6163
|
+
def horizontalRidges(rfs = [])
|
5836
6164
|
mth = "OSut::#{__callee__}"
|
5837
6165
|
ridges = []
|
5838
|
-
return ridges unless
|
6166
|
+
return ridges unless rfs.is_a?(Array)
|
5839
6167
|
|
5840
|
-
|
5841
|
-
|
6168
|
+
rfs = rfs.select { |s| s.is_a?(OpenStudio::Model::Surface) }
|
6169
|
+
rfs = rfs.select { |s| sloped?(s) }
|
5842
6170
|
|
5843
|
-
|
5844
|
-
maxZ =
|
5845
|
-
next if
|
6171
|
+
rfs.each do |rf|
|
6172
|
+
maxZ = rf.vertices.max_by(&:z).z
|
6173
|
+
next if rf.space.empty?
|
5846
6174
|
|
5847
|
-
space =
|
6175
|
+
space = rf.space.get
|
5848
6176
|
|
5849
|
-
|
6177
|
+
segments(rf).each do |edge|
|
5850
6178
|
next unless xyz?(edge, :z, maxZ)
|
5851
6179
|
|
5852
6180
|
# Skip if already tracked.
|
@@ -5861,19 +6189,19 @@ module OSut
|
|
5861
6189
|
|
5862
6190
|
next if match
|
5863
6191
|
|
5864
|
-
ridge = { edge: edge, length: (edge[1] - edge[0]).length, roofs: [
|
6192
|
+
ridge = { edge: edge, length: (edge[1] - edge[0]).length, roofs: [rf] }
|
5865
6193
|
|
5866
6194
|
# Links another roof (same space)?
|
5867
6195
|
match = false
|
5868
6196
|
|
5869
|
-
|
6197
|
+
rfs.each do |ruf|
|
5870
6198
|
break if match
|
5871
|
-
next if ruf ==
|
6199
|
+
next if ruf == rf
|
5872
6200
|
next if ruf.space.empty?
|
5873
6201
|
next unless ruf.space.get == space
|
5874
6202
|
|
5875
|
-
|
5876
|
-
break
|
6203
|
+
segments(ruf).each do |edg|
|
6204
|
+
break if match
|
5877
6205
|
next unless same?(edge, edg) || same?(edge, edg.reverse)
|
5878
6206
|
|
5879
6207
|
ridge[:roofs] << ruf
|
@@ -5887,49 +6215,180 @@ module OSut
|
|
5887
6215
|
ridges
|
5888
6216
|
end
|
5889
6217
|
|
6218
|
+
##
|
6219
|
+
# Preselects ideal spaces to toplight, based on 'addSkylights' options and key
|
6220
|
+
# building model geometry attributes. Can be called from within 'addSkylights'
|
6221
|
+
# by setting :ration (opts key:value argument) to 'true' ('false' by default).
|
6222
|
+
# Alternatively, the method can be called prior to 'addSkylights'. The set of
|
6223
|
+
# filters stems from previous rounds of 'addSkylights' stress testing. It is
|
6224
|
+
# intended as an option to prune away less ideal candidate spaces (irregular,
|
6225
|
+
# smaller) in favour of (larger) candidates (notably with more suitable
|
6226
|
+
# roof geometries). This is key when dealing with attic and plenums, where
|
6227
|
+
# 'addSkylights' seeks to add skylight wells (relying on roof cut-outs and
|
6228
|
+
# leader lines). Another check/outcome is whether to prioritize skylight
|
6229
|
+
# allocation in already sidelit spaces - opts[:sidelit] may be reset to 'true'.
|
6230
|
+
#
|
6231
|
+
# @param spaces [Array<OpenStudio::Model::Space>] candidate(s) to toplight
|
6232
|
+
# @param [Hash] opts requested skylight attributes (same as 'addSkylights')
|
6233
|
+
# @option opts [#to_f] :size (1.22m) template skylight width/depth (min 0.4m)
|
6234
|
+
#
|
6235
|
+
# @return [Array<OpenStudio::Model::Space>] candidates (see logs if empty)
|
6236
|
+
def toToplit(spaces = [], opts = {})
|
6237
|
+
mth = "OSut::#{__callee__}"
|
6238
|
+
gap4 = 0.4 # minimum skylight 16" width/depth (excluding frame width)
|
6239
|
+
w = 1.22 # default 48" x 48" skylight base
|
6240
|
+
w2 = w * w
|
6241
|
+
|
6242
|
+
# Validate skylight size, if provided.
|
6243
|
+
if opts.key?(:size)
|
6244
|
+
if opts[:size].respond_to?(:to_f)
|
6245
|
+
w = opts[:size].to_f
|
6246
|
+
w2 = w * w
|
6247
|
+
return invalid("size", mth, 0, ERR, []) if w.round(2) < gap4
|
6248
|
+
else
|
6249
|
+
return mismatch("size", opts[:size], Numeric, mth, DBG, [])
|
6250
|
+
end
|
6251
|
+
end
|
6252
|
+
|
6253
|
+
# Accept single 'OpenStudio::Model::Space' (vs an array of spaces). Filter.
|
6254
|
+
#
|
6255
|
+
# Whether individual spaces are UNCONDITIONED (e.g. attics, unheated areas)
|
6256
|
+
# or flagged as NOT being part of the total floor area (e.g. unoccupied
|
6257
|
+
# plenums), should of course reflect actual design intentions. It's up to
|
6258
|
+
# modellers to correctly flag such cases - can't safely guess in lieu of
|
6259
|
+
# design/modelling team.
|
6260
|
+
#
|
6261
|
+
# A friendly reminder: 'addSkylights' should be called separately for
|
6262
|
+
# strictly SEMIHEATED spaces vs REGRIGERATED spaces vs all other CONDITIONED
|
6263
|
+
# spaces, as per 90.1 and NECB requirements.
|
6264
|
+
if spaces.respond_to?(:spaceType) || spaces.respond_to?(:to_a)
|
6265
|
+
spaces = spaces.respond_to?(:to_a) ? spaces.to_a : [spaces]
|
6266
|
+
spaces = spaces.select { |sp| sp.respond_to?(:spaceType) }
|
6267
|
+
spaces = spaces.select { |sp| sp.partofTotalFloorArea }
|
6268
|
+
spaces = spaces.reject { |sp| unconditioned?(sp) }
|
6269
|
+
spaces = spaces.reject { |sp| vestibule?(sp) }
|
6270
|
+
spaces = spaces.reject { |sp| roofs(sp).empty? }
|
6271
|
+
spaces = spaces.reject { |sp| sp.floorArea < 4 * w2 }
|
6272
|
+
spaces = spaces.sort_by(&:floorArea).reverse
|
6273
|
+
return empty("spaces", mth, WRN, []) if spaces.empty?
|
6274
|
+
else
|
6275
|
+
return mismatch("spaces", spaces, Array, mth, DBG, [])
|
6276
|
+
end
|
6277
|
+
|
6278
|
+
# Unfenestrated spaces have no windows, glazed doors or skylights. By
|
6279
|
+
# default, 'addSkylights' will prioritize unfenestrated spaces (over all
|
6280
|
+
# existing sidelit ones) and maximize skylight sizes towards achieving the
|
6281
|
+
# required skylight area target. This concentrates skylights for instance in
|
6282
|
+
# typical (large) core spaces, vs (narrower) perimeter spaces. However, for
|
6283
|
+
# less conventional spatial layouts, this default approach can produce less
|
6284
|
+
# optimal skylight distributions. A balance is needed to prioritize large
|
6285
|
+
# unfenestrated spaces when appropriate on one hand, while excluding smaller
|
6286
|
+
# unfenestrated ones on the other. Here, exclusion is based on the average
|
6287
|
+
# floor area of spaces to toplight.
|
6288
|
+
fm2 = spaces.sum(&:floorArea)
|
6289
|
+
afm2 = fm2 / spaces.size
|
6290
|
+
|
6291
|
+
unfen = spaces.reject { |sp| daylit?(sp) }.sort_by(&:floorArea).reverse
|
6292
|
+
|
6293
|
+
# Target larger unfenestrated spaces, if sufficient in area.
|
6294
|
+
if unfen.empty?
|
6295
|
+
opts[:sidelit] = true
|
6296
|
+
else
|
6297
|
+
if spaces.size > unfen.size
|
6298
|
+
ufm2 = unfen.sum(&:floorArea)
|
6299
|
+
u0fm2 = unfen.first.floorArea
|
6300
|
+
|
6301
|
+
if ufm2 > 0.33 * fm2 && u0fm2 > 3 * afm2
|
6302
|
+
unfen = unfen.reject { |sp| sp.floorArea > 0.25 * afm2 }
|
6303
|
+
spaces = spaces.reject { |sp| unfen.include?(sp) }
|
6304
|
+
else
|
6305
|
+
opts[:sidelit] = true
|
6306
|
+
end
|
6307
|
+
end
|
6308
|
+
end
|
6309
|
+
|
6310
|
+
espaces = {}
|
6311
|
+
rooms = []
|
6312
|
+
toits = []
|
6313
|
+
|
6314
|
+
# Gather roof surfaces - possibly those of attics or plenums above.
|
6315
|
+
spaces.each do |sp|
|
6316
|
+
roofs(sp).each do |rf|
|
6317
|
+
espaces[sp] = {roofs: []} unless espaces.key?(sp)
|
6318
|
+
espaces[sp][:roofs] << rf unless espaces[sp][:roofs].include?(rf)
|
6319
|
+
end
|
6320
|
+
end
|
6321
|
+
|
6322
|
+
# Priortize larger spaces.
|
6323
|
+
espaces = espaces.sort_by { |espace, _| espace.floorArea }.reverse
|
6324
|
+
|
6325
|
+
# Prioritize larger roof surfaces.
|
6326
|
+
espaces.each do |_, datum|
|
6327
|
+
datum[:roofs] = datum[:roofs].sort_by(&:grossArea).reverse
|
6328
|
+
end
|
6329
|
+
|
6330
|
+
# Single out largest roof in largest space, key when dealing with shared
|
6331
|
+
# attics or plenum roofs.
|
6332
|
+
espaces.each do |espace, datum|
|
6333
|
+
rfs = datum[:roofs].reject { |ruf| toits.include?(ruf) }
|
6334
|
+
next if rfs.empty?
|
6335
|
+
|
6336
|
+
toits << rfs.sort { |ruf| ruf.grossArea }.reverse.first
|
6337
|
+
rooms << espace
|
6338
|
+
end
|
6339
|
+
|
6340
|
+
log(INF, "No ideal toplit candidates (#{mth})") if rooms.empty?
|
6341
|
+
|
6342
|
+
rooms
|
6343
|
+
end
|
6344
|
+
|
5890
6345
|
##
|
5891
6346
|
# Adds skylights to toplight selected OpenStudio (occupied, conditioned)
|
5892
|
-
# spaces, based on requested skylight-to-roof (SRR%)
|
5893
|
-
# user selects
|
5894
|
-
#
|
5895
|
-
# selected spaces, and exits while
|
5896
|
-
#
|
5897
|
-
#
|
5898
|
-
#
|
5899
|
-
#
|
5900
|
-
#
|
5901
|
-
#
|
6347
|
+
# spaces, based on requested skylight area, or a skylight-to-roof ratio (SRR%).
|
6348
|
+
# If the user selects 0m2 as the requested :area (or 0 as the requested :srr),
|
6349
|
+
# while setting the option :clear as true, the method simply purges all
|
6350
|
+
# pre-existing roof fenestrated subsurfaces of selected spaces, and exits while
|
6351
|
+
# returning 0 (without logging an error or warning). Pre-existing skylight
|
6352
|
+
# wells are not cleared however. Pre-toplit spaces are otherwise ignored.
|
6353
|
+
# Boolean options :attic, :plenum, :sloped and :sidelit further restrict
|
6354
|
+
# candidate spaces to toplight. If applicable, options :attic and :plenum add
|
6355
|
+
# skylight wells. Option :patterns restricts preset skylight allocation
|
6356
|
+
# layouts in order of preference; if left empty, all preset patterns are
|
6357
|
+
# considered, also in order of preference (see examples).
|
5902
6358
|
#
|
5903
6359
|
# @param spaces [Array<OpenStudio::Model::Space>] space(s) to toplight
|
5904
6360
|
# @param [Hash] opts requested skylight attributes
|
5905
|
-
# @option opts [#to_f] :
|
6361
|
+
# @option opts [#to_f] :area overall skylight area
|
6362
|
+
# @option opts [#to_f] :srr skylight-to-roof ratio (0.00, 0.90]
|
5906
6363
|
# @option opts [#to_f] :size (1.22) template skylight width/depth (min 0.4m)
|
5907
6364
|
# @option opts [#frameWidth] :frame (nil) OpenStudio Frame & Divider (optional)
|
5908
6365
|
# @option opts [Bool] :clear (true) whether to first purge existing skylights
|
6366
|
+
# @option opts [Bool] :ration (true) finer selection of candidates to toplight
|
5909
6367
|
# @option opts [Bool] :sidelit (true) whether to consider sidelit spaces
|
5910
6368
|
# @option opts [Bool] :sloped (true) whether to consider sloped roof surfaces
|
5911
6369
|
# @option opts [Bool] :plenum (true) whether to consider plenum wells
|
5912
6370
|
# @option opts [Bool] :attic (true) whether to consider attic wells
|
5913
6371
|
# @option opts [Array<#to_s>] :patterns requested skylight allocation (3x)
|
5914
|
-
# @example (a) consider 2D array of individual skylights, e.g. n(1.
|
6372
|
+
# @example (a) consider 2D array of individual skylights, e.g. n(1.22m x 1.22m)
|
5915
6373
|
# opts[:patterns] = ["array"]
|
5916
6374
|
# @example (b) consider 'a', then array of 1x(size) x n(size) skylight strips
|
5917
6375
|
# opts[:patterns] = ["array", "strips"]
|
5918
6376
|
#
|
5919
|
-
# @return [Float] returns gross roof area if successful (see logs if
|
6377
|
+
# @return [Float] returns gross roof area if successful (see logs if 0m2)
|
5920
6378
|
def addSkyLights(spaces = [], opts = {})
|
5921
6379
|
mth = "OSut::#{__callee__}"
|
5922
6380
|
clear = true
|
5923
|
-
srr =
|
6381
|
+
srr = nil
|
6382
|
+
area = nil
|
5924
6383
|
frame = nil # FrameAndDivider object
|
5925
6384
|
f = 0.0 # FrameAndDivider frame width
|
5926
|
-
gap = 0.1 # min 2" around well (2x), as well as max frame width
|
6385
|
+
gap = 0.1 # min 2" around well (2x == 4"), as well as max frame width
|
5927
6386
|
gap2 = 0.2 # 2x gap
|
5928
6387
|
gap4 = 0.4 # minimum skylight 16" width/depth (excluding frame width)
|
5929
6388
|
bfr = 0.005 # minimum array perimeter buffer (no wells)
|
5930
6389
|
w = 1.22 # default 48" x 48" skylight base
|
5931
6390
|
w2 = w * w # m2
|
5932
|
-
|
6391
|
+
v = OpenStudio.openStudioVersion.split(".").join.to_i
|
5933
6392
|
|
5934
6393
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5935
6394
|
# Excerpts of ASHRAE 90.1 2022 definitions:
|
@@ -5994,7 +6453,7 @@ module OSut
|
|
5994
6453
|
# to exclude portions of any roof surface: all plenum roof surfaces (in
|
5995
6454
|
# addition to soffit surfaces) would need to be insulated). The method takes
|
5996
6455
|
# such circumstances into account, which requires vertically casting of
|
5997
|
-
# surfaces
|
6456
|
+
# surfaces onto others, as well as overlap calculations. If successful, the
|
5998
6457
|
# method returns the "GROSS ROOF AREA" (in m2), based on the above rationale.
|
5999
6458
|
#
|
6000
6459
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
@@ -6014,7 +6473,7 @@ module OSut
|
|
6014
6473
|
# instance, if the GROSS ROOF AREA were based on insulated ceiling surfaces,
|
6015
6474
|
# there would be a topological disconnect between flat ceiling and sloped
|
6016
6475
|
# skylights above. Should NECB users first 'project' (sloped) skylight rough
|
6017
|
-
# openings onto flat ceilings when calculating
|
6476
|
+
# openings onto flat ceilings when calculating SRR%? Without much needed
|
6018
6477
|
# clarification, the (clearer) 90.1 rules equally apply here to NECB cases.
|
6019
6478
|
|
6020
6479
|
# If skylight wells are indeed required, well wall edges are always vertical
|
@@ -6037,19 +6496,8 @@ module OSut
|
|
6037
6496
|
|
6038
6497
|
mdl = spaces.first.model
|
6039
6498
|
|
6040
|
-
# Exit if mismatched or invalid
|
6499
|
+
# Exit if mismatched or invalid options.
|
6041
6500
|
return mismatch("opts", opts, Hash, mth, DBG, 0) unless opts.is_a?(Hash)
|
6042
|
-
return hashkey( "srr", opts, :srr, mth, ERR, 0) unless opts.key?(:srr)
|
6043
|
-
|
6044
|
-
# Validate requested skylight-to-roof ratio.
|
6045
|
-
if opts[:srr].respond_to?(:to_f)
|
6046
|
-
srr = opts[:srr].to_f
|
6047
|
-
log(WRN, "Resetting srr to 0% (#{mth})") if srr < 0
|
6048
|
-
log(WRN, "Resetting srr to 10% (#{mth})") if srr > 0.10
|
6049
|
-
srr = srr.clamp(0.00, 0.10)
|
6050
|
-
else
|
6051
|
-
return mismatch("srr", opts[:srr], Numeric, mth, DBG, 0)
|
6052
|
-
end
|
6053
6501
|
|
6054
6502
|
# Validate Frame & Divider object, if provided.
|
6055
6503
|
if opts.key?(:frame)
|
@@ -6057,10 +6505,10 @@ module OSut
|
|
6057
6505
|
|
6058
6506
|
if frame.respond_to?(:frameWidth)
|
6059
6507
|
frame = nil if v < 321
|
6060
|
-
frame = nil if
|
6061
|
-
frame = nil if
|
6508
|
+
frame = nil if frame.frameWidth.round(2) < 0
|
6509
|
+
frame = nil if frame.frameWidth.round(2) > gap
|
6062
6510
|
|
6063
|
-
f =
|
6511
|
+
f = frame.frameWidth if frame
|
6064
6512
|
log(WRN, "Skip Frame&Divider (#{mth})") unless frame
|
6065
6513
|
else
|
6066
6514
|
frame = nil
|
@@ -6085,6 +6533,27 @@ module OSut
|
|
6085
6533
|
wl = w0 + gap
|
6086
6534
|
wl2 = wl * wl
|
6087
6535
|
|
6536
|
+
# Validate requested skylight-to-roof ratio (or overall area).
|
6537
|
+
if opts.key?(:area)
|
6538
|
+
if opts[:area].respond_to?(:to_f)
|
6539
|
+
area = opts[:area].to_f
|
6540
|
+
log(WRN, "Area reset to 0.0m2 (#{mth})") if area < 0
|
6541
|
+
else
|
6542
|
+
return mismatch("area", opts[:area], Numeric, mth, DBG, 0)
|
6543
|
+
end
|
6544
|
+
elsif opts.key?(:srr)
|
6545
|
+
if opts[:srr].respond_to?(:to_f)
|
6546
|
+
srr = opts[:srr].to_f
|
6547
|
+
log(WRN, "SRR (#{srr.round(2)}) reset to 0% (#{mth})") if srr < 0
|
6548
|
+
log(WRN, "SRR (#{srr.round(2)}) reset to 90% (#{mth})") if srr > 0.90
|
6549
|
+
srr = srr.clamp(0.00, 0.10)
|
6550
|
+
else
|
6551
|
+
return mismatch("srr", opts[:srr], Numeric, mth, DBG, 0)
|
6552
|
+
end
|
6553
|
+
else
|
6554
|
+
return hashkey("area", opts, :area, mth, ERR, 0)
|
6555
|
+
end
|
6556
|
+
|
6088
6557
|
# Validate purge request, if provided.
|
6089
6558
|
if opts.key?(:clear)
|
6090
6559
|
clear = opts[:clear]
|
@@ -6095,35 +6564,97 @@ module OSut
|
|
6095
6564
|
end
|
6096
6565
|
end
|
6097
6566
|
|
6098
|
-
|
6567
|
+
# Purge if requested.
|
6568
|
+
roofs(spaces).each { |s| s.subSurfaces.map(&:remove) } if clear
|
6099
6569
|
|
6100
6570
|
# Safely exit, e.g. if strictly called to purge existing roof subsurfaces.
|
6101
|
-
return 0 if
|
6571
|
+
return 0 if area && area.round(2) == 0
|
6572
|
+
return 0 if srr && srr.round(2) == 0
|
6573
|
+
|
6574
|
+
m2 = 0 # total existing skylight rough opening area
|
6575
|
+
rm2 = grossRoofArea(spaces) # excludes e.g. overhangs
|
6576
|
+
|
6577
|
+
# Tally existing skylight rough opening areas.
|
6578
|
+
spaces.each do |space|
|
6579
|
+
m = space.multiplier
|
6580
|
+
|
6581
|
+
facets(space, "Outdoors", "RoofCeiling").each do |roof|
|
6582
|
+
roof.subSurfaces.each do |sub|
|
6583
|
+
next unless fenestration?(sub)
|
6584
|
+
|
6585
|
+
id = sub.nameString
|
6586
|
+
xm2 = sub.grossArea
|
6587
|
+
|
6588
|
+
if sub.allowWindowPropertyFrameAndDivider
|
6589
|
+
unless sub.windowPropertyFrameAndDivider.empty?
|
6590
|
+
fw = sub.windowPropertyFrameAndDivider.get.frameWidth
|
6591
|
+
vec = offset(sub.vertices, fw, 300)
|
6592
|
+
aire = OpenStudio.getArea(vec)
|
6593
|
+
|
6594
|
+
if aire.empty?
|
6595
|
+
log(ERR, "Skipping '#{id}': invalid Frame&Divider (#{mth})")
|
6596
|
+
else
|
6597
|
+
xm2 = aire.get
|
6598
|
+
end
|
6599
|
+
end
|
6600
|
+
end
|
6601
|
+
|
6602
|
+
m2 += xm2 * sub.multiplier * m
|
6603
|
+
end
|
6604
|
+
end
|
6605
|
+
end
|
6606
|
+
|
6607
|
+
# Required skylight area to add.
|
6608
|
+
sm2 = area ? area : rm2 * srr - m2
|
6609
|
+
|
6610
|
+
# Warn/skip if existing skylights exceed or ~roughly match targets.
|
6611
|
+
if sm2.round(2) < w02.round(2)
|
6612
|
+
if m2 > 0
|
6613
|
+
log(INF, "Skipping: existing skylight area > request (#{mth})")
|
6614
|
+
return rm2
|
6615
|
+
else
|
6616
|
+
log(INF, "Requested skylight area < min size (#{mth})")
|
6617
|
+
end
|
6618
|
+
elsif 0.9 * rm2.round(2) < sm2.round(2)
|
6619
|
+
log(INF, "Skipping: requested skylight area > 90% of GRA (#{mth})")
|
6620
|
+
return rm2
|
6621
|
+
end
|
6622
|
+
|
6623
|
+
opts[:ration] = true unless opts.key?(:ration)
|
6624
|
+
|
6625
|
+
# By default, seek ideal candidate spaces/roofs. Bail out if unsuccessful.
|
6626
|
+
unless opts[:ration] == false
|
6627
|
+
spaces = toToplit(spaces, opts)
|
6628
|
+
return rm2 if spaces.empty?
|
6629
|
+
end
|
6102
6630
|
|
6103
6631
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
6104
6632
|
# The method seeks to insert a skylight array within the largest rectangular
|
6105
6633
|
# 'bounded box' that neatly 'fits' within a given roof surface. This equally
|
6106
6634
|
# applies to any vertically-cast overlap between roof and plenum (or attic)
|
6107
6635
|
# floor, which in turn generates skylight wells. Skylight arrays are
|
6108
|
-
# inserted from left
|
6109
|
-
# (or cast 3D overlap) is 'aligned' in 2D
|
6636
|
+
# inserted from left-to-right & top-to-bottom (as illustrated below), once a
|
6637
|
+
# roof (or cast 3D overlap) is 'aligned' in 2D.
|
6110
6638
|
#
|
6111
6639
|
# Depending on geometric complexity (e.g. building/roof concavity,
|
6112
6640
|
# triangulation), the total area of bounded boxes may be significantly less
|
6113
6641
|
# than the calculated "GROSS ROOF AREA", which can make it challenging to
|
6114
|
-
# attain the
|
6115
|
-
# select
|
6116
|
-
#
|
6642
|
+
# attain the requested skylight area. If :patterns are left unaltered, the
|
6643
|
+
# method will select those that maximize the likelihood of attaining the
|
6644
|
+
# requested target, to the detriment of spatial daylighting distribution.
|
6117
6645
|
#
|
6118
|
-
# The default skylight module size is 1.
|
6119
|
-
# overridden by the user, e.g. 2.
|
6646
|
+
# The default skylight module size is 1.22m x 1.22m (4' x 4'), which can be
|
6647
|
+
# overridden by the user, e.g. 2.44m x 2.44m (8' x 8'). However, skylight
|
6648
|
+
# sizes usually end up either contracted or inflated to exactly meet a
|
6649
|
+
# request skylight area or SRR%,
|
6120
6650
|
#
|
6121
6651
|
# Preset skylight allocation patterns (in order of precedence):
|
6652
|
+
#
|
6122
6653
|
# 1. "array"
|
6123
6654
|
# _____________________
|
6124
6655
|
# | _ _ _ | - ?x columns ("cols") >= ?x rows (min 2x2)
|
6125
|
-
# | |_| |_| |_| | - SRR ~5% (1.
|
6126
|
-
# | | - SRR ~19% (2.
|
6656
|
+
# | |_| |_| |_| | - SRR ~5% (1.22m x 1.22m), as illustrated
|
6657
|
+
# | | - SRR ~19% (2.44m x 2.44m)
|
6127
6658
|
# | _ _ _ | - +suitable for wide spaces (storage, retail)
|
6128
6659
|
# | |_| |_| |_| | - ~1.4x height + skylight width 'ideal' rule
|
6129
6660
|
# |_____________________| - better daylight distribution, many wells
|
@@ -6132,21 +6663,21 @@ module OSut
|
|
6132
6663
|
# _____________________
|
6133
6664
|
# | _ _ _ | - ?x columns (min 2), 1x row
|
6134
6665
|
# | | | | | | | | - ~doubles %SRR ...
|
6135
|
-
# | | | | | | | | - SRR ~10% (1.
|
6136
|
-
# | | | | | | | | - SRR ~19% (2.
|
6666
|
+
# | | | | | | | | - SRR ~10% (1.22m x ?1.22m), as illustrated
|
6667
|
+
# | | | | | | | | - SRR ~19% (2.44m x ?1.22m)
|
6137
6668
|
# | |_| |_| |_| | - ~roof monitor layout
|
6138
6669
|
# |_____________________| - fewer wells
|
6139
6670
|
#
|
6140
6671
|
# 3. "strip"
|
6141
6672
|
# ____________________
|
6142
6673
|
# | | - 1x column, 1x row (min 1x)
|
6143
|
-
# | ______________ | - SRR ~11% (1.
|
6144
|
-
# | | ............ | | - SRR ~22% (2.
|
6674
|
+
# | ______________ | - SRR ~11% (1.22m x ?1.22m)
|
6675
|
+
# | | ............ | | - SRR ~22% (2.44m x ?1.22m), as illustrated
|
6145
6676
|
# | |______________| | - +suitable for elongated bounded boxes
|
6146
6677
|
# | | - 1x well
|
6147
6678
|
# |____________________|
|
6148
6679
|
#
|
6149
|
-
#
|
6680
|
+
# @todo: Support strips/strip patterns along ridge of paired roof surfaces.
|
6150
6681
|
layouts = ["array", "strips", "strip"]
|
6151
6682
|
patterns = []
|
6152
6683
|
|
@@ -6174,27 +6705,26 @@ module OSut
|
|
6174
6705
|
# - large roof surface areas (e.g. retail, classrooms ... not corridors)
|
6175
6706
|
# - not sidelit (favours core spaces)
|
6176
6707
|
# - having flat roofs (avoids sloped roofs)
|
6177
|
-
# -
|
6708
|
+
# - neither under plenums, nor attics (avoids wells)
|
6178
6709
|
#
|
6179
6710
|
# This ideal (albeit stringent) set of conditions is "combo a".
|
6180
6711
|
#
|
6181
|
-
# If
|
6182
|
-
# selection criteria and
|
6712
|
+
# If the requested skylight area has not yet been achieved (after initially
|
6713
|
+
# applying "combo a"), the method decrementally drops selection criteria and
|
6714
|
+
# starts over, e.g.:
|
6183
6715
|
# - then considers sidelit spaces
|
6184
6716
|
# - then considers sloped roofs
|
6185
6717
|
# - then considers skylight wells
|
6186
6718
|
#
|
6187
6719
|
# A maximum number of skylights are allocated to roof surfaces matching a
|
6188
|
-
# given combo
|
6189
|
-
#
|
6190
|
-
# then dropped (e.g. b, then c, etc.), and the allocation process is
|
6191
|
-
# relaunched. An error message is logged if the %SRR isn't ultimately met.
|
6720
|
+
# given combo, all the while giving priority to larger roof areas. An error
|
6721
|
+
# message is logged if the target isn't ultimately achieved.
|
6192
6722
|
#
|
6193
|
-
# Through filters, users may restrict candidate roof surfaces:
|
6723
|
+
# Through filters, users may in advance restrict candidate roof surfaces:
|
6194
6724
|
# b. above occupied sidelit spaces ('false' restricts to core spaces)
|
6195
6725
|
# c. that are sloped ('false' restricts to flat roofs)
|
6196
|
-
# d. above
|
6197
|
-
# e. above
|
6726
|
+
# d. above INDIRECTLY CONDITIONED spaces (e.g. plenums, uninsulated wells)
|
6727
|
+
# e. above UNCONDITIONED spaces (e.g. attics, insulated wells)
|
6198
6728
|
filters = ["a", "b", "bc", "bcd", "bcde"]
|
6199
6729
|
|
6200
6730
|
# Prune filters, based on user-selected options.
|
@@ -6203,18 +6733,18 @@ module OSut
|
|
6203
6733
|
next unless opts[opt] == false
|
6204
6734
|
|
6205
6735
|
case opt
|
6206
|
-
when :sidelit then filters.map! { |
|
6207
|
-
when :sloped then filters.map! { |
|
6208
|
-
when :plenum then filters.map! { |
|
6209
|
-
when :attic then filters.map! { |
|
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 }
|
6210
6740
|
end
|
6211
6741
|
end
|
6212
6742
|
|
6213
|
-
filters.reject! { |
|
6743
|
+
filters.reject! { |fl| fl.empty? }
|
6214
6744
|
filters.uniq!
|
6215
6745
|
|
6216
|
-
# Remaining filters may be further
|
6217
|
-
# depending on geometry, e.g.:
|
6746
|
+
# Remaining filters may be further pruned automatically after space/roof
|
6747
|
+
# processing, depending on geometry, e.g.:
|
6218
6748
|
# - if there are no sidelit spaces: filter "b" will be pruned away
|
6219
6749
|
# - if there are no sloped roofs : filter "c" will be pruned away
|
6220
6750
|
# - if no plenums are identified : filter "d" will be pruned away
|
@@ -6228,12 +6758,10 @@ module OSut
|
|
6228
6758
|
attics = {} # unoccupied UNCONDITIONED spaces above rooms
|
6229
6759
|
ceilings = {} # of occupied CONDITIONED space (if plenums/attics)
|
6230
6760
|
|
6231
|
-
#
|
6761
|
+
# Candidate 'rooms' to toplit - excludes plenums/attics.
|
6232
6762
|
spaces.each do |space|
|
6233
|
-
|
6234
|
-
next unless space.partofTotalFloorArea # occupied (not plenum)
|
6763
|
+
id = space.nameString
|
6235
6764
|
|
6236
|
-
# Already toplit?
|
6237
6765
|
if daylit?(space, false, true, false)
|
6238
6766
|
log(WRN, "#{id} is already toplit, skipping (#{mth})")
|
6239
6767
|
next
|
@@ -6241,30 +6769,40 @@ module OSut
|
|
6241
6769
|
|
6242
6770
|
# When unoccupied spaces are involved (e.g. plenums, attics), the occupied
|
6243
6771
|
# space (to toplight) may not share the same local transformation as its
|
6244
|
-
# unoccupied space(s) above. Fetching
|
6245
|
-
h = 0
|
6772
|
+
# unoccupied space(s) above. Fetching site transformation.
|
6246
6773
|
t0 = transforms(space)
|
6247
6774
|
next unless t0[:t]
|
6248
6775
|
|
6249
|
-
|
6250
|
-
|
6776
|
+
# Calculate space height.
|
6777
|
+
hMIN = 10000
|
6778
|
+
hMAX = 0
|
6779
|
+
surfs = facets(space)
|
6780
|
+
|
6781
|
+
surfs.each { |surf| hMAX = [hMAX, surf.vertices.max_by(&:z).z].max }
|
6782
|
+
surfs.each { |surf| hMIN = [hMIN, surf.vertices.min_by(&:z).z].min }
|
6783
|
+
|
6784
|
+
h = hMAX - hMIN
|
6251
6785
|
|
6252
|
-
|
6253
|
-
|
6786
|
+
unless h > 0
|
6787
|
+
log(ERR, "#{id} height? #{hMIN.round(2)} vs #{hMAX.round(2)} (#{mth})")
|
6788
|
+
next
|
6789
|
+
end
|
6254
6790
|
|
6255
6791
|
rooms[space] = {}
|
6256
|
-
rooms[space][:
|
6792
|
+
rooms[space][:t0 ] = t0[:t]
|
6257
6793
|
rooms[space][:m ] = space.multiplier
|
6258
6794
|
rooms[space][:h ] = h
|
6259
|
-
rooms[space][:roofs ] =
|
6795
|
+
rooms[space][:roofs ] = facets(space, "Outdoors", "RoofCeiling")
|
6260
6796
|
rooms[space][:sidelit] = daylit?(space, true, false, false)
|
6261
6797
|
|
6262
6798
|
# Fetch and process room-specific outdoor-facing roof surfaces, the most
|
6263
|
-
# basic 'set' to track
|
6264
|
-
# - no skylight wells
|
6799
|
+
# basic 'set' to track, e.g.:
|
6800
|
+
# - no skylight wells (i.e. no leader lines)
|
6265
6801
|
# - 1x skylight array per roof surface
|
6266
|
-
# - no need to
|
6802
|
+
# - no need to consider site transformation
|
6267
6803
|
rooms[space][:roofs].each do |roof|
|
6804
|
+
next unless roof?(roof)
|
6805
|
+
|
6268
6806
|
box = boundedBox(roof)
|
6269
6807
|
next if box.empty?
|
6270
6808
|
|
@@ -6274,133 +6812,112 @@ module OSut
|
|
6274
6812
|
bm2 = bm2.get
|
6275
6813
|
next if bm2.round(2) < w02.round(2)
|
6276
6814
|
|
6277
|
-
|
6815
|
+
width = alignedWidth(box, true)
|
6816
|
+
depth = alignedHeight(box, true)
|
6817
|
+
next if width < wl * 3
|
6818
|
+
next if depth < wl
|
6819
|
+
|
6820
|
+
# A set is 'tight' if the area of its bounded box is significantly
|
6821
|
+
# smaller than that of its roof. A set is 'thin' if the depth of its
|
6822
|
+
# bounded box is (too) narrow. If either is true, some geometry rules
|
6823
|
+
# may be relaxed to maximize allocated skylight area. Neither apply to
|
6824
|
+
# cases with skylight wells.
|
6278
6825
|
tight = bm2 < roof.grossArea / 2 ? true : false
|
6826
|
+
thin = depth.round(2) < (1.5 * wl).round(2) ? true : false
|
6279
6827
|
|
6280
6828
|
set = {}
|
6281
6829
|
set[:box ] = box
|
6282
6830
|
set[:bm2 ] = bm2
|
6283
6831
|
set[:tight ] = tight
|
6832
|
+
set[:thin ] = thin
|
6284
6833
|
set[:roof ] = roof
|
6285
6834
|
set[:space ] = space
|
6835
|
+
set[:m ] = space.multiplier
|
6286
6836
|
set[:sidelit] = rooms[space][:sidelit]
|
6287
|
-
set[:
|
6288
|
-
set[:
|
6837
|
+
set[:t0 ] = rooms[space][:t0]
|
6838
|
+
set[:t ] = OpenStudio::Transformation.alignFace(roof.vertices)
|
6289
6839
|
sets << set
|
6290
6840
|
end
|
6291
6841
|
end
|
6292
6842
|
|
6293
6843
|
# Process outdoor-facing roof surfaces of plenums and attics above.
|
6294
6844
|
rooms.each do |space, room|
|
6295
|
-
t0
|
6296
|
-
|
6297
|
-
rufs = room.key?(:roofs) ? toits - room[:roofs] : toits
|
6298
|
-
next if rufs.empty?
|
6845
|
+
t0 = room[:t0]
|
6846
|
+
rufs = roofs(space) - room[:roofs]
|
6299
6847
|
|
6300
|
-
# Process room ceilings, as 1x or more are overlapping roofs above. Fetch
|
6301
|
-
# vertically-cast overlaps.
|
6302
6848
|
rufs.each do |ruf|
|
6849
|
+
next unless roof?(ruf)
|
6850
|
+
|
6303
6851
|
espace = ruf.space
|
6304
6852
|
next if espace.empty?
|
6305
6853
|
|
6306
6854
|
espace = espace.get
|
6307
6855
|
next if espace.partofTotalFloorArea
|
6308
6856
|
|
6309
|
-
m
|
6310
|
-
ti = transforms(espace)
|
6311
|
-
next unless ti[:t]
|
6857
|
+
m = espace.multiplier
|
6312
6858
|
|
6313
|
-
|
6314
|
-
|
6315
|
-
|
6316
|
-
# Ensure BLC vertex sequence.
|
6317
|
-
if facingUp?(vtx)
|
6318
|
-
vtx = ti * vtx
|
6319
|
-
|
6320
|
-
if xyz?(vtx, :z)
|
6321
|
-
vtx = blc(vtx)
|
6322
|
-
else
|
6323
|
-
dZ = vtx.first.z
|
6324
|
-
vtz = blc(flatten(vtx)).to_a
|
6325
|
-
vtx = []
|
6326
|
-
|
6327
|
-
vtz.each { |v| vtx << OpenStudio::Point3d.new(v.x, v.y, v.z + dZ) }
|
6328
|
-
end
|
6329
|
-
|
6330
|
-
ruf.setVertices(ti.inverse * vtx)
|
6331
|
-
else
|
6332
|
-
tr = OpenStudio::Transformation.alignFace(vtx)
|
6333
|
-
vtx = blc(tr.inverse * vtx)
|
6334
|
-
ruf.setVertices(tr * vtx)
|
6859
|
+
if m != space.multiplier
|
6860
|
+
log(ERR, "Skipping #{ruf.nameString} (multiplier mismatch) (#{mth})")
|
6861
|
+
next
|
6335
6862
|
end
|
6336
6863
|
|
6337
|
-
|
6338
|
-
|
6339
|
-
facets(space, "Surface", "RoofCeiling").each do |tile|
|
6340
|
-
vtx = tile.vertices
|
6341
|
-
|
6342
|
-
# Ensure BLC vertex sequence.
|
6343
|
-
if facingUp?(vtx)
|
6344
|
-
vtx = t0 * vtx
|
6345
|
-
|
6346
|
-
if xyz?(vtx, :z)
|
6347
|
-
vtx = blc(vtx)
|
6348
|
-
else
|
6349
|
-
dZ = vtx.first.z
|
6350
|
-
vtz = blc(flatten(vtx)).to_a
|
6351
|
-
vtx = []
|
6352
|
-
|
6353
|
-
vtz.each { |v| vtx << OpenStudio::Point3d.new(v.x, v.y, v.z + dZ) }
|
6354
|
-
end
|
6355
|
-
|
6356
|
-
vtx = t0.inverse * vtx
|
6357
|
-
else
|
6358
|
-
tr = OpenStudio::Transformation.alignFace(vtx)
|
6359
|
-
vtx = blc(tr.inverse * vtx)
|
6360
|
-
vtx = tr * vtx
|
6361
|
-
end
|
6864
|
+
ti = transforms(espace)
|
6865
|
+
next unless ti[:t]
|
6362
6866
|
|
6363
|
-
|
6867
|
+
ti = ti[:t]
|
6868
|
+
rpts = ti * ruf.vertices
|
6364
6869
|
|
6365
|
-
|
6870
|
+
# Process occupied room ceilings, as 1x or more are overlapping roof
|
6871
|
+
# surfaces above. Vertically cast, then fetch overlap.
|
6872
|
+
facets(space, "Surface", "RoofCeiling").each do |tile|
|
6873
|
+
tpts = t0 * tile.vertices
|
6874
|
+
ci0 = cast(tpts, rpts, ray)
|
6366
6875
|
next if ci0.empty?
|
6367
6876
|
|
6368
|
-
olap = overlap(
|
6877
|
+
olap = overlap(rpts, ci0)
|
6369
6878
|
next if olap.empty?
|
6370
6879
|
|
6880
|
+
om2 = OpenStudio.getArea(olap)
|
6881
|
+
next if om2.empty?
|
6882
|
+
|
6883
|
+
om2 = om2.get
|
6884
|
+
next if om2.round(2) < w02.round(2)
|
6885
|
+
|
6371
6886
|
box = boundedBox(olap)
|
6372
6887
|
next if box.empty?
|
6373
6888
|
|
6374
6889
|
# Adding skylight wells (plenums/attics) is contingent to safely
|
6375
|
-
# linking new base roof 'inserts'
|
6376
|
-
#
|
6890
|
+
# linking new base roof 'inserts' (as well as new ceiling ones)
|
6891
|
+
# through 'leader lines'. This requires an offset to ensure no
|
6892
|
+
# conflicts with roof or (ceiling) tile edges.
|
6377
6893
|
#
|
6378
|
-
#
|
6894
|
+
# @todo: Expand the method to factor in cases where simple 'side'
|
6379
6895
|
# cutouts can be supported (no need for leader lines), e.g.
|
6380
6896
|
# skylight strips along roof ridges.
|
6381
6897
|
box = offset(box, -gap, 300)
|
6382
|
-
box = poly(box, false, false, false, false, :blc)
|
6383
6898
|
next if box.empty?
|
6384
6899
|
|
6385
6900
|
bm2 = OpenStudio.getArea(box)
|
6386
6901
|
next if bm2.empty?
|
6387
6902
|
|
6388
6903
|
bm2 = bm2.get
|
6389
|
-
next if bm2.round(2) <
|
6904
|
+
next if bm2.round(2) < wl2.round(2)
|
6390
6905
|
|
6391
|
-
|
6392
|
-
|
6906
|
+
width = alignedWidth(box, true)
|
6907
|
+
depth = alignedHeight(box, true)
|
6908
|
+
next if width < wl * 3
|
6909
|
+
next if depth < wl * 2
|
6910
|
+
|
6911
|
+
# Vertically cast box onto tile below.
|
6912
|
+
cbox = cast(box, tpts, ray)
|
6393
6913
|
next if cbox.empty?
|
6394
6914
|
|
6395
6915
|
cm2 = OpenStudio.getArea(cbox)
|
6396
6916
|
next if cm2.empty?
|
6397
6917
|
|
6398
|
-
cm2
|
6399
|
-
|
6400
|
-
|
6401
|
-
# or ceiling.
|
6402
|
-
tight = bm2 < ruf.grossArea / 2 ? true : false
|
6403
|
-
tight = cm2 < tile.grossArea / 2 ? true : tight
|
6918
|
+
cm2 = cm2.get
|
6919
|
+
box = ti.inverse * box
|
6920
|
+
cbox = t0.inverse * cbox
|
6404
6921
|
|
6405
6922
|
unless ceilings.key?(tile)
|
6406
6923
|
floor = tile.adjacentSurface
|
@@ -6412,10 +6929,6 @@ module OSut
|
|
6412
6929
|
|
6413
6930
|
floor = floor.get
|
6414
6931
|
|
6415
|
-
# Ensure BLC vertex sequence.
|
6416
|
-
vtx = t0 * vtx
|
6417
|
-
floor.setVertices(ti.inverse * vtx.reverse)
|
6418
|
-
|
6419
6932
|
if floor.space.empty?
|
6420
6933
|
log(ERR, "#{floor.nameString} space? (#{mth})")
|
6421
6934
|
next
|
@@ -6428,32 +6941,40 @@ module OSut
|
|
6428
6941
|
next
|
6429
6942
|
end
|
6430
6943
|
|
6431
|
-
ceilings[tile]
|
6432
|
-
ceilings[tile][:roofs] = []
|
6433
|
-
ceilings[tile][:space] = space
|
6434
|
-
ceilings[tile][:floor] = floor
|
6944
|
+
ceilings[tile] = {}
|
6945
|
+
ceilings[tile][:roofs ] = []
|
6946
|
+
ceilings[tile][:space ] = space
|
6947
|
+
ceilings[tile][:floor ] = floor
|
6435
6948
|
end
|
6436
6949
|
|
6437
6950
|
ceilings[tile][:roofs] << ruf
|
6438
6951
|
|
6439
|
-
#
|
6952
|
+
# Skylight set key:values are more detailed with suspended ceilings.
|
6953
|
+
# The overlap (:olap) remains in 'transformed' site coordinates (with
|
6954
|
+
# regards to the roof). The :box polygon reverts to attic/plenum space
|
6955
|
+
# coordinates, while the :cbox polygon is reset with regards to the
|
6956
|
+
# occupied space coordinates.
|
6440
6957
|
set = {}
|
6441
6958
|
set[:olap ] = olap
|
6442
6959
|
set[:box ] = box
|
6443
6960
|
set[:cbox ] = cbox
|
6961
|
+
set[:om2 ] = om2
|
6444
6962
|
set[:bm2 ] = bm2
|
6445
6963
|
set[:cm2 ] = cm2
|
6446
|
-
set[:tight ] =
|
6964
|
+
set[:tight ] = false
|
6965
|
+
set[:thin ] = false
|
6447
6966
|
set[:roof ] = ruf
|
6448
6967
|
set[:space ] = space
|
6968
|
+
set[:m ] = space.multiplier
|
6449
6969
|
set[:clng ] = tile
|
6450
|
-
set[:
|
6970
|
+
set[:t0 ] = t0
|
6971
|
+
set[:ti ] = ti
|
6972
|
+
set[:t ] = OpenStudio::Transformation.alignFace(ruf.vertices)
|
6451
6973
|
set[:sidelit] = room[:sidelit]
|
6452
|
-
set[:sloped ] = slopedRoof?(ruf)
|
6453
6974
|
|
6454
6975
|
if unconditioned?(espace) # e.g. attic
|
6455
6976
|
unless attics.key?(espace)
|
6456
|
-
attics[espace] = {
|
6977
|
+
attics[espace] = {ti: ti, m: m, bm2: 0, roofs: []}
|
6457
6978
|
end
|
6458
6979
|
|
6459
6980
|
attics[espace][:bm2 ] += bm2
|
@@ -6464,7 +6985,7 @@ module OSut
|
|
6464
6985
|
ceilings[tile][:attic] = espace
|
6465
6986
|
else # e.g. plenum
|
6466
6987
|
unless plenums.key?(espace)
|
6467
|
-
plenums[espace] = {
|
6988
|
+
plenums[espace] = {ti: ti, m: m, bm2: 0, roofs: []}
|
6468
6989
|
end
|
6469
6990
|
|
6470
6991
|
plenums[espace][:bm2 ] += bm2
|
@@ -6481,21 +7002,20 @@ module OSut
|
|
6481
7002
|
end
|
6482
7003
|
end
|
6483
7004
|
|
6484
|
-
# Ensure uniqueness of plenum roofs
|
7005
|
+
# Ensure uniqueness of plenum roofs.
|
6485
7006
|
attics.values.each do |attic|
|
6486
7007
|
attic[:roofs ].uniq!
|
6487
|
-
attic[:ridges] =
|
7008
|
+
attic[:ridges] = horizontalRidges(attic[:roofs]) # @todo
|
6488
7009
|
end
|
6489
7010
|
|
6490
7011
|
plenums.values.each do |plenum|
|
6491
7012
|
plenum[:roofs ].uniq!
|
6492
|
-
|
6493
|
-
plenum[:ridges] = getHorizontalRidges(plenum[:roofs]) # TO-DO
|
7013
|
+
plenum[:ridges] = horizontalRidges(plenum[:roofs]) # @todo
|
6494
7014
|
end
|
6495
7015
|
|
6496
|
-
# Regardless of the selected skylight arrangement pattern, the
|
6497
|
-
#
|
6498
|
-
#
|
7016
|
+
# Regardless of the selected skylight arrangement pattern, the solution only
|
7017
|
+
# considers attic/plenum sets that can be successfully linked to leader line
|
7018
|
+
# anchors, for both roof and ceiling surfaces. First, attic/plenum roofs.
|
6499
7019
|
[attics, plenums].each do |greniers|
|
6500
7020
|
k = greniers == attics ? :attic : :plenum
|
6501
7021
|
|
@@ -6512,7 +7032,8 @@ module OSut
|
|
6512
7032
|
sts = sts.select { |st| st[:roof] == roof }
|
6513
7033
|
next if sts.empty?
|
6514
7034
|
|
6515
|
-
sts = sts.sort_by { |st| st[:bm2] }
|
7035
|
+
sts = sts.sort_by { |st| st[:bm2] }.reverse
|
7036
|
+
|
6516
7037
|
genAnchors(roof, sts, :box)
|
6517
7038
|
end
|
6518
7039
|
end
|
@@ -6527,7 +7048,7 @@ module OSut
|
|
6527
7048
|
next unless ceiling.key?(k)
|
6528
7049
|
|
6529
7050
|
space = ceiling[:space]
|
6530
|
-
spce = ceiling[k
|
7051
|
+
spce = ceiling[k]
|
6531
7052
|
next unless ceiling.key?(:roofs)
|
6532
7053
|
next unless rooms.key?(space)
|
6533
7054
|
|
@@ -6553,108 +7074,72 @@ module OSut
|
|
6553
7074
|
|
6554
7075
|
next if stz.empty?
|
6555
7076
|
|
7077
|
+
stz = stz.sort_by { |st| st[:cm2] }.reverse
|
6556
7078
|
genAnchors(tile, stz, :cbox)
|
6557
7079
|
end
|
6558
7080
|
|
6559
7081
|
# Delete voided sets.
|
6560
7082
|
sets.reject! { |set| set.key?(:void) }
|
6561
7083
|
|
6562
|
-
|
6563
|
-
rm2 = grossRoofArea(spaces)
|
6564
|
-
|
6565
|
-
# Tally existing skylight rough opening areas (%SRR calculations).
|
6566
|
-
rooms.values.each do |room|
|
6567
|
-
m = room[:m]
|
6568
|
-
|
6569
|
-
room[:roofs].each do |roof|
|
6570
|
-
roof.subSurfaces.each do |sub|
|
6571
|
-
next unless fenestration?(sub)
|
6572
|
-
|
6573
|
-
id = sub.nameString
|
6574
|
-
xm2 = sub.grossArea
|
6575
|
-
|
6576
|
-
if sub.allowWindowPropertyFrameAndDivider
|
6577
|
-
unless sub.windowPropertyFrameAndDivider.empty?
|
6578
|
-
fw = sub.windowPropertyFrameAndDivider.get.frameWidth
|
6579
|
-
vec = offset(sub.vertices, fw, 300)
|
6580
|
-
aire = OpenStudio.getArea(vec)
|
6581
|
-
|
6582
|
-
if aire.empty?
|
6583
|
-
log(ERR, "Skipping '#{id}': invalid Frame&Divider (#{mth})")
|
6584
|
-
else
|
6585
|
-
xm2 = aire.get
|
6586
|
-
end
|
6587
|
-
end
|
6588
|
-
end
|
6589
|
-
|
6590
|
-
m2 += xm2 * sub.multiplier * m
|
6591
|
-
end
|
6592
|
-
end
|
6593
|
-
end
|
6594
|
-
|
6595
|
-
# Required skylight area to add.
|
6596
|
-
sm2 = rm2 * srr - m2
|
7084
|
+
return empty("sets", mth, WRN, rm2) if sets.empty?
|
6597
7085
|
|
6598
|
-
#
|
6599
|
-
|
6600
|
-
log(INF, "Skipping: existing srr > requested srr (#{mth})")
|
6601
|
-
return 0
|
6602
|
-
end
|
7086
|
+
# Sort sets, from largest to smallest bounded box area.
|
7087
|
+
sets = sets.sort_by { |st| st[:bm2] * st[:m] }.reverse
|
6603
7088
|
|
6604
|
-
# Any sidelit/sloped roofs being targeted?
|
6605
|
-
#
|
6606
|
-
#
|
6607
|
-
# skylights/wells (patterns "strip"/"strips").
|
7089
|
+
# Any sidelit and/or sloped roofs being targeted?
|
7090
|
+
# @todo: enable double-ridged, sloped roofs have double-sloped
|
7091
|
+
# skylights/wells (patterns "strip"/"strips").
|
6608
7092
|
sidelit = sets.any? { |set| set[:sidelit] }
|
6609
7093
|
sloped = sets.any? { |set| set[:sloped ] }
|
6610
7094
|
|
7095
|
+
# Average sandbox area + revised 'working' SRR%.
|
7096
|
+
sbm2 = sets.map { |set| set[:bm2] }.reduce(:+)
|
7097
|
+
avm2 = sbm2 / sets.size
|
7098
|
+
srr2 = sm2 / sets.size / avm2
|
7099
|
+
|
6611
7100
|
# Precalculate skylight rows + cols, for each selected pattern. In the case
|
6612
7101
|
# of 'cols x rows' arrays of skylights, the method initially overshoots
|
6613
|
-
# with regards to ideal skylight placement, e.g.:
|
7102
|
+
# with regards to 'ideal' skylight placement, e.g.:
|
6614
7103
|
#
|
6615
7104
|
# aceee.org/files/proceedings/2004/data/papers/SS04_Panel3_Paper18.pdf
|
6616
7105
|
#
|
6617
|
-
#
|
7106
|
+
# Skylight areas are subsequently contracted to strictly meet the target.
|
6618
7107
|
sets.each_with_index do |set, i|
|
6619
|
-
|
6620
|
-
well = set.key?(:clng)
|
6621
|
-
space = set[:space]
|
7108
|
+
thin = set[:thin ]
|
6622
7109
|
tight = set[:tight]
|
6623
7110
|
factor = tight ? 1.75 : 1.25
|
7111
|
+
well = set.key?(:clng)
|
7112
|
+
space = set[:space]
|
6624
7113
|
room = rooms[space]
|
6625
7114
|
h = room[:h]
|
6626
|
-
|
6627
|
-
|
6628
|
-
|
6629
|
-
|
6630
|
-
|
6631
|
-
width = width(obox[:set])
|
6632
|
-
depth = height(obox[:set])
|
6633
|
-
area = width * depth
|
6634
|
-
skym2 = srr * area
|
7115
|
+
width = alignedWidth( set[:box], true)
|
7116
|
+
depth = alignedHeight(set[:box], true)
|
7117
|
+
barea = set.key?(:om2) ? set[:om2] : set[:bm2]
|
7118
|
+
rtio = barea / avm2
|
7119
|
+
skym2 = srr2 * barea * rtio
|
6635
7120
|
|
6636
|
-
# Flag
|
7121
|
+
# Flag set if too narrow/shallow to hold a single skylight.
|
6637
7122
|
if well
|
6638
7123
|
if width.round(2) < wl.round(2)
|
6639
|
-
log(
|
7124
|
+
log(WRN, "set #{i+1} well: Too narrow (#{mth})")
|
6640
7125
|
set[:void] = true
|
6641
7126
|
next
|
6642
7127
|
end
|
6643
7128
|
|
6644
7129
|
if depth.round(2) < wl.round(2)
|
6645
|
-
log(
|
7130
|
+
log(WRN, "set #{i+1} well: Too shallow (#{mth})")
|
6646
7131
|
set[:void] = true
|
6647
7132
|
next
|
6648
7133
|
end
|
6649
7134
|
else
|
6650
7135
|
if width.round(2) < w0.round(2)
|
6651
|
-
log(
|
7136
|
+
log(WRN, "set #{i+1}: Too narrow (#{mth})")
|
6652
7137
|
set[:void] = true
|
6653
7138
|
next
|
6654
7139
|
end
|
6655
7140
|
|
6656
7141
|
if depth.round(2) < w0.round(2)
|
6657
|
-
log(
|
7142
|
+
log(WRN, "set #{i+1}: Too shallow (#{mth})")
|
6658
7143
|
set[:void] = true
|
6659
7144
|
next
|
6660
7145
|
end
|
@@ -6667,8 +7152,8 @@ module OSut
|
|
6667
7152
|
rows = 1
|
6668
7153
|
wx = w0
|
6669
7154
|
wy = w0
|
6670
|
-
wxl = wl
|
6671
|
-
wyl = wl
|
7155
|
+
wxl = well ? wl : nil
|
7156
|
+
wyl = well ? wl : nil
|
6672
7157
|
dX = nil
|
6673
7158
|
dY = nil
|
6674
7159
|
|
@@ -6676,31 +7161,26 @@ module OSut
|
|
6676
7161
|
when "array" # min 2x cols x min 2x rows
|
6677
7162
|
cols = 2
|
6678
7163
|
rows = 2
|
7164
|
+
next if thin
|
6679
7165
|
|
6680
7166
|
if tight
|
6681
7167
|
sp = 1.4 * h / 2
|
6682
|
-
lx =
|
6683
|
-
ly =
|
7168
|
+
lx = width - cols * wx
|
7169
|
+
ly = depth - rows * wy
|
6684
7170
|
next if lx.round(2) < sp.round(2)
|
6685
7171
|
next if ly.round(2) < sp.round(2)
|
6686
7172
|
|
6687
|
-
|
6688
|
-
|
6689
|
-
rows = ((depth - wyl) / (wyl + sp)).round(2).to_i + 1
|
6690
|
-
else
|
6691
|
-
cols = ((width - wx) / (wx + sp)).round(2).to_i + 1
|
6692
|
-
rows = ((depth - wy) / (wy + sp)).round(2).to_i + 1
|
6693
|
-
end
|
6694
|
-
|
7173
|
+
cols = ((width - wx) / (wx + sp)).round(2).to_i + 1
|
7174
|
+
rows = ((depth - wy) / (wy + sp)).round(2).to_i + 1
|
6695
7175
|
next if cols < 2
|
6696
7176
|
next if rows < 2
|
6697
7177
|
|
6698
|
-
dX =
|
6699
|
-
dY =
|
7178
|
+
dX = bfr + f
|
7179
|
+
dY = bfr + f
|
6700
7180
|
else
|
6701
7181
|
sp = 1.4 * h
|
6702
7182
|
lx = well ? (width - cols * wxl) / cols : (width - cols * wx) / cols
|
6703
|
-
ly = well ? (depth - rows * wyl) / rows : (depth - rows * wy) /
|
7183
|
+
ly = well ? (depth - rows * wyl) / rows : (depth - rows * wy) / rows
|
6704
7184
|
next if lx.round(2) < sp.round(2)
|
6705
7185
|
next if ly.round(2) < sp.round(2)
|
6706
7186
|
|
@@ -6715,246 +7195,238 @@ module OSut
|
|
6715
7195
|
next if cols < 2
|
6716
7196
|
next if rows < 2
|
6717
7197
|
|
6718
|
-
ly = well ? (depth - rows * wyl) / rows : (depth - rows * wy) /
|
7198
|
+
ly = well ? (depth - rows * wyl) / rows : (depth - rows * wy) / rows
|
6719
7199
|
dY = ly / 2
|
6720
7200
|
end
|
6721
7201
|
|
6722
|
-
#
|
6723
|
-
#
|
6724
|
-
# undershooting means not reaching 1.75x the required
|
6725
|
-
# undershooting means not reaching 1.25x the required
|
6726
|
-
# consequent overshooting is later corrected.
|
6727
|
-
tm2
|
6728
|
-
undershot = tm2.round(2) < factor * skym2.round(2) ? true : false
|
7202
|
+
# Default allocated skylight area. If undershooting, inflate skylight
|
7203
|
+
# width/depth (with reduced spacing). For geometrically-constrained
|
7204
|
+
# cases, undershooting means not reaching 1.75x the required target.
|
7205
|
+
# Otherwise, undershooting means not reaching 1.25x the required
|
7206
|
+
# target. Any consequent overshooting is later corrected.
|
7207
|
+
tm2 = wx * cols * wy * rows
|
6729
7208
|
|
6730
|
-
# Inflate skylight width/depth (and reduce spacing) to reach
|
6731
|
-
if
|
7209
|
+
# Inflate skylight width/depth (and reduce spacing) to reach target.
|
7210
|
+
if tm2.round(2) < factor * skym2.round(2)
|
6732
7211
|
ratio2 = 1 + (factor * skym2 - tm2) / tm2
|
6733
7212
|
ratio = Math.sqrt(ratio2)
|
6734
7213
|
|
6735
|
-
sp =
|
7214
|
+
sp = wl
|
6736
7215
|
wx *= ratio
|
6737
7216
|
wy *= ratio
|
6738
|
-
wxl = wx + gap
|
6739
|
-
wyl = wy + gap
|
7217
|
+
wxl = wx + gap if well
|
7218
|
+
wyl = wy + gap if well
|
6740
7219
|
|
6741
7220
|
if tight
|
6742
|
-
|
6743
|
-
|
6744
|
-
ly = (depth - rows * wyl) / (rows - 1)
|
6745
|
-
else
|
6746
|
-
lx = (width - cols * wx) / (cols - 1)
|
6747
|
-
ly = (depth - rows * wy) / (rows - 1)
|
6748
|
-
end
|
6749
|
-
|
7221
|
+
lx = (width - 2 * (bfr + f) - cols * wx) / (cols - 1)
|
7222
|
+
ly = (depth - 2 * (bfr + f) - rows * wy) / (rows - 1)
|
6750
7223
|
lx = lx.round(2) < sp.round(2) ? sp : lx
|
6751
7224
|
ly = ly.round(2) < sp.round(2) ? sp : ly
|
6752
|
-
|
6753
|
-
|
6754
|
-
wxl = (width - (cols - 1) * lx) / cols
|
6755
|
-
wyl = (depth - (rows - 1) * ly) / rows
|
6756
|
-
wx = wxl - gap
|
6757
|
-
wy = wyl - gap
|
6758
|
-
else
|
6759
|
-
wx = (width - (cols - 1) * lx) / cols
|
6760
|
-
wy = (depth - (rows - 1) * ly) / rows
|
6761
|
-
wxl = wx + gap
|
6762
|
-
wyl = wy + gap
|
6763
|
-
end
|
7225
|
+
wx = (width - 2 * (bfr + f) - (cols - 1) * lx) / cols
|
7226
|
+
wy = (depth - 2 * (bfr + f) - (rows - 1) * ly) / rows
|
6764
7227
|
else
|
6765
7228
|
if well
|
6766
|
-
lx
|
6767
|
-
ly
|
6768
|
-
|
6769
|
-
|
6770
|
-
ly = (depth - rows * wy) / rows
|
6771
|
-
end
|
6772
|
-
|
6773
|
-
lx = lx.round(2) < sp.round(2) ? sp : lx
|
6774
|
-
ly = ly.round(2) < sp.round(2) ? sp : ly
|
6775
|
-
|
6776
|
-
if well
|
7229
|
+
lx = (width - cols * wxl) / cols
|
7230
|
+
ly = (depth - rows * wyl) / rows
|
7231
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7232
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
6777
7233
|
wxl = (width - cols * lx) / cols
|
6778
7234
|
wyl = (depth - rows * ly) / rows
|
6779
7235
|
wx = wxl - gap
|
6780
7236
|
wy = wyl - gap
|
6781
|
-
lx = (width - cols * wxl) / cols
|
6782
7237
|
ly = (depth - rows * wyl) / rows
|
6783
7238
|
else
|
7239
|
+
lx = (width - cols * wx) / cols
|
7240
|
+
ly = (depth - rows * wy) / rows
|
7241
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7242
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
6784
7243
|
wx = (width - cols * lx) / cols
|
6785
7244
|
wy = (depth - rows * ly) / rows
|
6786
|
-
wxl = wx + gap
|
6787
|
-
wyl = wy + gap
|
6788
|
-
lx = (width - cols * wx) / cols
|
6789
7245
|
ly = (depth - rows * wy) / rows
|
6790
7246
|
end
|
6791
|
-
end
|
6792
7247
|
|
6793
|
-
|
7248
|
+
dY = ly / 2
|
7249
|
+
end
|
6794
7250
|
end
|
6795
7251
|
when "strips" # min 2x cols x 1x row
|
6796
7252
|
cols = 2
|
6797
7253
|
|
6798
7254
|
if tight
|
6799
7255
|
sp = h / 2
|
6800
|
-
|
6801
|
-
|
7256
|
+
dX = bfr + f
|
7257
|
+
lx = width - cols * wx
|
6802
7258
|
next if lx.round(2) < sp.round(2)
|
6803
|
-
next if ly.round(2) < sp.round(2)
|
6804
|
-
|
6805
|
-
if well
|
6806
|
-
cols = ((width - wxl) / (wxl + sp)).round(2).to_i + 1
|
6807
|
-
else
|
6808
|
-
cols = ((width - wx) / (wx + sp)).round(2).to_i + 1
|
6809
|
-
end
|
6810
7259
|
|
7260
|
+
cols = ((width - wx) / (wx + sp)).round(2).to_i + 1
|
6811
7261
|
next if cols < 2
|
6812
7262
|
|
6813
|
-
if
|
6814
|
-
|
6815
|
-
wy
|
7263
|
+
if thin
|
7264
|
+
dY = bfr + f
|
7265
|
+
wy = depth - 2 * dY
|
7266
|
+
next if wy.round(2) < gap4
|
6816
7267
|
else
|
6817
|
-
|
6818
|
-
|
6819
|
-
end
|
7268
|
+
ly = depth - wy
|
7269
|
+
next if ly.round(2) < wl.round(2)
|
6820
7270
|
|
6821
|
-
|
6822
|
-
|
7271
|
+
dY = ly / 2
|
7272
|
+
end
|
6823
7273
|
else
|
6824
7274
|
sp = h
|
6825
|
-
lx = well ? (width - cols * wxl) / cols : (width - cols * wx) / cols
|
6826
|
-
ly = well ? depth - wyl : depth - wy
|
6827
|
-
next if lx.round(2) < sp.round(2)
|
6828
|
-
next if ly.round(2) < w.round(2)
|
6829
7275
|
|
6830
7276
|
if well
|
7277
|
+
lx = (width - cols * wxl) / cols
|
7278
|
+
next if lx.round(2) < sp.round(2)
|
7279
|
+
|
6831
7280
|
cols = (width / (wxl + sp)).round(2).to_i
|
7281
|
+
next if cols < 2
|
7282
|
+
|
7283
|
+
ly = depth - wyl
|
7284
|
+
dY = ly / 2
|
7285
|
+
next if ly.round(2) < wl.round(2)
|
6832
7286
|
else
|
7287
|
+
lx = (width - cols * wx) / cols
|
7288
|
+
next if lx.round(2) < sp.round(2)
|
7289
|
+
|
6833
7290
|
cols = (width / (wx + sp)).round(2).to_i
|
6834
|
-
|
7291
|
+
next if cols < 2
|
6835
7292
|
|
6836
|
-
|
7293
|
+
if thin
|
7294
|
+
dY = bfr + f
|
7295
|
+
wy = depth - 2 * dY
|
7296
|
+
next if wy.round(2) < gap4
|
7297
|
+
else
|
7298
|
+
ly = depth - wy
|
7299
|
+
next if ly.round(2) < wl.round(2)
|
6837
7300
|
|
6838
|
-
|
6839
|
-
|
6840
|
-
wy = wyl - gap
|
6841
|
-
else
|
6842
|
-
wy = depth - ly
|
6843
|
-
wyl = wy + gap
|
7301
|
+
dY = ly / 2
|
7302
|
+
end
|
6844
7303
|
end
|
6845
|
-
|
6846
|
-
dY = ly / 2
|
6847
7304
|
end
|
6848
7305
|
|
6849
|
-
tm2
|
6850
|
-
undershot = tm2.round(2) < factor * skym2.round(2) ? true : false
|
7306
|
+
tm2 = wx * cols * wy
|
6851
7307
|
|
6852
|
-
# Inflate skylight
|
6853
|
-
if
|
6854
|
-
|
7308
|
+
# Inflate skylight depth to reach target.
|
7309
|
+
if tm2.round(2) < factor * skym2.round(2)
|
7310
|
+
sp = wl
|
6855
7311
|
|
6856
|
-
|
6857
|
-
|
6858
|
-
|
7312
|
+
# Skip if already thin.
|
7313
|
+
unless thin
|
7314
|
+
ratio2 = 1 + (factor * skym2 - tm2) / tm2
|
7315
|
+
|
7316
|
+
wy *= ratio2
|
6859
7317
|
|
6860
|
-
if tight
|
6861
7318
|
if well
|
6862
|
-
|
7319
|
+
wyl = wy + gap
|
7320
|
+
ly = depth - wyl
|
7321
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
7322
|
+
wyl = depth - ly
|
7323
|
+
wy = wyl - gap
|
6863
7324
|
else
|
6864
|
-
|
7325
|
+
ly = depth - wy
|
7326
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
7327
|
+
wy = depth - ly
|
6865
7328
|
end
|
6866
7329
|
|
6867
|
-
|
7330
|
+
dY = ly / 2
|
7331
|
+
end
|
7332
|
+
end
|
6868
7333
|
|
6869
|
-
|
6870
|
-
|
6871
|
-
|
6872
|
-
|
6873
|
-
|
6874
|
-
wxl = wx + gap
|
6875
|
-
end
|
6876
|
-
else
|
6877
|
-
if well
|
6878
|
-
lx = (width - cols * wxl) / cols
|
6879
|
-
else
|
6880
|
-
lx = (width - cols * wx) / cols
|
6881
|
-
end
|
7334
|
+
tm2 = wx * cols * wy
|
7335
|
+
|
7336
|
+
# Inflate skylight width (and reduce spacing) to reach target.
|
7337
|
+
if tm2.round(2) < factor * skym2.round(2)
|
7338
|
+
ratio2 = 1 + (factor * skym2 - tm2) / tm2
|
6882
7339
|
|
6883
|
-
|
7340
|
+
wx *= ratio2
|
7341
|
+
wxl = wx + gap if well
|
6884
7342
|
|
7343
|
+
if tight
|
7344
|
+
lx = (width - 2 * (bfr + f) - cols * wx) / (cols - 1)
|
7345
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7346
|
+
wx = (width - 2 * (bfr + f) - (cols - 1) * lx) / cols
|
7347
|
+
else
|
6885
7348
|
if well
|
7349
|
+
lx = (width - cols * wxl) / cols
|
7350
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
6886
7351
|
wxl = (width - cols * lx) / cols
|
6887
7352
|
wx = wxl - gap
|
6888
|
-
lx = (width - cols * wxl) / cols
|
6889
7353
|
else
|
6890
|
-
wx = (width - cols * lx) / cols
|
6891
|
-
wxl = wx + gap
|
6892
7354
|
lx = (width - cols * wx) / cols
|
7355
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7356
|
+
wx = (width - cols * lx) / cols
|
6893
7357
|
end
|
6894
7358
|
end
|
6895
7359
|
end
|
6896
7360
|
else # "strip" 1 (long?) row x 1 column
|
6897
|
-
sp = w
|
6898
|
-
lx = well ? width - wxl : width - wx
|
6899
|
-
ly = well ? depth - wyl : depth - wy
|
6900
|
-
|
6901
7361
|
if tight
|
6902
|
-
|
6903
|
-
|
6904
|
-
|
6905
|
-
if
|
6906
|
-
|
6907
|
-
|
6908
|
-
|
6909
|
-
wy
|
7362
|
+
sp = gap4
|
7363
|
+
dX = bfr + f
|
7364
|
+
wx = width - 2 * dX
|
7365
|
+
next if wx.round(2) < sp.round(2)
|
7366
|
+
|
7367
|
+
if thin
|
7368
|
+
dY = bfr + f
|
7369
|
+
wy = depth - 2 * dY
|
7370
|
+
next if wy.round(2) < sp.round(2)
|
6910
7371
|
else
|
6911
|
-
|
6912
|
-
|
6913
|
-
|
6914
|
-
wyl = wy + gap
|
7372
|
+
ly = depth - wy
|
7373
|
+
dY = ly / 2
|
7374
|
+
next if ly.round(2) < sp.round(2)
|
6915
7375
|
end
|
6916
|
-
|
6917
|
-
dX = well ? 0.0 : bfr + f
|
6918
|
-
dY = ly / 2
|
6919
7376
|
else
|
7377
|
+
sp = wl
|
7378
|
+
lx = well ? width - wxl : width - wx
|
7379
|
+
ly = well ? depth - wyl : depth - wy
|
7380
|
+
dY = ly / 2
|
6920
7381
|
next if lx.round(2) < sp.round(2)
|
6921
7382
|
next if ly.round(2) < sp.round(2)
|
7383
|
+
end
|
6922
7384
|
|
6923
|
-
|
6924
|
-
wxl = width - lx
|
6925
|
-
wyl = depth - ly
|
6926
|
-
wx = wxl - gap
|
6927
|
-
wy = wyl - gap
|
6928
|
-
else
|
6929
|
-
wx = width - lx
|
6930
|
-
wy = depth - ly
|
6931
|
-
wxl = wx + gap
|
6932
|
-
wyl = wy + gap
|
6933
|
-
end
|
7385
|
+
tm2 = wx * wy
|
6934
7386
|
|
6935
|
-
|
7387
|
+
# Inflate skylight width (and reduce spacing) to reach target.
|
7388
|
+
if tm2.round(2) < factor * skym2.round(2)
|
7389
|
+
unless tight
|
7390
|
+
ratio2 = 1 + (factor * skym2 - tm2) / tm2
|
7391
|
+
|
7392
|
+
wx *= ratio2
|
7393
|
+
|
7394
|
+
if well
|
7395
|
+
wxl = wx + gap
|
7396
|
+
lx = width - wxl
|
7397
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7398
|
+
wxl = width - lx
|
7399
|
+
wx = wxl - gap
|
7400
|
+
else
|
7401
|
+
lx = width - wx
|
7402
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7403
|
+
wx = width - lx
|
7404
|
+
end
|
7405
|
+
end
|
6936
7406
|
end
|
6937
7407
|
|
6938
|
-
tm2
|
6939
|
-
undershot = tm2.round(2) < factor * skym2.round(2) ? true : false
|
7408
|
+
tm2 = wx * wy
|
6940
7409
|
|
6941
|
-
# Inflate skylight depth to reach
|
6942
|
-
if
|
6943
|
-
|
7410
|
+
# Inflate skylight depth to reach target. Skip if already tight thin.
|
7411
|
+
if tm2.round(2) < factor * skym2.round(2)
|
7412
|
+
unless thin
|
7413
|
+
ratio2 = 1 + (factor * skym2 - tm2) / tm2
|
6944
7414
|
|
6945
|
-
|
6946
|
-
wy *= ratio2
|
6947
|
-
wyl = wy + gap
|
7415
|
+
wy *= ratio2
|
6948
7416
|
|
6949
|
-
|
6950
|
-
|
7417
|
+
if well
|
7418
|
+
wyl = wy + gap
|
7419
|
+
ly = depth - wyl
|
7420
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
7421
|
+
wyl = depth - ly
|
7422
|
+
wy = wyl - gap
|
7423
|
+
else
|
7424
|
+
ly = depth - wy
|
7425
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
7426
|
+
wy = depth - ly
|
7427
|
+
end
|
6951
7428
|
|
6952
|
-
|
6953
|
-
wyl = depth - ly
|
6954
|
-
wy = wyl - gap
|
6955
|
-
else
|
6956
|
-
wy = depth - ly
|
6957
|
-
wyl = wy + gap
|
7429
|
+
dY = ly / 2
|
6958
7430
|
end
|
6959
7431
|
end
|
6960
7432
|
end
|
@@ -6972,10 +7444,13 @@ module OSut
|
|
6972
7444
|
|
6973
7445
|
set[pattern] = st
|
6974
7446
|
end
|
7447
|
+
|
7448
|
+
set[:void] = true unless patterns.any? { |k| set.key?(k) }
|
6975
7449
|
end
|
6976
7450
|
|
6977
7451
|
# Delete voided sets.
|
6978
7452
|
sets.reject! { |set| set.key?(:void) }
|
7453
|
+
return empty("sets (2)", mth, WRN, rm2) if sets.empty?
|
6979
7454
|
|
6980
7455
|
# Final reset of filters.
|
6981
7456
|
filters.map! { |f| f.include?("b") ? f.delete("b") : f } unless sidelit
|
@@ -6986,15 +7461,15 @@ module OSut
|
|
6986
7461
|
filters.reject! { |f| f.empty? }
|
6987
7462
|
filters.uniq!
|
6988
7463
|
|
6989
|
-
# Initialize skylight area tally.
|
7464
|
+
# Initialize skylight area tally (to increment).
|
6990
7465
|
skm2 = 0
|
6991
7466
|
|
6992
7467
|
# Assign skylight pattern.
|
6993
|
-
filters.
|
7468
|
+
filters.each do |filter|
|
6994
7469
|
next if skm2.round(2) >= sm2.round(2)
|
6995
7470
|
|
7471
|
+
dm2 = sm2 - skm2 # differential (remaining skylight area to meet).
|
6996
7472
|
sts = sets
|
6997
|
-
sts = sts.sort_by { |st| st[:bm2] }.reverse!
|
6998
7473
|
sts = sts.reject { |st| st.key?(:pattern) }
|
6999
7474
|
|
7000
7475
|
if filter.include?("a")
|
@@ -7029,33 +7504,49 @@ module OSut
|
|
7029
7504
|
|
7030
7505
|
fpm2[pattern] = {m2: 0, tight: false} unless fpm2.key?(pattern)
|
7031
7506
|
|
7032
|
-
fpm2[pattern][:m2 ] += wx * wy * cols * rows
|
7033
|
-
fpm2[pattern][:tight] = st[:tight]
|
7507
|
+
fpm2[pattern][:m2 ] += st[:m] * wx * wy * cols * rows
|
7508
|
+
fpm2[pattern][:tight] = true if st[:tight]
|
7034
7509
|
end
|
7035
7510
|
end
|
7036
7511
|
|
7037
7512
|
pattern = nil
|
7038
7513
|
next if fpm2.empty?
|
7039
7514
|
|
7040
|
-
|
7041
|
-
|
7042
|
-
# Select suitable pattern, often overshooting. Favour array unless
|
7043
|
-
# geometrically constrainted.
|
7515
|
+
# Favour (large) arrays if meeting residual target, unless constrained.
|
7044
7516
|
if fpm2.keys.include?("array")
|
7045
|
-
if
|
7517
|
+
if fpm2["array"][:m2].round(2) >= dm2.round(2)
|
7046
7518
|
pattern = "array" unless fpm2[:tight]
|
7047
7519
|
end
|
7048
7520
|
end
|
7049
7521
|
|
7050
7522
|
unless pattern
|
7051
|
-
|
7052
|
-
|
7053
|
-
|
7054
|
-
|
7523
|
+
fpm2 = fpm2.sort_by { |_, fm2| fm2[:m2] }.to_h
|
7524
|
+
min_m2 = fpm2.values.first[:m2]
|
7525
|
+
max_m2 = fpm2.values.last[:m2]
|
7526
|
+
|
7527
|
+
if min_m2.round(2) >= dm2.round(2)
|
7528
|
+
# If not large array, then retain pattern generating smallest skylight
|
7529
|
+
# area if ALL patterns >= residual target (deterministic sorting).
|
7530
|
+
fpm2.keep_if { |_, fm2| fm2[:m2].round(2) == min_m2.round(2) }
|
7531
|
+
|
7532
|
+
if fpm2.keys.include?("array")
|
7533
|
+
pattern = "array"
|
7534
|
+
elsif fpm2.keys.include?("strips")
|
7535
|
+
pattern = "strips"
|
7536
|
+
else # fpm2.keys.include?("strip")
|
7537
|
+
pattern = "strip"
|
7538
|
+
end
|
7055
7539
|
else
|
7056
|
-
|
7057
|
-
|
7058
|
-
|
7540
|
+
# Pick pattern offering greatest skylight area (deterministic sorting).
|
7541
|
+
fpm2.keep_if { |_, fm2| fm2[:m2].round(2) == max_m2.round(2) }
|
7542
|
+
|
7543
|
+
if fpm2.keys.include?("strip")
|
7544
|
+
pattern = "strip"
|
7545
|
+
elsif fpm2.keys.include?("strips")
|
7546
|
+
pattern = "strips"
|
7547
|
+
else # fpm2.keys.include?("array")
|
7548
|
+
pattern = "array"
|
7549
|
+
end
|
7059
7550
|
end
|
7060
7551
|
end
|
7061
7552
|
|
@@ -7086,55 +7577,162 @@ module OSut
|
|
7086
7577
|
end
|
7087
7578
|
end
|
7088
7579
|
|
7089
|
-
#
|
7090
|
-
|
7091
|
-
|
7580
|
+
# Delete incomplete sets (same as rejected if 'voided').
|
7581
|
+
sets.reject! { |set| set.key?(:void) }
|
7582
|
+
sets.select! { |set| set.key?(:pattern) }
|
7583
|
+
return empty("sets (3)", mth, WRN, rm2) if sets.empty?
|
7584
|
+
|
7585
|
+
# Skylight size contraction if overshot (e.g. scale down by -13% if > +13%).
|
7586
|
+
# Applied on a surface/pattern basis: individual skylight sizes may vary
|
7587
|
+
# from one surface to the next, depending on respective patterns.
|
7588
|
+
|
7589
|
+
# First, skip whole sets altogether if their total m2 < (skm2 - sm2). Only
|
7590
|
+
# considered if significant discrepancies vs average set skylight m2.
|
7591
|
+
sbm2 = 0
|
7592
|
+
|
7593
|
+
sets.each do |set|
|
7594
|
+
sbm2 += set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7595
|
+
end
|
7596
|
+
|
7597
|
+
avm2 = sbm2 / sets.size
|
7598
|
+
|
7599
|
+
if skm2.round(2) > sm2.round(2)
|
7600
|
+
sets.reverse.each do |set|
|
7601
|
+
break unless skm2.round(2) > sm2.round(2)
|
7602
|
+
|
7603
|
+
stm2 = set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7604
|
+
next unless stm2 < 0.75 * avm2
|
7605
|
+
next unless stm2.round(2) < (skm2 - sm2).round(2)
|
7606
|
+
|
7607
|
+
skm2 -= stm2
|
7608
|
+
set[:void] = true
|
7609
|
+
end
|
7610
|
+
end
|
7611
|
+
|
7612
|
+
sets.reject! { |set| set.key?(:void) }
|
7613
|
+
return empty("sets (4)", mth, WRN, rm2) if sets.empty?
|
7614
|
+
|
7615
|
+
# Size contraction: round 1: low-hanging fruit.
|
7092
7616
|
if skm2.round(2) > sm2.round(2)
|
7093
7617
|
ratio2 = 1 - (skm2 - sm2) / skm2
|
7094
7618
|
ratio = Math.sqrt(ratio2)
|
7095
|
-
skm2 *= ratio2
|
7096
7619
|
|
7097
7620
|
sets.each do |set|
|
7098
|
-
|
7099
|
-
|
7621
|
+
am2 = set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7622
|
+
xr = set[:w]
|
7623
|
+
yr = set[:d]
|
7100
7624
|
|
7101
|
-
|
7102
|
-
|
7625
|
+
if xr > w0
|
7626
|
+
xr = xr * ratio < w0 ? w0 : xr * ratio
|
7627
|
+
end
|
7103
7628
|
|
7104
|
-
|
7105
|
-
|
7106
|
-
xr = set[:w] * ratio
|
7107
|
-
yr = set[:d] * ratio
|
7108
|
-
dyr = set[:d] - yr
|
7109
|
-
|
7110
|
-
set[:w ] = xr
|
7111
|
-
set[:d ] = yr
|
7112
|
-
set[:w0] = set[:w] + gap
|
7113
|
-
set[:d0] = set[:d] + gap
|
7114
|
-
set[:dY] += dyr / 2
|
7115
|
-
when "strips" # adjust depth
|
7116
|
-
xr2 = set[:w] * ratio2
|
7117
|
-
|
7118
|
-
set[:w ] = xr2
|
7119
|
-
set[:w0] = set[:w] + gap
|
7120
|
-
else # "strip", adjust width
|
7121
|
-
yr2 = set[:d] * ratio2
|
7122
|
-
dyr = set[:d] - yr2
|
7123
|
-
|
7124
|
-
set[:d ] = yr2
|
7125
|
-
set[:d0] = set[:w] + gap
|
7126
|
-
set[:dY] += dyr / 2
|
7629
|
+
if yr > w0
|
7630
|
+
yr = yr * ratio < w0 ? w0 : yr * ratio
|
7127
7631
|
end
|
7632
|
+
|
7633
|
+
xm2 = set[:cols] * xr * set[:rows] * yr * set[:m]
|
7634
|
+
next if xm2.round(2) == am2.round(2)
|
7635
|
+
|
7636
|
+
set[:dY] += (set[:d] - yr) / 2
|
7637
|
+
set[:dX] += (set[:w] - xr) / 2 if set.key?(:dX)
|
7638
|
+
set[:w ] = xr
|
7639
|
+
set[:d ] = yr
|
7640
|
+
set[:w0] = set[:w] + gap
|
7641
|
+
set[:d0] = set[:d] + gap
|
7642
|
+
|
7643
|
+
skm2 -= (am2 - xm2)
|
7128
7644
|
end
|
7129
7645
|
end
|
7130
7646
|
|
7131
|
-
#
|
7647
|
+
# Size contraction: round 2: prioritize larger sets.
|
7648
|
+
adm2 = 0
|
7649
|
+
|
7650
|
+
sets.each_with_index do |set|
|
7651
|
+
next if set[:w].round(2) <= w0
|
7652
|
+
next if set[:d].round(2) <= w0
|
7653
|
+
|
7654
|
+
adm2 += set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7655
|
+
end
|
7656
|
+
|
7657
|
+
if skm2.round(2) > sm2.round(2) && adm2.round(2) > sm2.round(2)
|
7658
|
+
ratio2 = 1 - (adm2 - sm2) / adm2
|
7659
|
+
ratio = Math.sqrt(ratio2)
|
7660
|
+
|
7661
|
+
sets.each do |set|
|
7662
|
+
next if set[:w].round(2) <= w0
|
7663
|
+
next if set[:d].round(2) <= w0
|
7664
|
+
|
7665
|
+
am2 = set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7666
|
+
xr = set[:w]
|
7667
|
+
yr = set[:d]
|
7668
|
+
|
7669
|
+
if xr > w0
|
7670
|
+
xr = xr * ratio < w0 ? w0 : xr * ratio
|
7671
|
+
end
|
7672
|
+
|
7673
|
+
if yr > w0
|
7674
|
+
yr = yr * ratio < w0 ? w0 : yr * ratio
|
7675
|
+
end
|
7676
|
+
|
7677
|
+
xm2 = set[:cols] * xr * set[:rows] * yr * set[:m]
|
7678
|
+
next if xm2.round(2) == am2.round(2)
|
7679
|
+
|
7680
|
+
set[:dY] += (set[:d] - yr) / 2
|
7681
|
+
set[:dX] += (set[:w] - xr) / 2 if set.key?(:dX)
|
7682
|
+
set[:w ] = xr
|
7683
|
+
set[:d ] = yr
|
7684
|
+
set[:w0] = set[:w] + gap
|
7685
|
+
set[:d0] = set[:d] + gap
|
7686
|
+
|
7687
|
+
skm2 -= (am2 - xm2)
|
7688
|
+
adm2 -= (am2 - xm2)
|
7689
|
+
end
|
7690
|
+
end
|
7691
|
+
|
7692
|
+
# Size contraction: round 3: Resort to sizes < requested w0.
|
7693
|
+
if skm2.round(2) > sm2.round(2)
|
7694
|
+
ratio2 = 1 - (skm2 - sm2) / skm2
|
7695
|
+
ratio = Math.sqrt(ratio2)
|
7696
|
+
|
7697
|
+
sets.each do |set|
|
7698
|
+
break unless skm2.round(2) > sm2.round(2)
|
7699
|
+
|
7700
|
+
am2 = set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7701
|
+
xr = set[:w]
|
7702
|
+
yr = set[:d]
|
7703
|
+
|
7704
|
+
if xr > gap4
|
7705
|
+
xr = xr * ratio < gap4 ? gap4 : xr * ratio
|
7706
|
+
end
|
7707
|
+
|
7708
|
+
if yr > gap4
|
7709
|
+
yr = yr * ratio < gap4 ? gap4 : yr * ratio
|
7710
|
+
end
|
7711
|
+
|
7712
|
+
xm2 = set[:cols] * xr * set[:rows] * yr * set[:m]
|
7713
|
+
next if xm2.round(2) == am2.round(2)
|
7714
|
+
|
7715
|
+
set[:dY] += (set[:d] - yr) / 2
|
7716
|
+
set[:dX] += (set[:w] - xr) / 2 if set.key?(:dX)
|
7717
|
+
set[:w ] = xr
|
7718
|
+
set[:d ] = yr
|
7719
|
+
set[:w0] = set[:w] + gap
|
7720
|
+
set[:d0] = set[:d] + gap
|
7721
|
+
|
7722
|
+
skm2 -= (am2 - xm2)
|
7723
|
+
end
|
7724
|
+
end
|
7725
|
+
|
7726
|
+
# Log warning if unable to entirely contract skylight dimensions.
|
7727
|
+
if skm2.round(2) > sm2.round(2)
|
7728
|
+
log(WRN, "Skylights slightly oversized (#{mth})")
|
7729
|
+
end
|
7730
|
+
|
7731
|
+
# Generate skylight well vertices for roofs, attics & plenums.
|
7132
7732
|
[attics, plenums].each do |greniers|
|
7133
7733
|
k = greniers == attics ? :attic : :plenum
|
7134
7734
|
|
7135
7735
|
greniers.each do |spce, grenier|
|
7136
|
-
ti = grenier[:t]
|
7137
|
-
|
7138
7736
|
grenier[:roofs].each do |roof|
|
7139
7737
|
sts = sets
|
7140
7738
|
sts = sts.select { |st| st.key?(k) }
|
@@ -7150,15 +7748,15 @@ module OSut
|
|
7150
7748
|
sts = sts.select { |st| st[:ld].key?(roof) }
|
7151
7749
|
next if sts.empty?
|
7152
7750
|
|
7153
|
-
# If successful, 'genInserts' returns extended
|
7154
|
-
# including leader lines to support cutouts. The
|
7751
|
+
# If successful, 'genInserts' returns extended ROOF surface vertices,
|
7752
|
+
# including leader lines to support cutouts. The method also generates
|
7753
|
+
# new roof inserts. See key:value pair :vts. The FINAL go/no-go is
|
7155
7754
|
# contingent to successfully inserting corresponding room ceiling
|
7156
|
-
# inserts (vis-à-vis attic/plenum floor below).
|
7157
|
-
# generates new roof inserts. See key:value pair :vts.
|
7755
|
+
# inserts (vis-à-vis attic/plenum floor below).
|
7158
7756
|
vz = genInserts(roof, sts)
|
7159
|
-
next if vz.empty?
|
7757
|
+
next if vz.empty?
|
7160
7758
|
|
7161
|
-
roof.setVertices(
|
7759
|
+
roof.setVertices(vz)
|
7162
7760
|
end
|
7163
7761
|
end
|
7164
7762
|
end
|
@@ -7178,14 +7776,15 @@ module OSut
|
|
7178
7776
|
|
7179
7777
|
room = rooms[space]
|
7180
7778
|
grenier = greniers[spce]
|
7181
|
-
ti = grenier[:
|
7182
|
-
t0 = room[:
|
7779
|
+
ti = grenier[:ti]
|
7780
|
+
t0 = room[:t0]
|
7183
7781
|
stz = []
|
7184
7782
|
|
7185
7783
|
ceiling[:roofs].each do |roof|
|
7186
7784
|
sts = sets
|
7187
7785
|
|
7188
7786
|
sts = sts.select { |st| st.key?(k) }
|
7787
|
+
sts = sts.select { |st| st.key?(:pattern) }
|
7189
7788
|
sts = sts.select { |st| st.key?(:clng) }
|
7190
7789
|
sts = sts.select { |st| st.key?(:cm2) }
|
7191
7790
|
sts = sts.select { |st| st.key?(:roof) }
|
@@ -7206,104 +7805,109 @@ module OSut
|
|
7206
7805
|
|
7207
7806
|
next if stz.empty?
|
7208
7807
|
|
7209
|
-
# Vertically-cast set roof :vtx onto ceiling.
|
7210
|
-
stz.each do |st|
|
7211
|
-
cvtx = cast(ti * st[:vtx], t0 * tile.vertices, ray)
|
7212
|
-
st[:cvtx] = t0.inverse * cvtx
|
7213
|
-
end
|
7214
|
-
|
7215
|
-
# Extended ceiling vertices.
|
7216
|
-
vertices = genExtendedVertices(tile, stz, :cvtx)
|
7217
|
-
next if vertices.empty?
|
7218
|
-
|
7219
|
-
# Reset ceiling and adjacent floor vertices.
|
7220
|
-
tile.setVertices(t0.inverse * vertices)
|
7221
|
-
floor.setVertices(ti.inverse * vertices.to_a.reverse)
|
7222
|
-
|
7223
7808
|
# Add new roof inserts & skylights for the (now) toplit space.
|
7224
7809
|
stz.each_with_index do |st, i|
|
7225
|
-
sub
|
7226
|
-
sub[:type
|
7227
|
-
sub[:
|
7228
|
-
sub[:
|
7229
|
-
sub[:sill ] = gap / 2
|
7230
|
-
sub[:frame ] = frame if frame
|
7810
|
+
sub = {}
|
7811
|
+
sub[:type ] = "Skylight"
|
7812
|
+
sub[:frame] = frame if frame
|
7813
|
+
sub[:sill ] = gap / 2
|
7231
7814
|
|
7232
7815
|
st[:vts].each do |id, vt|
|
7233
|
-
roof = OpenStudio::Model::Surface.new(t0.inverse * vt, mdl)
|
7816
|
+
roof = OpenStudio::Model::Surface.new(t0.inverse * (ti * vt), mdl)
|
7234
7817
|
roof.setSpace(space)
|
7235
|
-
roof.setName("#{
|
7818
|
+
roof.setName("#{id}:#{space.nameString}")
|
7236
7819
|
|
7237
7820
|
# Generate well walls.
|
7238
|
-
v0 = roof.vertices
|
7239
7821
|
vX = cast(roof, tile, ray)
|
7240
|
-
s0 =
|
7241
|
-
sX =
|
7822
|
+
s0 = segments(t0 * roof.vertices)
|
7823
|
+
sX = segments(t0 * vX)
|
7242
7824
|
|
7243
7825
|
s0.each_with_index do |sg, j|
|
7244
|
-
sg0 = sg
|
7245
|
-
sgX = sX[j]
|
7246
|
-
vec
|
7826
|
+
sg0 = sg
|
7827
|
+
sgX = sX[j]
|
7828
|
+
vec = OpenStudio::Point3dVector.new
|
7247
7829
|
vec << sg0.first
|
7248
7830
|
vec << sg0.last
|
7249
7831
|
vec << sgX.last
|
7250
7832
|
vec << sgX.first
|
7251
7833
|
|
7252
|
-
|
7834
|
+
v_grenier = ti.inverse * vec
|
7835
|
+
v_room = (t0.inverse * vec).to_a.reverse
|
7836
|
+
|
7837
|
+
grenier_wall = OpenStudio::Model::Surface.new(v_grenier, mdl)
|
7253
7838
|
grenier_wall.setSpace(spce)
|
7254
|
-
grenier_wall.setName("#{id}:#{j}:#{spce.nameString}")
|
7839
|
+
grenier_wall.setName("#{id}:#{i}:#{j}:#{spce.nameString}")
|
7255
7840
|
|
7256
|
-
room_wall = OpenStudio::Model::Surface.new(
|
7841
|
+
room_wall = OpenStudio::Model::Surface.new(v_room, mdl)
|
7257
7842
|
room_wall.setSpace(space)
|
7258
|
-
room_wall.setName("#{id}:#{j}:#{space.nameString}")
|
7843
|
+
room_wall.setName("#{id}:#{i}:#{j}:#{space.nameString}")
|
7259
7844
|
|
7260
7845
|
grenier_wall.setAdjacentSurface(room_wall)
|
7261
7846
|
room_wall.setAdjacentSurface(grenier_wall)
|
7262
7847
|
end
|
7263
7848
|
|
7264
|
-
# Add individual skylights.
|
7265
|
-
|
7849
|
+
# Add individual skylights. Independently of the set layout (rows x
|
7850
|
+
# cols), individual roof inserts may be deeper than wider (or
|
7851
|
+
# vice-versa). Adapt skylight width vs depth accordingly.
|
7852
|
+
if st[:d].round(2) > st[:w].round(2)
|
7853
|
+
sub[:width ] = st[:d] - f2
|
7854
|
+
sub[:height] = st[:w] - f2
|
7855
|
+
else
|
7856
|
+
sub[:width ] = st[:w] - f2
|
7857
|
+
sub[:height] = st[:d] - f2
|
7858
|
+
end
|
7859
|
+
|
7860
|
+
sub[:id] = roof.nameString
|
7861
|
+
addSubs(roof, sub, false, true, true)
|
7266
7862
|
end
|
7267
7863
|
end
|
7864
|
+
|
7865
|
+
# Vertically-cast set roof :vtx onto ceiling.
|
7866
|
+
stz.each do |st|
|
7867
|
+
st[:cvtx] = t0.inverse * cast(ti * st[:vtx], t0 * tile.vertices, ray)
|
7868
|
+
end
|
7869
|
+
|
7870
|
+
# Extended ceiling vertices.
|
7871
|
+
vertices = genExtendedVertices(tile, stz, :cvtx)
|
7872
|
+
next if vertices.empty?
|
7873
|
+
|
7874
|
+
# Reset ceiling and adjacent floor vertices.
|
7875
|
+
tile.setVertices(vertices)
|
7876
|
+
floor.setVertices(ti.inverse * (t0 * vertices).to_a.reverse)
|
7268
7877
|
end
|
7269
7878
|
|
7270
|
-
#
|
7271
|
-
# coordinate adjustments.
|
7879
|
+
# Loop through 'direct' roof surfaces of rooms to toplit (no attics or
|
7880
|
+
# plenums). No overlaps, so no relative space coordinate adjustments.
|
7272
7881
|
rooms.each do |space, room|
|
7273
7882
|
room[:roofs].each do |roof|
|
7274
|
-
sets.each_with_index do |
|
7275
|
-
next
|
7276
|
-
next unless
|
7277
|
-
next
|
7278
|
-
next unless
|
7279
|
-
next unless
|
7280
|
-
next unless
|
7281
|
-
next unless
|
7282
|
-
next unless
|
7283
|
-
next unless
|
7284
|
-
|
7285
|
-
|
7286
|
-
|
7287
|
-
|
7288
|
-
|
7289
|
-
|
7290
|
-
# Y-axis 'height' of the roof, once re/aligned.
|
7291
|
-
# TODO: retrieve st[:out], +efficient
|
7292
|
-
y = alignedHeight(set[:box])
|
7293
|
-
dY = set[:dY] if set[:dY]
|
7294
|
-
|
7295
|
-
set[:rows].times.each do |j|
|
7883
|
+
sets.each_with_index do |st, i|
|
7884
|
+
next unless st.key?(:roof)
|
7885
|
+
next unless st[:roof] == roof
|
7886
|
+
next if st.key?(:clng)
|
7887
|
+
next unless st.key?(:box)
|
7888
|
+
next unless st.key?(:cols)
|
7889
|
+
next unless st.key?(:rows)
|
7890
|
+
next unless st.key?(:d)
|
7891
|
+
next unless st.key?(:w)
|
7892
|
+
next unless st.key?(:dY)
|
7893
|
+
|
7894
|
+
w1 = st[:w ] - f2
|
7895
|
+
d1 = st[:d ] - f2
|
7896
|
+
dY = st[:dY]
|
7897
|
+
|
7898
|
+
st[:rows].times.each do |j|
|
7296
7899
|
sub = {}
|
7297
7900
|
sub[:type ] = "Skylight"
|
7298
|
-
sub[:count ] =
|
7901
|
+
sub[:count ] = st[:cols]
|
7299
7902
|
sub[:width ] = w1
|
7300
7903
|
sub[:height ] = d1
|
7301
7904
|
sub[:frame ] = frame if frame
|
7302
|
-
sub[:id ] = "
|
7905
|
+
sub[:id ] = "#{roof.nameString}:#{i}:#{j}"
|
7303
7906
|
sub[:sill ] = dY + j * (2 * dY + d1)
|
7304
|
-
sub[:r_buffer] =
|
7305
|
-
sub[:l_buffer] =
|
7306
|
-
|
7907
|
+
sub[:r_buffer] = st[:dX] if st[:dX]
|
7908
|
+
sub[:l_buffer] = st[:dX] if st[:dX]
|
7909
|
+
|
7910
|
+
addSubs(roof, sub, false, true, true)
|
7307
7911
|
end
|
7308
7912
|
end
|
7309
7913
|
end
|