Files
Gas-Tank-AR-App/GasTankAR/Views/TankPreview.swift
Dennis Nemec b8536d4182 Add tank catalog with USDZ models and tank selection
Introduce TankDefinition/TankCatalog loaded from Tanks.json, bundle the
Drachengas USDZ tank models, add tank selection and preview views, and
update the AR experience, editor and project views to use the catalog.
Includes tests for catalog loading and model selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:12:39 +02:00

107 lines
3.4 KiB
Swift

//
// 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)
}
}
}