import SwiftUI /// Zeigt die Neigung am Fahrzeug selbst, statt an einer abstrakten Blase. /// /// Zwei Ansichten, jede um ihre Achse gekippt: /// /// * **Seitenansicht** für die Längsneigung. Die Front zeigt nach links, das /// Heck nach rechts. /// * **Heckansicht** für die Querneigung. Sie teilt die Blickrichtung des /// Fahrers, links im Bild ist also links am Fahrzeug – bei einer /// Frontansicht wäre es seitenverkehrt. /// /// In beiden Fällen wird gegen den mathematischen Drehsinn gekippt: Steht das /// Heck höher, muss die rechte Bildseite nach oben. struct VehicleTiltView: View { let pitch: Double? let roll: Double? /// Kleine Neigungen sind am Fahrzeug sonst kaum zu erkennen – zwei Grad /// wären ein knappes Grad Bildneigung. Die Überhöhung wird angeschrieben, /// damit niemand den Winkel für bare Münze nimmt. static let exaggeration: Double = 3 var body: some View { VStack(spacing: 20) { tiltPanel(image: "VehicleSide", angle: pitch, title: "Längs", lowerLabel: "Front", upperLabel: "Heck", aspect: 925.0 / 600.0) tiltPanel(image: "VehicleRear", angle: roll, title: "Quer", lowerLabel: "links", upperLabel: "rechts", aspect: 1) // Ohne diesen Hinweis nähme man den Bildwinkel für den echten. Text(String(format: "Neigung %.0f-fach überhöht dargestellt – " + "sonst wäre sie kaum zu erkennen. Die Gradzahlen sind echt.", Self.exaggeration)) .font(.caption2) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) } } private func tiltPanel(image: String, angle: Double?, title: String, lowerLabel: String, upperLabel: String, aspect: Double) -> some View { VStack(spacing: 8) { HStack { Text(title) .font(.subheadline.weight(.medium)) Spacer() Text(angle.map { String(format: "%.1f°", $0) } ?? "–") .font(.subheadline.weight(.semibold).monospacedDigit()) .foregroundStyle(colour(for: angle)) } ZStack { // Waagerechte als Bezug – ohne sie ist eine kleine Neigung // nicht einzuschätzen. Rectangle() .fill(Color.secondary.opacity(0.35)) .frame(height: 1) Image(image) .resizable() .scaledToFit() .foregroundStyle(colour(for: angle)) .aspectRatio(aspect, contentMode: .fit) .rotationEffect(.degrees(-(angle ?? 0) * Self.exaggeration)) .animation(.spring(duration: 0.4), value: angle) .opacity(angle == nil ? 0.3 : 1) } .frame(height: 110) HStack { Text(lowerLabel) Spacer() Text(upperLabel) } .font(.caption2) .foregroundStyle(.secondary) } } private func colour(for angle: Double?) -> Color { guard let angle else { return .secondary } if abs(angle) <= LevelState.levelTolerance { return .green } if abs(angle) <= 2 { return .orange } return .red } } /// Umschalter zwischen den beiden Darstellungen, gemerkt über Starts hinweg. enum LevelDisplayStyle: String, CaseIterable, Identifiable { case bubble case vehicle var id: String { rawValue } var title: String { switch self { case .bubble: return "Libelle" case .vehicle: return "Fahrzeug" } } }