99 lines
3.8 KiB
Swift
99 lines
3.8 KiB
Swift
//
|
||
// 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<Float>) -> 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<Float>, to end: SIMD3<Float>) {
|
||
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<Float>, to end: SIMD3<Float>) -> SIMD3<Float> {
|
||
(start + end) / 2 + SIMD3(0, labelYOffset, 0)
|
||
}
|
||
}
|