feat: Finalizes room-pressure view.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -20,23 +20,6 @@ public extension RoomPressure.Request {
|
|||||||
14: OrderedSet([14])
|
14: OrderedSet([14])
|
||||||
]
|
]
|
||||||
|
|
||||||
// OrderedSet([
|
|
||||||
// (width: 8, height: 4),
|
|
||||||
// (width: 10, height: 4),
|
|
||||||
// (width: 12, height: 4),
|
|
||||||
// (width: 14, height: 4),
|
|
||||||
// (width: 8, height: 6),
|
|
||||||
// (width: 10, height: 6),
|
|
||||||
// (width: 12, height: 6),
|
|
||||||
// (width: 14, height: 6),
|
|
||||||
// (width: 10, height: 8),
|
|
||||||
// (width: 12, height: 8),
|
|
||||||
// (width: 14, height: 8),
|
|
||||||
// (width: 10, height: 10),
|
|
||||||
// (width: 12, height: 12),
|
|
||||||
// (width: 14, height: 14)
|
|
||||||
// ])
|
|
||||||
|
|
||||||
func respond(logger: Logger) async throws -> RoomPressure.Response {
|
func respond(logger: Logger) async throws -> RoomPressure.Response {
|
||||||
switch self {
|
switch self {
|
||||||
case let .knownAirflow(request):
|
case let .knownAirflow(request):
|
||||||
@@ -67,6 +50,7 @@ public extension RoomPressure.Request {
|
|||||||
let (standardDuctSize, actualVelocity) = calculateDuctMetrics(for: request.supplyAirflow)
|
let (standardDuctSize, actualVelocity) = calculateDuctMetrics(for: request.supplyAirflow)
|
||||||
|
|
||||||
return .init(
|
return .init(
|
||||||
|
mode: .knownAirflow,
|
||||||
grilleSize: .init(width: grilleDimensions.width, height: grilleDimensions.height, area: netFreeArea / 144),
|
grilleSize: .init(width: grilleDimensions.width, height: grilleDimensions.height, area: netFreeArea / 144),
|
||||||
ductSize: .init(diameter: standardDuctSize, velocity: actualVelocity),
|
ductSize: .init(diameter: standardDuctSize, velocity: actualVelocity),
|
||||||
warnings: generateWarnings(
|
warnings: generateWarnings(
|
||||||
@@ -101,6 +85,7 @@ public extension RoomPressure.Request {
|
|||||||
let (standardDuctSize, actualVelocity) = calculateDuctMetrics(for: calculatedAirflow)
|
let (standardDuctSize, actualVelocity) = calculateDuctMetrics(for: calculatedAirflow)
|
||||||
|
|
||||||
return .init(
|
return .init(
|
||||||
|
mode: .measuredPressure,
|
||||||
grilleSize: .init(width: grilleDimensions.width, height: grilleDimensions.height, area: netFreeArea / 144),
|
grilleSize: .init(width: grilleDimensions.width, height: grilleDimensions.height, area: netFreeArea / 144),
|
||||||
ductSize: .init(diameter: standardDuctSize, velocity: actualVelocity),
|
ductSize: .init(diameter: standardDuctSize, velocity: actualVelocity),
|
||||||
calculatedAirflow: calculatedAirflow,
|
calculatedAirflow: calculatedAirflow,
|
||||||
|
|||||||
@@ -68,17 +68,20 @@ public enum RoomPressure {
|
|||||||
|
|
||||||
public struct Response: Codable, Equatable, Sendable {
|
public struct Response: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let mode: Mode
|
||||||
public let grilleSize: GrilleSize
|
public let grilleSize: GrilleSize
|
||||||
public let ductSize: DuctSize
|
public let ductSize: DuctSize
|
||||||
public let calculatedAirflow: Double?
|
public let calculatedAirflow: Double?
|
||||||
public let warnings: [String]
|
public let warnings: [String]
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
mode: Mode,
|
||||||
grilleSize: RoomPressure.Response.GrilleSize,
|
grilleSize: RoomPressure.Response.GrilleSize,
|
||||||
ductSize: RoomPressure.Response.DuctSize,
|
ductSize: RoomPressure.Response.DuctSize,
|
||||||
calculatedAirflow: Double? = nil,
|
calculatedAirflow: Double? = nil,
|
||||||
warnings: [String]
|
warnings: [String]
|
||||||
) {
|
) {
|
||||||
|
self.mode = mode
|
||||||
self.grilleSize = grilleSize
|
self.grilleSize = grilleSize
|
||||||
self.ductSize = ductSize
|
self.ductSize = ductSize
|
||||||
self.calculatedAirflow = calculatedAirflow
|
self.calculatedAirflow = calculatedAirflow
|
||||||
@@ -124,3 +127,16 @@ public enum RoomPressure {
|
|||||||
public var label: String { "\(rawValue)\"" }
|
public var label: String { "\(rawValue)\"" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
public extension RoomPressure.Response {
|
||||||
|
static let mock = Self(
|
||||||
|
mode: .knownAirflow,
|
||||||
|
grilleSize: .init(width: 14, height: 8, area: 72),
|
||||||
|
ductSize: .init(diameter: 10, velocity: 367),
|
||||||
|
warnings: [
|
||||||
|
"Duct leakage area is significant - consider reducing gaps or increasing grille size."
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ public extension SiteRoute {
|
|||||||
Path { rootPath }
|
Path { rootPath }
|
||||||
Method.get
|
Method.get
|
||||||
Query {
|
Query {
|
||||||
Optionally { Field("form") { Routes.RoomPressure.Mode.parser() } }
|
Optionally { Field("mode") { Routes.RoomPressure.Mode.parser() } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Route(.case(Self.submit)) {
|
Route(.case(Self.submit)) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public struct WarningBox<Header: HTML>: HTML, Sendable {
|
|||||||
|
|
||||||
public var content: some HTML<HTMLTag.div> {
|
public var content: some HTML<HTMLTag.div> {
|
||||||
div(.id("warnings")) {
|
div(.id("warnings")) {
|
||||||
|
if warnings.count > 0 {
|
||||||
header(warnings)
|
header(warnings)
|
||||||
ul(.class("list-disc mx-10")) {
|
ul(.class("list-disc mx-10")) {
|
||||||
for warning in warnings {
|
for warning in warnings {
|
||||||
@@ -31,6 +32,7 @@ public struct WarningBox<Header: HTML>: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.attributes(
|
.attributes(
|
||||||
.class("""
|
.class("""
|
||||||
mt-6 p-4 rounded-lg shadow-lg
|
mt-6 p-4 rounded-lg shadow-lg
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ extension ViewController: DependencyKey {
|
|||||||
@Dependency(\.psychrometricClient) var psychrometricClient
|
@Dependency(\.psychrometricClient) var psychrometricClient
|
||||||
|
|
||||||
return .init(view: { request in
|
return .init(view: { request in
|
||||||
request.logger.debug("View route: \(request.route)")
|
let logger = request.logger
|
||||||
|
logger.debug("View route: \(request.route)")
|
||||||
switch request.route {
|
switch request.route {
|
||||||
case .index:
|
case .index:
|
||||||
return MainPage {
|
return MainPage {
|
||||||
@@ -108,9 +109,9 @@ extension ViewController: DependencyKey {
|
|||||||
case let .index(mode):
|
case let .index(mode):
|
||||||
return request.respond(RoomPressureForm(mode: mode, response: nil))
|
return request.respond(RoomPressureForm(mode: mode, response: nil))
|
||||||
|
|
||||||
// FIX:
|
case let .submit(request):
|
||||||
case .submit:
|
let response = try await request.respond(logger: logger)
|
||||||
fatalError()
|
return RoomPressureResult(response: response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ private struct Header: HTML {
|
|||||||
|
|
||||||
private struct Footer: HTML {
|
private struct Footer: HTML {
|
||||||
var content: some HTML {
|
var content: some HTML {
|
||||||
div(.class("bg-blue-500 text-yellow-300 text-sm font-semibold border-t border-yellow-300")) {
|
div(.class("bg-blue-500 text-yellow-300 text-sm font-semibold border-t border-yellow-300 mt-20")) {
|
||||||
div(.class("sm:grid sm:grid-cols-1 lg:flex lg:justify-between")) {
|
div(.class("sm:grid sm:grid-cols-1 lg:flex lg:justify-between")) {
|
||||||
div(.class("grid grid-cols-1 p-4")) {
|
div(.class("grid grid-cols-1 p-4")) {
|
||||||
div(.class("flex")) {
|
div(.class("flex")) {
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ struct RoomPressureForm: HTML, Sendable {
|
|||||||
div(.class("relative")) {
|
div(.class("relative")) {
|
||||||
FormHeader(label: "Room Pressure Calculator", svg: .leftRightArrow)
|
FormHeader(label: "Room Pressure Calculator", svg: .leftRightArrow)
|
||||||
|
|
||||||
// FIX: Remove when done testing.
|
|
||||||
WarningBox(
|
|
||||||
"This calculator is currently under construction, so it does not do anything when the form is submitted."
|
|
||||||
)
|
|
||||||
.attributes(.class("mb-8"))
|
|
||||||
|
|
||||||
// Mode toggle / buttons.
|
// Mode toggle / buttons.
|
||||||
div(.class("absolute top-0 right-0 flex items-center gap-x-0")) {
|
div(.class("absolute top-0 right-0 flex items-center gap-x-0")) {
|
||||||
switch mode {
|
switch mode {
|
||||||
@@ -74,7 +68,10 @@ struct RoomPressureForm: HTML, Sendable {
|
|||||||
let mode: RoomPressure.Mode
|
let mode: RoomPressure.Mode
|
||||||
|
|
||||||
var content: some HTML<HTMLTag.form> {
|
var content: some HTML<HTMLTag.form> {
|
||||||
form {
|
form(
|
||||||
|
.hx.post(route: .roomPressure(.index)),
|
||||||
|
.hx.target("#result")
|
||||||
|
) {
|
||||||
div(.class("space-y-6")) {
|
div(.class("space-y-6")) {
|
||||||
LabeledContent(label: pressureLabel) {
|
LabeledContent(label: pressureLabel) {
|
||||||
// NB: using .attributes(..., when:...) not working, so using a switch statement.
|
// NB: using .attributes(..., when:...) not working, so using a switch statement.
|
||||||
@@ -171,4 +168,76 @@ struct RoomPressureForm: HTML, Sendable {
|
|||||||
|
|
||||||
struct RoomPressureResult: HTML, Sendable {
|
struct RoomPressureResult: HTML, Sendable {
|
||||||
let response: RoomPressure.Response
|
let response: RoomPressure.Response
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
ResultContainer(reset: .roomPressure(.index(mode: response.mode))) {
|
||||||
|
div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) {
|
||||||
|
RoundedContainer(title: "Return / Transfer Grille") {
|
||||||
|
div(.class("flex justify-between mt-6")) {
|
||||||
|
span(.class("font-semibold")) { "Standard Size:" }
|
||||||
|
span { """
|
||||||
|
\(response.grilleSize.width)" x \(response.grilleSize.height)"
|
||||||
|
""" }
|
||||||
|
}
|
||||||
|
div(.class("flex justify-between mt-3")) {
|
||||||
|
span(.class("font-semibold")) { "Required Net Free Area:" }
|
||||||
|
span {
|
||||||
|
"""
|
||||||
|
\(double: response.grilleSize.area, fractionDigits: 1) in
|
||||||
|
"""
|
||||||
|
sup { "2" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(.class("mt-8 text-sm")) {
|
||||||
|
span(.class("font-semibold")) { "Note: " }
|
||||||
|
span {
|
||||||
|
"Select a grille with at least \(double: response.grilleSize.area, fractionDigits: 1) in"
|
||||||
|
sup { "2" }
|
||||||
|
span { " net free area." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.attributes(.class("bg-blue-100 border border-blue-600 text-blue-600"))
|
||||||
|
|
||||||
|
RoundedContainer(title: "Return / Transfer Duct") {
|
||||||
|
div(.class("flex justify-between mt-6")) {
|
||||||
|
span(.class("font-semibold")) { "Standard Size:" }
|
||||||
|
span { "\(response.ductSize.diameter)\"" }
|
||||||
|
}
|
||||||
|
div(.class("flex justify-between mt-3")) {
|
||||||
|
span(.class("font-semibold")) { "Air Velocity:" }
|
||||||
|
span { "\(double: response.ductSize.velocity, fractionDigits: 1) FPM" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.attributes(.class("bg-purple-100 border border-purple-600 text-purple-600"))
|
||||||
|
}
|
||||||
|
|
||||||
|
WarningBox(warnings: response.warnings)
|
||||||
|
|
||||||
|
Note {
|
||||||
|
"""
|
||||||
|
Calculations are based on a target velocity of 400 FPM for return/transfer air paths.
|
||||||
|
The required net free area is the minimum needed - select a grille that meets or exceeds this value.
|
||||||
|
Verify manufacturer specifications for actual net free area of selected grilles.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct RoundedContainer<Body: HTML>: HTML, Sendable where Body: Sendable {
|
||||||
|
let title: String
|
||||||
|
let body: Body
|
||||||
|
|
||||||
|
init(title: String, @HTMLBuilder body: () -> Body) {
|
||||||
|
self.title = title
|
||||||
|
self.body = body()
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: some HTML<HTMLTag.div> {
|
||||||
|
div(.class("rounded-xl p-6")) {
|
||||||
|
h4(.class("text-xl font-bold")) { title }
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user