Rotating the phone into landscape while leveling used to leave a portrait-shaped column of controls that needed scrolling to take in. All three level-integration screens now branch on verticalSizeClass: the bubble/vehicle status in the device screen and the full alignment assistant (advice, readings, and wedge heights) lay out side-by-side instead of stacked, sized to fit without scrolling. The device screen also measures its actual available height via a GeometryReader and feeds it to the level display so the bubble/vehicle graphic scales to fill the landscape screen rather than being capped at a fixed size, and the connection-status section moves to the bottom of the list in landscape so it doesn't compete with the display for space. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
114 lines
4.5 KiB
Swift
114 lines
4.5 KiB
Swift
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 style: VehicleGraphicStyle = .vanster
|
||
/// Für Querformat: beide Ansichten nebeneinander statt untereinander,
|
||
/// kleinere Schrift, ohne Überhöhungs-Hinweis – muss ohne Scrollen in die
|
||
/// Bildschirmhöhe passen.
|
||
var compact = false
|
||
/// Bildhöhe je Panel im Querformat – vom Aufrufer an die tatsächlich
|
||
/// verfügbare Bildschirmhöhe angepasst, statt fest verdrahtet.
|
||
var compactPanelHeight: CGFloat = 62
|
||
|
||
var body: some View {
|
||
if compact {
|
||
HStack(alignment: .top, spacing: 16) {
|
||
tiltPanel(image: style.sideImageName, angle: pitch, title: "Längs",
|
||
lowerLabel: "Front", upperLabel: "Heck", aspect: style.sideAspect)
|
||
tiltPanel(image: style.rearImageName, angle: roll, title: "Quer",
|
||
lowerLabel: "links", upperLabel: "rechts", aspect: style.rearAspect)
|
||
}
|
||
} else {
|
||
VStack(spacing: 20) {
|
||
tiltPanel(image: style.sideImageName, angle: pitch, title: "Längs",
|
||
lowerLabel: "Front", upperLabel: "Heck", aspect: style.sideAspect)
|
||
tiltPanel(image: style.rearImageName, angle: roll, title: "Quer",
|
||
lowerLabel: "links", upperLabel: "rechts", aspect: style.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: compact ? 4 : 8) {
|
||
HStack {
|
||
Text(title)
|
||
.font(compact ? .caption2.weight(.medium) : .subheadline.weight(.medium))
|
||
Spacer()
|
||
Text(angle.map { String(format: "%.1f°", $0) } ?? "–")
|
||
.font((compact ? Font.caption2 : .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: compact ? compactPanelHeight : 110)
|
||
|
||
HStack {
|
||
Text(lowerLabel)
|
||
Spacer()
|
||
Text(upperLabel)
|
||
}
|
||
.font(compact ? .system(size: 9) : .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"
|
||
}
|
||
}
|
||
}
|