diff options
| -rw-r--r-- | ShedCalc/BOMController.swift | 7 | ||||
| -rw-r--r-- | ShedCalc/BOMModel.swift | 7 | ||||
| -rw-r--r-- | ShedCalc/BOMView.swift | 63 | ||||
| -rw-r--r-- | ShedCalc/ContentView.swift | 34 | ||||
| -rw-r--r-- | ShedCalc/ParamController.swift | 7 | ||||
| -rw-r--r-- | ShedCalc/ParamModel.swift | 37 | ||||
| -rw-r--r-- | ShedCalc/ParamView.swift | 41 | ||||
| -rw-r--r-- | ShedCalc/RenderController.swift | 145 | ||||
| -rw-r--r-- | ShedCalc/RenderModel.swift | 19 | ||||
| -rw-r--r-- | ShedCalc/RenderView.swift | 70 |
10 files changed, 424 insertions, 6 deletions
diff --git a/ShedCalc/BOMController.swift b/ShedCalc/BOMController.swift new file mode 100644 index 0000000..fa05eeb --- /dev/null +++ b/ShedCalc/BOMController.swift @@ -0,0 +1,7 @@ +// +// BOMController.swift +// ShedCalc +// +// Created by Jacky Jack on 01/08/2026. +// + diff --git a/ShedCalc/BOMModel.swift b/ShedCalc/BOMModel.swift new file mode 100644 index 0000000..0605fed --- /dev/null +++ b/ShedCalc/BOMModel.swift @@ -0,0 +1,7 @@ +// +// BOMModel.swift +// ShedCalc +// +// Created by Jacky Jack on 01/08/2026. +// + diff --git a/ShedCalc/BOMView.swift b/ShedCalc/BOMView.swift new file mode 100644 index 0000000..1bb6523 --- /dev/null +++ b/ShedCalc/BOMView.swift @@ -0,0 +1,63 @@ +// +// BOMView.swift +// ShedCalc +// +// Created by Jacky Jack on 28/07/2026. +// + +import SwiftUI +import Combine + +struct Materials: Identifiable { + let materialName: String + let pricePerMeter: Double + let totalMeters: Double + let totalPrice: Double + let metersPerPiece: Double + let totalPieces: Double + + let id = UUID() +} + + + +struct BOMView: View { + @Environment(ParamModel.self) private var paramModel + + @State private var materials = [ + Materials(materialName: "2x4", pricePerMeter: 1.2, totalMeters: 10, totalPrice: 12, metersPerPiece: 4.8, totalPieces: 23), + Materials(materialName: "OSB", pricePerMeter: 1.2, totalMeters: 10, totalPrice: 12, metersPerPiece: 4.8, totalPieces: 23), + Materials(materialName: "Roof Felt", pricePerMeter: 1.2, totalMeters: 10, totalPrice: 12, metersPerPiece: 4.8, totalPieces: 23), + Materials(materialName: "Nails", pricePerMeter: 1.2, totalMeters: 10, totalPrice: 12, metersPerPiece: 4.8, totalPieces: 23), + Materials(materialName: "Cladding", pricePerMeter: 1.2, totalMeters: 10, totalPrice: 12, metersPerPiece: 4.8, totalPieces: 23) + ] + + var body: some View { + VStack { + Text("BOM") + } + VStack { + Table(materials) { + TableColumn("Material", value: \.materialName) + TableColumn("Price per m") { material in + Text(material.pricePerMeter, format: .number.precision(.fractionLength(2))) + } + TableColumn("Total m") { material in + Text(material.totalMeters, format: .number.precision(.fractionLength(2))) + } + TableColumn("Total price") { material in + Text(material.totalPrice, format: .number.precision(.fractionLength(2))) + } + TableColumn("m per Piece") { material in + Text(material.metersPerPiece, format: .number.precision(.fractionLength(2))) + } + TableColumn("Pieces") { material in + Text(material.totalPieces, format: .number) + } + } + } + HStack { + Text("Total price: 0.00") + } + } +} diff --git a/ShedCalc/ContentView.swift b/ShedCalc/ContentView.swift index c19fdb3..dbf31a0 100644 --- a/ShedCalc/ContentView.swift +++ b/ShedCalc/ContentView.swift @@ -10,10 +10,12 @@ import SwiftData struct ContentView: View { @Environment(\.modelContext) private var modelContext + @Query private var items: [Item] + @State private var paramModel = ParamModel() var body: some View { - NavigationSplitView { + /*NavigationSplitView { List { ForEach(items) { item in NavigationLink { @@ -34,22 +36,33 @@ struct ContentView: View { } } detail: { Text("Select an item") - } + }*/ + TabView { + Tab("Render", systemImage: "tray") { + RenderView() + } + Tab("Params", systemImage: "tray") { + ParamView() + } + Tab("BOM", systemImage: "keyboard") { + BOMView() + } + }.environment(paramModel) } private func addItem() { - withAnimation { + /*withAnimation { let newItem = Item(timestamp: Date()) modelContext.insert(newItem) - } + }*/ } private func deleteItems(offsets: IndexSet) { - withAnimation { + /*withAnimation { for index in offsets { modelContext.delete(items[index]) } - } + }*/ } } @@ -57,3 +70,12 @@ struct ContentView: View { ContentView() .modelContainer(for: Item.self, inMemory: true) } + +#Preview { + NavigationSplitView { + ContentView() + } detail: { + Text("Select an item") + } +} + diff --git a/ShedCalc/ParamController.swift b/ShedCalc/ParamController.swift new file mode 100644 index 0000000..5678624 --- /dev/null +++ b/ShedCalc/ParamController.swift @@ -0,0 +1,7 @@ +// +// ParamController.swift +// ShedCalc +// +// Created by Jacky Jack on 01/08/2026. +// + diff --git a/ShedCalc/ParamModel.swift b/ShedCalc/ParamModel.swift new file mode 100644 index 0000000..4935bcd --- /dev/null +++ b/ShedCalc/ParamModel.swift @@ -0,0 +1,37 @@ +// +// ParamModel.swift +// ShedCalc +// +// Created by Jacky Jack on 01/08/2026. +// +import Combine +import SwiftUI +import Observation + +@Observable +class ParamModel { + var width: Float = 1.0 + var height: Float = 1.0 + var length: Float = 1.0 + var beamSpace: Float = 0.6 + + func floorArea() -> Float { + return width*length + } + + func roofArea() -> Float { + return width*length + } + + func dimensions() -> SIMD3<Float> { + return [width,length,height] + } + + func widthBeamNum() -> Int { + return Int(width/beamSpace) + 1 + } + + func lengthBeamNum() -> Int { + return Int(length/beamSpace) + 1 + } +} diff --git a/ShedCalc/ParamView.swift b/ShedCalc/ParamView.swift new file mode 100644 index 0000000..3e27978 --- /dev/null +++ b/ShedCalc/ParamView.swift @@ -0,0 +1,41 @@ +// +// ParamView.swift +// ShedCalc +// +// Created by Jacky Jack on 28/07/2026. +// + +import SwiftUI + + +struct ParamView: View { + //@StateObject private var viewModel = ParamModel() + @Environment(ParamModel.self) var paramModel + + var body: some View { + @Bindable var paramModel = paramModel + VStack { + HStack { + Text("All in meters") + } + HStack { + Text("Width") + TextField("Width", value: $paramModel.width, format: .number) + } + HStack { + Text("Height") + TextField("Height", value: $paramModel.height, format: .number) + } + HStack { + Text("Length") + TextField("Length", value: $paramModel.length, format: .number) + } + HStack { + Text("Total floor area: \(paramModel.floorArea(), specifier: "%.2f") m^2") + } + HStack { + Text("Total roof area: \(paramModel.roofArea(), specifier: "%.2f") m^2") + } + } + } +} diff --git a/ShedCalc/RenderController.swift b/ShedCalc/RenderController.swift new file mode 100644 index 0000000..1a57cb0 --- /dev/null +++ b/ShedCalc/RenderController.swift @@ -0,0 +1,145 @@ +// +// RenderController.swift +// ShedCalc +// +// Created by Jacky Jack on 28/07/2026. +// + +import SwiftUI +import RealityKit + +let BEAM_2x4_WIDTH: Float = 0.02 +let BEAM_2x4_LENGTH: Float = 0.04 + +struct BeamData { + let id: String + let position: SIMD3<Float> + let height: Float + let rotation: SIMD3<Float> + } + +class RenderController { + func createScene(dimensions: SIMD3<Float>, rotation: SIMD2<Float>) -> Entity { + let scene = Entity() + scene.name = "scene" + + let box = createBox(dimensions: [1,1,1]) + scene.addChild(box) + + let beam0 = createBeam2x4(position: [1,1,0], height: 2.4, rotate: [90,0,0], id: "beam0") + scene.addChild(beam0) + + //let beam1 = createBeam2x4(position: [1,0,0], height: 2.4, rotate: [0,0,0], id: "beam1") + //scene.addChild(beam1) + + //let beam2 = createBeam2x4(position: [1,1,0], height: 2.4, rotate: [0,0,90], id: "beam2") //Z-around beam + //scene.addChild(beam2) + + //let beam3 = createBeam2x4(position: [1,1,0], height: 2.4, rotate: [0,90,0], id: "beam3") + //scene.addChild(beam3) + + let ground = createGround() + scene.addChild(ground) + + return scene + } + + func createLights() -> (DirectionalLight, PointLight) { + let directional = DirectionalLight() + directional.light.intensity = 2000 + directional.look(at: .zero, from: [2, 4, 3], relativeTo: nil) + + let point = PointLight() + point.light.intensity = 5000 + point.position = [-2, 2, -2] + + return (directional, point) + } + + func createCamera() -> PerspectiveCamera { + let camera = PerspectiveCamera() + camera.camera.fieldOfViewInDegrees = 60 + camera.position = [0,1,5] + camera.look(at: .zero, from: camera.position, relativeTo: nil) + return camera + } + + // MARK: - Gesture Handling + + func handleDragGesture( + _ value: DragGesture.Value, + lastDrag: CGSize, + currentRotation: SIMD2<Float> + ) -> (rotation: SIMD2<Float>, drag: CGSize) { + let dx = Float(value.translation.width - lastDrag.width) * 0.01 + let dy = Float(value.translation.height - lastDrag.height) * 0.01 + + let newRotation = SIMD2<Float>( + currentRotation.x + dy, + currentRotation.y + dx + ) + + return (rotation: newRotation, drag: value.translation) + } + + // MARK: - Private Helpers + + private func createBox(dimensions: SIMD3<Float>) -> ModelEntity { + let box = ModelEntity( + mesh: .generateBox(size: dimensions, cornerRadius: 0.02), + materials: [SimpleMaterial(color: .yellow, isMetallic: false)] + ) + box.position = [0,0,0] + box.name = "box" + return box + } + + private func createBeam2x4(position: SIMD3<Float>, height: Float, rotate: SIMD3<Float>, id: String) -> ModelEntity { + let box = ModelEntity( + mesh: .generateBox(size: SIMD3(BEAM_2x4_WIDTH,BEAM_2x4_LENGTH,height), cornerRadius: 0.02), + materials: [SimpleMaterial(color: .yellow, isMetallic: false)] + ) + //box.look(at: position, from: lookFrom, relativeTo: nil) + //box.setOrientation(.init(eulerAngles: lookFrom), relativeTo: nil) + box.position = position + // Create individual axis rotations + let pitch = simd_quatf(angle: Float.pi*rotate.x/180.0, axis: [1, 0, 0]) // X-axis + let yaw = simd_quatf(angle: Float.pi*rotate.y/180.0, axis: [0, 1, 0]) // Y-axis + let roll = simd_quatf(angle: Float.pi*rotate.z/180.0, axis: [0, 0, 1]) // Z-axis + + // Combine rotations (order matters: usually yaw * pitch * roll) + box.transform.rotation = yaw * pitch * roll + box.name = "beam-\(id)" + return box + } + + func generateShedWall(position: SIMD3<Float>, height: Float, widthNum: Int, lengthNum: Int, spaceBeem: Float) -> [BeamData] { + var beams: [BeamData] = [] + var id: Int = 0 + //Wall 0 + for i in 0...widthNum { + let x0 = Float(i)*spaceBeem + beams.append(BeamData(id: "beam\(id)", position: [x0,position.y, position.z], height: height, rotation: [90,0,0])) + id += 1 + } + //Wall 1 + //Wall 2 + //Wall 3 + return beams + } + + func createBeams(_ beams: [BeamData]) -> [ModelEntity] { + return beams.map { beam in + createBeam2x4(position: beam.position, height: beam.height, rotate: beam.rotation, id: beam.id) + } + } + + private func createGround() -> ModelEntity { + let ground = ModelEntity( + mesh: .generatePlane(width: 10, depth: 10), + materials: [UnlitMaterial(color: .green)] + ) + ground.position.y = -0.5 + return ground + } +} diff --git a/ShedCalc/RenderModel.swift b/ShedCalc/RenderModel.swift new file mode 100644 index 0000000..86c5a1d --- /dev/null +++ b/ShedCalc/RenderModel.swift @@ -0,0 +1,19 @@ +// +// RenderModel.swift +// ShedCalc +// +// Created by Jacky Jack on 01/08/2026. +// +import SwiftUI +import Combine + +class RenderModel: ObservableObject { + // Scene rotation + @Published var rotationX: Float = 0.4 + @Published var rotationY: Float = 0.6 + + func reset() { + rotationX = 0.4 + rotationY = 0.6 + } +} diff --git a/ShedCalc/RenderView.swift b/ShedCalc/RenderView.swift new file mode 100644 index 0000000..a143e99 --- /dev/null +++ b/ShedCalc/RenderView.swift @@ -0,0 +1,70 @@ +// +// RenderView.swift +// ShedCalc +// +// Created by Jacky Jack on 28/07/2026. +// +import SwiftUI +import RealityKit + +struct RenderView: View { + private let renderController = RenderController() + @Environment(ParamModel.self) var paramModel + @StateObject private var viewModel = RenderModel() + @State private var lastDrag: CGSize = .zero + + var body: some View { + VStack(spacing: 0) { + RealityView { content in + //add objects to scence + let scene = renderController.createScene(dimensions: [0,0,0], rotation: [0,0]) + content.add(scene) + + //add lights to scene + + //create camera view + let camera = renderController.createCamera() + content.add(camera) + + //draw beams + //let beam = renderController.createBeam2x4(position: [0,0,0], height: 2.4, lookFrom: camera.position, id: "beam0") + //content.addChild(beam) + + //draw all beams + let beamData = renderController.generateShedWall(position: [0,0,0], height: paramModel.height, widthNum: paramModel.widthBeamNum(), lengthNum: paramModel.lengthBeamNum(), spaceBeem: paramModel.beamSpace) + //content.add(beams) + let beams = renderController.createBeams(beamData) + beams.forEach { scene.addChild($0) } + + } update: { content in + // Update box geometry directly in the scene + if let scene = content.entities.first(where: { $0.name == "scene" }), + let box = scene.findEntity(named: "box") as? ModelEntity { + box.model?.mesh = .generateBox(size: paramModel.dimensions(), cornerRadius: 0.02) + } + // Update scene rotation directly + if let scene = content.entities.first(where: { $0.name == "scene" }) { + let xRot = simd_quatf(angle: viewModel.rotationX, axis: [1, 0, 0]) + let yRot = simd_quatf(angle: viewModel.rotationY, axis: [0, 1, 0]) + scene.transform.rotation = yRot * xRot + } + }.gesture( + DragGesture() + .onChanged { value in + let result = renderController.handleDragGesture( + value, + lastDrag: lastDrag, + currentRotation: [viewModel.rotationX, viewModel.rotationY] + ) + viewModel.rotationX = result.rotation.x + viewModel.rotationY = result.rotation.y + lastDrag = result.drag + } + .onEnded { _ in + lastDrag = .zero + } + ) + } + .frame(minWidth: 500, minHeight: 500) + } +} |
