Compare commits
8 Commits
0.1.0
...
146baa7815
| Author | SHA1 | Date | |
|---|---|---|---|
|
146baa7815
|
|||
|
b5436c2073
|
|||
|
59c1c9ec4a
|
|||
|
65fc8565b6
|
|||
|
d14477e97a
|
|||
|
dbec7fb920
|
|||
|
6b8cb73434
|
|||
|
c6a29313aa
|
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ extension DatabaseClient.Migrations: DependencyKey {
|
||||
EquipmentInfo.Migrate(),
|
||||
Room.Migrate(),
|
||||
EffectiveLength.Migrate(),
|
||||
DuctSizing.TrunkSize.Migrate(),
|
||||
TrunkSize.Migrate(),
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10,12 +10,11 @@ extension DatabaseClient {
|
||||
public var create: @Sendable (Room.Create) async throws -> Room
|
||||
public var delete: @Sendable (Room.ID) async throws -> Void
|
||||
public var deleteRectangularSize:
|
||||
@Sendable (Room.ID, DuctSizing.RectangularDuct.ID) async throws -> Room
|
||||
@Sendable (Room.ID, Room.RectangularSize.ID) async throws -> Room
|
||||
public var get: @Sendable (Room.ID) async throws -> Room?
|
||||
public var fetch: @Sendable (Project.ID) async throws -> [Room]
|
||||
public var update: @Sendable (Room.ID, Room.Update) async throws -> Room
|
||||
public var updateRectangularSize:
|
||||
@Sendable (Room.ID, DuctSizing.RectangularDuct) async throws -> Room
|
||||
public var updateRectangularSize: @Sendable (Room.ID, Room.RectangularSize) async throws -> Room
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +203,7 @@ final class RoomModel: Model, @unchecked Sendable {
|
||||
var registerCount: Int
|
||||
|
||||
@Field(key: "rectangularSizes")
|
||||
var rectangularSizes: [DuctSizing.RectangularDuct]?
|
||||
var rectangularSizes: [Room.RectangularSize]?
|
||||
|
||||
@Timestamp(key: "createdAt", on: .create, format: .iso8601)
|
||||
var createdAt: Date?
|
||||
@@ -224,7 +223,7 @@ final class RoomModel: Model, @unchecked Sendable {
|
||||
coolingTotal: Double,
|
||||
coolingSensible: Double? = nil,
|
||||
registerCount: Int,
|
||||
rectangularSizes: [DuctSizing.RectangularDuct]? = nil,
|
||||
rectangularSizes: [Room.RectangularSize]? = nil,
|
||||
createdAt: Date? = nil,
|
||||
updatedAt: Date? = nil,
|
||||
projectID: Project.ID
|
||||
|
||||
@@ -7,13 +7,13 @@ import ManualDCore
|
||||
extension DatabaseClient {
|
||||
@DependencyClient
|
||||
public struct TrunkSizes: Sendable {
|
||||
public var create: @Sendable (DuctSizing.TrunkSize.Create) async throws -> DuctSizing.TrunkSize
|
||||
public var delete: @Sendable (DuctSizing.TrunkSize.ID) async throws -> Void
|
||||
public var fetch: @Sendable (Project.ID) async throws -> [DuctSizing.TrunkSize]
|
||||
public var get: @Sendable (DuctSizing.TrunkSize.ID) async throws -> DuctSizing.TrunkSize?
|
||||
public var create: @Sendable (TrunkSize.Create) async throws -> TrunkSize
|
||||
public var delete: @Sendable (TrunkSize.ID) async throws -> Void
|
||||
public var fetch: @Sendable (Project.ID) async throws -> [TrunkSize]
|
||||
public var get: @Sendable (TrunkSize.ID) async throws -> TrunkSize?
|
||||
public var update:
|
||||
@Sendable (DuctSizing.TrunkSize.ID, DuctSizing.TrunkSize.Update) async throws ->
|
||||
DuctSizing.TrunkSize
|
||||
@Sendable (TrunkSize.ID, TrunkSize.Update) async throws ->
|
||||
TrunkSize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
try request.validate()
|
||||
|
||||
let trunk = request.toModel()
|
||||
var roomProxies = [DuctSizing.TrunkSize.RoomProxy]()
|
||||
var roomProxies = [TrunkSize.RoomProxy]()
|
||||
|
||||
try await trunk.save(on: database)
|
||||
|
||||
@@ -90,7 +90,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize.Create {
|
||||
extension TrunkSize.Create {
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
guard rooms.count > 0 else {
|
||||
@@ -113,7 +113,7 @@ extension DuctSizing.TrunkSize.Create {
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize.Update {
|
||||
extension TrunkSize.Update {
|
||||
func validate() throws(ValidationError) {
|
||||
if let rooms {
|
||||
guard rooms.count > 0 else {
|
||||
@@ -128,7 +128,7 @@ extension DuctSizing.TrunkSize.Update {
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize {
|
||||
extension TrunkSize {
|
||||
|
||||
struct Migrate: AsyncMigration {
|
||||
let name = "CreateTrunkSize"
|
||||
@@ -192,7 +192,7 @@ final class TrunkRoomModel: Model, @unchecked Sendable {
|
||||
trunkID: TrunkModel.IDValue,
|
||||
roomID: RoomModel.IDValue,
|
||||
registers: [Int],
|
||||
type: DuctSizing.TrunkSize.TrunkType
|
||||
type: TrunkSize.TrunkType
|
||||
) {
|
||||
self.id = id
|
||||
$trunk.id = trunkID
|
||||
@@ -201,7 +201,7 @@ final class TrunkRoomModel: Model, @unchecked Sendable {
|
||||
self.type = type.rawValue
|
||||
}
|
||||
|
||||
func toDTO(on database: any Database) async throws -> DuctSizing.TrunkSize.RoomProxy {
|
||||
func toDTO(on database: any Database) async throws -> TrunkSize.RoomProxy {
|
||||
guard let room = try await RoomModel.find($room.id, on: database) else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
@@ -240,7 +240,7 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
init(
|
||||
id: UUID? = nil,
|
||||
projectID: Project.ID,
|
||||
type: DuctSizing.TrunkSize.TrunkType,
|
||||
type: TrunkSize.TrunkType,
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
@@ -251,15 +251,15 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
self.name = name
|
||||
}
|
||||
|
||||
func toDTO(on database: any Database) async throws -> DuctSizing.TrunkSize {
|
||||
let rooms = try await withThrowingTaskGroup(of: DuctSizing.TrunkSize.RoomProxy.self) { group in
|
||||
func toDTO(on database: any Database) async throws -> TrunkSize {
|
||||
let rooms = try await withThrowingTaskGroup(of: TrunkSize.RoomProxy.self) { group in
|
||||
for room in self.rooms {
|
||||
group.addTask {
|
||||
try await room.toDTO(on: database)
|
||||
}
|
||||
}
|
||||
|
||||
return try await group.reduce(into: [DuctSizing.TrunkSize.RoomProxy]()) {
|
||||
return try await group.reduce(into: [TrunkSize.RoomProxy]()) {
|
||||
$0.append($1)
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
}
|
||||
|
||||
func applyUpdates(
|
||||
_ updates: DuctSizing.TrunkSize.Update,
|
||||
_ updates: TrunkSize.Update,
|
||||
on database: any Database
|
||||
) async throws {
|
||||
if let type = updates.type, type.rawValue != self.type {
|
||||
@@ -340,15 +340,15 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
|
||||
extension Array where Element == TrunkModel {
|
||||
|
||||
func toDTO(on database: any Database) async throws -> [DuctSizing.TrunkSize] {
|
||||
try await withThrowingTaskGroup(of: DuctSizing.TrunkSize.self) { group in
|
||||
func toDTO(on database: any Database) async throws -> [TrunkSize] {
|
||||
try await withThrowingTaskGroup(of: TrunkSize.self) { group in
|
||||
for model in self {
|
||||
group.addTask {
|
||||
try await model.toDTO(on: database)
|
||||
}
|
||||
}
|
||||
|
||||
return try await group.reduce(into: [DuctSizing.TrunkSize]()) {
|
||||
return try await group.reduce(into: [TrunkSize]()) {
|
||||
$0.append($1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ extension Room {
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize.RoomProxy {
|
||||
extension 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
|
||||
@@ -35,7 +35,7 @@ extension DuctSizing.TrunkSize.RoomProxy {
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize {
|
||||
extension TrunkSize {
|
||||
|
||||
var totalHeatingLoad: Double {
|
||||
rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,26 +3,9 @@ import Foundation
|
||||
|
||||
public enum DuctSizing {
|
||||
|
||||
public struct RectangularDuct: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public let id: UUID
|
||||
public let register: Int?
|
||||
public let height: Int
|
||||
|
||||
public init(
|
||||
id: UUID = .init(),
|
||||
register: Int? = nil,
|
||||
height: Int,
|
||||
) {
|
||||
self.id = id
|
||||
self.register = register
|
||||
self.height = height
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct SizeContainer: Codable, Equatable, Sendable {
|
||||
|
||||
public let rectangularID: Room.RectangularSize.ID?
|
||||
public let designCFM: DesignCFM
|
||||
public let roundSize: Double
|
||||
public let finalSize: Int
|
||||
@@ -32,6 +15,7 @@ public enum DuctSizing {
|
||||
public let width: Int?
|
||||
|
||||
public init(
|
||||
rectangularID: Room.RectangularSize.ID? = nil,
|
||||
designCFM: DuctSizing.DesignCFM,
|
||||
roundSize: Double,
|
||||
finalSize: Int,
|
||||
@@ -40,6 +24,7 @@ public enum DuctSizing {
|
||||
height: Int? = nil,
|
||||
width: Int? = nil
|
||||
) {
|
||||
self.rectangularID = rectangularID
|
||||
self.designCFM = designCFM
|
||||
self.roundSize = roundSize
|
||||
self.finalSize = finalSize
|
||||
@@ -52,6 +37,7 @@ public enum DuctSizing {
|
||||
|
||||
// TODO: Uses SizeContainer
|
||||
|
||||
@dynamicMemberLookup
|
||||
public struct RoomContainer: Codable, Equatable, Sendable {
|
||||
|
||||
public let roomID: Room.ID
|
||||
@@ -61,13 +47,7 @@ public enum DuctSizing {
|
||||
public let coolingLoad: Double
|
||||
public let heatingCFM: Double
|
||||
public let coolingCFM: Double
|
||||
public let designCFM: DesignCFM
|
||||
public let roundSize: Double
|
||||
public let finalSize: Int
|
||||
public let velocity: Int
|
||||
public let flexSize: Int
|
||||
public let rectangularSize: RectangularDuct?
|
||||
public let rectangularWidth: Int?
|
||||
public let ductSize: SizeContainer
|
||||
|
||||
public init(
|
||||
roomID: Room.ID,
|
||||
@@ -77,13 +57,7 @@ public enum DuctSizing {
|
||||
coolingLoad: Double,
|
||||
heatingCFM: Double,
|
||||
coolingCFM: Double,
|
||||
designCFM: DesignCFM,
|
||||
roundSize: Double,
|
||||
finalSize: Int,
|
||||
velocity: Int,
|
||||
flexSize: Int,
|
||||
rectangularSize: RectangularDuct? = nil,
|
||||
rectangularWidth: Int? = nil
|
||||
ductSize: SizeContainer
|
||||
) {
|
||||
self.roomID = roomID
|
||||
self.roomName = roomName
|
||||
@@ -92,13 +66,11 @@ public enum DuctSizing {
|
||||
self.coolingLoad = coolingLoad
|
||||
self.heatingCFM = heatingCFM
|
||||
self.coolingCFM = coolingCFM
|
||||
self.designCFM = designCFM
|
||||
self.roundSize = roundSize
|
||||
self.finalSize = finalSize
|
||||
self.velocity = velocity
|
||||
self.flexSize = flexSize
|
||||
self.rectangularSize = rectangularSize
|
||||
self.rectangularWidth = rectangularWidth
|
||||
self.ductSize = ductSize
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember keyPath: KeyPath<DuctSizing.SizeContainer, T>) -> T {
|
||||
ductSize[keyPath: keyPath]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +114,7 @@ extension DuctSizing {
|
||||
self.ductSize = ductSize
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember keyPath: KeyPath<DuctSizing.TrunkSize, T>) -> T {
|
||||
public subscript<T>(dynamicMember keyPath: KeyPath<TrunkSize, T>) -> T {
|
||||
trunk[keyPath: keyPath]
|
||||
}
|
||||
|
||||
@@ -150,99 +122,4 @@ extension DuctSizing {
|
||||
ductSize[keyPath: keyPath]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add an optional label that the user can set.
|
||||
|
||||
// Represents the database model.
|
||||
public struct TrunkSize: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public let id: UUID
|
||||
public let projectID: Project.ID
|
||||
public let type: TrunkType
|
||||
public let rooms: [RoomProxy]
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
id: UUID,
|
||||
projectID: Project.ID,
|
||||
type: DuctSizing.TrunkSize.TrunkType,
|
||||
rooms: [DuctSizing.TrunkSize.RoomProxy],
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.projectID = projectID
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize {
|
||||
public struct Create: Codable, Equatable, Sendable {
|
||||
|
||||
public let projectID: Project.ID
|
||||
public let type: TrunkType
|
||||
public let rooms: [Room.ID: [Int]]
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
projectID: Project.ID,
|
||||
type: DuctSizing.TrunkSize.TrunkType,
|
||||
rooms: [Room.ID: [Int]],
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.projectID = projectID
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public struct Update: Codable, Equatable, Sendable {
|
||||
|
||||
public let type: TrunkType?
|
||||
public let rooms: [Room.ID: [Int]]?
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
type: DuctSizing.TrunkSize.TrunkType? = nil,
|
||||
rooms: [Room.ID: [Int]]? = nil,
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make registers non-optional
|
||||
public struct RoomProxy: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public var id: Room.ID { room.id }
|
||||
public let room: Room
|
||||
public let registers: [Int]
|
||||
|
||||
public init(room: Room, registers: [Int]) {
|
||||
self.room = room
|
||||
self.registers = registers
|
||||
}
|
||||
}
|
||||
|
||||
public enum TrunkType: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
case `return`
|
||||
case supply
|
||||
|
||||
public static let allCases = [Self.supply, .return]
|
||||
}
|
||||
}
|
||||
|
||||
12
Sources/ManualDCore/PageRequest+extensions.swift
Normal file
12
Sources/ManualDCore/PageRequest+extensions.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
|
||||
public let coolingTotal: Double
|
||||
public let coolingSensible: Double?
|
||||
public let registerCount: Int
|
||||
public let rectangularSizes: [DuctSizing.RectangularDuct]?
|
||||
public let rectangularSizes: [RectangularSize]?
|
||||
public let createdAt: Date
|
||||
public let updatedAt: Date
|
||||
|
||||
@@ -21,7 +21,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
|
||||
coolingTotal: Double,
|
||||
coolingSensible: Double? = nil,
|
||||
registerCount: Int = 1,
|
||||
rectangularSizes: [DuctSizing.RectangularDuct]? = nil,
|
||||
rectangularSizes: [RectangularSize]? = nil,
|
||||
createdAt: Date,
|
||||
updatedAt: Date
|
||||
) {
|
||||
@@ -65,13 +65,30 @@ extension Room {
|
||||
}
|
||||
}
|
||||
|
||||
public struct RectangularSize: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public let id: UUID
|
||||
public let register: Int?
|
||||
public let height: Int
|
||||
|
||||
public init(
|
||||
id: UUID = .init(),
|
||||
register: Int? = nil,
|
||||
height: Int,
|
||||
) {
|
||||
self.id = id
|
||||
self.register = register
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
|
||||
public struct Update: Codable, Equatable, Sendable {
|
||||
public let name: String?
|
||||
public let heatingLoad: Double?
|
||||
public let coolingTotal: Double?
|
||||
public let coolingSensible: Double?
|
||||
public let registerCount: Int?
|
||||
public let rectangularSizes: [DuctSizing.RectangularDuct]?
|
||||
public let rectangularSizes: [RectangularSize]?
|
||||
|
||||
public init(
|
||||
name: String? = nil,
|
||||
@@ -89,7 +106,7 @@ extension Room {
|
||||
}
|
||||
|
||||
public init(
|
||||
rectangularSizes: [DuctSizing.RectangularDuct]
|
||||
rectangularSizes: [RectangularSize]
|
||||
) {
|
||||
self.name = nil
|
||||
self.heatingLoad = nil
|
||||
|
||||
@@ -627,7 +627,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
}
|
||||
Method.delete
|
||||
Query {
|
||||
Field("rectangularSize") { DuctSizing.RectangularDuct.ID.parser() }
|
||||
Field("rectangularSize") { Room.RectangularSize.ID.parser() }
|
||||
Field("register") { Int.parser() }
|
||||
}
|
||||
.map(.memberwise(DeleteRectangularDuct.init))
|
||||
@@ -642,7 +642,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Body {
|
||||
FormData {
|
||||
Optionally {
|
||||
Field("id") { DuctSizing.RectangularDuct.ID.parser() }
|
||||
Field("id") { Room.RectangularSize.ID.parser() }
|
||||
}
|
||||
Field("register") { Int.parser() }
|
||||
Field("height") { Int.parser() }
|
||||
@@ -658,19 +658,19 @@ extension SiteRoute.View.ProjectRoute {
|
||||
|
||||
public struct DeleteRectangularDuct: Equatable, Sendable {
|
||||
|
||||
public let rectangularSizeID: DuctSizing.RectangularDuct.ID
|
||||
public let rectangularSizeID: Room.RectangularSize.ID
|
||||
public let register: Int
|
||||
|
||||
public init(rectangularSizeID: DuctSizing.RectangularDuct.ID, register: Int) {
|
||||
public init(rectangularSizeID: Room.RectangularSize.ID, register: Int) {
|
||||
self.rectangularSizeID = rectangularSizeID
|
||||
self.register = register
|
||||
}
|
||||
}
|
||||
|
||||
public enum TrunkRoute: Equatable, Sendable {
|
||||
case delete(DuctSizing.TrunkSize.ID)
|
||||
case delete(TrunkSize.ID)
|
||||
case submit(TrunkSizeForm)
|
||||
case update(DuctSizing.TrunkSize.ID, TrunkSizeForm)
|
||||
case update(TrunkSize.ID, TrunkSizeForm)
|
||||
|
||||
public static let rootPath = "trunk"
|
||||
|
||||
@@ -678,7 +678,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Route(.case(Self.delete)) {
|
||||
Path {
|
||||
rootPath
|
||||
DuctSizing.TrunkSize.ID.parser()
|
||||
TrunkSize.ID.parser()
|
||||
}
|
||||
Method.delete
|
||||
}
|
||||
@@ -690,7 +690,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Body {
|
||||
FormData {
|
||||
Field("projectID") { Project.ID.parser() }
|
||||
Field("type") { DuctSizing.TrunkSize.TrunkType.parser() }
|
||||
Field("type") { TrunkSize.TrunkType.parser() }
|
||||
Optionally {
|
||||
Field("height") { Int.parser() }
|
||||
|
||||
@@ -708,13 +708,13 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Route(.case(Self.update)) {
|
||||
Path {
|
||||
rootPath
|
||||
DuctSizing.TrunkSize.ID.parser()
|
||||
TrunkSize.ID.parser()
|
||||
}
|
||||
Method.patch
|
||||
Body {
|
||||
FormData {
|
||||
Field("projectID") { Project.ID.parser() }
|
||||
Field("type") { DuctSizing.TrunkSize.TrunkType.parser() }
|
||||
Field("type") { TrunkSize.TrunkType.parser() }
|
||||
Optionally {
|
||||
Field("height") { Int.parser() }
|
||||
}
|
||||
@@ -732,17 +732,43 @@ extension SiteRoute.View.ProjectRoute {
|
||||
}
|
||||
|
||||
public struct RoomRectangularForm: Equatable, Sendable {
|
||||
public let id: DuctSizing.RectangularDuct.ID?
|
||||
|
||||
public let id: Room.RectangularSize.ID?
|
||||
public let register: Int
|
||||
public let height: Int
|
||||
|
||||
public init(
|
||||
id: Room.RectangularSize.ID? = nil,
|
||||
register: Int,
|
||||
height: Int
|
||||
) {
|
||||
self.id = id
|
||||
self.register = register
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
|
||||
public struct TrunkSizeForm: Equatable, Sendable {
|
||||
|
||||
public let projectID: Project.ID
|
||||
public let type: DuctSizing.TrunkSize.TrunkType
|
||||
public let type: TrunkSize.TrunkType
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
public let rooms: [String]
|
||||
|
||||
public init(
|
||||
projectID: Project.ID,
|
||||
type: TrunkSize.TrunkType,
|
||||
height: Int? = nil,
|
||||
name: String? = nil,
|
||||
rooms: [String]
|
||||
) {
|
||||
self.projectID = projectID
|
||||
self.type = type
|
||||
self.height = height
|
||||
self.name = name
|
||||
self.rooms = rooms
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
93
Sources/ManualDCore/TrunkSize.swift
Normal file
93
Sources/ManualDCore/TrunkSize.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
import Foundation
|
||||
|
||||
// Represents the database model.
|
||||
public struct TrunkSize: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public let id: UUID
|
||||
public let projectID: Project.ID
|
||||
public let type: TrunkType
|
||||
public let rooms: [RoomProxy]
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
id: UUID,
|
||||
projectID: Project.ID,
|
||||
type: TrunkType,
|
||||
rooms: [RoomProxy],
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.projectID = projectID
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
extension TrunkSize {
|
||||
public struct Create: Codable, Equatable, Sendable {
|
||||
|
||||
public let projectID: Project.ID
|
||||
public let type: TrunkType
|
||||
public let rooms: [Room.ID: [Int]]
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
projectID: Project.ID,
|
||||
type: TrunkType,
|
||||
rooms: [Room.ID: [Int]],
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.projectID = projectID
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public struct Update: Codable, Equatable, Sendable {
|
||||
|
||||
public let type: TrunkType?
|
||||
public let rooms: [Room.ID: [Int]]?
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
type: TrunkType? = nil,
|
||||
rooms: [Room.ID: [Int]]? = nil,
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make registers non-optional
|
||||
public struct RoomProxy: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public var id: Room.ID { room.id }
|
||||
public let room: Room
|
||||
public let registers: [Int]
|
||||
|
||||
public init(room: Room, registers: [Int]) {
|
||||
self.room = room
|
||||
self.registers = registers
|
||||
}
|
||||
}
|
||||
|
||||
public enum TrunkType: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
case `return`
|
||||
case supply
|
||||
|
||||
public static let allCases = [Self.supply, .return]
|
||||
}
|
||||
}
|
||||
9
Sources/ProjectClient/DuctCalcClientError.swift
Normal file
9
Sources/ProjectClient/DuctCalcClientError.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
public struct ProjectClientError: Error {
|
||||
public let reason: String
|
||||
|
||||
public init(_ reason: String) {
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
86
Sources/ProjectClient/Interface.swift
Normal file
86
Sources/ProjectClient/Interface.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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: [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,
|
||||
ductSize: .init(
|
||||
designCFM: designCFM,
|
||||
sizes: sizes,
|
||||
rectangularSize: rectangularSize,
|
||||
width: rectangularWidth
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
func calculateTrunkSizes(
|
||||
rooms: [Room],
|
||||
trunks: [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,
|
||||
sizes: sizes,
|
||||
height: trunk.height,
|
||||
width: width
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DuctSizing.SizeContainer {
|
||||
init(
|
||||
designCFM: DuctSizing.DesignCFM,
|
||||
sizes: ManualDClient.DuctSizeResponse,
|
||||
height: Int?,
|
||||
width: Int?
|
||||
) {
|
||||
self.init(
|
||||
rectangularID: nil,
|
||||
designCFM: designCFM,
|
||||
roundSize: sizes.ductulatorSize,
|
||||
finalSize: sizes.finalSize,
|
||||
velocity: sizes.velocity,
|
||||
flexSize: sizes.flexSize,
|
||||
height: height,
|
||||
width: width
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
designCFM: DuctSizing.DesignCFM,
|
||||
sizes: ManualDClient.DuctSizeResponse,
|
||||
rectangularSize: Room.RectangularSize?,
|
||||
width: Int?
|
||||
) {
|
||||
self.init(
|
||||
rectangularID: rectangularSize?.id,
|
||||
designCFM: designCFM,
|
||||
roundSize: sizes.ductulatorSize,
|
||||
finalSize: sizes.finalSize,
|
||||
velocity: sizes.velocity,
|
||||
flexSize: sizes.flexSize,
|
||||
height: rectangularSize?.height,
|
||||
width: width
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Room {
|
||||
|
||||
var heatingLoadPerRegister: Double {
|
||||
|
||||
heatingLoad / Double(registerCount)
|
||||
}
|
||||
|
||||
func coolingSensiblePerRegister(projectSHR: Double) -> Double {
|
||||
let sensible = coolingSensible ?? (coolingTotal * projectSHR)
|
||||
return sensible / Double(registerCount)
|
||||
}
|
||||
}
|
||||
|
||||
extension 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 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) }
|
||||
}
|
||||
}
|
||||
62
Sources/ProjectClient/Live.swift
Normal file
62
Sources/ProjectClient/Live.swift
Normal 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)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import ManualDCore
|
||||
|
||||
extension SiteRoute.View.ProjectRoute.DuctSizingRoute.TrunkSizeForm {
|
||||
|
||||
func toCreate(logger: Logger? = nil) throws -> DuctSizing.TrunkSize.Create {
|
||||
func toCreate(logger: Logger? = nil) throws -> TrunkSize.Create {
|
||||
try .init(
|
||||
projectID: projectID,
|
||||
type: type,
|
||||
@@ -14,7 +14,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute.TrunkSizeForm {
|
||||
)
|
||||
}
|
||||
|
||||
func toUpdate(logger: Logger? = nil) throws -> DuctSizing.TrunkSize.Update {
|
||||
func toUpdate(logger: Logger? = nil) throws -> TrunkSize.Update {
|
||||
try .init(
|
||||
type: type,
|
||||
rooms: makeRooms(logger: logger),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +118,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
|
||||
func renderView(on request: ViewController.Request) async -> AnySendableHTML {
|
||||
@Dependency(\.database) var database
|
||||
@Dependency(\.projectClient) var projectClient
|
||||
|
||||
switch self {
|
||||
case .index:
|
||||
@@ -121,7 +127,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
let user = try request.currentUser()
|
||||
return try await (
|
||||
user.id,
|
||||
database.projects.fetchPage(userID: user.id)
|
||||
database.projects.fetch(user.id, .first)
|
||||
)
|
||||
|
||||
} onSuccess: { (userID, projects) in
|
||||
@@ -143,19 +149,14 @@ extension SiteRoute.View.ProjectRoute {
|
||||
return await request.view {
|
||||
await ResultView {
|
||||
let user = try request.currentUser()
|
||||
let project = try await database.projects.create(user.id, form)
|
||||
try await database.componentLoss.createDefaults(projectID: project.id)
|
||||
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
|
||||
return try await projectClient.createProject(user.id, form)
|
||||
} onSuccess: { response in
|
||||
ProjectView(
|
||||
projectID: projectID,
|
||||
projectID: response.projectID,
|
||||
activeTab: .rooms,
|
||||
completedSteps: completedSteps
|
||||
completedSteps: response.completedSteps
|
||||
) {
|
||||
RoomsView(rooms: rooms, sensibleHeatRatio: shr)
|
||||
RoomsView(rooms: response.rooms, sensibleHeatRatio: response.sensibleHeatRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,32 +414,21 @@ extension SiteRoute.View.ProjectRoute.ComponentLossRoute {
|
||||
) async -> AnySendableHTML {
|
||||
|
||||
@Dependency(\.database) var database
|
||||
@Dependency(\.manualD) var manualD
|
||||
@Dependency(\.projectClient) var projectClient
|
||||
|
||||
return await request.view {
|
||||
await ResultView {
|
||||
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 (
|
||||
try await database.projects.getCompletedSteps(projectID),
|
||||
componentLosses,
|
||||
lengths,
|
||||
try await manualD.frictionRate(
|
||||
equipmentInfo: equipment,
|
||||
componentLosses: componentLosses,
|
||||
effectiveLength: lengths
|
||||
)
|
||||
try await projectClient.frictionRate(projectID)
|
||||
)
|
||||
} onSuccess: { (steps, losses, lengths, frictionRate) in
|
||||
} onSuccess: { (steps, response) in
|
||||
ProjectView(projectID: projectID, activeTab: .frictionRate, completedSteps: steps) {
|
||||
FrictionRateView(
|
||||
componentLosses: losses,
|
||||
equivalentLengths: lengths,
|
||||
frictionRateResponse: frictionRate
|
||||
componentLosses: response.componentLosses,
|
||||
equivalentLengths: response.equivalentLengths,
|
||||
frictionRateResponse: response.frictionRate
|
||||
)
|
||||
}
|
||||
|
||||
@@ -552,6 +542,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,8 +551,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)
|
||||
.rooms
|
||||
return try await projectClient.calculateRoomDuctSizes(projectID)
|
||||
.filter({ $0.roomID == room.id && $0.roomRegister == request.register })
|
||||
.first!
|
||||
} onSuccess: { room in
|
||||
@@ -574,8 +564,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)
|
||||
.rooms
|
||||
return try await projectClient.calculateRoomDuctSizes(projectID)
|
||||
.filter({ $0.roomID == room.id && $0.roomRegister == form.register })
|
||||
.first!
|
||||
} onSuccess: { room in
|
||||
@@ -609,13 +598,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) {
|
||||
|
||||
@@ -40,7 +40,7 @@ struct RectangularSizeForm: HTML, Sendable {
|
||||
}
|
||||
|
||||
var height: Int? {
|
||||
room.rectangularSize?.height
|
||||
room.ductSize.height
|
||||
}
|
||||
|
||||
var body: some HTML<HTMLTag.dialog> {
|
||||
@@ -54,7 +54,7 @@ struct RectangularSizeForm: HTML, Sendable {
|
||||
.hx.swap(.outerHTML)
|
||||
) {
|
||||
input(.class("hidden"), .name("register"), .value(room.roomRegister))
|
||||
input(.class("hidden"), .name("id"), .value(room.rectangularSize?.id))
|
||||
input(.class("hidden"), .name("id"), .value(room.ductSize.rectangularID))
|
||||
|
||||
LabeledInput(
|
||||
"Height",
|
||||
|
||||
@@ -44,7 +44,7 @@ extension DuctSizingView {
|
||||
let formID = UUID().idString
|
||||
|
||||
var deleteRoute: String {
|
||||
guard let id = room.rectangularSize?.id else { return "" }
|
||||
guard let id = room.rectangularID else { return "" }
|
||||
|
||||
return SiteRoute.View.router.path(
|
||||
for: .project(
|
||||
@@ -80,7 +80,7 @@ extension DuctSizingView {
|
||||
|
||||
span(.class("label")) { "Design" }
|
||||
div(.class("flex justify-center")) {
|
||||
Badge(number: room.designCFM.value, digits: 0)
|
||||
Badge(number: room.ductSize.designCFM.value, digits: 0)
|
||||
}
|
||||
|
||||
span(.class("label")) { "Heating" }
|
||||
@@ -103,28 +103,28 @@ extension DuctSizingView {
|
||||
|
||||
div(.class("label")) { "Calculated" }
|
||||
div(.class("flex justify-center")) {
|
||||
Badge(number: room.roundSize, digits: 2)
|
||||
Badge(number: room.ductSize.roundSize, digits: 2)
|
||||
}
|
||||
div {}
|
||||
|
||||
div(.class("label")) { "Final" }
|
||||
div(.class("flex justify-center")) {
|
||||
Badge(number: room.finalSize)
|
||||
Badge(number: room.ductSize.finalSize)
|
||||
.attributes(.class("badge-secondary"))
|
||||
}
|
||||
div {}
|
||||
|
||||
div(.class("label")) { "Flex" }
|
||||
div(.class("flex justify-center")) {
|
||||
Badge(number: room.flexSize)
|
||||
Badge(number: room.ductSize.flexSize)
|
||||
.attributes(.class("badge-primary"))
|
||||
}
|
||||
div {}
|
||||
|
||||
div(.class("label")) { "Rectangular" }
|
||||
div(.class("flex justify-center")) {
|
||||
if let width = room.rectangularWidth,
|
||||
let height = room.rectangularSize?.height
|
||||
if let width = room.ductSize.width,
|
||||
let height = room.ductSize.height
|
||||
{
|
||||
Badge {
|
||||
span { "\(width) x \(height)" }
|
||||
@@ -134,7 +134,7 @@ extension DuctSizingView {
|
||||
}
|
||||
div(.class("flex justify-end")) {
|
||||
div(.class("join")) {
|
||||
if room.rectangularSize != nil {
|
||||
if room.ductSize.width != nil {
|
||||
Tooltip("Delete Size", position: .bottom) {
|
||||
TrashButton()
|
||||
.attributes(.class("join-item btn-ghost"))
|
||||
@@ -142,7 +142,7 @@ extension DuctSizingView {
|
||||
.hx.delete(deleteRoute),
|
||||
.hx.target("#\(rowID)"),
|
||||
.hx.swap(.outerHTML),
|
||||
when: room.rectangularSize != nil
|
||||
when: room.ductSize.width != nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ struct TrunkSizeForm: HTML, Sendable {
|
||||
let rooms: [DuctSizing.RoomContainer]
|
||||
let dismiss: Bool
|
||||
|
||||
var trunk: DuctSizing.TrunkSize? {
|
||||
var trunk: TrunkSize? {
|
||||
container?.trunk
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ struct TrunkSizeForm: HTML, Sendable {
|
||||
label(.class("select w-full")) {
|
||||
span(.class("label")) { "Type" }
|
||||
select(.name("type")) {
|
||||
for type in DuctSizing.TrunkSize.TrunkType.allCases {
|
||||
for type in TrunkSize.TrunkType.allCases {
|
||||
option(.value(type.rawValue)) { type.rawValue.capitalized }
|
||||
.attributes(.selected, when: trunk?.type == type)
|
||||
}
|
||||
@@ -121,7 +121,7 @@ struct TrunkSizeForm: HTML, Sendable {
|
||||
|
||||
}
|
||||
|
||||
extension Array where Element == DuctSizing.TrunkSize.RoomProxy {
|
||||
extension Array where Element == TrunkSize.RoomProxy {
|
||||
func hasRoom(_ room: DuctSizing.RoomContainer) -> Bool {
|
||||
first {
|
||||
$0.id == room.roomID
|
||||
|
||||
@@ -21,7 +21,7 @@ struct RoomsView: HTML, Sendable {
|
||||
}
|
||||
|
||||
div(.class("flex justify-end grow")) {
|
||||
Tooltip("Project wide sensible heat ratio", position: .left) {
|
||||
Tooltip("Set sensible heat ratio", position: .left) {
|
||||
button(
|
||||
.class(
|
||||
"""
|
||||
@@ -43,6 +43,7 @@ struct RoomsView: HTML, Sendable {
|
||||
}
|
||||
.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")) {
|
||||
@@ -67,7 +68,7 @@ struct RoomsView: HTML, Sendable {
|
||||
|
||||
SHRForm(
|
||||
sensibleHeatRatio: sensibleHeatRatio,
|
||||
dismiss: sensibleHeatRatio != nil
|
||||
dismiss: true
|
||||
)
|
||||
|
||||
table(.class("table table-zebra text-lg"), .id("roomsTable")) {
|
||||
|
||||
9
TODO.md
Normal file
9
TODO.md
Normal 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
|
||||
Reference in New Issue
Block a user