// // 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 radialDirection = SIMD3(sin(angle), 0, cos(angle)) let tangentDirection = SIMD3(cos(angle), 0, -sin(angle)) let holder = Entity() holder.addChild(text) // Wie bei der Längenbeschriftung: in die Bodenebene drehen und tangential // zum Kreis ausrichten. Der Text steht radial außerhalb des Rings. let textClearance: Float = 0.015 let radialOffset = bounds.extents.y / 2 + textClearance holder.position = radialDirection * (zone.radius + radialOffset) holder.position.y = 0.01 holder.orientation = simd_quatf( from: SIMD3(1, 0, 0), to: tangentDirection ) * simd_quatf( angle: -.pi / 2, axis: tangentDirection ) 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] = [] 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..