Initial commit for first version of project
This commit is contained in:
128
GasTankAR/Views/TankAR/SafetyZone.swift
Normal file
128
GasTankAR/Views/TankAR/SafetyZone.swift
Normal file
@ -0,0 +1,128 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user