import AVFoundation import Observation import SceneKit import SwiftUI import UIKit @Observable final class InventoryStore { enum Mode: String, CaseIterable, Identifiable { case sunseeker = "Sunseeker" case dreamWeaver = "Dream Weaver" case dollhouse = "Dollhouse" var id: String { rawValue } } var mode: Mode = .sunseeker var selectedPrompt: String = "Modern Islamic" var sourceImage: UIImage? var generatedImage: UIImage? var isProcessing: Bool = false var sunNodesReady: Bool = false var dollhouseHour: Double = 12 let prompts = ["Modern Islamic", "Minimalist", "Night Mode"] } struct InventoryView: View { @State private var store = InventoryStore() @State private var showCamera = false @State private var sliderTickHour = 12 private let haptics = UIImpactFeedbackGenerator(style: .light) var body: some View { VStack(alignment: .leading, spacing: 16) { // Page header VStack(alignment: .leading, spacing: 4) { Text("Inventory") .font(.system(size: 28, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) Text("Sunseeker · Dream Weaver · Dollhouse") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } .padding(.horizontal, 20) .padding(.top, 20) Picker("Mode", selection: $store.mode) { ForEach(InventoryStore.Mode.allCases) { mode in Text(mode.rawValue).tag(mode) } } .pickerStyle(.segmented) .padding(.horizontal, 20) .padding(.top, 12) Group { switch store.mode { case .sunseeker: #if targetEnvironment(simulator) ZStack { VStack(spacing: 14) { Image(systemName: "camera.metering.unknown") .font(.system(size: 40)) .foregroundStyle(VelocityTheme.mutedFg) Text("AR Not Available in Simulator") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text("Sunseeker requires a real device with a camera and compass. Run on iPhone or iPad to use this feature.") .font(.system(size: 13)) .foregroundStyle(VelocityTheme.mutedFg) .multilineTextAlignment(.center) } .padding(24) .frame(maxWidth: .infinity, maxHeight: .infinity) } .background( RoundedRectangle(cornerRadius: 16) .fill(Color(red: 0.031, green: 0.039, blue: 0.071)) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(VelocityTheme.borderAccent, lineWidth: 1) ) ) #else SunseekerPanel(sunNodesReady: $store.sunNodesReady) #endif case .dreamWeaver: DreamWeaverPanel( sourceImage: $store.sourceImage, generatedImage: $store.generatedImage, selectedPrompt: $store.selectedPrompt, isProcessing: $store.isProcessing, prompts: store.prompts, showCamera: $showCamera ) case .dollhouse: DollhousePanel(hour: $store.dollhouseHour, tickHour: $sliderTickHour, haptics: haptics) } } .padding(.horizontal, 20) .padding(.bottom, 20) .animation(.easeInOut(duration: 0.25), value: store.mode) } .background(VelocityTheme.background) .onAppear { // Dark-theme the segmented control UISegmentedControl.appearance().selectedSegmentTintColor = UIColor( red: 0.231, green: 0.510, blue: 0.965, alpha: 0.85) UISegmentedControl.appearance().setTitleTextAttributes( [.foregroundColor: UIColor.white], for: .selected) UISegmentedControl.appearance().setTitleTextAttributes( [.foregroundColor: UIColor(white: 0.62, alpha: 1)], for: .normal) UISegmentedControl.appearance().backgroundColor = UIColor( red: 0.031, green: 0.039, blue: 0.071, alpha: 1) } .sheet(isPresented: $showCamera) { CameraPicker(image: $store.sourceImage, isPresented: $showCamera) } } } private struct SunseekerPanel: View { @Binding var sunNodesReady: Bool var body: some View { ZStack(alignment: .topLeading) { ARSunOverlayView(sunNodesReady: $sunNodesReady) .clipShape(RoundedRectangle(cornerRadius: 20)) DashedSunLine() .stroke(Color.yellow.opacity(0.9), style: StrokeStyle(lineWidth: 3, dash: [10, 8])) .padding(.horizontal, 24) .padding(.vertical, 80) VStack(alignment: .leading, spacing: 8) { Text("Sunseeker") .font(.headline) Text("Point the iPad toward windows to inspect yearly sun-entry path.") .font(.subheadline) .foregroundStyle(.secondary) } .padding(14) .background { GlassBlurView(style: .systemThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 14)) } .padding(20) } } } private struct DreamWeaverPanel: View { @Binding var sourceImage: UIImage? @Binding var generatedImage: UIImage? @Binding var selectedPrompt: String @Binding var isProcessing: Bool let prompts: [String] @Binding var showCamera: Bool var body: some View { VStack(spacing: 14) { ZStack { RoundedRectangle(cornerRadius: 20) .fill(Color.black.opacity(0.9)) if let sourceImage { Image(uiImage: sourceImage) .resizable() .scaledToFit() .clipShape(RoundedRectangle(cornerRadius: 16)) .padding(12) } else { ContentUnavailableView("No Capture", systemImage: "camera.viewfinder", description: Text("Tap Capture to snap a room.")) .foregroundStyle(.white) } if let generatedImage { Image(uiImage: generatedImage) .resizable() .scaledToFit() .clipShape(RoundedRectangle(cornerRadius: 16)) .padding(12) .transition(.opacity) } if isProcessing { ProcessingOverlay() } } .frame(maxWidth: .infinity, minHeight: 420) .animation(.easeInOut(duration: 0.35), value: generatedImage) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(prompts, id: \.self) { prompt in Text(prompt) .font(.subheadline.weight(.semibold)) .padding(.horizontal, 14) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 12) .fill(prompt == selectedPrompt ? Color.yellow.opacity(0.85) : Color.white.opacity(0.12)) ) .onTapGesture { selectedPrompt = prompt } } } } HStack(spacing: 12) { Button("Capture") { showCamera = true } .buttonStyle(.borderedProminent) Button("Reimagine") { Task { await generate() } } .buttonStyle(.bordered) .disabled(sourceImage == nil || isProcessing) } } .padding(16) .background { GlassBlurView(style: .systemThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 18)) } } @MainActor private func generate() async { guard let sourceImage, !isProcessing else { return } isProcessing = true do { let result = try await ComfyClient.shared.generateImage(source: sourceImage, prompt: selectedPrompt) withAnimation(.easeInOut(duration: 0.4)) { generatedImage = result } } catch { print("Dream Weaver error: \(error)") } isProcessing = false } } private struct DollhousePanel: View { @Binding var hour: Double @Binding var tickHour: Int let haptics: UIImpactFeedbackGenerator var body: some View { VStack(spacing: 12) { SceneKitDollhouseView(hour: $hour) .clipShape(RoundedRectangle(cornerRadius: 20)) .frame(maxWidth: .infinity, minHeight: 460) VStack(alignment: .leading, spacing: 8) { Text(String(format: "Time: %02d:00", Int(hour.rounded()))) .font(.headline) Slider(value: $hour, in: 0...24, step: 0.25) .onChange(of: hour) { _, newValue in let rounded = Int(newValue.rounded()) if rounded != tickHour { tickHour = rounded haptics.impactOccurred(intensity: 0.7) } } } .padding(14) .background { GlassBlurView(style: .systemThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 14)) } } } } private struct SceneKitDollhouseView: UIViewRepresentable { @Binding var hour: Double func makeCoordinator() -> Coordinator { Coordinator() } func makeUIView(context: Context) -> SCNView { let view = SCNView() view.scene = context.coordinator.scene view.autoenablesDefaultLighting = false view.allowsCameraControl = true view.backgroundColor = UIColor.systemBackground context.coordinator.setupScene() context.coordinator.updateSunLight(hour: hour) return view } func updateUIView(_ uiView: SCNView, context: Context) { context.coordinator.updateSunLight(hour: hour) } final class Coordinator { let scene = SCNScene() private let sunNode = SCNNode() func setupScene() { if let modelScene = SCNScene(named: "Building.usdz") ?? SCNScene(named: "Building.scn") { let container = SCNNode() for child in modelScene.rootNode.childNodes { container.addChildNode(child.clone()) } scene.rootNode.addChildNode(container) } else { let fallback = SCNFloor() fallback.firstMaterial?.diffuse.contents = UIColor.secondarySystemBackground scene.rootNode.addChildNode(SCNNode(geometry: fallback)) } let camera = SCNCamera() let cameraNode = SCNNode() cameraNode.camera = camera cameraNode.position = SCNVector3(0, 4, 10) scene.rootNode.addChildNode(cameraNode) let light = SCNLight() light.type = .directional light.intensity = 1_200 light.castsShadow = true sunNode.light = light scene.rootNode.addChildNode(sunNode) let ambient = SCNLight() ambient.type = .ambient ambient.intensity = 200 let ambientNode = SCNNode() ambientNode.light = ambient scene.rootNode.addChildNode(ambientNode) } func updateSunLight(hour: Double) { let normalized = (hour / 24.0) * (2 * Double.pi) let x = Float(cos(normalized) * 8.0) let y = Float(max(sin(normalized) * 8.0, 1.0)) let z = Float(sin(normalized + .pi / 3) * 6.0) sunNode.position = SCNVector3(x, y, z) sunNode.look(at: SCNVector3(0, 0, 0)) } } } private struct ProcessingOverlay: View { @State private var animate = false var body: some View { ZStack { RoundedRectangle(cornerRadius: 16) .fill(.black.opacity(0.45)) Text("AI Processing...") .font(.headline.weight(.bold)) .foregroundStyle(.white) .padding(.horizontal, 20) .padding(.vertical, 12) .background { GlassBlurView(style: .systemUltraThinMaterialDark) .clipShape(Capsule()) } .overlay( Rectangle() .fill( LinearGradient( colors: [.clear, .white.opacity(0.6), .clear], startPoint: .leading, endPoint: .trailing ) ) .rotationEffect(.degrees(18)) .offset(x: animate ? 160 : -160) .animation(.linear(duration: 1.2).repeatForever(autoreverses: false), value: animate) .blendMode(.screen) .mask(Capsule().frame(height: 44)) ) } .padding(12) .onAppear { animate = true } } } private struct DashedSunLine: Shape { func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: rect.minX, y: rect.maxY * 0.75)) path.addQuadCurve( to: CGPoint(x: rect.maxX, y: rect.maxY * 0.25), control: CGPoint(x: rect.midX, y: rect.minY + 30) ) return path } } private struct CameraPicker: UIViewControllerRepresentable { @Binding var image: UIImage? @Binding var isPresented: Bool func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() picker.delegate = context.coordinator if UIImagePickerController.isSourceTypeAvailable(.camera) { picker.sourceType = .camera picker.cameraCaptureMode = .photo } else { picker.sourceType = .photoLibrary } return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { private let parent: CameraPicker init(_ parent: CameraPicker) { self.parent = parent } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { parent.isPresented = false } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { if let captured = info[.originalImage] as? UIImage { parent.image = captured } parent.isPresented = false } } }