Auffahrkeile: Höhe je Rad statt getrennt für quer und längs
Bisher standen dort zwei Angaben nebeneinander, etwa "rechts 8 cm" und "vorne 4 cm". Das liest sich wie zwei Aufgaben, ist aber eine: Unter "rechts" liegen zwei Räder, unter "vorne" auch, und eines davon ist dasselbe. Ausführen lässt sich das so nicht. Gerechnet wird jetzt über alle vier Aufstandspunkte auf einmal. Aus Spurweite, Radstand und den beiden Winkeln ergeben sich deren Höhen; angehoben wird auf das höchststehende Rad, das liegen bleibt. Aus dem Beispiel oben werden damit 12 cm vorne rechts, 8 cm hinten rechts, 4 cm vorne links und nichts hinten links – dieselbe Geometrie, nur richtig zusammengesetzt. Angezeigt wird das als Draufsicht auf das Fahrzeug (WheelLiftPlan), auf iPhone und Uhr dieselbe Ansicht, auf der Uhr kompakt. LevelingWedge entfällt. Abgesichert in run-tests.sh: reine Querneigung, reine Längsneigung, beides zusammen (16,1 / 9,2 / 7,0 / 0 cm), gespiegelte Vorzeichen und die Fälle ohne Fahrzeugmasse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -129,55 +129,85 @@ struct AlignmentAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wie hoch ein Auffahrkeil sein muss, um eine Neigung auszugleichen.
|
||||
/// Wie hoch jedes einzelne Rad unterlegt werden muss.
|
||||
///
|
||||
/// Rein geometrisch und damit exakt: Höhe = tan(Winkel) × Abstand der Achsen
|
||||
/// beziehungsweise der Räder.
|
||||
struct LevelingWedge: Equatable {
|
||||
/// Wo der Keil hin muss.
|
||||
let side: Side
|
||||
/// Höhe in Metern.
|
||||
let height: Double
|
||||
/// Der zugrundeliegende Winkel in Grad.
|
||||
let angle: Double
|
||||
/// Getrennte Angaben wie „rechts 8 cm" und „vorne 4 cm" beschreiben dasselbe
|
||||
/// Fahrzeug und lassen sich nicht getrennt ausführen: Unter „rechts" liegen
|
||||
/// zwei Räder, unter „vorne" auch, und zwei davon sind dieselben. Gefragt ist
|
||||
/// deshalb die Höhe **je Rad**.
|
||||
///
|
||||
/// Das ist reine Geometrie und damit exakt. Das Fahrzeug steht auf vier
|
||||
/// Punkten; deren Höhen ergeben sich aus Spurweite, Radstand und den beiden
|
||||
/// Winkeln. Angehoben wird auf die Höhe des höchsten Rades – das bleibt
|
||||
/// liegen, die anderen bekommen die Differenz.
|
||||
struct LevelingLift: Equatable {
|
||||
|
||||
enum Side: Equatable {
|
||||
case front, rear, left, right
|
||||
enum Wheel: CaseIterable, Hashable {
|
||||
case frontLeft, frontRight, rearLeft, rearRight
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .front: return "vorne"
|
||||
case .rear: return "hinten"
|
||||
case .left: return "links"
|
||||
case .right: return "rechts"
|
||||
case .frontLeft: return "vorne links"
|
||||
case .frontRight: return "vorne rechts"
|
||||
case .rearLeft: return "hinten links"
|
||||
case .rearRight: return "hinten rechts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var heightInCentimetres: Double { height * 100 }
|
||||
/// Höhen in Metern, je Rad. Das höchste Rad steht auf 0.
|
||||
let frontLeft: Double
|
||||
let frontRight: Double
|
||||
let rearLeft: Double
|
||||
let rearRight: Double
|
||||
|
||||
/// Quer: die tieferliegende Seite muss angehoben werden. Positiver Roll
|
||||
/// heisst, dass rechts höher steht – der Keil gehört also nach links.
|
||||
static func across(roll: Double, trackWidth: Double) -> LevelingWedge? {
|
||||
wedge(angle: roll, distance: trackWidth,
|
||||
whenPositive: .left, whenNegative: .right)
|
||||
func height(_ wheel: Wheel) -> Double {
|
||||
switch wheel {
|
||||
case .frontLeft: return frontLeft
|
||||
case .frontRight: return frontRight
|
||||
case .rearLeft: return rearLeft
|
||||
case .rearRight: return rearRight
|
||||
}
|
||||
}
|
||||
|
||||
/// Längs: positiver Pitch heisst, dass das Heck höher steht – der Keil
|
||||
/// gehört unter die Vorderräder.
|
||||
static func along(pitch: Double, wheelbase: Double) -> LevelingWedge? {
|
||||
wedge(angle: pitch, distance: wheelbase,
|
||||
whenPositive: .front, whenNegative: .rear)
|
||||
func centimetres(_ wheel: Wheel) -> Double { height(wheel) * 100 }
|
||||
|
||||
/// Das am tiefsten stehende Rad – dort liegt die grösste Höhe.
|
||||
var deepest: Wheel {
|
||||
Wheel.allCases.max { height($0) < height($1) } ?? .frontLeft
|
||||
}
|
||||
|
||||
private static func wedge(angle: Double,
|
||||
distance: Double,
|
||||
whenPositive: Side,
|
||||
whenNegative: Side) -> LevelingWedge? {
|
||||
guard distance > 0, abs(angle) > LevelState.levelTolerance else { return nil }
|
||||
let height = tan(abs(angle) * .pi / 180) * distance
|
||||
return LevelingWedge(side: angle > 0 ? whenPositive : whenNegative,
|
||||
height: height,
|
||||
angle: abs(angle))
|
||||
var maximumCentimetres: Double { centimetres(deepest) }
|
||||
|
||||
/// Unter einem Zentimeter lohnt kein Keil; so genau steht kein Fahrzeug,
|
||||
/// und so genau misst der Sensor auch nicht.
|
||||
var isNegligible: Bool { maximumCentimetres < 1 }
|
||||
|
||||
/// `pitch` positiv heisst, das Heck steht höher; `roll` positiv, die rechte
|
||||
/// Seite steht höher. Ohne Fahrzeugmasse gibt es nichts zu rechnen.
|
||||
static func compute(pitch: Double,
|
||||
roll: Double,
|
||||
trackWidth: Double,
|
||||
wheelbase: Double) -> LevelingLift? {
|
||||
guard trackWidth > 0, wheelbase > 0 else { return nil }
|
||||
|
||||
// Wie viel höher die rechte Seite und wie viel höher das Heck steht.
|
||||
// Vorzeichen inbegriffen: tan(-2°) ist negativ, dann steht links höher.
|
||||
let rightRise = tan(roll * .pi / 180) * trackWidth
|
||||
let rearRise = tan(pitch * .pi / 180) * wheelbase
|
||||
|
||||
// Höhen der vier Aufstandspunkte, bezogen auf vorne links.
|
||||
let heights: [Wheel: Double] = [
|
||||
.frontLeft: 0,
|
||||
.frontRight: rightRise,
|
||||
.rearLeft: rearRise,
|
||||
.rearRight: rightRise + rearRise,
|
||||
]
|
||||
let top = heights.values.max() ?? 0
|
||||
|
||||
return LevelingLift(frontLeft: top - heights[.frontLeft]!,
|
||||
frontRight: top - heights[.frontRight]!,
|
||||
rearLeft: top - heights[.rearLeft]!,
|
||||
rearRight: top - heights[.rearRight]!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Die Keilhöhen als Draufsicht auf das Fahrzeug.
|
||||
///
|
||||
/// Vier Zahlen an vier Rädern, statt zweier Angaben „rechts" und „vorne", die
|
||||
/// sich nicht getrennt ausführen lassen. Oben ist vorne; links im Bild ist
|
||||
/// links am Fahrzeug, aus Sicht des Fahrers.
|
||||
///
|
||||
/// Das höchststehende Rad bekommt nichts – es bleibt liegen, die anderen
|
||||
/// werden auf seine Höhe gebracht.
|
||||
struct WheelLiftPlan: View {
|
||||
let lift: LevelingLift
|
||||
/// Für die Uhr: kleinere Schrift, engerer Aufbau.
|
||||
var isCompact = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: isCompact ? 2 : 6) {
|
||||
Text("vorne")
|
||||
.font(labelFont)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack(spacing: isCompact ? 6 : 12) {
|
||||
VStack(spacing: isCompact ? 6 : 14) {
|
||||
badge(.frontLeft)
|
||||
badge(.rearLeft)
|
||||
}
|
||||
body_
|
||||
VStack(spacing: isCompact ? 6 : 14) {
|
||||
badge(.frontRight)
|
||||
badge(.rearRight)
|
||||
}
|
||||
}
|
||||
|
||||
Text("hinten")
|
||||
.font(labelFont)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(accessibilityText)
|
||||
}
|
||||
|
||||
/// Der Umriss dazwischen macht aus vier Zahlen ein Fahrzeug.
|
||||
private var body_: some View {
|
||||
RoundedRectangle(cornerRadius: isCompact ? 4 : 8)
|
||||
.strokeBorder(Color.secondary.opacity(0.4), lineWidth: 1)
|
||||
.frame(width: isCompact ? 26 : 48, height: isCompact ? 44 : 84)
|
||||
}
|
||||
|
||||
private func badge(_ wheel: LevelingLift.Wheel) -> some View {
|
||||
let value = lift.centimetres(wheel)
|
||||
let needed = value >= 1
|
||||
return Text(needed ? String(format: "%.0f", value) : "–")
|
||||
.font(valueFont)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(needed ? .primary : .secondary)
|
||||
.frame(width: isCompact ? 26 : 42, height: isCompact ? 20 : 30)
|
||||
.background(needed ? Color.orange.opacity(0.25) : Color.secondary.opacity(0.12),
|
||||
in: .rect(cornerRadius: isCompact ? 4 : 6))
|
||||
}
|
||||
|
||||
private var labelFont: Font { isCompact ? .system(size: 9) : .caption2 }
|
||||
private var valueFont: Font {
|
||||
isCompact ? .system(size: 12, weight: .semibold) : .callout.weight(.semibold)
|
||||
}
|
||||
|
||||
private var accessibilityText: String {
|
||||
let parts = LevelingLift.Wheel.allCases.compactMap { wheel -> String? in
|
||||
let value = lift.centimetres(wheel)
|
||||
guard value >= 1 else { return nil }
|
||||
return String(format: "%@ %.0f Zentimeter", wheel.text, value)
|
||||
}
|
||||
return parts.isEmpty ? "Keine Keile nötig" : "Unterlegen: " + parts.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user