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>
126 lines
3.9 KiB
Swift
126 lines
3.9 KiB
Swift
//
|
|
// TankEditor.swift
|
|
// GasTankAR
|
|
//
|
|
// Created by Dennis Nemec on 25.08.26.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// 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(availableSizes) { size in
|
|
Text(catalog.displayName(for: size)).tag(size)
|
|
}
|
|
}
|
|
|
|
Picker("Farbe", selection: $tank.color) {
|
|
ForEach(availableColors) { color in
|
|
Text(catalog.displayName(for: color)).tag(color)
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Aufstellungsart")
|
|
|
|
HStack(spacing: 12) {
|
|
ForEach(catalog.placements) { placement in
|
|
PlacementOption(
|
|
placement: placement,
|
|
isSelected: tank.placement == placement
|
|
) {
|
|
tank.placement = placement
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Auswahlkachel für eine Aufstellungsart mit Icon und umrahmter Auswahl.
|
|
private struct PlacementOption: View {
|
|
let placement: TankPlacement
|
|
let isSelected: Bool
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
VStack(spacing: 8) {
|
|
Image(placement.imageName)
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.foregroundStyle(Color.accentColor)
|
|
.frame(height: 72)
|
|
.scaleEffect(1.25)
|
|
.padding(8)
|
|
|
|
Text(placement.displayName)
|
|
.font(.caption)
|
|
.foregroundStyle(isSelected ? .primary : .secondary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(8)
|
|
.background(
|
|
(isSelected ? Color.accentColor.opacity(0.12) : Color.clear),
|
|
in: .rect(cornerRadius: 12)
|
|
)
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(
|
|
isSelected ? Color.accentColor : Color.secondary.opacity(0.4),
|
|
lineWidth: isSelected ? 2 : 1
|
|
)
|
|
}
|
|
.contentShape(.rect(cornerRadius: 12))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
@Previewable @State var tank = Tank()
|
|
return Form {
|
|
Section("Tankauswahl") {
|
|
TankEditor(tank: $tank)
|
|
}
|
|
}
|
|
}
|