Add help view and refine AR tank placement
This commit is contained in:
@ -295,6 +295,7 @@
|
|||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = GasTankAR/Info.plist;
|
INFOPLIST_FILE = GasTankAR/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = DrachenGas;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
|
||||||
INFOPLIST_KEY_NSCameraUsageDescription = "Die Kamera wird benötigt, um Gastanks im AR-Modus auf dem Grundstück zu visualisieren.";
|
INFOPLIST_KEY_NSCameraUsageDescription = "Die Kamera wird benötigt, um Gastanks im AR-Modus auf dem Grundstück zu visualisieren.";
|
||||||
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Der Standort wird verwendet, um den Aufnahmeort von AR-Fotos in der Galerie anzuzeigen.";
|
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Der Standort wird verwendet, um den Aufnahmeort von AR-Fotos in der Galerie anzuzeigen.";
|
||||||
@ -304,7 +305,7 @@
|
|||||||
INFOPLIST_KEY_UIStatusBarHidden = YES;
|
INFOPLIST_KEY_UIStatusBarHidden = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 27.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -314,6 +315,10 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@ -335,6 +340,7 @@
|
|||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = GasTankAR/Info.plist;
|
INFOPLIST_FILE = GasTankAR/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = DrachenGas;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
|
||||||
INFOPLIST_KEY_NSCameraUsageDescription = "Die Kamera wird benötigt, um Gastanks im AR-Modus auf dem Grundstück zu visualisieren.";
|
INFOPLIST_KEY_NSCameraUsageDescription = "Die Kamera wird benötigt, um Gastanks im AR-Modus auf dem Grundstück zu visualisieren.";
|
||||||
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Der Standort wird verwendet, um den Aufnahmeort von AR-Fotos in der Galerie anzuzeigen.";
|
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Der Standort wird verwendet, um den Aufnahmeort von AR-Fotos in der Galerie anzuzeigen.";
|
||||||
@ -344,7 +350,7 @@
|
|||||||
INFOPLIST_KEY_UIStatusBarHidden = YES;
|
INFOPLIST_KEY_UIStatusBarHidden = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 27.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -354,6 +360,10 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "Vertrieb-AppIcon-1024.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
@ -12,6 +13,7 @@
|
|||||||
"value" : "dark"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"filename" : "Vertrieb-AppIcon-Dark-1024.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
@ -23,6 +25,7 @@
|
|||||||
"value" : "tinted"
|
"value" : "tinted"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"filename" : "Vertrieb-AppIcon-Tinted-1024.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 825 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 964 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 839 KiB |
@ -15,7 +15,9 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
#Preview {
|
#Preview {
|
||||||
ProjectListView()
|
ProjectListView()
|
||||||
.modelContainer(PreviewSupport.container)
|
.modelContainer(PreviewSupport.container)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
BIN
GasTankAR/Icon/Vertrieb-AppIcon-1024.png
Normal file
BIN
GasTankAR/Icon/Vertrieb-AppIcon-1024.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 825 KiB |
BIN
GasTankAR/Icon/Vertrieb-AppIcon-Dark-1024.png
Normal file
BIN
GasTankAR/Icon/Vertrieb-AppIcon-Dark-1024.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 964 KiB |
BIN
GasTankAR/Icon/Vertrieb-AppIcon-Tinted-1024.png
Normal file
BIN
GasTankAR/Icon/Vertrieb-AppIcon-Tinted-1024.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 839 KiB |
@ -16,6 +16,10 @@ nonisolated struct TankDefinition: Codable, Hashable, Identifiable {
|
|||||||
let placement: TankPlacement
|
let placement: TankPlacement
|
||||||
/// Pfad bzw. Dateiname des 3D-Modells im App-Bundle (z. B. `DG-2700_wh.usdz`).
|
/// Pfad bzw. Dateiname des 3D-Modells im App-Bundle (z. B. `DG-2700_wh.usdz`).
|
||||||
let modelPath: String
|
let modelPath: String
|
||||||
|
/// X-Koordinate der Domschachtmitte im lokalen Modellraum (Meter).
|
||||||
|
/// Aus der oberen Schachtkante: (kleinstes X + größtes X) / 2.
|
||||||
|
/// Die Tank-Längsachse der gelieferten USDZ-Modelle liegt auf X.
|
||||||
|
let domeCenterX: Float
|
||||||
|
|
||||||
/// Stabile Identität aus der Kombination der Auswahlmerkmale.
|
/// Stabile Identität aus der Kombination der Auswahlmerkmale.
|
||||||
var id: String { "\(size.rawValue)_\(color.rawValue)_\(placement.rawValue)" }
|
var id: String { "\(size.rawValue)_\(color.rawValue)_\(placement.rawValue)" }
|
||||||
|
|||||||
@ -10,14 +10,14 @@
|
|||||||
"olive": "Olivgrün"
|
"olive": "Olivgrün"
|
||||||
},
|
},
|
||||||
"tanks": [
|
"tanks": [
|
||||||
{ "size": "t1_2", "color": "white", "placement": "aboveGround", "modelPath": "DG-2700_wh.usdz" },
|
{ "size": "t1_2", "color": "white", "placement": "aboveGround", "modelPath": "DG-2700_wh.usdz", "domeCenterX": -0.426 },
|
||||||
{ "size": "t1_2", "color": "green", "placement": "aboveGround", "modelPath": "DG-2700_mi.usdz" },
|
{ "size": "t1_2", "color": "green", "placement": "aboveGround", "modelPath": "DG-2700_mi.usdz", "domeCenterX": -0.426 },
|
||||||
{ "size": "t1_2", "color": "olive", "placement": "belowGround", "modelPath": "DG-2700_ol.usdz" },
|
{ "size": "t1_2", "color": "olive", "placement": "belowGround", "modelPath": "DG-2700_ol.usdz", "domeCenterX": -0.498 },
|
||||||
{ "size": "t2_1", "color": "white", "placement": "aboveGround", "modelPath": "DG-4850_wh.usdz" },
|
{ "size": "t2_1", "color": "white", "placement": "aboveGround", "modelPath": "DG-4850_wh.usdz", "domeCenterX": -1.317 },
|
||||||
{ "size": "t2_1", "color": "green", "placement": "aboveGround", "modelPath": "DG-4850_mi.usdz" },
|
{ "size": "t2_1", "color": "green", "placement": "aboveGround", "modelPath": "DG-4850_mi.usdz", "domeCenterX": -1.317 },
|
||||||
{ "size": "t2_1", "color": "olive", "placement": "belowGround", "modelPath": "DG-4850_ol.usdz" },
|
{ "size": "t2_1", "color": "olive", "placement": "belowGround", "modelPath": "DG-4850_ol.usdz", "domeCenterX": -1.395 },
|
||||||
{ "size": "t2_9", "color": "white", "placement": "aboveGround", "modelPath": "DG-6400_wh.usdz" },
|
{ "size": "t2_9", "color": "white", "placement": "aboveGround", "modelPath": "DG-6400_wh.usdz", "domeCenterX": -1.939 },
|
||||||
{ "size": "t2_9", "color": "green", "placement": "aboveGround", "modelPath": "DG-6400_mi.usdz" },
|
{ "size": "t2_9", "color": "green", "placement": "aboveGround", "modelPath": "DG-6400_mi.usdz", "domeCenterX": -1.939 },
|
||||||
{ "size": "t2_9", "color": "olive", "placement": "belowGround", "modelPath": "DG-6400_ol.usdz" }
|
{ "size": "t2_9", "color": "olive", "placement": "belowGround", "modelPath": "DG-6400_ol.usdz", "domeCenterX": -2.018 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -117,6 +117,7 @@ struct DocuframeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
#Preview {
|
#Preview {
|
||||||
let sample = PreviewSupport.galleryProjectContainer()
|
let sample = PreviewSupport.galleryProjectContainer()
|
||||||
return NavigationStack {
|
return NavigationStack {
|
||||||
@ -124,3 +125,4 @@ struct DocuframeView: View {
|
|||||||
}
|
}
|
||||||
.modelContainer(sample.container)
|
.modelContainer(sample.container)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@ -607,6 +607,8 @@ private struct PhotoLocationCard: View {
|
|||||||
String(format: "%.5f, %.5f", coordinate.latitude, coordinate.longitude)
|
String(format: "%.5f, %.5f", coordinate.latitude, coordinate.longitude)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
#Preview("Galerie") {
|
#Preview("Galerie") {
|
||||||
let sample = PreviewSupport.galleryProjectContainer()
|
let sample = PreviewSupport.galleryProjectContainer()
|
||||||
return NavigationStack {
|
return NavigationStack {
|
||||||
@ -619,3 +621,4 @@ private struct PhotoLocationCard: View {
|
|||||||
let photo = PreviewSupport.samplePhoto()
|
let photo = PreviewSupport.samplePhoto()
|
||||||
PhotoViewer(photo: photo, photos: [photo]) {}
|
PhotoViewer(photo: photo, photos: [photo]) {}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
95
GasTankAR/Views/HelpView.swift
Normal file
95
GasTankAR/Views/HelpView.swift
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct HelpView: View {
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
HelpAppInfoSection()
|
||||||
|
|
||||||
|
HelpTopicSection(
|
||||||
|
title: "Projekte und Tankauswahl",
|
||||||
|
systemImage: "folder",
|
||||||
|
explanation: "Tippe in der Projektübersicht auf +, um ein Projekt anzulegen. Öffne ein Projekt, um seine Daten anzusehen und den gewünschten Tank auszuwählen. Über „AR-Modus starten“ wechselst du zur Darstellung in der realen Umgebung."
|
||||||
|
)
|
||||||
|
|
||||||
|
HelpTopicSection(
|
||||||
|
title: "Tank in der Umgebung platzieren",
|
||||||
|
systemImage: "arkit",
|
||||||
|
explanation: "Erlaube den Kamerazugriff und bewege das Gerät langsam, damit die Umgebung erkannt wird. Richte das Fadenkreuz auf den gewünschten Standort und tippe auf „Tank platzieren“. Beachte dabei die Hinweise auf dem Bildschirm."
|
||||||
|
)
|
||||||
|
|
||||||
|
HelpTopicSection(
|
||||||
|
title: "Tank anpassen",
|
||||||
|
systemImage: "move.3d",
|
||||||
|
explanation: "Tippe auf den platzierten Tank, um die Bedienelemente zu öffnen. Damit kannst du seine Position, Höhe und Drehung anpassen oder ihn neu platzieren."
|
||||||
|
)
|
||||||
|
|
||||||
|
HelpTopicSection(
|
||||||
|
title: "Werkzeuge im AR-Modus",
|
||||||
|
systemImage: "ruler",
|
||||||
|
explanation: "Das Schild-Symbol blendet die Sicherheitsabstände ein oder aus. Über das Lineal öffnest du die Funktion zum Ausmessen. Mit „Rohrführung“ kannst du einen Leitungsverlauf darstellen. Folge für die einzelnen Schritte den Hinweisen im AR-Modus."
|
||||||
|
)
|
||||||
|
|
||||||
|
HelpTopicSection(
|
||||||
|
title: "Fotos und Galerie",
|
||||||
|
systemImage: "camera",
|
||||||
|
explanation: "Mit dem Kamera-Symbol im AR-Modus nimmst du die aktuelle Ansicht auf. Die gespeicherten Aufnahmen findest du im jeweiligen Projekt im Reiter „Galerie“."
|
||||||
|
)
|
||||||
|
|
||||||
|
HelpTopicSection(
|
||||||
|
title: "Darstellung und Erkennung",
|
||||||
|
systemImage: "gearshape",
|
||||||
|
explanation: "In den Einstellungen kannst du auf unterstützten Geräten LiDAR-Funktionen aktivieren und die Belichtung der oberirdischen Tanks anpassen. Änderungen werden beim nächsten Start des AR-Modus übernommen. Falls die Umgebung nicht erkannt wird, bewege das Gerät langsam weiter und achte auf gute Beleuchtung und erkennbare Strukturen."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.navigationTitle("Hilfe")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct HelpAppInfoSection: View {
|
||||||
|
private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
|
||||||
|
private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section("Über die App") {
|
||||||
|
Text("GasTankAR")
|
||||||
|
.font(.headline)
|
||||||
|
Text("Mit GasTankAR kannst du Gastanks in deiner realen Umgebung visualisieren und die Ansichten in Projekten festhalten.")
|
||||||
|
LabeledContent("Version") {
|
||||||
|
if let version, !version.isEmpty {
|
||||||
|
Text(verbatim: version)
|
||||||
|
} else {
|
||||||
|
Text("Nicht verfügbar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let build, !build.isEmpty {
|
||||||
|
LabeledContent("Build") {
|
||||||
|
Text(verbatim: build)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text("Diese App wurde von der Firma SoftVisor GmbH erstellt.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct HelpTopicSection: View {
|
||||||
|
let title: LocalizedStringKey
|
||||||
|
let systemImage: String
|
||||||
|
let explanation: LocalizedStringKey
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
Text(explanation)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
} header: {
|
||||||
|
Label(title, systemImage: systemImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationStack {
|
||||||
|
HelpView()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -58,6 +58,7 @@ struct ProjectDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
#Preview {
|
#Preview {
|
||||||
let sample = PreviewSupport.singleProjectContainer()
|
let sample = PreviewSupport.singleProjectContainer()
|
||||||
|
|
||||||
@ -66,3 +67,4 @@ struct ProjectDetailView: View {
|
|||||||
}
|
}
|
||||||
.modelContainer(sample.container)
|
.modelContainer(sample.container)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@ -90,6 +90,7 @@ struct ProjectListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
#Preview("Gefüllt") {
|
#Preview("Gefüllt") {
|
||||||
ProjectListView()
|
ProjectListView()
|
||||||
.modelContainer(PreviewSupport.container)
|
.modelContainer(PreviewSupport.container)
|
||||||
@ -99,3 +100,4 @@ struct ProjectListView: View {
|
|||||||
ProjectListView()
|
ProjectListView()
|
||||||
.modelContainer(PreviewSupport.makeEmptyContainer())
|
.modelContainer(PreviewSupport.makeEmptyContainer())
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@ -8,15 +8,30 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ARKit
|
import ARKit
|
||||||
|
|
||||||
/// Zentraler Schlüssel für die LiDAR-Einstellung (in AppStorage/UserDefaults).
|
/// Zentrale Schlüssel für AppStorage/UserDefaults.
|
||||||
enum SettingsKeys {
|
enum SettingsKeys {
|
||||||
static let lidarFeaturesEnabled = "lidarFeaturesEnabled"
|
static let lidarFeaturesEnabled = "lidarFeaturesEnabled"
|
||||||
static let arExposure = "arExposure"
|
static let arExposure = "arExposure"
|
||||||
|
static let devTankResizingEnabled = "devTankResizingEnabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eine gespeicherte Entwickler-Option darf in Release niemals die Tankgröße ändern.
|
||||||
|
enum TankResizePolicy {
|
||||||
|
static func isEnabled(devFlag: Bool) -> Bool {
|
||||||
|
#if DEBUG
|
||||||
|
devFlag
|
||||||
|
#else
|
||||||
|
false
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@AppStorage(SettingsKeys.lidarFeaturesEnabled) private var lidarEnabled = false
|
@AppStorage(SettingsKeys.lidarFeaturesEnabled) private var lidarEnabled = false
|
||||||
@AppStorage(SettingsKeys.arExposure) private var arExposure = 1.0
|
@AppStorage(SettingsKeys.arExposure) private var arExposure = 1.0
|
||||||
|
#if DEBUG
|
||||||
|
@AppStorage(SettingsKeys.devTankResizingEnabled) private var devTankResizingEnabled = false
|
||||||
|
#endif
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
/// LiDAR ist verfügbar, wenn das Gerät Scene-Reconstruction (Mesh) unterstützt.
|
/// LiDAR ist verfügbar, wenn das Gerät Scene-Reconstruction (Mesh) unterstützt.
|
||||||
@ -56,6 +71,22 @@ struct SettingsView: View {
|
|||||||
} footer: {
|
} footer: {
|
||||||
Text("Passt die zusätzliche Beleuchtung der oberirdischen Tanks an. Änderungen greifen beim nächsten Start des AR-Modus.")
|
Text("Passt die zusätzliche Beleuchtung der oberirdischen Tanks an. Änderungen greifen beim nächsten Start des AR-Modus.")
|
||||||
}
|
}
|
||||||
|
#if DEBUG
|
||||||
|
Section {
|
||||||
|
Toggle("Tankgröße per Geste ändern", isOn: $devTankResizingEnabled)
|
||||||
|
} header: {
|
||||||
|
Text("Entwicklung")
|
||||||
|
} footer: {
|
||||||
|
Text("Nur im Debug-Build verfügbar. Die Änderung greift beim nächsten Start des AR-Modus.")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Section {
|
||||||
|
NavigationLink {
|
||||||
|
HelpView()
|
||||||
|
} label: {
|
||||||
|
Label("Hilfe", systemImage: "questionmark.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Einstellungen")
|
.navigationTitle("Einstellungen")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
|||||||
@ -15,6 +15,9 @@ struct ARExperienceView: View {
|
|||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@AppStorage(SettingsKeys.lidarFeaturesEnabled) private var lidarEnabled = false
|
@AppStorage(SettingsKeys.lidarFeaturesEnabled) private var lidarEnabled = false
|
||||||
@AppStorage(SettingsKeys.arExposure) private var arExposure = 1.0
|
@AppStorage(SettingsKeys.arExposure) private var arExposure = 1.0
|
||||||
|
#if DEBUG
|
||||||
|
@AppStorage(SettingsKeys.devTankResizingEnabled) private var devTankResizingEnabled = false
|
||||||
|
#endif
|
||||||
|
|
||||||
let project: Project
|
let project: Project
|
||||||
|
|
||||||
@ -95,10 +98,16 @@ struct ARExperienceView: View {
|
|||||||
.task {
|
.task {
|
||||||
guard controller == nil else { return }
|
guard controller == nil else { return }
|
||||||
let persistence = ProjectPersistenceAdapter(project: project, modelContext: modelContext)
|
let persistence = ProjectPersistenceAdapter(project: project, modelContext: modelContext)
|
||||||
|
#if DEBUG
|
||||||
|
let devResizing = devTankResizingEnabled
|
||||||
|
#else
|
||||||
|
let devResizing = false
|
||||||
|
#endif
|
||||||
controller = ARSessionController(
|
controller = ARSessionController(
|
||||||
persistence: persistence,
|
persistence: persistence,
|
||||||
useLiDAR: lidarEnabled,
|
useLiDAR: lidarEnabled,
|
||||||
arExposure: arExposure
|
arExposure: arExposure,
|
||||||
|
devTankResizingEnabled: devResizing
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,8 @@ final class ARSessionController: NSObject, ARSessionDelegate, ARCoachingOverlayV
|
|||||||
}
|
}
|
||||||
/// Ob die Sicherheitsabstände (Ringe um den Tank) eingeblendet sind.
|
/// Ob die Sicherheitsabstände (Ringe um den Tank) eingeblendet sind.
|
||||||
private(set) var showSafetyZones = false
|
private(set) var showSafetyZones = false
|
||||||
|
/// Nur für Debug-Builds mit eingeschalteter Entwickler-Option.
|
||||||
|
let allowsTankResizing: Bool
|
||||||
/// Wird bei jedem erfolgreichen Foto erhöht – die UI nutzt das für ein kurzes Aufblitzen.
|
/// Wird bei jedem erfolgreichen Foto erhöht – die UI nutzt das für ein kurzes Aufblitzen.
|
||||||
private(set) var snapshotFlashCounter = 0
|
private(set) var snapshotFlashCounter = 0
|
||||||
/// Angelegte Strecken (nur für die laufende Sitzung gehalten).
|
/// Angelegte Strecken (nur für die laufende Sitzung gehalten).
|
||||||
@ -127,9 +129,15 @@ final class ARSessionController: NSObject, ARSessionDelegate, ARCoachingOverlayV
|
|||||||
private static let anchorDetectionTimeout: Duration = .seconds(20)
|
private static let anchorDetectionTimeout: Duration = .seconds(20)
|
||||||
private static let relocationTimeout: Duration = .seconds(15)
|
private static let relocationTimeout: Duration = .seconds(15)
|
||||||
|
|
||||||
init(persistence: ARProjectPersisting, useLiDAR: Bool, arExposure: Double = 1.0) {
|
init(
|
||||||
|
persistence: ARProjectPersisting,
|
||||||
|
useLiDAR: Bool,
|
||||||
|
arExposure: Double = 1.0,
|
||||||
|
devTankResizingEnabled: Bool = false
|
||||||
|
) {
|
||||||
self.persistence = persistence
|
self.persistence = persistence
|
||||||
self.useLiDAR = useLiDAR
|
self.useLiDAR = useLiDAR
|
||||||
|
self.allowsTankResizing = TankResizePolicy.isEnabled(devFlag: devTankResizingEnabled)
|
||||||
self.showsPit = persistence.tank.placement == .belowGround && !persistence.tank.showLidOnly
|
self.showsPit = persistence.tank.placement == .belowGround && !persistence.tank.showLidOnly
|
||||||
self.entityFactory = TankEntityFactory(aboveGroundLightMultiplier: Float(arExposure))
|
self.entityFactory = TankEntityFactory(aboveGroundLightMultiplier: Float(arExposure))
|
||||||
self.configFactory = SessionConfigurationFactory(
|
self.configFactory = SessionConfigurationFactory(
|
||||||
@ -255,7 +263,7 @@ final class ARSessionController: NSObject, ARSessionDelegate, ARCoachingOverlayV
|
|||||||
|
|
||||||
/// Setzt die Tank-Skalierung auf die Originalgröße zurück.
|
/// Setzt die Tank-Skalierung auf die Originalgröße zurück.
|
||||||
func resetTankScale() {
|
func resetTankScale() {
|
||||||
guard let tankEntity else { return }
|
guard allowsTankResizing, let tankEntity else { return }
|
||||||
tankEntity.scale = SIMD3<Float>(repeating: 1)
|
tankEntity.scale = SIMD3<Float>(repeating: 1)
|
||||||
updateSafetyZones()
|
updateSafetyZones()
|
||||||
updateTankDimensionValues()
|
updateTankDimensionValues()
|
||||||
@ -967,6 +975,10 @@ final class ARSessionController: NSObject, ARSessionDelegate, ARCoachingOverlayV
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.transform = Transform(matrix: worldTransform)
|
container.transform = Transform(matrix: worldTransform)
|
||||||
|
// Ältere Projekte können eine gespeicherte Skalierung enthalten.
|
||||||
|
if !allowsTankResizing {
|
||||||
|
container.scale = SIMD3<Float>(repeating: 1)
|
||||||
|
}
|
||||||
updateDepthFadeGroundY()
|
updateDepthFadeGroundY()
|
||||||
updateSceneOcclusion()
|
updateSceneOcclusion()
|
||||||
updateSafetyZones()
|
updateSafetyZones()
|
||||||
@ -992,30 +1004,21 @@ final class ARSessionController: NSObject, ARSessionDelegate, ARCoachingOverlayV
|
|||||||
safetyZoneEntity = entity
|
safetyZoneEntity = entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Der Katalogwert ist im Modellraum gemessen. w = 0 vermeidet eine
|
||||||
|
// zweite Translation; Rotation und Skalierung des Tanks bleiben erhalten.
|
||||||
|
let domeCenterX = entityFactory.domeCenterX(for: persistence.tank)
|
||||||
|
let localOffset = SIMD4<Float>(domeCenterX, 0, 0, 0)
|
||||||
|
let transformedOffset = tankEntity.transformMatrix(relativeTo: tankAnchor) * localOffset
|
||||||
let tankPosition = tankEntity.position(relativeTo: tankAnchor)
|
let tankPosition = tankEntity.position(relativeTo: tankAnchor)
|
||||||
let localDomeOffset = SIMD4<Float>(0, 0, safetyZoneDomeOffset, 0)
|
entity.position = tankPosition + SIMD3(transformedOffset.x, 0, transformedOffset.z)
|
||||||
let tankTransform = tankEntity.transformMatrix(relativeTo: tankAnchor)
|
|
||||||
let domeOffset = tankTransform * localDomeOffset
|
|
||||||
|
|
||||||
entity.position = tankPosition + SIMD3(domeOffset.x, domeOffset.y, domeOffset.z)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Abstand vom Tankmittelpunkt zum seitlichen Domschacht entlang der lokalen
|
|
||||||
/// Tank-Längsachse (+Z), abgeleitet aus den Vorgabemaßen.
|
|
||||||
private var safetyZoneDomeOffset: Float {
|
|
||||||
switch persistence.tank.size {
|
|
||||||
case .t1_2:
|
|
||||||
return 2.460 / 2
|
|
||||||
case .t2_1:
|
|
||||||
return 4.255 / 2
|
|
||||||
case .t2_9:
|
|
||||||
return 5.500 / 2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func installManipulationGestures(on entity: ModelEntity) {
|
private func installManipulationGestures(on entity: ModelEntity) {
|
||||||
guard let arView else { return }
|
guard let arView else { return }
|
||||||
let recognizers = arView.installGestures([.translation, .rotation, .scale], for: entity)
|
let gestures: ARView.EntityGestures = allowsTankResizing
|
||||||
|
? [.translation, .rotation, .scale]
|
||||||
|
: [.translation, .rotation]
|
||||||
|
let recognizers = arView.installGestures(gestures, for: entity)
|
||||||
for recognizer in recognizers {
|
for recognizer in recognizers {
|
||||||
recognizer.addTarget(self, action: #selector(handleManipulationChanged(_:)))
|
recognizer.addTarget(self, action: #selector(handleManipulationChanged(_:)))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,13 +9,13 @@ import RealityKit
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// Ein Sicherheitsabstand rund um den Tank: ein Kreis mit gegebenem Radius und Farbe,
|
/// Ein Sicherheitsabstand rund um den Tank: ein Kreis mit gegebenem Radius und Farbe,
|
||||||
/// dessen Mittelpunkt der Tank ist.
|
/// dessen Mittelpunkt über dem Domschacht liegt.
|
||||||
///
|
///
|
||||||
/// Modular gehalten: Ein weiterer Sicherheitsabstand wird schlicht durch Hinzufügen
|
/// Modular gehalten: Ein weiterer Sicherheitsabstand wird schlicht durch Hinzufügen
|
||||||
/// eines Eintrags zu ``SafetyZone/all`` ergänzt – die Darstellung folgt automatisch.
|
/// eines Eintrags zu ``SafetyZone/all`` ergänzt – die Darstellung folgt automatisch.
|
||||||
struct SafetyZone: Identifiable {
|
struct SafetyZone: Identifiable {
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
/// Radius des Kreises in Metern (Abstand vom Tank-Mittelpunkt).
|
/// Radius des Kreises in Metern (Abstand von der Domschachtmitte).
|
||||||
let radius: Float
|
let radius: Float
|
||||||
/// Farbe des Rings.
|
/// Farbe des Rings.
|
||||||
let color: UIColor
|
let color: UIColor
|
||||||
|
|||||||
@ -42,6 +42,12 @@ final class TankEntityFactory {
|
|||||||
self.aboveGroundLightMultiplier = aboveGroundLightMultiplier
|
self.aboveGroundLightMultiplier = aboveGroundLightMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Domschachtposition der tatsächlich geladenen Variante, gemessen ab dem
|
||||||
|
/// Ursprung des USDZ-Modells. Bei fehlendem Modell bleibt der Ersatzkörper zentriert.
|
||||||
|
func domeCenterX(for tank: Tank) -> Float {
|
||||||
|
catalog.resolvedDefinition(for: tank)?.domeCenterX ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
/// Lädt das zum Tank passende Modell asynchron vor (blockiert den Main-Actor nicht).
|
/// Lädt das zum Tank passende Modell asynchron vor (blockiert den Main-Actor nicht).
|
||||||
func preload(_ tank: Tank) {
|
func preload(_ tank: Tank) {
|
||||||
guard let name = catalog.resolvedDefinition(for: tank)?.modelResourceName else { return }
|
guard let name = catalog.resolvedDefinition(for: tank)?.modelResourceName else { return }
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Steuer-Panel für den ausgewählten Tank: Höhe (↑↓), Verschieben auf dem Boden
|
/// Steuer-Panel für den ausgewählten Tank: Höhe (↑↓), Verschieben auf dem Boden
|
||||||
/// (vor/zurück/links/rechts) und Drehen (⟲⟳). Erscheint von unten; Pinch bleibt möglich.
|
/// (vor/zurück/links/rechts) und Drehen (⟲⟳). Erscheint von unten.
|
||||||
/// Buttons unterstützen Gedrückt-halten für kontinuierliche Anpassung.
|
/// Buttons unterstützen Gedrückt-halten für kontinuierliche Anpassung.
|
||||||
struct TankManipulationPanel: View {
|
struct TankManipulationPanel: View {
|
||||||
let controller: ARSessionController
|
let controller: ARSessionController
|
||||||
@ -109,10 +109,12 @@ struct TankManipulationPanel: View {
|
|||||||
|
|
||||||
private var actionButtons: some View {
|
private var actionButtons: some View {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
|
if controller.allowsTankResizing {
|
||||||
Button("Skalierung zurücksetzen", systemImage: "arrow.counterclockwise") {
|
Button("Skalierung zurücksetzen", systemImage: "arrow.counterclockwise") {
|
||||||
controller.resetTankScale()
|
controller.resetTankScale()
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
|
|
||||||
Button("Neu platzieren", systemImage: "arrow.up.and.down.and.arrow.left.and.right") {
|
Button("Neu platzieren", systemImage: "arrow.up.and.down.and.arrow.left.and.right") {
|
||||||
controller.reposition()
|
controller.reposition()
|
||||||
|
|||||||
@ -10,10 +10,13 @@ import Testing
|
|||||||
|
|
||||||
struct GasTankARTests {
|
struct GasTankARTests {
|
||||||
|
|
||||||
@Test func example() async throws {
|
@Test func tankResizingIsOffByDefaultAndReleaseCannotEnableIt() {
|
||||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
#expect(TankResizePolicy.isEnabled(devFlag: false) == false)
|
||||||
// Swift Testing Documentation
|
#if DEBUG
|
||||||
// https://developer.apple.com/documentation/testing
|
#expect(TankResizePolicy.isEnabled(devFlag: true) == true)
|
||||||
|
#else
|
||||||
|
#expect(TankResizePolicy.isEnabled(devFlag: true) == false)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,11 +19,11 @@ struct TankCatalogTests {
|
|||||||
"sizeDisplayNames": { "t1_2": "1,2 t", "t2_9": "2,9 t" },
|
"sizeDisplayNames": { "t1_2": "1,2 t", "t2_9": "2,9 t" },
|
||||||
"colorDisplayNames": { "white": "Weiß", "green": "Hellgrün", "olive": "Olivgrün" },
|
"colorDisplayNames": { "white": "Weiß", "green": "Hellgrün", "olive": "Olivgrün" },
|
||||||
"tanks": [
|
"tanks": [
|
||||||
{ "size": "t1_2", "color": "white", "placement": "aboveGround", "modelPath": "DG-2700_wh.usdz" },
|
{ "size": "t1_2", "color": "white", "placement": "aboveGround", "modelPath": "DG-2700_wh.usdz", "domeCenterX": -0.426 },
|
||||||
{ "size": "t1_2", "color": "green", "placement": "aboveGround", "modelPath": "DG-2700_mi.usdz" },
|
{ "size": "t1_2", "color": "green", "placement": "aboveGround", "modelPath": "DG-2700_mi.usdz", "domeCenterX": -0.426 },
|
||||||
{ "size": "t1_2", "color": "olive", "placement": "belowGround", "modelPath": "DG-2700_ol.usdz" },
|
{ "size": "t1_2", "color": "olive", "placement": "belowGround", "modelPath": "DG-2700_ol.usdz", "domeCenterX": -0.498 },
|
||||||
{ "size": "t2_9", "color": "white", "placement": "aboveGround", "modelPath": "DG-6400_wh.usdz" },
|
{ "size": "t2_9", "color": "white", "placement": "aboveGround", "modelPath": "DG-6400_wh.usdz", "domeCenterX": -1.939 },
|
||||||
{ "size": "t2_9", "color": "olive", "placement": "belowGround", "modelPath": "DG-6400_ol.usdz" }
|
{ "size": "t2_9", "color": "olive", "placement": "belowGround", "modelPath": "DG-6400_ol.usdz", "domeCenterX": -2.018 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@ -43,6 +43,7 @@ struct TankCatalogTests {
|
|||||||
#expect(def.color == .white)
|
#expect(def.color == .white)
|
||||||
#expect(def.placement == .aboveGround)
|
#expect(def.placement == .aboveGround)
|
||||||
#expect(def.modelPath == "DG-2700_wh.usdz")
|
#expect(def.modelPath == "DG-2700_wh.usdz")
|
||||||
|
#expect(def.domeCenterX == -0.426)
|
||||||
#expect(def.modelResourceName == "DG-2700_wh")
|
#expect(def.modelResourceName == "DG-2700_wh")
|
||||||
#expect(def.modelFileExtension == "usdz")
|
#expect(def.modelFileExtension == "usdz")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,29 @@ struct TankModelSelectionTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func everyVariantHasItsMeasuredDomeCenter() {
|
||||||
|
let expected: [String: Float] = [
|
||||||
|
"DG-2700_wh.usdz": -0.426,
|
||||||
|
"DG-2700_mi.usdz": -0.426,
|
||||||
|
"DG-2700_ol.usdz": -0.498,
|
||||||
|
"DG-4850_wh.usdz": -1.317,
|
||||||
|
"DG-4850_mi.usdz": -1.317,
|
||||||
|
"DG-4850_ol.usdz": -1.395,
|
||||||
|
"DG-6400_wh.usdz": -1.939,
|
||||||
|
"DG-6400_mi.usdz": -1.939,
|
||||||
|
"DG-6400_ol.usdz": -2.018,
|
||||||
|
]
|
||||||
|
let definitions = Self.bundleCatalog().definitions
|
||||||
|
#expect(definitions.count == expected.count)
|
||||||
|
for definition in definitions {
|
||||||
|
let measured = expected[definition.modelPath]
|
||||||
|
#expect(measured != nil, "Kein Domschachtmaß für \(definition.modelPath)")
|
||||||
|
if let measured {
|
||||||
|
#expect(abs(definition.domeCenterX - measured) < 0.001)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Auswahl → richtige Datei
|
// MARK: - Auswahl → richtige Datei
|
||||||
|
|
||||||
@Test func aboveGroundColorsMapToExpectedFiles() {
|
@Test func aboveGroundColorsMapToExpectedFiles() {
|
||||||
|
|||||||
Reference in New Issue
Block a user