forked from fritob/Camper-Monitor
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:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user