// // ARStatusOverlay.swift // GasTankAR // // Created by Dennis Nemec on 26.08.26. // import SwiftUI import ARKit /// Dezente, nicht-distraktive Overlays über der AR-View: ein schmaler Status-Banner /// oben sowie Aktions-Karten unten für die Entscheidungs-States der State-Machine. struct ARStatusOverlay: View { let controller: ARSessionController let onCancel: () -> Void var body: some View { ZStack { // Fadenkreuz exakt im Bildschirmzentrum – dort trifft der Raycast. if controller.state == .selectLocation { crosshair } VStack { if let message = controller.statusMessage { statusBanner(message) .transition(.move(edge: .top).combined(with: .opacity)) } Spacer() bottomContent .transition(.move(edge: .bottom).combined(with: .opacity)) } .padding() } .animation(.easeInOut(duration: 0.25), value: controller.statusMessage) .animation(.easeInOut(duration: 0.25), value: controller.state) .animation(.easeInOut(duration: 0.25), value: controller.isTankSelected) } // MARK: - Fadenkreuz & Auslöser (Kamera-Style) private var crosshair: some View { ZStack { Circle() .stroke(.white.opacity(0.9), lineWidth: 2) .frame(width: 44, height: 44) Circle() .fill(.white) .frame(width: 5, height: 5) } .shadow(radius: 2) .allowsHitTesting(false) } private var shutterButton: some View { VStack(spacing: 10) { Text("Tank platzieren") .font(.subheadline.weight(.medium)) .padding(.horizontal, 12) .padding(.vertical, 6) .background(.ultraThinMaterial, in: .capsule) Button { controller.capture() } label: { ZStack { Circle() .stroke(.white, lineWidth: 4) .frame(width: 74, height: 74) Circle() .fill(.white) .frame(width: 60, height: 60) } .shadow(radius: 4) .contentShape(.circle) } .buttonStyle(.plain) .accessibilityLabel("Tank platzieren") } } // MARK: - Status-Banner private func statusBanner(_ message: String) -> some View { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.yellow) Text(message) .font(.footnote) .fixedSize(horizontal: false, vertical: true) } .padding(.horizontal, 14) .padding(.vertical, 10) .background(.ultraThinMaterial, in: .capsule) .frame(maxWidth: .infinity) } // MARK: - Untere Aktions-Inhalte je State @ViewBuilder private var bottomContent: some View { switch controller.state { case .selectLocation: shutterButton case .showOptions: actionCard( title: "Zu wenige Merkmale erkannt", message: "Die Umgebung bietet ARKit zu wenig Struktur." ) { Button("Weiter umhergehen") { controller.keepMoving() } .buttonStyle(.borderedProminent) Button("Marker platzieren") { controller.placeAnchorMarker() } .buttonStyle(.bordered) Button("Abbrechen", role: .cancel, action: onCancel) .buttonStyle(.bordered) } case .anchorDetection: hintCard( icon: "viewfinder", text: "Richte die Kamera auf den Marker, um ihn zu erkennen.", showsProgress: true, onCancel: onCancel ) case .askToCancel: actionCard( title: "Marker nicht erkannt", message: "Der Marker konnte nicht zuverlässig erkannt werden." ) { Button("Erneut versuchen") { controller.tryAgainAnchor() } .buttonStyle(.borderedProminent) Button("Abbrechen", role: .cancel, action: onCancel) .buttonStyle(.bordered) } case .relocating: hintCard( icon: "arrow.triangle.2.circlepath", text: "Position wird wiederhergestellt …", showsProgress: true ) case .relocationFailed: actionCard( title: "Position nicht gefunden", message: "Die gespeicherte Position konnte nicht wiederhergestellt werden. Neu starten und den Tank neu platzieren?" ) { Button("Neu starten & platzieren", systemImage: "arrow.counterclockwise") { controller.restart() } .buttonStyle(.borderedProminent) Button("Weiter versuchen") { controller.retryRelocation() } .buttonStyle(.bordered) Button("Abbrechen", role: .cancel, action: onCancel) .buttonStyle(.bordered) } case .immersive: if controller.isTankSelected { TankManipulationPanel(controller: controller) } else if controller.measurementPhase == .idle && controller.pipeRoutingPhase == .idle { Text("Tank antippen zum Neu-Platzieren · Ziehen / 2 Finger / Pinch zum Anpassen") .font(.caption) .multilineTextAlignment(.center) .padding(.horizontal, 12) .padding(.vertical, 6) .background(.ultraThinMaterial, in: .capsule) } default: // initializingWorld / initializingNotPossible / userMovesAround übernimmt das // Coaching-Overlay; immersive/worldNotInitialized brauchen keine Aktion. EmptyView() } } // MARK: - Bausteine private func hintCard( icon: String, text: String, showsProgress: Bool = false, onCancel: (() -> Void)? = nil ) -> some View { HStack(spacing: 12) { if showsProgress { ProgressView() } else { Image(systemName: icon) .font(.title3) .foregroundStyle(.tint) } Text(text) .font(.subheadline) Spacer(minLength: 0) if let onCancel { Button("Abbrechen", role: .cancel, action: onCancel) .buttonStyle(.bordered) .controlSize(.small) } } .padding() .background(.ultraThinMaterial, in: .rect(cornerRadius: 16)) } private func actionCard( title: String, message: String, @ViewBuilder buttons: () -> Buttons ) -> some View { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.headline) Text(message) .font(.subheadline) .foregroundStyle(.secondary) } VStack(spacing: 8) { buttons() } .frame(maxWidth: .infinity) } .padding() .background(.ultraThinMaterial, in: .rect(cornerRadius: 16)) } }