Initial commit for first version of project

This commit is contained in:
Dennis Nemec
2026-09-02 20:54:36 +02:00
parent 3d9f1ab6bc
commit 992bb32025
56 changed files with 5139 additions and 31 deletions

View File

@ -0,0 +1,234 @@
//
// 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<Buttons: View>(
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))
}
}