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? var style: VehicleGraphicStyle = .vanster var body: some View { VStack(spacing: 20) { tiltPanel(image: style.sideImageName, angle: pitch, title: "Längs", lowerLabel: "Front", upperLabel: "Heck", aspect: style.sideAspect) tiltPanel(image: style.rearImageName, angle: roll, title: "Quer", lowerLabel: "links", upperLabel: "rechts", aspect: style.rearAspect) // 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.", VehicleTilt.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(VehicleTilt.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(VehicleTilt.colour(for: angle)) .aspectRatio(aspect, contentMode: .fit) .rotationEffect(.degrees(-(angle ?? 0) * VehicleTilt.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) } } } /// 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" } } }