tbd 3.4.1 → 3.4.3
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 +16 -0
- data/Rakefile +1 -1
- data/lib/measures/tbd/measure.xml +6 -6
- data/lib/measures/tbd/resources/psi.rb +20 -43
- data/lib/measures/tbd/resources/tbd.rb +10 -10
- data/lib/measures/tbd/resources/ua.rb +3 -3
- data/lib/measures/tbd/resources/utils.rb +1354 -895
- data/lib/tbd/psi.rb +20 -43
- data/lib/tbd/ua.rb +3 -3
- data/lib/tbd/version.rb +1 -1
- data/lib/tbd.rb +13 -13
- data/tbd.gemspec +1 -1
- metadata +5 -5
@@ -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
|
@@ -1924,7 +1924,7 @@ module OSut
|
|
1924
1924
|
# isolation) to determine whether an UNOCCUPIED space should have its
|
1925
1925
|
# envelope insulated ("plenum") or not ("attic").
|
1926
1926
|
#
|
1927
|
-
# In contrast to OpenStudio-Standards' "space_plenum?",
|
1927
|
+
# In contrast to OpenStudio-Standards' "space_plenum?", the method below
|
1928
1928
|
# strictly returns FALSE if a space is indeed "partofTotalFloorArea". It
|
1929
1929
|
# also returns FALSE if the space is a vestibule. Otherwise, it needs more
|
1930
1930
|
# information to determine if such an UNOCCUPIED space is indeed a
|
@@ -1933,7 +1933,7 @@ module OSut
|
|
1933
1933
|
# CASE A: it includes the substring "plenum" (case insensitive) in its
|
1934
1934
|
# spaceType's name, or in the latter's standardsSpaceType string;
|
1935
1935
|
#
|
1936
|
-
# CASE B: "isPlenum" == TRUE in an OpenStudio model WITH HVAC airloops
|
1936
|
+
# CASE B: "isPlenum" == TRUE in an OpenStudio model WITH HVAC airloops; OR
|
1937
1937
|
#
|
1938
1938
|
# CASE C: its zone holds an 'inactive' thermostat (i.e. can't extract valid
|
1939
1939
|
# setpoints) in an OpenStudio model with setpoint temperatures.
|
@@ -2054,7 +2054,7 @@ module OSut
|
|
2054
2054
|
res[:heating] = nil
|
2055
2055
|
res[:cooling] = nil
|
2056
2056
|
elsif cnd.downcase == "semiheated"
|
2057
|
-
res[:heating] =
|
2057
|
+
res[:heating] = 14.0 if res[:heating].nil?
|
2058
2058
|
res[:cooling] = nil
|
2059
2059
|
elsif cnd.downcase.include?("conditioned")
|
2060
2060
|
# "nonresconditioned", "resconditioned" or "indirectlyconditioned"
|
@@ -2090,6 +2090,60 @@ module OSut
|
|
2090
2090
|
ok
|
2091
2091
|
end
|
2092
2092
|
|
2093
|
+
##
|
2094
|
+
# Validates whether a space can be considered as REFRIGERATED.
|
2095
|
+
#
|
2096
|
+
# @param space [OpenStudio::Model::Space] a space
|
2097
|
+
#
|
2098
|
+
# @return [Bool] whether space is considered REFRIGERATED
|
2099
|
+
# @return [false] if invalid input (see logs)
|
2100
|
+
def refrigerated?(space = nil)
|
2101
|
+
mth = "OSut::#{__callee__}"
|
2102
|
+
cl = OpenStudio::Model::Space
|
2103
|
+
tg0 = "refrigerated"
|
2104
|
+
return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
|
2105
|
+
|
2106
|
+
# 1. First check OSut's REFRIGERATED status.
|
2107
|
+
status = space.additionalProperties.getFeatureAsString(tg0)
|
2108
|
+
|
2109
|
+
unless status.empty?
|
2110
|
+
status = status.get
|
2111
|
+
return status if [true, false].include?(status)
|
2112
|
+
|
2113
|
+
log(ERR, "Unknown #{space.nameString} REFRIGERATED #{status} (#{mth})")
|
2114
|
+
end
|
2115
|
+
|
2116
|
+
# 2. Else, compare design heating/cooling setpoints.
|
2117
|
+
stps = setpoints(space)
|
2118
|
+
return false unless stps[:heating].nil?
|
2119
|
+
return false if stps[:cooling].nil?
|
2120
|
+
return true if stps[:cooling] < 15
|
2121
|
+
|
2122
|
+
false
|
2123
|
+
end
|
2124
|
+
|
2125
|
+
##
|
2126
|
+
# Validates whether a space can be considered as SEMIHEATED as per NECB 2020
|
2127
|
+
# 1.2.1.2. 2): design heating setpoint < 15°C (and non-REFRIGERATED).
|
2128
|
+
#
|
2129
|
+
# @param space [OpenStudio::Model::Space] a space
|
2130
|
+
#
|
2131
|
+
# @return [Bool] whether space is considered SEMIHEATED
|
2132
|
+
# @return [false] if invalid input (see logs)
|
2133
|
+
def semiheated?(space = nil)
|
2134
|
+
mth = "OSut::#{__callee__}"
|
2135
|
+
cl = OpenStudio::Model::Space
|
2136
|
+
return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
|
2137
|
+
return false if refrigerated?(space)
|
2138
|
+
|
2139
|
+
stps = setpoints(space)
|
2140
|
+
return false unless stps[:cooling].nil?
|
2141
|
+
return false if stps[:heating].nil?
|
2142
|
+
return true if stps[:heating] < 15
|
2143
|
+
|
2144
|
+
false
|
2145
|
+
end
|
2146
|
+
|
2093
2147
|
##
|
2094
2148
|
# Generates an HVAC availability schedule.
|
2095
2149
|
#
|
@@ -2256,9 +2310,9 @@ module OSut
|
|
2256
2310
|
# ---- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---- #
|
2257
2311
|
# This final set of utilities targets OpenStudio geometry. Many of the
|
2258
2312
|
# following geometry methods rely on Boost as an OpenStudio dependency.
|
2259
|
-
# As per Boost requirements, points (e.g.
|
2313
|
+
# As per Boost requirements, points (e.g. vertical polygon) must be 'aligned':
|
2260
2314
|
# - first rotated/tilted as to lay flat along XY plane (Z-axis ~= 0)
|
2261
|
-
# - initial Z-axis values
|
2315
|
+
# - initial Z-axis values now become Y-axis values
|
2262
2316
|
# - points with the lowest X-axis values are 'aligned' along X-axis (0)
|
2263
2317
|
# - points with the lowest Z-axis values are 'aligned' along Y-axis (0)
|
2264
2318
|
# - for several Boost methods, points must be clockwise in sequence
|
@@ -2945,9 +2999,16 @@ module OSut
|
|
2945
2999
|
a1b2 = b2 - a1
|
2946
3000
|
xa1b1 = a.cross(a1b1)
|
2947
3001
|
xa1b2 = a.cross(a1b2)
|
3002
|
+
xa1b1.normalize
|
3003
|
+
xa1b2.normalize
|
3004
|
+
xab.normalize
|
2948
3005
|
return nil unless xab.cross(xa1b1).length.round(4) < TOL2
|
2949
3006
|
return nil unless xab.cross(xa1b2).length.round(4) < TOL2
|
2950
3007
|
|
3008
|
+
# Reset.
|
3009
|
+
xa1b1 = a.cross(a1b1)
|
3010
|
+
xa1b2 = a.cross(a1b2)
|
3011
|
+
|
2951
3012
|
# Both segment endpoints can't be 'behind' point.
|
2952
3013
|
return nil if a.dot(a1b1) < 0 && a.dot(a1b2) < 0
|
2953
3014
|
|
@@ -3001,20 +3062,23 @@ module OSut
|
|
3001
3062
|
end
|
3002
3063
|
|
3003
3064
|
##
|
3004
|
-
#
|
3065
|
+
# Validates whether OpenStudio 3D points are listed clockwise, assuming points
|
3066
|
+
# have been pre-'aligned' - not just flattened along XY (i.e. Z = 0).
|
3005
3067
|
#
|
3006
|
-
# @param pts [OpenStudio::Point3dVector] 3D points
|
3068
|
+
# @param pts [OpenStudio::Point3dVector] pre-aligned 3D points
|
3007
3069
|
#
|
3008
3070
|
# @return [Bool] whether sequence is clockwise
|
3009
3071
|
# @return [false] if invalid input (see logs)
|
3010
3072
|
def clockwise?(pts = nil)
|
3011
3073
|
mth = "OSut::#{__callee__}"
|
3012
3074
|
pts = to_p3Dv(pts)
|
3013
|
-
|
3014
|
-
return invalid("
|
3015
|
-
return invalid("flat points", mth, 1, DBG, n) unless xyz?(pts, :z)
|
3075
|
+
return invalid("3+ points" , mth, 1, DBG, false) if pts.size < 3
|
3076
|
+
return invalid("flat points", mth, 1, DBG, false) unless xyz?(pts, :z)
|
3016
3077
|
|
3017
|
-
OpenStudio.
|
3078
|
+
n = OpenStudio.getOutwardNormal(pts)
|
3079
|
+
return invalid("polygon", mth, 1, DBG, false) if n.empty?
|
3080
|
+
|
3081
|
+
n.get.z > 0 ? false : true
|
3018
3082
|
end
|
3019
3083
|
|
3020
3084
|
##
|
@@ -3144,8 +3208,8 @@ module OSut
|
|
3144
3208
|
# polygon. In addition to basic OpenStudio polygon tests (e.g. all points
|
3145
3209
|
# sharing the same 3D plane, non-self-intersecting), the method can
|
3146
3210
|
# 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
|
-
#
|
3211
|
+
# Returned vector can also be 'aligned', as well as in UpperLeftCorner (ULC),
|
3212
|
+
# BottomLeftCorner (BLC), in clockwise (or counterclockwise) sequence.
|
3149
3213
|
#
|
3150
3214
|
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
3151
3215
|
# @param vx [Bool] whether to check for convexity
|
@@ -3162,7 +3226,7 @@ module OSut
|
|
3162
3226
|
v = OpenStudio::Point3dVector.new
|
3163
3227
|
vx = false unless [true, false].include?(vx)
|
3164
3228
|
uq = false unless [true, false].include?(uq)
|
3165
|
-
co =
|
3229
|
+
co = false unless [true, false].include?(co)
|
3166
3230
|
|
3167
3231
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3168
3232
|
# Exit if mismatched/invalid arguments.
|
@@ -3174,7 +3238,7 @@ module OSut
|
|
3174
3238
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3175
3239
|
# Minimum 3 points?
|
3176
3240
|
p3 = getNonCollinears(pts, 3)
|
3177
|
-
return empty("polygon", mth, ERR, v) if p3.size < 3
|
3241
|
+
return empty("polygon (non-collinears < 3)", mth, ERR, v) if p3.size < 3
|
3178
3242
|
|
3179
3243
|
# Coplanar?
|
3180
3244
|
pln = OpenStudio::Plane.new(p3)
|
@@ -3273,7 +3337,7 @@ module OSut
|
|
3273
3337
|
return mismatch("point", p0, cl, mth, DBG, false) unless p0.is_a?(cl)
|
3274
3338
|
|
3275
3339
|
n = OpenStudio.getOutwardNormal(s)
|
3276
|
-
return false if n.empty?
|
3340
|
+
return invalid("plane/normal", mth, 2, DBG, false) if n.empty?
|
3277
3341
|
|
3278
3342
|
n = n.get
|
3279
3343
|
pl = OpenStudio::Plane.new(s.first, n)
|
@@ -3299,23 +3363,21 @@ module OSut
|
|
3299
3363
|
mpV = scalar(mid - p0, 1000)
|
3300
3364
|
p1 = p0 + mpV
|
3301
3365
|
ctr = 0
|
3302
|
-
pts = []
|
3303
3366
|
|
3304
3367
|
# Skip if ~collinear.
|
3305
|
-
next if
|
3368
|
+
next if mpV.cross(segment.last - segment.first).length.round(4) < TOL2
|
3306
3369
|
|
3307
3370
|
segments.each do |sg|
|
3308
3371
|
intersect = getLineIntersection([p0, p1], sg)
|
3309
3372
|
next unless intersect
|
3310
3373
|
|
3311
|
-
#
|
3374
|
+
# Skip test altogether if one of the polygon vertices.
|
3312
3375
|
if holds?(s, intersect)
|
3313
|
-
|
3314
|
-
|
3315
|
-
|
3376
|
+
ctr = 0
|
3377
|
+
break
|
3378
|
+
else
|
3379
|
+
ctr += 1
|
3316
3380
|
end
|
3317
|
-
|
3318
|
-
ctr += 1
|
3319
3381
|
end
|
3320
3382
|
|
3321
3383
|
next if ctr.zero?
|
@@ -3334,56 +3396,88 @@ module OSut
|
|
3334
3396
|
# @return [Bool] whether 2 polygons are parallel
|
3335
3397
|
# @return [false] if invalid input (see logs)
|
3336
3398
|
def parallel?(p1 = nil, p2 = nil)
|
3337
|
-
p1
|
3338
|
-
p2
|
3399
|
+
p1 = poly(p1, false, true)
|
3400
|
+
p2 = poly(p2, false, true)
|
3339
3401
|
return false if p1.empty?
|
3340
3402
|
return false if p2.empty?
|
3341
3403
|
|
3342
|
-
|
3343
|
-
|
3344
|
-
return false if
|
3345
|
-
return false if
|
3404
|
+
n1 = OpenStudio.getOutwardNormal(p1)
|
3405
|
+
n2 = OpenStudio.getOutwardNormal(p2)
|
3406
|
+
return false if n1.empty?
|
3407
|
+
return false if n2.empty?
|
3408
|
+
|
3409
|
+
n1.get.dot(n2.get).abs > 0.99
|
3410
|
+
end
|
3411
|
+
|
3412
|
+
##
|
3413
|
+
# Validates whether a polygon can be considered a valid 'roof' surface, as per
|
3414
|
+
# ASHRAE 90.1 & Canadian NECBs, i.e. outward normal within 60° from vertical
|
3415
|
+
#
|
3416
|
+
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
3417
|
+
#
|
3418
|
+
# @return [Bool] if considered a roof surface
|
3419
|
+
# @return [false] if invalid input (see logs)
|
3420
|
+
def roof?(pts = nil)
|
3421
|
+
ray = OpenStudio::Point3d.new(0,0,1) - OpenStudio::Point3d.new(0,0,0)
|
3422
|
+
dut = Math.cos(60 * Math::PI / 180)
|
3423
|
+
pts = poly(pts, false, true, true)
|
3424
|
+
return false if pts.empty?
|
3346
3425
|
|
3347
|
-
|
3348
|
-
|
3426
|
+
dot = ray.dot(OpenStudio.getOutwardNormal(pts).get)
|
3427
|
+
return false if dot.round(2) <= 0
|
3428
|
+
return true if dot.round(2) == 1
|
3349
3429
|
|
3350
|
-
|
3430
|
+
dot.round(4) >= dut.round(4)
|
3351
3431
|
end
|
3352
3432
|
|
3353
3433
|
##
|
3354
|
-
# Validates whether a polygon faces upwards
|
3434
|
+
# Validates whether a polygon faces upwards, harmonized with OpenStudio
|
3435
|
+
# Utilities' "alignZPrime" function.
|
3355
3436
|
#
|
3356
3437
|
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
3357
3438
|
#
|
3358
3439
|
# @return [Bool] if facing upwards
|
3359
3440
|
# @return [false] if invalid input (see logs)
|
3360
3441
|
def facingUp?(pts = nil)
|
3361
|
-
|
3362
|
-
pts = poly(pts, false, true,
|
3363
|
-
return false if pts.empty?
|
3364
|
-
|
3365
|
-
pts = getNonCollinears(pts, 3)
|
3442
|
+
ray = OpenStudio::Point3d.new(0,0,1) - OpenStudio::Point3d.new(0,0,0)
|
3443
|
+
pts = poly(pts, false, true, true)
|
3366
3444
|
return false if pts.empty?
|
3367
3445
|
|
3368
|
-
OpenStudio
|
3446
|
+
OpenStudio.getOutwardNormal(pts).get.dot(ray) > 0.99
|
3369
3447
|
end
|
3370
3448
|
|
3371
3449
|
##
|
3372
|
-
# Validates whether a polygon faces downwards
|
3450
|
+
# Validates whether a polygon faces downwards, harmonized with OpenStudio
|
3451
|
+
# Utilities' "alignZPrime" function.
|
3373
3452
|
#
|
3374
3453
|
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
3375
3454
|
#
|
3376
3455
|
# @return [Bool] if facing downwards
|
3377
3456
|
# @return [false] if invalid input (see logs)
|
3378
3457
|
def facingDown?(pts = nil)
|
3379
|
-
|
3380
|
-
pts = poly(pts, false, true,
|
3458
|
+
ray = OpenStudio::Point3d.new(0,0,-1) - OpenStudio::Point3d.new(0,0,0)
|
3459
|
+
pts = poly(pts, false, true, true)
|
3381
3460
|
return false if pts.empty?
|
3382
3461
|
|
3383
|
-
pts
|
3462
|
+
OpenStudio.getOutwardNormal(pts).get.dot(ray) > 0.99
|
3463
|
+
end
|
3464
|
+
|
3465
|
+
##
|
3466
|
+
# Validates whether surface can be considered 'sloped' (i.e. not ~flat, as per
|
3467
|
+
# OpenStudio Utilities' "alignZPrime"). A vertical polygon returns true.
|
3468
|
+
#
|
3469
|
+
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
3470
|
+
#
|
3471
|
+
# @return [Bool] whether surface is sloped
|
3472
|
+
# @return [false] if invalid input (see logs)
|
3473
|
+
def sloped?(pts = nil)
|
3474
|
+
mth = "OSut::#{__callee__}"
|
3475
|
+
pts = poly(pts, false, true, true)
|
3384
3476
|
return false if pts.empty?
|
3477
|
+
return false if facingUp?(pts)
|
3478
|
+
return false if facingDown?(pts)
|
3385
3479
|
|
3386
|
-
|
3480
|
+
true
|
3387
3481
|
end
|
3388
3482
|
|
3389
3483
|
##
|
@@ -3454,6 +3548,15 @@ module OSut
|
|
3454
3548
|
|
3455
3549
|
p1.each { |p0| return false unless pointWithinPolygon?(p0, p2) }
|
3456
3550
|
|
3551
|
+
# Although p2 points may lie ALONG p1, none may lie entirely WITHIN p1.
|
3552
|
+
p2.each { |p0| return false if pointWithinPolygon?(p0, p1, true) }
|
3553
|
+
|
3554
|
+
# p1 segment mid-points must not lie OUTSIDE of p2.
|
3555
|
+
getSegments(p1).each do |sg|
|
3556
|
+
mp = midpoint(sg.first, sg.last)
|
3557
|
+
return false unless pointWithinPolygon?(mp, p2)
|
3558
|
+
end
|
3559
|
+
|
3457
3560
|
entirely = false unless [true, false].include?(entirely)
|
3458
3561
|
return true unless entirely
|
3459
3562
|
|
@@ -3600,7 +3703,7 @@ module OSut
|
|
3600
3703
|
# The following +/- replicates the same solution, based on:
|
3601
3704
|
# https://stackoverflow.com/a/65832417
|
3602
3705
|
p0 = p2.first
|
3603
|
-
pl = OpenStudio::Plane.new(
|
3706
|
+
pl = OpenStudio::Plane.new(p2)
|
3604
3707
|
n = pl.outwardNormal
|
3605
3708
|
return face if n.dot(ray).abs < TOL
|
3606
3709
|
|
@@ -3962,6 +4065,8 @@ module OSut
|
|
3962
4065
|
box << OpenStudio::Point3d.new(p1.x, p1.y, p1.z)
|
3963
4066
|
box << OpenStudio::Point3d.new(p2.x, p2.y, p2.z)
|
3964
4067
|
box << OpenStudio::Point3d.new(p3.x, p3.y, p3.z)
|
4068
|
+
box = getNonCollinears(box, 4)
|
4069
|
+
return bkp unless box.size == 4
|
3965
4070
|
|
3966
4071
|
box = blc(box)
|
3967
4072
|
return bkp unless rectangular?(box)
|
@@ -4007,6 +4112,9 @@ module OSut
|
|
4007
4112
|
box << mpoints.first
|
4008
4113
|
box << mpoints.last
|
4009
4114
|
box << plane.project(mpoints.last)
|
4115
|
+
box = getNonCollinears(box).to_a
|
4116
|
+
return bkp unless box.size == 4
|
4117
|
+
|
4010
4118
|
box = clockwise?(box) ? blc(box.reverse) : blc(box)
|
4011
4119
|
return bkp unless rectangular?(box)
|
4012
4120
|
return bkp unless fits?(box, pts)
|
@@ -4071,6 +4179,7 @@ module OSut
|
|
4071
4179
|
out = triadBox(OpenStudio::Point3dVector.new([m0, p1, p2]))
|
4072
4180
|
next if out.empty?
|
4073
4181
|
next unless fits?(out, pts)
|
4182
|
+
next if fits?(pts, out)
|
4074
4183
|
|
4075
4184
|
area = OpenStudio.getArea(out)
|
4076
4185
|
next if area.empty?
|
@@ -4096,6 +4205,7 @@ module OSut
|
|
4096
4205
|
out = triadBox(OpenStudio::Point3dVector.new([p0, p1, p2]))
|
4097
4206
|
next if out.empty?
|
4098
4207
|
next unless fits?(out, pts)
|
4208
|
+
next if fits?(pts, out)
|
4099
4209
|
|
4100
4210
|
area = OpenStudio.getArea(out)
|
4101
4211
|
next if area.empty?
|
@@ -4128,6 +4238,7 @@ module OSut
|
|
4128
4238
|
out = medialBox(OpenStudio::Point3dVector.new([p0, p1, p2]))
|
4129
4239
|
next if out.empty?
|
4130
4240
|
next unless fits?(out, pts)
|
4241
|
+
next if fits?(pts, out)
|
4131
4242
|
|
4132
4243
|
area = OpenStudio.getArea(box)
|
4133
4244
|
next if area.empty?
|
@@ -4157,6 +4268,7 @@ module OSut
|
|
4157
4268
|
out = medialBox(OpenStudio::Point3dVector.new([p0, p1, p2]))
|
4158
4269
|
next if out.empty?
|
4159
4270
|
next unless fits?(out, pts)
|
4271
|
+
next if fits?(pts, out)
|
4160
4272
|
|
4161
4273
|
area = OpenStudio.getArea(box)
|
4162
4274
|
next if area.empty?
|
@@ -4191,6 +4303,7 @@ module OSut
|
|
4191
4303
|
out = medialBox(OpenStudio::Point3dVector.new([p0, p1, p2]))
|
4192
4304
|
next if out.empty?
|
4193
4305
|
next unless fits?(out, pts)
|
4306
|
+
next if fits?(pts, out)
|
4194
4307
|
|
4195
4308
|
area = OpenStudio.getArea(out)
|
4196
4309
|
next if area.empty?
|
@@ -4214,25 +4327,29 @@ module OSut
|
|
4214
4327
|
|
4215
4328
|
##
|
4216
4329
|
# Generates re-'aligned' polygon vertices wrt main axis of symmetry of its
|
4217
|
-
# largest bounded box.
|
4330
|
+
# largest bounded box. Input polygon vertex Z-axis values must equal 0, and be
|
4331
|
+
# counterclockwise. A Hash is returned with 6x key:value pairs ...
|
4218
4332
|
# set: realigned (cloned) polygon vertices, box: its bounded box (wrt to :set),
|
4219
4333
|
# bbox: its bounding box, t: its translation transformation, r: its rotation
|
4220
4334
|
# transformation, and o: the origin coordinates of its axis of rotation. First,
|
4221
4335
|
# cloned polygon vertices are rotated so the longest axis of symmetry of its
|
4222
4336
|
# 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).
|
4337
|
+
# side (of the bounded box) nearest to grid origin (0,0,0). If the axis of
|
4338
|
+
# symmetry of the bounded box is already parallel to the X-axis, then the
|
4339
|
+
# rotation step is skipped (unless force == true). Whether rotated or not,
|
4224
4340
|
# polygon vertices are then translated as to ensure one or more vertices are
|
4225
4341
|
# 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
|
4342
|
+
# Y-axis (no vertices with negative X or Y coordinate values). To unalign
|
4343
|
+
# the returned set of vertices (or its bounded box, or its bounding box),
|
4344
|
+
# first inverse the translation transformation, then inverse the rotation
|
4229
4345
|
# transformation.
|
4230
4346
|
#
|
4231
4347
|
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
4348
|
+
# @param force [Bool] whether to force rotation for aligned yet narrow boxes
|
4232
4349
|
#
|
4233
4350
|
# @return [Hash] :set, :box, :bbox, :t, :r & :o
|
4234
4351
|
# @return [Hash] :set, :box, :bbox, :t, :r & :o (nil) if invalid (see logs)
|
4235
|
-
def getRealignedFace(pts = nil)
|
4352
|
+
def getRealignedFace(pts = nil, force = false)
|
4236
4353
|
mth = "OSut::#{__callee__}"
|
4237
4354
|
out = { set: nil, box: nil, bbox: nil, t: nil, r: nil, o: nil }
|
4238
4355
|
pts = poly(pts, false, true)
|
@@ -4240,6 +4357,13 @@ module OSut
|
|
4240
4357
|
return invalid("aligned plane", mth, 1, DBG, out) unless xyz?(pts, :z)
|
4241
4358
|
return invalid("clockwise pts", mth, 1, DBG, out) if clockwise?(pts)
|
4242
4359
|
|
4360
|
+
# Optionally force rotation so bounded box ends up wider than taller.
|
4361
|
+
# Strongly suggested for flat surfaces like roofs (see 'sloped?').
|
4362
|
+
unless [true, false].include?(force)
|
4363
|
+
log(DBG, "Ignoring force input (#{mth})")
|
4364
|
+
force = false
|
4365
|
+
end
|
4366
|
+
|
4243
4367
|
o = OpenStudio::Point3d.new(0, 0, 0)
|
4244
4368
|
w = width(pts)
|
4245
4369
|
h = height(pts)
|
@@ -4263,7 +4387,6 @@ module OSut
|
|
4263
4387
|
sgs = sgs.sort_by { |sg, s| s[:mo] }.first(2).to_h if square?(box)
|
4264
4388
|
sgs = sgs.sort_by { |sg, s| s[:l ] }.first(2).to_h unless square?(box)
|
4265
4389
|
sgs = sgs.sort_by { |sg, s| s[:mo] }.first(2).to_h unless square?(box)
|
4266
|
-
|
4267
4390
|
sg0 = sgs.values[0]
|
4268
4391
|
sg1 = sgs.values[1]
|
4269
4392
|
|
@@ -4283,6 +4406,13 @@ module OSut
|
|
4283
4406
|
axis = OpenStudio::Point3d.new(origin.x, origin.y , d) - origin
|
4284
4407
|
angle = OpenStudio::getAngle(right, seg)
|
4285
4408
|
angle = -angle if north.dot(seg) < 0
|
4409
|
+
|
4410
|
+
# Skip rotation if bounded box is already aligned along XY grid (albeit
|
4411
|
+
# 'narrow'), i.e. if the angle is 90°.
|
4412
|
+
if angle.round(3) == (Math::PI/2).round(3)
|
4413
|
+
angle = 0 unless force
|
4414
|
+
end
|
4415
|
+
|
4286
4416
|
r = OpenStudio.createRotation(origin, axis, angle)
|
4287
4417
|
pts = to_p3Dv(r.inverse * pts)
|
4288
4418
|
box = to_p3Dv(r.inverse * box)
|
@@ -4291,13 +4421,13 @@ module OSut
|
|
4291
4421
|
xy = OpenStudio::Point3d.new(origin.x + dX, origin.y + dY, 0)
|
4292
4422
|
origin2 = xy - origin
|
4293
4423
|
t = OpenStudio.createTranslation(origin2)
|
4294
|
-
set = t.inverse * pts
|
4295
|
-
box = t.inverse * box
|
4424
|
+
set = to_p3Dv(t.inverse * pts)
|
4425
|
+
box = to_p3Dv(t.inverse * box)
|
4296
4426
|
bbox = outline([set])
|
4297
4427
|
|
4298
|
-
out[:set ] = set
|
4299
|
-
out[:box ] = box
|
4300
|
-
out[:bbox] = bbox
|
4428
|
+
out[:set ] = blc(set)
|
4429
|
+
out[:box ] = blc(box)
|
4430
|
+
out[:bbox] = blc(bbox)
|
4301
4431
|
out[:t ] = t
|
4302
4432
|
out[:r ] = r
|
4303
4433
|
out[:o ] = origin
|
@@ -4309,14 +4439,21 @@ module OSut
|
|
4309
4439
|
# Returns 'width' of a set of OpenStudio 3D points, once re/aligned.
|
4310
4440
|
#
|
4311
4441
|
# @param pts [Set<OpenStudio::Point3d>] 3D points, once re/aligned
|
4442
|
+
# @param force [Bool] whether to force rotation of (narrow) bounded box
|
4312
4443
|
#
|
4313
|
-
# @return [Float] width
|
4444
|
+
# @return [Float] width al©ong X-axis, once re/aligned
|
4314
4445
|
# @return [0.0] if invalid inputs
|
4315
|
-
def alignedWidth(pts = nil)
|
4446
|
+
def alignedWidth(pts = nil, force = false)
|
4447
|
+
mth = "OSut::#{__callee__}"
|
4316
4448
|
pts = poly(pts, false, true, true, true)
|
4317
4449
|
return 0 if pts.size < 2
|
4318
4450
|
|
4319
|
-
|
4451
|
+
unless [true, false].include?(force)
|
4452
|
+
log(DBG, "Ignoring force input (#{mth})")
|
4453
|
+
force = false
|
4454
|
+
end
|
4455
|
+
|
4456
|
+
pts = getRealignedFace(pts, force)[:set]
|
4320
4457
|
return 0 if pts.size < 2
|
4321
4458
|
|
4322
4459
|
pts.max_by(&:x).x - pts.min_by(&:x).x
|
@@ -4326,57 +4463,77 @@ module OSut
|
|
4326
4463
|
# Returns 'height' of a set of OpenStudio 3D points, once re/aligned.
|
4327
4464
|
#
|
4328
4465
|
# @param pts [Set<OpenStudio::Point3d>] 3D points, once re/aligned
|
4466
|
+
# @param force [Bool] whether to force rotation of (narrow) bounded box
|
4329
4467
|
#
|
4330
4468
|
# @return [Float] height along Y-axis, once re/aligned
|
4331
4469
|
# @return [0.0] if invalid inputs
|
4332
|
-
def alignedHeight(pts = nil)
|
4333
|
-
|
4470
|
+
def alignedHeight(pts = nil, force = false)
|
4471
|
+
mth = "OSut::#{__callee__}"
|
4472
|
+
pts = poly(pts, false, true, true, true)
|
4334
4473
|
return 0 if pts.size < 2
|
4335
4474
|
|
4336
|
-
|
4475
|
+
unless [true, false].include?(force)
|
4476
|
+
log(DBG, "Ignoring force input (#{mth})")
|
4477
|
+
force = false
|
4478
|
+
end
|
4479
|
+
|
4480
|
+
pts = getRealignedFace(pts, force)[:set]
|
4337
4481
|
return 0 if pts.size < 2
|
4338
4482
|
|
4339
4483
|
pts.max_by(&:y).y - pts.min_by(&:y).y
|
4340
4484
|
end
|
4341
4485
|
|
4342
4486
|
##
|
4343
|
-
#
|
4344
|
-
# (
|
4345
|
-
#
|
4346
|
-
#
|
4347
|
-
# (e.g.
|
4348
|
-
#
|
4349
|
-
#
|
4350
|
-
#
|
4351
|
-
#
|
4352
|
-
#
|
4353
|
-
#
|
4354
|
-
#
|
4355
|
-
#
|
4356
|
-
#
|
4357
|
-
#
|
4358
|
-
#
|
4359
|
-
#
|
4360
|
-
|
4487
|
+
# Identifies 'leader line anchors', i.e. specific 3D points of a (larger) set
|
4488
|
+
# (e.g. delineating a larger, parent polygon), each anchor linking the BLC
|
4489
|
+
# corner of one or more (smaller) subsets (free-floating within the parent)
|
4490
|
+
# - see follow-up 'genInserts'. Subsets may hold several 'tagged' vertices
|
4491
|
+
# (e.g. :box, :cbox). By default, the solution seeks to anchor subset :box
|
4492
|
+
# vertices. Users can select other tags, e.g. tag == :cbox. The solution
|
4493
|
+
# minimally validates individual subsets (e.g. no self-intersecting polygons,
|
4494
|
+
# coplanarity, no inter-subset conflicts, must fit within larger set).
|
4495
|
+
# Potential leader lines cannot intersect each other, similarly tagged subsets
|
4496
|
+
# or (parent) polygon edges. For highly-articulated cases (e.g. a narrow
|
4497
|
+
# parent polygon with multiple concavities, holding multiple subsets), such
|
4498
|
+
# leader line conflicts are likely unavoidable. It is recommended to first
|
4499
|
+
# sort subsets (e.g. areas), given the solution's 'first-come-first-served'
|
4500
|
+
# policy. Subsets without valid leader lines are ultimately ignored (check
|
4501
|
+
# for new set :void keys, see error logs). The larger set of points is
|
4502
|
+
# expected to be in space coordinates - not building or site coordinates,
|
4503
|
+
# while subset points are expected to 'fit?' in the larger set.
|
4504
|
+
#
|
4505
|
+
# @param s [Set<OpenStudio::Point3d>] a (larger) parent set of points
|
4506
|
+
# @param [Array<Hash>] set a collection of (smaller) sequenced points
|
4507
|
+
# @option [Symbol] tag sequence of subset vertices to target
|
4508
|
+
#
|
4509
|
+
# @return [Integer] number of successfully anchored subsets (see logs)
|
4510
|
+
def genAnchors(s = nil, set = [], tag = :box)
|
4361
4511
|
mth = "OSut::#{__callee__}"
|
4362
|
-
|
4363
|
-
t = nil
|
4512
|
+
n = 0
|
4364
4513
|
id = s.respond_to?(:nameString) ? "#{s.nameString}: " : ""
|
4365
4514
|
pts = poly(s)
|
4366
|
-
n
|
4367
|
-
return n if pts.empty?
|
4515
|
+
return invalid("#{id} polygon", mth, 1, DBG, n) if pts.empty?
|
4368
4516
|
return mismatch("set", set, Array, mth, DBG, n) unless set.respond_to?(:to_a)
|
4369
4517
|
|
4370
|
-
|
4518
|
+
origin = OpenStudio::Point3d.new(0,0,0)
|
4519
|
+
zenith = OpenStudio::Point3d.new(0,0,1)
|
4520
|
+
ray = zenith - origin
|
4521
|
+
set = set.to_a
|
4371
4522
|
|
4372
|
-
# Validate individual
|
4523
|
+
# Validate individual subsets. Purge surface-specific leader line anchors.
|
4373
4524
|
set.each_with_index do |st, i|
|
4374
|
-
str1 = id + "
|
4525
|
+
str1 = id + "subset ##{i+1}"
|
4375
4526
|
str2 = str1 + " #{tag.to_s}"
|
4376
4527
|
return mismatch(str1, st, Hash, mth, DBG, n) unless st.respond_to?(:key?)
|
4377
4528
|
return hashkey( str1, st, tag, mth, DBG, n) unless st.key?(tag)
|
4378
4529
|
return empty("#{str2} vertices", mth, DBG, n) if st[tag].empty?
|
4379
4530
|
|
4531
|
+
if st.key?(:out)
|
4532
|
+
return hashkey( str1, st, :t, mth, DBG, n) unless st.key?(:t)
|
4533
|
+
return hashkey( str1, st, :ti, mth, DBG, n) unless st.key?(:ti)
|
4534
|
+
return hashkey( str1, st, :t0, mth, DBG, n) unless st.key?(:t0)
|
4535
|
+
end
|
4536
|
+
|
4380
4537
|
stt = poly(st[tag])
|
4381
4538
|
return invalid("#{str2} polygon", mth, 0, DBG, n) if stt.empty?
|
4382
4539
|
return invalid("#{str2} gap", mth, 0, DBG, n) unless fits?(stt, pts, true)
|
@@ -4391,32 +4548,52 @@ module OSut
|
|
4391
4548
|
end
|
4392
4549
|
end
|
4393
4550
|
|
4394
|
-
|
4395
|
-
|
4396
|
-
|
4551
|
+
set.each_with_index do |st, i|
|
4552
|
+
# When a subset already holds a leader line anchor (from an initial call
|
4553
|
+
# to 'genAnchors'), it inherits key :out - a Hash holding (among others) a
|
4554
|
+
# 'realigned' set of points (by default a 'realigned' :box). The latter is
|
4555
|
+
# typically generated from an outdoor-facing roof (e.g. when called from
|
4556
|
+
# 'lights'). Subsequent calls to 'genAnchors' may send (as first
|
4557
|
+
# argument) a corresponding ceiling tile below (also from 'addSkylights').
|
4558
|
+
# Roof vs ceiling may neither share alignment transformation nor space
|
4559
|
+
# site transformation identities. All subsequent calls to 'genAnchors'
|
4560
|
+
# shall recover the :out points, apply a succession of de/alignments and
|
4561
|
+
# transformations in sync , and overwrite tagged points.
|
4562
|
+
#
|
4563
|
+
# Although 'genAnchors' and 'genInserts' have both been developed to
|
4564
|
+
# support anchor insertions in other cases (e.g. bay window in a wall),
|
4565
|
+
# variables and terminology here continue pertain to roofs, ceilings,
|
4566
|
+
# skylights and wells - less abstract, simpler to follow.
|
4567
|
+
if st.key?(:out)
|
4568
|
+
ti = st[:ti ] # unoccupied attic/plenum space site transformation
|
4569
|
+
t0 = st[:t0 ] # occupied space site transformation
|
4570
|
+
t = st[:t ] # initial alignment transformation of roof surface
|
4571
|
+
o = st[:out]
|
4572
|
+
tpts = t0.inverse * (ti * (t * (o[:r] * (o[:t] * o[:set]))))
|
4573
|
+
tpts = cast(tpts, pts, ray)
|
4574
|
+
|
4575
|
+
st[tag] = tpts
|
4397
4576
|
else
|
4398
|
-
|
4399
|
-
|
4577
|
+
st[:t] = OpenStudio::Transformation.alignFace(pts) unless st.key?(:t)
|
4578
|
+
tpts = st[:t].inverse * st[tag]
|
4579
|
+
o = getRealignedFace(tpts, true)
|
4580
|
+
tpts = st[:t] * (o[:r] * (o[:t] * o[:set]))
|
4581
|
+
|
4582
|
+
st[:out] = o
|
4583
|
+
st[tag ] = tpts
|
4400
4584
|
end
|
4401
|
-
else
|
4402
|
-
t = OpenStudio::Transformation.alignFace(pts)
|
4403
|
-
pts = t.inverse * pts
|
4404
4585
|
end
|
4405
4586
|
|
4406
|
-
#
|
4407
|
-
# anchor with shortest distance to first vertex of 'tagged' set.
|
4587
|
+
# Identify candidate leader line anchors for each subset.
|
4408
4588
|
set.each_with_index do |st, i|
|
4409
4589
|
candidates = []
|
4410
|
-
|
4411
|
-
|
4412
|
-
stt = dZ ? flatten(st[tag]).to_a : t.inverse * st[tag]
|
4413
|
-
p1 = stt.first
|
4590
|
+
tpts = st[tag]
|
4414
4591
|
|
4415
|
-
pts.
|
4416
|
-
ld
|
4417
|
-
nb
|
4592
|
+
pts.each do |pt|
|
4593
|
+
ld = [pt, tpts.first]
|
4594
|
+
nb = 0
|
4418
4595
|
|
4419
|
-
# Check for intersections between leader line and polygon edges.
|
4596
|
+
# Check for intersections between leader line and larger polygon edges.
|
4420
4597
|
getSegments(pts).each do |sg|
|
4421
4598
|
break unless nb.zero?
|
4422
4599
|
next if holds?(sg, pt)
|
@@ -4424,43 +4601,34 @@ module OSut
|
|
4424
4601
|
nb += 1 if lineIntersects?(sg, ld)
|
4425
4602
|
end
|
4426
4603
|
|
4427
|
-
|
4428
|
-
|
4429
|
-
# Check for intersections between candidate leader line and other sets.
|
4430
|
-
set.each_with_index do |other, j|
|
4604
|
+
# Check for intersections between candidate leader line vs other subsets.
|
4605
|
+
set.each do |other|
|
4431
4606
|
break unless nb.zero?
|
4432
|
-
next if
|
4607
|
+
next if st == other
|
4433
4608
|
|
4434
|
-
ost =
|
4435
|
-
sgj = getSegments(ost)
|
4609
|
+
ost = other[tag]
|
4436
4610
|
|
4437
|
-
|
4611
|
+
getSegments(ost).each { |sg| nb += 1 if lineIntersects?(ld, sg) }
|
4438
4612
|
end
|
4439
4613
|
|
4440
|
-
next unless nb.zero?
|
4441
|
-
|
4442
4614
|
# ... and previous leader lines (first come, first serve basis).
|
4443
|
-
set.
|
4615
|
+
set.each do |other|
|
4444
4616
|
break unless nb.zero?
|
4445
|
-
next if
|
4617
|
+
next if st == other
|
4618
|
+
next unless other.key?(:ld)
|
4446
4619
|
next unless other[:ld].key?(s)
|
4447
4620
|
|
4448
4621
|
ost = other[tag]
|
4449
|
-
|
4450
|
-
|
4451
|
-
ldj = dZ ? flatten([ old, pj ]) : t.inverse * [ old, pj ]
|
4622
|
+
pld = other[:ld][s]
|
4623
|
+
next if same?(pld, pt)
|
4452
4624
|
|
4453
|
-
|
4454
|
-
nb += 1 if lineIntersects?(ld, ldj)
|
4455
|
-
end
|
4625
|
+
nb += 1 if lineIntersects?(ld, [pld, ost.first])
|
4456
4626
|
end
|
4457
4627
|
|
4458
|
-
next unless nb.zero?
|
4459
|
-
|
4460
4628
|
# Finally, check for self-intersections.
|
4461
|
-
getSegments(
|
4629
|
+
getSegments(tpts).each do |sg|
|
4462
4630
|
break unless nb.zero?
|
4463
|
-
next if holds?(sg,
|
4631
|
+
next if holds?(sg, tpts.first)
|
4464
4632
|
|
4465
4633
|
nb += 1 if lineIntersects?(sg, ld)
|
4466
4634
|
nb += 1 if (sg.first - sg.last).cross(ld.first - ld.last).length < TOL
|
@@ -4471,18 +4639,13 @@ module OSut
|
|
4471
4639
|
|
4472
4640
|
if candidates.empty?
|
4473
4641
|
str = id + "set ##{i+1}"
|
4474
|
-
log(
|
4642
|
+
log(WRN, "#{str}: unable to anchor #{tag} leader line (#{mth})")
|
4475
4643
|
st[:void] = true
|
4476
4644
|
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
|
-
|
4645
|
+
p0 = candidates.sort_by { |pt| (pt - tpts.first).length }.first
|
4485
4646
|
n += 1
|
4647
|
+
|
4648
|
+
st[:ld][s] = p0
|
4486
4649
|
end
|
4487
4650
|
end
|
4488
4651
|
|
@@ -4490,17 +4653,19 @@ module OSut
|
|
4490
4653
|
end
|
4491
4654
|
|
4492
4655
|
##
|
4493
|
-
#
|
4494
|
-
#
|
4495
|
-
#
|
4496
|
-
#
|
4497
|
-
#
|
4498
|
-
#
|
4499
|
-
#
|
4656
|
+
# Extends (larger) polygon vertices to circumscribe one or more (smaller)
|
4657
|
+
# subsets of vertices, based on previously-generated 'leader line' anchors.
|
4658
|
+
# The solution minimally validates individual subsets (e.g. no
|
4659
|
+
# self-intersecting polygons, coplanarity, no inter-subset conflicts, must fit
|
4660
|
+
# within larger set). Valid leader line anchors (set key :ld) need to be
|
4661
|
+
# generated prior to calling the method - see 'genAnchors'. Subsets may hold
|
4662
|
+
# several 'tag'ged vertices (e.g. :box, :vtx). By default, the solution
|
4663
|
+
# seeks to anchor subset :vtx vertices. Users can select other tags, e.g.
|
4664
|
+
# tag == :box).
|
4500
4665
|
#
|
4501
4666
|
# @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
|
4667
|
+
# @param [Array<Hash>] set a collection of (smaller) sequenced vertices
|
4668
|
+
# @option set [Hash] :ld a polygon-specific leader line anchors
|
4504
4669
|
# @option [Symbol] tag sequence of set vertices to target
|
4505
4670
|
#
|
4506
4671
|
# @return [OpenStudio::Point3dVector] extended vertices (see logs if empty)
|
@@ -4519,8 +4684,9 @@ module OSut
|
|
4519
4684
|
|
4520
4685
|
# Validate individual sets.
|
4521
4686
|
set.each_with_index do |st, i|
|
4522
|
-
str1 = id + "
|
4687
|
+
str1 = id + "subset ##{i+1}"
|
4523
4688
|
str2 = str1 + " #{tag.to_s}"
|
4689
|
+
next if st.key?(:void) && st[:void]
|
4524
4690
|
return mismatch(str1, st, Hash, mth, DBG, a) unless st.respond_to?(:key?)
|
4525
4691
|
return hashkey( str1, st, tag, mth, DBG, a) unless st.key?(tag)
|
4526
4692
|
return empty("#{str2} vertices", mth, DBG, a) if st[tag].empty?
|
@@ -4540,7 +4706,8 @@ module OSut
|
|
4540
4706
|
v << pt
|
4541
4707
|
|
4542
4708
|
# Loop through each valid set; concatenate circumscribing vertices.
|
4543
|
-
set.
|
4709
|
+
set.each do |st|
|
4710
|
+
next if st.key?(:void) && st[:void]
|
4544
4711
|
next unless same?(st[:ld][s], pt)
|
4545
4712
|
next unless st.key?(tag)
|
4546
4713
|
|
@@ -4553,21 +4720,22 @@ module OSut
|
|
4553
4720
|
end
|
4554
4721
|
|
4555
4722
|
##
|
4556
|
-
# Generates
|
4557
|
-
#
|
4558
|
-
# (
|
4559
|
-
#
|
4560
|
-
#
|
4561
|
-
#
|
4562
|
-
#
|
4563
|
-
#
|
4564
|
-
# @param [
|
4565
|
-
# @
|
4566
|
-
# @option set [
|
4723
|
+
# Generates (1D or 2D) arrays of (smaller) rectangular collection of points,
|
4724
|
+
# (e.g. arrays of polygon inserts) from subset parameters, within a (larger)
|
4725
|
+
# set (e.g. parent polygon). If successful, each subset inherits additional
|
4726
|
+
# key:value pairs: namely :vtx (collection of circumscribing vertices), and
|
4727
|
+
# :vts (collection of individual insert vertices). Valid leader line anchors
|
4728
|
+
# (set key :ld) need to be generated prior to calling the solution
|
4729
|
+
# - see 'genAnchors'.
|
4730
|
+
#
|
4731
|
+
# @param s [Set<OpenStudio::Point3d>] a larger (parent) set of points
|
4732
|
+
# @param [Array<Hash>] set a collection of (smaller) sequenced vertices
|
4733
|
+
# @option set [Set<OpenStudio::Point3d>] :box bounding box of each subset
|
4734
|
+
# @option set [Hash] :ld a collection of leader line anchors
|
4567
4735
|
# @option set [Integer] :rows (1) number of rows of inserts
|
4568
4736
|
# @option set [Integer] :cols (1) number of columns of inserts
|
4569
|
-
# @option set [Numeric] :w0
|
4570
|
-
# @option set [Numeric] :d0
|
4737
|
+
# @option set [Numeric] :w0 width of individual inserts (wrt cols) min 0.4
|
4738
|
+
# @option set [Numeric] :d0 depth of individual inserts (wrt rows) min 0.4
|
4571
4739
|
# @option set [Numeric] :dX (0) optional left/right X-axis buffer
|
4572
4740
|
# @option set [Numeric] :dY (0) optional top/bottom Y-axis buffer
|
4573
4741
|
#
|
@@ -4587,10 +4755,12 @@ module OSut
|
|
4587
4755
|
|
4588
4756
|
# Validate/reset individual set collections.
|
4589
4757
|
set.each_with_index do |st, i|
|
4590
|
-
str1 = id + "
|
4758
|
+
str1 = id + "subset ##{i+1}"
|
4759
|
+
next if st.key?(:void) && st[:void]
|
4591
4760
|
return mismatch(str1, st, Hash, mth, DBG, a) unless st.respond_to?(:key?)
|
4592
4761
|
return hashkey( str1, st, :box, mth, DBG, a) unless st.key?(:box)
|
4593
4762
|
return hashkey( str1, st, :ld, mth, DBG, a) unless st.key?(:ld)
|
4763
|
+
return hashkey( str1, st, :out, mth, DBG, a) unless st.key?(:out)
|
4594
4764
|
|
4595
4765
|
str2 = str1 + " anchor"
|
4596
4766
|
ld = st[:ld]
|
@@ -4599,7 +4769,7 @@ module OSut
|
|
4599
4769
|
return mismatch(str2, ld[s], cl, mth, DBG, a) unless ld[s].is_a?(cl)
|
4600
4770
|
|
4601
4771
|
# Ensure each set bounding box is safely within larger polygon boundaries.
|
4602
|
-
#
|
4772
|
+
# @todo: In line with related addSkylights' @todo, expand solution to
|
4603
4773
|
# safely handle 'side' cutouts (i.e. no need for leader lines). In
|
4604
4774
|
# so doing, boxes could eventually align along surface edges.
|
4605
4775
|
str3 = str1 + " box"
|
@@ -4659,9 +4829,10 @@ module OSut
|
|
4659
4829
|
end
|
4660
4830
|
end
|
4661
4831
|
|
4662
|
-
# Flag conflicts between set bounding boxes.
|
4832
|
+
# Flag conflicts between set bounding boxes. @todo: ease up for ridges.
|
4663
4833
|
set.each_with_index do |st, i|
|
4664
4834
|
bx = st[:box]
|
4835
|
+
next if st.key?(:void) && st[:void]
|
4665
4836
|
|
4666
4837
|
set.each_with_index do |other, j|
|
4667
4838
|
next if i == j
|
@@ -4673,37 +4844,18 @@ module OSut
|
|
4673
4844
|
end
|
4674
4845
|
end
|
4675
4846
|
|
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]))
|
4847
|
+
t = OpenStudio::Transformation.alignFace(pts)
|
4848
|
+
rpts = t.inverse * pts
|
4702
4849
|
|
4850
|
+
# Loop through each 'valid' subset (i.e. linking a valid leader line anchor),
|
4851
|
+
# generate subset vertex array based on user-provided specs.
|
4852
|
+
set.each_with_index do |st, i|
|
4853
|
+
str = id + "subset ##{i+1}"
|
4854
|
+
next if st.key?(:void) && st[:void]
|
4703
4855
|
|
4856
|
+
o = st[:out]
|
4704
4857
|
vts = {} # collection of individual (named) polygon insert vertices
|
4705
4858
|
vtx = [] # sequence of circumscribing polygon vertices
|
4706
|
-
|
4707
4859
|
bx = o[:set]
|
4708
4860
|
w = width(bx) # overall sandbox width
|
4709
4861
|
d = height(bx) # overall sandbox depth
|
@@ -4712,7 +4864,7 @@ module OSut
|
|
4712
4864
|
cols = st[:cols] # number of array columns
|
4713
4865
|
rows = st[:rows] # number of array rows
|
4714
4866
|
x = st[:w0 ] # width of individual insert
|
4715
|
-
y = st[:d0 ] # depth of
|
4867
|
+
y = st[:d0 ] # depth of individual insert
|
4716
4868
|
gX = 0 # gap between insert columns
|
4717
4869
|
gY = 0 # gap between insert rows
|
4718
4870
|
|
@@ -4790,15 +4942,7 @@ module OSut
|
|
4790
4942
|
vec << OpenStudio::Point3d.new(xC + x, yC , 0)
|
4791
4943
|
|
4792
4944
|
# 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
|
4945
|
+
vts[nom] = to_p3Dv(t * ulc(o[:r] * (o[:t] * vec)))
|
4802
4946
|
|
4803
4947
|
# Add reverse vertices, circumscribing each insert.
|
4804
4948
|
vec.reverse!
|
@@ -4814,18 +4958,8 @@ module OSut
|
|
4814
4958
|
end
|
4815
4959
|
end
|
4816
4960
|
|
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
4961
|
st[:vts] = vts
|
4828
|
-
st[:vtx] = vtx
|
4962
|
+
st[:vtx] = to_p3Dv(t * (o[:r] * (o[:t] * vtx)))
|
4829
4963
|
end
|
4830
4964
|
|
4831
4965
|
# Extended vertex sequence of the larger polygon.
|
@@ -4835,9 +4969,11 @@ module OSut
|
|
4835
4969
|
##
|
4836
4970
|
# Returns an array of OpenStudio space surfaces or subsurfaces that match
|
4837
4971
|
# criteria, e.g. exterior, north-east facing walls in hotel "lobby". Note that
|
4838
|
-
# 'sides' rely on space coordinates (not
|
4972
|
+
# 'sides' rely on space coordinates (not building or site coordinates). Also,
|
4839
4973
|
# 'sides' are exclusive (not inclusive), e.g. walls strictly north-facing or
|
4840
4974
|
# strictly east-facing would not be returned if 'sides' holds [:north, :east].
|
4975
|
+
# No outside boundary condition filters if 'boundary' argument == "all". No
|
4976
|
+
# surface type filters if 'type' argument == "all".
|
4841
4977
|
#
|
4842
4978
|
# @param spaces [Set<OpenStudio::Model::Space>] target spaces
|
4843
4979
|
# @param boundary [#to_s] OpenStudio outside boundary condition
|
@@ -4845,7 +4981,7 @@ module OSut
|
|
4845
4981
|
# @param sides [Set<Symbols>] direction keys, e.g. :north (see OSut::SIDZ)
|
4846
4982
|
#
|
4847
4983
|
# @return [Array<OpenStudio::Model::Surface>] surfaces (may be empty, no logs)
|
4848
|
-
def facets(spaces = [], boundary = "
|
4984
|
+
def facets(spaces = [], boundary = "all", type = "all", sides = [])
|
4849
4985
|
spaces = spaces.is_a?(OpenStudio::Model::Space) ? [spaces] : spaces
|
4850
4986
|
spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
|
4851
4987
|
return [] if spaces.empty?
|
@@ -4859,8 +4995,8 @@ module OSut
|
|
4859
4995
|
return [] if boundary.empty?
|
4860
4996
|
return [] if type.empty?
|
4861
4997
|
|
4862
|
-
# Filter sides. If sides is initially empty, return all surfaces of
|
4863
|
-
# type and outside boundary condition.
|
4998
|
+
# Filter sides. If 'sides' is initially empty, return all surfaces of
|
4999
|
+
# matching type and outside boundary condition.
|
4864
5000
|
unless sides.empty?
|
4865
5001
|
sides = sides.select { |side| SIDZ.include?(side) }
|
4866
5002
|
return [] if sides.empty?
|
@@ -4870,8 +5006,13 @@ module OSut
|
|
4870
5006
|
return [] unless space.respond_to?(:setSpaceType)
|
4871
5007
|
|
4872
5008
|
space.surfaces.each do |s|
|
4873
|
-
|
4874
|
-
|
5009
|
+
unless boundary == "all"
|
5010
|
+
next unless s.outsideBoundaryCondition.downcase == boundary
|
5011
|
+
end
|
5012
|
+
|
5013
|
+
unless type == "all"
|
5014
|
+
next unless s.surfaceType.downcase == type
|
5015
|
+
end
|
4875
5016
|
|
4876
5017
|
if sides.empty?
|
4877
5018
|
faces << s
|
@@ -4891,13 +5032,15 @@ module OSut
|
|
4891
5032
|
|
4892
5033
|
# SubSurfaces?
|
4893
5034
|
spaces.each do |space|
|
4894
|
-
break unless faces.empty?
|
4895
|
-
|
4896
5035
|
space.surfaces.each do |s|
|
4897
|
-
|
5036
|
+
unless boundary == "all"
|
5037
|
+
next unless s.outsideBoundaryCondition.downcase == boundary
|
5038
|
+
end
|
4898
5039
|
|
4899
5040
|
s.subSurfaces.each do |sub|
|
4900
|
-
|
5041
|
+
unless type == "all"
|
5042
|
+
next unless sub.subSurfaceType.downcase == type
|
5043
|
+
end
|
4901
5044
|
|
4902
5045
|
if sides.empty?
|
4903
5046
|
faces << sub
|
@@ -5012,7 +5155,9 @@ module OSut
|
|
5012
5155
|
# Returns outdoor-facing, space-related roof surfaces. These include
|
5013
5156
|
# outdoor-facing roofs of each space per se, as well as any outdoor-facing
|
5014
5157
|
# roof surface of unoccupied spaces immediately above (e.g. plenums, attics)
|
5015
|
-
# overlapping any of the ceiling surfaces of each space.
|
5158
|
+
# overlapping any of the ceiling surfaces of each space. It does not include
|
5159
|
+
# surfaces labelled as 'RoofCeiling', which do not comply with ASHRAE 90.1 or
|
5160
|
+
# NECB tilt criteria - see 'roof?'.
|
5016
5161
|
#
|
5017
5162
|
# @param spaces [Set<OpenStudio::Model::Space>] target spaces
|
5018
5163
|
#
|
@@ -5028,12 +5173,12 @@ module OSut
|
|
5028
5173
|
|
5029
5174
|
# Space-specific outdoor-facing roof surfaces.
|
5030
5175
|
roofs = facets(spaces, "Outdoors", "RoofCeiling")
|
5176
|
+
roofs = roofs.select { |roof| roof?(roof) }
|
5031
5177
|
|
5032
|
-
# Outdoor-facing roof surfaces of unoccupied plenums or attics above?
|
5033
5178
|
spaces.each do |space|
|
5034
|
-
# When
|
5179
|
+
# When unoccupied spaces are involved (e.g. plenums, attics), the target
|
5035
5180
|
# space may not share the same local transformation as the space(s) above.
|
5036
|
-
# Fetching
|
5181
|
+
# Fetching site transformation.
|
5037
5182
|
t0 = transforms(space)
|
5038
5183
|
next unless t0[:t]
|
5039
5184
|
|
@@ -5056,8 +5201,10 @@ module OSut
|
|
5056
5201
|
|
5057
5202
|
ti = ti[:t]
|
5058
5203
|
|
5059
|
-
#
|
5204
|
+
# @todo: recursive call for stacked spaces as atria (via AirBoundaries).
|
5060
5205
|
facets(other, "Outdoors", "RoofCeiling").each do |ruf|
|
5206
|
+
next unless roof?(ruf)
|
5207
|
+
|
5061
5208
|
rvi = ti * ruf.vertices
|
5062
5209
|
cst = cast(cv0, rvi, up)
|
5063
5210
|
next unless overlaps?(cst, rvi, false)
|
@@ -5128,11 +5275,13 @@ module OSut
|
|
5128
5275
|
# @option subs [#to_f] :r_buffer gap between sub/array and right corner
|
5129
5276
|
# @option subs [#to_f] :l_buffer gap between sub/array and left corner
|
5130
5277
|
# @param clear [Bool] whether to remove current sub surfaces
|
5278
|
+
# @param bound [Bool] whether to add subs wrt surface's bounded box
|
5279
|
+
# @param realign [Bool] whether to first realign bounded box
|
5131
5280
|
# @param bfr [#to_f] safety buffer, to maintain near other edges
|
5132
5281
|
#
|
5133
5282
|
# @return [Bool] whether addition is successful
|
5134
5283
|
# @return [false] if invalid input (see logs)
|
5135
|
-
def addSubs(s = nil, subs = [], clear = false, bfr = 0.005)
|
5284
|
+
def addSubs(s = nil, subs = [], clear = false, bound = false, realign = false, bfr = 0.005)
|
5136
5285
|
mth = "OSut::#{__callee__}"
|
5137
5286
|
v = OpenStudio.openStudioVersion.split(".").join.to_i
|
5138
5287
|
cl1 = OpenStudio::Model::Surface
|
@@ -5144,15 +5293,18 @@ module OSut
|
|
5144
5293
|
|
5145
5294
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5146
5295
|
# Exit if mismatched or invalid argument classes.
|
5147
|
-
|
5148
|
-
|
5296
|
+
sbs = subs.is_a?(cl3) ? [subs] : subs
|
5297
|
+
sbs = sbs.respond_to?(:to_a) ? sbs.to_a : []
|
5298
|
+
return mismatch("surface", s, cl1, mth, DBG, no) unless s.is_a?(cl1)
|
5299
|
+
return mismatch("subs", subs, cl2, mth, DBG, no) if sbs.empty?
|
5149
5300
|
return empty("surface points", mth, DBG, no) if poly(s).empty?
|
5150
5301
|
|
5151
|
-
|
5152
|
-
|
5153
|
-
|
5154
|
-
mdl = s.model
|
5302
|
+
subs = sbs
|
5303
|
+
nom = s.nameString
|
5304
|
+
mdl = s.model
|
5155
5305
|
|
5306
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5307
|
+
# Purge existing sub surfaces?
|
5156
5308
|
unless [true, false].include?(clear)
|
5157
5309
|
log(WRN, "#{nom}: Keeping existing sub surfaces (#{mth})")
|
5158
5310
|
clear = false
|
@@ -5160,6 +5312,27 @@ module OSut
|
|
5160
5312
|
|
5161
5313
|
s.subSurfaces.map(&:remove) if clear
|
5162
5314
|
|
5315
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5316
|
+
# Add sub surfaces with respect to base surface's bounded box? This is
|
5317
|
+
# often useful (in some cases necessary) with irregular or concave surfaces.
|
5318
|
+
# If true, sub surface parameters (e.g. height, offset, centreline) no
|
5319
|
+
# longer apply to the original surface 'bounding' box, but instead to its
|
5320
|
+
# largest 'bounded' box. This can be combined with the 'realign' parameter.
|
5321
|
+
unless [true, false].include?(bound)
|
5322
|
+
log(WRN, "#{nom}: Ignoring bounded box (#{mth})")
|
5323
|
+
bound = false
|
5324
|
+
end
|
5325
|
+
|
5326
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5327
|
+
# Force re-alignment of base surface (or its 'bounded' box)? False by
|
5328
|
+
# default (ideal for vertical/tilted walls & sloped roofs). If set to true
|
5329
|
+
# for a narrow wall for instance, an array of sub surfaces will be added
|
5330
|
+
# from bottom to top (rather from left to right).
|
5331
|
+
unless [true, false].include?(realign)
|
5332
|
+
log(WRN, "#{nom}: Ignoring realignment (#{mth})")
|
5333
|
+
realign = false
|
5334
|
+
end
|
5335
|
+
|
5163
5336
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5164
5337
|
# Ensure minimum safety buffer.
|
5165
5338
|
if bfr.respond_to?(:to_f)
|
@@ -5192,15 +5365,21 @@ module OSut
|
|
5192
5365
|
s0 = poly(s, false, false, false, t, :ulc)
|
5193
5366
|
s00 = nil
|
5194
5367
|
|
5195
|
-
if
|
5196
|
-
|
5197
|
-
|
5368
|
+
# Adapt sandbox if user selects to 'bound' and/or 'realign'.
|
5369
|
+
if bound
|
5370
|
+
box = boundedBox(s0)
|
5198
5371
|
|
5199
|
-
|
5372
|
+
if realign
|
5373
|
+
s00 = getRealignedFace(box, true)
|
5374
|
+
return invalid("bound realignment", mth, 0, DBG, false) unless s00[:set]
|
5375
|
+
end
|
5376
|
+
elsif realign
|
5377
|
+
s00 = getRealignedFace(s0, false)
|
5378
|
+
return invalid("unbound realignment", mth, 0, DBG, false) unless s00[:set]
|
5200
5379
|
end
|
5201
5380
|
|
5202
|
-
max_x = width(s0)
|
5203
|
-
max_y = height(s0)
|
5381
|
+
max_x = s00 ? width( s00[:set]) : width(s0)
|
5382
|
+
max_y = s00 ? height(s00[:set]) : height(s0)
|
5204
5383
|
mid_x = max_x / 2
|
5205
5384
|
mid_y = max_y / 2
|
5206
5385
|
|
@@ -5219,7 +5398,7 @@ module OSut
|
|
5219
5398
|
sub[:type ] = trim(sub[:type])
|
5220
5399
|
sub[:id ] = trim(sub[:id])
|
5221
5400
|
sub[:type ] = type if sub[:type].empty?
|
5222
|
-
sub[:id ] = "OSut
|
5401
|
+
sub[:id ] = "OSut:#{nom}:#{index}" if sub[:id ].empty?
|
5223
5402
|
sub[:count ] = 1 unless sub[:count ].respond_to?(:to_i)
|
5224
5403
|
sub[:multiplier] = 1 unless sub[:multiplier].respond_to?(:to_i)
|
5225
5404
|
sub[:count ] = sub[:count ].to_i
|
@@ -5271,8 +5450,10 @@ module OSut
|
|
5271
5450
|
next if key == :frame
|
5272
5451
|
next if key == :assembly
|
5273
5452
|
|
5274
|
-
|
5275
|
-
|
5453
|
+
unless value.respond_to?(:to_f)
|
5454
|
+
return mismatch(key, value, Float, mth, DBG, no)
|
5455
|
+
end
|
5456
|
+
|
5276
5457
|
next if key == :centreline
|
5277
5458
|
|
5278
5459
|
negative(key, mth, WRN) if value < 0
|
@@ -5329,27 +5510,27 @@ module OSut
|
|
5329
5510
|
# Log/reset "height" if beyond min/max.
|
5330
5511
|
if sub.key?(:height)
|
5331
5512
|
unless sub[:height].between?(glass - TOL2, max_height + TOL2)
|
5332
|
-
|
5333
|
-
sub[:height] =
|
5334
|
-
log(WRN, "
|
5513
|
+
log(WRN, "Reset '#{id}' height #{sub[:height].round(3)}m (#{mth})")
|
5514
|
+
sub[:height] = sub[:height].clamp(glass, max_height)
|
5515
|
+
log(WRN, "Height '#{id}' reset to #{sub[:height].round(3)}m (#{mth})")
|
5335
5516
|
end
|
5336
5517
|
end
|
5337
5518
|
|
5338
5519
|
# Log/reset "head" height if beyond min/max.
|
5339
5520
|
if sub.key?(:head)
|
5340
5521
|
unless sub[:head].between?(min_head - TOL2, max_head + TOL2)
|
5341
|
-
|
5342
|
-
sub[:head] =
|
5343
|
-
log(WRN, "
|
5522
|
+
log(WRN, "Reset '#{id}' head #{sub[:head].round(3)}m (#{mth})")
|
5523
|
+
sub[:head] = sub[:head].clamp(min_head, max_head)
|
5524
|
+
log(WRN, "Head '#{id}' reset to #{sub[:head].round(3)}m (#{mth})")
|
5344
5525
|
end
|
5345
5526
|
end
|
5346
5527
|
|
5347
5528
|
# Log/reset "sill" height if beyond min/max.
|
5348
5529
|
if sub.key?(:sill)
|
5349
5530
|
unless sub[:sill].between?(min_sill - TOL2, max_sill + TOL2)
|
5350
|
-
|
5351
|
-
sub[:sill] =
|
5352
|
-
log(WRN, "
|
5531
|
+
log(WRN, "Reset '#{id}' sill #{sub[:sill].round(3)}m (#{mth})")
|
5532
|
+
sub[:sill] = sub[:sill].clamp(min_sill, max_sill)
|
5533
|
+
log(WRN, "Sill '#{id}' reset to #{sub[:sill].round(3)}m (#{mth})")
|
5353
5534
|
end
|
5354
5535
|
end
|
5355
5536
|
|
@@ -5368,8 +5549,9 @@ module OSut
|
|
5368
5549
|
log(ERR, "Skip: invalid '#{id}' head/sill combo (#{mth})")
|
5369
5550
|
next
|
5370
5551
|
else
|
5552
|
+
log(WRN, "(Re)set '#{id}' sill #{sub[:sill].round(3)}m (#{mth})")
|
5371
5553
|
sub[:sill] = sill
|
5372
|
-
log(WRN, "
|
5554
|
+
log(WRN, "Sill '#{id}' (re)set to #{sub[:sill].round(3)}m (#{mth})")
|
5373
5555
|
end
|
5374
5556
|
end
|
5375
5557
|
|
@@ -5379,7 +5561,8 @@ module OSut
|
|
5379
5561
|
height = sub[:head] - sub[:sill]
|
5380
5562
|
|
5381
5563
|
if sub.key?(:height) && (sub[:height] - height).abs > TOL2
|
5382
|
-
log(WRN, "(Re)set '#{id}' height
|
5564
|
+
log(WRN, "(Re)set '#{id}' height #{sub[:height].round(3)}m (#{mth})")
|
5565
|
+
log(WRN, "Height '#{id}' (re)set to #{height.round(3)}m (#{mth})")
|
5383
5566
|
end
|
5384
5567
|
|
5385
5568
|
sub[:height] = height
|
@@ -5400,9 +5583,10 @@ module OSut
|
|
5400
5583
|
log(ERR, "Skip: invalid '#{id}' head/height combo (#{mth})")
|
5401
5584
|
next
|
5402
5585
|
else
|
5586
|
+
log(WRN, "(Re)set '#{id}' height #{sub[:height].round(3)}m (#{mth})")
|
5403
5587
|
sub[:sill ] = sill
|
5404
5588
|
sub[:height] = height
|
5405
|
-
log(WRN, "
|
5589
|
+
log(WRN, "Height '#{id}' re(set) #{sub[:height].round(3)}m (#{mth})")
|
5406
5590
|
end
|
5407
5591
|
else
|
5408
5592
|
sub[:sill] = sill
|
@@ -5428,9 +5612,10 @@ module OSut
|
|
5428
5612
|
log(ERR, "Skip: invalid '#{id}' sill/height combo (#{mth})")
|
5429
5613
|
next
|
5430
5614
|
else
|
5615
|
+
log(WRN, "(Re)set '#{id}' height #{sub[:height].round(3)}m (#{mth})")
|
5431
5616
|
sub[:head ] = head
|
5432
5617
|
sub[:height] = height
|
5433
|
-
log(WRN, "
|
5618
|
+
log(WRN, "Height '#{id}' reset to #{sub[:height].round(3)}m (#{mth})")
|
5434
5619
|
end
|
5435
5620
|
else
|
5436
5621
|
sub[:head] = head
|
@@ -5459,9 +5644,9 @@ module OSut
|
|
5459
5644
|
# Log/reset "width" if beyond min/max.
|
5460
5645
|
if sub.key?(:width)
|
5461
5646
|
unless sub[:width].between?(glass - TOL2, max_width + TOL2)
|
5462
|
-
|
5463
|
-
sub[:width] =
|
5464
|
-
log(WRN, "
|
5647
|
+
log(WRN, "Reset '#{id}' width #{sub[:width].round(3)}m (#{mth})")
|
5648
|
+
sub[:width] = sub[:width].clamp(glass, max_width)
|
5649
|
+
log(WRN, "Width '#{id}' reset to #{sub[:width].round(3)}m (#{mth})")
|
5465
5650
|
end
|
5466
5651
|
end
|
5467
5652
|
|
@@ -5471,7 +5656,7 @@ module OSut
|
|
5471
5656
|
|
5472
5657
|
if sub[:count] < 1
|
5473
5658
|
sub[:count] = 1
|
5474
|
-
log(WRN, "Reset '#{id}' count to
|
5659
|
+
log(WRN, "Reset '#{id}' count to min 1 (#{mth})")
|
5475
5660
|
end
|
5476
5661
|
else
|
5477
5662
|
sub[:count] = 1
|
@@ -5482,16 +5667,18 @@ module OSut
|
|
5482
5667
|
# Log/reset if left-sided buffer under min jamb position.
|
5483
5668
|
if sub.key?(:l_buffer)
|
5484
5669
|
if sub[:l_buffer] < min_ljamb - TOL
|
5670
|
+
log(WRN, "Reset '#{id}' left buffer #{sub[:l_buffer].round(3)}m (#{mth})")
|
5485
5671
|
sub[:l_buffer] = min_ljamb
|
5486
|
-
log(WRN, "
|
5672
|
+
log(WRN, "Left buffer '#{id}' reset to #{sub[:l_buffer].round(3)}m (#{mth})")
|
5487
5673
|
end
|
5488
5674
|
end
|
5489
5675
|
|
5490
5676
|
# Log/reset if right-sided buffer beyond max jamb position.
|
5491
5677
|
if sub.key?(:r_buffer)
|
5492
5678
|
if sub[:r_buffer] > max_rjamb - TOL
|
5679
|
+
log(WRN, "Reset '#{id}' right buffer #{sub[:r_buffer].round(3)}m (#{mth})")
|
5493
5680
|
sub[:r_buffer] = min_rjamb
|
5494
|
-
log(WRN, "
|
5681
|
+
log(WRN, "Right buffer '#{id}' reset to #{sub[:r_buffer].round(3)}m (#{mth})")
|
5495
5682
|
end
|
5496
5683
|
end
|
5497
5684
|
|
@@ -5511,15 +5698,15 @@ module OSut
|
|
5511
5698
|
sub[:multiplier] = 0
|
5512
5699
|
sub[:height ] = 0 if sub.key?(:height)
|
5513
5700
|
sub[:width ] = 0 if sub.key?(:width)
|
5514
|
-
log(ERR, "Skip:
|
5701
|
+
log(ERR, "Skip: ratio ~0 (#{mth})")
|
5515
5702
|
next
|
5516
5703
|
end
|
5517
5704
|
|
5518
5705
|
# Log/reset if "ratio" beyond min/max?
|
5519
5706
|
unless sub[:ratio].between?(min, max)
|
5520
|
-
|
5521
|
-
sub[:ratio] =
|
5522
|
-
log(WRN, "
|
5707
|
+
log(WRN, "Reset ratio #{sub[:ratio].round(3)} (#{mth})")
|
5708
|
+
sub[:ratio] = sub[:ratio].clamp(min, max)
|
5709
|
+
log(WRN, "Ratio reset to #{sub[:ratio].round(3)} (#{mth})")
|
5523
5710
|
end
|
5524
5711
|
|
5525
5712
|
# Log/reset "count" unless 1.
|
@@ -5536,7 +5723,7 @@ module OSut
|
|
5536
5723
|
|
5537
5724
|
if sub.key?(:l_buffer)
|
5538
5725
|
if sub.key?(:centreline)
|
5539
|
-
log(WRN, "Skip #{id} left buffer (vs centreline) (#{mth})")
|
5726
|
+
log(WRN, "Skip '#{id}' left buffer (vs centreline) (#{mth})")
|
5540
5727
|
else
|
5541
5728
|
x0 = sub[:l_buffer] - frame
|
5542
5729
|
xf = x0 + w
|
@@ -5544,7 +5731,7 @@ module OSut
|
|
5544
5731
|
end
|
5545
5732
|
elsif sub.key?(:r_buffer)
|
5546
5733
|
if sub.key?(:centreline)
|
5547
|
-
log(WRN, "Skip #{id} right buffer (vs centreline) (#{mth})")
|
5734
|
+
log(WRN, "Skip '#{id}' right buffer (vs centreline) (#{mth})")
|
5548
5735
|
else
|
5549
5736
|
xf = max_x - sub[:r_buffer] + frame
|
5550
5737
|
x0 = xf - w
|
@@ -5559,13 +5746,14 @@ module OSut
|
|
5559
5746
|
sub[:multiplier] = 0
|
5560
5747
|
sub[:height ] = 0 if sub.key?(:height)
|
5561
5748
|
sub[:width ] = 0 if sub.key?(:width)
|
5562
|
-
log(ERR, "Skip: invalid (ratio) width/centreline (#{mth})")
|
5749
|
+
log(ERR, "Skip '#{id}': invalid (ratio) width/centreline (#{mth})")
|
5563
5750
|
next
|
5564
5751
|
end
|
5565
5752
|
|
5566
5753
|
if sub.key?(:width) && (sub[:width] - width).abs > TOL
|
5754
|
+
log(WRN, "Reset '#{id}' width (ratio) #{sub[:width].round(2)}m (#{mth})")
|
5567
5755
|
sub[:width] = width
|
5568
|
-
log(WRN, "
|
5756
|
+
log(WRN, "Width (ratio) '#{id}' reset to #{sub[:width].round(2)}m (#{mth})")
|
5569
5757
|
end
|
5570
5758
|
|
5571
5759
|
sub[:width] = width unless sub.key?(:width)
|
@@ -5583,12 +5771,13 @@ module OSut
|
|
5583
5771
|
width = sub[:width] + frames
|
5584
5772
|
gap = (max_x - n * width) / (n + 1)
|
5585
5773
|
gap = sub[:offset] - width if sub.key?(:offset)
|
5586
|
-
gap = 0 if gap <
|
5774
|
+
gap = 0 if gap < buffer
|
5587
5775
|
offset = gap + width
|
5588
5776
|
|
5589
5777
|
if sub.key?(:offset) && (offset - sub[:offset]).abs > TOL
|
5778
|
+
log(WRN, "Reset '#{id}' sub offset #{sub[:offset].round(2)}m (#{mth})")
|
5590
5779
|
sub[:offset] = offset
|
5591
|
-
log(WRN, "
|
5780
|
+
log(WRN, "Sub offset (#{id}) reset to #{sub[:offset].round(2)}m (#{mth})")
|
5592
5781
|
end
|
5593
5782
|
|
5594
5783
|
sub[:offset] = offset unless sub.key?(:offset)
|
@@ -5600,7 +5789,7 @@ module OSut
|
|
5600
5789
|
|
5601
5790
|
if sub.key?(:l_buffer)
|
5602
5791
|
if sub.key?(:centreline)
|
5603
|
-
log(WRN, "Skip #{id} left buffer (vs centreline) (#{mth})")
|
5792
|
+
log(WRN, "Skip '#{id}' left buffer (vs centreline) (#{mth})")
|
5604
5793
|
else
|
5605
5794
|
x0 = sub[:l_buffer] - frame
|
5606
5795
|
xf = x0 + w
|
@@ -5608,7 +5797,7 @@ module OSut
|
|
5608
5797
|
end
|
5609
5798
|
elsif sub.key?(:r_buffer)
|
5610
5799
|
if sub.key?(:centreline)
|
5611
|
-
log(WRN, "Skip #{id} right buffer (vs centreline) (#{mth})")
|
5800
|
+
log(WRN, "Skip '#{id}' right buffer (vs centreline) (#{mth})")
|
5612
5801
|
else
|
5613
5802
|
xf = max_x - sub[:r_buffer] + frame
|
5614
5803
|
x0 = xf - w
|
@@ -5617,7 +5806,7 @@ module OSut
|
|
5617
5806
|
end
|
5618
5807
|
|
5619
5808
|
# Too wide?
|
5620
|
-
if x0 <
|
5809
|
+
if x0 < buffer - TOL2 || xf > max_x - buffer - TOL2
|
5621
5810
|
sub[:ratio ] = 0 if sub.key?(:ratio)
|
5622
5811
|
sub[:count ] = 0
|
5623
5812
|
sub[:multiplier] = 0
|
@@ -5633,10 +5822,9 @@ module OSut
|
|
5633
5822
|
|
5634
5823
|
# Generate sub(s).
|
5635
5824
|
sub[:count].times do |i|
|
5636
|
-
name = "#{id}
|
5825
|
+
name = "#{id}:#{i}"
|
5637
5826
|
fr = 0
|
5638
5827
|
fr = sub[:frame].frameWidth if sub[:frame]
|
5639
|
-
|
5640
5828
|
vec = OpenStudio::Point3dVector.new
|
5641
5829
|
vec << OpenStudio::Point3d.new(pos, sub[:head], 0)
|
5642
5830
|
vec << OpenStudio::Point3d.new(pos, sub[:sill], 0)
|
@@ -5647,34 +5835,41 @@ module OSut
|
|
5647
5835
|
# Log/skip if conflict between individual sub and base surface.
|
5648
5836
|
vc = vec
|
5649
5837
|
vc = offset(vc, fr, 300) if fr > 0
|
5650
|
-
ok = fits?(vc, s)
|
5651
5838
|
|
5652
|
-
|
5653
|
-
|
5839
|
+
unless fits?(vc, s)
|
5840
|
+
log(ERR, "Skip '#{name}': won't fit in '#{nom}' (#{mth})")
|
5841
|
+
break
|
5842
|
+
end
|
5654
5843
|
|
5655
5844
|
# Log/skip if conflicts with existing subs (even if same array).
|
5845
|
+
conflict = false
|
5846
|
+
|
5656
5847
|
s.subSurfaces.each do |sb|
|
5657
5848
|
nome = sb.nameString
|
5658
5849
|
fd = sb.windowPropertyFrameAndDivider
|
5659
|
-
fr = 0
|
5660
|
-
fr = fd.get.frameWidth unless fd.empty?
|
5850
|
+
fr = fd.empty? ? 0 : fd.get.frameWidth
|
5661
5851
|
vk = sb.vertices
|
5662
5852
|
vk = offset(vk, fr, 300) if fr > 0
|
5663
|
-
|
5664
|
-
|
5665
|
-
|
5666
|
-
|
5853
|
+
|
5854
|
+
if overlaps?(vc, vk)
|
5855
|
+
log(ERR, "Skip '#{name}': overlaps '#{nome}' (#{mth})")
|
5856
|
+
conflict = true
|
5857
|
+
break
|
5858
|
+
end
|
5667
5859
|
end
|
5668
5860
|
|
5669
|
-
break
|
5861
|
+
break if conflict
|
5670
5862
|
|
5671
5863
|
sb = OpenStudio::Model::SubSurface.new(vec, mdl)
|
5672
5864
|
sb.setName(name)
|
5673
5865
|
sb.setSubSurfaceType(sub[:type])
|
5674
|
-
sb.setConstruction(sub[:assembly])
|
5675
|
-
|
5676
|
-
|
5677
|
-
|
5866
|
+
sb.setConstruction(sub[:assembly]) if sub[:assembly]
|
5867
|
+
sb.setMultiplier(sub[:multiplier]) if sub[:multiplier] > 1
|
5868
|
+
|
5869
|
+
if sub[:frame] && sb.allowWindowPropertyFrameAndDivider
|
5870
|
+
sb.setWindowPropertyFrameAndDivider(sub[:frame])
|
5871
|
+
end
|
5872
|
+
|
5678
5873
|
sb.setSurface(s)
|
5679
5874
|
|
5680
5875
|
# Reset "pos" if array.
|
@@ -5685,32 +5880,15 @@ module OSut
|
|
5685
5880
|
true
|
5686
5881
|
end
|
5687
5882
|
|
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
5883
|
##
|
5708
5884
|
# Returns the "gross roof area" above selected conditioned, occupied spaces.
|
5709
5885
|
# This includes all roof surfaces of indirectly-conditioned, unoccupied spaces
|
5710
5886
|
# like plenums (if located above any of the selected spaces). This also
|
5711
5887
|
# includes roof surfaces of unconditioned or unenclosed spaces like attics, if
|
5712
5888
|
# vertically-overlapping any ceiling of occupied spaces below; attic roof
|
5713
|
-
# sections above uninsulated soffits are excluded, for instance.
|
5889
|
+
# sections above uninsulated soffits are excluded, for instance. It does not
|
5890
|
+
# include surfaces labelled as 'RoofCeiling', which do not comply with ASHRAE
|
5891
|
+
# 90.1 or NECB tilt criteria - see 'roof?'.
|
5714
5892
|
def grossRoofArea(spaces = [])
|
5715
5893
|
mth = "OSut::#{__callee__}"
|
5716
5894
|
up = OpenStudio::Point3d.new(0,0,1) - OpenStudio::Point3d.new(0,0,0)
|
@@ -5721,6 +5899,7 @@ module OSut
|
|
5721
5899
|
spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
|
5722
5900
|
spaces = spaces.select { |space| space.is_a?(OpenStudio::Model::Space) }
|
5723
5901
|
spaces = spaces.select { |space| space.partofTotalFloorArea }
|
5902
|
+
spaces = spaces.reject { |space| unconditioned?(space) }
|
5724
5903
|
return invalid("spaces", mth, 1, DBG, 0) if spaces.empty?
|
5725
5904
|
|
5726
5905
|
# The method is very similar to OpenStudio-Standards' :
|
@@ -5732,17 +5911,18 @@ module OSut
|
|
5732
5911
|
#
|
5733
5912
|
# ... yet differs with regards to attics with overhangs/soffits.
|
5734
5913
|
|
5735
|
-
# Start with roof surfaces of occupied spaces.
|
5914
|
+
# Start with roof surfaces of occupied, conditioned spaces.
|
5736
5915
|
spaces.each do |space|
|
5737
5916
|
facets(space, "Outdoors", "RoofCeiling").each do |roof|
|
5738
5917
|
next if rfs.key?(roof)
|
5918
|
+
next unless roof?(roof)
|
5739
5919
|
|
5740
5920
|
rfs[roof] = {m2: roof.grossArea, m: space.multiplier}
|
5741
5921
|
end
|
5742
5922
|
end
|
5743
5923
|
|
5744
5924
|
# Roof surfaces of unoccupied, conditioned spaces above (e.g. plenums)?
|
5745
|
-
#
|
5925
|
+
# @todo: recursive call for stacked spaces as atria (via AirBoundaries).
|
5746
5926
|
spaces.each do |space|
|
5747
5927
|
facets(space, "Surface", "RoofCeiling").each do |ceiling|
|
5748
5928
|
floor = ceiling.adjacentSurface
|
@@ -5757,6 +5937,7 @@ module OSut
|
|
5757
5937
|
|
5758
5938
|
facets(other, "Outdoors", "RoofCeiling").each do |roof|
|
5759
5939
|
next if rfs.key?(roof)
|
5940
|
+
next unless roof?(roof)
|
5760
5941
|
|
5761
5942
|
rfs[roof] = {m2: roof.grossArea, m: other.multiplier}
|
5762
5943
|
end
|
@@ -5764,7 +5945,7 @@ module OSut
|
|
5764
5945
|
end
|
5765
5946
|
|
5766
5947
|
# Roof surfaces of unoccupied, unconditioned spaces above (e.g. attics)?
|
5767
|
-
#
|
5948
|
+
# @todo: recursive call for stacked spaces as atria (via AirBoundaries).
|
5768
5949
|
spaces.each do |space|
|
5769
5950
|
# When taking overlaps into account, the target space may not share the
|
5770
5951
|
# same local transformation as the space(s) above.
|
@@ -5792,6 +5973,8 @@ module OSut
|
|
5792
5973
|
ti = ti[:t]
|
5793
5974
|
|
5794
5975
|
facets(other, "Outdoors", "RoofCeiling").each do |roof|
|
5976
|
+
next unless roof?(roof)
|
5977
|
+
|
5795
5978
|
rvi = ti * roof.vertices
|
5796
5979
|
cst = cast(cv0, rvi, up)
|
5797
5980
|
next if cst.empty?
|
@@ -5799,7 +5982,7 @@ module OSut
|
|
5799
5982
|
# The overlap calculation fails for roof and ceiling surfaces with
|
5800
5983
|
# previously-added leader lines.
|
5801
5984
|
#
|
5802
|
-
#
|
5985
|
+
# @todo: revise approach for attics ONCE skylight wells have been added.
|
5803
5986
|
olap = nil
|
5804
5987
|
olap = overlap(cst, rvi, false)
|
5805
5988
|
next if olap.empty?
|
@@ -5823,13 +6006,14 @@ module OSut
|
|
5823
6006
|
end
|
5824
6007
|
|
5825
6008
|
##
|
5826
|
-
# Identifies horizontal ridges
|
5827
|
-
#
|
6009
|
+
# Identifies horizontal ridges along 2x sloped (roof?) surfaces (same space).
|
6010
|
+
# The concept of 'sloped' is harmonized with OpenStudio's "alignZPrime". If
|
6011
|
+
# successful, the returned Array holds 'ridge' Hashes. Each Hash holds: an
|
5828
6012
|
# :edge (OpenStudio::Point3dVector), the edge :length (Numeric), and :roofs
|
5829
|
-
# (Array of 2x linked
|
5830
|
-
#
|
6013
|
+
# (Array of 2x linked surfaces). Each surface may be linked to more than one
|
6014
|
+
# horizontal ridge.
|
5831
6015
|
#
|
5832
|
-
# @param roofs [Array<OpenStudio::Model::Surface>] target
|
6016
|
+
# @param roofs [Array<OpenStudio::Model::Surface>] target surfaces
|
5833
6017
|
#
|
5834
6018
|
# @return [Array] horizontal ridges (see logs if empty)
|
5835
6019
|
def getHorizontalRidges(roofs = [])
|
@@ -5838,7 +6022,7 @@ module OSut
|
|
5838
6022
|
return ridges unless roofs.is_a?(Array)
|
5839
6023
|
|
5840
6024
|
roofs = roofs.select { |s| s.is_a?(OpenStudio::Model::Surface) }
|
5841
|
-
roofs = roofs.select { |s|
|
6025
|
+
roofs = roofs.select { |s| sloped?(s) }
|
5842
6026
|
|
5843
6027
|
roofs.each do |roof|
|
5844
6028
|
maxZ = roof.vertices.max_by(&:z).z
|
@@ -5873,7 +6057,7 @@ module OSut
|
|
5873
6057
|
next unless ruf.space.get == space
|
5874
6058
|
|
5875
6059
|
getSegments(ruf).each do |edg|
|
5876
|
-
break
|
6060
|
+
break if match
|
5877
6061
|
next unless same?(edge, edg) || same?(edge, edg.reverse)
|
5878
6062
|
|
5879
6063
|
ridge[:roofs] << ruf
|
@@ -5887,50 +6071,180 @@ module OSut
|
|
5887
6071
|
ridges
|
5888
6072
|
end
|
5889
6073
|
|
6074
|
+
##
|
6075
|
+
# Preselects ideal spaces to toplight, based on 'addSkylights' options and key
|
6076
|
+
# building model geometry attributes. Can be called from within 'addSkylights'
|
6077
|
+
# by setting :ration (opts key:value argument) to 'true' ('false' by default).
|
6078
|
+
# Alternatively, the method can be called prior to 'addSkylights'. The set of
|
6079
|
+
# filters stems from previous rounds of 'addSkylights' stress testing. It is
|
6080
|
+
# intended as an option to prune away less ideal candidate spaces (irregular,
|
6081
|
+
# smaller) in favour of (larger) candidates (notably with more suitable
|
6082
|
+
# roof geometries). This is key when dealing with attic and plenums, where
|
6083
|
+
# 'addSkylights' seeks to add skylight wells (relying on roof cut-outs and
|
6084
|
+
# leader lines). Another check/outcome is whether to prioritize skylight
|
6085
|
+
# allocation in already sidelit spaces - opts[:sidelit] may be reset to 'true'.
|
6086
|
+
#
|
6087
|
+
# @param spaces [Array<OpenStudio::Model::Space>] candidate(s) to toplight
|
6088
|
+
# @param [Hash] opts requested skylight attributes (same as 'addSkylights')
|
6089
|
+
# @option opts [#to_f] :size (1.22m) template skylight width/depth (min 0.4m)
|
6090
|
+
#
|
6091
|
+
# @return [Array<OpenStudio::Model::Space>] candidates (see logs if empty)
|
6092
|
+
def toToplit(spaces = [], opts = {})
|
6093
|
+
mth = "OSut::#{__callee__}"
|
6094
|
+
gap4 = 0.4 # minimum skylight 16" width/depth (excluding frame width)
|
6095
|
+
w = 1.22 # default 48" x 48" skylight base
|
6096
|
+
w2 = w * w
|
6097
|
+
|
6098
|
+
# Validate skylight size, if provided.
|
6099
|
+
if opts.key?(:size)
|
6100
|
+
if opts[:size].respond_to?(:to_f)
|
6101
|
+
w = opts[:size].to_f
|
6102
|
+
w2 = w * w
|
6103
|
+
return invalid(size, mth, 0, ERR, []) if w.round(2) < gap4
|
6104
|
+
else
|
6105
|
+
return mismatch("size", opts[:size], Numeric, mth, DBG, [])
|
6106
|
+
end
|
6107
|
+
end
|
6108
|
+
|
6109
|
+
# Accept single 'OpenStudio::Model::Space' (vs an array of spaces). Filter.
|
6110
|
+
#
|
6111
|
+
# Whether individual spaces are UNCONDITIONED (e.g. attics, unheated areas)
|
6112
|
+
# or flagged as NOT being part of the total floor area (e.g. unoccupied
|
6113
|
+
# plenums), should of course reflect actual design intentions. It's up to
|
6114
|
+
# modellers to correctly flag such cases - can't safely guess in lieu of
|
6115
|
+
# design/modelling team.
|
6116
|
+
#
|
6117
|
+
# A friendly reminder: 'addSkylights' should be called separately for
|
6118
|
+
# strictly SEMIHEATED spaces vs REGRIGERATED spaces vs all other CONDITIONED
|
6119
|
+
# spaces, as per 90.1 and NECB requirements.
|
6120
|
+
if spaces.respond_to?(:spaceType) || spaces.respond_to?(:to_a)
|
6121
|
+
spaces = spaces.respond_to?(:to_a) ? spaces.to_a : [spaces]
|
6122
|
+
spaces = spaces.select { |sp| sp.respond_to?(:spaceType) }
|
6123
|
+
spaces = spaces.select { |sp| sp.partofTotalFloorArea }
|
6124
|
+
spaces = spaces.reject { |sp| unconditioned?(sp) }
|
6125
|
+
spaces = spaces.reject { |sp| vestibule?(sp) }
|
6126
|
+
spaces = spaces.reject { |sp| getRoofs(sp).empty? }
|
6127
|
+
spaces = spaces.reject { |sp| sp.floorArea < 4 * w2 }
|
6128
|
+
spaces = spaces.sort_by(&:floorArea).reverse
|
6129
|
+
return empty("spaces", mth, WRN, 0) if spaces.empty?
|
6130
|
+
else
|
6131
|
+
return mismatch("spaces", spaces, Array, mth, DBG, 0)
|
6132
|
+
end
|
6133
|
+
|
6134
|
+
# Unfenestrated spaces have no windows, glazed doors or skylights. By
|
6135
|
+
# default, 'addSkylights' will prioritize unfenestrated spaces (over all
|
6136
|
+
# existing sidelit ones) and maximize skylight sizes towards achieving the
|
6137
|
+
# required skylight area target. This concentrates skylights for instance in
|
6138
|
+
# typical (large) core spaces, vs (narrower) perimeter spaces. However, for
|
6139
|
+
# less conventional spatial layouts, this default approach can produce less
|
6140
|
+
# optimal skylight distributions. A balance is needed to prioritize large
|
6141
|
+
# unfenestrated spaces when appropriate on one hand, while excluding smaller
|
6142
|
+
# unfenestrated ones on the other. Here, exclusion is based on the average
|
6143
|
+
# floor area of spaces to toplight.
|
6144
|
+
fm2 = spaces.sum(&:floorArea)
|
6145
|
+
afm2 = fm2 / spaces.size
|
6146
|
+
|
6147
|
+
unfen = spaces.reject { |sp| daylit?(sp) }.sort_by(&:floorArea).reverse
|
6148
|
+
|
6149
|
+
# Target larger unfenestrated spaces, if sufficient in area.
|
6150
|
+
if unfen.empty?
|
6151
|
+
opts[:sidelit] = true
|
6152
|
+
else
|
6153
|
+
if spaces.size > unfen.size
|
6154
|
+
ufm2 = unfen.sum(&:floorArea)
|
6155
|
+
u0fm2 = unfen.first.floorArea
|
6156
|
+
|
6157
|
+
if ufm2 > 0.33 * fm2 && u0fm2 > 3 * afm2
|
6158
|
+
unfen = unfen.reject { |sp| sp.floorArea > 0.25 * afm2 }
|
6159
|
+
spaces = spaces.reject { |sp| unfen.include?(sp) }
|
6160
|
+
else
|
6161
|
+
opts[:sidelit] = true
|
6162
|
+
end
|
6163
|
+
end
|
6164
|
+
end
|
6165
|
+
|
6166
|
+
espaces = {}
|
6167
|
+
rooms = []
|
6168
|
+
toits = []
|
6169
|
+
|
6170
|
+
# Gather roof surfaces - possibly those of attics or plenums above.
|
6171
|
+
spaces.each do |sp|
|
6172
|
+
getRoofs(sp).each do |rf|
|
6173
|
+
espaces[sp] = {roofs: []} unless espaces.key?(sp)
|
6174
|
+
espaces[sp][:roofs] << rf unless espaces[sp][:roofs].include?(rf)
|
6175
|
+
end
|
6176
|
+
end
|
6177
|
+
|
6178
|
+
# Priortize larger spaces.
|
6179
|
+
espaces = espaces.sort_by { |espace, _| espace.floorArea }.reverse
|
6180
|
+
|
6181
|
+
# Prioritize larger roof surfaces.
|
6182
|
+
espaces.each do |_, datum|
|
6183
|
+
datum[:roofs] = datum[:roofs].sort_by(&:grossArea).reverse
|
6184
|
+
end
|
6185
|
+
|
6186
|
+
# Single out largest roof in largest space, key when dealing with shared
|
6187
|
+
# attics or plenum roofs.
|
6188
|
+
espaces.each do |espace, datum|
|
6189
|
+
rfs = datum[:roofs].reject { |ruf| toits.include?(ruf) }
|
6190
|
+
next if rfs.empty?
|
6191
|
+
|
6192
|
+
toits << rfs.sort { |ruf| ruf.grossArea }.reverse.first
|
6193
|
+
rooms << espace
|
6194
|
+
end
|
6195
|
+
|
6196
|
+
log(INF, "No ideal toplit candidates (#{mth})") if rooms.empty?
|
6197
|
+
|
6198
|
+
rooms
|
6199
|
+
end
|
6200
|
+
|
5890
6201
|
##
|
5891
6202
|
# 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
|
-
#
|
6203
|
+
# spaces, based on requested skylight area, or a skylight-to-roof ratio (SRR%).
|
6204
|
+
# If the user selects 0m2 as the requested :area (or 0 as the requested :srr),
|
6205
|
+
# while setting the option :clear as true, the method simply purges all
|
6206
|
+
# pre-existing roof fenestrated subsurfaces of selected spaces, and exits while
|
6207
|
+
# returning 0 (without logging an error or warning). Pre-existing skylight
|
6208
|
+
# wells are not cleared however. Pre-toplit spaces are otherwise ignored.
|
6209
|
+
# Boolean options :attic, :plenum, :sloped and :sidelit further restrict
|
6210
|
+
# candidate spaces to toplight. If applicable, options :attic and :plenum add
|
6211
|
+
# skylight wells. Option :patterns restricts preset skylight allocation
|
6212
|
+
# layouts in order of preference; if left empty, all preset patterns are
|
6213
|
+
# considered, also in order of preference (see examples).
|
5902
6214
|
#
|
5903
6215
|
# @param spaces [Array<OpenStudio::Model::Space>] space(s) to toplight
|
5904
6216
|
# @param [Hash] opts requested skylight attributes
|
5905
|
-
# @option opts [#to_f] :
|
6217
|
+
# @option opts [#to_f] :area overall skylight area
|
6218
|
+
# @option opts [#to_f] :srr skylight-to-roof ratio (0.00, 0.90]
|
5906
6219
|
# @option opts [#to_f] :size (1.22) template skylight width/depth (min 0.4m)
|
5907
6220
|
# @option opts [#frameWidth] :frame (nil) OpenStudio Frame & Divider (optional)
|
5908
6221
|
# @option opts [Bool] :clear (true) whether to first purge existing skylights
|
6222
|
+
# @option opts [Bool] :ration (true) finer selection of candidates to toplight
|
5909
6223
|
# @option opts [Bool] :sidelit (true) whether to consider sidelit spaces
|
5910
6224
|
# @option opts [Bool] :sloped (true) whether to consider sloped roof surfaces
|
5911
6225
|
# @option opts [Bool] :plenum (true) whether to consider plenum wells
|
5912
6226
|
# @option opts [Bool] :attic (true) whether to consider attic wells
|
5913
6227
|
# @option opts [Array<#to_s>] :patterns requested skylight allocation (3x)
|
5914
|
-
# @example (a) consider 2D array of individual skylights, e.g. n(1.
|
6228
|
+
# @example (a) consider 2D array of individual skylights, e.g. n(1.22m x 1.22m)
|
5915
6229
|
# opts[:patterns] = ["array"]
|
5916
6230
|
# @example (b) consider 'a', then array of 1x(size) x n(size) skylight strips
|
5917
6231
|
# opts[:patterns] = ["array", "strips"]
|
5918
6232
|
#
|
5919
|
-
# @return [Float] returns gross roof area if successful (see logs if
|
6233
|
+
# @return [Float] returns gross roof area if successful (see logs if 0m2)
|
5920
6234
|
def addSkyLights(spaces = [], opts = {})
|
5921
6235
|
mth = "OSut::#{__callee__}"
|
5922
6236
|
clear = true
|
5923
|
-
srr =
|
6237
|
+
srr = nil
|
6238
|
+
area = nil
|
5924
6239
|
frame = nil # FrameAndDivider object
|
5925
6240
|
f = 0.0 # FrameAndDivider frame width
|
5926
|
-
gap = 0.1 # min 2" around well (2x), as well as max frame width
|
6241
|
+
gap = 0.1 # min 2" around well (2x == 4"), as well as max frame width
|
5927
6242
|
gap2 = 0.2 # 2x gap
|
5928
6243
|
gap4 = 0.4 # minimum skylight 16" width/depth (excluding frame width)
|
5929
6244
|
bfr = 0.005 # minimum array perimeter buffer (no wells)
|
5930
6245
|
w = 1.22 # default 48" x 48" skylight base
|
5931
6246
|
w2 = w * w # m2
|
5932
6247
|
|
5933
|
-
|
5934
6248
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
5935
6249
|
# Excerpts of ASHRAE 90.1 2022 definitions:
|
5936
6250
|
#
|
@@ -5994,7 +6308,7 @@ module OSut
|
|
5994
6308
|
# to exclude portions of any roof surface: all plenum roof surfaces (in
|
5995
6309
|
# addition to soffit surfaces) would need to be insulated). The method takes
|
5996
6310
|
# such circumstances into account, which requires vertically casting of
|
5997
|
-
# surfaces
|
6311
|
+
# surfaces onto others, as well as overlap calculations. If successful, the
|
5998
6312
|
# method returns the "GROSS ROOF AREA" (in m2), based on the above rationale.
|
5999
6313
|
#
|
6000
6314
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
@@ -6014,7 +6328,7 @@ module OSut
|
|
6014
6328
|
# instance, if the GROSS ROOF AREA were based on insulated ceiling surfaces,
|
6015
6329
|
# there would be a topological disconnect between flat ceiling and sloped
|
6016
6330
|
# skylights above. Should NECB users first 'project' (sloped) skylight rough
|
6017
|
-
# openings onto flat ceilings when calculating
|
6331
|
+
# openings onto flat ceilings when calculating SRR%? Without much needed
|
6018
6332
|
# clarification, the (clearer) 90.1 rules equally apply here to NECB cases.
|
6019
6333
|
|
6020
6334
|
# If skylight wells are indeed required, well wall edges are always vertical
|
@@ -6037,19 +6351,8 @@ module OSut
|
|
6037
6351
|
|
6038
6352
|
mdl = spaces.first.model
|
6039
6353
|
|
6040
|
-
# Exit if mismatched or invalid
|
6354
|
+
# Exit if mismatched or invalid options.
|
6041
6355
|
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
6356
|
|
6054
6357
|
# Validate Frame & Divider object, if provided.
|
6055
6358
|
if opts.key?(:frame)
|
@@ -6085,6 +6388,27 @@ module OSut
|
|
6085
6388
|
wl = w0 + gap
|
6086
6389
|
wl2 = wl * wl
|
6087
6390
|
|
6391
|
+
# Validate requested skylight-to-roof ratio (or overall area).
|
6392
|
+
if opts.key?(:area)
|
6393
|
+
if opts[:area].respond_to?(:to_f)
|
6394
|
+
area = opts[:area].to_f
|
6395
|
+
log(WRN, "Area reset to 0.0m2 (#{mth})") if area < 0
|
6396
|
+
else
|
6397
|
+
return mismatch("area", opts[:area], Numeric, mth, DBG, 0)
|
6398
|
+
end
|
6399
|
+
elsif opts.key?(:srr)
|
6400
|
+
if opts[:srr].respond_to?(:to_f)
|
6401
|
+
srr = opts[:srr].to_f
|
6402
|
+
log(WRN, "SRR (#{srr.round(2)}) reset to 0% (#{mth})") if srr < 0
|
6403
|
+
log(WRN, "SRR (#{srr.round(2)}) reset to 90% (#{mth})") if srr > 0.90
|
6404
|
+
srr = srr.clamp(0.00, 0.10)
|
6405
|
+
else
|
6406
|
+
return mismatch("srr", opts[:srr], Numeric, mth, DBG, 0)
|
6407
|
+
end
|
6408
|
+
else
|
6409
|
+
return hashkey("area", opts, :area, mth, ERR, 0)
|
6410
|
+
end
|
6411
|
+
|
6088
6412
|
# Validate purge request, if provided.
|
6089
6413
|
if opts.key?(:clear)
|
6090
6414
|
clear = opts[:clear]
|
@@ -6095,64 +6419,126 @@ module OSut
|
|
6095
6419
|
end
|
6096
6420
|
end
|
6097
6421
|
|
6422
|
+
# Purge if requested.
|
6098
6423
|
getRoofs(spaces).each { |s| s.subSurfaces.map(&:remove) } if clear
|
6099
6424
|
|
6100
6425
|
# Safely exit, e.g. if strictly called to purge existing roof subsurfaces.
|
6101
|
-
return 0 if
|
6426
|
+
return 0 if area && area.round(2) == 0
|
6427
|
+
return 0 if srr && srr.round(2) == 0
|
6102
6428
|
|
6103
|
-
|
6104
|
-
|
6105
|
-
# 'bounded box' that neatly 'fits' within a given roof surface. This equally
|
6106
|
-
# applies to any vertically-cast overlap between roof and plenum (or attic)
|
6107
|
-
# floor, which in turn generates skylight wells. Skylight arrays are
|
6108
|
-
# inserted from left/right + top/bottom (as illustrated below), once a roof
|
6109
|
-
# (or cast 3D overlap) is 'aligned' in 2D (possibly also 'realigned').
|
6110
|
-
#
|
6111
|
-
# Depending on geometric complexity (e.g. building/roof concavity,
|
6112
|
-
# triangulation), the total area of bounded boxes may be significantly less
|
6113
|
-
# than the calculated "GROSS ROOF AREA", which can make it challenging to
|
6114
|
-
# attain the desired %SRR. If :patterns are left unaltered, the method will
|
6115
|
-
# select patterns that maximize the likelihood of attaining the requested
|
6116
|
-
# %SRR, to the detriment of spatial distribution of daylighting.
|
6117
|
-
#
|
6118
|
-
# The default skylight module size is 1.2m x 1.2m (4' x 4'), which be
|
6119
|
-
# overridden by the user, e.g. 2.4m x 2.4m (8' x 8').
|
6120
|
-
#
|
6121
|
-
# Preset skylight allocation patterns (in order of precedence):
|
6122
|
-
# 1. "array"
|
6123
|
-
# _____________________
|
6124
|
-
# | _ _ _ | - ?x columns ("cols") >= ?x rows (min 2x2)
|
6125
|
-
# | |_| |_| |_| | - SRR ~5% (1.2m x 1.2m), as illustrated
|
6126
|
-
# | | - SRR ~19% (2.4m x 2.4m)
|
6127
|
-
# | _ _ _ | - +suitable for wide spaces (storage, retail)
|
6128
|
-
# | |_| |_| |_| | - ~1.4x height + skylight width 'ideal' rule
|
6129
|
-
# |_____________________| - better daylight distribution, many wells
|
6130
|
-
#
|
6131
|
-
# 2. "strips"
|
6132
|
-
# _____________________
|
6133
|
-
# | _ _ _ | - ?x columns (min 2), 1x row
|
6134
|
-
# | | | | | | | | - ~doubles %SRR ...
|
6135
|
-
# | | | | | | | | - SRR ~10% (1.2m x ?1.2m), as illustrated
|
6136
|
-
# | | | | | | | | - SRR ~19% (2.4m x ?1.2m)
|
6137
|
-
# | |_| |_| |_| | - ~roof monitor layout
|
6138
|
-
# |_____________________| - fewer wells
|
6139
|
-
#
|
6140
|
-
# 3. "strip"
|
6141
|
-
# ____________________
|
6142
|
-
# | | - 1x column, 1x row (min 1x)
|
6143
|
-
# | ______________ | - SRR ~11% (1.2m x ?1.2m)
|
6144
|
-
# | | ............ | | - SRR ~22% (2.4m x ?1.2m), as illustrated
|
6145
|
-
# | |______________| | - +suitable for elongated bounded boxes
|
6146
|
-
# | | - 1x well
|
6147
|
-
# |____________________|
|
6148
|
-
#
|
6149
|
-
# TO-DO: Support strips/strip patterns along ridge of paired roof surfaces.
|
6150
|
-
layouts = ["array", "strips", "strip"]
|
6151
|
-
patterns = []
|
6429
|
+
m2 = 0 # total existing skylight rough opening area
|
6430
|
+
rm2 = grossRoofArea(spaces) # excludes e.g. overhangs
|
6152
6431
|
|
6153
|
-
#
|
6154
|
-
|
6155
|
-
|
6432
|
+
# Tally existing skylight rough opening areas.
|
6433
|
+
spaces.each do |space|
|
6434
|
+
m = space.multiplier
|
6435
|
+
|
6436
|
+
facets(space, "Outdoors", "RoofCeiling").each do |roof|
|
6437
|
+
roof.subSurfaces.each do |sub|
|
6438
|
+
next unless fenestration?(sub)
|
6439
|
+
|
6440
|
+
id = sub.nameString
|
6441
|
+
xm2 = sub.grossArea
|
6442
|
+
|
6443
|
+
if sub.allowWindowPropertyFrameAndDivider
|
6444
|
+
unless sub.windowPropertyFrameAndDivider.empty?
|
6445
|
+
fw = sub.windowPropertyFrameAndDivider.get.frameWidth
|
6446
|
+
vec = offset(sub.vertices, fw, 300)
|
6447
|
+
aire = OpenStudio.getArea(vec)
|
6448
|
+
|
6449
|
+
if aire.empty?
|
6450
|
+
log(ERR, "Skipping '#{id}': invalid Frame&Divider (#{mth})")
|
6451
|
+
else
|
6452
|
+
xm2 = aire.get
|
6453
|
+
end
|
6454
|
+
end
|
6455
|
+
end
|
6456
|
+
|
6457
|
+
m2 += xm2 * sub.multiplier * m
|
6458
|
+
end
|
6459
|
+
end
|
6460
|
+
end
|
6461
|
+
|
6462
|
+
# Required skylight area to add.
|
6463
|
+
sm2 = area ? area : rm2 * srr - m2
|
6464
|
+
|
6465
|
+
# Warn/skip if existing skylights exceed or ~roughly match targets.
|
6466
|
+
if sm2.round(2) < w02.round(2)
|
6467
|
+
if m2 > 0
|
6468
|
+
log(INF, "Skipping: existing skylight area > request (#{mth})")
|
6469
|
+
return rm2
|
6470
|
+
else
|
6471
|
+
log(INF, "Requested skylight area < min size (#{mth})")
|
6472
|
+
end
|
6473
|
+
elsif 0.9 * rm2.round(2) < sm2.round(2)
|
6474
|
+
log(INF, "Skipping: requested skylight area > 90% of GRA (#{mth})")
|
6475
|
+
return rm2
|
6476
|
+
end
|
6477
|
+
|
6478
|
+
opts[:ration] = true unless opts.key?(:ration)
|
6479
|
+
|
6480
|
+
# By default, seek ideal candidate spaces/roofs. Bail out if unsuccessful.
|
6481
|
+
unless opts[:ration] == false
|
6482
|
+
spaces = toToplit(spaces, opts)
|
6483
|
+
return rm2 if spaces.empty?
|
6484
|
+
end
|
6485
|
+
|
6486
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
6487
|
+
# The method seeks to insert a skylight array within the largest rectangular
|
6488
|
+
# 'bounded box' that neatly 'fits' within a given roof surface. This equally
|
6489
|
+
# applies to any vertically-cast overlap between roof and plenum (or attic)
|
6490
|
+
# floor, which in turn generates skylight wells. Skylight arrays are
|
6491
|
+
# inserted from left-to-right & top-to-bottom (as illustrated below), once a
|
6492
|
+
# roof (or cast 3D overlap) is 'aligned' in 2D.
|
6493
|
+
#
|
6494
|
+
# Depending on geometric complexity (e.g. building/roof concavity,
|
6495
|
+
# triangulation), the total area of bounded boxes may be significantly less
|
6496
|
+
# than the calculated "GROSS ROOF AREA", which can make it challenging to
|
6497
|
+
# attain the requested skylight area. If :patterns are left unaltered, the
|
6498
|
+
# method will select those that maximize the likelihood of attaining the
|
6499
|
+
# requested target, to the detriment of spatial daylighting distribution.
|
6500
|
+
#
|
6501
|
+
# The default skylight module size is 1.22m x 1.22m (4' x 4'), which can be
|
6502
|
+
# overridden by the user, e.g. 2.44m x 2.44m (8' x 8'). However, skylight
|
6503
|
+
# sizes usually end up either contracted or inflated to exactly meet a
|
6504
|
+
# request skylight area or SRR%,
|
6505
|
+
#
|
6506
|
+
# Preset skylight allocation patterns (in order of precedence):
|
6507
|
+
#
|
6508
|
+
# 1. "array"
|
6509
|
+
# _____________________
|
6510
|
+
# | _ _ _ | - ?x columns ("cols") >= ?x rows (min 2x2)
|
6511
|
+
# | |_| |_| |_| | - SRR ~5% (1.22m x 1.22m), as illustrated
|
6512
|
+
# | | - SRR ~19% (2.44m x 2.44m)
|
6513
|
+
# | _ _ _ | - +suitable for wide spaces (storage, retail)
|
6514
|
+
# | |_| |_| |_| | - ~1.4x height + skylight width 'ideal' rule
|
6515
|
+
# |_____________________| - better daylight distribution, many wells
|
6516
|
+
#
|
6517
|
+
# 2. "strips"
|
6518
|
+
# _____________________
|
6519
|
+
# | _ _ _ | - ?x columns (min 2), 1x row
|
6520
|
+
# | | | | | | | | - ~doubles %SRR ...
|
6521
|
+
# | | | | | | | | - SRR ~10% (1.22m x ?1.22m), as illustrated
|
6522
|
+
# | | | | | | | | - SRR ~19% (2.44m x ?1.22m)
|
6523
|
+
# | |_| |_| |_| | - ~roof monitor layout
|
6524
|
+
# |_____________________| - fewer wells
|
6525
|
+
#
|
6526
|
+
# 3. "strip"
|
6527
|
+
# ____________________
|
6528
|
+
# | | - 1x column, 1x row (min 1x)
|
6529
|
+
# | ______________ | - SRR ~11% (1.22m x ?1.22m)
|
6530
|
+
# | | ............ | | - SRR ~22% (2.44m x ?1.22m), as illustrated
|
6531
|
+
# | |______________| | - +suitable for elongated bounded boxes
|
6532
|
+
# | | - 1x well
|
6533
|
+
# |____________________|
|
6534
|
+
#
|
6535
|
+
# @todo: Support strips/strip patterns along ridge of paired roof surfaces.
|
6536
|
+
layouts = ["array", "strips", "strip"]
|
6537
|
+
patterns = []
|
6538
|
+
|
6539
|
+
# Validate skylight placement patterns, if provided.
|
6540
|
+
if opts.key?(:patterns)
|
6541
|
+
if opts[:patterns].is_a?(Array)
|
6156
6542
|
opts[:patterns].each_with_index do |pattern, i|
|
6157
6543
|
pattern = trim(pattern).downcase
|
6158
6544
|
|
@@ -6174,27 +6560,26 @@ module OSut
|
|
6174
6560
|
# - large roof surface areas (e.g. retail, classrooms ... not corridors)
|
6175
6561
|
# - not sidelit (favours core spaces)
|
6176
6562
|
# - having flat roofs (avoids sloped roofs)
|
6177
|
-
# -
|
6563
|
+
# - neither under plenums, nor attics (avoids wells)
|
6178
6564
|
#
|
6179
6565
|
# This ideal (albeit stringent) set of conditions is "combo a".
|
6180
6566
|
#
|
6181
|
-
# If
|
6182
|
-
# selection criteria and
|
6567
|
+
# If the requested skylight area has not yet been achieved (after initially
|
6568
|
+
# applying "combo a"), the method decrementally drops selection criteria and
|
6569
|
+
# starts over, e.g.:
|
6183
6570
|
# - then considers sidelit spaces
|
6184
6571
|
# - then considers sloped roofs
|
6185
6572
|
# - then considers skylight wells
|
6186
6573
|
#
|
6187
6574
|
# 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.
|
6575
|
+
# given combo, all the while giving priority to larger roof areas. An error
|
6576
|
+
# message is logged if the target isn't ultimately achieved.
|
6192
6577
|
#
|
6193
|
-
# Through filters, users may restrict candidate roof surfaces:
|
6578
|
+
# Through filters, users may in advance restrict candidate roof surfaces:
|
6194
6579
|
# b. above occupied sidelit spaces ('false' restricts to core spaces)
|
6195
6580
|
# c. that are sloped ('false' restricts to flat roofs)
|
6196
|
-
# d. above
|
6197
|
-
# e. above
|
6581
|
+
# d. above INDIRECTLY CONDITIONED spaces (e.g. plenums, uninsulated wells)
|
6582
|
+
# e. above UNCONDITIONED spaces (e.g. attics, insulated wells)
|
6198
6583
|
filters = ["a", "b", "bc", "bcd", "bcde"]
|
6199
6584
|
|
6200
6585
|
# Prune filters, based on user-selected options.
|
@@ -6213,8 +6598,8 @@ module OSut
|
|
6213
6598
|
filters.reject! { |f| f.empty? }
|
6214
6599
|
filters.uniq!
|
6215
6600
|
|
6216
|
-
# Remaining filters may be further
|
6217
|
-
# depending on geometry, e.g.:
|
6601
|
+
# Remaining filters may be further pruned automatically after space/roof
|
6602
|
+
# processing, depending on geometry, e.g.:
|
6218
6603
|
# - if there are no sidelit spaces: filter "b" will be pruned away
|
6219
6604
|
# - if there are no sloped roofs : filter "c" will be pruned away
|
6220
6605
|
# - if no plenums are identified : filter "d" will be pruned away
|
@@ -6228,12 +6613,10 @@ module OSut
|
|
6228
6613
|
attics = {} # unoccupied UNCONDITIONED spaces above rooms
|
6229
6614
|
ceilings = {} # of occupied CONDITIONED space (if plenums/attics)
|
6230
6615
|
|
6231
|
-
#
|
6616
|
+
# Candidate 'rooms' to toplit - excludes plenums/attics.
|
6232
6617
|
spaces.each do |space|
|
6233
|
-
|
6234
|
-
next unless space.partofTotalFloorArea # occupied (not plenum)
|
6618
|
+
id = space.nameString
|
6235
6619
|
|
6236
|
-
# Already toplit?
|
6237
6620
|
if daylit?(space, false, true, false)
|
6238
6621
|
log(WRN, "#{id} is already toplit, skipping (#{mth})")
|
6239
6622
|
next
|
@@ -6241,30 +6624,40 @@ module OSut
|
|
6241
6624
|
|
6242
6625
|
# When unoccupied spaces are involved (e.g. plenums, attics), the occupied
|
6243
6626
|
# space (to toplight) may not share the same local transformation as its
|
6244
|
-
# unoccupied space(s) above. Fetching
|
6245
|
-
h = 0
|
6627
|
+
# unoccupied space(s) above. Fetching site transformation.
|
6246
6628
|
t0 = transforms(space)
|
6247
6629
|
next unless t0[:t]
|
6248
6630
|
|
6249
|
-
|
6250
|
-
|
6631
|
+
# Calculate space height.
|
6632
|
+
hMIN = 10000
|
6633
|
+
hMAX = 0
|
6634
|
+
surfs = facets(space)
|
6635
|
+
|
6636
|
+
surfs.each { |surf| hMAX = [hMAX, surf.vertices.max_by(&:z).z].max }
|
6637
|
+
surfs.each { |surf| hMIN = [hMIN, surf.vertices.min_by(&:z).z].min }
|
6251
6638
|
|
6252
|
-
|
6253
|
-
|
6639
|
+
h = hMAX - hMIN
|
6640
|
+
|
6641
|
+
unless h > 0
|
6642
|
+
log(ERR, "#{id} height? #{hMIN.round(2)} vs #{hMAX.round(2)} (#{mth})")
|
6643
|
+
next
|
6644
|
+
end
|
6254
6645
|
|
6255
6646
|
rooms[space] = {}
|
6256
|
-
rooms[space][:
|
6647
|
+
rooms[space][:t0 ] = t0[:t]
|
6257
6648
|
rooms[space][:m ] = space.multiplier
|
6258
6649
|
rooms[space][:h ] = h
|
6259
|
-
rooms[space][:roofs ] =
|
6650
|
+
rooms[space][:roofs ] = facets(space, "Outdoors", "RoofCeiling")
|
6260
6651
|
rooms[space][:sidelit] = daylit?(space, true, false, false)
|
6261
6652
|
|
6262
6653
|
# Fetch and process room-specific outdoor-facing roof surfaces, the most
|
6263
|
-
# basic 'set' to track
|
6264
|
-
# - no skylight wells
|
6654
|
+
# basic 'set' to track, e.g.:
|
6655
|
+
# - no skylight wells (i.e. no leader lines)
|
6265
6656
|
# - 1x skylight array per roof surface
|
6266
|
-
# - no need to
|
6657
|
+
# - no need to consider site transformation
|
6267
6658
|
rooms[space][:roofs].each do |roof|
|
6659
|
+
next unless roof?(roof)
|
6660
|
+
|
6268
6661
|
box = boundedBox(roof)
|
6269
6662
|
next if box.empty?
|
6270
6663
|
|
@@ -6274,133 +6667,112 @@ module OSut
|
|
6274
6667
|
bm2 = bm2.get
|
6275
6668
|
next if bm2.round(2) < w02.round(2)
|
6276
6669
|
|
6277
|
-
|
6670
|
+
width = alignedWidth(box, true)
|
6671
|
+
depth = alignedHeight(box, true)
|
6672
|
+
next if width < wl * 3
|
6673
|
+
next if depth < wl
|
6674
|
+
|
6675
|
+
# A set is 'tight' if the area of its bounded box is significantly
|
6676
|
+
# smaller than that of its roof. A set is 'thin' if the depth of its
|
6677
|
+
# bounded box is (too) narrow. If either is true, some geometry rules
|
6678
|
+
# may be relaxed to maximize allocated skylight area. Neither apply to
|
6679
|
+
# cases with skylight wells.
|
6278
6680
|
tight = bm2 < roof.grossArea / 2 ? true : false
|
6681
|
+
thin = depth.round(2) < (1.5 * wl).round(2) ? true : false
|
6279
6682
|
|
6280
6683
|
set = {}
|
6281
6684
|
set[:box ] = box
|
6282
6685
|
set[:bm2 ] = bm2
|
6283
6686
|
set[:tight ] = tight
|
6687
|
+
set[:thin ] = thin
|
6284
6688
|
set[:roof ] = roof
|
6285
6689
|
set[:space ] = space
|
6690
|
+
set[:m ] = space.multiplier
|
6286
6691
|
set[:sidelit] = rooms[space][:sidelit]
|
6287
|
-
set[:
|
6288
|
-
set[:
|
6692
|
+
set[:t0 ] = rooms[space][:t0]
|
6693
|
+
set[:t ] = OpenStudio::Transformation.alignFace(roof.vertices)
|
6289
6694
|
sets << set
|
6290
6695
|
end
|
6291
6696
|
end
|
6292
6697
|
|
6293
6698
|
# Process outdoor-facing roof surfaces of plenums and attics above.
|
6294
6699
|
rooms.each do |space, room|
|
6295
|
-
t0
|
6296
|
-
|
6297
|
-
rufs = room.key?(:roofs) ? toits - room[:roofs] : toits
|
6298
|
-
next if rufs.empty?
|
6700
|
+
t0 = room[:t0]
|
6701
|
+
rufs = getRoofs(space) - room[:roofs]
|
6299
6702
|
|
6300
|
-
# Process room ceilings, as 1x or more are overlapping roofs above. Fetch
|
6301
|
-
# vertically-cast overlaps.
|
6302
6703
|
rufs.each do |ruf|
|
6704
|
+
next unless roof?(ruf)
|
6705
|
+
|
6303
6706
|
espace = ruf.space
|
6304
6707
|
next if espace.empty?
|
6305
6708
|
|
6306
6709
|
espace = espace.get
|
6307
6710
|
next if espace.partofTotalFloorArea
|
6308
6711
|
|
6309
|
-
m
|
6310
|
-
ti = transforms(espace)
|
6311
|
-
next unless ti[:t]
|
6312
|
-
|
6313
|
-
ti = ti[:t]
|
6314
|
-
vtx = ruf.vertices
|
6712
|
+
m = espace.multiplier
|
6315
6713
|
|
6316
|
-
|
6317
|
-
|
6318
|
-
|
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)
|
6714
|
+
if m != space.multiplier
|
6715
|
+
log(ERR, "Skipping #{ruf.nameString} (multiplier mismatch) (#{mth})")
|
6716
|
+
next
|
6335
6717
|
end
|
6336
6718
|
|
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
|
6719
|
+
ti = transforms(espace)
|
6720
|
+
next unless ti[:t]
|
6362
6721
|
|
6363
|
-
|
6722
|
+
ti = ti[:t]
|
6723
|
+
rpts = ti * ruf.vertices
|
6364
6724
|
|
6365
|
-
|
6725
|
+
# Process occupied room ceilings, as 1x or more are overlapping roof
|
6726
|
+
# surfaces above. Vertically cast, then fetch overlap.
|
6727
|
+
facets(space, "Surface", "RoofCeiling").each do |tile|
|
6728
|
+
tpts = t0 * tile.vertices
|
6729
|
+
ci0 = cast(tpts, rpts, ray)
|
6366
6730
|
next if ci0.empty?
|
6367
6731
|
|
6368
|
-
olap = overlap(
|
6732
|
+
olap = overlap(rpts, ci0)
|
6369
6733
|
next if olap.empty?
|
6370
6734
|
|
6735
|
+
om2 = OpenStudio.getArea(olap)
|
6736
|
+
next if om2.empty?
|
6737
|
+
|
6738
|
+
om2 = om2.get
|
6739
|
+
next if om2.round(2) < w02.round(2)
|
6740
|
+
|
6371
6741
|
box = boundedBox(olap)
|
6372
6742
|
next if box.empty?
|
6373
6743
|
|
6374
6744
|
# Adding skylight wells (plenums/attics) is contingent to safely
|
6375
|
-
# linking new base roof 'inserts'
|
6376
|
-
#
|
6745
|
+
# linking new base roof 'inserts' (as well as new ceiling ones)
|
6746
|
+
# through 'leader lines'. This requires an offset to ensure no
|
6747
|
+
# conflicts with roof or (ceiling) tile edges.
|
6377
6748
|
#
|
6378
|
-
#
|
6749
|
+
# @todo: Expand the method to factor in cases where simple 'side'
|
6379
6750
|
# cutouts can be supported (no need for leader lines), e.g.
|
6380
6751
|
# skylight strips along roof ridges.
|
6381
6752
|
box = offset(box, -gap, 300)
|
6382
|
-
box = poly(box, false, false, false, false, :blc)
|
6383
6753
|
next if box.empty?
|
6384
6754
|
|
6385
6755
|
bm2 = OpenStudio.getArea(box)
|
6386
6756
|
next if bm2.empty?
|
6387
6757
|
|
6388
6758
|
bm2 = bm2.get
|
6389
|
-
next if bm2.round(2) <
|
6759
|
+
next if bm2.round(2) < wl2.round(2)
|
6760
|
+
|
6761
|
+
width = alignedWidth(box, true)
|
6762
|
+
depth = alignedHeight(box, true)
|
6763
|
+
next if width < wl * 3
|
6764
|
+
next if depth < wl * 2
|
6390
6765
|
|
6391
|
-
# Vertically
|
6392
|
-
cbox = cast(box,
|
6766
|
+
# Vertically cast box onto tile below.
|
6767
|
+
cbox = cast(box, tpts, ray)
|
6393
6768
|
next if cbox.empty?
|
6394
6769
|
|
6395
6770
|
cm2 = OpenStudio.getArea(cbox)
|
6396
6771
|
next if cm2.empty?
|
6397
6772
|
|
6398
|
-
cm2
|
6399
|
-
|
6400
|
-
|
6401
|
-
# or ceiling.
|
6402
|
-
tight = bm2 < ruf.grossArea / 2 ? true : false
|
6403
|
-
tight = cm2 < tile.grossArea / 2 ? true : tight
|
6773
|
+
cm2 = cm2.get
|
6774
|
+
box = ti.inverse * box
|
6775
|
+
cbox = t0.inverse * cbox
|
6404
6776
|
|
6405
6777
|
unless ceilings.key?(tile)
|
6406
6778
|
floor = tile.adjacentSurface
|
@@ -6412,10 +6784,6 @@ module OSut
|
|
6412
6784
|
|
6413
6785
|
floor = floor.get
|
6414
6786
|
|
6415
|
-
# Ensure BLC vertex sequence.
|
6416
|
-
vtx = t0 * vtx
|
6417
|
-
floor.setVertices(ti.inverse * vtx.reverse)
|
6418
|
-
|
6419
6787
|
if floor.space.empty?
|
6420
6788
|
log(ERR, "#{floor.nameString} space? (#{mth})")
|
6421
6789
|
next
|
@@ -6428,32 +6796,40 @@ module OSut
|
|
6428
6796
|
next
|
6429
6797
|
end
|
6430
6798
|
|
6431
|
-
ceilings[tile]
|
6432
|
-
ceilings[tile][:roofs] = []
|
6433
|
-
ceilings[tile][:space] = space
|
6434
|
-
ceilings[tile][:floor] = floor
|
6799
|
+
ceilings[tile] = {}
|
6800
|
+
ceilings[tile][:roofs ] = []
|
6801
|
+
ceilings[tile][:space ] = space
|
6802
|
+
ceilings[tile][:floor ] = floor
|
6435
6803
|
end
|
6436
6804
|
|
6437
6805
|
ceilings[tile][:roofs] << ruf
|
6438
6806
|
|
6439
|
-
#
|
6807
|
+
# Skylight set key:values are more detailed with suspended ceilings.
|
6808
|
+
# The overlap (:olap) remains in 'transformed' site coordinates (with
|
6809
|
+
# regards to the roof). The :box polygon reverts to attic/plenum space
|
6810
|
+
# coordinates, while the :cbox polygon is reset with regards to the
|
6811
|
+
# occupied space coordinates.
|
6440
6812
|
set = {}
|
6441
6813
|
set[:olap ] = olap
|
6442
6814
|
set[:box ] = box
|
6443
6815
|
set[:cbox ] = cbox
|
6816
|
+
set[:om2 ] = om2
|
6444
6817
|
set[:bm2 ] = bm2
|
6445
6818
|
set[:cm2 ] = cm2
|
6446
|
-
set[:tight ] =
|
6819
|
+
set[:tight ] = false
|
6820
|
+
set[:thin ] = false
|
6447
6821
|
set[:roof ] = ruf
|
6448
6822
|
set[:space ] = space
|
6823
|
+
set[:m ] = space.multiplier
|
6449
6824
|
set[:clng ] = tile
|
6450
|
-
set[:
|
6825
|
+
set[:t0 ] = t0
|
6826
|
+
set[:ti ] = ti
|
6827
|
+
set[:t ] = OpenStudio::Transformation.alignFace(ruf.vertices)
|
6451
6828
|
set[:sidelit] = room[:sidelit]
|
6452
|
-
set[:sloped ] = slopedRoof?(ruf)
|
6453
6829
|
|
6454
6830
|
if unconditioned?(espace) # e.g. attic
|
6455
6831
|
unless attics.key?(espace)
|
6456
|
-
attics[espace] = {
|
6832
|
+
attics[espace] = {ti: ti, m: m, bm2: 0, roofs: []}
|
6457
6833
|
end
|
6458
6834
|
|
6459
6835
|
attics[espace][:bm2 ] += bm2
|
@@ -6464,7 +6840,7 @@ module OSut
|
|
6464
6840
|
ceilings[tile][:attic] = espace
|
6465
6841
|
else # e.g. plenum
|
6466
6842
|
unless plenums.key?(espace)
|
6467
|
-
plenums[espace] = {
|
6843
|
+
plenums[espace] = {ti: ti, m: m, bm2: 0, roofs: []}
|
6468
6844
|
end
|
6469
6845
|
|
6470
6846
|
plenums[espace][:bm2 ] += bm2
|
@@ -6481,21 +6857,20 @@ module OSut
|
|
6481
6857
|
end
|
6482
6858
|
end
|
6483
6859
|
|
6484
|
-
# Ensure uniqueness of plenum roofs
|
6860
|
+
# Ensure uniqueness of plenum roofs.
|
6485
6861
|
attics.values.each do |attic|
|
6486
6862
|
attic[:roofs ].uniq!
|
6487
|
-
attic[:ridges] = getHorizontalRidges(attic[:roofs]) #
|
6863
|
+
attic[:ridges] = getHorizontalRidges(attic[:roofs]) # @todo
|
6488
6864
|
end
|
6489
6865
|
|
6490
6866
|
plenums.values.each do |plenum|
|
6491
6867
|
plenum[:roofs ].uniq!
|
6492
|
-
|
6493
|
-
plenum[:ridges] = getHorizontalRidges(plenum[:roofs]) # TO-DO
|
6868
|
+
plenum[:ridges] = getHorizontalRidges(plenum[:roofs]) # @todo
|
6494
6869
|
end
|
6495
6870
|
|
6496
|
-
# Regardless of the selected skylight arrangement pattern, the
|
6497
|
-
#
|
6498
|
-
#
|
6871
|
+
# Regardless of the selected skylight arrangement pattern, the solution only
|
6872
|
+
# considers attic/plenum sets that can be successfully linked to leader line
|
6873
|
+
# anchors, for both roof and ceiling surfaces. First, attic/plenum roofs.
|
6499
6874
|
[attics, plenums].each do |greniers|
|
6500
6875
|
k = greniers == attics ? :attic : :plenum
|
6501
6876
|
|
@@ -6512,7 +6887,8 @@ module OSut
|
|
6512
6887
|
sts = sts.select { |st| st[:roof] == roof }
|
6513
6888
|
next if sts.empty?
|
6514
6889
|
|
6515
|
-
sts = sts.sort_by { |st| st[:bm2] }
|
6890
|
+
sts = sts.sort_by { |st| st[:bm2] }.reverse
|
6891
|
+
|
6516
6892
|
genAnchors(roof, sts, :box)
|
6517
6893
|
end
|
6518
6894
|
end
|
@@ -6527,7 +6903,7 @@ module OSut
|
|
6527
6903
|
next unless ceiling.key?(k)
|
6528
6904
|
|
6529
6905
|
space = ceiling[:space]
|
6530
|
-
spce = ceiling[k
|
6906
|
+
spce = ceiling[k]
|
6531
6907
|
next unless ceiling.key?(:roofs)
|
6532
6908
|
next unless rooms.key?(space)
|
6533
6909
|
|
@@ -6553,108 +6929,72 @@ module OSut
|
|
6553
6929
|
|
6554
6930
|
next if stz.empty?
|
6555
6931
|
|
6932
|
+
stz = stz.sort_by { |st| st[:cm2] }.reverse
|
6556
6933
|
genAnchors(tile, stz, :cbox)
|
6557
6934
|
end
|
6558
6935
|
|
6559
6936
|
# Delete voided sets.
|
6560
6937
|
sets.reject! { |set| set.key?(:void) }
|
6561
6938
|
|
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)
|
6939
|
+
return empty("sets", mth, WRN, rm2) if sets.empty?
|
6581
6940
|
|
6582
|
-
|
6583
|
-
|
6584
|
-
else
|
6585
|
-
xm2 = aire.get
|
6586
|
-
end
|
6587
|
-
end
|
6588
|
-
end
|
6941
|
+
# Sort sets, from largest to smallest bounded box area.
|
6942
|
+
sets = sets.sort_by { |st| st[:bm2] * st[:m] }.reverse
|
6589
6943
|
|
6590
|
-
|
6591
|
-
|
6592
|
-
|
6593
|
-
end
|
6594
|
-
|
6595
|
-
# Required skylight area to add.
|
6596
|
-
sm2 = rm2 * srr - m2
|
6597
|
-
|
6598
|
-
# Skip if existing skylights exceed or ~roughly match requested %SRR.
|
6599
|
-
if sm2.round(2) < w02.round(2)
|
6600
|
-
log(INF, "Skipping: existing srr > requested srr (#{mth})")
|
6601
|
-
return 0
|
6602
|
-
end
|
6603
|
-
|
6604
|
-
# Any sidelit/sloped roofs being targeted?
|
6605
|
-
#
|
6606
|
-
# TODO: enable double-ridged, sloped roofs have double-sloped
|
6607
|
-
# skylights/wells (patterns "strip"/"strips").
|
6944
|
+
# Any sidelit and/or sloped roofs being targeted?
|
6945
|
+
# @todo: enable double-ridged, sloped roofs have double-sloped
|
6946
|
+
# skylights/wells (patterns "strip"/"strips").
|
6608
6947
|
sidelit = sets.any? { |set| set[:sidelit] }
|
6609
6948
|
sloped = sets.any? { |set| set[:sloped ] }
|
6610
6949
|
|
6950
|
+
# Average sandbox area + revised 'working' SRR%.
|
6951
|
+
sbm2 = sets.map { |set| set[:bm2] }.reduce(:+)
|
6952
|
+
avm2 = sbm2 / sets.size
|
6953
|
+
srr2 = sm2 / sets.size / avm2
|
6954
|
+
|
6611
6955
|
# Precalculate skylight rows + cols, for each selected pattern. In the case
|
6612
6956
|
# of 'cols x rows' arrays of skylights, the method initially overshoots
|
6613
|
-
# with regards to ideal skylight placement, e.g.:
|
6957
|
+
# with regards to 'ideal' skylight placement, e.g.:
|
6614
6958
|
#
|
6615
6959
|
# aceee.org/files/proceedings/2004/data/papers/SS04_Panel3_Paper18.pdf
|
6616
6960
|
#
|
6617
|
-
#
|
6961
|
+
# Skylight areas are subsequently contracted to strictly meet the target.
|
6618
6962
|
sets.each_with_index do |set, i|
|
6619
|
-
|
6620
|
-
well = set.key?(:clng)
|
6621
|
-
space = set[:space]
|
6963
|
+
thin = set[:thin ]
|
6622
6964
|
tight = set[:tight]
|
6623
6965
|
factor = tight ? 1.75 : 1.25
|
6966
|
+
well = set.key?(:clng)
|
6967
|
+
space = set[:space]
|
6624
6968
|
room = rooms[space]
|
6625
6969
|
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
|
6970
|
+
width = alignedWidth( set[:box], true)
|
6971
|
+
depth = alignedHeight(set[:box], true)
|
6972
|
+
barea = set.key?(:om2) ? set[:om2] : set[:bm2]
|
6973
|
+
rtio = barea / avm2
|
6974
|
+
skym2 = srr2 * barea * rtio
|
6635
6975
|
|
6636
|
-
# Flag
|
6976
|
+
# Flag set if too narrow/shallow to hold a single skylight.
|
6637
6977
|
if well
|
6638
6978
|
if width.round(2) < wl.round(2)
|
6639
|
-
log(
|
6979
|
+
log(WRN, "set #{i+1} well: Too narrow (#{mth})")
|
6640
6980
|
set[:void] = true
|
6641
6981
|
next
|
6642
6982
|
end
|
6643
6983
|
|
6644
6984
|
if depth.round(2) < wl.round(2)
|
6645
|
-
log(
|
6985
|
+
log(WRN, "set #{i+1} well: Too shallow (#{mth})")
|
6646
6986
|
set[:void] = true
|
6647
6987
|
next
|
6648
6988
|
end
|
6649
6989
|
else
|
6650
6990
|
if width.round(2) < w0.round(2)
|
6651
|
-
log(
|
6991
|
+
log(WRN, "set #{i+1}: Too narrow (#{mth})")
|
6652
6992
|
set[:void] = true
|
6653
6993
|
next
|
6654
6994
|
end
|
6655
6995
|
|
6656
6996
|
if depth.round(2) < w0.round(2)
|
6657
|
-
log(
|
6997
|
+
log(WRN, "set #{i+1}: Too shallow (#{mth})")
|
6658
6998
|
set[:void] = true
|
6659
6999
|
next
|
6660
7000
|
end
|
@@ -6667,8 +7007,8 @@ module OSut
|
|
6667
7007
|
rows = 1
|
6668
7008
|
wx = w0
|
6669
7009
|
wy = w0
|
6670
|
-
wxl = wl
|
6671
|
-
wyl = wl
|
7010
|
+
wxl = well ? wl : nil
|
7011
|
+
wyl = well ? wl : nil
|
6672
7012
|
dX = nil
|
6673
7013
|
dY = nil
|
6674
7014
|
|
@@ -6676,31 +7016,26 @@ module OSut
|
|
6676
7016
|
when "array" # min 2x cols x min 2x rows
|
6677
7017
|
cols = 2
|
6678
7018
|
rows = 2
|
7019
|
+
next if thin
|
6679
7020
|
|
6680
7021
|
if tight
|
6681
7022
|
sp = 1.4 * h / 2
|
6682
|
-
lx =
|
6683
|
-
ly =
|
7023
|
+
lx = width - cols * wx
|
7024
|
+
ly = depth - rows * wy
|
6684
7025
|
next if lx.round(2) < sp.round(2)
|
6685
7026
|
next if ly.round(2) < sp.round(2)
|
6686
7027
|
|
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
|
-
|
7028
|
+
cols = ((width - wx) / (wx + sp)).round(2).to_i + 1
|
7029
|
+
rows = ((depth - wy) / (wy + sp)).round(2).to_i + 1
|
6695
7030
|
next if cols < 2
|
6696
7031
|
next if rows < 2
|
6697
7032
|
|
6698
|
-
dX =
|
6699
|
-
dY =
|
7033
|
+
dX = bfr + f
|
7034
|
+
dY = bfr + f
|
6700
7035
|
else
|
6701
7036
|
sp = 1.4 * h
|
6702
7037
|
lx = well ? (width - cols * wxl) / cols : (width - cols * wx) / cols
|
6703
|
-
ly = well ? (depth - rows * wyl) / rows : (depth - rows * wy) /
|
7038
|
+
ly = well ? (depth - rows * wyl) / rows : (depth - rows * wy) / rows
|
6704
7039
|
next if lx.round(2) < sp.round(2)
|
6705
7040
|
next if ly.round(2) < sp.round(2)
|
6706
7041
|
|
@@ -6715,246 +7050,238 @@ module OSut
|
|
6715
7050
|
next if cols < 2
|
6716
7051
|
next if rows < 2
|
6717
7052
|
|
6718
|
-
ly = well ? (depth - rows * wyl) / rows : (depth - rows * wy) /
|
7053
|
+
ly = well ? (depth - rows * wyl) / rows : (depth - rows * wy) / rows
|
6719
7054
|
dY = ly / 2
|
6720
7055
|
end
|
6721
7056
|
|
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
|
7057
|
+
# Default allocated skylight area. If undershooting, inflate skylight
|
7058
|
+
# width/depth (with reduced spacing). For geometrically-constrained
|
7059
|
+
# cases, undershooting means not reaching 1.75x the required target.
|
7060
|
+
# Otherwise, undershooting means not reaching 1.25x the required
|
7061
|
+
# target. Any consequent overshooting is later corrected.
|
7062
|
+
tm2 = wx * cols * wy * rows
|
6729
7063
|
|
6730
|
-
# Inflate skylight width/depth (and reduce spacing) to reach
|
6731
|
-
if
|
7064
|
+
# Inflate skylight width/depth (and reduce spacing) to reach target.
|
7065
|
+
if tm2.round(2) < factor * skym2.round(2)
|
6732
7066
|
ratio2 = 1 + (factor * skym2 - tm2) / tm2
|
6733
7067
|
ratio = Math.sqrt(ratio2)
|
6734
7068
|
|
6735
|
-
sp =
|
7069
|
+
sp = wl
|
6736
7070
|
wx *= ratio
|
6737
7071
|
wy *= ratio
|
6738
|
-
wxl = wx + gap
|
6739
|
-
wyl = wy + gap
|
7072
|
+
wxl = wx + gap if well
|
7073
|
+
wyl = wy + gap if well
|
6740
7074
|
|
6741
7075
|
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
|
-
|
7076
|
+
lx = (width - 2 * (bfr + f) - cols * wx) / (cols - 1)
|
7077
|
+
ly = (depth - 2 * (bfr + f) - rows * wy) / (rows - 1)
|
6750
7078
|
lx = lx.round(2) < sp.round(2) ? sp : lx
|
6751
7079
|
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
|
7080
|
+
wx = (width - 2 * (bfr + f) - (cols - 1) * lx) / cols
|
7081
|
+
wy = (depth - 2 * (bfr + f) - (rows - 1) * ly) / rows
|
6764
7082
|
else
|
6765
7083
|
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
|
7084
|
+
lx = (width - cols * wxl) / cols
|
7085
|
+
ly = (depth - rows * wyl) / rows
|
7086
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7087
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
6777
7088
|
wxl = (width - cols * lx) / cols
|
6778
7089
|
wyl = (depth - rows * ly) / rows
|
6779
7090
|
wx = wxl - gap
|
6780
7091
|
wy = wyl - gap
|
6781
|
-
lx = (width - cols * wxl) / cols
|
6782
7092
|
ly = (depth - rows * wyl) / rows
|
6783
7093
|
else
|
7094
|
+
lx = (width - cols * wx) / cols
|
7095
|
+
ly = (depth - rows * wy) / rows
|
7096
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7097
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
6784
7098
|
wx = (width - cols * lx) / cols
|
6785
7099
|
wy = (depth - rows * ly) / rows
|
6786
|
-
wxl = wx + gap
|
6787
|
-
wyl = wy + gap
|
6788
|
-
lx = (width - cols * wx) / cols
|
6789
7100
|
ly = (depth - rows * wy) / rows
|
6790
7101
|
end
|
6791
|
-
end
|
6792
7102
|
|
6793
|
-
|
7103
|
+
dY = ly / 2
|
7104
|
+
end
|
6794
7105
|
end
|
6795
7106
|
when "strips" # min 2x cols x 1x row
|
6796
7107
|
cols = 2
|
6797
7108
|
|
6798
7109
|
if tight
|
6799
7110
|
sp = h / 2
|
6800
|
-
|
6801
|
-
|
7111
|
+
dX = bfr + f
|
7112
|
+
lx = width - cols * wx
|
6802
7113
|
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
7114
|
|
7115
|
+
cols = ((width - wx) / (wx + sp)).round(2).to_i + 1
|
6811
7116
|
next if cols < 2
|
6812
7117
|
|
6813
|
-
if
|
6814
|
-
|
6815
|
-
wy
|
7118
|
+
if thin
|
7119
|
+
dY = bfr + f
|
7120
|
+
wy = depth - 2 * dY
|
7121
|
+
next if wy.round(2) < gap4
|
6816
7122
|
else
|
6817
|
-
|
6818
|
-
|
6819
|
-
end
|
7123
|
+
ly = depth - wy
|
7124
|
+
next if ly.round(2) < wl.round(2)
|
6820
7125
|
|
6821
|
-
|
6822
|
-
|
7126
|
+
dY = ly / 2
|
7127
|
+
end
|
6823
7128
|
else
|
6824
7129
|
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
7130
|
|
6830
7131
|
if well
|
7132
|
+
lx = (width - cols * wxl) / cols
|
7133
|
+
next if lx.round(2) < sp.round(2)
|
7134
|
+
|
6831
7135
|
cols = (width / (wxl + sp)).round(2).to_i
|
7136
|
+
next if cols < 2
|
7137
|
+
|
7138
|
+
ly = depth - wyl
|
7139
|
+
dY = ly / 2
|
7140
|
+
next if ly.round(2) < wl.round(2)
|
6832
7141
|
else
|
7142
|
+
lx = (width - cols * wx) / cols
|
7143
|
+
next if lx.round(2) < sp.round(2)
|
7144
|
+
|
6833
7145
|
cols = (width / (wx + sp)).round(2).to_i
|
6834
|
-
|
7146
|
+
next if cols < 2
|
6835
7147
|
|
6836
|
-
|
7148
|
+
if thin
|
7149
|
+
dY = bfr + f
|
7150
|
+
wy = depth - 2 * dY
|
7151
|
+
next if wy.round(2) < gap4
|
7152
|
+
else
|
7153
|
+
ly = depth - wy
|
7154
|
+
next if ly.round(2) < wl.round(2)
|
6837
7155
|
|
6838
|
-
|
6839
|
-
|
6840
|
-
wy = wyl - gap
|
6841
|
-
else
|
6842
|
-
wy = depth - ly
|
6843
|
-
wyl = wy + gap
|
7156
|
+
dY = ly / 2
|
7157
|
+
end
|
6844
7158
|
end
|
6845
|
-
|
6846
|
-
dY = ly / 2
|
6847
7159
|
end
|
6848
7160
|
|
6849
|
-
tm2
|
6850
|
-
undershot = tm2.round(2) < factor * skym2.round(2) ? true : false
|
7161
|
+
tm2 = wx * cols * wy
|
6851
7162
|
|
6852
|
-
# Inflate skylight
|
6853
|
-
if
|
6854
|
-
|
7163
|
+
# Inflate skylight depth to reach target.
|
7164
|
+
if tm2.round(2) < factor * skym2.round(2)
|
7165
|
+
sp = wl
|
6855
7166
|
|
6856
|
-
|
6857
|
-
|
6858
|
-
|
7167
|
+
# Skip if already thin.
|
7168
|
+
unless thin
|
7169
|
+
ratio2 = 1 + (factor * skym2 - tm2) / tm2
|
7170
|
+
|
7171
|
+
wy *= ratio2
|
6859
7172
|
|
6860
|
-
if tight
|
6861
7173
|
if well
|
6862
|
-
|
7174
|
+
wyl = wy + gap
|
7175
|
+
ly = depth - wyl
|
7176
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
7177
|
+
wyl = depth - ly
|
7178
|
+
wy = wyl - gap
|
6863
7179
|
else
|
6864
|
-
|
7180
|
+
ly = depth - wy
|
7181
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
7182
|
+
wy = depth - ly
|
6865
7183
|
end
|
6866
7184
|
|
6867
|
-
|
7185
|
+
dY = ly / 2
|
7186
|
+
end
|
7187
|
+
end
|
6868
7188
|
|
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
|
7189
|
+
tm2 = wx * cols * wy
|
7190
|
+
|
7191
|
+
# Inflate skylight width (and reduce spacing) to reach target.
|
7192
|
+
if tm2.round(2) < factor * skym2.round(2)
|
7193
|
+
ratio2 = 1 + (factor * skym2 - tm2) / tm2
|
6882
7194
|
|
6883
|
-
|
7195
|
+
wx *= ratio2
|
7196
|
+
wxl = wx + gap if well
|
6884
7197
|
|
7198
|
+
if tight
|
7199
|
+
lx = (width - 2 * (bfr + f) - cols * wx) / (cols - 1)
|
7200
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7201
|
+
wx = (width - 2 * (bfr + f) - (cols - 1) * lx) / cols
|
7202
|
+
else
|
6885
7203
|
if well
|
7204
|
+
lx = (width - cols * wxl) / cols
|
7205
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
6886
7206
|
wxl = (width - cols * lx) / cols
|
6887
7207
|
wx = wxl - gap
|
6888
|
-
lx = (width - cols * wxl) / cols
|
6889
7208
|
else
|
6890
|
-
wx = (width - cols * lx) / cols
|
6891
|
-
wxl = wx + gap
|
6892
7209
|
lx = (width - cols * wx) / cols
|
7210
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7211
|
+
wx = (width - cols * lx) / cols
|
6893
7212
|
end
|
6894
7213
|
end
|
6895
7214
|
end
|
6896
7215
|
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
7216
|
if tight
|
6902
|
-
|
6903
|
-
|
6904
|
-
|
6905
|
-
if
|
6906
|
-
|
6907
|
-
|
6908
|
-
|
6909
|
-
wy
|
7217
|
+
sp = gap4
|
7218
|
+
dX = bfr + f
|
7219
|
+
wx = width - 2 * dX
|
7220
|
+
next if wx.round(2) < sp.round(2)
|
7221
|
+
|
7222
|
+
if thin
|
7223
|
+
dY = bfr + f
|
7224
|
+
wy = depth - 2 * dY
|
7225
|
+
next if wy.round(2) < sp.round(2)
|
6910
7226
|
else
|
6911
|
-
|
6912
|
-
|
6913
|
-
|
6914
|
-
wyl = wy + gap
|
7227
|
+
ly = depth - wy
|
7228
|
+
dY = ly / 2
|
7229
|
+
next if ly.round(2) < sp.round(2)
|
6915
7230
|
end
|
6916
|
-
|
6917
|
-
dX = well ? 0.0 : bfr + f
|
6918
|
-
dY = ly / 2
|
6919
7231
|
else
|
7232
|
+
sp = wl
|
7233
|
+
lx = well ? width - wxl : width - wx
|
7234
|
+
ly = well ? depth - wyl : depth - wy
|
7235
|
+
dY = ly / 2
|
6920
7236
|
next if lx.round(2) < sp.round(2)
|
6921
7237
|
next if ly.round(2) < sp.round(2)
|
7238
|
+
end
|
6922
7239
|
|
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
|
7240
|
+
tm2 = wx * wy
|
6934
7241
|
|
6935
|
-
|
7242
|
+
# Inflate skylight width (and reduce spacing) to reach target.
|
7243
|
+
if tm2.round(2) < factor * skym2.round(2)
|
7244
|
+
unless tight
|
7245
|
+
ratio2 = 1 + (factor * skym2 - tm2) / tm2
|
7246
|
+
|
7247
|
+
wx *= ratio2
|
7248
|
+
|
7249
|
+
if well
|
7250
|
+
wxl = wx + gap
|
7251
|
+
lx = width - wxl
|
7252
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7253
|
+
wxl = width - lx
|
7254
|
+
wx = wxl - gap
|
7255
|
+
else
|
7256
|
+
lx = width - wx
|
7257
|
+
lx = lx.round(2) < sp.round(2) ? sp : lx
|
7258
|
+
wx = width - lx
|
7259
|
+
end
|
7260
|
+
end
|
6936
7261
|
end
|
6937
7262
|
|
6938
|
-
tm2
|
6939
|
-
undershot = tm2.round(2) < factor * skym2.round(2) ? true : false
|
7263
|
+
tm2 = wx * wy
|
6940
7264
|
|
6941
|
-
# Inflate skylight depth to reach
|
6942
|
-
if
|
6943
|
-
|
7265
|
+
# Inflate skylight depth to reach target. Skip if already tight thin.
|
7266
|
+
if tm2.round(2) < factor * skym2.round(2)
|
7267
|
+
unless thin
|
7268
|
+
ratio2 = 1 + (factor * skym2 - tm2) / tm2
|
6944
7269
|
|
6945
|
-
|
6946
|
-
wy *= ratio2
|
6947
|
-
wyl = wy + gap
|
7270
|
+
wy *= ratio2
|
6948
7271
|
|
6949
|
-
|
6950
|
-
|
7272
|
+
if well
|
7273
|
+
wyl = wy + gap
|
7274
|
+
ly = depth - wyl
|
7275
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
7276
|
+
wyl = depth - ly
|
7277
|
+
wy = wyl - gap
|
7278
|
+
else
|
7279
|
+
ly = depth - wy
|
7280
|
+
ly = ly.round(2) < sp.round(2) ? sp : ly
|
7281
|
+
wy = depth - ly
|
7282
|
+
end
|
6951
7283
|
|
6952
|
-
|
6953
|
-
wyl = depth - ly
|
6954
|
-
wy = wyl - gap
|
6955
|
-
else
|
6956
|
-
wy = depth - ly
|
6957
|
-
wyl = wy + gap
|
7284
|
+
dY = ly / 2
|
6958
7285
|
end
|
6959
7286
|
end
|
6960
7287
|
end
|
@@ -6972,10 +7299,13 @@ module OSut
|
|
6972
7299
|
|
6973
7300
|
set[pattern] = st
|
6974
7301
|
end
|
7302
|
+
|
7303
|
+
set[:void] = true unless patterns.any? { |k| set.key?(k) }
|
6975
7304
|
end
|
6976
7305
|
|
6977
7306
|
# Delete voided sets.
|
6978
7307
|
sets.reject! { |set| set.key?(:void) }
|
7308
|
+
return empty("sets (2)", mth, WRN, rm2) if sets.empty?
|
6979
7309
|
|
6980
7310
|
# Final reset of filters.
|
6981
7311
|
filters.map! { |f| f.include?("b") ? f.delete("b") : f } unless sidelit
|
@@ -6986,15 +7316,15 @@ module OSut
|
|
6986
7316
|
filters.reject! { |f| f.empty? }
|
6987
7317
|
filters.uniq!
|
6988
7318
|
|
6989
|
-
# Initialize skylight area tally.
|
7319
|
+
# Initialize skylight area tally (to increment).
|
6990
7320
|
skm2 = 0
|
6991
7321
|
|
6992
7322
|
# Assign skylight pattern.
|
6993
|
-
filters.
|
7323
|
+
filters.each do |filter|
|
6994
7324
|
next if skm2.round(2) >= sm2.round(2)
|
6995
7325
|
|
7326
|
+
dm2 = sm2 - skm2 # differential (remaining skylight area to meet).
|
6996
7327
|
sts = sets
|
6997
|
-
sts = sts.sort_by { |st| st[:bm2] }.reverse!
|
6998
7328
|
sts = sts.reject { |st| st.key?(:pattern) }
|
6999
7329
|
|
7000
7330
|
if filter.include?("a")
|
@@ -7029,33 +7359,49 @@ module OSut
|
|
7029
7359
|
|
7030
7360
|
fpm2[pattern] = {m2: 0, tight: false} unless fpm2.key?(pattern)
|
7031
7361
|
|
7032
|
-
fpm2[pattern][:m2 ] += wx * wy * cols * rows
|
7033
|
-
fpm2[pattern][:tight] = st[:tight]
|
7362
|
+
fpm2[pattern][:m2 ] += st[:m] * wx * wy * cols * rows
|
7363
|
+
fpm2[pattern][:tight] = true if st[:tight]
|
7034
7364
|
end
|
7035
7365
|
end
|
7036
7366
|
|
7037
7367
|
pattern = nil
|
7038
7368
|
next if fpm2.empty?
|
7039
7369
|
|
7040
|
-
|
7041
|
-
|
7042
|
-
# Select suitable pattern, often overshooting. Favour array unless
|
7043
|
-
# geometrically constrainted.
|
7370
|
+
# Favour (large) arrays if meeting residual target, unless constrained.
|
7044
7371
|
if fpm2.keys.include?("array")
|
7045
|
-
if
|
7372
|
+
if fpm2["array"][:m2].round(2) >= dm2.round(2)
|
7046
7373
|
pattern = "array" unless fpm2[:tight]
|
7047
7374
|
end
|
7048
7375
|
end
|
7049
7376
|
|
7050
7377
|
unless pattern
|
7051
|
-
|
7052
|
-
|
7053
|
-
|
7054
|
-
|
7378
|
+
fpm2 = fpm2.sort_by { |_, fm2| fm2[:m2] }.to_h
|
7379
|
+
min_m2 = fpm2.values.first[:m2]
|
7380
|
+
max_m2 = fpm2.values.last[:m2]
|
7381
|
+
|
7382
|
+
if min_m2.round(2) >= dm2.round(2)
|
7383
|
+
# If not large array, then retain pattern generating smallest skylight
|
7384
|
+
# area if ALL patterns >= residual target (deterministic sorting).
|
7385
|
+
fpm2.keep_if { |_, fm2| fm2[:m2].round(2) == min_m2.round(2) }
|
7386
|
+
|
7387
|
+
if fpm2.keys.include?("array")
|
7388
|
+
pattern = "array"
|
7389
|
+
elsif fpm2.keys.include?("strips")
|
7390
|
+
pattern = "strips"
|
7391
|
+
else fpm2.keys.include?("strip")
|
7392
|
+
pattern = "strip"
|
7393
|
+
end
|
7055
7394
|
else
|
7056
|
-
|
7057
|
-
|
7058
|
-
|
7395
|
+
# Pick pattern offering greatest skylight area (deterministic sorting).
|
7396
|
+
fpm2.keep_if { |_, fm2| fm2[:m2].round(2) == max_m2.round(2) }
|
7397
|
+
|
7398
|
+
if fpm2.keys.include?("strip")
|
7399
|
+
pattern = "strip"
|
7400
|
+
elsif fpm2.keys.include?("strips")
|
7401
|
+
pattern = "strips"
|
7402
|
+
else fpm2.keys.include?("array")
|
7403
|
+
pattern = "array"
|
7404
|
+
end
|
7059
7405
|
end
|
7060
7406
|
end
|
7061
7407
|
|
@@ -7086,55 +7432,162 @@ module OSut
|
|
7086
7432
|
end
|
7087
7433
|
end
|
7088
7434
|
|
7089
|
-
#
|
7090
|
-
|
7091
|
-
|
7435
|
+
# Delete incomplete sets (same as rejected if 'voided').
|
7436
|
+
sets.reject! { |set| set.key?(:void) }
|
7437
|
+
sets.select! { |set| set.key?(:pattern) }
|
7438
|
+
return empty("sets (3)", mth, WRN, rm2) if sets.empty?
|
7439
|
+
|
7440
|
+
# Skylight size contraction if overshot (e.g. scale down by -13% if > +13%).
|
7441
|
+
# Applied on a surface/pattern basis: individual skylight sizes may vary
|
7442
|
+
# from one surface to the next, depending on respective patterns.
|
7443
|
+
|
7444
|
+
# First, skip whole sets altogether if their total m2 < (skm2 - sm2). Only
|
7445
|
+
# considered if significant discrepancies vs average set skylight m2.
|
7446
|
+
sbm2 = 0
|
7447
|
+
|
7448
|
+
sets.each do |set|
|
7449
|
+
sbm2 += set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7450
|
+
end
|
7451
|
+
|
7452
|
+
avm2 = sbm2 / sets.size
|
7453
|
+
|
7454
|
+
if skm2.round(2) > sm2.round(2)
|
7455
|
+
sets.reverse.each do |set|
|
7456
|
+
break unless skm2.round(2) > sm2.round(2)
|
7457
|
+
|
7458
|
+
stm2 = set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7459
|
+
next unless stm2 < 0.75 * avm2
|
7460
|
+
next unless stm2.round(2) < (skm2 - sm2).round(2)
|
7461
|
+
|
7462
|
+
skm2 -= stm2
|
7463
|
+
set[:void] = true
|
7464
|
+
end
|
7465
|
+
end
|
7466
|
+
|
7467
|
+
sets.reject! { |set| set.key?(:void) }
|
7468
|
+
return empty("sets (4)", mth, WRN, rm2) if sets.empty?
|
7469
|
+
|
7470
|
+
# Size contraction: round 1: low-hanging fruit.
|
7092
7471
|
if skm2.round(2) > sm2.round(2)
|
7093
7472
|
ratio2 = 1 - (skm2 - sm2) / skm2
|
7094
7473
|
ratio = Math.sqrt(ratio2)
|
7095
|
-
skm2 *= ratio2
|
7096
7474
|
|
7097
7475
|
sets.each do |set|
|
7098
|
-
|
7099
|
-
|
7476
|
+
am2 = set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7477
|
+
xr = set[:w]
|
7478
|
+
yr = set[:d]
|
7100
7479
|
|
7101
|
-
|
7102
|
-
|
7480
|
+
if xr > w0
|
7481
|
+
xr = xr * ratio < w0 ? w0 : xr * ratio
|
7482
|
+
end
|
7103
7483
|
|
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
|
7484
|
+
if yr > w0
|
7485
|
+
yr = yr * ratio < w0 ? w0 : yr * ratio
|
7127
7486
|
end
|
7487
|
+
|
7488
|
+
xm2 = set[:cols] * xr * set[:rows] * yr * set[:m]
|
7489
|
+
next if xm2.round(2) == am2.round(2)
|
7490
|
+
|
7491
|
+
set[:dY] += (set[:d] - yr) / 2
|
7492
|
+
set[:dX] += (set[:w] - xr) / 2 if set.key?(:dX)
|
7493
|
+
set[:w ] = xr
|
7494
|
+
set[:d ] = yr
|
7495
|
+
set[:w0] = set[:w] + gap
|
7496
|
+
set[:d0] = set[:d] + gap
|
7497
|
+
|
7498
|
+
skm2 -= (am2 - xm2)
|
7499
|
+
end
|
7500
|
+
end
|
7501
|
+
|
7502
|
+
# Size contraction: round 2: prioritize larger sets.
|
7503
|
+
adm2 = 0
|
7504
|
+
|
7505
|
+
sets.each_with_index do |set, i|
|
7506
|
+
next if set[:w].round(2) <= w0
|
7507
|
+
next if set[:d].round(2) <= w0
|
7508
|
+
|
7509
|
+
adm2 += set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7510
|
+
end
|
7511
|
+
|
7512
|
+
if skm2.round(2) > sm2.round(2) && adm2.round(2) > sm2.round(2)
|
7513
|
+
ratio2 = 1 - (adm2 - sm2) / adm2
|
7514
|
+
ratio = Math.sqrt(ratio2)
|
7515
|
+
|
7516
|
+
sets.each do |set|
|
7517
|
+
next if set[:w].round(2) <= w0
|
7518
|
+
next if set[:d].round(2) <= w0
|
7519
|
+
|
7520
|
+
am2 = set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7521
|
+
xr = set[:w]
|
7522
|
+
yr = set[:d]
|
7523
|
+
|
7524
|
+
if xr > w0
|
7525
|
+
xr = xr * ratio < w0 ? w0 : xr * ratio
|
7526
|
+
end
|
7527
|
+
|
7528
|
+
if yr > w0
|
7529
|
+
yr = yr * ratio < w0 ? w0 : yr * ratio
|
7530
|
+
end
|
7531
|
+
|
7532
|
+
xm2 = set[:cols] * xr * set[:rows] * yr * set[:m]
|
7533
|
+
next if xm2.round(2) == am2.round(2)
|
7534
|
+
|
7535
|
+
set[:dY] += (set[:d] - yr) / 2
|
7536
|
+
set[:dX] += (set[:w] - xr) / 2 if set.key?(:dX)
|
7537
|
+
set[:w ] = xr
|
7538
|
+
set[:d ] = yr
|
7539
|
+
set[:w0] = set[:w] + gap
|
7540
|
+
set[:d0] = set[:d] + gap
|
7541
|
+
|
7542
|
+
skm2 -= (am2 - xm2)
|
7543
|
+
adm2 -= (am2 - xm2)
|
7128
7544
|
end
|
7129
7545
|
end
|
7130
7546
|
|
7131
|
-
#
|
7547
|
+
# Size contraction: round 3: Resort to sizes < requested w0.
|
7548
|
+
if skm2.round(2) > sm2.round(2)
|
7549
|
+
ratio2 = 1 - (skm2 - sm2) / skm2
|
7550
|
+
ratio = Math.sqrt(ratio2)
|
7551
|
+
|
7552
|
+
sets.each do |set|
|
7553
|
+
break unless skm2.round(2) > sm2.round(2)
|
7554
|
+
|
7555
|
+
am2 = set[:cols] * set[:w] * set[:rows] * set[:d] * set[:m]
|
7556
|
+
xr = set[:w]
|
7557
|
+
yr = set[:d]
|
7558
|
+
|
7559
|
+
if xr > gap4
|
7560
|
+
xr = xr * ratio < gap4 ? gap4 : xr * ratio
|
7561
|
+
end
|
7562
|
+
|
7563
|
+
if yr > gap4
|
7564
|
+
yr = yr * ratio < gap4 ? gap4 : yr * ratio
|
7565
|
+
end
|
7566
|
+
|
7567
|
+
xm2 = set[:cols] * xr * set[:rows] * yr * set[:m]
|
7568
|
+
next if xm2.round(2) == am2.round(2)
|
7569
|
+
|
7570
|
+
set[:dY] += (set[:d] - yr) / 2
|
7571
|
+
set[:dX] += (set[:w] - xr) / 2 if set.key?(:dX)
|
7572
|
+
set[:w ] = xr
|
7573
|
+
set[:d ] = yr
|
7574
|
+
set[:w0] = set[:w] + gap
|
7575
|
+
set[:d0] = set[:d] + gap
|
7576
|
+
|
7577
|
+
skm2 -= (am2 - xm2)
|
7578
|
+
end
|
7579
|
+
end
|
7580
|
+
|
7581
|
+
# Log warning if unable to entirely contract skylight dimensions.
|
7582
|
+
if skm2.round(2) > sm2.round(2)
|
7583
|
+
log(WRN, "Skylights slightly oversized (#{mth})")
|
7584
|
+
end
|
7585
|
+
|
7586
|
+
# Generate skylight well vertices for roofs, attics & plenums.
|
7132
7587
|
[attics, plenums].each do |greniers|
|
7133
7588
|
k = greniers == attics ? :attic : :plenum
|
7134
7589
|
|
7135
7590
|
greniers.each do |spce, grenier|
|
7136
|
-
ti = grenier[:t]
|
7137
|
-
|
7138
7591
|
grenier[:roofs].each do |roof|
|
7139
7592
|
sts = sets
|
7140
7593
|
sts = sts.select { |st| st.key?(k) }
|
@@ -7150,15 +7603,15 @@ module OSut
|
|
7150
7603
|
sts = sts.select { |st| st[:ld].key?(roof) }
|
7151
7604
|
next if sts.empty?
|
7152
7605
|
|
7153
|
-
# If successful, 'genInserts' returns extended
|
7154
|
-
# including leader lines to support cutouts. The
|
7606
|
+
# If successful, 'genInserts' returns extended ROOF surface vertices,
|
7607
|
+
# including leader lines to support cutouts. The method also generates
|
7608
|
+
# new roof inserts. See key:value pair :vts. The FINAL go/no-go is
|
7155
7609
|
# 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.
|
7610
|
+
# inserts (vis-à-vis attic/plenum floor below).
|
7158
7611
|
vz = genInserts(roof, sts)
|
7159
|
-
next if vz.empty?
|
7612
|
+
next if vz.empty?
|
7160
7613
|
|
7161
|
-
roof.setVertices(
|
7614
|
+
roof.setVertices(vz)
|
7162
7615
|
end
|
7163
7616
|
end
|
7164
7617
|
end
|
@@ -7178,14 +7631,15 @@ module OSut
|
|
7178
7631
|
|
7179
7632
|
room = rooms[space]
|
7180
7633
|
grenier = greniers[spce]
|
7181
|
-
ti = grenier[:
|
7182
|
-
t0 = room[:
|
7634
|
+
ti = grenier[:ti]
|
7635
|
+
t0 = room[:t0]
|
7183
7636
|
stz = []
|
7184
7637
|
|
7185
7638
|
ceiling[:roofs].each do |roof|
|
7186
7639
|
sts = sets
|
7187
7640
|
|
7188
7641
|
sts = sts.select { |st| st.key?(k) }
|
7642
|
+
sts = sts.select { |st| st.key?(:pattern) }
|
7189
7643
|
sts = sts.select { |st| st.key?(:clng) }
|
7190
7644
|
sts = sts.select { |st| st.key?(:cm2) }
|
7191
7645
|
sts = sts.select { |st| st.key?(:roof) }
|
@@ -7206,104 +7660,109 @@ module OSut
|
|
7206
7660
|
|
7207
7661
|
next if stz.empty?
|
7208
7662
|
|
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
7663
|
# Add new roof inserts & skylights for the (now) toplit space.
|
7224
7664
|
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
|
7665
|
+
sub = {}
|
7666
|
+
sub[:type ] = "Skylight"
|
7667
|
+
sub[:frame] = frame if frame
|
7668
|
+
sub[:sill ] = gap / 2
|
7231
7669
|
|
7232
7670
|
st[:vts].each do |id, vt|
|
7233
|
-
roof = OpenStudio::Model::Surface.new(t0.inverse * vt, mdl)
|
7671
|
+
roof = OpenStudio::Model::Surface.new(t0.inverse * (ti * vt), mdl)
|
7234
7672
|
roof.setSpace(space)
|
7235
|
-
roof.setName("#{
|
7673
|
+
roof.setName("#{id}:#{space.nameString}")
|
7236
7674
|
|
7237
7675
|
# Generate well walls.
|
7238
|
-
v0 = roof.vertices
|
7239
7676
|
vX = cast(roof, tile, ray)
|
7240
|
-
s0 = getSegments(
|
7241
|
-
sX = getSegments(vX)
|
7677
|
+
s0 = getSegments(t0 * roof.vertices)
|
7678
|
+
sX = getSegments(t0 * vX)
|
7242
7679
|
|
7243
7680
|
s0.each_with_index do |sg, j|
|
7244
7681
|
sg0 = sg.to_a
|
7245
7682
|
sgX = sX[j].to_a
|
7246
|
-
vec
|
7683
|
+
vec = OpenStudio::Point3dVector.new
|
7247
7684
|
vec << sg0.first
|
7248
7685
|
vec << sg0.last
|
7249
7686
|
vec << sgX.last
|
7250
7687
|
vec << sgX.first
|
7251
7688
|
|
7252
|
-
|
7689
|
+
v_grenier = ti.inverse * vec
|
7690
|
+
v_room = (t0.inverse * vec).to_a.reverse
|
7691
|
+
|
7692
|
+
grenier_wall = OpenStudio::Model::Surface.new(v_grenier, mdl)
|
7253
7693
|
grenier_wall.setSpace(spce)
|
7254
|
-
grenier_wall.setName("#{id}:#{j}:#{spce.nameString}")
|
7694
|
+
grenier_wall.setName("#{id}:#{i}:#{j}:#{spce.nameString}")
|
7255
7695
|
|
7256
|
-
room_wall = OpenStudio::Model::Surface.new(
|
7696
|
+
room_wall = OpenStudio::Model::Surface.new(v_room, mdl)
|
7257
7697
|
room_wall.setSpace(space)
|
7258
|
-
room_wall.setName("#{id}:#{j}:#{space.nameString}")
|
7698
|
+
room_wall.setName("#{id}:#{i}:#{j}:#{space.nameString}")
|
7259
7699
|
|
7260
7700
|
grenier_wall.setAdjacentSurface(room_wall)
|
7261
7701
|
room_wall.setAdjacentSurface(grenier_wall)
|
7262
7702
|
end
|
7263
7703
|
|
7264
|
-
# Add individual skylights.
|
7265
|
-
|
7704
|
+
# Add individual skylights. Independently of the set layout (rows x
|
7705
|
+
# cols), individual roof inserts may be deeper than wider (or
|
7706
|
+
# vice-versa). Adapt skylight width vs depth accordingly.
|
7707
|
+
if st[:d].round(2) > st[:w].round(2)
|
7708
|
+
sub[:width ] = st[:d] - f2
|
7709
|
+
sub[:height] = st[:w] - f2
|
7710
|
+
else
|
7711
|
+
sub[:width ] = st[:w] - f2
|
7712
|
+
sub[:height] = st[:d] - f2
|
7713
|
+
end
|
7714
|
+
|
7715
|
+
sub[:id] = roof.nameString
|
7716
|
+
addSubs(roof, sub, false, true, true)
|
7266
7717
|
end
|
7267
7718
|
end
|
7719
|
+
|
7720
|
+
# Vertically-cast set roof :vtx onto ceiling.
|
7721
|
+
stz.each do |st|
|
7722
|
+
st[:cvtx] = t0.inverse * cast(ti * st[:vtx], t0 * tile.vertices, ray)
|
7723
|
+
end
|
7724
|
+
|
7725
|
+
# Extended ceiling vertices.
|
7726
|
+
vertices = genExtendedVertices(tile, stz, :cvtx)
|
7727
|
+
next if vertices.empty?
|
7728
|
+
|
7729
|
+
# Reset ceiling and adjacent floor vertices.
|
7730
|
+
tile.setVertices(vertices)
|
7731
|
+
floor.setVertices(ti.inverse * (t0 * vertices).to_a.reverse)
|
7268
7732
|
end
|
7269
7733
|
|
7270
|
-
#
|
7271
|
-
# coordinate adjustments.
|
7734
|
+
# Loop through 'direct' roof surfaces of rooms to toplit (no attics or
|
7735
|
+
# plenums). No overlaps, so no relative space coordinate adjustments.
|
7272
7736
|
rooms.each do |space, room|
|
7273
7737
|
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|
|
7738
|
+
sets.each_with_index do |st, i|
|
7739
|
+
next unless st.key?(:roof)
|
7740
|
+
next unless st[:roof] == roof
|
7741
|
+
next if st.key?(:clng)
|
7742
|
+
next unless st.key?(:box)
|
7743
|
+
next unless st.key?(:cols)
|
7744
|
+
next unless st.key?(:rows)
|
7745
|
+
next unless st.key?(:d)
|
7746
|
+
next unless st.key?(:w)
|
7747
|
+
next unless st.key?(:dY)
|
7748
|
+
|
7749
|
+
w1 = st[:w ] - f2
|
7750
|
+
d1 = st[:d ] - f2
|
7751
|
+
dY = st[:dY]
|
7752
|
+
|
7753
|
+
st[:rows].times.each do |j|
|
7296
7754
|
sub = {}
|
7297
7755
|
sub[:type ] = "Skylight"
|
7298
|
-
sub[:count ] =
|
7756
|
+
sub[:count ] = st[:cols]
|
7299
7757
|
sub[:width ] = w1
|
7300
7758
|
sub[:height ] = d1
|
7301
7759
|
sub[:frame ] = frame if frame
|
7302
|
-
sub[:id ] = "
|
7760
|
+
sub[:id ] = "#{roof.nameString}:#{i}:#{j}"
|
7303
7761
|
sub[:sill ] = dY + j * (2 * dY + d1)
|
7304
|
-
sub[:r_buffer] =
|
7305
|
-
sub[:l_buffer] =
|
7306
|
-
|
7762
|
+
sub[:r_buffer] = st[:dX] if st[:dX]
|
7763
|
+
sub[:l_buffer] = st[:dX] if st[:dX]
|
7764
|
+
|
7765
|
+
addSubs(roof, sub, false, true, true)
|
7307
7766
|
end
|
7308
7767
|
end
|
7309
7768
|
end
|