diff --git a/GasTankAR.xcodeproj/project.pbxproj b/GasTankAR.xcodeproj/project.pbxproj index d1ecd29..3d6b484 100644 --- a/GasTankAR.xcodeproj/project.pbxproj +++ b/GasTankAR.xcodeproj/project.pbxproj @@ -295,6 +295,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = GasTankAR/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = DrachenGas; 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_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_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 27.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -314,6 +315,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; 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_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; @@ -335,6 +340,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = GasTankAR/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = DrachenGas; 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_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_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 27.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -354,6 +360,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; 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_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/GasTankAR/Assets.xcassets/AppIcon.appiconset/Contents.json b/GasTankAR/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..8099174 100644 --- a/GasTankAR/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/GasTankAR/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "Vertrieb-AppIcon-1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -12,6 +13,7 @@ "value" : "dark" } ], + "filename" : "Vertrieb-AppIcon-Dark-1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -23,6 +25,7 @@ "value" : "tinted" } ], + "filename" : "Vertrieb-AppIcon-Tinted-1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/GasTankAR/Assets.xcassets/AppIcon.appiconset/Vertrieb-AppIcon-1024.png b/GasTankAR/Assets.xcassets/AppIcon.appiconset/Vertrieb-AppIcon-1024.png new file mode 100644 index 0000000..8cecee1 Binary files /dev/null and b/GasTankAR/Assets.xcassets/AppIcon.appiconset/Vertrieb-AppIcon-1024.png differ diff --git a/GasTankAR/Assets.xcassets/AppIcon.appiconset/Vertrieb-AppIcon-Dark-1024.png b/GasTankAR/Assets.xcassets/AppIcon.appiconset/Vertrieb-AppIcon-Dark-1024.png new file mode 100644 index 0000000..e62fc53 Binary files /dev/null and b/GasTankAR/Assets.xcassets/AppIcon.appiconset/Vertrieb-AppIcon-Dark-1024.png differ diff --git a/GasTankAR/Assets.xcassets/AppIcon.appiconset/Vertrieb-AppIcon-Tinted-1024.png b/GasTankAR/Assets.xcassets/AppIcon.appiconset/Vertrieb-AppIcon-Tinted-1024.png new file mode 100644 index 0000000..8a3f873 Binary files /dev/null and b/GasTankAR/Assets.xcassets/AppIcon.appiconset/Vertrieb-AppIcon-Tinted-1024.png differ diff --git a/GasTankAR/ContentView.swift b/GasTankAR/ContentView.swift index 5d22745..56ecf5b 100644 --- a/GasTankAR/ContentView.swift +++ b/GasTankAR/ContentView.swift @@ -15,7 +15,9 @@ struct ContentView: View { } } +#if DEBUG #Preview { ProjectListView() .modelContainer(PreviewSupport.container) } +#endif diff --git a/GasTankAR/Icon/Vertrieb-AppIcon-1024.png b/GasTankAR/Icon/Vertrieb-AppIcon-1024.png new file mode 100644 index 0000000..8cecee1 Binary files /dev/null and b/GasTankAR/Icon/Vertrieb-AppIcon-1024.png differ diff --git a/GasTankAR/Icon/Vertrieb-AppIcon-Dark-1024.png b/GasTankAR/Icon/Vertrieb-AppIcon-Dark-1024.png new file mode 100644 index 0000000..e62fc53 Binary files /dev/null and b/GasTankAR/Icon/Vertrieb-AppIcon-Dark-1024.png differ diff --git a/GasTankAR/Icon/Vertrieb-AppIcon-Tinted-1024.png b/GasTankAR/Icon/Vertrieb-AppIcon-Tinted-1024.png new file mode 100644 index 0000000..8a3f873 Binary files /dev/null and b/GasTankAR/Icon/Vertrieb-AppIcon-Tinted-1024.png differ diff --git a/GasTankAR/Model/TankDefinition.swift b/GasTankAR/Model/TankDefinition.swift index 4e21ae6..c8f7459 100644 --- a/GasTankAR/Model/TankDefinition.swift +++ b/GasTankAR/Model/TankDefinition.swift @@ -16,6 +16,10 @@ nonisolated struct TankDefinition: Codable, Hashable, Identifiable { let placement: TankPlacement /// Pfad bzw. Dateiname des 3D-Modells im App-Bundle (z. B. `DG-2700_wh.usdz`). 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. var id: String { "\(size.rawValue)_\(color.rawValue)_\(placement.rawValue)" } diff --git a/GasTankAR/TankModels/Tanks.json b/GasTankAR/TankModels/Tanks.json index d179d92..6c0675d 100644 --- a/GasTankAR/TankModels/Tanks.json +++ b/GasTankAR/TankModels/Tanks.json @@ -10,14 +10,14 @@ "olive": "Olivgrün" }, "tanks": [ - { "size": "t1_2", "color": "white", "placement": "aboveGround", "modelPath": "DG-2700_wh.usdz" }, - { "size": "t1_2", "color": "green", "placement": "aboveGround", "modelPath": "DG-2700_mi.usdz" }, - { "size": "t1_2", "color": "olive", "placement": "belowGround", "modelPath": "DG-2700_ol.usdz" }, - { "size": "t2_1", "color": "white", "placement": "aboveGround", "modelPath": "DG-4850_wh.usdz" }, - { "size": "t2_1", "color": "green", "placement": "aboveGround", "modelPath": "DG-4850_mi.usdz" }, - { "size": "t2_1", "color": "olive", "placement": "belowGround", "modelPath": "DG-4850_ol.usdz" }, - { "size": "t2_9", "color": "white", "placement": "aboveGround", "modelPath": "DG-6400_wh.usdz" }, - { "size": "t2_9", "color": "green", "placement": "aboveGround", "modelPath": "DG-6400_mi.usdz" }, - { "size": "t2_9", "color": "olive", "placement": "belowGround", "modelPath": "DG-6400_ol.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", "domeCenterX": -0.426 }, + { "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", "domeCenterX": -1.317 }, + { "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", "domeCenterX": -1.395 }, + { "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", "domeCenterX": -1.939 }, + { "size": "t2_9", "color": "olive", "placement": "belowGround", "modelPath": "DG-6400_ol.usdz", "domeCenterX": -2.018 } ] } diff --git a/GasTankAR/Views/Docuframe/DocuframeView.swift b/GasTankAR/Views/Docuframe/DocuframeView.swift index 388a29e..0f9baa4 100644 --- a/GasTankAR/Views/Docuframe/DocuframeView.swift +++ b/GasTankAR/Views/Docuframe/DocuframeView.swift @@ -117,6 +117,7 @@ struct DocuframeView: View { } } +#if DEBUG #Preview { let sample = PreviewSupport.galleryProjectContainer() return NavigationStack { @@ -124,3 +125,4 @@ struct DocuframeView: View { } .modelContainer(sample.container) } +#endif diff --git a/GasTankAR/Views/GalleryView.swift b/GasTankAR/Views/GalleryView.swift index 01452ac..4fc1939 100644 --- a/GasTankAR/Views/GalleryView.swift +++ b/GasTankAR/Views/GalleryView.swift @@ -607,6 +607,8 @@ private struct PhotoLocationCard: View { String(format: "%.5f, %.5f", coordinate.latitude, coordinate.longitude) } } + +#if DEBUG #Preview("Galerie") { let sample = PreviewSupport.galleryProjectContainer() return NavigationStack { @@ -619,3 +621,4 @@ private struct PhotoLocationCard: View { let photo = PreviewSupport.samplePhoto() PhotoViewer(photo: photo, photos: [photo]) {} } +#endif diff --git a/GasTankAR/Views/HelpView.swift b/GasTankAR/Views/HelpView.swift new file mode 100644 index 0000000..abe478b --- /dev/null +++ b/GasTankAR/Views/HelpView.swift @@ -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() + } +} diff --git a/GasTankAR/Views/ProjectDetailView.swift b/GasTankAR/Views/ProjectDetailView.swift index 71d3ec6..b03ab4e 100644 --- a/GasTankAR/Views/ProjectDetailView.swift +++ b/GasTankAR/Views/ProjectDetailView.swift @@ -58,6 +58,7 @@ struct ProjectDetailView: View { } } +#if DEBUG #Preview { let sample = PreviewSupport.singleProjectContainer() @@ -66,3 +67,4 @@ struct ProjectDetailView: View { } .modelContainer(sample.container) } +#endif diff --git a/GasTankAR/Views/ProjectListView.swift b/GasTankAR/Views/ProjectListView.swift index a5c2dd4..0b4abfd 100644 --- a/GasTankAR/Views/ProjectListView.swift +++ b/GasTankAR/Views/ProjectListView.swift @@ -90,6 +90,7 @@ struct ProjectListView: View { } } +#if DEBUG #Preview("Gefüllt") { ProjectListView() .modelContainer(PreviewSupport.container) @@ -99,3 +100,4 @@ struct ProjectListView: View { ProjectListView() .modelContainer(PreviewSupport.makeEmptyContainer()) } +#endif diff --git a/GasTankAR/Views/SettingsView.swift b/GasTankAR/Views/SettingsView.swift index 22b1a9a..c6cb9b3 100644 --- a/GasTankAR/Views/SettingsView.swift +++ b/GasTankAR/Views/SettingsView.swift @@ -8,15 +8,30 @@ import SwiftUI import ARKit -/// Zentraler Schlüssel für die LiDAR-Einstellung (in AppStorage/UserDefaults). +/// Zentrale Schlüssel für AppStorage/UserDefaults. enum SettingsKeys { static let lidarFeaturesEnabled = "lidarFeaturesEnabled" 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 { @AppStorage(SettingsKeys.lidarFeaturesEnabled) private var lidarEnabled = false @AppStorage(SettingsKeys.arExposure) private var arExposure = 1.0 +#if DEBUG + @AppStorage(SettingsKeys.devTankResizingEnabled) private var devTankResizingEnabled = false +#endif @Environment(\.dismiss) private var dismiss /// LiDAR ist verfügbar, wenn das Gerät Scene-Reconstruction (Mesh) unterstützt. @@ -56,6 +71,22 @@ struct SettingsView: View { } footer: { 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") .navigationBarTitleDisplayMode(.inline) diff --git a/GasTankAR/Views/TankAR/ARExperienceView.swift b/GasTankAR/Views/TankAR/ARExperienceView.swift index 7cfc5ff..422ccc5 100644 --- a/GasTankAR/Views/TankAR/ARExperienceView.swift +++ b/GasTankAR/Views/TankAR/ARExperienceView.swift @@ -15,6 +15,9 @@ struct ARExperienceView: View { @Environment(\.dismiss) private var dismiss @AppStorage(SettingsKeys.lidarFeaturesEnabled) private var lidarEnabled = false @AppStorage(SettingsKeys.arExposure) private var arExposure = 1.0 +#if DEBUG + @AppStorage(SettingsKeys.devTankResizingEnabled) private var devTankResizingEnabled = false +#endif let project: Project @@ -95,10 +98,16 @@ struct ARExperienceView: View { .task { guard controller == nil else { return } let persistence = ProjectPersistenceAdapter(project: project, modelContext: modelContext) +#if DEBUG + let devResizing = devTankResizingEnabled +#else + let devResizing = false +#endif controller = ARSessionController( persistence: persistence, useLiDAR: lidarEnabled, - arExposure: arExposure + arExposure: arExposure, + devTankResizingEnabled: devResizing ) } } diff --git a/GasTankAR/Views/TankAR/ARSessionController.swift b/GasTankAR/Views/TankAR/ARSessionController.swift index 325e816..ecaf076 100644 --- a/GasTankAR/Views/TankAR/ARSessionController.swift +++ b/GasTankAR/Views/TankAR/ARSessionController.swift @@ -26,6 +26,8 @@ final class ARSessionController: NSObject, ARSessionDelegate, ARCoachingOverlayV } /// Ob die Sicherheitsabstände (Ringe um den Tank) eingeblendet sind. 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. private(set) var snapshotFlashCounter = 0 /// 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 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.useLiDAR = useLiDAR + self.allowsTankResizing = TankResizePolicy.isEnabled(devFlag: devTankResizingEnabled) self.showsPit = persistence.tank.placement == .belowGround && !persistence.tank.showLidOnly self.entityFactory = TankEntityFactory(aboveGroundLightMultiplier: Float(arExposure)) self.configFactory = SessionConfigurationFactory( @@ -255,7 +263,7 @@ final class ARSessionController: NSObject, ARSessionDelegate, ARCoachingOverlayV /// Setzt die Tank-Skalierung auf die Originalgröße zurück. func resetTankScale() { - guard let tankEntity else { return } + guard allowsTankResizing, let tankEntity else { return } tankEntity.scale = SIMD3(repeating: 1) updateSafetyZones() updateTankDimensionValues() @@ -967,6 +975,10 @@ final class ARSessionController: NSObject, ARSessionDelegate, ARCoachingOverlayV } container.transform = Transform(matrix: worldTransform) + // Ältere Projekte können eine gespeicherte Skalierung enthalten. + if !allowsTankResizing { + container.scale = SIMD3(repeating: 1) + } updateDepthFadeGroundY() updateSceneOcclusion() updateSafetyZones() @@ -992,30 +1004,21 @@ final class ARSessionController: NSObject, ARSessionDelegate, ARCoachingOverlayV 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(domeCenterX, 0, 0, 0) + let transformedOffset = tankEntity.transformMatrix(relativeTo: tankAnchor) * localOffset let tankPosition = tankEntity.position(relativeTo: tankAnchor) - let localDomeOffset = SIMD4(0, 0, safetyZoneDomeOffset, 0) - 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 - } + entity.position = tankPosition + SIMD3(transformedOffset.x, 0, transformedOffset.z) } private func installManipulationGestures(on entity: ModelEntity) { 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 { recognizer.addTarget(self, action: #selector(handleManipulationChanged(_:))) } diff --git a/GasTankAR/Views/TankAR/SafetyZone.swift b/GasTankAR/Views/TankAR/SafetyZone.swift index 3811eb1..50be4ae 100644 --- a/GasTankAR/Views/TankAR/SafetyZone.swift +++ b/GasTankAR/Views/TankAR/SafetyZone.swift @@ -9,13 +9,13 @@ import RealityKit import UIKit /// 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 /// eines Eintrags zu ``SafetyZone/all`` ergänzt – die Darstellung folgt automatisch. struct SafetyZone: Identifiable { let id = UUID() - /// Radius des Kreises in Metern (Abstand vom Tank-Mittelpunkt). + /// Radius des Kreises in Metern (Abstand von der Domschachtmitte). let radius: Float /// Farbe des Rings. let color: UIColor diff --git a/GasTankAR/Views/TankAR/TankEntityFactory.swift b/GasTankAR/Views/TankAR/TankEntityFactory.swift index dcfc626..fc8f2d7 100644 --- a/GasTankAR/Views/TankAR/TankEntityFactory.swift +++ b/GasTankAR/Views/TankAR/TankEntityFactory.swift @@ -42,6 +42,12 @@ final class TankEntityFactory { 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). func preload(_ tank: Tank) { guard let name = catalog.resolvedDefinition(for: tank)?.modelResourceName else { return } diff --git a/GasTankAR/Views/TankAR/TankManipulationPanel.swift b/GasTankAR/Views/TankAR/TankManipulationPanel.swift index cb7d0c9..e96b6d8 100644 --- a/GasTankAR/Views/TankAR/TankManipulationPanel.swift +++ b/GasTankAR/Views/TankAR/TankManipulationPanel.swift @@ -8,7 +8,7 @@ import SwiftUI /// 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. struct TankManipulationPanel: View { let controller: ARSessionController @@ -109,10 +109,12 @@ struct TankManipulationPanel: View { private var actionButtons: some View { HStack(spacing: 10) { - Button("Skalierung zurücksetzen", systemImage: "arrow.counterclockwise") { - controller.resetTankScale() + if controller.allowsTankResizing { + Button("Skalierung zurücksetzen", systemImage: "arrow.counterclockwise") { + controller.resetTankScale() + } + .buttonStyle(.bordered) } - .buttonStyle(.bordered) Button("Neu platzieren", systemImage: "arrow.up.and.down.and.arrow.left.and.right") { controller.reposition() diff --git a/GasTankARTests/GasTankARTests.swift b/GasTankARTests/GasTankARTests.swift index 060600c..bfa5c0b 100644 --- a/GasTankARTests/GasTankARTests.swift +++ b/GasTankARTests/GasTankARTests.swift @@ -10,10 +10,13 @@ import Testing struct GasTankARTests { - @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. - // Swift Testing Documentation - // https://developer.apple.com/documentation/testing + @Test func tankResizingIsOffByDefaultAndReleaseCannotEnableIt() { + #expect(TankResizePolicy.isEnabled(devFlag: false) == false) +#if DEBUG + #expect(TankResizePolicy.isEnabled(devFlag: true) == true) +#else + #expect(TankResizePolicy.isEnabled(devFlag: true) == false) +#endif } } diff --git a/GasTankARTests/TankCatalogTests.swift b/GasTankARTests/TankCatalogTests.swift index 13511a9..608056b 100644 --- a/GasTankARTests/TankCatalogTests.swift +++ b/GasTankARTests/TankCatalogTests.swift @@ -19,11 +19,11 @@ struct TankCatalogTests { "sizeDisplayNames": { "t1_2": "1,2 t", "t2_9": "2,9 t" }, "colorDisplayNames": { "white": "Weiß", "green": "Hellgrün", "olive": "Olivgrün" }, "tanks": [ - { "size": "t1_2", "color": "white", "placement": "aboveGround", "modelPath": "DG-2700_wh.usdz" }, - { "size": "t1_2", "color": "green", "placement": "aboveGround", "modelPath": "DG-2700_mi.usdz" }, - { "size": "t1_2", "color": "olive", "placement": "belowGround", "modelPath": "DG-2700_ol.usdz" }, - { "size": "t2_9", "color": "white", "placement": "aboveGround", "modelPath": "DG-6400_wh.usdz" }, - { "size": "t2_9", "color": "olive", "placement": "belowGround", "modelPath": "DG-6400_ol.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", "domeCenterX": -0.426 }, + { "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", "domeCenterX": -1.939 }, + { "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.placement == .aboveGround) #expect(def.modelPath == "DG-2700_wh.usdz") + #expect(def.domeCenterX == -0.426) #expect(def.modelResourceName == "DG-2700_wh") #expect(def.modelFileExtension == "usdz") } diff --git a/GasTankARTests/TankModelSelectionTests.swift b/GasTankARTests/TankModelSelectionTests.swift index dbb1fa7..412fae7 100644 --- a/GasTankARTests/TankModelSelectionTests.swift +++ b/GasTankARTests/TankModelSelectionTests.swift @@ -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 @Test func aboveGroundColorsMapToExpectedFiles() {