feat: Adds capacitor calculations.

This commit is contained in:
2025-03-01 18:11:49 -05:00
parent eaf0387899
commit 3c7147ad0e
11 changed files with 622 additions and 39 deletions

View File

@@ -76,6 +76,17 @@ extension ViewController: DependencyKey {
}
}
case let .capacitor(route):
switch route {
case let .index(mode: mode):
logger.debug("Capacitor view: \(String(describing: mode))")
return request.respond(CapacitorForm(mode: mode, response: nil))
case let .submit(request):
logger.debug("Capaitor view submit: \(request)")
let response = try await request.respond(logger: logger)
return CapacitorResponse(response: response)
}
case let .dehumidifierSize(route):
switch route {
case .index:

View File

@@ -66,6 +66,7 @@ private struct Header: HTML {
navLink(label: "Dehumidifier-Sizing", route: .dehumidifierSize(.index))
navLink(label: "HVAC-System-Performance", route: .hvacSystemPerformance(.index))
navLink(label: "Room-Pressure", route: .roomPressure(.index))
navLink(label: "Capcitor-Calculator", route: .capacitor(.index))
}
}
}

View File

@@ -0,0 +1,239 @@
import Elementary
import ElementaryHTMX
import Routes
import Styleguide
struct CapacitorForm: HTML, Sendable {
let mode: Capacitor.Mode
let response: Capacitor.Response?
init(mode: Capacitor.Mode?, response: Capacitor.Response? = nil) {
self.mode = mode ?? .test
self.response = response
}
var content: some HTML {
div(.class("relative")) {
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
)
}
}
}
Form(mode: mode).attributes(.class("mt-6"))
div(.id("result")) {
if let response {
CapacitorResponse(response: response)
}
}
}
}
private struct Form: HTML, Sendable {
let mode: Capacitor.Mode
var content: some HTML<HTMLTag.form> {
form(
.hx.post(route: .capacitor(.index)),
.hx.target("#result")
) {
div(.class("space-y-6")) {
switch mode {
case .size:
LabeledContent(label: "Running Current: (amps)") {
Input(id: "runningAmps", placeholder: "Current amps")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .autofocus, .required)
}
LabeledContent(label: "Line Voltage") {
Input(id: "lineVoltage", placeholder: "Voltage")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
}
LabeledContent(label: "Power Factor") {
Input(id: "powerFactor", placeholder: "Power factor (0-1)")
.attributes(.type(.number), .step("0.01"), .min("0.1"), .max("1.00"), .required)
}
case .test:
LabeledContent(label: "Start Winding: (amps)") {
Input(id: "startWindingAmps", placeholder: "Current amps")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .autofocus, .required)
}
LabeledContent(label: "Run to Common: (volts)") {
Input(id: "runToCommonVoltage", placeholder: "Voltage")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
}
LabeledContent(label: "Capacitor Rated Size: (µF)") {
Input(id: "ratedCapacitorSize", placeholder: "Size (optional)")
.attributes(.type(.number), .step("0.1"), .min("0.1"))
}
}
div {
SubmitButton(label: "\(mode == .size ? "Calculate Size" : "Test Capacitor")")
}
}
}
}
}
}
struct CapacitorResponse: HTML, Sendable {
let response: Capacitor.Response
var content: some HTML {
ResultContainer(reset: .capacitor(.index(mode: response.mode))) {
switch response {
case let .size(result: result):
sizeResponse(result)
case let .test(result: result):
testResponse(result)
}
}
}
private func sizeResponse(_ response: Capacitor.Response.SizeResponse) -> some HTML {
div {
div(
.class("""
w-full rounded-lg shadow-lg p-4
border-2 border-blue-600 bg-blue-100 text-blue-600
""")
) {
div(.class("flex justify-center")) {
span(.class("font-extrabold")) { "Required Capacitor Size" }
}
row("Recommended Standard Size", "\(response.standardSize) µF")
row("Calculated Size:", "\(double: response.capacitance, fractionDigits: 1) µF")
toleranceRow(response.tolerance)
}
WarningBox(
"Always verify voltage rating matches the application.",
"Use the next larger size if exact match is unavailable.",
"Ensure capacitor is rated for continuous duty.",
"Consider ambient temperature in final selection."
) { _ in
span(.class("font-extrabold")) { "Important Notes:" }
}
}
}
private func testResponse(_ response: Capacitor.Response.TestResponse) -> some HTML {
div {
if let comparison = response.ratedComparison {
div(
.class("""
w-full rounded-lg shadow-lg p-4
border-2 \(comparison.borderColor) \(comparison.bgColor) \(comparison.textColor)
""")
) {
div(.class("flex font-bold gap-x-4")) {
SVG(
comparison.isInRange ? .checkCircle : .exclamation,
color: comparison.textColor
)
span(.class("font-normal")) {
"Capacitor is \(comparison.isInRange ? "within" : "outside of") acceptable range: (± 6% of rated value)"
}
}
}
}
div(.class("grid grid-cols-1 \(response.ratedComparison != nil ? "lg:grid-cols-2" : "") gap-4 mt-8")) {
div(.class("bg-blue-100 rounded-lg border-2 border-blue-600 text-blue-500 px-4 pb-4")) {
div(.class("flex justify-center mb-6 mt-2")) {
span(.class("text-2xl font-extrabold")) { "Measured" }
}
row("Capacitance", "\(double: response.capacitance) µF")
toleranceRow(response.tolerance)
}
if let comparison = response.ratedComparison {
div(.class("bg-blue-100 rounded-lg border-2 border-blue-600 text-blue-500 px-4 pb-4")) {
div(.class("flex justify-center mb-6 mt-2")) {
span(.class("text-2xl font-extrabold")) { "Rated Comparison" }
}
row("Rated Value", "\(comparison.value) µF")
row("Deviation", "\(double: comparison.percentDeviation, fractionDigits: 1)%")
}
}
}
}
}
private func row(_ label: String, _ value: String) -> some HTML {
div(.class("flex justify-between mb-2")) {
span(.class("font-semibold")) { label }
span { value }
}
}
private func toleranceRow(_ tolerance: Capacitor.Tolerance) -> some HTML {
row(
"Acceptable Range (±6%):",
"\(double: tolerance.minimum, fractionDigits: 1) - \(double: tolerance.maximum, fractionDigits: 1) µF"
)
}
}
private extension Capacitor.RatedComparison {
var bgColor: String {
guard isInRange else { return "bg-red-100" }
return "bg-green-100"
}
var borderColor: String {
guard isInRange else { return "border-red-600" }
return "border-green-600"
}
var textColor: String {
guard isInRange else { return "text-red-600" }
return "text-green-600"
}
}
private extension Capacitor.Response {
var mode: Capacitor.Mode {
switch self {
case .size: return .size
case .test: return .test
}
}
}

View File

@@ -14,47 +14,50 @@ struct RoomPressureForm: HTML, Sendable {
var content: some HTML {
div(.class("relative")) {
FormHeader(label: "Room Pressure Calculator", svg: .leftRightArrow)
div(.class("flex flex-wrap justify-between")) {
FormHeader(label: "Room Pressure Calculator - \(mode.label)", svg: .leftRightArrow)
// Mode toggle / buttons.
div(.class("absolute top-0 right-0 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
)
// 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
)
}
}
}
Form(mode: mode)
.attributes(.class("mt-6"))
div(.id("result")) {
if let response {
@@ -241,3 +244,13 @@ struct RoomPressureResult: HTML, Sendable {
}
}
}
private extension RoomPressure.Mode {
var label: String {
switch self {
case let .knownAirflow: return "Known Airflow"
case let .measuredPressure: return "Measured Pressure"
}
}
}