WIP: Inital duct rectangular form, needs better thought out.

This commit is contained in:
2026-01-09 17:03:00 -05:00
parent 7083178844
commit 07818d24ed
13 changed files with 347 additions and 7 deletions

View File

@@ -18,6 +18,7 @@ public struct DatabaseClient: Sendable {
public var equipment: Equipment public var equipment: Equipment
public var componentLoss: ComponentLoss public var componentLoss: ComponentLoss
public var effectiveLength: EffectiveLengthClient public var effectiveLength: EffectiveLengthClient
// public var rectangularDuct: RectangularDuct
public var users: Users public var users: Users
} }
@@ -29,6 +30,7 @@ extension DatabaseClient: TestDependencyKey {
equipment: .testValue, equipment: .testValue,
componentLoss: .testValue, componentLoss: .testValue,
effectiveLength: .testValue, effectiveLength: .testValue,
// rectangularDuct: .testValue,
users: .testValue users: .testValue
) )
@@ -40,6 +42,7 @@ extension DatabaseClient: TestDependencyKey {
equipment: .live(database: database), equipment: .live(database: database),
componentLoss: .live(database: database), componentLoss: .live(database: database),
effectiveLength: .live(database: database), effectiveLength: .live(database: database),
// rectangularDuct: .live(database: database),
users: .live(database: database) users: .live(database: database)
) )
} }
@@ -67,6 +70,7 @@ extension DatabaseClient.Migrations: DependencyKey {
EquipmentInfo.Migrate(), EquipmentInfo.Migrate(),
Room.Migrate(), Room.Migrate(),
EffectiveLength.Migrate(), EffectiveLength.Migrate(),
// DuctSizing.RectangularDuct.Migrate(),
] ]
} }
) )

View File

@@ -0,0 +1,175 @@
// import Dependencies
// import DependenciesMacros
// import Fluent
// import Foundation
// import ManualDCore
//
// extension DatabaseClient {
// @DependencyClient
// public struct RectangularDuct: Sendable {
// public var create:
// @Sendable (DuctSizing.RectangularDuct.Create) async throws -> DuctSizing.RectangularDuct
// public var delete: @Sendable (DuctSizing.RectangularDuct.ID) async throws -> Void
// public var fetch: @Sendable (Room.ID) async throws -> [DuctSizing.RectangularDuct]
// public var get:
// @Sendable (DuctSizing.RectangularDuct.ID) async throws -> DuctSizing.RectangularDuct?
// public var update:
// @Sendable (DuctSizing.RectangularDuct.ID, DuctSizing.RectangularDuct.Update) async throws ->
// DuctSizing.RectangularDuct
// }
// }
//
// extension DatabaseClient.RectangularDuct: TestDependencyKey {
// public static let testValue = Self()
//
// public static func live(database: any Database) -> Self {
// .init(
// create: { request in
// try request.validate()
// let model = request.toModel()
// try await model.save(on: database)
// return try model.toDTO()
// },
// delete: { id in
// guard let model = try await RectangularDuctModel.find(id, on: database) else {
// throw NotFoundError()
// }
// try await model.delete(on: database)
// },
// fetch: { roomID in
// try await RectangularDuctModel.query(on: database)
// .with(\.$room)
// .filter(\.$room.$id == roomID)
// .all()
// .map { try $0.toDTO() }
// },
// get: { id in
// try await RectangularDuctModel.find(id, on: database)
// .map { try $0.toDTO() }
// },
// update: { id, updates in
// guard let model = try await RectangularDuctModel.find(id, on: database) else {
// throw NotFoundError()
// }
// try updates.validate()
// model.applyUpdates(updates)
// if model.hasChanges {
// try await model.save(on: database)
// }
// return try model.toDTO()
// }
// )
// }
// }
//
// extension DuctSizing.RectangularDuct.Create {
//
// func validate() throws(ValidationError) {
// guard height > 0 else {
// throw ValidationError("Rectangular duct size height should be greater than 0.")
// }
// if let register {
// guard register > 0 else {
// throw ValidationError("Rectangular duct size register should be greater than 0.")
// }
// }
// }
//
// func toModel() -> RectangularDuctModel {
// .init(roomID: roomID, height: height)
// }
// }
//
// extension DuctSizing.RectangularDuct.Update {
//
// func validate() throws(ValidationError) {
// if let height {
// guard height > 0 else {
// throw ValidationError("Rectangular duct size height should be greater than 0.")
// }
// }
// if let register {
// guard register > 0 else {
// throw ValidationError("Rectangular duct size register should be greater than 0.")
// }
// }
// }
// }
//
// extension DuctSizing.RectangularDuct {
// struct Migrate: AsyncMigration {
// let name = "CreateRectangularDuct"
//
// func prepare(on database: any Database) async throws {
// try await database.schema(RectangularDuctModel.schema)
// .id()
// .field("register", .int8)
// .field("height", .int8, .required)
// .field("roomID", .uuid, .required, .references(RoomModel.schema, "id", onDelete: .cascade))
// .field("createdAt", .datetime)
// .field("updatedAt", .datetime)
// .create()
// }
//
// func revert(on database: any Database) async throws {
// try await database.schema(RectangularDuctModel.schema).delete()
// }
// }
// }
//
// final class RectangularDuctModel: Model, @unchecked Sendable {
//
// static let schema = "rectangularDuct"
//
// @ID(key: .id)
// var id: UUID?
//
// @Parent(key: "roomID")
// var room: RoomModel
//
// @Field(key: "height")
// var height: Int
//
// @Field(key: "register")
// var register: Int?
//
// @Timestamp(key: "createdAt", on: .create, format: .iso8601)
// var createdAt: Date?
//
// @Timestamp(key: "updatedAt", on: .update, format: .iso8601)
// var updatedAt: Date?
//
// init() {}
//
// init(
// id: UUID? = nil,
// roomID: Room.ID,
// register: Int? = nil,
// height: Int
// ) {
// self.id = id
// $room.id = roomID
// self.register = register
// self.height = height
// }
//
// func toDTO() throws -> DuctSizing.RectangularDuct {
// return try .init(
// id: requireID(),
// roomID: $room.id,
// register: register,
// height: height,
// createdAt: createdAt!,
// updatedAt: updatedAt!
// )
// }
//
// func applyUpdates(_ updates: DuctSizing.RectangularDuct.Update) {
// if let height = updates.height, height != self.height {
// self.height = height
// }
// if let register = updates.register, register != self.register {
// self.register = register
// }
// }
// }

View File

@@ -38,6 +38,7 @@ extension DatabaseClient.Rooms: TestDependencyKey {
try await RoomModel.query(on: database) try await RoomModel.query(on: database)
.with(\.$project) .with(\.$project)
.filter(\.$project.$id, .equal, projectID) .filter(\.$project.$id, .equal, projectID)
.sort(\.$name, .ascending)
.all() .all()
.map { try $0.toDTO() } .map { try $0.toDTO() }
}, },
@@ -135,6 +136,7 @@ extension Room {
.field("coolingTotal", .double, .required) .field("coolingTotal", .double, .required)
.field("coolingSensible", .double) .field("coolingSensible", .double)
.field("registerCount", .int8, .required) .field("registerCount", .int8, .required)
.field("rectangularSizes", .array)
.field("createdAt", .datetime) .field("createdAt", .datetime)
.field("updatedAt", .datetime) .field("updatedAt", .datetime)
.field( .field(
@@ -172,6 +174,9 @@ final class RoomModel: Model, @unchecked Sendable {
@Field(key: "registerCount") @Field(key: "registerCount")
var registerCount: Int var registerCount: Int
@Field(key: "rectangularSizes")
var rectangularSizes: [DuctSizing.RectangularDuct]?
@Timestamp(key: "createdAt", on: .create, format: .iso8601) @Timestamp(key: "createdAt", on: .create, format: .iso8601)
var createdAt: Date? var createdAt: Date?
@@ -190,6 +195,7 @@ final class RoomModel: Model, @unchecked Sendable {
coolingTotal: Double, coolingTotal: Double,
coolingSensible: Double? = nil, coolingSensible: Double? = nil,
registerCount: Int, registerCount: Int,
rectangularSizes: [DuctSizing.RectangularDuct]? = nil,
createdAt: Date? = nil, createdAt: Date? = nil,
updatedAt: Date? = nil, updatedAt: Date? = nil,
projectID: Project.ID projectID: Project.ID
@@ -200,6 +206,7 @@ final class RoomModel: Model, @unchecked Sendable {
self.coolingTotal = coolingTotal self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible self.coolingSensible = coolingSensible
self.registerCount = registerCount self.registerCount = registerCount
self.rectangularSizes = rectangularSizes
self.createdAt = createdAt self.createdAt = createdAt
self.updatedAt = updatedAt self.updatedAt = updatedAt
$project.id = projectID $project.id = projectID
@@ -214,6 +221,7 @@ final class RoomModel: Model, @unchecked Sendable {
coolingTotal: coolingTotal, coolingTotal: coolingTotal,
coolingSensible: coolingSensible, coolingSensible: coolingSensible,
registerCount: registerCount, registerCount: registerCount,
rectangularSizes: rectangularSizes,
createdAt: createdAt!, createdAt: createdAt!,
updatedAt: updatedAt! updatedAt: updatedAt!
) )
@@ -236,6 +244,9 @@ final class RoomModel: Model, @unchecked Sendable {
if let registerCount = updates.registerCount, registerCount != self.registerCount { if let registerCount = updates.registerCount, registerCount != self.registerCount {
self.registerCount = registerCount self.registerCount = registerCount
} }
if let rectangularSizes = updates.rectangularSizes, rectangularSizes != self.rectangularSizes {
self.rectangularSizes = rectangularSizes
}
} }

View File

@@ -20,6 +20,7 @@ public struct ManualDClient: Sendable {
projectSHR: Double, projectSHR: Double,
logger: Logger? = nil logger: Logger? = nil
) async throws -> [DuctSizing.RoomContainer] { ) async throws -> [DuctSizing.RoomContainer] {
var registerIDCount = 1 var registerIDCount = 1
var retval: [DuctSizing.RoomContainer] = [] var retval: [DuctSizing.RoomContainer] = []
let totalHeatingLoad = rooms.totalHeatingLoad let totalHeatingLoad = rooms.totalHeatingLoad
@@ -38,6 +39,18 @@ public struct ManualDClient: Sendable {
) )
for n in 1...room.registerCount { 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( retval.append(
.init( .init(
registerID: "SR-\(registerIDCount)", registerID: "SR-\(registerIDCount)",
@@ -51,7 +64,9 @@ public struct ManualDClient: Sendable {
roundSize: sizes.ductulatorSize, roundSize: sizes.ductulatorSize,
finalSize: sizes.finalSize, finalSize: sizes.finalSize,
velocity: sizes.velocity, velocity: sizes.velocity,
flexSize: sizes.flexSize flexSize: sizes.flexSize,
rectangularSize: rectangularSize,
rectangularWidth: rectangularWidth
) )
) )
registerIDCount += 1 registerIDCount += 1

View File

@@ -3,6 +3,21 @@ import Foundation
public enum DuctSizing { public enum DuctSizing {
public struct RectangularDuct: Codable, Equatable, Sendable {
public let register: Int?
public let height: Int
public init(
register: Int? = nil,
height: Int,
) {
self.register = register
self.height = height
}
}
public struct RoomContainer: Codable, Equatable, Sendable { public struct RoomContainer: Codable, Equatable, Sendable {
public let registerID: String public let registerID: String
@@ -17,6 +32,8 @@ public enum DuctSizing {
public let finalSize: Int public let finalSize: Int
public let velocity: Int public let velocity: Int
public let flexSize: Int public let flexSize: Int
public let rectangularSize: RectangularDuct?
public let rectangularWidth: Int?
public init( public init(
registerID: String, registerID: String,
@@ -30,7 +47,9 @@ public enum DuctSizing {
roundSize: Double, roundSize: Double,
finalSize: Int, finalSize: Int,
velocity: Int, velocity: Int,
flexSize: Int flexSize: Int,
rectangularSize: RectangularDuct? = nil,
rectangularWidth: Int? = nil
) { ) {
self.registerID = registerID self.registerID = registerID
self.roomID = roomID self.roomID = roomID
@@ -44,6 +63,8 @@ public enum DuctSizing {
self.finalSize = finalSize self.finalSize = finalSize
self.velocity = velocity self.velocity = velocity
self.flexSize = flexSize self.flexSize = flexSize
self.rectangularSize = rectangularSize
self.rectangularWidth = rectangularWidth
} }
} }

View File

@@ -9,6 +9,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
public let coolingTotal: Double public let coolingTotal: Double
public let coolingSensible: Double? public let coolingSensible: Double?
public let registerCount: Int public let registerCount: Int
public let rectangularSizes: [DuctSizing.RectangularDuct]?
public let createdAt: Date public let createdAt: Date
public let updatedAt: Date public let updatedAt: Date
@@ -20,6 +21,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
coolingTotal: Double, coolingTotal: Double,
coolingSensible: Double? = nil, coolingSensible: Double? = nil,
registerCount: Int = 1, registerCount: Int = 1,
rectangularSizes: [DuctSizing.RectangularDuct]? = nil,
createdAt: Date, createdAt: Date,
updatedAt: Date updatedAt: Date
) { ) {
@@ -30,6 +32,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
self.coolingTotal = coolingTotal self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible self.coolingSensible = coolingSensible
self.registerCount = registerCount self.registerCount = registerCount
self.rectangularSizes = rectangularSizes
self.createdAt = createdAt self.createdAt = createdAt
self.updatedAt = updatedAt self.updatedAt = updatedAt
} }
@@ -68,6 +71,7 @@ extension Room {
public let coolingTotal: Double? public let coolingTotal: Double?
public let coolingSensible: Double? public let coolingSensible: Double?
public let registerCount: Int? public let registerCount: Int?
public let rectangularSizes: [DuctSizing.RectangularDuct]?
public init( public init(
name: String? = nil, name: String? = nil,
@@ -81,6 +85,18 @@ extension Room {
self.coolingTotal = coolingTotal self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible self.coolingSensible = coolingSensible
self.registerCount = registerCount self.registerCount = registerCount
self.rectangularSizes = nil
}
public init(
rectangularSizes: [DuctSizing.RectangularDuct]
) {
self.name = nil
self.heatingLoad = nil
self.coolingTotal = nil
self.coolingSensible = nil
self.registerCount = nil
self.rectangularSizes = rectangularSizes
} }
} }
} }

View File

@@ -677,6 +677,7 @@ extension SiteRoute.View.ProjectRoute {
public enum DuctSizingRoute: Equatable, Sendable { public enum DuctSizingRoute: Equatable, Sendable {
case index case index
case roomRectangularForm(Room.ID, RoomRectangularForm)
static let rootPath = "duct-sizing" static let rootPath = "duct-sizing"
@@ -685,6 +686,26 @@ extension SiteRoute.View.ProjectRoute {
Path { rootPath } Path { rootPath }
Method.get Method.get
} }
Route(.case(Self.roomRectangularForm)) {
Path {
rootPath
"room"
Room.ID.parser()
}
Method.post
Body {
FormData {
Field("register") { Int.parser() }
Field("height") { Int.parser() }
}
.map(.memberwise(RoomRectangularForm.init))
}
}
}
public struct RoomRectangularForm: Equatable, Sendable {
public let register: Int
public let height: Int
} }
} }
} }

View File

@@ -28,7 +28,7 @@ extension ManualDClient {
logger: logger logger: logger
) )
logger?.debug("Rooms: \(ductRooms)") // logger?.debug("Rooms: \(ductRooms)")
return ductRooms return ductRooms

View File

@@ -342,11 +342,31 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
func renderView(on request: ViewController.Request, projectID: Project.ID) async throws func renderView(on request: ViewController.Request, projectID: Project.ID) async throws
-> AnySendableHTML -> AnySendableHTML
{ {
@Dependency(\.database) var database
@Dependency(\.manualD) var manualD
switch self { switch self {
case .index: case .index:
return request.view { return request.view {
ProjectView(projectID: projectID, activeTab: .ductSizing, logger: request.logger) ProjectView(projectID: projectID, activeTab: .ductSizing, logger: request.logger)
} }
case .roomRectangularForm(let roomID, let form):
let _ = try await database.rooms.update(
roomID,
.init(rectangularSizes: [.init(register: form.register, height: form.height)])
)
// request.logger.debug("Got room rectangular form: \(roomID)")
//
// let containers = try await manualD.calculate(
// rooms: [room],
// designFrictionRateResult: database.designFrictionRate(projectID: projectID),
// projectSHR: database.projects.getSensibleHeatRatio(projectID)
// )
// request.logger.debug("Room Containers: \(containers)")
// let container = containers.first(where: { $0.roomName == "\(room.name)-\(form.register)" })!
// request.logger.debug("Room Container: \(container)")
// return DuctSizingView.RoomRow(projectID: projectID, room: container)
return ProjectView(projectID: projectID, activeTab: .ductSizing, logger: request.logger)
} }
} }
} }

View File

@@ -3,6 +3,8 @@ import ElementaryHTMX
import ManualDCore import ManualDCore
import Styleguide import Styleguide
// FIX: The value field is sometimes wonky as far as what values it accepts.
struct ComponentLossForm: HTML, Sendable { struct ComponentLossForm: HTML, Sendable {
static func id(_ componentLoss: ComponentPressureLoss? = nil) -> String { static func id(_ componentLoss: ComponentPressureLoss? = nil) -> String {
@@ -49,7 +51,7 @@ struct ComponentLossForm: HTML, Sendable {
label(.for("value")) { "Value" } label(.for("value")) { "Value" }
Input(id: "value", placeholder: "Pressure loss") Input(id: "value", placeholder: "Pressure loss")
.attributes( .attributes(
.type(.number), .min("0.03"), .max("1.0"), .step("0.1"), .required, .type(.number), .min("0.03"), .max("1.0"), .step("0.01"), .required,
.value(componentLoss?.value) .value(componentLoss?.value)
) )
} }

View File

@@ -7,12 +7,27 @@ import Styleguide
struct DuctSizingView: HTML, Sendable { struct DuctSizingView: HTML, Sendable {
let projectID: Project.ID
let rooms: [DuctSizing.RoomContainer] let rooms: [DuctSizing.RoomContainer]
var body: some HTML { var body: some HTML {
div { div {
h1(.class("text-2xl py-4")) { "Duct Sizes" } h1(.class("text-2xl py-4")) { "Duct Sizes" }
if rooms.count == 0 {
p(.class("text-error italic")) {
"Must complete all the previous sections to display duct sizing calculations."
}
} else {
RoomsTable(projectID: projectID, rooms: rooms)
}
}
}
struct RoomsTable: HTML, Sendable {
let projectID: Project.ID
let rooms: [DuctSizing.RoomContainer]
var body: some HTML<HTMLTag.div> {
div(.class("overflow-x-auto")) { div(.class("overflow-x-auto")) {
table(.class("table table-zebra")) { table(.class("table table-zebra")) {
thead { thead {
@@ -27,12 +42,14 @@ struct DuctSizingView: HTML, Sendable {
th(.class("hidden xl:table-cell")) { "Round Size" } th(.class("hidden xl:table-cell")) { "Round Size" }
th { "Velocity" } th { "Velocity" }
th { "Final Size" } th { "Final Size" }
th { "Height" }
th { "Width" }
th { "Flex Size" } th { "Flex Size" }
} }
} }
tbody { tbody {
for room in rooms { for room in rooms {
RoomRow(room: room) RoomRow(projectID: projectID, room: room)
} }
} }
} }
@@ -41,10 +58,19 @@ struct DuctSizingView: HTML, Sendable {
} }
struct RoomRow: HTML, Sendable { struct RoomRow: HTML, Sendable {
let projectID: Project.ID
let room: DuctSizing.RoomContainer let room: DuctSizing.RoomContainer
var route: String {
SiteRoute.View.router.path(
for: .project(.detail(projectID, .ductSizing(.index)))
)
.appendingPath("room")
.appendingPath(room.roomID)
}
var body: some HTML<HTMLTag.tr> { var body: some HTML<HTMLTag.tr> {
tr(.class("text-lg")) { tr(.class("text-lg"), .id(room.roomID.idString)) {
td { room.registerID } td { room.registerID }
td { room.roomName } td { room.roomName }
td { Number(room.heatingLoad, digits: 0) } td { Number(room.heatingLoad, digits: 0) }
@@ -62,6 +88,32 @@ struct DuctSizingView: HTML, Sendable {
Number(room.finalSize) Number(room.finalSize)
.attributes(.class("badge badge-outline badge-secondary text-xl font-bold")) .attributes(.class("badge badge-outline badge-secondary text-xl font-bold"))
} }
td {
form(
.hx.post(route),
.hx.target("body"),
.hx.swap(.outerHTML)
// .hx.trigger(
// .event(.change).from("#rectangularSize_\(room.roomID.idString)")
// )
) {
input(.class("hidden"), .name("register"), .value("\(room.roomName.last!)"))
Row {
Input(
id: "height",
name: "height",
placeholder: "Height"
)
.attributes(.type(.number), .min("0"), .value(room.rectangularSize?.height))
SubmitButton()
}
}
}
td {
if let width = room.rectangularWidth {
Number(width)
}
}
td { td {
Number(room.flexSize) Number(room.flexSize)
.attributes(.class("badge badge-outline badge-primary text-xl font-bold")) .attributes(.class("badge badge-outline badge-primary text-xl font-bold"))

View File

@@ -2,6 +2,8 @@ import Elementary
import ManualDCore import ManualDCore
import Styleguide import Styleguide
// FIX: Need to update available static, etc. when equipment info is submitted.
struct FrictionRateView: HTML, Sendable { struct FrictionRateView: HTML, Sendable {
let equipmentInfo: EquipmentInfo? let equipmentInfo: EquipmentInfo?
@@ -46,8 +48,8 @@ struct FrictionRateView: HTML, Sendable {
div(.class("p-4 space-y-6")) { div(.class("p-4 space-y-6")) {
h1(.class("text-4xl font-bold pb-6")) { "Friction Rate" } h1(.class("text-4xl font-bold pb-6")) { "Friction Rate" }
div(.class("flex space-x-4")) { div(.class("flex space-x-4")) {
Label("Available Static Pressure")
if let availableStaticPressure { if let availableStaticPressure {
Label("Available Static Pressure")
Number(availableStaticPressure, digits: 2) Number(availableStaticPressure, digits: 2)
.attributes(.class("badge badge-lg badge-outline font-bold ms-4")) .attributes(.class("badge badge-lg badge-outline font-bold ms-4"))
} }

View File

@@ -68,6 +68,7 @@ struct ProjectView: HTML, Sendable {
) )
case .ductSizing: case .ductSizing:
try await DuctSizingView( try await DuctSizingView(
projectID: projectID,
rooms: manualD.calculate( rooms: manualD.calculate(
rooms: database.rooms.fetch(projectID), rooms: database.rooms.fetch(projectID),
designFrictionRateResult: database.designFrictionRate(projectID: projectID), designFrictionRateResult: database.designFrictionRate(projectID: projectID),