feat: Initial filter pressure drop views, calculations need implemented.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -40,6 +40,11 @@ extension ApiController: DependencyKey {
|
||||
logger.debug("Calculating dehumidifier size: \(request)")
|
||||
return try await request.respond(logger)
|
||||
|
||||
case let .calculateFilterPressureDrop(request):
|
||||
logger.debug("Calculating filter pressure drop: \(request)")
|
||||
// FIX:
|
||||
fatalError()
|
||||
|
||||
case let .calculateHVACSystemPerformance(request):
|
||||
logger.debug("Calculating hvac system performance: \(request)")
|
||||
return try await request.respond(logger: logger)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
public enum ClimateZone: String, Codable, Equatable, Sendable {
|
||||
case dry
|
||||
public enum ClimateZone: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
// NOTE: Keep in this order.
|
||||
|
||||
case hotHumid
|
||||
case marine
|
||||
case moist
|
||||
case dry
|
||||
case marine
|
||||
|
||||
public var zoneIdentifiers: [String] {
|
||||
switch self {
|
||||
@@ -26,6 +28,6 @@ public enum ClimateZone: String, Codable, Equatable, Sendable {
|
||||
}
|
||||
|
||||
public var label: String {
|
||||
return "\(rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ",")))"
|
||||
return "\(self == .hotHumid ? "Hot Humid" : rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ", ")))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,25 @@ public enum FilterPressureDrop {
|
||||
Calculate filter pressure drop and sizing based on system requirements.
|
||||
"""
|
||||
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
case basic
|
||||
case fanLaw
|
||||
}
|
||||
|
||||
public enum Request: Codable, Equatable, Sendable {
|
||||
case basic(Basic)
|
||||
case fanLaw(FanLaw)
|
||||
|
||||
public struct Basic: Codable, Equatable, Sendable {
|
||||
|
||||
let systemSize: HVACSystemSize
|
||||
let systemSize: Double
|
||||
let climateZone: ClimateZone
|
||||
let filterType: FilterType
|
||||
let filterWidth: Double
|
||||
let filterHeight: Double
|
||||
|
||||
public init(
|
||||
systemSize: HVACSystemSize,
|
||||
systemSize: Double,
|
||||
climateZone: ClimateZone,
|
||||
filterType: FilterPressureDrop.FilterType,
|
||||
filterWidth: Double,
|
||||
@@ -58,22 +63,30 @@ public enum FilterPressureDrop {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Result: Codable, Equatable, Sendable {
|
||||
public enum Response: Codable, Equatable, Sendable {
|
||||
case basic(Basic)
|
||||
case fanLaw(FanLaw)
|
||||
|
||||
public struct Basic: Codable, Equatable, Sendable {
|
||||
|
||||
let filterArea: Double
|
||||
let feetPerMinute: Double
|
||||
let initialPressureDrop: Double
|
||||
let maxPressureDrop: Double
|
||||
public let filterArea: Double
|
||||
public let feetPerMinute: Double
|
||||
public let initialPressureDrop: Double
|
||||
public let maxPressureDrop: Double
|
||||
public let warnings: [String]
|
||||
|
||||
public init(filterArea: Double, feetPerMinute: Double, initialPressureDrop: Double, maxPressureDrop: Double) {
|
||||
public init(
|
||||
filterArea: Double,
|
||||
feetPerMinute: Double,
|
||||
initialPressureDrop: Double,
|
||||
maxPressureDrop: Double,
|
||||
warnings: [String]
|
||||
) {
|
||||
self.filterArea = filterArea
|
||||
self.feetPerMinute = feetPerMinute
|
||||
self.initialPressureDrop = initialPressureDrop
|
||||
self.maxPressureDrop = maxPressureDrop
|
||||
self.warnings = warnings
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +111,7 @@ public enum FilterPressureDrop {
|
||||
}
|
||||
}
|
||||
|
||||
public enum FilterType: String, Codable, Equatable, Sendable {
|
||||
public enum FilterType: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
case fiberglass
|
||||
case pleatedBasic
|
||||
case pleatedBetter
|
||||
@@ -114,3 +127,34 @@ public enum FilterPressureDrop {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
public extension FilterPressureDrop.Response {
|
||||
|
||||
static func mock(mode: FilterPressureDrop.Mode) -> Self {
|
||||
switch mode {
|
||||
case .basic:
|
||||
return .basic(.init(
|
||||
filterArea: 3.47,
|
||||
feetPerMinute: 230,
|
||||
initialPressureDrop: 0.2,
|
||||
maxPressureDrop: 0.15,
|
||||
warnings: [
|
||||
"""
|
||||
Intial pressure drop is more than 50% of maximum allowable.
|
||||
Consider using a larger filter or different type with lower intial pressure drop.
|
||||
"""
|
||||
]
|
||||
))
|
||||
case .fanLaw:
|
||||
return .fanLaw(.init(
|
||||
predictedPressureDrop: 0.127,
|
||||
velocityRatio: 1.13,
|
||||
pressureRatio: 1.27,
|
||||
faceVelocity: 259
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
public enum HVACSystemSize: Double, Codable, Equatable, Sendable {
|
||||
public enum HVACSystemSize: Double, CaseIterable, Codable, Equatable, Sendable {
|
||||
case one = 1
|
||||
case oneAndAHalf = 1.5
|
||||
case two = 2
|
||||
|
||||
@@ -13,6 +13,10 @@ public enum SiteRoute: Equatable, Sendable {
|
||||
Route(.case(Self.api)) {
|
||||
Api.router
|
||||
}
|
||||
Route(.case(Self.health)) {
|
||||
Path { "health" }
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.view)) {
|
||||
View.router
|
||||
}
|
||||
@@ -26,6 +30,7 @@ public extension SiteRoute {
|
||||
case calculateAtticVentilation(AtticVentilation.Request)
|
||||
case calculateCapacitor(Capacitor.Request)
|
||||
case calculateDehumidifierSize(DehumidifierSize.Request)
|
||||
case calculateFilterPressureDrop(FilterPressureDrop.Request)
|
||||
case calculateHVACSystemPerformance(HVACSystemPerformance.Request)
|
||||
case calculateMoldRisk(MoldRisk.Request)
|
||||
case calculateRoomPressure(RoomPressure.Request)
|
||||
@@ -53,6 +58,16 @@ public extension SiteRoute {
|
||||
Method.post
|
||||
Body(.json(DehumidifierSize.Request.self))
|
||||
}
|
||||
Route(.case(Self.calculateFilterPressureDrop)) {
|
||||
Path { "api"; "v1"; "calculateFilterPressureDrop" }
|
||||
Method.post
|
||||
OneOf {
|
||||
Body(.json(FilterPressureDrop.Request.Basic.self))
|
||||
.map(.case(FilterPressureDrop.Request.basic))
|
||||
Body(.json(FilterPressureDrop.Request.FanLaw.self))
|
||||
.map(.case(FilterPressureDrop.Request.fanLaw))
|
||||
}
|
||||
}
|
||||
Route(.case(Self.calculateHVACSystemPerformance)) {
|
||||
Path { "api"; "v1"; "calculateHVACSystemPerformance" }
|
||||
Method.post
|
||||
@@ -84,6 +99,7 @@ public extension SiteRoute {
|
||||
case atticVentilation(AtticVentilation)
|
||||
case capacitor(Capacitor)
|
||||
case dehumidifierSize(DehumidifierSize)
|
||||
case filterPressureDrop(FilterPressureDrop)
|
||||
case hvacSystemPerformance(HVACSystemPerformance)
|
||||
case moldRisk(MoldRisk)
|
||||
case roomPressure(RoomPressure)
|
||||
@@ -101,6 +117,9 @@ public extension SiteRoute {
|
||||
Route(.case(Self.dehumidifierSize)) {
|
||||
DehumidifierSize.router
|
||||
}
|
||||
Route(.case(Self.filterPressureDrop)) {
|
||||
FilterPressureDrop.router
|
||||
}
|
||||
Route(.case(Self.hvacSystemPerformance)) {
|
||||
HVACSystemPerformance.router
|
||||
}
|
||||
@@ -213,6 +232,53 @@ public extension SiteRoute {
|
||||
}
|
||||
}
|
||||
|
||||
public enum FilterPressureDrop: Equatable, Sendable {
|
||||
case index(mode: Routes.FilterPressureDrop.Mode? = nil)
|
||||
case submit(Routes.FilterPressureDrop.Request)
|
||||
|
||||
public static let index = Self.index()
|
||||
|
||||
static let rootPath = "filter-pressure-drop"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.index)) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
Query {
|
||||
Optionally { Field("mode") { Routes.FilterPressureDrop.Mode.parser() } }
|
||||
}
|
||||
}
|
||||
Route(.case(Self.submit)) {
|
||||
Path { rootPath }
|
||||
Method.post
|
||||
Body {
|
||||
OneOf {
|
||||
FormData {
|
||||
Field("systemSize") { Double.parser() }
|
||||
Field("climateZone") { ClimateZone.parser() }
|
||||
Field("filterType") { Routes.FilterPressureDrop.FilterType.parser() }
|
||||
Field("filterWidth") { Double.parser() }
|
||||
Field("filterHeight") { Double.parser() }
|
||||
}
|
||||
.map(.memberwise(Routes.FilterPressureDrop.Request.Basic.init))
|
||||
.map(.case(Routes.FilterPressureDrop.Request.basic))
|
||||
|
||||
FormData {
|
||||
Field("filterWidth") { Double.parser() }
|
||||
Field("filterHeight") { Double.parser() }
|
||||
Field("filterDepth") { Double.parser() }
|
||||
Field("ratedAirflow") { Double.parser() }
|
||||
Field("ratedPressureDrop") { Double.parser() }
|
||||
Field("designAirflow") { Double.parser() }
|
||||
}
|
||||
.map(.memberwise(Routes.FilterPressureDrop.Request.FanLaw.init))
|
||||
.map(.case(Routes.FilterPressureDrop.Request.fanLaw))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum HVACSystemPerformance: Equatable, Sendable {
|
||||
case index
|
||||
case submit(Routes.HVACSystemPerformance.Request)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Elementary
|
||||
import Routes
|
||||
|
||||
public struct PrimaryButton: HTML, Sendable {
|
||||
let label: String
|
||||
@@ -75,3 +76,70 @@ public struct ResetButton: HTML, Sendable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Toggle: HTML {
|
||||
|
||||
let isOn: Bool
|
||||
|
||||
// Left hand side / 'on' label.
|
||||
let onLabel: String
|
||||
// Applied to the rhs when the toggle is consider 'off'.
|
||||
let onAttributes: [HTMLAttribute<HTMLTag.button>]
|
||||
|
||||
// Right hand side / 'off' label.
|
||||
let offLabel: String
|
||||
// Applied to the left hand side when the toggle is consider 'on'.
|
||||
let offAttributes: [HTMLAttribute<HTMLTag.button>]
|
||||
|
||||
public init(
|
||||
isOn: Bool,
|
||||
onLabel: String,
|
||||
onAttributes: [HTMLAttribute<HTMLTag.button>],
|
||||
offLabel: String,
|
||||
offAttributes: [HTMLAttribute<HTMLTag.button>]
|
||||
) {
|
||||
self.isOn = isOn
|
||||
self.onLabel = onLabel
|
||||
self.onAttributes = onAttributes
|
||||
self.offLabel = offLabel
|
||||
self.offAttributes = offAttributes
|
||||
}
|
||||
|
||||
public var content: some HTML<HTMLTag.div> {
|
||||
div(.class("flex items-center gap-x-0")) {
|
||||
switch isOn {
|
||||
case true:
|
||||
SecondaryButton(label: onLabel)
|
||||
.attributes(.class("rounded-s-lg"), .disabled)
|
||||
|
||||
PrimaryButton(label: offLabel)
|
||||
.attributes(contentsOf: offAttributes + [.class("rounded-e-lg")])
|
||||
|
||||
case false:
|
||||
PrimaryButton(label: onLabel)
|
||||
.attributes(contentsOf: onAttributes + [.class("rounded-s-lg")])
|
||||
|
||||
SecondaryButton(label: offLabel)
|
||||
.attributes(.class("rounded-e-lg"), .disabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Array where Element == HTMLAttribute<HTMLTag.button> {
|
||||
|
||||
static func hxDefaults(
|
||||
_ attributes: HTMLAttribute<HTMLTag.button>...
|
||||
) -> Self {
|
||||
[
|
||||
.hx.target("#content"),
|
||||
.hx.pushURL(true)
|
||||
] + attributes
|
||||
}
|
||||
|
||||
static func hxDefaults(
|
||||
get route: SiteRoute.View
|
||||
) -> Self {
|
||||
.hxDefaults(.hx.get(route: route))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public struct Input: HTML, Sendable {
|
||||
input(
|
||||
.id(id), .placeholder(placeholder), .name(name ?? id),
|
||||
.class("""
|
||||
w-full px-4 py-2 border rounded-md
|
||||
w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500
|
||||
@@ -84,3 +84,57 @@ public struct InputLabel<InputLabel: HTML>: HTML {
|
||||
}
|
||||
|
||||
extension InputLabel: Sendable where InputLabel: Sendable {}
|
||||
|
||||
public struct Select<T>: HTML {
|
||||
|
||||
let id: String
|
||||
let name: String?
|
||||
let values: [T]
|
||||
let label: @Sendable (T) -> String
|
||||
let value: @Sendable (T) -> String
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
name: String? = nil,
|
||||
values: [T],
|
||||
label: @escaping @Sendable (T) -> String,
|
||||
value: @escaping @Sendable (T) -> String
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.values = values
|
||||
self.label = label
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public var content: some HTML<HTMLTag.select> {
|
||||
select(
|
||||
.id(id),
|
||||
.name(name ?? id),
|
||||
.class("w-full rounded-md border px-4 py-2 min-h-11")
|
||||
) {
|
||||
for value in values {
|
||||
option(.value(self.value(value))) { label(value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Select: Sendable where T: Sendable {}
|
||||
|
||||
public extension Select where T: CaseIterable, T: RawRepresentable, T.RawValue: CustomStringConvertible {
|
||||
init(
|
||||
for type: T.Type,
|
||||
id: String,
|
||||
name: String? = nil,
|
||||
label: @escaping @Sendable (T) -> String
|
||||
) {
|
||||
self.init(
|
||||
id: id,
|
||||
name: name,
|
||||
values: Array(T.allCases),
|
||||
label: label,
|
||||
value: { $0.rawValue.description }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,16 @@ public struct Note: HTML, Sendable {
|
||||
}
|
||||
|
||||
public var content: some HTML {
|
||||
div(.class("mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md border border-blue-500")) {
|
||||
p(.class("text-sm text-blue-500")) {
|
||||
span(.class("font-extrabold pe-2")) { label }
|
||||
text
|
||||
}
|
||||
div(
|
||||
.class(
|
||||
"""
|
||||
mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||
border border-blue-500 text-blue-500 text-sm
|
||||
"""
|
||||
)
|
||||
) {
|
||||
p(.class("font-extrabold mb-3")) { label }
|
||||
p(.class("px-6")) { text }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
Sources/Styleguide/Row.swift
Normal file
34
Sources/Styleguide/Row.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import Elementary
|
||||
|
||||
public struct Row<Body: HTML>: HTML {
|
||||
|
||||
let body: Body
|
||||
|
||||
public init(
|
||||
@HTMLBuilder body: () -> Body
|
||||
) {
|
||||
self.body = body()
|
||||
}
|
||||
|
||||
public var content: some HTML<HTMLTag.div> {
|
||||
div(.class("flex justify-between")) {
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Row
|
||||
where Body == _HTMLTuple2<HTMLElement<HTMLTag.span, HTMLText>, HTMLElement<HTMLTag.span, HTMLText>>
|
||||
{
|
||||
init(
|
||||
label: String,
|
||||
value: String
|
||||
) {
|
||||
self.init {
|
||||
span(.class("font-bold")) { label }
|
||||
span { value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Row: Sendable where Body: Sendable {}
|
||||
25
Sources/Styleguide/VerticalGroup.swift
Normal file
25
Sources/Styleguide/VerticalGroup.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
import Elementary
|
||||
|
||||
public struct VerticalGroup: HTML, Sendable {
|
||||
let label: String
|
||||
let value: String
|
||||
let valueLabel: String?
|
||||
|
||||
public init(label: String, value: String, valueLabel: String? = nil) {
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.valueLabel = valueLabel
|
||||
}
|
||||
|
||||
public var content: some HTML<HTMLTag.div> {
|
||||
div(.class("grid grid-cols-1 justify-items-center")) {
|
||||
p(.class("font-medium")) { label }
|
||||
h3(.class("text-3xl font-extrabold")) {
|
||||
value
|
||||
if let valueLabel {
|
||||
span(.class("text-lg ms-2")) { valueLabel }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,12 @@ struct HomePage: HTML, Sendable {
|
||||
svg: .zap,
|
||||
route: .capacitor(.index)
|
||||
)
|
||||
group(
|
||||
label: "Filter Pressure Drop",
|
||||
description: FilterPressureDrop.description,
|
||||
svg: .funnel,
|
||||
route: .filterPressureDrop(.index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +59,7 @@ struct HomePage: HTML, Sendable {
|
||||
route: SiteRoute.View
|
||||
) -> some HTML {
|
||||
button(
|
||||
.hx.get(route: route),
|
||||
.hx.target("#content"),
|
||||
.hx.get(route: route), .hx.target("#content"), .hx.pushURL(true),
|
||||
.class("""
|
||||
w-full rounded-xl shadow-lg border border-blue-600 justify-items-start
|
||||
hover:bg-blue-600 hover:border-yellow-300 hover:text-yellow-300 transition-colors
|
||||
|
||||
@@ -100,6 +100,16 @@ extension ViewController: DependencyKey {
|
||||
return DehumidifierSizeResult(response: response)
|
||||
}
|
||||
|
||||
case let .filterPressureDrop(route):
|
||||
switch route {
|
||||
case let .index(mode: mode):
|
||||
// FIX: remove response.
|
||||
return request.respond(FilterPressureDropForm(mode: mode, response: .mock(mode: mode ?? .basic)))
|
||||
case .submit:
|
||||
// FIX: implement.
|
||||
fatalError()
|
||||
}
|
||||
|
||||
case let .hvacSystemPerformance(route):
|
||||
switch route {
|
||||
case .index:
|
||||
|
||||
@@ -143,6 +143,7 @@ struct AtticVentilationResponse: HTML, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use Styleguid.VerticalGroup
|
||||
private func group(_ label: String, _ value: String) -> some HTML<HTMLTag.div> {
|
||||
div(.class("justify-self-center")) {
|
||||
div(.class("grid grid-cols-1 justify-items-center")) {
|
||||
|
||||
@@ -17,42 +17,16 @@ struct CapacitorForm: HTML, Sendable {
|
||||
div(.class("flex flex-wrap justify-between")) {
|
||||
FormHeader(label: "Capacitor Calculator - \(mode.rawValue.capitalized) Capacitor", svg: .zap)
|
||||
// Mode toggle / buttons.
|
||||
div(.class("flex items-center gap-x-0")) {
|
||||
switch mode {
|
||||
case .test:
|
||||
SecondaryButton(label: "Test Capacitor")
|
||||
.attributes(.class("rounded-s-lg"))
|
||||
.attributes(.disabled, when: mode == .test)
|
||||
.attributes(
|
||||
.hx.get(route: .capacitor(.index(mode: .test))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .size
|
||||
)
|
||||
PrimaryButton(label: "Size Capacitor")
|
||||
.attributes(.class("rounded-e-lg"))
|
||||
.attributes(
|
||||
.hx.get(route: .capacitor(.index(mode: .size))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .test
|
||||
)
|
||||
case .size:
|
||||
PrimaryButton(label: "Test Capacitor")
|
||||
.attributes(.class("rounded-s-lg"))
|
||||
.attributes(
|
||||
.hx.get(route: .capacitor(.index(mode: .test))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .size
|
||||
)
|
||||
SecondaryButton(label: "Size Capacitor")
|
||||
.attributes(.class("rounded-e-lg"))
|
||||
.attributes(.disabled, when: mode == .size)
|
||||
.attributes(
|
||||
.hx.get(route: .capacitor(.index(mode: .size))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .test
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Mode toggle / buttons.
|
||||
Toggle(
|
||||
isOn: mode == .test,
|
||||
onLabel: "Test Capacitor",
|
||||
onAttributes: .hxDefaults(get: .capacitor(.index(mode: .test))),
|
||||
offLabel: "Size Capacitor",
|
||||
offAttributes: .hxDefaults(get: .capacitor(.index(mode: .size)))
|
||||
)
|
||||
.attributes(.class("mb-6"))
|
||||
}
|
||||
|
||||
Form(mode: mode).attributes(.class("mt-6"))
|
||||
|
||||
249
Sources/ViewController/Views/FilterPressureDrop.swift
Normal file
249
Sources/ViewController/Views/FilterPressureDrop.swift
Normal file
@@ -0,0 +1,249 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import Routes
|
||||
import Styleguide
|
||||
|
||||
struct FilterPressureDropForm: HTML, Sendable {
|
||||
let mode: FilterPressureDrop.Mode
|
||||
let response: FilterPressureDrop.Response?
|
||||
|
||||
init(mode: FilterPressureDrop.Mode? = nil, response: FilterPressureDrop.Response? = nil) {
|
||||
self.mode = mode ?? .basic
|
||||
self.response = response
|
||||
}
|
||||
|
||||
var content: some HTML {
|
||||
div(.class("flex flex-wrap justify-between")) {
|
||||
FormHeader(label: "Filter Pressure Drop - \(mode.label)", svg: .funnel)
|
||||
Toggle(
|
||||
isOn: mode == .basic,
|
||||
onLabel: FilterPressureDrop.Mode.basic.label,
|
||||
onAttributes: .hxDefaults(get: .filterPressureDrop(.index(mode: .basic))),
|
||||
offLabel: FilterPressureDrop.Mode.fanLaw.label,
|
||||
offAttributes: .hxDefaults(get: .filterPressureDrop(.index(mode: .fanLaw)))
|
||||
)
|
||||
.attributes(.class("mb-6"))
|
||||
}
|
||||
|
||||
// FIX: Remove when done.
|
||||
WarningBox("This is under construction and does not work currently.")
|
||||
|
||||
form(
|
||||
.hx.post(route: .filterPressureDrop(.index)),
|
||||
.hx.target("#result")
|
||||
) {
|
||||
div(.class("space-y-6")) {
|
||||
switch mode {
|
||||
case .basic:
|
||||
BasicFormFields()
|
||||
case .fanLaw:
|
||||
FanLawFormFields()
|
||||
}
|
||||
div {
|
||||
SubmitButton(label: "Calculate Pressure Drop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div(.id("result")) {
|
||||
if let response {
|
||||
FilterPressureDropResponse(response: response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct BasicFormFields: HTML, Sendable {
|
||||
|
||||
var content: some HTML {
|
||||
div {
|
||||
div {
|
||||
h4(.class("text-lg font-bold mb-4")) { "System Parameters" }
|
||||
div(.class("grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6")) {
|
||||
LabeledContent(label: "System Size: (Tons)") {
|
||||
Input(id: "systemSize", placeholder: "Tons")
|
||||
.attributes(.type(.number), .step("0.5"), .min("1.0"), .autofocus, .required)
|
||||
}
|
||||
div {
|
||||
InputLabel(for: "climateZone") { "Climate Zone" }
|
||||
Select(for: ClimateZone.self, id: "climateZone") {
|
||||
$0.label
|
||||
}
|
||||
}
|
||||
div {
|
||||
InputLabel(for: "filterType") { "Filter Type" }
|
||||
Select(for: FilterPressureDrop.FilterType.self, id: "filterType") {
|
||||
$0.label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div(.class("mt-6 lg:mt-0")) {
|
||||
h4(.class("text-lg font-bold mb-4")) { "Filter Parameters" }
|
||||
|
||||
div(.class("grid grid-cols-1 lg:grid-cols-2 gap-x-4 space-y-6")) {
|
||||
LabeledContent(label: "Width: (in)") {
|
||||
Input(id: "filterWidth", placeholder: "Width")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
LabeledContent(label: "Height: (in)") {
|
||||
Input(id: "filterHeight", placeholder: "Height")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct FanLawFormFields: HTML, Sendable {
|
||||
|
||||
var content: some HTML {
|
||||
div {
|
||||
div(.class("mt-6 lg:mt-0")) {
|
||||
h4(.class("text-lg font-bold mb-4")) { "Filter Parameters" }
|
||||
div(.class("grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6")) {
|
||||
LabeledContent(label: "Width: (in)") {
|
||||
Input(id: "filterWidth", placeholder: "Width")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required, .autofocus)
|
||||
}
|
||||
LabeledContent(label: "Height: (in)") {
|
||||
Input(id: "filterHeight", placeholder: "Height")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
LabeledContent(label: "Depth: (in)") {
|
||||
Input(id: "filterDepth", placeholder: "Depth")
|
||||
.attributes(.type(.number), .step("1.0"), .min("1.0"), .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
div(.class("mt-6 lg:mt-0")) {
|
||||
h4(.class("text-lg font-bold mb-4")) { "Airflow Parameters" }
|
||||
div(.class("grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6")) {
|
||||
LabeledContent(label: "Rated Airflow: (CFM)") {
|
||||
Input(id: "ratedAirflow", placeholder: "Rated or measured Airflow")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
LabeledContent(label: "Filter Pressure Drop: (in. w.c.)") {
|
||||
Input(id: "ratedPressure", placeholder: "Rated or measured pressure drop")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
LabeledContent(label: "Design Airflow: (CFM)") {
|
||||
Input(id: "designAirflow", placeholder: "Design or target airflow")
|
||||
.attributes(.type(.number), .step("1.0"), .min("1.0"), .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FilterPressureDropResponse: HTML, Sendable {
|
||||
let response: FilterPressureDrop.Response
|
||||
|
||||
var content: some HTML {
|
||||
ResultContainer(reset: .filterPressureDrop(.index(mode: response.resetMode))) {
|
||||
switch response {
|
||||
case let .basic(response):
|
||||
BasicView(response: response)
|
||||
case let .fanLaw(response):
|
||||
FanLawView(response: response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct BasicView: HTML {
|
||||
let response: FilterPressureDrop.Response.Basic
|
||||
|
||||
var content: some HTML {
|
||||
div(
|
||||
.class("""
|
||||
grid grid-cols-1 lg:grid-cols-2 gap-6 rounded-lg shadow-lg
|
||||
border border-blue-600 text-blue-600 bg-blue-100 p-6
|
||||
""")
|
||||
) {
|
||||
VerticalGroup(
|
||||
label: "Filter Face Area",
|
||||
value: "\(double: response.filterArea, fractionDigits: 2)",
|
||||
valueLabel: "ft²"
|
||||
)
|
||||
VerticalGroup(
|
||||
label: "Filter Face Velocity",
|
||||
value: "\(double: response.feetPerMinute, fractionDigits: 1)",
|
||||
valueLabel: "FPM"
|
||||
)
|
||||
VerticalGroup(
|
||||
label: "Initial Pressure Drop",
|
||||
value: "\(double: response.initialPressureDrop, fractionDigits: 2)\"",
|
||||
valueLabel: "w.c."
|
||||
)
|
||||
VerticalGroup(
|
||||
label: "Maximum Allowable Pressure Drop",
|
||||
value: "\(double: response.maxPressureDrop, fractionDigits: 2)\"",
|
||||
valueLabel: "w.c."
|
||||
)
|
||||
}
|
||||
|
||||
WarningBox(warnings: response.warnings)
|
||||
}
|
||||
}
|
||||
|
||||
private struct FanLawView: HTML {
|
||||
let response: FilterPressureDrop.Response.FanLaw
|
||||
|
||||
var content: some HTML {
|
||||
div(
|
||||
.class("""
|
||||
grid grid-cols-1 lg:grid-cols-2 gap-6 rounded-lg shadow-lg
|
||||
border border-blue-600 text-blue-600 bg-blue-100 p-6
|
||||
""")
|
||||
) {
|
||||
VerticalGroup(
|
||||
label: "Filter Face Velocity",
|
||||
value: "\(double: response.faceVelocity, fractionDigits: 1)",
|
||||
valueLabel: "FPM"
|
||||
)
|
||||
VerticalGroup(
|
||||
label: "Predicted Pressure Drop",
|
||||
value: "\(double: response.predictedPressureDrop, fractionDigits: 2)\"",
|
||||
valueLabel: "w.c."
|
||||
)
|
||||
VerticalGroup(
|
||||
label: "Velocity Ratio",
|
||||
value: "\(double: response.velocityRatio, fractionDigits: 2)"
|
||||
)
|
||||
VerticalGroup(
|
||||
label: "Pressure Ratio",
|
||||
value: "\(double: response.pressureRatio, fractionDigits: 2)"
|
||||
)
|
||||
}
|
||||
Note {
|
||||
"""
|
||||
Predictions are based on fan laws where pressure drop varies with the square of the airflow ratio.
|
||||
Results assume similar air properties and filter loading conditions.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension FilterPressureDrop.Mode {
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .basic: return rawValue.capitalized
|
||||
case .fanLaw: return "Fan Law"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension FilterPressureDrop.Response {
|
||||
|
||||
var resetMode: FilterPressureDrop.Mode {
|
||||
switch self {
|
||||
case .basic: return .basic
|
||||
case .fanLaw: return .fanLaw
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,44 +16,14 @@ struct RoomPressureForm: HTML, Sendable {
|
||||
div(.class("relative")) {
|
||||
div(.class("flex flex-wrap justify-between")) {
|
||||
FormHeader(label: "Room Pressure Calculator - \(mode.label)", svg: .leftRightArrow)
|
||||
|
||||
// Mode toggle / buttons.
|
||||
div(.class("flex items-center gap-x-0")) {
|
||||
switch mode {
|
||||
case .knownAirflow:
|
||||
SecondaryButton(label: "Known Airflow")
|
||||
.attributes(.class("rounded-s-lg"))
|
||||
.attributes(.disabled, when: mode == .knownAirflow)
|
||||
.attributes(
|
||||
.hx.get(route: .roomPressure(.index(mode: .knownAirflow))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .measuredPressure
|
||||
)
|
||||
PrimaryButton(label: "Measured Pressure")
|
||||
.attributes(.class("rounded-e-lg"))
|
||||
.attributes(
|
||||
.hx.get(route: .roomPressure(.index(mode: .measuredPressure))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .knownAirflow
|
||||
)
|
||||
case .measuredPressure:
|
||||
PrimaryButton(label: "Known Airflow")
|
||||
.attributes(.class("rounded-s-lg"))
|
||||
.attributes(
|
||||
.hx.get(route: .roomPressure(.index(mode: .knownAirflow))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .measuredPressure
|
||||
)
|
||||
SecondaryButton(label: "Measured Pressure")
|
||||
.attributes(.class("rounded-e-lg"))
|
||||
.attributes(.disabled, when: mode == .measuredPressure)
|
||||
.attributes(
|
||||
.hx.get(route: .roomPressure(.index(mode: .measuredPressure))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .knownAirflow
|
||||
)
|
||||
}
|
||||
}
|
||||
Toggle(
|
||||
isOn: mode == .knownAirflow,
|
||||
onLabel: "Known Airflow",
|
||||
onAttributes: .hxDefaults(get: .roomPressure(.index(mode: .knownAirflow))),
|
||||
offLabel: "Measured Pressure",
|
||||
offAttributes: .hxDefaults(get: .roomPressure(.index(mode: .measuredPressure)))
|
||||
)
|
||||
.attributes(.class("mb-6"))
|
||||
}
|
||||
|
||||
Form(mode: mode)
|
||||
@@ -156,6 +126,7 @@ struct RoomPressureForm: HTML, Sendable {
|
||||
|
||||
var content: some HTML {
|
||||
InputLabel(for: "preferredGrilleHeight") { "Preferred Grille Height" }
|
||||
// TODO: Use Styleguide.Select
|
||||
select(
|
||||
.id("preferredGrilleHeight"),
|
||||
.name("preferredGrilleHeight"),
|
||||
|
||||
Reference in New Issue
Block a user