// // TankManipulationPanel.swift // GasTankAR // // Created by Dennis Nemec on 28.08.26. // import SwiftUI /// Steuer-Panel für den ausgewählten Tank: Höhe (↑↓), Verschieben auf dem Boden /// (vor/zurück/links/rechts) und Drehen (⟲⟳). Erscheint von unten; Pinch bleibt möglich. /// Buttons unterstützen Gedrückt-halten für kontinuierliche Anpassung. struct TankManipulationPanel: View { let controller: ARSessionController var body: some View { Group { if controller.isUndergroundTank { splitPanel } else { standardPanel } } .padding() .background(.ultraThinMaterial, in: .rect(cornerRadius: 16)) .fixedSize(horizontal: false, vertical: true) } private var standardPanel: some View { VStack(spacing: 14) { dismissHandle panelTitle HStack(alignment: .center, spacing: 24) { heightGroup moveGroup rotateGroup } actionButtons } } private var splitPanel: some View { VStack(spacing: 14) { dismissHandle HStack(alignment: .top, spacing: 0) { VStack(spacing: 24) { panelTitle HStack(alignment: .bottom, spacing: 24) { heightGroup moveGroup rotateGroup } } .frame(maxWidth: .infinity) .padding(.horizontal, 12) Divider() VStack(spacing: 8) { Toggle( "Grube anzeigen", isOn: Binding( get: { controller.showsPit }, set: { controller.setShowsPit($0) } ) ) .toggleStyle(.switch) transparencyControl .disabled(controller.showsPit) Spacer(minLength: 0) actionButtons .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxWidth: .infinity) .padding(.horizontal, 12) } } .padding(.bottom, 10) } private var dismissHandle: some View { Capsule() .fill(.secondary.opacity(0.7)) .frame(width: 36, height: 4) .padding(.vertical, 2) .contentShape(Rectangle()) .gesture( DragGesture(minimumDistance: 10) .onEnded { value in if value.translation.height > 60 { controller.deselectTank() } } ) .accessibilityLabel("Zum Schließen nach unten wischen") } private var panelTitle: some View { Text("Tank anpassen") .font(.headline) } private var actionButtons: some View { HStack(spacing: 10) { Button("Skalierung zurücksetzen", systemImage: "arrow.counterclockwise") { controller.resetTankScale() } .buttonStyle(.bordered) Button("Neu platzieren", systemImage: "arrow.up.and.down.and.arrow.left.and.right") { controller.reposition() } .buttonStyle(.bordered) } } private var transparencyControl: some View { VStack(alignment: .leading, spacing: 4) { Text("Transparenz unter Boden") .font(.caption) .foregroundStyle(.secondary) Slider( value: Binding( get: { controller.undergroundTransparency }, set: { controller.setUndergroundTransparency($0) } ), in: 0...1 ) HStack { Text("sichtbar") Spacer() Text("transparent") } .font(.caption2) .foregroundStyle(.secondary) } .frame(minWidth: 180) } // MARK: - Gruppen private var heightGroup: some View { controlGroup(title: "Höhe") { VStack(spacing: 8) { holdButton("chevron.up", label: "Höher") { controller.adjustTankHeight(up: true) } holdButton("chevron.down", label: "Tiefer") { controller.adjustTankHeight(up: false) } } } } private var moveGroup: some View { controlGroup(title: "Verschieben") { VStack(spacing: 8) { holdButton("arrow.up", label: "Vorwärts") { controller.moveTank(forward: true) } HStack(spacing: 8) { holdButton("arrow.left", label: "Nach links") { controller.moveTank(left: true) } holdButton("arrow.right", label: "Nach rechts") { controller.moveTank(left: false) } } holdButton("arrow.down", label: "Rückwärts") { controller.moveTank(forward: false) } } } } private var rotateGroup: some View { controlGroup(title: "Drehen") { HStack(spacing: 8) { holdButton("rotate.left", label: "Gegen den Uhrzeigersinn") { controller.rotateTank(clockwise: false) } holdButton("rotate.right", label: "Im Uhrzeigersinn") { controller.rotateTank(clockwise: true) } } } } private func controlGroup(title: String, @ViewBuilder content: () -> Content) -> some View { VStack(spacing: 10) { content() Text(title) .font(.caption) .foregroundStyle(.secondary) } } private func holdButton(_ systemImage: String, label: String, action: @escaping () -> Void) -> some View { HoldRepeatButton( systemImage: systemImage, accessibilityLabel: label, action: action, onRelease: { controller.commitTankEdit() } ) } } /// Runder Button, der bei Gedrückt-halten die Aktion wiederholt (Tastenwiederholung). /// Einzel-Tap = ein Schritt; Halten = fortlaufend. Beim Loslassen wird `onRelease` aufgerufen. private struct HoldRepeatButton: View { let systemImage: String let accessibilityLabel: String let action: () -> Void let onRelease: () -> Void @State private var repeatTask: Task? private static let initialDelay: Duration = .milliseconds(350) private static let repeatInterval: Duration = .milliseconds(60) var body: some View { Image(systemName: systemImage) .font(.title3) .frame(width: 54, height: 54) .background(.ultraThinMaterial, in: .circle) .contentShape(.circle) .gesture( DragGesture(minimumDistance: 0) .onChanged { _ in if repeatTask == nil { start() } } .onEnded { _ in stop() } ) .accessibilityLabel(accessibilityLabel) .accessibilityAddTraits(.isButton) } private func start() { action() // sofortiger erster Schritt (Einzel-Tap) repeatTask = Task { @MainActor in try? await Task.sleep(for: Self.initialDelay) while !Task.isCancelled { action() try? await Task.sleep(for: Self.repeatInterval) } } } private func stop() { repeatTask?.cancel() repeatTask = nil onRelease() } }