Neigung am Fahrzeug darstellen

Neben der Libelle gibt es jetzt eine zweite Darstellung: die Neigung am
Fahrzeug selbst, mit den Zeichnungen aus dem Ursprungsprojekt. Umschaltbar
und über Starts hinweg gemerkt.

Seitenansicht für längs, Heckansicht für quer. Die Heckansicht ist bewusst
gewählt, nicht die Frontansicht: sie teilt die Blickrichtung des Fahrers,
links im Bild ist also links am Fahrzeug. Von vorne betrachtet wäre es
seitenverkehrt und damit beim Ausrichten irreführend.

Die Zeichnungen sind weiss gefüllt mit dunklen Linien. Als Schablone
eingefärbt wurde daraus eine massive Fläche, weil alles Deckende zur
Tintfarbe wird - Fenster, Räder und Konturen verschwanden. Tools/
make-vehicle-art übersetzt deshalb die Helligkeit in Deckkraft: dunkle
Linien voll deckend, helle Karosserie nur angedeutet. Eingefärbt bleibt
die Zeichnung erhalten und passt sich Hell- wie Dunkeldarstellung an.

Die Neigung wird dreifach überhöht gezeigt, sonst wären zwei Grad am
Fahrzeug kaum auszumachen. Das steht als Hinweis darunter, zusammen mit
der Klarstellung, dass die Gradzahlen echt sind - sonst nähme man den
Bildwinkel für den tatsächlichen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
BiasF
2026-08-30 23:22:07 +02:00
co-authored by Claude Opus 5
parent 767fe4e798
commit 49138312ea
10 changed files with 263 additions and 9 deletions
+116
View File
@@ -0,0 +1,116 @@
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"
}
}
}