3 Commits

14 changed files with 637 additions and 302 deletions

View File

@@ -8,6 +8,7 @@ let package = Package(
.executable(name: "App", targets: ["App"]), .executable(name: "App", targets: ["App"]),
.library(name: "ApiController", targets: ["ApiController"]), .library(name: "ApiController", targets: ["ApiController"]),
.library(name: "DatabaseClient", targets: ["DatabaseClient"]), .library(name: "DatabaseClient", targets: ["DatabaseClient"]),
.library(name: "ProjectClient", targets: ["ProjectClient"]),
.library(name: "ManualDCore", targets: ["ManualDCore"]), .library(name: "ManualDCore", targets: ["ManualDCore"]),
.library(name: "ManualDClient", targets: ["ManualDClient"]), .library(name: "ManualDClient", targets: ["ManualDClient"]),
.library(name: "Styleguide", targets: ["Styleguide"]), .library(name: "Styleguide", targets: ["Styleguide"]),
@@ -63,6 +64,13 @@ let package = Package(
.product(name: "Vapor", package: "vapor"), .product(name: "Vapor", package: "vapor"),
] ]
), ),
.target(
name: "ProjectClient",
dependencies: [
.target(name: "DatabaseClient"),
.target(name: "ManualDClient"),
]
),
.target( .target(
name: "ManualDCore", name: "ManualDCore",
dependencies: [ dependencies: [
@@ -105,6 +113,7 @@ let package = Package(
name: "ViewController", name: "ViewController",
dependencies: [ dependencies: [
.target(name: "DatabaseClient"), .target(name: "DatabaseClient"),
.target(name: "ProjectClient"),
.target(name: "ManualDClient"), .target(name: "ManualDClient"),
.target(name: "ManualDCore"), .target(name: "ManualDCore"),
.target(name: "Styleguide"), .target(name: "Styleguide"),

View File

@@ -4224,9 +4224,6 @@
.top-2 { .top-2 {
top: calc(var(--spacing) * 2); top: calc(var(--spacing) * 2);
} }
.top-\[100vh\] {
top: 100vh;
}
.right-2 { .right-2 {
right: calc(var(--spacing) * 2); right: calc(var(--spacing) * 2);
} }
@@ -4807,18 +4804,12 @@
} }
} }
} }
.col-span-1 {
grid-column: span 1 / span 1;
}
.col-span-2 { .col-span-2 {
grid-column: span 2 / span 2; grid-column: span 2 / span 2;
} }
.col-span-3 { .col-span-3 {
grid-column: span 3 / span 3; grid-column: span 3 / span 3;
} }
.col-span-5 {
grid-column: span 5 / span 5;
}
.timeline-end { .timeline-end {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
grid-column-start: 1; grid-column-start: 1;
@@ -5288,12 +5279,6 @@
} }
} }
} }
.mx-2 {
margin-inline: calc(var(--spacing) * 2);
}
.mx-4 {
margin-inline: calc(var(--spacing) * 4);
}
.mx-auto { .mx-auto {
margin-inline: auto; margin-inline: auto;
} }
@@ -5383,15 +5368,9 @@
} }
} }
} }
.my-1 {
margin-block: calc(var(--spacing) * 1);
}
.my-1\.5 { .my-1\.5 {
margin-block: calc(var(--spacing) * 1.5); margin-block: calc(var(--spacing) * 1.5);
} }
.my-4 {
margin-block: calc(var(--spacing) * 4);
}
.my-6 { .my-6 {
margin-block: calc(var(--spacing) * 6); margin-block: calc(var(--spacing) * 6);
} }
@@ -5661,9 +5640,6 @@
.mt-6 { .mt-6 {
margin-top: calc(var(--spacing) * 6); margin-top: calc(var(--spacing) * 6);
} }
.mt-auto {
margin-top: auto;
}
.breadcrumbs { .breadcrumbs {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
max-width: 100%; max-width: 100%;
@@ -5740,9 +5716,6 @@
font-weight: 600; font-weight: 600;
} }
} }
.mb-2 {
margin-bottom: calc(var(--spacing) * 2);
}
.mb-4 { .mb-4 {
margin-bottom: calc(var(--spacing) * 4); margin-bottom: calc(var(--spacing) * 4);
} }
@@ -6473,12 +6446,6 @@
.h-full { .h-full {
height: 100%; height: 100%;
} }
.h-screen {
height: 100vh;
}
.min-h-full {
min-height: 100%;
}
.min-h-screen { .min-h-screen {
min-height: 100vh; min-height: 100vh;
} }
@@ -6610,12 +6577,6 @@
width: calc(var(--size-selector, 0.25rem) * 4); width: calc(var(--size-selector, 0.25rem) * 4);
} }
} }
.w-\[300px\] {
width: 300px;
}
.w-\[310px\] {
width: 310px;
}
.w-\[330px\] { .w-\[330px\] {
width: 330px; width: 330px;
} }
@@ -6625,18 +6586,12 @@
.w-full { .w-full {
width: 100%; width: 100%;
} }
.min-w-\[100px\] {
min-width: 100px;
}
.min-w-\[200px\] { .min-w-\[200px\] {
min-width: 200px; min-width: 200px;
} }
.min-w-\[220px\] { .min-w-\[220px\] {
min-width: 220px; min-width: 220px;
} }
.min-w-\[300px\] {
min-width: 300px;
}
.min-w-full { .min-w-full {
min-width: 100%; min-width: 100%;
} }
@@ -6839,15 +6794,9 @@
.grid-cols-3 { .grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
.flex-row {
flex-direction: row;
}
.flex-wrap { .flex-wrap {
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -6872,9 +6821,6 @@
.justify-start { .justify-start {
justify-content: flex-start; justify-content: flex-start;
} }
.justify-items-end {
justify-items: end;
}
.gap-1 { .gap-1 {
gap: calc(var(--spacing) * 1); gap: calc(var(--spacing) * 1);
} }
@@ -6884,9 +6830,6 @@
.gap-4 { .gap-4 {
gap: calc(var(--spacing) * 4); gap: calc(var(--spacing) * 4);
} }
.gap-6 {
gap: calc(var(--spacing) * 6);
}
.space-y-1 { .space-y-1 {
:where(& > :not(:last-child)) { :where(& > :not(:last-child)) {
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
@@ -6936,9 +6879,6 @@
margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse))); margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)));
} }
} }
.gap-y-2 {
row-gap: calc(var(--spacing) * 2);
}
.gap-y-4 { .gap-y-4 {
row-gap: calc(var(--spacing) * 4); row-gap: calc(var(--spacing) * 4);
} }
@@ -7230,10 +7170,6 @@
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 2px; border-width: 2px;
} }
.border-b {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px;
}
.border-b-1 { .border-b-1 {
border-bottom-style: var(--tw-border-style); border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px; border-bottom-width: 1px;
@@ -7507,9 +7443,6 @@
} }
} }
} }
.bg-base-100 {
background-color: var(--color-base-100);
}
.bg-base-300 { .bg-base-300 {
background-color: var(--color-base-300); background-color: var(--color-base-300);
} }
@@ -7939,9 +7872,6 @@
.px-4 { .px-4 {
padding-inline: calc(var(--spacing) * 4); padding-inline: calc(var(--spacing) * 4);
} }
.py-1 {
padding-block: calc(var(--spacing) * 1);
}
.py-1\.5 { .py-1\.5 {
padding-block: calc(var(--spacing) * 1.5); padding-block: calc(var(--spacing) * 1.5);
} }
@@ -7968,9 +7898,6 @@
.pt-2 { .pt-2 {
padding-top: calc(var(--spacing) * 2); padding-top: calc(var(--spacing) * 2);
} }
.pt-6 {
padding-top: calc(var(--spacing) * 6);
}
.pb-6 { .pb-6 {
padding-bottom: calc(var(--spacing) * 6); padding-bottom: calc(var(--spacing) * 6);
} }
@@ -8603,9 +8530,6 @@
.text-info { .text-info {
color: var(--color-info); color: var(--color-info);
} }
.text-primary {
color: var(--color-primary);
}
.text-slate-900 { .text-slate-900 {
color: var(--color-slate-900); color: var(--color-slate-900);
} }

View File

@@ -73,10 +73,16 @@ extension DatabaseClient.Projects: TestDependencyKey {
) )
}, },
getSensibleHeatRatio: { id in getSensibleHeatRatio: { id in
guard let model = try await ProjectModel.find(id, on: database) else { guard
let shr = try await ProjectModel.query(on: database)
.field(\.$id)
.field(\.$sensibleHeatRatio)
.filter(\.$id == id)
.first()
else {
throw NotFoundError() throw NotFoundError()
} }
return model.sensibleHeatRatio return shr.sensibleHeatRatio
}, },
fetch: { userID, request in fetch: { userID, request in
try await ProjectModel.query(on: database) try await ProjectModel.query(on: database)

View File

@@ -3,6 +3,13 @@ import DependenciesMacros
import Logging import Logging
import ManualDCore import ManualDCore
extension DependencyValues {
public var manualD: ManualDClient {
get { self[ManualDClient.self] }
set { self[ManualDClient.self] = newValue }
}
}
@DependencyClient @DependencyClient
public struct ManualDClient: Sendable { public struct ManualDClient: Sendable {
public var ductSize: @Sendable (DuctSizeRequest) async throws -> DuctSizeResponse public var ductSize: @Sendable (DuctSizeRequest) async throws -> DuctSizeResponse
@@ -11,144 +18,155 @@ public struct ManualDClient: Sendable {
public var equivalentRectangularDuct: public var equivalentRectangularDuct:
@Sendable (EquivalentRectangularDuctRequest) async throws -> EquivalentRectangularDuctResponse @Sendable (EquivalentRectangularDuctRequest) async throws -> EquivalentRectangularDuctResponse
public func calculateSizes( // TODO: add (Project.ID) async throws -> ProjectResponse
rooms: [Room],
trunks: [DuctSizing.TrunkSize],
equipmentInfo: EquipmentInfo,
maxSupplyLength: EffectiveLength,
maxReturnLength: EffectiveLength,
designFrictionRate: Double,
projectSHR: Double,
logger: Logger? = nil
) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) {
try await (
calculateSizes(
rooms: rooms, equipmentInfo: equipmentInfo,
maxSupplyLength: maxSupplyLength, maxReturnLength: maxReturnLength,
designFrictionRate: designFrictionRate, projectSHR: projectSHR
),
calculateSizes(
rooms: rooms, trunks: trunks, equipmentInfo: equipmentInfo,
maxSupplyLength: maxSupplyLength, maxReturnLength: maxReturnLength,
designFrictionRate: designFrictionRate, projectSHR: projectSHR)
)
}
func calculateSizes( // TODO: Move to helpers.
rooms: [Room], // public func calculateSizes(
equipmentInfo: EquipmentInfo, // rooms: [Room],
maxSupplyLength: EffectiveLength, // trunks: [DuctSizing.TrunkSize],
maxReturnLength: EffectiveLength, // equipmentInfo: EquipmentInfo,
designFrictionRate: Double, // maxSupplyLength: EffectiveLength,
projectSHR: Double, // maxReturnLength: EffectiveLength,
logger: Logger? = nil // designFrictionRate: Double,
) async throws -> [DuctSizing.RoomContainer] { // projectSHR: Double,
// logger: Logger? = nil
var retval: [DuctSizing.RoomContainer] = [] // ) async throws -> ProjectResponse {
let totalHeatingLoad = rooms.totalHeatingLoad // try await .init(
let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR) // rooms: calculateRoomSizes(
// rooms: rooms,
for room in rooms { // equipmentInfo: equipmentInfo,
let heatingLoad = room.heatingLoadPerRegister // maxSupplyLength: maxSupplyLength,
let coolingLoad = room.coolingSensiblePerRegister(projectSHR: projectSHR) // maxReturnLength: maxReturnLength,
let heatingPercent = heatingLoad / totalHeatingLoad // designFrictionRate: designFrictionRate,
let coolingPercent = coolingLoad / totalCoolingSensible // projectSHR: projectSHR
let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM) // ),
let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM) // trunks: calculateTrunkSizes(
let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM) // rooms: rooms,
let sizes = try await self.ductSize( // trunks: trunks,
.init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate) // equipmentInfo: equipmentInfo,
) // maxSupplyLength: maxSupplyLength,
// maxReturnLength: maxReturnLength,
for n in 1...room.registerCount { // designFrictionRate: designFrictionRate,
// projectSHR: projectSHR
var rectangularWidth: Int? = nil // )
let rectangularSize = room.rectangularSizes? // )
.first(where: { $0.register == nil || $0.register == n }) // }
//
if let rectangularSize { // func calculateRoomSizes(
let response = try await self.equivalentRectangularDuct( // rooms: [Room],
.init(round: sizes.finalSize, height: rectangularSize.height) // equipmentInfo: EquipmentInfo,
) // maxSupplyLength: EffectiveLength,
rectangularWidth = response.width // maxReturnLength: EffectiveLength,
} // designFrictionRate: Double,
// projectSHR: Double,
retval.append( // logger: Logger? = nil
.init( // ) async throws -> [DuctSizing.RoomContainer] {
roomID: room.id, //
roomName: "\(room.name)-\(n)", // var retval: [DuctSizing.RoomContainer] = []
roomRegister: n, // let totalHeatingLoad = rooms.totalHeatingLoad
heatingLoad: heatingLoad, // let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR)
coolingLoad: coolingLoad, //
heatingCFM: heatingCFM, // for room in rooms {
coolingCFM: coolingCFM, // let heatingLoad = room.heatingLoadPerRegister
designCFM: designCFM, // let coolingLoad = room.coolingSensiblePerRegister(projectSHR: projectSHR)
roundSize: sizes.ductulatorSize, // let heatingPercent = heatingLoad / totalHeatingLoad
finalSize: sizes.finalSize, // let coolingPercent = coolingLoad / totalCoolingSensible
velocity: sizes.velocity, // let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM)
flexSize: sizes.flexSize, // let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM)
rectangularSize: rectangularSize, // let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
rectangularWidth: rectangularWidth // let sizes = try await self.ductSize(
) // .init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate)
) // )
} //
} // for n in 1...room.registerCount {
//
return retval // var rectangularWidth: Int? = nil
} // let rectangularSize = room.rectangularSizes?
// .first(where: { $0.register == nil || $0.register == n })
func calculateSizes( //
rooms: [Room], // if let rectangularSize {
trunks: [DuctSizing.TrunkSize], // let response = try await self.equivalentRectangularDuct(
equipmentInfo: EquipmentInfo, // .init(round: sizes.finalSize, height: rectangularSize.height)
maxSupplyLength: EffectiveLength, // )
maxReturnLength: EffectiveLength, // rectangularWidth = response.width
designFrictionRate: Double, // }
projectSHR: Double, //
logger: Logger? = nil // retval.append(
) async throws -> [DuctSizing.TrunkContainer] { // .init(
// roomID: room.id,
var retval = [DuctSizing.TrunkContainer]() // roomName: "\(room.name)-\(n)",
let totalHeatingLoad = rooms.totalHeatingLoad // roomRegister: n,
let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR) // heatingLoad: heatingLoad,
// coolingLoad: coolingLoad,
for trunk in trunks { // heatingCFM: heatingCFM,
let heatingLoad = trunk.totalHeatingLoad // coolingCFM: coolingCFM,
let coolingLoad = trunk.totalCoolingSensible(projectSHR: projectSHR) // designCFM: designCFM,
let heatingPercent = heatingLoad / totalHeatingLoad // roundSize: sizes.ductulatorSize,
let coolingPercent = coolingLoad / totalCoolingSensible // finalSize: sizes.finalSize,
let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM) // velocity: sizes.velocity,
let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM) // flexSize: sizes.flexSize,
let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM) // rectangularSize: rectangularSize,
let sizes = try await self.ductSize( // rectangularWidth: rectangularWidth
.init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate) // )
) // )
var width: Int? = nil // }
if let height = trunk.height { // }
let rectangularSize = try await self.equivalentRectangularDuct( //
.init(round: sizes.finalSize, height: height) // return retval
) // }
width = rectangularSize.width //
} // func calculateTrunkSizes(
// rooms: [Room],
retval.append( // trunks: [DuctSizing.TrunkSize],
.init( // equipmentInfo: EquipmentInfo,
trunk: trunk, // maxSupplyLength: EffectiveLength,
ductSize: .init( // maxReturnLength: EffectiveLength,
designCFM: designCFM, // designFrictionRate: Double,
roundSize: sizes.ductulatorSize, // projectSHR: Double,
finalSize: sizes.finalSize, // logger: Logger? = nil
velocity: sizes.velocity, // ) async throws -> [DuctSizing.TrunkContainer] {
flexSize: sizes.flexSize, //
height: trunk.height, // var retval = [DuctSizing.TrunkContainer]()
width: width // let totalHeatingLoad = rooms.totalHeatingLoad
) // let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR)
) //
) // for trunk in trunks {
} // let heatingLoad = trunk.totalHeatingLoad
// let coolingLoad = trunk.totalCoolingSensible(projectSHR: projectSHR)
return retval // let heatingPercent = heatingLoad / totalHeatingLoad
} // let coolingPercent = coolingLoad / totalCoolingSensible
// let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM)
// let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM)
// let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
// let sizes = try await self.ductSize(
// .init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate)
// )
// var width: Int? = nil
// if let height = trunk.height {
// let rectangularSize = try await self.equivalentRectangularDuct(
// .init(round: sizes.finalSize, height: height)
// )
// width = rectangularSize.width
// }
//
// retval.append(
// .init(
// trunk: trunk,
// ductSize: .init(
// designCFM: designCFM,
// roundSize: sizes.ductulatorSize,
// finalSize: sizes.finalSize,
// velocity: sizes.velocity,
// flexSize: sizes.flexSize,
// height: trunk.height,
// width: width
// )
// )
// )
// }
//
// return retval
// }
} }
@@ -156,15 +174,27 @@ extension ManualDClient: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
} }
extension DependencyValues { // MARK: Project Response
public var manualD: ManualDClient { // extension ManualDClient {
get { self[ManualDClient.self] } //
set { self[ManualDClient.self] = newValue } // public struct ProjectResponse: Codable, Equatable, Sendable {
} // public let rooms: [DuctSizing.RoomContainer]
} // public let trunks: [DuctSizing.TrunkContainer]
//
// public init(
// rooms: [DuctSizing.RoomContainer],
// trunks: [DuctSizing.TrunkContainer]
// ) {
// self.rooms = rooms
// self.trunks = trunks
// }
// }
//
// }
// MARK: Duct Size // MARK: Duct Size
extension ManualDClient { extension ManualDClient {
public struct DuctSizeRequest: Codable, Equatable, Sendable { public struct DuctSizeRequest: Codable, Equatable, Sendable {
public let designCFM: Int public let designCFM: Int
public let frictionRate: Double public let frictionRate: Double

View File

@@ -0,0 +1,9 @@
import Foundation
public struct DuctCalcClientError: Error {
public let reason: String
public init(_ reason: String) {
self.reason = reason
}
}

View File

@@ -0,0 +1,35 @@
import Dependencies
import DependenciesMacros
import ManualDCore
extension DependencyValues {
public var projectClient: ProjectClient {
get { self[ProjectClient.self] }
set { self[ProjectClient.self] = newValue }
}
}
@DependencyClient
public struct ProjectClient: Sendable {
public var calculateDuctSizes: @Sendable (Project.ID) async throws -> ProjectResponse
}
extension ProjectClient: TestDependencyKey {
public static let testValue = Self()
}
extension ProjectClient {
public struct ProjectResponse: Codable, Equatable, Sendable {
public let rooms: [DuctSizing.RoomContainer]
public let trunks: [DuctSizing.TrunkContainer]
public init(
rooms: [DuctSizing.RoomContainer],
trunks: [DuctSizing.TrunkContainer]
) {
self.rooms = rooms
self.trunks = trunks
}
}
}

View File

@@ -0,0 +1,83 @@
import DatabaseClient
import Dependencies
import ManualDClient
import ManualDCore
extension DatabaseClient {
func calculateDuctSizes(
projectID: Project.ID
) async throws -> ProjectClient.ProjectResponse {
@Dependency(\.manualD) var manualD
guard let dfrResponse = try await designFrictionRate(projectID: projectID) else {
throw DuctCalcClientError("Project not complete.")
}
let ensuredTEL = try dfrResponse.ensureMaxContainer()
return try await manualD.calculateDuctSizes(
rooms: rooms.fetch(projectID),
trunks: trunkSizes.fetch(projectID),
equipmentInfo: dfrResponse.equipmentInfo,
maxSupplyLength: ensuredTEL.supply,
maxReturnLength: ensuredTEL.return,
designFrictionRate: dfrResponse.designFrictionRate,
projectSHR: ensuredSHR(projectID)
)
}
// Fetches the project sensible heat ratio or throws an error if it's nil.
func ensuredSHR(_ projectID: Project.ID) async throws -> Double {
guard let projectSHR = try await projects.getSensibleHeatRatio(projectID) else {
throw DuctCalcClientError("Project sensible heat ratio not set.")
}
return projectSHR
}
// Internal container.
struct DesignFrictionRateResponse: Equatable, Sendable {
typealias EnsuredTEL = (supply: EffectiveLength, return: EffectiveLength)
let designFrictionRate: Double
let equipmentInfo: EquipmentInfo
let telMaxContainer: EffectiveLength.MaxContainer
func ensureMaxContainer() throws -> EnsuredTEL {
guard let maxSupplyLength = telMaxContainer.supply else {
throw DuctCalcClientError("Max supply TEL not found")
}
guard let maxReturnLength = telMaxContainer.return else {
throw DuctCalcClientError("Max supply TEL not found")
}
return (maxSupplyLength, maxReturnLength)
}
}
func designFrictionRate(
projectID: Project.ID
) async throws -> DesignFrictionRateResponse? {
guard let equipmentInfo = try await equipment.fetch(projectID) else {
return nil
}
let equivalentLengths = try await effectiveLength.fetchMax(projectID)
guard let tel = equivalentLengths.total else { return nil }
let componentLosses = try await componentLoss.fetch(projectID)
guard componentLosses.count > 0 else { return nil }
let availableStaticPressure =
equipmentInfo.staticPressure - componentLosses.total
return .init(
designFrictionRate: (availableStaticPressure * 100) / tel,
equipmentInfo: equipmentInfo,
telMaxContainer: equivalentLengths
)
}
}

View File

@@ -0,0 +1,201 @@
import Logging
import ManualDClient
import ManualDCore
// TODO: Remove Logger and use depedency logger.
extension ManualDClient {
func calculateDuctSizes(
rooms: [Room],
trunks: [DuctSizing.TrunkSize],
equipmentInfo: EquipmentInfo,
maxSupplyLength: EffectiveLength,
maxReturnLength: EffectiveLength,
designFrictionRate: Double,
projectSHR: Double,
logger: Logger? = nil
) async throws -> ProjectClient.ProjectResponse {
try await .init(
rooms: calculateRoomSizes(
rooms: rooms,
equipmentInfo: equipmentInfo,
maxSupplyLength: maxSupplyLength,
maxReturnLength: maxReturnLength,
designFrictionRate: designFrictionRate,
projectSHR: projectSHR
),
trunks: calculateTrunkSizes(
rooms: rooms,
trunks: trunks,
equipmentInfo: equipmentInfo,
maxSupplyLength: maxSupplyLength,
maxReturnLength: maxReturnLength,
designFrictionRate: designFrictionRate,
projectSHR: projectSHR
)
)
}
func calculateRoomSizes(
rooms: [Room],
equipmentInfo: EquipmentInfo,
maxSupplyLength: EffectiveLength,
maxReturnLength: EffectiveLength,
designFrictionRate: Double,
projectSHR: Double,
logger: Logger? = nil
) async throws -> [DuctSizing.RoomContainer] {
var retval: [DuctSizing.RoomContainer] = []
let totalHeatingLoad = rooms.totalHeatingLoad
let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR)
for room in rooms {
let heatingLoad = room.heatingLoadPerRegister
let coolingLoad = room.coolingSensiblePerRegister(projectSHR: projectSHR)
let heatingPercent = heatingLoad / totalHeatingLoad
let coolingPercent = coolingLoad / totalCoolingSensible
let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM)
let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM)
let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
let sizes = try await self.ductSize(
.init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate)
)
for n in 1...room.registerCount {
var rectangularWidth: Int? = nil
let rectangularSize = room.rectangularSizes?
.first(where: { $0.register == nil || $0.register == n })
if let rectangularSize {
let response = try await self.equivalentRectangularDuct(
.init(round: sizes.finalSize, height: rectangularSize.height)
)
rectangularWidth = response.width
}
retval.append(
.init(
roomID: room.id,
roomName: "\(room.name)-\(n)",
roomRegister: n,
heatingLoad: heatingLoad,
coolingLoad: coolingLoad,
heatingCFM: heatingCFM,
coolingCFM: coolingCFM,
designCFM: designCFM,
roundSize: sizes.ductulatorSize,
finalSize: sizes.finalSize,
velocity: sizes.velocity,
flexSize: sizes.flexSize,
rectangularSize: rectangularSize,
rectangularWidth: rectangularWidth
)
)
}
}
return retval
}
func calculateTrunkSizes(
rooms: [Room],
trunks: [DuctSizing.TrunkSize],
equipmentInfo: EquipmentInfo,
maxSupplyLength: EffectiveLength,
maxReturnLength: EffectiveLength,
designFrictionRate: Double,
projectSHR: Double,
logger: Logger? = nil
) async throws -> [DuctSizing.TrunkContainer] {
var retval = [DuctSizing.TrunkContainer]()
let totalHeatingLoad = rooms.totalHeatingLoad
let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR)
for trunk in trunks {
let heatingLoad = trunk.totalHeatingLoad
let coolingLoad = trunk.totalCoolingSensible(projectSHR: projectSHR)
let heatingPercent = heatingLoad / totalHeatingLoad
let coolingPercent = coolingLoad / totalCoolingSensible
let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM)
let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM)
let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
let sizes = try await self.ductSize(
.init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate)
)
var width: Int? = nil
if let height = trunk.height {
let rectangularSize = try await self.equivalentRectangularDuct(
.init(round: sizes.finalSize, height: height)
)
width = rectangularSize.width
}
retval.append(
.init(
trunk: trunk,
ductSize: .init(
designCFM: designCFM,
roundSize: sizes.ductulatorSize,
finalSize: sizes.finalSize,
velocity: sizes.velocity,
flexSize: sizes.flexSize,
height: trunk.height,
width: width
)
)
)
}
return retval
}
}
extension Room {
var heatingLoadPerRegister: Double {
heatingLoad / Double(registerCount)
}
func coolingSensiblePerRegister(projectSHR: Double) -> Double {
let sensible = coolingSensible ?? (coolingTotal * projectSHR)
return sensible / Double(registerCount)
}
}
extension DuctSizing.TrunkSize.RoomProxy {
// We need to make sure if registers got removed after a trunk
// was already made / saved that we do not include registers that
// no longer exist.
private var actualRegisterCount: Int {
guard registers.count <= room.registerCount else {
return room.registerCount
}
return registers.count
}
var totalHeatingLoad: Double {
room.heatingLoadPerRegister * Double(actualRegisterCount)
}
func totalCoolingSensible(projectSHR: Double) -> Double {
room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(actualRegisterCount)
}
}
extension DuctSizing.TrunkSize {
var totalHeatingLoad: Double {
rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad }
}
func totalCoolingSensible(projectSHR: Double) -> Double {
rooms.reduce(into: 0) { $0 += $1.totalCoolingSensible(projectSHR: projectSHR) }
}
}

View File

@@ -0,0 +1,19 @@
import DatabaseClient
import Dependencies
import Logging
import ManualDClient
import ManualDCore
extension ProjectClient: DependencyKey {
public static var liveValue: Self {
@Dependency(\.database) var database
return .init(
calculateDuctSizes: { projectID in
try await database.calculateDuctSizes(projectID: projectID)
}
)
}
}

View File

@@ -24,42 +24,42 @@ extension DatabaseClient.Projects {
} }
} }
extension DatabaseClient { // extension DatabaseClient {
//
func calculateDuctSizes( // func calculateDuctSizes(
projectID: Project.ID // projectID: Project.ID
) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) { // ) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) {
@Dependency(\.manualD) var manualD // @Dependency(\.manualD) var manualD
//
return try await manualD.calculate( // return try await manualD.calculate(
rooms: rooms.fetch(projectID), // rooms: rooms.fetch(projectID),
trunks: trunkSizes.fetch(projectID), // trunks: trunkSizes.fetch(projectID),
designFrictionRateResult: designFrictionRate(projectID: projectID), // designFrictionRateResult: designFrictionRate(projectID: projectID),
projectSHR: projects.getSensibleHeatRatio(projectID) // projectSHR: projects.getSensibleHeatRatio(projectID)
) // )
} // }
//
func designFrictionRate( // func designFrictionRate(
projectID: Project.ID // projectID: Project.ID
) async throws -> (EquipmentInfo, EffectiveLength.MaxContainer, Double)? { // ) async throws -> (EquipmentInfo, EffectiveLength.MaxContainer, Double)? {
guard let equipmentInfo = try await equipment.fetch(projectID) else { // guard let equipmentInfo = try await equipment.fetch(projectID) else {
return nil // return nil
} // }
//
let equivalentLengths = try await effectiveLength.fetchMax(projectID) // let equivalentLengths = try await effectiveLength.fetchMax(projectID)
guard let tel = equivalentLengths.total else { return nil } // guard let tel = equivalentLengths.total else { return nil }
//
let componentLosses = try await componentLoss.fetch(projectID) // let componentLosses = try await componentLoss.fetch(projectID)
guard componentLosses.count > 0 else { return nil } // guard componentLosses.count > 0 else { return nil }
//
let availableStaticPressure = // let availableStaticPressure =
equipmentInfo.staticPressure - componentLosses.total // equipmentInfo.staticPressure - componentLosses.total
//
let designFrictionRate = (availableStaticPressure * 100) / tel // let designFrictionRate = (availableStaticPressure * 100) / tel
//
return (equipmentInfo, equivalentLengths, designFrictionRate) // return (equipmentInfo, equivalentLengths, designFrictionRate)
} // }
} // }
extension DatabaseClient.ComponentLoss { extension DatabaseClient.ComponentLoss {

View File

@@ -1,38 +1,38 @@
import Logging // import Logging
import ManualDClient // import ManualDClient
import ManualDCore // import ManualDCore
//
extension ManualDClient { // extension ManualDClient {
//
func calculate( // func calculate(
rooms: [Room], // rooms: [Room],
trunks: [DuctSizing.TrunkSize], // trunks: [DuctSizing.TrunkSize],
designFrictionRateResult: (EquipmentInfo, EffectiveLength.MaxContainer, Double)?, // designFrictionRateResult: (EquipmentInfo, EffectiveLength.MaxContainer, Double)?,
projectSHR: Double?, // projectSHR: Double?,
logger: Logger? = nil // logger: Logger? = nil
) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) { // ) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) {
guard let designFrictionRateResult else { return ([], []) } // guard let designFrictionRateResult else { return ([], []) }
let equipmentInfo = designFrictionRateResult.0 // let equipmentInfo = designFrictionRateResult.0
let effectiveLengths = designFrictionRateResult.1 // let effectiveLengths = designFrictionRateResult.1
let designFrictionRate = designFrictionRateResult.2 // let designFrictionRate = designFrictionRateResult.2
//
guard let maxSupply = effectiveLengths.supply else { return ([], []) } // guard let maxSupply = effectiveLengths.supply else { return ([], []) }
guard let maxReturn = effectiveLengths.return else { return ([], []) } // guard let maxReturn = effectiveLengths.return else { return ([], []) }
//
let ductRooms = try await self.calculateSizes( // let ductRooms = try await self.calculateSizes(
rooms: rooms, // rooms: rooms,
trunks: trunks, // trunks: trunks,
equipmentInfo: equipmentInfo, // equipmentInfo: equipmentInfo,
maxSupplyLength: maxSupply, // maxSupplyLength: maxSupply,
maxReturnLength: maxReturn, // maxReturnLength: maxReturn,
designFrictionRate: designFrictionRate, // designFrictionRate: designFrictionRate,
projectSHR: projectSHR ?? 1.0, // projectSHR: projectSHR ?? 1.0,
logger: logger // logger: logger
) // )
//
// logger?.debug("Rooms: \(ductRooms)") // // logger?.debug("Rooms: \(ductRooms)")
//
return ductRooms // return ductRooms
//
} // }
} // }

View File

@@ -3,6 +3,7 @@ import Dependencies
import Elementary import Elementary
import Foundation import Foundation
import ManualDCore import ManualDCore
import ProjectClient
import Styleguide import Styleguide
extension ViewController.Request { extension ViewController.Request {
@@ -10,6 +11,7 @@ extension ViewController.Request {
func render() async -> AnySendableHTML { func render() async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
@Dependency(\.projectClient) var projectClient
switch route { switch route {
case .test: case .test:
@@ -18,7 +20,7 @@ extension ViewController.Request {
await ResultView { await ResultView {
return ( return (
try await database.projects.getCompletedSteps(projectID), try await database.projects.getCompletedSteps(projectID),
try await database.calculateDuctSizes(projectID: projectID) try await projectClient.calculateDuctSizes(projectID)
) )
} onSuccess: { (_, result) in } onSuccess: { (_, result) in
TestPage(trunks: result.trunks, rooms: result.rooms) TestPage(trunks: result.trunks, rooms: result.rooms)
@@ -65,10 +67,13 @@ extension ViewController.Request {
// let user = try currentUser() // let user = try currentUser()
return ( return (
userID, userID,
try await database.projects.fetch(userID, .init(page: 1, per: 25)) try await database.projects.fetch(userID, .init(page: 1, per: 25)),
profile.theme
) )
} onSuccess: { (userID, projects) in } onSuccess: { (userID, projects, theme) in
ProjectsTable(userID: userID, projects: projects) MainPage(displayFooter: true, theme: theme) {
ProjectsTable(userID: userID, projects: projects)
}
} }
} }
} }
@@ -552,6 +557,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
) async -> AnySendableHTML { ) async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
@Dependency(\.manualD) var manualD @Dependency(\.manualD) var manualD
@Dependency(\.projectClient) var projectClient
switch self { switch self {
case .index: case .index:
@@ -560,7 +566,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
case .deleteRectangularSize(let roomID, let request): case .deleteRectangularSize(let roomID, let request):
return await ResultView { return await ResultView {
let room = try await database.rooms.deleteRectangularSize(roomID, request.rectangularSizeID) let room = try await database.rooms.deleteRectangularSize(roomID, request.rectangularSizeID)
return try await database.calculateDuctSizes(projectID: projectID) return try await projectClient.calculateDuctSizes(projectID)
.rooms .rooms
.filter({ $0.roomID == room.id && $0.roomRegister == request.register }) .filter({ $0.roomID == room.id && $0.roomRegister == request.register })
.first! .first!
@@ -574,7 +580,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
roomID, roomID,
.init(id: form.id ?? .init(), register: form.register, height: form.height) .init(id: form.id ?? .init(), register: form.register, height: form.height)
) )
return try await database.calculateDuctSizes(projectID: projectID) return try await projectClient.calculateDuctSizes(projectID)
.rooms .rooms
.filter({ $0.roomID == room.id && $0.roomRegister == form.register }) .filter({ $0.roomID == room.id && $0.roomRegister == form.register })
.first! .first!
@@ -609,13 +615,14 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
catching: @escaping @Sendable () async throws -> Void = {} catching: @escaping @Sendable () async throws -> Void = {}
) async -> AnySendableHTML { ) async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
@Dependency(\.projectClient) var project
return await request.view { return await request.view {
await ResultView { await ResultView {
try await catching() try await catching()
return ( return (
try await database.projects.getCompletedSteps(projectID), try await database.projects.getCompletedSteps(projectID),
try await database.calculateDuctSizes(projectID: projectID) try await project.calculateDuctSizes(projectID)
) )
} onSuccess: { (steps, ducts) in } onSuccess: { (steps, ducts) in
ProjectView(projectID: projectID, activeTab: .ductSizing, completedSteps: steps) { ProjectView(projectID: projectID, activeTab: .ductSizing, completedSteps: steps) {

9
TODO.md Normal file
View File

@@ -0,0 +1,9 @@
# TODO's
- [x] Fix theme not working when selected upon signup.
- [ ] Pdf generation
- [ ] Add postgres / mysql support
- [ ] Opensource / license ??
- [ ] Figure out domain to host (currently thinking ductcalc.pro)
- [ ] Logo / navbar name may have to change if it's not duct-calc.
- [ ] MainPage meta items will have to change also

View File

@@ -1,6 +1,9 @@
docker_image := "ductcalc" docker_image := "ductcalc"
docker_tag := "latest" docker_tag := "latest"
clean:
rm -rf .build
install-deps: install-deps:
@curl -sL daisyui.com/fast | bash @curl -sL daisyui.com/fast | bash