feat: Initial filter pressure drop views, calculations need implemented.

This commit is contained in:
2025-03-02 21:51:52 -05:00
parent a8022ec80a
commit 67488e06a9
17 changed files with 610 additions and 97 deletions

View File

@@ -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")) {

View File

@@ -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"))

View 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
}
}
}

View File

@@ -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"),