// // 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)] ) } }