first commit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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 body: some View {
|
||||
VStack(spacing: 20) {
|
||||
tiltPanel(image: "VehicleSide",
|
||||
angle: pitch,
|
||||
title: "Längs",
|
||||
lowerLabel: "Front",
|
||||
upperLabel: "Heck",
|
||||
aspect: VehicleTilt.sideAspect)
|
||||
|
||||
tiltPanel(image: "VehicleRear",
|
||||
angle: roll,
|
||||
title: "Quer",
|
||||
lowerLabel: "links",
|
||||
upperLabel: "rechts",
|
||||
aspect: VehicleTilt.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user