import SwiftUI /// Libelle für die Uhr: eine Blase, die dorthin wandert, wo das Fahrzeug /// **höher** steht – wie bei einer echten Wasserwaage. /// /// Bewusst schlichter als am iPhone: Auf 40 mm Bildschirm ist der innere Ring /// (die Toleranz) die einzige Hilfslinie, die sich noch ablesen lässt. struct WatchBubble: View { let pitch: Double? let roll: Double? /// Bis zu welcher Neigung die Anzeige ausschlägt. var range: Double = 4 var body: some View { GeometryReader { geometry in let side = min(geometry.size.width, geometry.size.height) let radius = side / 2 let bubble = side * 0.22 let travel = radius - bubble / 2 - 2 let tolerance = max(LevelState.levelTolerance / range * travel, bubble * 0.55) ZStack { Circle().fill(Color.gray.opacity(0.25)) Circle().strokeBorder(Color.gray.opacity(0.5), lineWidth: 1) Circle() .strokeBorder(isLevel ? Color.green : Color.gray.opacity(0.6), lineWidth: isLevel ? 2 : 1) .frame(width: tolerance * 2, height: tolerance * 2) Circle() .fill(color) .frame(width: bubble, height: bubble) // Draufsicht mit Fahrtrichtung oben: Positiver Pitch heisst, // das Heck steht höher – die Blase wandert nach hinten, also // nach unten. .offset(x: clamped(roll) / range * travel, y: clamped(pitch) / range * travel) .animation(.spring(duration: 0.3), value: pitch) .animation(.spring(duration: 0.3), value: roll) .opacity(pitch == nil && roll == nil ? 0.25 : 1) } .frame(width: side, height: side) .frame(maxWidth: .infinity, maxHeight: .infinity) } .aspectRatio(1, contentMode: .fit) .accessibilityLabel(accessibilityText) } /// Hält die Blase im Ring, auch wenn das Fahrzeug stark schräg steht. private func clamped(_ value: Double?) -> Double { guard let value else { return 0 } return min(max(value, -range), range) } private var deviation: Double? { switch (pitch, roll) { case let (p?, r?): return max(abs(p), abs(r)) case let (p?, nil): return abs(p) case let (nil, r?): return abs(r) default: return nil } } private var isLevel: Bool { (deviation ?? .infinity) <= LevelState.levelTolerance } /// Grün nur, wenn es wirklich eben ist – die Akzentfarbe der App ist selbst /// grün, sonst sähe schief aus wie eben. private var color: Color { guard let deviation else { return .gray } if deviation <= LevelState.levelTolerance { return .green } return deviation <= 2 ? .orange : .red } private var accessibilityText: String { guard let pitch, let roll else { return "Keine Messwerte" } return String(format: "Längsneigung %.1f Grad, Querneigung %.1f Grad", pitch, roll) } }