Add auswählbare Fahrzeuggrafik am Neigungsmesser, neues California-Set
Jedes Grafikset (Seiten-/Heckansicht) ist jetzt ein VehicleGraphicStyle mit eigenem Seitenverhältnis, wählbar in der Neigungsmesser-Einrichtung und angewendet in Fahrzeug-Ansicht und Ausrichtungs-Assistent. Die bisherigen VehicleSide/VehicleRear-Assets heißen jetzt VansterSide/ VansterRear, damit sie eindeutig ihrem Set zuzuordnen sind; California kommt als zweites Set mit eigener Seitenansicht dazu (Heckansicht vorerst von Vanster übernommen). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
After Width: | Height: | Size: 341 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "CaliforniaSide.png",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 },
|
||||||
|
"properties" : { "template-rendering-intent" : "template" }
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "VehicleRear.png",
|
"filename" : "VansterRear.png",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "VehicleSide.png",
|
"filename" : "VansterSide.png",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 253 KiB |
@@ -35,7 +35,8 @@ struct AlignmentAssistantView: View {
|
|||||||
LevelBubble(pitch: state.pitch, roll: state.roll)
|
LevelBubble(pitch: state.pitch, roll: state.roll)
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
case .vehicle:
|
case .vehicle:
|
||||||
VehicleTiltView(pitch: state.pitch, roll: state.roll)
|
VehicleTiltView(pitch: state.pitch, roll: state.roll,
|
||||||
|
style: device.vehicleGraphicStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isLive { disconnectedBanner }
|
if !isLive { disconnectedBanner }
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ struct LevelSetupView: View {
|
|||||||
store.devices.first { $0.id == device.id } ?? device
|
store.devices.first { $0.id == device.id } ?? device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let graphicColumns = [GridItem(.adaptive(minimum: 76), spacing: 12)]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
@@ -80,6 +82,44 @@ struct LevelSetupView: View {
|
|||||||
} footer: {
|
} footer: {
|
||||||
Text(calibrationHint)
|
Text(calibrationHint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
LazyVGrid(columns: graphicColumns, spacing: 12) {
|
||||||
|
ForEach(VehicleGraphicStyle.allCases) { style in
|
||||||
|
Button {
|
||||||
|
var updated = currentDevice
|
||||||
|
updated.vehicleGraphicStyle = style
|
||||||
|
store.update(updated)
|
||||||
|
} label: {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(style.sideImageName)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.padding(10)
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.background(currentDevice.vehicleGraphicStyle == style
|
||||||
|
? Color.accentColor.opacity(0.18)
|
||||||
|
: Color.secondary.opacity(0.08),
|
||||||
|
in: .rect(cornerRadius: 12))
|
||||||
|
.foregroundStyle(currentDevice.vehicleGraphicStyle == style
|
||||||
|
? Color.accentColor : Color.primary)
|
||||||
|
Text(style.title)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(currentDevice.vehicleGraphicStyle == style
|
||||||
|
? Color.accentColor : Color.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel(style.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
} header: {
|
||||||
|
Text("Fahrzeuggrafik")
|
||||||
|
} footer: {
|
||||||
|
Text("Wird in der Fahrzeug-Darstellung des Neigungsmessers verwendet. "
|
||||||
|
+ "Weitere Grafiken folgen.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Neigungsmesser einrichten")
|
.navigationTitle("Neigungsmesser einrichten")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ struct LevelControls: View {
|
|||||||
LevelBubble(pitch: state.pitch, roll: state.roll)
|
LevelBubble(pitch: state.pitch, roll: state.roll)
|
||||||
.frame(maxHeight: 220)
|
.frame(maxHeight: 220)
|
||||||
case .vehicle:
|
case .vehicle:
|
||||||
VehicleTiltView(pitch: state.pitch, roll: state.roll)
|
VehicleTiltView(pitch: state.pitch, roll: state.roll,
|
||||||
|
style: device.vehicleGraphicStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let instruction = state.instruction {
|
if let instruction = state.instruction {
|
||||||
|
|||||||
@@ -15,22 +15,23 @@ import SwiftUI
|
|||||||
struct VehicleTiltView: View {
|
struct VehicleTiltView: View {
|
||||||
let pitch: Double?
|
let pitch: Double?
|
||||||
let roll: Double?
|
let roll: Double?
|
||||||
|
var style: VehicleGraphicStyle = .vanster
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
tiltPanel(image: "VehicleSide",
|
tiltPanel(image: style.sideImageName,
|
||||||
angle: pitch,
|
angle: pitch,
|
||||||
title: "Längs",
|
title: "Längs",
|
||||||
lowerLabel: "Front",
|
lowerLabel: "Front",
|
||||||
upperLabel: "Heck",
|
upperLabel: "Heck",
|
||||||
aspect: VehicleTilt.sideAspect)
|
aspect: style.sideAspect)
|
||||||
|
|
||||||
tiltPanel(image: "VehicleRear",
|
tiltPanel(image: style.rearImageName,
|
||||||
angle: roll,
|
angle: roll,
|
||||||
title: "Quer",
|
title: "Quer",
|
||||||
lowerLabel: "links",
|
lowerLabel: "links",
|
||||||
upperLabel: "rechts",
|
upperLabel: "rechts",
|
||||||
aspect: VehicleTilt.rearAspect)
|
aspect: style.rearAspect)
|
||||||
|
|
||||||
// Ohne diesen Hinweis nähme man den Bildwinkel für den echten.
|
// Ohne diesen Hinweis nähme man den Bildwinkel für den echten.
|
||||||
Text(String(format: "Neigung %.0f-fach überhöht dargestellt – "
|
Text(String(format: "Neigung %.0f-fach überhöht dargestellt – "
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "VehicleRear.png",
|
"filename" : "VansterRear.png",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "VehicleSide.png",
|
"filename" : "VansterSide.png",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
@@ -14,9 +14,9 @@ struct WatchVehicleView: View {
|
|||||||
/// Die angeschriebenen Gradzahlen bleiben echt.
|
/// Die angeschriebenen Gradzahlen bleiben echt.
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
panel(image: "VehicleSide", angle: pitch, title: "Längs",
|
panel(image: "VansterSide", angle: pitch, title: "Längs",
|
||||||
lower: "Front", upper: "Heck", aspect: VehicleTilt.sideAspect)
|
lower: "Front", upper: "Heck", aspect: VehicleTilt.sideAspect)
|
||||||
panel(image: "VehicleRear", angle: roll, title: "Quer",
|
panel(image: "VansterRear", angle: roll, title: "Quer",
|
||||||
lower: "links", upper: "rechts", aspect: VehicleTilt.rearAspect)
|
lower: "links", upper: "rechts", aspect: VehicleTilt.rearAspect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ struct ConfiguredDevice: Identifiable, Codable, Hashable, Sendable {
|
|||||||
//var fridgeZoneMode: FridgeZoneMode = .automatic
|
//var fridgeZoneMode: FridgeZoneMode = .automatic
|
||||||
/// Nur für den Neigungsmesser: wie er im Fahrzeug sitzt.
|
/// Nur für den Neigungsmesser: wie er im Fahrzeug sitzt.
|
||||||
var sensorOrientation = SensorOrientation.identity
|
var sensorOrientation = SensorOrientation.identity
|
||||||
|
/// Nur für den Neigungsmesser: welche Fahrzeuggrafik die Fahrzeug-Ansicht zeigt.
|
||||||
|
var vehicleGraphicStyle = VehicleGraphicStyle.vanster
|
||||||
|
|
||||||
init(id: UUID = UUID(),
|
init(id: UUID = UUID(),
|
||||||
name: String,
|
name: String,
|
||||||
@@ -135,5 +137,7 @@ struct ConfiguredDevice: Identifiable, Codable, Hashable, Sendable {
|
|||||||
// ?? .automatic
|
// ?? .automatic
|
||||||
sensorOrientation = try container.decodeIfPresent(SensorOrientation.self,
|
sensorOrientation = try container.decodeIfPresent(SensorOrientation.self,
|
||||||
forKey: .sensorOrientation) ?? .identity
|
forKey: .sensorOrientation) ?? .identity
|
||||||
|
vehicleGraphicStyle = try container.decodeIfPresent(VehicleGraphicStyle.self,
|
||||||
|
forKey: .vehicleGraphicStyle) ?? .vanster
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Welche Silhouette die Fahrzeug-Ansicht des Neigungsmessers zeigt.
|
||||||
|
///
|
||||||
|
/// Jeder Fall ist ein zusammengehöriges Bilderset aus Seiten- und
|
||||||
|
/// Heckansicht in Assets.xcassets, beide als Vorlage ("template")
|
||||||
|
/// exportiert, damit `foregroundStyle` sie einfärben kann. Im Picker wird
|
||||||
|
/// nur die Seitenansicht gezeigt.
|
||||||
|
enum VehicleGraphicStyle: String, CaseIterable, Identifiable, Codable, Sendable {
|
||||||
|
case vanster
|
||||||
|
case california
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .vanster: return "Vanster"
|
||||||
|
case .california: return "California"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sideImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .vanster: return "VansterSide"
|
||||||
|
case .california: return "CaliforniaSide"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Für California liegt noch keine eigene Heckansicht vor, deshalb
|
||||||
|
/// vorerst die von Vanster.
|
||||||
|
var rearImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .vanster, .california: return "VansterRear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seitenverhältnis der jeweiligen Bilder – ohne das würde eine Grafik
|
||||||
|
/// mit anderen Proportionen in der gemeinsamen Bildhülle verzerrt oder
|
||||||
|
/// ungewollt verkleinert erscheinen.
|
||||||
|
var sideAspect: Double {
|
||||||
|
switch self {
|
||||||
|
case .vanster: return VehicleTilt.sideAspect
|
||||||
|
case .california: return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rearAspect: Double {
|
||||||
|
switch self {
|
||||||
|
case .vanster, .california: return VehicleTilt.rearAspect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,5 +32,11 @@ gefüllte Form, in der die Details erhalten bleiben.
|
|||||||
```bash
|
```bash
|
||||||
swiftc -O -o /tmp/make-vehicle-art Tools/make-vehicle-art.swift
|
swiftc -O -o /tmp/make-vehicle-art Tools/make-vehicle-art.swift
|
||||||
sips --resampleHeight 600 heckansicht.png --out /tmp/rear.png
|
sips --resampleHeight 600 heckansicht.png --out /tmp/rear.png
|
||||||
/tmp/make-vehicle-art /tmp/rear.png CamperMonitor/Assets.xcassets/VehicleRear.imageset/VehicleRear.png
|
/tmp/make-vehicle-art /tmp/rear.png CamperMonitor/Assets.xcassets/VansterRear.imageset/VansterRear.png
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Jedes Fahrzeug-Grafikset (siehe `VehicleGraphicStyle`) hat sein eigenes
|
||||||
|
Bildpaar `<Name>Side`/`<Name>Rear`. Hat die Vorlage schon einen
|
||||||
|
transparenten Hintergrund, geht die Quelldatei direkt ins Skript. Liegt sie
|
||||||
|
auf weißem Grund, muss der Rand vorher weg – sonst wird die ganze Fläche
|
||||||
|
mitgefärbt.
|
||||||
|
|||||||