Add help view and refine AR tank placement
This commit is contained in:
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Float>(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<Float>(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<Float>(domeCenterX, 0, 0, 0)
|
||||
let transformedOffset = tankEntity.transformMatrix(relativeTo: tankAnchor) * localOffset
|
||||
let tankPosition = tankEntity.position(relativeTo: tankAnchor)
|
||||
let localDomeOffset = SIMD4<Float>(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(_:)))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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()
|
||||
|
||||
Reference in New Issue
Block a user