From 0fe80d05c6c3f42309bbec60169305c8fa99cc80 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Sat, 17 Jan 2026 12:59:46 -0500 Subject: [PATCH] WIP: Begins work on pdf client. --- .gitignore | 1 + Package.swift | 9 + Sources/App/configure.swift | 9 + .../{ManualDClient.swift => Interface.swift} | 2 +- Sources/ManualDClient/Live.swift | 2 +- Sources/ManualDCore/FrictionRate.swift | 14 + Sources/PdfClient/Interface.swift | 47 +++ Sources/PdfClient/Request+markdown.swift | 42 ++ Sources/ProjectClient/Interface.swift | 4 +- Sources/ViewController/Live.swift | 6 +- .../Views/FrictionRate/FrictionRateView.swift | 28 +- .../Views/Project/ProjectView.swift | 2 +- ductsizes.json | 359 ++++++++++++++++++ 13 files changed, 501 insertions(+), 24 deletions(-) rename Sources/ManualDClient/{ManualDClient.swift => Interface.swift} (99%) create mode 100644 Sources/ManualDCore/FrictionRate.swift create mode 100644 Sources/PdfClient/Interface.swift create mode 100644 Sources/PdfClient/Request+markdown.swift create mode 100644 ductsizes.json diff --git a/.gitignore b/.gitignore index fe8c227..86312e5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ DerivedData/ .swift-version node_modules/ tailwindcss +.envrc diff --git a/Package.swift b/Package.swift index c4961b9..d1c899c 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,7 @@ let package = Package( .library(name: "ApiController", targets: ["ApiController"]), .library(name: "AuthClient", targets: ["AuthClient"]), .library(name: "DatabaseClient", targets: ["DatabaseClient"]), + .library(name: "PdfClient", targets: ["PdfClient"]), .library(name: "ProjectClient", targets: ["ProjectClient"]), .library(name: "ManualDCore", targets: ["ManualDCore"]), .library(name: "ManualDClient", targets: ["ManualDClient"]), @@ -75,6 +76,14 @@ let package = Package( .product(name: "Vapor", package: "vapor"), ] ), + .target( + name: "PdfClient", + dependencies: [ + .target(name: "ManualDCore"), + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "DependenciesMacros", package: "swift-dependencies"), + ] + ), .target( name: "ProjectClient", dependencies: [ diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index ebb6a8c..115153c 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -5,6 +5,7 @@ import Fluent import FluentSQLiteDriver import ManualDCore import NIOSSL +import ProjectClient import Vapor import VaporElementary @preconcurrency import VaporRouting @@ -111,6 +112,8 @@ extension SiteRoute { } } +extension DuctSizes: Content {} + @Sendable private func siteHandler( request: Request, @@ -118,6 +121,7 @@ private func siteHandler( ) async throws -> any AsyncResponseEncodable { @Dependency(\.apiController) var apiController @Dependency(\.viewController) var viewController + @Dependency(\.projectClient) var projectClient switch route { case .api(let route): @@ -125,6 +129,11 @@ private func siteHandler( case .health: return HTTPStatus.ok case .view(let route): + // FIX: Remove. + if route == .test { + let projectID = UUID(uuidString: "E796C96C-F527-4753-A00A-EBCF25630663")! + return try await projectClient.calculateDuctSizes(projectID) + } return try await viewController.respond(route: route, request: request) } } diff --git a/Sources/ManualDClient/ManualDClient.swift b/Sources/ManualDClient/Interface.swift similarity index 99% rename from Sources/ManualDClient/ManualDClient.swift rename to Sources/ManualDClient/Interface.swift index f03813c..89ba90e 100644 --- a/Sources/ManualDClient/ManualDClient.swift +++ b/Sources/ManualDClient/Interface.swift @@ -16,7 +16,7 @@ extension DependencyValues { @DependencyClient public struct ManualDClient: Sendable { public var ductSize: @Sendable (DuctSizeRequest) async throws -> DuctSizeResponse - public var frictionRate: @Sendable (FrictionRateRequest) async throws -> FrictionRateResponse + public var frictionRate: @Sendable (FrictionRateRequest) async throws -> FrictionRate public var totalEquivalentLength: @Sendable (TotalEquivalentLengthRequest) async throws -> Int public var rectangularSize: @Sendable (RectangularSizeRequest) async throws -> RectangularSizeResponse diff --git a/Sources/ManualDClient/Live.swift b/Sources/ManualDClient/Live.swift index 5cce168..62cd048 100644 --- a/Sources/ManualDClient/Live.swift +++ b/Sources/ManualDClient/Live.swift @@ -28,7 +28,7 @@ extension ManualDClient: DependencyKey { let totalComponentLosses = request.componentPressureLosses.total let availableStaticPressure = request.externalStaticPressure - totalComponentLosses let frictionRate = availableStaticPressure * 100.0 / Double(request.totalEffectiveLength) - return .init(availableStaticPressure: availableStaticPressure, frictionRate: frictionRate) + return .init(availableStaticPressure: availableStaticPressure, value: frictionRate) }, totalEquivalentLength: { request in let trunkLengths = request.trunkLengths.reduce(0) { $0 + $1 } diff --git a/Sources/ManualDCore/FrictionRate.swift b/Sources/ManualDCore/FrictionRate.swift new file mode 100644 index 0000000..7949765 --- /dev/null +++ b/Sources/ManualDCore/FrictionRate.swift @@ -0,0 +1,14 @@ +/// Holds onto values returned when calculating the design +/// friction rate for a project. +public struct FrictionRate: Codable, Equatable, Sendable { + public let availableStaticPressure: Double + public let value: Double + + public init( + availableStaticPressure: Double, + value: Double + ) { + self.availableStaticPressure = availableStaticPressure + self.value = value + } +} diff --git a/Sources/PdfClient/Interface.swift b/Sources/PdfClient/Interface.swift new file mode 100644 index 0000000..e495f1a --- /dev/null +++ b/Sources/PdfClient/Interface.swift @@ -0,0 +1,47 @@ +import Dependencies +import DependenciesMacros +import ManualDCore + +@DependencyClient +public struct PdfClient: Sendable { + public var markdown: @Sendable (Request) async throws -> String +} + +extension PdfClient: TestDependencyKey { + public static let testValue = Self() +} + +extension PdfClient { + + public struct Request: Codable, Equatable, Sendable { + + public let project: Project + public let componentLosses: [ComponentPressureLoss] + public let ductSizes: DuctSizes + public let equipmentInfo: EquipmentInfo + public let maxSupplyTEL: EffectiveLength + public let maxReturnTEL: EffectiveLength + public let designFrictionRate: FrictionRate + public let projectSHR: Double + + public init( + project: Project, + componentLosses: [ComponentPressureLoss], + ductSizes: DuctSizes, + equipmentInfo: EquipmentInfo, + maxSupplyTEL: EffectiveLength, + maxReturnTEL: EffectiveLength, + designFrictionRate: FrictionRate, + projectSHR: Double + ) { + self.project = project + self.componentLosses = componentLosses + self.ductSizes = ductSizes + self.equipmentInfo = equipmentInfo + self.maxSupplyTEL = maxSupplyTEL + self.maxReturnTEL = maxReturnTEL + self.designFrictionRate = designFrictionRate + self.projectSHR = projectSHR + } + } +} diff --git a/Sources/PdfClient/Request+markdown.swift b/Sources/PdfClient/Request+markdown.swift new file mode 100644 index 0000000..6ec90d9 --- /dev/null +++ b/Sources/PdfClient/Request+markdown.swift @@ -0,0 +1,42 @@ +import ManualDCore + +extension PdfClient.Request { + + func toMarkdown() -> String { + var retval = """ + # Duct Calc + + **Name:** \(project.name) + **Address:** \(project.streetAddress) + \(project.city), \(project.state) \(project.zipCode) + + ## Equipment + + | | Value | + |-----------------|---------------------------------| + | Static Pressure | \(equipmentInfo.staticPressure) | + | Heating CFM | \(equipmentInfo.heatingCFM) | + | Cooling CFM | \(equipmentInfo.coolingCFM) | + + ## Friction Rate + + | | Value | + |-----------------|---------------------------------| + + """ + for row in componentLosses { + retval = """ + \(retval) + \(componentLossRow(row)) + """ + } + + return retval + } + + func componentLossRow(_ row: ComponentPressureLoss) -> String { + return """ + | \(row.name) | \(row.value) | + """ + } +} diff --git a/Sources/ProjectClient/Interface.swift b/Sources/ProjectClient/Interface.swift index e9f2d13..4de8cbe 100644 --- a/Sources/ProjectClient/Interface.swift +++ b/Sources/ProjectClient/Interface.swift @@ -58,12 +58,12 @@ extension ProjectClient { public let componentLosses: [ComponentPressureLoss] public let equivalentLengths: EffectiveLength.MaxContainer - public let frictionRate: ManualDClient.FrictionRateResponse? + public let frictionRate: FrictionRate? public init( componentLosses: [ComponentPressureLoss], equivalentLengths: EffectiveLength.MaxContainer, - frictionRate: ManualDClient.FrictionRateResponse? = nil + frictionRate: FrictionRate? = nil ) { self.componentLosses = componentLosses self.equivalentLengths = equivalentLengths diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index 6af672a..13142e2 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -15,7 +15,7 @@ extension ViewController.Request { switch route { case .test: - // let projectID = UUID(uuidString: "A9C20153-E2E5-4C65-B33F-4D8A29C63A7A")! + // let projectID = UUID(uuidString: "E796C96C-F527-4753-A00A-EBCF25630663")! return await view { await ResultView { @@ -372,7 +372,7 @@ extension SiteRoute.View.ProjectRoute.FrictionRateRoute { FrictionRateView( componentLosses: losses, equivalentLengths: lengths, - frictionRateResponse: frictionRate + frictionRate: frictionRate ) } @@ -430,7 +430,7 @@ extension SiteRoute.View.ProjectRoute.ComponentLossRoute { FrictionRateView( componentLosses: response.componentLosses, equivalentLengths: response.equivalentLengths, - frictionRateResponse: response.frictionRate + frictionRate: response.frictionRate ) } diff --git a/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift b/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift index 2a68af5..2bf2ec6 100644 --- a/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift +++ b/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift @@ -9,37 +9,33 @@ struct FrictionRateView: HTML, Sendable { let componentLosses: [ComponentPressureLoss] let equivalentLengths: EffectiveLength.MaxContainer - let frictionRateResponse: ManualDClient.FrictionRateResponse? + let frictionRate: FrictionRate? private var availableStaticPressure: Double? { - frictionRateResponse?.availableStaticPressure - } - - private var frictionRateDesignValue: Double? { - frictionRateResponse?.frictionRate + frictionRate?.availableStaticPressure } private var shouldShowBadges: Bool { - frictionRateDesignValue != nil || availableStaticPressure != nil + frictionRate != nil } private var badgeColor: String { let base = "badge-info" - guard let frictionRateDesignValue else { return base } - if frictionRateDesignValue >= 0.18 || frictionRateDesignValue <= 0.02 { + guard let frictionRate = frictionRate?.value else { return base } + if frictionRate >= 0.18 || frictionRate <= 0.02 { return "badge-error" } return base } private var showHighErrors: Bool { - guard let frictionRateDesignValue else { return false } - return frictionRateDesignValue >= 0.18 + guard let frictionRate = frictionRate?.value else { return false } + return frictionRate >= 0.18 } private var showLowErrors: Bool { - guard let frictionRateDesignValue else { return false } - return frictionRateDesignValue <= 0.02 + guard let frictionRate = frictionRate?.value else { return false } + return frictionRate <= 0.02 } private var showNoComponentLossesError: Bool { @@ -47,7 +43,7 @@ struct FrictionRateView: HTML, Sendable { } private var showIncompleteSectionsError: Bool { - availableStaticPressure == nil || frictionRateDesignValue == nil + availableStaticPressure == nil || frictionRate?.value == nil } private var hasAlerts: Bool { @@ -68,11 +64,11 @@ struct FrictionRateView: HTML, Sendable { div(.class("space-y-2 justify-end font-bold text-lg")) { if shouldShowBadges { - if let frictionRateDesignValue { + if let frictionRate = frictionRate?.value { LabeledContent { span { "Friction Rate Design Value" } } content: { - Badge(number: frictionRateDesignValue, digits: 2) + Badge(number: frictionRate, digits: 2) .attributes(.class("\(badgeColor) badge-lg")) .bold() } diff --git a/Sources/ViewController/Views/Project/ProjectView.swift b/Sources/ViewController/Views/Project/ProjectView.swift index 38389ac..891d2e7 100644 --- a/Sources/ViewController/Views/Project/ProjectView.swift +++ b/Sources/ViewController/Views/Project/ProjectView.swift @@ -234,7 +234,7 @@ extension ManualDClient { equipmentInfo: EquipmentInfo?, componentLosses: [ComponentPressureLoss], effectiveLength: EffectiveLength.MaxContainer - ) async throws -> FrictionRateResponse? { + ) async throws -> FrictionRate? { guard let staticPressure = equipmentInfo?.staticPressure else { return nil } diff --git a/ductsizes.json b/ductsizes.json new file mode 100644 index 0000000..0131f7b --- /dev/null +++ b/ductsizes.json @@ -0,0 +1,359 @@ +{ + "trunks": [ + { + "trunk": { + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663", + "type": "supply", + "name": "West", + "id": "AFBC5264-8129-4383-97D8-F44CF5258A53", + "rooms": [ + { + "room": { + "coolingTotal": 4567, + "id": "FA2F107F-5DF0-4D69-B231-DE3C6EF64134", + "registerCount": 3, + "heatingLoad": 9876, + "updatedAt": "2026-01-17T16:51:33Z", + "createdAt": "2026-01-17T16:51:33Z", + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663", + "name": "Kitchen" + }, + "registers": [1, 2, 3] + }, + { + "room": { + "coolingTotal": 4567, + "id": "9A01CEE8-6A4B-4299-9353-58AFAD042903", + "registerCount": 3, + "heatingLoad": 9876, + "updatedAt": "2026-01-17T16:51:21Z", + "createdAt": "2026-01-17T16:51:21Z", + "name": "Master", + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663" + }, + "registers": [1, 2, 3] + } + ], + "height": 8 + }, + "ductSize": { + "height": 8, + "roundSize": 12.3660960368007, + "width": 19, + "velocity": 737, + "finalSize": 14, + "flexSize": 14, + "designCFM": { + "cooling": { + "_0": 787.278055507671 + } + } + } + }, + { + "trunk": { + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663", + "name": "East", + "id": "C6A21594-0A28-4A20-BC15-628502010201", + "type": "supply", + "height": 8, + "rooms": [ + { + "room": { + "coolingTotal": 1234, + "id": "2420F9C7-0FCA-4E92-BAC9-8A054CB3201B", + "registerCount": 1, + "heatingLoad": 4567, + "updatedAt": "2026-01-17T16:51:10Z", + "createdAt": "2026-01-17T16:51:10Z", + "name": "Entry", + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663" + }, + "registers": [1] + }, + { + "room": { + "coolingTotal": 1234, + "id": "449FE324-2ACF-4C12-83A3-EB86FD45A991", + "registerCount": 2, + "heatingLoad": 4567, + "updatedAt": "2026-01-17T16:51:02Z", + "createdAt": "2026-01-17T16:51:02Z", + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663", + "name": "Bed-1" + }, + "registers": [1, 2] + } + ] + }, + "ductSize": { + "height": 8, + "roundSize": 8.39504804369037, + "width": 8, + "velocity": 643, + "flexSize": 10, + "finalSize": 9, + "designCFM": { + "heating": { + "_0": 284.587689538185 + } + } + } + }, + { + "trunk": { + "name": "Main", + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663", + "id": "C65863A0-D2EE-4D90-8C71-B1FD2454A8DF", + "type": "return", + "rooms": [ + { + "registers": [1, 2, 3], + "room": { + "coolingTotal": 4567, + "id": "FA2F107F-5DF0-4D69-B231-DE3C6EF64134", + "registerCount": 3, + "heatingLoad": 9876, + "updatedAt": "2026-01-17T16:51:33Z", + "createdAt": "2026-01-17T16:51:33Z", + "name": "Kitchen", + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663" + } + }, + { + "registers": [1, 2, 3], + "room": { + "coolingTotal": 4567, + "id": "9A01CEE8-6A4B-4299-9353-58AFAD042903", + "registerCount": 3, + "heatingLoad": 9876, + "updatedAt": "2026-01-17T16:51:21Z", + "createdAt": "2026-01-17T16:51:21Z", + "name": "Master", + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663" + } + }, + { + "registers": [1], + "room": { + "coolingTotal": 1234, + "id": "2420F9C7-0FCA-4E92-BAC9-8A054CB3201B", + "registerCount": 1, + "heatingLoad": 4567, + "updatedAt": "2026-01-17T16:51:10Z", + "createdAt": "2026-01-17T16:51:10Z", + "name": "Entry", + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663" + } + }, + { + "registers": [1, 2], + "room": { + "coolingTotal": 1234, + "id": "449FE324-2ACF-4C12-83A3-EB86FD45A991", + "registerCount": 2, + "heatingLoad": 4567, + "updatedAt": "2026-01-17T16:51:02Z", + "createdAt": "2026-01-17T16:51:02Z", + "name": "Bed-1", + "projectID": "E796C96C-F527-4753-A00A-EBCF25630663" + } + } + ] + }, + "ductSize": { + "designCFM": { + "cooling": { + "_0": 1000 + } + }, + "roundSize": 13.539327773393, + "velocity": 935, + "finalSize": 14, + "flexSize": 16 + } + } + ], + "rooms": [ + { + "ductSize": { + "velocity": 521, + "flexSize": 6, + "designCFM": { + "heating": { + "_0": 71.1469223845461 + } + }, + "roundSize": 4.95724506597333, + "finalSize": 5 + }, + "heatingCFM": 71.1469223845461, + "roomRegister": 1, + "heatingLoad": 2283.5, + "roomName": "Bed-1-1", + "roomID": "449FE324-2ACF-4C12-83A3-EB86FD45A991", + "coolingLoad": 512.11, + "coolingCFM": 53.1804861230822 + }, + { + "ductSize": { + "velocity": 521, + "flexSize": 6, + "designCFM": { + "heating": { + "_0": 71.1469223845461 + } + }, + "roundSize": 4.95724506597333, + "finalSize": 5 + }, + "heatingCFM": 71.1469223845461, + "roomRegister": 2, + "heatingLoad": 2283.5, + "roomName": "Bed-1-2", + "roomID": "449FE324-2ACF-4C12-83A3-EB86FD45A991", + "coolingLoad": 512.11, + "coolingCFM": 53.1804861230822 + }, + { + "ductSize": { + "velocity": 532, + "flexSize": 7, + "designCFM": { + "heating": { + "_0": 142.293844769092 + } + }, + "roundSize": 6.4510704920341, + "finalSize": 7 + }, + "heatingCFM": 142.293844769092, + "roomRegister": 1, + "heatingLoad": 4567, + "roomName": "Entry-1", + "roomID": "2420F9C7-0FCA-4E92-BAC9-8A054CB3201B", + "coolingLoad": 1024.22, + "coolingCFM": 106.360972246164 + }, + { + "ductSize": { + "velocity": 490, + "flexSize": 7, + "designCFM": { + "cooling": { + "_0": 131.213009251279 + } + }, + "roundSize": 6.25641154872314, + "finalSize": 7 + }, + "heatingCFM": 102.568718410303, + "roomRegister": 1, + "heatingLoad": 3292, + "roomName": "Kitchen-1", + "roomID": "FA2F107F-5DF0-4D69-B231-DE3C6EF64134", + "coolingLoad": 1263.53666666667, + "coolingCFM": 131.213009251279 + }, + { + "ductSize": { + "velocity": 490, + "flexSize": 7, + "designCFM": { + "cooling": { + "_0": 131.213009251279 + } + }, + "roundSize": 6.25641154872314, + "finalSize": 7 + }, + "heatingCFM": 102.568718410303, + "roomRegister": 2, + "heatingLoad": 3292, + "roomName": "Kitchen-2", + "roomID": "FA2F107F-5DF0-4D69-B231-DE3C6EF64134", + "coolingLoad": 1263.53666666667, + "coolingCFM": 131.213009251279 + }, + { + "ductSize": { + "velocity": 490, + "flexSize": 7, + "designCFM": { + "cooling": { + "_0": 131.213009251279 + } + }, + "roundSize": 6.25641154872314, + "finalSize": 7 + }, + "heatingCFM": 102.568718410303, + "roomRegister": 3, + "heatingLoad": 3292, + "roomName": "Kitchen-3", + "roomID": "FA2F107F-5DF0-4D69-B231-DE3C6EF64134", + "coolingLoad": 1263.53666666667, + "coolingCFM": 131.213009251279 + }, + { + "ductSize": { + "velocity": 490, + "flexSize": 7, + "designCFM": { + "cooling": { + "_0": 131.213009251279 + } + }, + "roundSize": 6.25641154872314, + "finalSize": 7 + }, + "heatingCFM": 102.568718410303, + "roomRegister": 1, + "heatingLoad": 3292, + "roomName": "Master-1", + "roomID": "9A01CEE8-6A4B-4299-9353-58AFAD042903", + "coolingLoad": 1263.53666666667, + "coolingCFM": 131.213009251279 + }, + { + "ductSize": { + "velocity": 490, + "flexSize": 7, + "designCFM": { + "cooling": { + "_0": 131.213009251279 + } + }, + "roundSize": 6.25641154872314, + "finalSize": 7 + }, + "heatingCFM": 102.568718410303, + "roomRegister": 2, + "heatingLoad": 3292, + "roomName": "Master-2", + "roomID": "9A01CEE8-6A4B-4299-9353-58AFAD042903", + "coolingLoad": 1263.53666666667, + "coolingCFM": 131.213009251279 + }, + { + "ductSize": { + "velocity": 490, + "flexSize": 7, + "designCFM": { + "cooling": { + "_0": 131.213009251279 + } + }, + "roundSize": 6.25641154872314, + "finalSize": 7 + }, + "heatingCFM": 102.568718410303, + "roomRegister": 3, + "heatingLoad": 3292, + "roomName": "Master-3", + "roomID": "9A01CEE8-6A4B-4299-9353-58AFAD042903", + "coolingLoad": 1263.53666666667, + "coolingCFM": 131.213009251279 + } +} + ]