Files
Gas-Tank-AR-App/GasTankAR/Views/TankAR/SafetyZone.swift
2026-09-02 20:54:36 +02:00

129 lines
5.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// SafetyZone.swift
// GasTankAR
//
// Created by Dennis Nemec on 27.08.26.
//
import RealityKit
import UIKit
/// Ein Sicherheitsabstand rund um den Tank: ein Kreis mit gegebenem Radius und Farbe,
/// dessen Mittelpunkt der Tank ist.
///
/// Modular gehalten: Ein weiterer Sicherheitsabstand wird schlicht durch Hinzufügen
/// eines Eintrags zu ``SafetyZone/all`` ergänzt – die Darstellung folgt automatisch.
struct SafetyZone: Identifiable {
let id = UUID()
/// Radius des Kreises in Metern (Abstand vom Tank-Mittelpunkt).
let radius: Float
/// Farbe des Rings.
let color: UIColor
/// Anzeigename, z. B. für Barrierefreiheit oder eine spätere Legende.
let label: String
/// Alle darzustellenden Sicherheitsabstände. Zum Ergänzen genügt ein weiterer Eintrag.
static let all: [SafetyZone] = [
SafetyZone(radius: 3, color: .systemRed, label: "3 m"),
SafetyZone(radius: 5, color: .systemOrange, label: "5 m"),
]
}
/// Baut die flachen, auf dem Boden liegenden Ringe für die Sicherheitsabstände.
@MainActor
enum SafetyZoneFactory {
/// Halbe Linienbreite des Rings in Metern (Ring = Radius ± Halbbreite).
private static let lineHalfWidth: Float = 0.03
/// Anzahl der Segmente pro Ring (höher = runder).
private static let segments = 96
/// Deckkraft der Ringe.
private static let opacity: Float = 0.85
/// Texthöhe der Meter-Beschriftung in Metern.
private static let labelHeight: Float = 0.25
/// Winkelpositionen (im Bogenmaß), an denen die Beschriftung auf dem Ring liegt.
/// Mehrere Positionen sorgen dafür, dass die Meterangabe aus jeder Richtung lesbar ist.
private static let labelAngles: [Float] = [0, .pi / 2, .pi, 3 * .pi / 2]
/// Erzeugt eine Entity mit je einem Ring pro Sicherheitsabstand, zentriert im Ursprung
/// und flach in der XZ-Ebene (y = 0) liegend.
static func makeEntity(for zones: [SafetyZone] = SafetyZone.all) -> Entity {
let root = Entity()
root.name = "SafetyZones"
for zone in zones {
root.addChild(makeRing(for: zone))
}
return root
}
private static func makeRing(for zone: SafetyZone) -> Entity {
let container = Entity()
container.name = "SafetyZone-\(zone.label)"
let mesh = annulusMesh(radius: zone.radius, halfWidth: lineHalfWidth, segments: segments)
var material = UnlitMaterial(color: zone.color.withAlphaComponent(CGFloat(opacity)))
material.blending = .transparent(opacity: .init(floatLiteral: opacity))
// Beidseitig sichtbar – unabhängig von der Blickrichtung auf den Boden.
material.faceCulling = .none
container.addChild(ModelEntity(mesh: mesh, materials: [material]))
for angle in labelAngles {
container.addChild(makeLabel(for: zone, at: angle))
}
return container
}
/// Erzeugt eine flach auf dem Boden liegende Meter-Beschriftung, tangential zum Ring
/// ausgerichtet (Oberkante zeigt zur Mitte, damit sie von außen aufrecht lesbar ist).
private static func makeLabel(for zone: SafetyZone, at angle: Float) -> Entity {
let mesh = MeshResource.generateText(
zone.label,
extrusionDepth: 0.002,
font: .systemFont(ofSize: CGFloat(labelHeight), weight: .bold)
)
var material = UnlitMaterial(color: zone.color)
material.faceCulling = .none
let text = ModelEntity(mesh: mesh, materials: [material])
// Auf den eigenen Ursprung zentrieren.
let bounds = text.visualBounds(relativeTo: nil)
text.position = -bounds.center
let holder = Entity()
holder.addChild(text)
holder.position = SIMD3(sin(angle) * zone.radius, 0.01, cos(angle) * zone.radius)
holder.orientation = simd_quatf(angle: angle, axis: SIMD3(0, 1, 0))
* simd_quatf(angle: -.pi / 2, axis: SIMD3(1, 0, 0))
return holder
}
/// Erzeugt einen flachen Kreisring (Annulus) in der XZ-Ebene.
private static func annulusMesh(radius: Float, halfWidth: Float, segments: Int) -> MeshResource {
var positions: [SIMD3<Float>] = []
var indices: [UInt32] = []
let inner = max(0, radius - halfWidth)
let outer = radius + halfWidth
for i in 0...segments {
let angle = Float(i) / Float(segments) * 2 * .pi
let cosA = cos(angle)
let sinA = sin(angle)
positions.append(SIMD3(inner * cosA, 0, inner * sinA))
positions.append(SIMD3(outer * cosA, 0, outer * sinA))
}
for i in 0..<segments {
let base = UInt32(i * 2)
indices.append(contentsOf: [base, base + 1, base + 2])
indices.append(contentsOf: [base + 2, base + 1, base + 3])
}
var descriptor = MeshDescriptor(name: "safetyRing")
descriptor.positions = MeshBuffers.Positions(positions)
descriptor.primitives = .triangles(indices)
return (try? MeshResource.generate(from: [descriptor])) ?? .generatePlane(width: 0, depth: 0)
}
}