4 Commits

16 changed files with 710 additions and 297 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

@@ -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,12 @@
import Fluent
extension PageRequest {
public static var first: Self {
.init(page: 1, per: 25)
}
public static func next<T>(_ currentPage: Page<T>) -> Self {
.init(page: currentPage.metadata.page + 1, per: currentPage.metadata.per)
}
}

View File

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

View File

@@ -0,0 +1,86 @@
import Dependencies
import DependenciesMacros
import ManualDClient
import ManualDCore
extension DependencyValues {
public var projectClient: ProjectClient {
get { self[ProjectClient.self] }
set { self[ProjectClient.self] = newValue }
}
}
/// Useful helper utilities for project's.
///
/// This is primarily used for implementing logic required to get the needed data
/// for the view controller client to render views.
@DependencyClient
public struct ProjectClient: Sendable {
public var calculateDuctSizes: @Sendable (Project.ID) async throws -> DuctSizeResponse
public var calculateRoomDuctSizes:
@Sendable (Project.ID) async throws -> [DuctSizing.RoomContainer]
public var calculateTrunkDuctSizes:
@Sendable (Project.ID) async throws -> [DuctSizing.TrunkContainer]
public var createProject:
@Sendable (User.ID, Project.Create) async throws -> CreateProjectResponse
public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse
}
extension ProjectClient: TestDependencyKey {
public static let testValue = Self()
}
extension ProjectClient {
public struct CreateProjectResponse: Codable, Equatable, Sendable {
public let projectID: Project.ID
public let rooms: [Room]
public let sensibleHeatRatio: Double?
public let completedSteps: Project.CompletedSteps
public init(
projectID: Project.ID,
rooms: [Room],
sensibleHeatRatio: Double? = nil,
completedSteps: Project.CompletedSteps
) {
self.projectID = projectID
self.rooms = rooms
self.sensibleHeatRatio = sensibleHeatRatio
self.completedSteps = completedSteps
}
}
public struct DuctSizeResponse: 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
}
}
public struct FrictionRateResponse: Codable, Equatable, Sendable {
public let componentLosses: [ComponentPressureLoss]
public let equivalentLengths: EffectiveLength.MaxContainer
public let frictionRate: ManualDClient.FrictionRateResponse?
public init(
componentLosses: [ComponentPressureLoss],
equivalentLengths: EffectiveLength.MaxContainer,
frictionRate: ManualDClient.FrictionRateResponse? = nil
) {
self.componentLosses = componentLosses
self.equivalentLengths = equivalentLengths
self.frictionRate = frictionRate
}
}
}

View File

@@ -0,0 +1,12 @@
import DatabaseClient
import ManualDCore
extension DatabaseClient.ComponentLoss {
func createDefaults(projectID: Project.ID) async throws {
let defaults = ComponentPressureLoss.Create.default(projectID: projectID)
for loss in defaults {
_ = try await create(loss)
}
}
}

View File

@@ -0,0 +1,114 @@
import DatabaseClient
import Dependencies
import ManualDClient
import ManualDCore
extension DatabaseClient {
func calculateDuctSizes(
projectID: Project.ID
) async throws -> ProjectClient.DuctSizeResponse {
@Dependency(\.manualD) var manualD
return try await manualD.calculateDuctSizes(
rooms: rooms.fetch(projectID),
trunks: trunkSizes.fetch(projectID),
sharedRequest: sharedDuctRequest(projectID)
)
}
func calculateRoomDuctSizes(
projectID: Project.ID
) async throws -> [DuctSizing.RoomContainer] {
@Dependency(\.manualD) var manualD
return try await manualD.calculateRoomSizes(
rooms: rooms.fetch(projectID),
sharedRequest: sharedDuctRequest(projectID)
)
}
func calculateTrunkDuctSizes(
projectID: Project.ID
) async throws -> [DuctSizing.TrunkContainer] {
@Dependency(\.manualD) var manualD
return try await manualD.calculateTrunkSizes(
rooms: rooms.fetch(projectID),
trunks: trunkSizes.fetch(projectID),
sharedRequest: sharedDuctRequest(projectID)
)
}
func sharedDuctRequest(_ projectID: Project.ID) async throws -> DuctSizeSharedRequest {
guard let dfrResponse = try await designFrictionRate(projectID: projectID) else {
throw ProjectClientError("Project not complete.")
}
let ensuredTEL = try dfrResponse.ensureMaxContainer()
return try await .init(
equipmentInfo: dfrResponse.equipmentInfo,
maxSupplyLength: ensuredTEL.supply,
maxReturnLenght: 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 ProjectClientError("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 ProjectClientError("Max supply TEL not found")
}
guard let maxReturnLength = telMaxContainer.return else {
throw ProjectClientError("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,189 @@
import Logging
import ManualDClient
import ManualDCore
struct DuctSizeSharedRequest {
let equipmentInfo: EquipmentInfo
let maxSupplyLength: EffectiveLength
let maxReturnLenght: EffectiveLength
let designFrictionRate: Double
let projectSHR: Double
}
// TODO: Remove Logger and use depedency logger.
extension ManualDClient {
func calculateDuctSizes(
rooms: [Room],
trunks: [DuctSizing.TrunkSize],
sharedRequest: DuctSizeSharedRequest,
logger: Logger? = nil
) async throws -> ProjectClient.DuctSizeResponse {
try await .init(
rooms: calculateRoomSizes(
rooms: rooms,
sharedRequest: sharedRequest
),
trunks: calculateTrunkSizes(
rooms: rooms,
trunks: trunks,
sharedRequest: sharedRequest
)
)
}
func calculateRoomSizes(
rooms: [Room],
sharedRequest: DuctSizeSharedRequest,
logger: Logger? = nil
) async throws -> [DuctSizing.RoomContainer] {
var retval: [DuctSizing.RoomContainer] = []
let totalHeatingLoad = rooms.totalHeatingLoad
let totalCoolingSensible = rooms.totalCoolingSensible(shr: sharedRequest.projectSHR)
for room in rooms {
let heatingLoad = room.heatingLoadPerRegister
let coolingLoad = room.coolingSensiblePerRegister(projectSHR: sharedRequest.projectSHR)
let heatingPercent = heatingLoad / totalHeatingLoad
let coolingPercent = coolingLoad / totalCoolingSensible
let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM)
let coolingCFM = coolingPercent * Double(sharedRequest.equipmentInfo.coolingCFM)
let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
let sizes = try await self.ductSize(
.init(designCFM: Int(designCFM.value), frictionRate: sharedRequest.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],
sharedRequest: DuctSizeSharedRequest,
logger: Logger? = nil
) async throws -> [DuctSizing.TrunkContainer] {
var retval = [DuctSizing.TrunkContainer]()
let totalHeatingLoad = rooms.totalHeatingLoad
let totalCoolingSensible = rooms.totalCoolingSensible(shr: sharedRequest.projectSHR)
for trunk in trunks {
let heatingLoad = trunk.totalHeatingLoad
let coolingLoad = trunk.totalCoolingSensible(projectSHR: sharedRequest.projectSHR)
let heatingPercent = heatingLoad / totalHeatingLoad
let coolingPercent = coolingLoad / totalCoolingSensible
let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM)
let coolingCFM = coolingPercent * Double(sharedRequest.equipmentInfo.coolingCFM)
let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
let sizes = try await self.ductSize(
.init(designCFM: Int(designCFM.value), frictionRate: sharedRequest.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,62 @@
import DatabaseClient
import Dependencies
import Logging
import ManualDClient
import ManualDCore
extension ProjectClient: DependencyKey {
public static var liveValue: Self {
@Dependency(\.database) var database
@Dependency(\.manualD) var manualD
return .init(
calculateDuctSizes: { projectID in
try await database.calculateDuctSizes(projectID: projectID)
},
calculateRoomDuctSizes: { projectID in
try await database.calculateRoomDuctSizes(projectID: projectID)
},
calculateTrunkDuctSizes: { projectID in
try await database.calculateTrunkDuctSizes(projectID: projectID)
},
createProject: { userID, request in
let project = try await database.projects.create(userID, request)
try await database.componentLoss.createDefaults(projectID: project.id)
return try await .init(
projectID: project.id,
rooms: database.rooms.fetch(project.id),
sensibleHeatRatio: database.projects.getSensibleHeatRatio(project.id),
completedSteps: database.projects.getCompletedSteps(project.id)
)
},
frictionRate: { projectID in
let componentLosses = try await database.componentLoss.fetch(projectID)
let lengths = try await database.effectiveLength.fetchMax(projectID)
let equipmentInfo = try await database.equipment.fetch(projectID)
guard let staticPressure = equipmentInfo?.staticPressure else {
return .init(componentLosses: componentLosses, equivalentLengths: lengths)
}
guard let totalEquivalentLength = lengths.total else {
return .init(componentLosses: componentLosses, equivalentLengths: lengths)
}
return try await .init(
componentLosses: componentLosses,
equivalentLengths: lengths,
frictionRate: manualD.frictionRate(
.init(
externalStaticPressure: staticPressure,
componentPressureLosses: database.componentLoss.fetch(projectID),
totalEffectiveLength: Int(totalEquivalentLength)
)
)
)
}
)
}
}

View File

@@ -1,78 +0,0 @@
import DatabaseClient
import Dependencies
import Fluent
import ManualDClient
import ManualDCore
import Vapor
// FIX: Remove these, not used currently.
extension DatabaseClient.Projects {
func fetchPage(
userID: User.ID,
page: Int = 1,
limit: Int = 25
) async throws -> Page<Project> {
try await fetch(userID, .init(page: page, per: limit))
}
func fetchPage(
userID: User.ID,
page: PageRequest
) async throws -> Page<Project> {
try await fetch(userID, page)
}
}
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 {
func createDefaults(projectID: Project.ID) async throws {
let defaults = ComponentPressureLoss.Create.default(projectID: projectID)
for loss in defaults {
_ = try await create(loss)
}
}
}
extension PageRequest {
static func next<T>(_ currentPage: Page<T>) -> Self {
.init(page: currentPage.metadata.page + 1, per: currentPage.metadata.per)
}
}

View File

@@ -1,38 +0,0 @@
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 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)
@@ -116,6 +118,7 @@ extension SiteRoute.View.ProjectRoute {
func renderView(on request: ViewController.Request) async -> AnySendableHTML { func renderView(on request: ViewController.Request) async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
@Dependency(\.projectClient) var projectClient
switch self { switch self {
case .index: case .index:
@@ -124,7 +127,7 @@ extension SiteRoute.View.ProjectRoute {
let user = try request.currentUser() let user = try request.currentUser()
return try await ( return try await (
user.id, user.id,
database.projects.fetchPage(userID: user.id) database.projects.fetch(user.id, .first)
) )
} onSuccess: { (userID, projects) in } onSuccess: { (userID, projects) in
@@ -146,19 +149,14 @@ extension SiteRoute.View.ProjectRoute {
return await request.view { return await request.view {
await ResultView { await ResultView {
let user = try request.currentUser() let user = try request.currentUser()
let project = try await database.projects.create(user.id, form) return try await projectClient.createProject(user.id, form)
try await database.componentLoss.createDefaults(projectID: project.id) } onSuccess: { response in
let rooms = try await database.rooms.fetch(project.id)
let shr = try await database.projects.getSensibleHeatRatio(project.id)
let completedSteps = try await database.projects.getCompletedSteps(project.id)
return (project.id, rooms, shr, completedSteps)
} onSuccess: { (projectID, rooms, shr, completedSteps) in
ProjectView( ProjectView(
projectID: projectID, projectID: response.projectID,
activeTab: .rooms, activeTab: .rooms,
completedSteps: completedSteps completedSteps: response.completedSteps
) { ) {
RoomsView(rooms: rooms, sensibleHeatRatio: shr) RoomsView(rooms: response.rooms, sensibleHeatRatio: response.sensibleHeatRatio)
} }
} }
} }
@@ -416,32 +414,21 @@ extension SiteRoute.View.ProjectRoute.ComponentLossRoute {
) async -> AnySendableHTML { ) async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
@Dependency(\.manualD) var manualD @Dependency(\.projectClient) var projectClient
return await request.view { return await request.view {
await ResultView { await ResultView {
try await catching() try await catching()
let equipment = try await database.equipment.fetch(projectID)
let componentLosses = try await database.componentLoss.fetch(projectID)
let lengths = try await database.effectiveLength.fetchMax(projectID)
return ( return (
try await database.projects.getCompletedSteps(projectID), try await database.projects.getCompletedSteps(projectID),
componentLosses, try await projectClient.frictionRate(projectID)
lengths,
try await manualD.frictionRate(
equipmentInfo: equipment,
componentLosses: componentLosses,
effectiveLength: lengths
) )
) } onSuccess: { (steps, response) in
} onSuccess: { (steps, losses, lengths, frictionRate) in
ProjectView(projectID: projectID, activeTab: .frictionRate, completedSteps: steps) { ProjectView(projectID: projectID, activeTab: .frictionRate, completedSteps: steps) {
FrictionRateView( FrictionRateView(
componentLosses: losses, componentLosses: response.componentLosses,
equivalentLengths: lengths, equivalentLengths: response.equivalentLengths,
frictionRateResponse: frictionRate frictionRateResponse: response.frictionRate
) )
} }
@@ -555,6 +542,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:
@@ -563,8 +551,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.calculateRoomDuctSizes(projectID)
.rooms
.filter({ $0.roomID == room.id && $0.roomRegister == request.register }) .filter({ $0.roomID == room.id && $0.roomRegister == request.register })
.first! .first!
} onSuccess: { room in } onSuccess: { room in
@@ -577,8 +564,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.calculateRoomDuctSizes(projectID)
.rooms
.filter({ $0.roomID == room.id && $0.roomRegister == form.register }) .filter({ $0.roomID == room.id && $0.roomRegister == form.register })
.first! .first!
} onSuccess: { room in } onSuccess: { room in
@@ -612,13 +598,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) {

View File

@@ -21,7 +21,7 @@ struct RoomsView: HTML, Sendable {
} }
div(.class("flex justify-end grow")) { div(.class("flex justify-end grow")) {
Tooltip("Project wide sensible heat ratio", position: .left) { Tooltip("Set sensible heat ratio", position: .left) {
button( button(
.class( .class(
""" """
@@ -43,6 +43,7 @@ struct RoomsView: HTML, Sendable {
} }
.attributes(.class("border border-error"), when: sensibleHeatRatio == nil) .attributes(.class("border border-error"), when: sensibleHeatRatio == nil)
} }
.attributes(.class("tooltip-open"), when: sensibleHeatRatio == nil)
} }
div(.class("flex items-end space-x-4 font-bold")) { div(.class("flex items-end space-x-4 font-bold")) {
@@ -67,7 +68,7 @@ struct RoomsView: HTML, Sendable {
SHRForm( SHRForm(
sensibleHeatRatio: sensibleHeatRatio, sensibleHeatRatio: sensibleHeatRatio,
dismiss: sensibleHeatRatio != nil dismiss: true
) )
table(.class("table table-zebra text-lg"), .id("roomsTable")) { table(.class("table table-zebra text-lg"), .id("roomsTable")) {

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