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>
This commit is contained in:
106
GasTankAR/Views/TankPreview.swift
Normal file
106
GasTankAR/Views/TankPreview.swift
Normal file
@ -0,0 +1,106 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user