// // TankPreview.swift // GasTankAR // // Created by Dennis Nemec on 14.09.26. // import SwiftUI import SceneKit /// 3D-Vorschau des ausgewählten Tanks. Lädt das passende USDZ-Modell aus dem /// App-Bundle und stellt es interaktiv (drehbar) dar. Die Darstellung /// aktualisiert sich automatisch, sobald sich die Tank-Konfiguration ändert. struct TankPreview: View { let tank: Tank @Environment(\.tankCatalog) private var catalog /// Zum aktuellen Tank passende Katalog-Definition (mit `modelPath`). private var definition: TankDefinition? { catalog.resolvedDefinition(for: tank) } var body: some View { Group { if let definition { TankSceneView( resourceName: definition.modelResourceName, fileExtension: definition.modelFileExtension ) .overlay(alignment: .bottomTrailing) { Label("Zum Drehen ziehen", systemImage: "hand.draw") .font(.caption2) .foregroundStyle(.secondary) .padding(6) } } else { ContentUnavailableView( "Kein Modell verfügbar", systemImage: "cube.transparent" ) } } .frame(height: 220) .frame(maxWidth: .infinity) .background(Color(.secondarySystemGroupedBackground)) .clipShape(.rect(cornerRadius: 12)) } } /// SceneKit-Wrapper, der ein 3D-Modell aus dem Bundle anzeigt und bei /// Namensänderung neu lädt. private struct TankSceneView: UIViewRepresentable { let resourceName: String let fileExtension: String func makeCoordinator() -> Coordinator { Coordinator() } func makeUIView(context: Context) -> SCNView { let view = SCNView() view.autoenablesDefaultLighting = true view.allowsCameraControl = true view.backgroundColor = .clear view.antialiasingMode = .multisampling4X load(into: view, context: context) return view } func updateUIView(_ uiView: SCNView, context: Context) { guard context.coordinator.loadedResourceName != resourceName else { return } load(into: uiView, context: context) } /// Lädt das aktuelle Modell und merkt sich den geladenen Namen, damit /// unnötiges Neuladen vermieden wird. private func load(into view: SCNView, context: Context) { context.coordinator.loadedResourceName = resourceName view.scene = Self.scene(resourceName: resourceName, fileExtension: fileExtension) } /// Erzeugt die Szene für ein Modell im Bundle. Fällt auf eine leere Szene /// zurück, falls die Datei nicht gefunden wird. private static func scene(resourceName: String, fileExtension: String) -> SCNScene { guard let url = Bundle.main.url(forResource: resourceName, withExtension: fileExtension), let scene = try? SCNScene(url: url) else { return SCNScene() } return scene } /// Behält den zuletzt geladenen Ressourcennamen, um Neuladen zu vermeiden. final class Coordinator { var loadedResourceName: String? } } #Preview { @Previewable @State var tank = Tank() return Form { Section("Tankauswahl") { TankPreview(tank: tank) TankEditor(tank: $tank) } } }