// // MeasurementEntityFactory.swift // GasTankAR // // Created by Dennis Nemec on 28.08.26. // import RealityKit import UIKit /// Baut die AR-Visualisierung einer Strecke: zwei Endpunkt-Kugeln, die verbindende Linie /// sowie ein Text-Label (Bezeichnung + Länge) am Mittelpunkt der Strecke. /// /// Enthält außerdem wiederverwendbare Bausteine für die *Live*-Vorschau während des Messens /// (Cursor-Kugel, skalierbare Linie, aktualisierbares Label). @MainActor enum MeasurementEntityFactory { /// Name des Label-Halters – wird zum Wiederfinden fürs Billboarding genutzt. static let labelName = "MeasurementLabel" private static let color = UIColor.systemYellow private static let pointRadius: Float = 0.012 private static let lineThickness: Float = 0.006 private static let labelHeight: Float = 0.06 private static let labelYOffset: Float = 0.03 /// Kleine Kugel für einen (auch vorläufigen) Messpunkt. static func makePoint(at position: SIMD3) -> ModelEntity { let sphere = ModelEntity( mesh: .generateSphere(radius: pointRadius), materials: [UnlitMaterial(color: color)] ) sphere.position = position return sphere } /// Vollständige Strecken-Visualisierung (Punkte + Linie + Label). static func makeMeasurement(_ measurement: Measurement) -> Entity { let container = Entity() container.name = "Measurement-\(measurement.id.uuidString)" container.addChild(makePoint(at: measurement.start)) container.addChild(makePoint(at: measurement.end)) let line = makeLineBase() configureLine(line, from: measurement.start, to: measurement.end) container.addChild(line) let label = makeLabelHolder() label.position = labelPosition(from: measurement.start, to: measurement.end) label.addChild(makeText("\(measurement.name) (\(measurement.formattedDistance))")) container.addChild(label) return container } /// Basis-Linie der Länge 1 m (entlang +Z). Für die Live-Linie wird sie via ``configureLine`` skaliert/rotiert. static func makeLineBase() -> ModelEntity { ModelEntity( mesh: .generateBox(size: SIMD3(lineThickness, lineThickness, 1)), materials: [UnlitMaterial(color: color)] ) } /// Richtet eine per ``makeLineBase`` erzeugte Linie zwischen zwei Punkten aus (Länge über Z-Skalierung). static func configureLine(_ line: Entity, from start: SIMD3, to end: SIMD3) { let length = max(simd_distance(start, end), 0.0001) line.transform = Transform( scale: SIMD3(1, 1, length), rotation: simd_quatf(from: SIMD3(0, 0, 1), to: normalize(end - start)), translation: (start + end) / 2 ) } /// Leerer Label-Halter (Billboard-Anker). Der Text wird als zentriertes Kind eingehängt. static func makeLabelHolder() -> Entity { let holder = Entity() holder.name = labelName return holder } /// Zentriertes Text-Entity für einen Label-Halter. static func makeText(_ string: String) -> ModelEntity { let mesh = MeshResource.generateText( string, extrusionDepth: 0.001, font: .systemFont(ofSize: CGFloat(labelHeight), weight: .semibold) ) var material = UnlitMaterial(color: .white) material.faceCulling = .none let text = ModelEntity(mesh: mesh, materials: [material]) text.position = -text.visualBounds(relativeTo: nil).center return text } /// Position des Labels (Streckenmitte, leicht angehoben). static func labelPosition(from start: SIMD3, to end: SIMD3) -> SIMD3 { (start + end) / 2 + SIMD3(0, labelYOffset, 0) } }