94 lines
3.7 KiB
Swift
94 lines
3.7 KiB
Swift
//
|
||
// 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)]
|
||
)
|
||
}
|
||
}
|