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

94 lines
3.7 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.

//
// TankEntityFactory.swift
// GasTankAR
//
// Created by Dennis Nemec on 26.08.26.
//
import RealityKit
import UIKit
/// Baut die darzustellende Tank-Entity (aus `GasTank.usdz`): lädt/cached das Modell,
/// richtet es je nach Aufstellungsart aus, erzeugt die Kollisionsform und – bei
/// unterirdischer Aufstellung – den Höhen-Transparenz-Effekt.
@MainActor
final class TankEntityFactory {
/// Name des USDZ-Assets im Bundle.
private static let modelName = "GasTank"
/// Anteil der Modellhöhe, der bei unterirdischer Aufstellung oben herausragt („Kopf").
private static let belowGroundExposedFraction: Float = 0.25
/// Zusätzlicher Abstand zwischen Tank-Umkreis und Grubenwand (in Metern) – großzügig,
/// damit die Grube nicht gequetscht wirkt.
private static let pitMargin: Float = 0.4
/// Zusätzliche Tiefe der Grubensohle unter der Tankunterkante (in Metern).
private static let pitFloorClearance: Float = 0.1
/// Vorab geladenes Modell; wird pro Platzierung geklont.
private var loadedModel: Entity?
/// Lädt das USDZ asynchron vor (blockiert den Main-Actor nicht).
func preload() {
guard loadedModel == nil else { return }
Task { @MainActor in
self.loadedModel = try? await Entity(named: Self.modelName)
}
}
/// Erzeugt den manipulierbaren Container (mit Kollision fürs Gesten-Hit-Testing).
func makeContainer(for tank: Tank) -> ModelEntity {
let model = loadModelClone()
let bounds = model.visualBounds(relativeTo: nil)
let container = ModelEntity()
switch tank.placement {
case .aboveGround:
model.position.y -= bounds.min.y // Unterkante auf die Fläche
case .belowGround:
// Modell absenken, sodass nur der obere Teil („Kopf") herausragt …
let exposed = bounds.extents.y * Self.belowGroundExposedFraction
let buried = bounds.extents.y - exposed
model.position.y -= (bounds.max.y - exposed)
// … und den Boden „aufreißen": eine runde Grube ausheben, in welcher der Tank steht.
// Radius = Umkreis der Grundfläche (halbe Diagonale) + großzügiger Rand.
let footprintRadius = 0.5 * (bounds.extents.x * bounds.extents.x + bounds.extents.z * bounds.extents.z).squareRoot()
container.addChild(TankPitFactory.makePit(
radius: footprintRadius + Self.pitMargin,
depth: buried + Self.pitFloorClearance,
center: bounds.center
))
}
container.addChild(model)
let colliderBounds = model.visualBounds(relativeTo: container)
container.collision = CollisionComponent(shapes: [
ShapeResource
.generateBox(size: colliderBounds.extents)
.offsetBy(translation: colliderBounds.center)
])
// Auswahl-Markierung (Eck-Winkel) – zunächst versteckt, wird bei Auswahl eingeblendet.
let highlight = TankHighlightFactory.makeHighlight(for: colliderBounds)
highlight.isEnabled = false
container.addChild(highlight)
return container
}
/// Liefert einen Klon des vorab geladenen Modells. Fällt auf synchrones Laden
/// bzw. – wenn das USDZ fehlt – auf einen einfachen Cube zurück.
private func loadModelClone() -> Entity {
if loadedModel == nil {
loadedModel = try? Entity.load(named: Self.modelName)
}
if let loadedModel {
return loadedModel.clone(recursive: true)
}
return ModelEntity(
mesh: .generateBox(size: 0.3),
materials: [SimpleMaterial(color: .white, isMetallic: false)]
)
}
}