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