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 componentLoss: ComponentLoss
public var effectiveLength: EffectiveLengthClient
// public var rectangularDuct: RectangularDuct
public var users: Users
}
@@ -29,6 +30,7 @@ extension DatabaseClient: TestDependencyKey {
equipment: .testValue,
componentLoss: .testValue,
effectiveLength: .testValue,
// rectangularDuct: .testValue,
users: .testValue
)
@@ -40,6 +42,7 @@ extension DatabaseClient: TestDependencyKey {
equipment: .live(database: database),
componentLoss: .live(database: database),
effectiveLength: .live(database: database),
// rectangularDuct: .live(database: database),
users: .live(database: database)
)
}
@@ -67,6 +70,7 @@ extension DatabaseClient.Migrations: DependencyKey {
EquipmentInfo.Migrate(),
Room.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)
.with(\.$project)
.filter(\.$project.$id, .equal, projectID)
.sort(\.$name, .ascending)
.all()
.map { try $0.toDTO() }
},
@@ -135,6 +136,7 @@ extension Room {
.field("coolingTotal", .double, .required)
.field("coolingSensible", .double)
.field("registerCount", .int8, .required)
.field("rectangularSizes", .array)
.field("createdAt", .datetime)
.field("updatedAt", .datetime)
.field(
@@ -172,6 +174,9 @@ final class RoomModel: Model, @unchecked Sendable {
@Field(key: "registerCount")
var registerCount: Int
@Field(key: "rectangularSizes")
var rectangularSizes: [DuctSizing.RectangularDuct]?
@Timestamp(key: "createdAt", on: .create, format: .iso8601)
var createdAt: Date?
@@ -190,6 +195,7 @@ final class RoomModel: Model, @unchecked Sendable {
coolingTotal: Double,
coolingSensible: Double? = nil,
registerCount: Int,
rectangularSizes: [DuctSizing.RectangularDuct]? = nil,
createdAt: Date? = nil,
updatedAt: Date? = nil,
projectID: Project.ID
@@ -200,6 +206,7 @@ final class RoomModel: Model, @unchecked Sendable {
self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible
self.registerCount = registerCount
self.rectangularSizes = rectangularSizes
self.createdAt = createdAt
self.updatedAt = updatedAt
$project.id = projectID
@@ -214,6 +221,7 @@ final class RoomModel: Model, @unchecked Sendable {
coolingTotal: coolingTotal,
coolingSensible: coolingSensible,
registerCount: registerCount,
rectangularSizes: rectangularSizes,
createdAt: createdAt!,
updatedAt: updatedAt!
)
@@ -236,6 +244,9 @@ final class RoomModel: Model, @unchecked Sendable {
if let registerCount = updates.registerCount, registerCount != self.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,
logger: Logger? = nil
) async throws -> [DuctSizing.RoomContainer] {
var registerIDCount = 1
var retval: [DuctSizing.RoomContainer] = []
let totalHeatingLoad = rooms.totalHeatingLoad
@@ -38,6 +39,18 @@ public struct ManualDClient: Sendable {
)
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(
registerID: "SR-\(registerIDCount)",
@@ -51,7 +64,9 @@ public struct ManualDClient: Sendable {
roundSize: sizes.ductulatorSize,
finalSize: sizes.finalSize,
velocity: sizes.velocity,
flexSize: sizes.flexSize
flexSize: sizes.flexSize,
rectangularSize: rectangularSize,
rectangularWidth: rectangularWidth
)
)
registerIDCount += 1

View File

@@ -3,6 +3,21 @@ import Foundation
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 let registerID: String
@@ -17,6 +32,8 @@ public enum DuctSizing {
public let finalSize: Int
public let velocity: Int
public let flexSize: Int
public let rectangularSize: RectangularDuct?
public let rectangularWidth: Int?
public init(
registerID: String,
@@ -30,7 +47,9 @@ public enum DuctSizing {
roundSize: Double,
finalSize: Int,
velocity: Int,
flexSize: Int
flexSize: Int,
rectangularSize: RectangularDuct? = nil,
rectangularWidth: Int? = nil
) {
self.registerID = registerID
self.roomID = roomID
@@ -44,6 +63,8 @@ public enum DuctSizing {
self.finalSize = finalSize
self.velocity = velocity
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 coolingSensible: Double?
public let registerCount: Int
public let rectangularSizes: [DuctSizing.RectangularDuct]?
public let createdAt: Date
public let updatedAt: Date
@@ -20,6 +21,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
coolingTotal: Double,
coolingSensible: Double? = nil,
registerCount: Int = 1,
rectangularSizes: [DuctSizing.RectangularDuct]? = nil,
createdAt: Date,
updatedAt: Date
) {
@@ -30,6 +32,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible
self.registerCount = registerCount
self.rectangularSizes = rectangularSizes
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -68,6 +71,7 @@ extension Room {
public let coolingTotal: Double?
public let coolingSensible: Double?
public let registerCount: Int?
public let rectangularSizes: [DuctSizing.RectangularDuct]?
public init(
name: String? = nil,
@@ -81,6 +85,18 @@ extension Room {
self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible
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 {
case index
case roomRectangularForm(Room.ID, RoomRectangularForm)
static let rootPath = "duct-sizing"
@@ -685,6 +686,26 @@ extension SiteRoute.View.ProjectRoute {
Path { rootPath }
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?.debug("Rooms: \(ductRooms)")
// logger?.debug("Rooms: \(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
-> AnySendableHTML
{
@Dependency(\.database) var database
@Dependency(\.manualD) var manualD
switch self {
case .index:
return request.view {
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 Styleguide
// FIX: The value field is sometimes wonky as far as what values it accepts.
struct ComponentLossForm: HTML, Sendable {
static func id(_ componentLoss: ComponentPressureLoss? = nil) -> String {
@@ -49,7 +51,7 @@ struct ComponentLossForm: HTML, Sendable {
label(.for("value")) { "Value" }
Input(id: "value", placeholder: "Pressure loss")
.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)
)
}

View File

@@ -7,12 +7,27 @@ import Styleguide
struct DuctSizingView: HTML, Sendable {
let projectID: Project.ID
let rooms: [DuctSizing.RoomContainer]
var body: some HTML {
div {
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")) {
table(.class("table table-zebra")) {
thead {
@@ -27,12 +42,14 @@ struct DuctSizingView: HTML, Sendable {
th(.class("hidden xl:table-cell")) { "Round Size" }
th { "Velocity" }
th { "Final Size" }
th { "Height" }
th { "Width" }
th { "Flex Size" }
}
}
tbody {
for room in rooms {
RoomRow(room: room)
RoomRow(projectID: projectID, room: room)
}
}
}
@@ -41,10 +58,19 @@ struct DuctSizingView: HTML, Sendable {
}
struct RoomRow: HTML, Sendable {
let projectID: Project.ID
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> {
tr(.class("text-lg")) {
tr(.class("text-lg"), .id(room.roomID.idString)) {
td { room.registerID }
td { room.roomName }
td { Number(room.heatingLoad, digits: 0) }
@@ -62,6 +88,32 @@ struct DuctSizingView: HTML, Sendable {
Number(room.finalSize)
.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 {
Number(room.flexSize)
.attributes(.class("badge badge-outline badge-primary text-xl font-bold"))

View File

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

View File

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