first commit

This commit is contained in:
fototeddy
2026-09-04 21:30:51 +02:00
commit da64b1b6bd
160 changed files with 20664 additions and 0 deletions
@@ -0,0 +1,77 @@
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)
// Positiver Pitch heisst: das Heck steht höher, die Blase
// wandert nach oben in der Ansicht also nach hinten.
.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)
}
}