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

View File

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

View File

@@ -73,10 +73,16 @@ extension DatabaseClient.Projects: TestDependencyKey {
)
},
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()
}
return model.sensibleHeatRatio
return shr.sensibleHeatRatio
},
fetch: { userID, request in
try await ProjectModel.query(on: database)

View File

@@ -3,6 +3,13 @@ import DependenciesMacros
import Logging
import ManualDCore
extension DependencyValues {
public var manualD: ManualDClient {
get { self[ManualDClient.self] }
set { self[ManualDClient.self] = newValue }
}
}
@DependencyClient
public struct ManualDClient: Sendable {
public var ductSize: @Sendable (DuctSizeRequest) async throws -> DuctSizeResponse
@@ -11,144 +18,155 @@ public struct ManualDClient: Sendable {
public var equivalentRectangularDuct:
@Sendable (EquivalentRectangularDuctRequest) async throws -> EquivalentRectangularDuctResponse
public func calculateSizes(
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)
)
}
// TODO: add (Project.ID) async throws -> ProjectResponse
func calculateSizes(
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 calculateSizes(
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
}
// TODO: Move to helpers.
// public func calculateSizes(
// rooms: [Room],
// trunks: [DuctSizing.TrunkSize],
// equipmentInfo: EquipmentInfo,
// maxSupplyLength: EffectiveLength,
// maxReturnLength: EffectiveLength,
// designFrictionRate: Double,
// projectSHR: Double,
// logger: Logger? = nil
// ) async throws -> 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
// }
}
@@ -156,15 +174,27 @@ extension ManualDClient: TestDependencyKey {
public static let testValue = Self()
}
extension DependencyValues {
public var manualD: ManualDClient {
get { self[ManualDClient.self] }
set { self[ManualDClient.self] = newValue }
}
}
// MARK: Project Response
// extension ManualDClient {
//
// 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
extension ManualDClient {
public struct DuctSizeRequest: Codable, Equatable, Sendable {
public let designCFM: Int
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 {
func calculateDuctSizes(
projectID: Project.ID
) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) {
@Dependency(\.manualD) var manualD
return try await manualD.calculate(
rooms: rooms.fetch(projectID),
trunks: trunkSizes.fetch(projectID),
designFrictionRateResult: designFrictionRate(projectID: projectID),
projectSHR: projects.getSensibleHeatRatio(projectID)
)
}
func designFrictionRate(
projectID: Project.ID
) async throws -> (EquipmentInfo, EffectiveLength.MaxContainer, Double)? {
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
let designFrictionRate = (availableStaticPressure * 100) / tel
return (equipmentInfo, equivalentLengths, designFrictionRate)
}
}
// extension DatabaseClient {
//
// func calculateDuctSizes(
// projectID: Project.ID
// ) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) {
// @Dependency(\.manualD) var manualD
//
// return try await manualD.calculate(
// rooms: rooms.fetch(projectID),
// trunks: trunkSizes.fetch(projectID),
// designFrictionRateResult: designFrictionRate(projectID: projectID),
// projectSHR: projects.getSensibleHeatRatio(projectID)
// )
// }
//
// func designFrictionRate(
// projectID: Project.ID
// ) async throws -> (EquipmentInfo, EffectiveLength.MaxContainer, Double)? {
// 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
//
// let designFrictionRate = (availableStaticPressure * 100) / tel
//
// return (equipmentInfo, equivalentLengths, designFrictionRate)
// }
// }
extension DatabaseClient.ComponentLoss {

View File

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

View File

@@ -3,6 +3,7 @@ import Dependencies
import Elementary
import Foundation
import ManualDCore
import ProjectClient
import Styleguide
extension ViewController.Request {
@@ -10,6 +11,7 @@ extension ViewController.Request {
func render() async -> AnySendableHTML {
@Dependency(\.database) var database
@Dependency(\.projectClient) var projectClient
switch route {
case .test:
@@ -18,7 +20,7 @@ extension ViewController.Request {
await ResultView {
return (
try await database.projects.getCompletedSteps(projectID),
try await database.calculateDuctSizes(projectID: projectID)
try await projectClient.calculateDuctSizes(projectID)
)
} onSuccess: { (_, result) in
TestPage(trunks: result.trunks, rooms: result.rooms)
@@ -65,10 +67,13 @@ extension ViewController.Request {
// let user = try currentUser()
return (
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
ProjectsTable(userID: userID, projects: projects)
} onSuccess: { (userID, projects, theme) in
MainPage(displayFooter: true, theme: theme) {
ProjectsTable(userID: userID, projects: projects)
}
}
}
}
@@ -552,6 +557,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
) async -> AnySendableHTML {
@Dependency(\.database) var database
@Dependency(\.manualD) var manualD
@Dependency(\.projectClient) var projectClient
switch self {
case .index:
@@ -560,7 +566,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
case .deleteRectangularSize(let roomID, let request):
return await ResultView {
let room = try await database.rooms.deleteRectangularSize(roomID, request.rectangularSizeID)
return try await database.calculateDuctSizes(projectID: projectID)
return try await projectClient.calculateDuctSizes(projectID)
.rooms
.filter({ $0.roomID == room.id && $0.roomRegister == request.register })
.first!
@@ -574,7 +580,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
roomID,
.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
.filter({ $0.roomID == room.id && $0.roomRegister == form.register })
.first!
@@ -609,13 +615,14 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
catching: @escaping @Sendable () async throws -> Void = {}
) async -> AnySendableHTML {
@Dependency(\.database) var database
@Dependency(\.projectClient) var project
return await request.view {
await ResultView {
try await catching()
return (
try await database.projects.getCompletedSteps(projectID),
try await database.calculateDuctSizes(projectID: projectID)
try await project.calculateDuctSizes(projectID)
)
} onSuccess: { (steps, ducts) in
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_tag := "latest"
clean:
rm -rf .build
install-deps:
@curl -sL daisyui.com/fast | bash