246 lines
7.6 KiB
Swift
246 lines
7.6 KiB
Swift
//
|
|
// 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.
|
|
/// 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) {
|
|
if controller.allowsTankResizing {
|
|
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<Content: View>(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<Void, Never>?
|
|
|
|
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()
|
|
}
|
|
}
|