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:
Dennis Nemec
2026-09-26 19:12:39 +02:00
parent 992bb32025
commit b8536d4182
63 changed files with 2317 additions and 188 deletions

View File

@ -7,21 +7,33 @@
import SwiftUI
/// Wiederverwendbare Auswahl für die Tank-Eigenschaften (Größe, Farbe,
/// Aufstellungsart und halbtransparente Darstellung).
/// Wiederverwendbare Auswahl für die Tank-Eigenschaften (Größe, Farbe
/// und Aufstellungsart). Die verfügbaren Optionen
/// stammen dynamisch aus dem `TankCatalog`.
struct TankEditor: View {
@Binding var tank: Tank
@Environment(\.tankCatalog) private var catalog
/// Für die aktuelle Aufstellungsart verfügbare Größen.
private var availableSizes: [TankSize] {
catalog.sizes(for: tank.placement)
}
/// Für aktuelle Größe & Aufstellungsart verfügbare Farben.
private var availableColors: [TankColor] {
catalog.colors(for: tank.size, placement: tank.placement)
}
var body: some View {
Picker("Größe", selection: $tank.size) {
ForEach(TankSize.allCases) { size in
Text(size.displayName).tag(size)
ForEach(availableSizes) { size in
Text(catalog.displayName(for: size)).tag(size)
}
}
Picker("Farbe", selection: $tank.color) {
ForEach(TankColor.allCases) { color in
Text(color.displayName).tag(color)
ForEach(availableColors) { color in
Text(catalog.displayName(for: color)).tag(color)
}
}
@ -29,7 +41,7 @@ struct TankEditor: View {
Text("Aufstellungsart")
HStack(spacing: 12) {
ForEach(TankPlacement.allCases) { placement in
ForEach(catalog.placements) { placement in
PlacementOption(
placement: placement,
isSelected: tank.placement == placement
@ -41,14 +53,24 @@ struct TankEditor: View {
}
.padding(.vertical, 4)
Toggle("Halbtransparent (nur Deckel)", isOn: $tank.showLidOnly)
.disabled(tank.placement != .belowGround)
.onChange(of: tank.placement) { _, newPlacement in
// Option nur für unterirdische Tanks sinnvoll – sonst zurücksetzen.
if newPlacement != .belowGround {
tank.showLidOnly = false
}
}
// Auswahl bei jeder Änderung auf eine gültige Katalog-Variante bringen …
.onChange(of: tank.placement) { _, _ in normalizeSelection() }
.onChange(of: tank.size) { _, _ in normalizeSelection() }
// … und Altdaten beim Öffnen selbstheilen.
.onAppear { normalizeSelection() }
}
/// Sorgt dafür, dass die aktuelle Auswahl einer existierenden Definition
/// entspricht, und setzt die „nur Deckel"-Option außerhalb unterirdischer
/// Tanks zurück.
private func normalizeSelection() {
let normalized = catalog.normalized(tank)
if normalized != tank {
tank = normalized
}
if tank.placement != .belowGround && tank.showLidOnly {
tank.showLidOnly = false
}
}
}
@ -87,6 +109,7 @@ private struct PlacementOption: View {
lineWidth: isSelected ? 2 : 1
)
}
.contentShape(.rect(cornerRadius: 12))
}
.buttonStyle(.plain)
}